]*>([\s\S]*?)<\/div>/ );
+ if ( wpDieMatch ) {
+ const textContent = wpDieMatch[ 1 ]
+ .replace( /<[^>]+>/g, ' ' )
+ .replace( /\s+/g, ' ' )
+ .trim();
+ if ( textContent ) {
+ return `WordPress error: ${ textContent }`;
+ }
+ }
+
+ return 'PHP error during startup';
+}
+
+function generateErrorPageHtml( errorMessage: string ): string {
+ const escaped = errorMessage
+ .replace( /&/g, '&' )
+ .replace( //g, '>' );
+ return `
PHP Error
+
+
PHP Error Detected
${ escaped }
+
Studio is watching for file changes. Fix the PHP error and the site will automatically restart.
`;
+}
+
+/**
+ * Start error recovery for a site with a PHP error.
+ * Serves an error page on the site's port and watches for PHP file changes.
+ * On file change, retries starting the site.
+ */
+export async function startErrorRecovery(
+ siteServer: SiteServer,
+ errorMessage: string,
+ readPm2Logs: ( siteId: string ) => { stdout?: string[]; stderr?: string[] }
+): Promise< void > {
+ const { id, port, path } = siteServer.details;
+
+ stopErrorRecovery( id );
+
+ const errorServer = http.createServer( ( _req, res ) => {
+ res.writeHead( 500, { 'Content-Type': 'text/html; charset=utf-8' } );
+ res.end( generateErrorPageHtml( errorMessage ) );
+ } );
+
+ await new Promise< void >( ( resolve, reject ) => {
+ errorServer.on( 'error', reject );
+ errorServer.listen( port, () => resolve() );
+ } );
+
+ let retrying = false;
+ const watcher = fs.watch( path, { recursive: true }, ( _event, filename ) => {
+ if ( ! filename?.endsWith( '.php' ) || retrying ) {
+ return;
+ }
+
+ const recovery = activeRecoveries.get( id );
+ if ( recovery?.debounceTimer ) {
+ clearTimeout( recovery.debounceTimer );
+ }
+
+ if ( recovery ) {
+ recovery.debounceTimer = setTimeout( () => {
+ void ( async () => {
+ retrying = true;
+ console.log( `[PHP Recovery - ${ id }] PHP file change detected, retrying...` );
+ try {
+ errorServer.close();
+ await siteServer.start();
+
+ // Success - clean up recovery
+ stopErrorRecovery( id );
+ console.log( `[PHP Recovery - ${ id }] Site recovered successfully` );
+ } catch ( retryError ) {
+ // Still failing - restart error server with updated error
+ console.log( `[PHP Recovery - ${ id }] Retry failed, still watching...` );
+ const pm2Logs = readPm2Logs( id );
+ const logContent = [ ...( pm2Logs.stdout ?? [] ), ...( pm2Logs.stderr ?? [] ) ].join(
+ '\n'
+ );
+ const newErrorMessage = parsePhpError( logContent );
+ const newHtml = generateErrorPageHtml( newErrorMessage );
+
+ const newErrorServer = http.createServer( ( _req, res ) => {
+ res.writeHead( 500, { 'Content-Type': 'text/html; charset=utf-8' } );
+ res.end( newHtml );
+ } );
+ await new Promise< void >( ( resolve ) => {
+ newErrorServer.listen( port, () => resolve() );
+ } );
+
+ const currentRecovery = activeRecoveries.get( id );
+ if ( currentRecovery ) {
+ currentRecovery.errorServer = newErrorServer;
+ }
+ } finally {
+ retrying = false;
+ }
+ } )();
+ }, 500 );
+ }
+ } );
+
+ activeRecoveries.set( id, { errorServer, watcher } );
+
+ // Mark site as running so the UI shows it
+ const url = `http://localhost:${ port }`;
+ siteServer.details = {
+ ...siteServer.details,
+ running: true,
+ url,
+ };
+ siteServer.server.url = url;
+}
+
+/**
+ * Stop error recovery for a site (clean up error server and file watcher).
+ */
+export function stopErrorRecovery( siteId: string ): void {
+ const recovery = activeRecoveries.get( siteId );
+ if ( ! recovery ) {
+ return;
+ }
+
+ if ( recovery.debounceTimer ) {
+ clearTimeout( recovery.debounceTimer );
+ }
+ recovery.watcher.close();
+ recovery.errorServer.close();
+ activeRecoveries.delete( siteId );
+}
+
+/**
+ * Check if a site is currently in error recovery mode.
+ */
+export function isInErrorRecovery( siteId: string ): boolean {
+ return activeRecoveries.has( siteId );
+}