From 2743dc84f6c3635ff20626d8b29d187dd9c9313e Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Thu, 18 Dec 2025 10:31:46 +0100 Subject: [PATCH 01/20] Abort async operations on SIGTERM/SIGINT --- cli/commands/site/list.ts | 8 ------ cli/lib/pm2-manager.ts | 3 ++- cli/lib/wordpress-server-manager.ts | 39 ++++++++++++++++++++++++----- 3 files changed, 35 insertions(+), 15 deletions(-) diff --git a/cli/commands/site/list.ts b/cli/commands/site/list.ts index 0ce79da6a..e4db8b48a 100644 --- a/cli/commands/site/list.ts +++ b/cli/commands/site/list.ts @@ -71,14 +71,6 @@ function displaySiteList( sitesData: SiteListEntry[], format: 'table' | 'json' ) const logger = new Logger< LoggerAction >(); export async function runCommand( format: 'table' | 'json', watch: boolean ): Promise< void > { - const handleTermination = () => { - disconnect(); - process.exit( 0 ); - }; - process.on( 'SIGTERM', handleTermination ); - process.on( 'SIGHUP', handleTermination ); - process.on( 'disconnect', handleTermination ); - try { logger.reportStart( LoggerAction.LOAD_SITES, __( 'Loading sites…' ) ); const appdata = await readAppdata(); diff --git a/cli/lib/pm2-manager.ts b/cli/lib/pm2-manager.ts index fda5b4157..9a9184603 100644 --- a/cli/lib/pm2-manager.ts +++ b/cli/lib/pm2-manager.ts @@ -60,7 +60,8 @@ export function disconnect(): void { } } -process.on( 'exit', disconnect ); +process.on( 'disconnect', disconnect ); +process.on( 'SIGHUP', disconnect ); process.on( 'SIGINT', disconnect ); process.on( 'SIGTERM', disconnect ); diff --git a/cli/lib/wordpress-server-manager.ts b/cli/lib/wordpress-server-manager.ts index 544ce17ac..10f93c5d8 100644 --- a/cli/lib/wordpress-server-manager.ts +++ b/cli/lib/wordpress-server-manager.ts @@ -31,6 +31,15 @@ import { Logger } from 'cli/logger'; const SITE_PROCESS_PREFIX = 'studio-site-'; +// Get an abort signal that's triggered on SIGINT/SIGTERM. This is useful for aborting and cleaning +// up async operations. +const abortController = new AbortController(); +function handleProcessTermination() { + abortController.abort(); +} +process.on( 'SIGINT', handleProcessTermination ); +process.on( 'SIGTERM', handleProcessTermination ); + function getProcessName( siteId: string ): string { return `${ SITE_PROCESS_PREFIX }${ siteId }`; } @@ -124,27 +133,37 @@ export async function startWordPressServer( async function waitForReadyMessage( pmId: number ): Promise< void > { const bus = await getPm2Bus(); + let timeoutId: NodeJS.Timeout; + let readyHandler: ( packet: unknown ) => void; - return new Promise( ( resolve, reject ) => { - const timeout = setTimeout( () => { - bus.off( 'process:msg', readyHandler ); + return new Promise< void >( ( resolve, reject ) => { + timeoutId = setTimeout( () => { reject( new Error( 'Timeout waiting for ready message from WordPress server child' ) ); }, PLAYGROUND_CLI_INACTIVITY_TIMEOUT ); - const readyHandler = ( packet: unknown ) => { + readyHandler = ( packet: unknown ) => { const result = childMessagePm2Schema.safeParse( packet ); if ( ! result.success ) { return; } if ( result.data.process.pm_id === pmId && result.data.raw.topic === 'ready' ) { - clearTimeout( timeout ); - bus.off( 'process:msg', readyHandler ); resolve(); } }; + abortController.signal.addEventListener( + 'abort', + () => { + reject( new Error( 'Operation aborted' ) ); + }, + { once: true } + ); + bus.on( 'process:msg', readyHandler ); + } ).finally( () => { + clearTimeout( timeoutId ); + bus.off( 'process:msg', readyHandler ); } ); } @@ -242,6 +261,14 @@ async function sendMessage( } }; + abortController.signal.addEventListener( + 'abort', + () => { + reject( new Error( 'Operation aborted' ) ); + }, + { once: true } + ); + bus.on( 'process:msg', responseHandler ); sendMessageToProcess( pmId, { ...message, messageId } ).catch( reject ); From d34ee1e97254b801111bfe0326d18132bacd1420 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Thu, 18 Dec 2025 10:46:05 +0100 Subject: [PATCH 02/20] Also send abort message to child server --- cli/lib/types/wordpress-server-ipc.ts | 6 ++ cli/lib/wordpress-server-manager.ts | 1 + cli/wordpress-server-child.ts | 85 +++++++++++++++++++-------- 3 files changed, 67 insertions(+), 25 deletions(-) diff --git a/cli/lib/types/wordpress-server-ipc.ts b/cli/lib/types/wordpress-server-ipc.ts index 3c4c0bda8..7276e4282 100644 --- a/cli/lib/types/wordpress-server-ipc.ts +++ b/cli/lib/types/wordpress-server-ipc.ts @@ -23,6 +23,10 @@ const serverConfig = z.object( { export type ServerConfig = z.infer< typeof serverConfig >; +const managerMessageAbort = z.object( { + topic: z.literal( 'abort' ), +} ); + const managerMessageStartServer = z.object( { topic: z.literal( 'start-server' ), data: z.object( { @@ -49,6 +53,7 @@ const managerMessageWpCliCommand = z.object( { } ); const _managerMessagePayloadSchema = z.discriminatedUnion( 'topic', [ + managerMessageAbort, managerMessageStartServer, managerMessageRunBlueprint, managerMessageStopServer, @@ -58,6 +63,7 @@ export type ManagerMessagePayload = z.infer< typeof _managerMessagePayloadSchema const managerMessageBase = z.object( { messageId: z.number() } ); export const managerMessageSchema = z.discriminatedUnion( 'topic', [ + managerMessageBase.merge( managerMessageAbort ), managerMessageBase.merge( managerMessageStartServer ), managerMessageBase.merge( managerMessageRunBlueprint ), managerMessageBase.merge( managerMessageStopServer ), diff --git a/cli/lib/wordpress-server-manager.ts b/cli/lib/wordpress-server-manager.ts index 10f93c5d8..6f1312759 100644 --- a/cli/lib/wordpress-server-manager.ts +++ b/cli/lib/wordpress-server-manager.ts @@ -264,6 +264,7 @@ async function sendMessage( abortController.signal.addEventListener( 'abort', () => { + void sendMessageToProcess( pmId, { messageId, topic: 'abort' } ); reject( new Error( 'Operation aborted' ) ); }, { once: true } diff --git a/cli/wordpress-server-child.ts b/cli/wordpress-server-child.ts index efbdda133..e42efad2f 100644 --- a/cli/wordpress-server-child.ts +++ b/cli/wordpress-server-child.ts @@ -224,30 +224,40 @@ function wrapWithStartingPromise< Args extends unknown[], Return extends void >( }; } -const startServer = wrapWithStartingPromise( async ( config: ServerConfig ): Promise< void > => { - if ( server ) { - logToConsole( `Server already running for site ${ config.siteId }` ); - return; - } - - try { - const args = await getBaseRunCLIArgs( 'server', config ); - lastCliArgs = sanitizeRunCLIArgs( args ); - server = await runCLI( args ); - - if ( config.enableMultiWorker && server ) { - logToConsole( `Server started with ${ server.workerThreadCount } worker thread(s)` ); +const startServer = wrapWithStartingPromise( + async ( config: ServerConfig, signal: AbortSignal ): Promise< void > => { + if ( server ) { + logToConsole( `Server already running for site ${ config.siteId }` ); + return; } - if ( config.adminPassword ) { - await setAdminPassword( server, config.adminPassword ); + try { + signal.addEventListener( + 'abort', + () => { + throw new Error( 'Operation aborted' ); + }, + { once: true } + ); + + const args = await getBaseRunCLIArgs( 'server', config ); + lastCliArgs = sanitizeRunCLIArgs( args ); + server = await runCLI( args ); + + if ( config.enableMultiWorker && server ) { + logToConsole( `Server started with ${ server.workerThreadCount } worker thread(s)` ); + } + + if ( config.adminPassword ) { + await setAdminPassword( server, config.adminPassword ); + } + } catch ( error ) { + server = null; + errorToConsole( `Failed to start server:`, error ); + throw error; } - } catch ( error ) { - server = null; - errorToConsole( `Failed to start server:`, error ); - throw error; } -} ); +); const STOP_SERVER_TIMEOUT = 5000; @@ -272,8 +282,16 @@ async function stopServer(): Promise< void > { } } -async function runBlueprint( config: ServerConfig ): Promise< void > { +async function runBlueprint( config: ServerConfig, signal: AbortSignal ): Promise< void > { try { + signal.addEventListener( + 'abort', + () => { + throw new Error( 'Operation aborted' ); + }, + { once: true } + ); + const args = await getBaseRunCLIArgs( 'run-blueprint', config ); lastCliArgs = sanitizeRunCLIArgs( args ); await runCLI( args ); @@ -286,7 +304,8 @@ async function runBlueprint( config: ServerConfig ): Promise< void > { } async function runWpCliCommand( - args: string[] + args: string[], + signal: AbortSignal ): Promise< { stdout: string; stderr: string; exitCode: number } > { await Promise.allSettled( [ startingPromise ] ); @@ -294,6 +313,14 @@ async function runWpCliCommand( throw new Error( `Failed to run WP CLI command because server is not running` ); } + signal.addEventListener( + 'abort', + () => { + throw new Error( 'Operation aborted' ); + }, + { once: true } + ); + const response = await server.playground.cli( [ 'php', '/tmp/wp-cli.phar', @@ -319,6 +346,8 @@ function sendErrorMessage( messageId: number, error: unknown ) { process.send!( errorResponse ); } +const abortControllers: Record< number, AbortController > = {}; + async function ipcMessageHandler( packet: unknown ) { const messageResult = managerMessageSchema.safeParse( packet ); @@ -334,22 +363,28 @@ async function ipcMessageHandler( packet: unknown ) { } const validMessage = messageResult.data; + abortControllers[ validMessage.messageId ] ??= new AbortController(); + const abortController = abortControllers[ validMessage.messageId ]; try { let result: unknown; switch ( validMessage.topic ) { + case 'abort': + abortController?.abort(); + delete abortControllers[ validMessage.messageId ]; + return; case 'start-server': - result = await startServer( validMessage.data.config ); + result = await startServer( validMessage.data.config, abortController.signal ); break; case 'run-blueprint': - result = await runBlueprint( validMessage.data.config ); + result = await runBlueprint( validMessage.data.config, abortController.signal ); break; case 'stop-server': result = await stopServer(); break; case 'wp-cli-command': - result = await runWpCliCommand( validMessage.data.args ); + result = await runWpCliCommand( validMessage.data.args, abortController.signal ); break; default: throw new Error( `Unknown message.` ); From 9a31e582ece629afea86e834dbaf7836168725a8 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Thu, 18 Dec 2025 10:57:01 +0100 Subject: [PATCH 03/20] Only create AbortController when topic is not `abort` --- cli/wordpress-server-child.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cli/wordpress-server-child.ts b/cli/wordpress-server-child.ts index e42efad2f..5456da94b 100644 --- a/cli/wordpress-server-child.ts +++ b/cli/wordpress-server-child.ts @@ -363,7 +363,9 @@ async function ipcMessageHandler( packet: unknown ) { } const validMessage = messageResult.data; - abortControllers[ validMessage.messageId ] ??= new AbortController(); + if ( validMessage.topic !== 'abort' ) { + abortControllers[ validMessage.messageId ] = new AbortController(); + } const abortController = abortControllers[ validMessage.messageId ]; try { From 61f3256555f690df26de11dc1ffda5237cba1daa Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Thu, 18 Dec 2025 12:39:38 +0100 Subject: [PATCH 04/20] Fix --- cli/lib/pm2-manager.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/cli/lib/pm2-manager.ts b/cli/lib/pm2-manager.ts index 9a9184603..37e34ab7d 100644 --- a/cli/lib/pm2-manager.ts +++ b/cli/lib/pm2-manager.ts @@ -60,8 +60,6 @@ export function disconnect(): void { } } -process.on( 'disconnect', disconnect ); -process.on( 'SIGHUP', disconnect ); process.on( 'SIGINT', disconnect ); process.on( 'SIGTERM', disconnect ); From f81ba0cf163013f4f35665cfb1c9db2f069b58ef Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Thu, 18 Dec 2025 15:15:18 +0100 Subject: [PATCH 05/20] More fixes --- cli/lib/pm2-manager.ts | 3 --- cli/lib/types/wordpress-server-ipc.ts | 2 ++ cli/lib/wordpress-server-manager.ts | 8 +++----- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/cli/lib/pm2-manager.ts b/cli/lib/pm2-manager.ts index 37e34ab7d..7d80f3d31 100644 --- a/cli/lib/pm2-manager.ts +++ b/cli/lib/pm2-manager.ts @@ -60,9 +60,6 @@ export function disconnect(): void { } } -process.on( 'SIGINT', disconnect ); -process.on( 'SIGTERM', disconnect ); - // Cache the return value of `pm2.list` for a very short time to make multiple calls in quick // succession more efficient const listProcesses = cacheFunctionTTL( () => { diff --git a/cli/lib/types/wordpress-server-ipc.ts b/cli/lib/types/wordpress-server-ipc.ts index 7276e4282..982ba83c5 100644 --- a/cli/lib/types/wordpress-server-ipc.ts +++ b/cli/lib/types/wordpress-server-ipc.ts @@ -25,6 +25,7 @@ export type ServerConfig = z.infer< typeof serverConfig >; const managerMessageAbort = z.object( { topic: z.literal( 'abort' ), + data: z.object( {} ), } ); const managerMessageStartServer = z.object( { @@ -43,6 +44,7 @@ const managerMessageRunBlueprint = z.object( { const managerMessageStopServer = z.object( { topic: z.literal( 'stop-server' ), + data: z.object( {} ), } ); const managerMessageWpCliCommand = z.object( { diff --git a/cli/lib/wordpress-server-manager.ts b/cli/lib/wordpress-server-manager.ts index 6f1312759..a04362643 100644 --- a/cli/lib/wordpress-server-manager.ts +++ b/cli/lib/wordpress-server-manager.ts @@ -264,7 +264,7 @@ async function sendMessage( abortController.signal.addEventListener( 'abort', () => { - void sendMessageToProcess( pmId, { messageId, topic: 'abort' } ); + void sendMessageToProcess( pmId, { messageId, topic: 'abort', data: {} } ); reject( new Error( 'Operation aborted' ) ); }, { once: true } @@ -294,10 +294,8 @@ export async function stopWordPressServer( siteId: string ): Promise< void > { try { await sendMessage( runningProcess.pmId, - { topic: 'stop-server' }, - { - maxTotalElapsedTime: GRACEFUL_STOP_TIMEOUT, - } + { topic: 'stop-server', data: {} }, + { maxTotalElapsedTime: GRACEFUL_STOP_TIMEOUT } ); } catch { // Graceful shutdown failed, PM2 delete will handle it From bd6b4d9f1b7c50b47beccae7fece96beb6e8904e Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Thu, 18 Dec 2025 15:24:14 +0100 Subject: [PATCH 06/20] Tweaks --- cli/lib/wordpress-server-manager.ts | 30 ++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/cli/lib/wordpress-server-manager.ts b/cli/lib/wordpress-server-manager.ts index a04362643..9c93a0a5c 100644 --- a/cli/lib/wordpress-server-manager.ts +++ b/cli/lib/wordpress-server-manager.ts @@ -133,8 +133,10 @@ export async function startWordPressServer( async function waitForReadyMessage( pmId: number ): Promise< void > { const bus = await getPm2Bus(); + let timeoutId: NodeJS.Timeout; let readyHandler: ( packet: unknown ) => void; + let abortListener: () => void; return new Promise< void >( ( resolve, reject ) => { timeoutId = setTimeout( () => { @@ -152,17 +154,15 @@ async function waitForReadyMessage( pmId: number ): Promise< void > { } }; - abortController.signal.addEventListener( - 'abort', - () => { - reject( new Error( 'Operation aborted' ) ); - }, - { once: true } - ); + abortListener = () => { + reject( new Error( 'Operation aborted' ) ); + }; + abortController.signal.addEventListener( 'abort', abortListener ); bus.on( 'process:msg', readyHandler ); } ).finally( () => { clearTimeout( timeoutId ); + abortController.signal.removeEventListener( 'abort', abortListener ); bus.off( 'process:msg', readyHandler ); } ); } @@ -195,7 +195,9 @@ async function sendMessage( const { maxTotalElapsedTime = PLAYGROUND_CLI_MAX_TIMEOUT, logger } = options; const bus = await getPm2Bus(); const messageId = nextMessageId++; + let responseHandler: ( packet: unknown ) => void; + let abortListener: () => void; return new Promise( ( resolve, reject ) => { const startTime = Date.now(); @@ -261,19 +263,17 @@ async function sendMessage( } }; - abortController.signal.addEventListener( - 'abort', - () => { - void sendMessageToProcess( pmId, { messageId, topic: 'abort', data: {} } ); - reject( new Error( 'Operation aborted' ) ); - }, - { once: true } - ); + abortListener = () => { + void sendMessageToProcess( pmId, { messageId, topic: 'abort', data: {} } ); + reject( new Error( 'Operation aborted' ) ); + }; + abortController.signal.addEventListener( 'abort', abortListener ); bus.on( 'process:msg', responseHandler ); sendMessageToProcess( pmId, { ...message, messageId } ).catch( reject ); } ).finally( () => { + abortController.signal.removeEventListener( 'abort', abortListener ); bus.off( 'process:msg', responseHandler ); const tracker = messageActivityTrackers.get( messageId ); From a6a7e7a6039c3d09cf6d1caa44397a54e3c85a3d Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Thu, 18 Dec 2025 15:59:27 +0100 Subject: [PATCH 07/20] Fix unit tests --- cli/lib/tests/pm2-manager.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cli/lib/tests/pm2-manager.test.ts b/cli/lib/tests/pm2-manager.test.ts index 0eff6dd49..a8d8d2b13 100644 --- a/cli/lib/tests/pm2-manager.test.ts +++ b/cli/lib/tests/pm2-manager.test.ts @@ -282,6 +282,7 @@ describe( 'PM2 Manager', () => { const message: ManagerMessage = { topic: 'stop-server', messageId: 1, + data: {}, }; await sendMessageToProcess( 42, message ); @@ -303,6 +304,7 @@ describe( 'PM2 Manager', () => { const message: ManagerMessage = { topic: 'stop-server', messageId: 1, + data: {}, }; await expect( sendMessageToProcess( 42, message ) ).rejects.toThrow( 'Send failed' ); From 569058fdb221640cc4d65059641023f5c2d7f319 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Fri, 19 Dec 2025 08:51:46 +0100 Subject: [PATCH 08/20] Fix types --- cli/wordpress-server-child.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/wordpress-server-child.ts b/cli/wordpress-server-child.ts index a1b19f492..5fd143ffe 100644 --- a/cli/wordpress-server-child.ts +++ b/cli/wordpress-server-child.ts @@ -350,7 +350,7 @@ function sendErrorMessage( messageId: string, error: unknown ) { process.send!( errorResponse ); } -const abortControllers: Record< number, AbortController > = {}; +const abortControllers: Record< string, AbortController > = {}; async function ipcMessageHandler( packet: unknown ) { const messageResult = managerMessageSchema.safeParse( packet ); From 8786163da64915463e270a6e2beba67ae097acc2 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Fri, 19 Dec 2025 10:01:43 +0100 Subject: [PATCH 09/20] Remove `this.sessionPath` files individually To help us diagnose which specific files or directories are causing trouble --- e2e/e2e-helpers.ts | 65 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 63 insertions(+), 2 deletions(-) diff --git a/e2e/e2e-helpers.ts b/e2e/e2e-helpers.ts index dcd4bbded..f166c3616 100644 --- a/e2e/e2e-helpers.ts +++ b/e2e/e2e-helpers.ts @@ -1,4 +1,5 @@ import { randomUUID } from 'crypto'; +import fsSync from 'fs'; import { tmpdir } from 'os'; import path from 'path'; import { findLatestBuild, parseElectronApp } from 'electron-playwright-helpers'; @@ -92,7 +93,67 @@ export class E2ESession { async cleanup() { await this.electronApp?.close(); - // Clean up temporary folder to hold application data - fs.rmSync( this.sessionPath, { recursive: true, force: true } ); + + // Attempt cleanup with retry logic to handle Windows file locking + let lastError: Error | null = null; + for ( let attempt = 0; attempt < 3; attempt++ ) { + try { + this.removeDirectoryRecursive( this.sessionPath ); + console.log( '[E2E Cleanup] Successfully cleaned up session directory' ); + return; + } catch ( error ) { + lastError = error as Error; + console.warn( + `[E2E Cleanup] Attempt ${ attempt + 1 } failed. Retrying in 1s...`, + lastError.message + ); + // Wait before retrying + await new Promise( ( resolve ) => setTimeout( resolve, 1000 ) ); + } + } + + // Log detailed error information for diagnostics + console.error( '[E2E Cleanup] Failed to clean up session after 3 attempts' ); + throw new Error( + `[E2E Cleanup] Failed to clean up session directory: ${ lastError?.message }` + ); + } + + private removeDirectoryRecursive( dirPath: string ): void { + if ( ! fsSync.existsSync( dirPath ) ) { + return; + } + + let items: string[]; + try { + items = fsSync.readdirSync( dirPath ); + } catch ( error ) { + console.error( `[E2E Cleanup] Failed to read directory ${ dirPath }:`, error ); + throw error; + } + + // Remove each item individually to isolate failures + for ( const item of items ) { + const itemPath = path.join( dirPath, item ); + try { + const stat = fsSync.lstatSync( itemPath ); // Use lstatSync to handle symlinks + if ( stat.isDirectory() ) { + this.removeDirectoryRecursive( itemPath ); + } else { + fsSync.unlinkSync( itemPath ); + } + } catch ( error ) { + console.error( `[E2E Cleanup] Failed to remove ${ itemPath }:`, error ); + throw error; + } + } + + // Remove the now-empty directory + try { + fsSync.rmdirSync( dirPath ); + } catch ( error ) { + console.error( `[E2E Cleanup] Failed to remove directory ${ dirPath }:`, error ); + throw error; + } } } From abbcfc88e8d795905ba8a1737407507b053250ff Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Fri, 19 Dec 2025 10:23:42 +0100 Subject: [PATCH 10/20] Stop running servers in a detached process --- src/modules/cli/lib/execute-command.ts | 14 ++++++++++---- src/site-server.ts | 1 + 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/modules/cli/lib/execute-command.ts b/src/modules/cli/lib/execute-command.ts index 0cdb0d01d..fa7bf4036 100644 --- a/src/modules/cli/lib/execute-command.ts +++ b/src/modules/cli/lib/execute-command.ts @@ -40,11 +40,12 @@ export interface ExecuteCliCommandOptions { * - 'capture': capture stdout/stderr, available in success/failure events */ output: 'ignore' | 'capture'; + detached?: boolean; } export function executeCliCommand( args: string[], - options: ExecuteCliCommandOptions = { output: 'ignore' } + options: ExecuteCliCommandOptions = { output: 'ignore', detached: false } ): [ CliCommandEventEmitter, ChildProcess ] { const cliPath = getCliPath(); @@ -58,6 +59,7 @@ export function executeCliCommand( // Using Electron's utilityProcess.fork API gave us issues with the child process never exiting const child = fork( cliPath, [ ...args, '--avoid-telemetry' ], { stdio, + detached: options.detached, env: { ...process.env, ELECTRON_RUN_AS_NODE: '1', @@ -107,9 +109,13 @@ export function executeCliCommand( } } ); - process.on( 'exit', () => { - child.kill(); - } ); + if ( options.detached ) { + child.unref(); + } else { + process.on( 'exit', () => { + child.kill(); + } ); + } return [ eventEmitter, child ]; } diff --git a/src/site-server.ts b/src/site-server.ts index 18795213d..63c69d9ed 100644 --- a/src/site-server.ts +++ b/src/site-server.ts @@ -43,6 +43,7 @@ export async function stopAllServersOnQuit() { return new Promise< void >( ( resolve ) => { const [ emitter ] = executeCliCommand( [ 'site', 'stop-all', '--auto-start' ], { output: 'ignore', + detached: true, } ); emitter.on( 'success', () => resolve() ); emitter.on( 'failure', () => resolve() ); From fa0537eddfcab2d77c0e5f26139532de16bed319 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Fri, 19 Dec 2025 10:38:15 +0100 Subject: [PATCH 11/20] Revert "Remove `this.sessionPath` files individually" This reverts commit 8786163da64915463e270a6e2beba67ae097acc2. --- e2e/e2e-helpers.ts | 65 ++-------------------------------------------- 1 file changed, 2 insertions(+), 63 deletions(-) diff --git a/e2e/e2e-helpers.ts b/e2e/e2e-helpers.ts index f166c3616..dcd4bbded 100644 --- a/e2e/e2e-helpers.ts +++ b/e2e/e2e-helpers.ts @@ -1,5 +1,4 @@ import { randomUUID } from 'crypto'; -import fsSync from 'fs'; import { tmpdir } from 'os'; import path from 'path'; import { findLatestBuild, parseElectronApp } from 'electron-playwright-helpers'; @@ -93,67 +92,7 @@ export class E2ESession { async cleanup() { await this.electronApp?.close(); - - // Attempt cleanup with retry logic to handle Windows file locking - let lastError: Error | null = null; - for ( let attempt = 0; attempt < 3; attempt++ ) { - try { - this.removeDirectoryRecursive( this.sessionPath ); - console.log( '[E2E Cleanup] Successfully cleaned up session directory' ); - return; - } catch ( error ) { - lastError = error as Error; - console.warn( - `[E2E Cleanup] Attempt ${ attempt + 1 } failed. Retrying in 1s...`, - lastError.message - ); - // Wait before retrying - await new Promise( ( resolve ) => setTimeout( resolve, 1000 ) ); - } - } - - // Log detailed error information for diagnostics - console.error( '[E2E Cleanup] Failed to clean up session after 3 attempts' ); - throw new Error( - `[E2E Cleanup] Failed to clean up session directory: ${ lastError?.message }` - ); - } - - private removeDirectoryRecursive( dirPath: string ): void { - if ( ! fsSync.existsSync( dirPath ) ) { - return; - } - - let items: string[]; - try { - items = fsSync.readdirSync( dirPath ); - } catch ( error ) { - console.error( `[E2E Cleanup] Failed to read directory ${ dirPath }:`, error ); - throw error; - } - - // Remove each item individually to isolate failures - for ( const item of items ) { - const itemPath = path.join( dirPath, item ); - try { - const stat = fsSync.lstatSync( itemPath ); // Use lstatSync to handle symlinks - if ( stat.isDirectory() ) { - this.removeDirectoryRecursive( itemPath ); - } else { - fsSync.unlinkSync( itemPath ); - } - } catch ( error ) { - console.error( `[E2E Cleanup] Failed to remove ${ itemPath }:`, error ); - throw error; - } - } - - // Remove the now-empty directory - try { - fsSync.rmdirSync( dirPath ); - } catch ( error ) { - console.error( `[E2E Cleanup] Failed to remove directory ${ dirPath }:`, error ); - throw error; - } + // Clean up temporary folder to hold application data + fs.rmSync( this.sessionPath, { recursive: true, force: true } ); } } From c984234b268ad0bd9358f4eabe15d83113ee6e88 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Fri, 19 Dec 2025 10:38:50 +0100 Subject: [PATCH 12/20] Retry --- e2e/e2e-helpers.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/e2e/e2e-helpers.ts b/e2e/e2e-helpers.ts index dcd4bbded..c538d033a 100644 --- a/e2e/e2e-helpers.ts +++ b/e2e/e2e-helpers.ts @@ -93,6 +93,11 @@ export class E2ESession { async cleanup() { await this.electronApp?.close(); // Clean up temporary folder to hold application data - fs.rmSync( this.sessionPath, { recursive: true, force: true } ); + fs.rmSync( this.sessionPath, { + recursive: true, + force: true, + maxRetries: 30, + retryDelay: 1000, + } ); } } From 87e62ec457dde92a237b49b70e305b5c7da01357 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Fri, 19 Dec 2025 11:20:39 +0100 Subject: [PATCH 13/20] Increase timeouts --- e2e/blueprints.test.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/e2e/blueprints.test.ts b/e2e/blueprints.test.ts index 221910e44..46d1cccf9 100644 --- a/e2e/blueprints.test.ts +++ b/e2e/blueprints.test.ts @@ -18,7 +18,7 @@ test.describe( 'Blueprints', () => { await onboarding.closeWhatsNew(); const siteContent = new SiteContent( session.mainWindow, DEFAULT_SITE_NAME ); - await expect( siteContent.siteNameHeading ).toBeVisible( { timeout: 120_000 } ); + await expect( siteContent.siteNameHeading ).toBeVisible( { timeout: 200_000 } ); } ); test.afterAll( async () => { @@ -49,7 +49,7 @@ test.describe( 'Blueprints', () => { // Wait for site to be created and running const siteContent = new SiteContent( session.mainWindow, siteName ); - await expect( siteContent.runningButton ).toBeAttached( { timeout: 120_000 } ); + await expect( siteContent.runningButton ).toBeAttached( { timeout: 300_000 } ); // Navigate to Settings tab to get admin URL const settingsTab = await siteContent.navigateToTab( 'Settings' ); @@ -85,7 +85,7 @@ test.describe( 'Blueprints', () => { // Wait for site to be created and running const siteContent = new SiteContent( session.mainWindow, siteName ); - await expect( siteContent.runningButton ).toBeAttached( { timeout: 120_000 } ); + await expect( siteContent.runningButton ).toBeAttached( { timeout: 300_000 } ); // Navigate to Settings tab to get admin URL const settingsTab = await siteContent.navigateToTab( 'Settings' ); @@ -123,7 +123,7 @@ test.describe( 'Blueprints', () => { // Wait for site to be created and running const siteContent = new SiteContent( session.mainWindow, siteName ); - await expect( siteContent.runningButton ).toBeAttached( { timeout: 120_000 } ); + await expect( siteContent.runningButton ).toBeAttached( { timeout: 300_000 } ); // Navigate to Settings tab to get admin URL const settingsTab = await siteContent.navigateToTab( 'Settings' ); @@ -159,7 +159,7 @@ test.describe( 'Blueprints', () => { // Wait for site to be created and running const siteContent = new SiteContent( session.mainWindow, siteName ); - await expect( siteContent.runningButton ).toBeAttached( { timeout: 120_000 } ); + await expect( siteContent.runningButton ).toBeAttached( { timeout: 300_000 } ); // Navigate to Settings tab to get admin URL const settingsTab = await siteContent.navigateToTab( 'Settings' ); @@ -197,7 +197,7 @@ test.describe( 'Blueprints', () => { // Wait for site to be created and running const siteContent = new SiteContent( session.mainWindow, siteName ); - await expect( siteContent.runningButton ).toBeAttached( { timeout: 120_000 } ); + await expect( siteContent.runningButton ).toBeAttached( { timeout: 300_000 } ); // Navigate to Settings tab to verify site is accessible const settingsTab = await siteContent.navigateToTab( 'Settings' ); @@ -236,7 +236,7 @@ test.describe( 'Blueprints', () => { // Wait for site to be created and running const siteContent = new SiteContent( session.mainWindow, siteName ); - await expect( siteContent.runningButton ).toBeAttached( { timeout: 120_000 } ); + await expect( siteContent.runningButton ).toBeAttached( { timeout: 300_000 } ); // Navigate to Settings tab to verify site is accessible const settingsTab = await siteContent.navigateToTab( 'Settings' ); From d42d25ebec6df5c31aa619dbe91e828bcfb5ab4b Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Fri, 19 Dec 2025 11:20:48 +0100 Subject: [PATCH 14/20] Fix deprecated blueprint syntax --- e2e/fixtures/blueprints/activate-plugin.json | 4 ++-- e2e/fixtures/blueprints/activate-theme.json | 4 ++-- e2e/fixtures/blueprints/install-plugin.json | 4 ++-- e2e/fixtures/blueprints/install-theme.json | 4 ++-- src/modules/cli/lib/execute-site-watch-command.ts | 3 --- 5 files changed, 8 insertions(+), 11 deletions(-) diff --git a/e2e/fixtures/blueprints/activate-plugin.json b/e2e/fixtures/blueprints/activate-plugin.json index 6b4f6b326..111bd4eef 100644 --- a/e2e/fixtures/blueprints/activate-plugin.json +++ b/e2e/fixtures/blueprints/activate-plugin.json @@ -4,7 +4,7 @@ "steps": [ { "step": "installPlugin", - "pluginZipFile": { + "pluginData": { "resource": "wordpress.org/plugins", "slug": "hello-dolly" } @@ -14,4 +14,4 @@ "pluginPath": "hello-dolly/hello.php" } ] -} \ No newline at end of file +} diff --git a/e2e/fixtures/blueprints/activate-theme.json b/e2e/fixtures/blueprints/activate-theme.json index fe60126e9..2600eab20 100644 --- a/e2e/fixtures/blueprints/activate-theme.json +++ b/e2e/fixtures/blueprints/activate-theme.json @@ -4,7 +4,7 @@ "steps": [ { "step": "installTheme", - "themeZipFile": { + "themeData": { "resource": "wordpress.org/themes", "slug": "twentytwentyone" } @@ -14,4 +14,4 @@ "themeFolderName": "twentytwentyone" } ] -} \ No newline at end of file +} diff --git a/e2e/fixtures/blueprints/install-plugin.json b/e2e/fixtures/blueprints/install-plugin.json index 76c840c7f..2dd6e971b 100644 --- a/e2e/fixtures/blueprints/install-plugin.json +++ b/e2e/fixtures/blueprints/install-plugin.json @@ -4,10 +4,10 @@ "steps": [ { "step": "installPlugin", - "pluginZipFile": { + "pluginData": { "resource": "wordpress.org/plugins", "slug": "akismet" } } ] -} \ No newline at end of file +} diff --git a/e2e/fixtures/blueprints/install-theme.json b/e2e/fixtures/blueprints/install-theme.json index 5bb349241..d718e55e3 100644 --- a/e2e/fixtures/blueprints/install-theme.json +++ b/e2e/fixtures/blueprints/install-theme.json @@ -4,10 +4,10 @@ "steps": [ { "step": "installTheme", - "themeZipFile": { + "themeData": { "resource": "wordpress.org/themes", "slug": "twentytwentytwo" } } ] -} \ No newline at end of file +} diff --git a/src/modules/cli/lib/execute-site-watch-command.ts b/src/modules/cli/lib/execute-site-watch-command.ts index 996643b62..ff69c7d7b 100644 --- a/src/modules/cli/lib/execute-site-watch-command.ts +++ b/src/modules/cli/lib/execute-site-watch-command.ts @@ -108,9 +108,6 @@ export function startSiteWatcher(): void { export function stopSiteWatcher(): void { if ( watcher ) { const [ , childProcess ] = watcher; - if ( childProcess.connected ) { - childProcess.disconnect(); - } childProcess.kill(); watcher = null; } From 10955cfebebd715c308f91b2226e3d5878a393ba Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Fri, 19 Dec 2025 14:01:12 +0100 Subject: [PATCH 15/20] Increase timeout --- e2e/localization.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/localization.test.ts b/e2e/localization.test.ts index cab3c30e6..572452977 100644 --- a/e2e/localization.test.ts +++ b/e2e/localization.test.ts @@ -139,7 +139,7 @@ test.describe( 'Localization', () => { // Wait for site to be created const siteContent = new SiteContent( session.mainWindow, siteName ); - await expect( siteContent.runningButton ).toBeAttached( { timeout: 120_000 } ); + await expect( siteContent.runningButton ).toBeAttached( { timeout: 200_000 } ); const settingsTabButton = session.mainWindow.getByRole( 'tab', { name: /Settings|設定/i } ); await settingsTabButton.click(); From fc4fa56d6ce60e91d99383f22726d0c10b165911 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Fri, 19 Dec 2025 14:34:21 +0100 Subject: [PATCH 16/20] Try adding a small delay --- e2e/e2e-helpers.ts | 8 ++++++-- src/modules/cli/lib/execute-site-watch-command.ts | 3 +++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/e2e/e2e-helpers.ts b/e2e/e2e-helpers.ts index c538d033a..cf02d280d 100644 --- a/e2e/e2e-helpers.ts +++ b/e2e/e2e-helpers.ts @@ -92,12 +92,16 @@ export class E2ESession { async cleanup() { await this.electronApp?.close(); + + // Give processes time to release file handles + await new Promise( ( resolve ) => setTimeout( resolve, 3000 ) ); + // Clean up temporary folder to hold application data fs.rmSync( this.sessionPath, { recursive: true, force: true, - maxRetries: 30, - retryDelay: 1000, + maxRetries: 5, + retryDelay: 500, } ); } } diff --git a/src/modules/cli/lib/execute-site-watch-command.ts b/src/modules/cli/lib/execute-site-watch-command.ts index ff69c7d7b..996643b62 100644 --- a/src/modules/cli/lib/execute-site-watch-command.ts +++ b/src/modules/cli/lib/execute-site-watch-command.ts @@ -108,6 +108,9 @@ export function startSiteWatcher(): void { export function stopSiteWatcher(): void { if ( watcher ) { const [ , childProcess ] = watcher; + if ( childProcess.connected ) { + childProcess.disconnect(); + } childProcess.kill(); watcher = null; } From 814d4ae613283adf8a2e775f7c4a52965ee8d321 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Fri, 19 Dec 2025 15:02:50 +0100 Subject: [PATCH 17/20] Kill `site list --watch` on SIGINT --- cli/commands/site/list.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cli/commands/site/list.ts b/cli/commands/site/list.ts index e4db8b48a..196c26c3d 100644 --- a/cli/commands/site/list.ts +++ b/cli/commands/site/list.ts @@ -126,6 +126,8 @@ export async function runCommand( format: 'table' | 'json', watch: boolean ): Pr }, { debounceMs: 500 } ); + + process.on( 'SIGINT', disconnect ); } } finally { if ( ! watch ) { From 96e03aa7ccb9ebae6591d4972c2762982f8e3f81 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Fri, 19 Dec 2025 15:09:53 +0100 Subject: [PATCH 18/20] Create main window after creating site watcher --- src/index.ts | 6 +- src/modules/cli/lib/execute-command.ts | 19 ++++--- .../cli/lib/execute-site-watch-command.ts | 57 +++++++++++-------- 3 files changed, 48 insertions(+), 34 deletions(-) diff --git a/src/index.ts b/src/index.ts index 09892facd..c05f073b3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -311,9 +311,11 @@ async function appBoot() { await renameLaunchUniquesStat(); - await createMainWindow(); await startUserDataWatcher(); - startSiteWatcher(); + + await startSiteWatcher(); + + await createMainWindow(); const userData = await loadUserData(); // Bump stats for the first time the app runs - this is when no lastBumpStats are available diff --git a/src/modules/cli/lib/execute-command.ts b/src/modules/cli/lib/execute-command.ts index fa7bf4036..4ca81b8cd 100644 --- a/src/modules/cli/lib/execute-command.ts +++ b/src/modules/cli/lib/execute-command.ts @@ -10,8 +10,9 @@ export interface CliCommandResult { } type CliCommandEventMap = { - data: { data: unknown }; + started: void; error: { error: Error }; + data: { data: unknown }; success: { result?: CliCommandResult }; failure: { result?: CliCommandResult }; }; @@ -67,6 +68,16 @@ export function executeCliCommand( } ); const eventEmitter = new CliCommandEventEmitter(); + child.on( 'spawn', () => { + eventEmitter.emit( 'started' ); + } ); + + child.on( 'error', ( error ) => { + console.error( 'Child process error:', error ); + Sentry.captureException( error ); + eventEmitter.emit( 'error', { error } ); + } ); + let stdout = ''; let stderr = ''; @@ -83,12 +94,6 @@ export function executeCliCommand( eventEmitter.emit( 'data', { data: message } ); } ); - child.on( 'error', ( error ) => { - console.error( 'Child process error:', error ); - Sentry.captureException( error ); - eventEmitter.emit( 'error', { error } ); - } ); - let capturedExitCode: number | null = null; child.on( 'exit', ( code ) => { diff --git a/src/modules/cli/lib/execute-site-watch-command.ts b/src/modules/cli/lib/execute-site-watch-command.ts index 996643b62..b99b2a475 100644 --- a/src/modules/cli/lib/execute-site-watch-command.ts +++ b/src/modules/cli/lib/execute-site-watch-command.ts @@ -71,37 +71,44 @@ async function updateSiteServerStatus( await current; } -export function startSiteWatcher(): void { - if ( watcher ) { - return; - } +export async function startSiteWatcher(): Promise< void > { + return new Promise( ( resolve, reject ) => { + if ( watcher ) { + return resolve(); + } - watcher = executeCliCommand( [ 'site', 'list', '--watch', '--format', 'json' ], { - output: 'ignore', - } ); - const [ eventEmitter ] = watcher; + watcher = executeCliCommand( [ 'site', 'list', '--watch', '--format', 'json' ], { + output: 'ignore', + } ); + const [ eventEmitter ] = watcher; - eventEmitter.on( 'data', ( { data } ) => { - const parsed = siteStatusEventSchema.safeParse( data ); - if ( ! parsed.success ) { - return; - } + eventEmitter.on( 'started', () => { + resolve(); + } ); - const { siteId, status, url } = parsed.data.value; - const isRunning = status === 'running'; + eventEmitter.on( 'error', ( { error } ) => { + reject(); + console.error( 'Site watcher error:', error ); + watcher = null; + } ); - void updateSiteServerStatus( siteId, isRunning, url ); - void sendIpcEventToRenderer( 'site-status-changed', parsed.data.value ); - } ); + eventEmitter.on( 'data', ( { data } ) => { + const parsed = siteStatusEventSchema.safeParse( data ); + if ( ! parsed.success ) { + return; + } - eventEmitter.on( 'error', ( { error } ) => { - console.error( 'Site watcher error:', error ); - watcher = null; - } ); + const { siteId, status, url } = parsed.data.value; + const isRunning = status === 'running'; - eventEmitter.on( 'failure', () => { - console.warn( 'Site watcher exited unexpectedly' ); - watcher = null; + void updateSiteServerStatus( siteId, isRunning, url ); + void sendIpcEventToRenderer( 'site-status-changed', parsed.data.value ); + } ); + + eventEmitter.on( 'failure', () => { + console.warn( 'Site watcher exited unexpectedly' ); + watcher = null; + } ); } ); } From ef932ca23922dc91f769dce9387e77c9c643b5a3 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Mon, 22 Dec 2025 09:25:34 +0100 Subject: [PATCH 19/20] Try using async fs method for cleanup --- e2e/e2e-helpers.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/e2e/e2e-helpers.ts b/e2e/e2e-helpers.ts index cf02d280d..684e9364f 100644 --- a/e2e/e2e-helpers.ts +++ b/e2e/e2e-helpers.ts @@ -93,11 +93,8 @@ export class E2ESession { async cleanup() { await this.electronApp?.close(); - // Give processes time to release file handles - await new Promise( ( resolve ) => setTimeout( resolve, 3000 ) ); - // Clean up temporary folder to hold application data - fs.rmSync( this.sessionPath, { + await fs.promises.rm( this.sessionPath, { recursive: true, force: true, maxRetries: 5, From 03545121f875ebfe2637d2723f36b923358486c3 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Mon, 22 Dec 2025 09:57:18 +0100 Subject: [PATCH 20/20] Try rimraf (which has advanced retry strategies) --- e2e/e2e-helpers.ts | 8 +- package-lock.json | 199 +++++++++++++++++++++++++++++++++++++++++++-- package.json | 1 + 3 files changed, 193 insertions(+), 15 deletions(-) diff --git a/e2e/e2e-helpers.ts b/e2e/e2e-helpers.ts index 684e9364f..4b7795bf3 100644 --- a/e2e/e2e-helpers.ts +++ b/e2e/e2e-helpers.ts @@ -4,6 +4,7 @@ import path from 'path'; import { findLatestBuild, parseElectronApp } from 'electron-playwright-helpers'; import fs from 'fs-extra'; import { _electron as electron, Page, ElectronApplication } from 'playwright'; +import { rimraf } from 'rimraf'; export class E2ESession { electronApp: ElectronApplication; @@ -94,11 +95,6 @@ export class E2ESession { await this.electronApp?.close(); // Clean up temporary folder to hold application data - await fs.promises.rm( this.sessionPath, { - recursive: true, - force: true, - maxRetries: 5, - retryDelay: 500, - } ); + await rimraf( this.sessionPath ); } } diff --git a/package-lock.json b/package-lock.json index 70146c823..2c2f048a1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -124,6 +124,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "resize-observer-polyfill": "^1.5.1", + "rimraf": "^6.1.2", "tailwindcss": "^3.3.6", "ts-jest": "^29.4.6", "typescript": "~5.9.3", @@ -5346,6 +5347,29 @@ } } }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -6427,6 +6451,23 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/@npmcli/move-file/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@octokit/app": { "version": "14.1.0", "resolved": "https://registry.npmjs.org/@octokit/app/-/app-14.1.0.tgz", @@ -13414,6 +13455,69 @@ "node": ">=10" } }, + "node_modules/cacache/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/cacache/node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/cacache/node_modules/tar": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", @@ -23602,10 +23706,11 @@ } }, "node_modules/package-json-from-dist": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", - "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", - "dev": true + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" }, "node_modules/pako": { "version": "1.0.11", @@ -25408,15 +25513,91 @@ "license": "MIT" }, "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.2.tgz", + "integrity": "sha512-cFCkPslJv7BAXJsYlK1dZsbP8/ZNLkCAQ0bi1hf5EKX2QHegmDFEFA6QhuYJlk7UDdc+02JjO80YSOrWPpw06g==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { - "glob": "^7.1.3" + "glob": "^13.0.0", + "package-json-from-dist": "^1.0.1" }, "bin": { - "rimraf": "bin.js" + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.0.tgz", + "integrity": "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "path-scurry": "^2.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/lru-cache": { + "version": "11.2.4", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", + "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", + "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/rimraf/node_modules/path-scurry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz", + "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" diff --git a/package.json b/package.json index 3a9a28cbd..53ea5969a 100644 --- a/package.json +++ b/package.json @@ -94,6 +94,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "resize-observer-polyfill": "^1.5.1", + "rimraf": "^6.1.2", "tailwindcss": "^3.3.6", "ts-jest": "^29.4.6", "typescript": "~5.9.3",