From 7459dd11c56f329b4eb31c6e55b8c16a506cc5c9 Mon Sep 17 00:00:00 2001 From: Enej Bajgoric Date: Sat, 14 Mar 2026 11:44:44 -0700 Subject: [PATCH 1/2] CLI: Add `jetpack playground` command for quick WordPress Playground sessions Adds a new `jetpack playground ` command that spins up a local WordPress Playground instance with the specified plugin mounted. This provides a fast, Docker-free alternative for quick plugin testing. The command: - Uses `jetpack rsync` to create a resolved copy (fixing monorepo symlinks) - Auto-discovers blueprints at `.wordpress-org/blueprints/blueprint.json` - Injects JETPACK_DEV_DEBUG for offline mode (no WP.com connection needed) - Auto-runs install/build if the plugin hasn't been built yet - Cleans up temp files on exit Co-Authored-By: Claude Opus 4.6 (1M context) --- tools/cli/cliRouter.js | 2 + tools/cli/commands/playground.js | 217 +++++++++++++++++++++++++++++++ 2 files changed, 219 insertions(+) create mode 100644 tools/cli/commands/playground.js diff --git a/tools/cli/cliRouter.js b/tools/cli/cliRouter.js index ed19849cb6ae..1c244b1a8c6a 100644 --- a/tools/cli/cliRouter.js +++ b/tools/cli/cliRouter.js @@ -13,6 +13,7 @@ import { generateDefine } from './commands/generate.js'; import * as installCommand from './commands/install.js'; import * as noopCommand from './commands/noop.js'; import * as phanCommand from './commands/phan.js'; +import { playgroundDefine } from './commands/playground.js'; import * as pnpmCommand from './commands/pnpm.js'; import { releaseDefine } from './commands/release.js'; import { rsyncDefine } from './commands/rsync.js'; @@ -49,6 +50,7 @@ export async function cli() { argv.command( installCommand ); argv.command( noopCommand ); argv.command( phanCommand ); + argv = playgroundDefine( argv ); argv.command( pnpmCommand ); argv = releaseDefine( argv ); argv = rsyncDefine( argv ); diff --git a/tools/cli/commands/playground.js b/tools/cli/commands/playground.js new file mode 100644 index 000000000000..a14580cc91d7 --- /dev/null +++ b/tools/cli/commands/playground.js @@ -0,0 +1,217 @@ +import child_process from 'child_process'; +import fs from 'fs'; +import os from 'os'; +import path from 'path'; +import chalk from 'chalk'; +import { dirs } from '../helpers/projectHelpers.js'; +import { chalkJetpackGreen } from '../helpers/styling.js'; + +/** + * Command definition for the playground subcommand. + * + * @param {object} yargs - The Yargs dependency. + * @return {object} Yargs with the playground commands defined. + */ +export function playgroundDefine( yargs ) { + yargs.command( + 'playground ', + 'Starts a WordPress Playground instance with a plugin mounted', + yarg => { + yarg + .positional( 'plugin', { + describe: 'Plugin name, e.g. jetpack', + type: 'string', + } ) + .option( 'blueprint', { + alias: 'b', + type: 'string', + description: 'Path to a custom blueprint.json file', + } ) + .option( 'port', { + alias: 'p', + type: 'number', + description: 'Port to run the Playground server on', + } ) + .example( 'jetpack playground jetpack', 'Start Playground with the Jetpack plugin' ) + .example( 'jetpack playground crm', 'Start Playground with the CRM plugin' ); + }, + async argv => { + await playgroundCli( argv ); + } + ); + + return yargs; +} + +/** + * Build a blueprint JSON file for the Playground session. + * + * Always injects a step to define JETPACK_DEV_DEBUG in wp-config.php so Jetpack + * runs in offline mode without needing a WordPress.com connection. If the plugin + * ships its own blueprint, the steps are merged. + * + * @param {string} pluginPath - Absolute path to the plugin source directory. + * @param {string} mountPath - Absolute path to the resolved plugin copy in the temp dir. + * @param {object} options - CLI options (may include a custom blueprint path). + * @return {string} Path to the generated blueprint file. + */ +function buildBlueprint( pluginPath, mountPath, options ) { + const tmpDir = path.dirname( mountPath ); + + // Start with a base blueprint. + let blueprint = { + $schema: 'https://playground.wordpress.net/blueprint-schema.json', + landingPage: '/wp-admin/', + login: true, + preferredVersions: { + php: '8.0', + wp: 'latest', + }, + features: { + networking: true, + }, + steps: [], + }; + + // Merge a user-provided or plugin-shipped blueprint. + let sourceBlueprint = null; + if ( options.blueprint ) { + sourceBlueprint = path.resolve( options.blueprint ); + } else { + const pluginBlueprintPath = path.join( + pluginPath, + '.wordpress-org', + 'blueprints', + 'blueprint.json' + ); + if ( fs.existsSync( pluginBlueprintPath ) ) { + sourceBlueprint = pluginBlueprintPath; + } + } + + if ( sourceBlueprint ) { + console.log( chalk.gray( `Using blueprint: ${ path.relative( '.', sourceBlueprint ) }` ) ); + const custom = JSON.parse( fs.readFileSync( sourceBlueprint, 'utf8' ) ); + blueprint = { + ...blueprint, + ...custom, + // Merge steps — custom steps run first, then ours. + steps: [ ...( custom.steps || [] ) ], + }; + } + + // Inject the offline mode step: define JETPACK_DEV_DEBUG in wp-config.php. + blueprint.steps.push( { + step: 'defineWpConfigConsts', + consts: { + JETPACK_DEV_DEBUG: true, + }, + } ); + + // Write the merged blueprint to the temp directory. + const blueprintOutPath = path.join( tmpDir, 'blueprint.json' ); + fs.writeFileSync( blueprintOutPath, JSON.stringify( blueprint, null, '\t' ) ); + + return blueprintOutPath; +} + +/** + * Entry point for the playground CLI. + * + * @param {object} options - The argv for the command line. + */ +async function playgroundCli( options ) { + const pluginPath = path.resolve( `projects/plugins/${ options.plugin }` ); + + // Validate that the plugin exists. + if ( ! fs.existsSync( pluginPath ) ) { + const available = dirs( './projects/plugins' ); + console.error( chalk.red( `Plugin "${ options.plugin }" not found.` ) ); + if ( available.length > 0 ) { + console.error( `\nAvailable plugins: ${ available.join( ', ' ) }` ); + } + process.exit( 1 ); + } + + // Check if the plugin has been built (vendor directories must exist). + const vendorPath = path.join( pluginPath, 'vendor' ); + const jetpackVendorPath = path.join( pluginPath, 'jetpack_vendor' ); + if ( ! fs.existsSync( vendorPath ) || ! fs.existsSync( jetpackVendorPath ) ) { + console.log( + chalk.yellow( + `Plugin "${ options.plugin }" has not been built yet. Running install and build...` + ) + ); + + const install = child_process.spawnSync( + 'jetpack', + [ 'install', `plugins/${ options.plugin }` ], + { shell: true, stdio: 'inherit' } + ); + if ( install.status !== 0 ) { + console.error( chalk.red( 'Install failed. Please run "jetpack install" manually.' ) ); + process.exit( 1 ); + } + + const build = child_process.spawnSync( 'jetpack', [ 'build', `plugins/${ options.plugin }` ], { + shell: true, + stdio: 'inherit', + } ); + if ( build.status !== 0 ) { + console.error( chalk.red( 'Build failed. Please run "jetpack build" manually.' ) ); + process.exit( 1 ); + } + } + + // Use `jetpack rsync` to create a resolved copy in a temp dir. + // This reuses the existing rsync logic which properly resolves monorepo + // symlinks and only includes production files. + const tmpDir = fs.mkdtempSync( + path.join( os.tmpdir(), `jetpack-playground-${ options.plugin }-` ) + ); + const mountPath = path.join( tmpDir, options.plugin ); + + console.log( chalk.gray( 'Syncing plugin files (resolving monorepo symlinks)...' ) ); + + const rsyncResult = child_process.spawnSync( + 'jetpack', + [ 'rsync', options.plugin, mountPath, '--non-interactive' ], + { shell: true, stdio: 'inherit' } + ); + if ( rsyncResult.status !== 0 ) { + console.error( chalk.red( 'Failed to sync plugin files.' ) ); + fs.rmSync( tmpDir, { recursive: true, force: true } ); + process.exit( 1 ); + } + + // Build the blueprint. We always inject a step to enable Jetpack offline mode + // (JETPACK_DEV_DEBUG) so the plugin works without a WordPress.com connection. + const blueprintPath = buildBlueprint( pluginPath, mountPath, options ); + + console.log( + chalkJetpackGreen( `Starting WordPress Playground with plugins/${ options.plugin }...` ) + ); + + const args = [ '@wp-playground/cli', 'server', '--auto-mount', `--blueprint=${ blueprintPath }` ]; + + if ( options.port ) { + args.push( `--port=${ options.port }` ); + } + + if ( options.verbose ) { + console.log( chalk.gray( `Running: npx ${ args.join( ' ' ) }` ) ); + console.log( chalk.gray( `Synced copy: ${ mountPath }` ) ); + } + + try { + child_process.spawnSync( 'npx', args, { + cwd: mountPath, + shell: true, + stdio: 'inherit', + } ); + } finally { + // Clean up the temporary copy. + console.log( chalk.gray( 'Cleaning up temporary files...' ) ); + fs.rmSync( tmpDir, { recursive: true, force: true } ); + } +} From f575a591bdf4576f666f23e0c59ccd62a31ffd85 Mon Sep 17 00:00:00 2001 From: Enej Bajgoric Date: Mon, 16 Mar 2026 14:44:50 -0700 Subject: [PATCH 2/2] CLI: Rewrite `jetpack playground` to address PR review feedback - Convert to newer module style (command/describe/builder/handler exports) - Use `projectDir()` and `maybePromptForPlugin()` from existing helpers - Remove `shell: true` from all subprocess calls - Remove hardcoded PHP 8.0, use Playground default - Fix blueprint step merging to actually merge both arrays - Read wp-plugin-slug from composer.json for correct mount paths - Mount plugin + packages dirs so vendor symlinks resolve correctly - Add mu-plugin to fix plugins_url() for symlink-resolved paths - Auto-detect and activate the plugin's main PHP file - Auto-build if plugin hasn't been built yet - Validate port range and blueprint file existence - Wrap temp dir in try/finally for cleanup on failure - Show admin URL, login credentials, and quit instructions after startup - Warn on first run if @wp-playground/cli needs downloading Co-Authored-By: Claude Opus 4.6 (1M context) --- tools/cli/cliRouter.js | 4 +- tools/cli/commands/playground.js | 352 +++++++++++++++++++------------ 2 files changed, 220 insertions(+), 136 deletions(-) diff --git a/tools/cli/cliRouter.js b/tools/cli/cliRouter.js index 1c244b1a8c6a..1ce398ed010a 100644 --- a/tools/cli/cliRouter.js +++ b/tools/cli/cliRouter.js @@ -13,7 +13,7 @@ import { generateDefine } from './commands/generate.js'; import * as installCommand from './commands/install.js'; import * as noopCommand from './commands/noop.js'; import * as phanCommand from './commands/phan.js'; -import { playgroundDefine } from './commands/playground.js'; +import * as playgroundCommand from './commands/playground.js'; import * as pnpmCommand from './commands/pnpm.js'; import { releaseDefine } from './commands/release.js'; import { rsyncDefine } from './commands/rsync.js'; @@ -50,7 +50,7 @@ export async function cli() { argv.command( installCommand ); argv.command( noopCommand ); argv.command( phanCommand ); - argv = playgroundDefine( argv ); + argv.command( playgroundCommand ); argv.command( pnpmCommand ); argv = releaseDefine( argv ); argv = rsyncDefine( argv ); diff --git a/tools/cli/commands/playground.js b/tools/cli/commands/playground.js index a14580cc91d7..bb0c5b1d0b93 100644 --- a/tools/cli/commands/playground.js +++ b/tools/cli/commands/playground.js @@ -2,45 +2,175 @@ import child_process from 'child_process'; import fs from 'fs'; import os from 'os'; import path from 'path'; +import { fileURLToPath } from 'url'; import chalk from 'chalk'; -import { dirs } from '../helpers/projectHelpers.js'; -import { chalkJetpackGreen } from '../helpers/styling.js'; +import { projectDir } from '../helpers/install.js'; +import { maybePromptForPlugin } from './rsync.js'; + +const cliPath = fileURLToPath( new URL( '../bin/jetpack.js', import.meta.url ) ); + +export const command = 'playground [plugin]'; +export const describe = 'Starts a WordPress Playground instance with a plugin mounted'; /** - * Command definition for the playground subcommand. + * Options definition for the playground subcommand. * * @param {object} yargs - The Yargs dependency. * @return {object} Yargs with the playground commands defined. */ -export function playgroundDefine( yargs ) { - yargs.command( - 'playground ', - 'Starts a WordPress Playground instance with a plugin mounted', - yarg => { - yarg - .positional( 'plugin', { - describe: 'Plugin name, e.g. jetpack', - type: 'string', - } ) - .option( 'blueprint', { - alias: 'b', - type: 'string', - description: 'Path to a custom blueprint.json file', - } ) - .option( 'port', { - alias: 'p', - type: 'number', - description: 'Port to run the Playground server on', - } ) - .example( 'jetpack playground jetpack', 'Start Playground with the Jetpack plugin' ) - .example( 'jetpack playground crm', 'Start Playground with the CRM plugin' ); - }, - async argv => { - await playgroundCli( argv ); +export function builder( yargs ) { + return yargs + .positional( 'plugin', { + describe: 'Plugin name, e.g. jetpack', + type: 'string', + } ) + .option( 'blueprint', { + alias: 'b', + type: 'string', + description: 'Path to a custom blueprint.json file', + } ) + .option( 'port', { + alias: 'p', + type: 'number', + description: 'Port to run the Playground server on', + } ) + .example( 'jetpack playground jetpack', 'Start Playground with the Jetpack plugin' ) + .example( 'jetpack playground crm', 'Start Playground with the CRM plugin' ); +} + +/** + * Entry point for the playground CLI. + * + * @param {object} argv - The argv for the command line. + */ +export async function handler( argv ) { + argv = await maybePromptForPlugin( argv ); + + // Validate port if provided. + if ( argv.port !== undefined ) { + if ( ! Number.isInteger( argv.port ) || argv.port < 1 || argv.port > 65535 ) { + console.error( chalk.red( 'Port must be an integer between 1 and 65535.' ) ); + process.exit( 1 ); } + } + + // Warn if @wp-playground/cli needs to be downloaded (first run). + const playgroundCheck = child_process.spawnSync( + 'npx', + [ '--no-install', '@wp-playground/cli', '--version' ], + { stdio: 'ignore' } ); + if ( playgroundCheck.status !== 0 ) { + console.log( + chalk.yellow( + '@wp-playground/cli is not yet cached. The first run will download it, which may take a moment.' + ) + ); + } + + const pluginPath = projectDir( `plugins/${ argv.plugin }` ); - return yargs; + // Read the plugin slug from composer.json so the mount path inside + // Playground uses the correct wp-plugin-slug (e.g. zero-bs-crm for crm). + const composerJsonPath = path.join( pluginPath, 'composer.json' ); + let wpPluginSlug = argv.plugin; + if ( fs.existsSync( composerJsonPath ) ) { + try { + const composerJson = JSON.parse( fs.readFileSync( composerJsonPath, 'utf8' ) ); + wpPluginSlug = + composerJson?.extra?.[ 'wp-plugin-slug' ] ?? + composerJson?.extra?.[ 'beta-plugin-slug' ] ?? + argv.plugin; + } catch { + // Fall back to directory name if composer.json is unreadable. + } + } + + // If the plugin hasn't been built, run the build first. + if ( + ! fs.existsSync( path.join( pluginPath, 'vendor' ) ) || + ! fs.existsSync( path.join( pluginPath, 'node_modules' ) ) + ) { + console.log( + chalk.yellow( `Plugin "${ argv.plugin }" does not appear to be built. Building...` ) + ); + const build = child_process.spawnSync( + process.execPath, + [ cliPath, 'build', `plugins/${ argv.plugin }` ], + { stdio: 'inherit' } + ); + if ( build.status !== 0 ) { + console.error( + chalk.red( 'Build failed. Please run "jetpack build" manually and try again.' ) + ); + process.exit( 1 ); + } + } + + // Build the blueprint into a temp directory. + const tmpDir = fs.mkdtempSync( path.join( os.tmpdir(), `jetpack-playground-${ argv.plugin }-` ) ); + + // The vendor/jetpack_vendor directories contain symlinks like + // ../../../../packages// which, from the plugin location at + // /wordpress/wp-content/plugins//jetpack_vendor/automattic/, + // resolve to /wordpress/wp-content/packages//. We mount + // projects/packages there so these symlinks resolve to real files and + // plugins_url() can generate correct URLs (paths stay under wp-content). + const packagesPath = projectDir( 'packages' ); + + try { + const blueprintPath = buildBlueprint( pluginPath, tmpDir, argv, wpPluginSlug ); + + const port = argv.port ?? 9400; + + const args = [ + '@wp-playground/cli', + 'server', + `--mount=${ pluginPath }:/wordpress/wp-content/plugins/${ wpPluginSlug }`, + `--mount=${ packagesPath }:/wordpress/wp-content/packages`, + `--blueprint=${ blueprintPath }`, + ]; + + if ( argv.port !== undefined ) { + args.push( `--port=${ argv.port }` ); + } + + if ( argv.verbose ) { + console.log( chalk.gray( `Running: npx ${ args.join( ' ' ) }` ) ); + } + + await new Promise( ( resolve, reject ) => { + const proc = child_process.spawn( 'npx', args, { + stdio: [ 'inherit', 'pipe', 'inherit' ], + } ); + + proc.stdout.on( 'data', data => { + process.stdout.write( data ); + + // After Playground prints its "Ready!" line, show helpful info. + if ( data.toString().includes( 'Ready!' ) ) { + console.log(); + console.log( ` Admin: ${ chalk.cyan( `http://127.0.0.1:${ port }/wp-admin/` ) }` ); + console.log( chalk.gray( ' Login: admin / password' ) ); + console.log( chalk.gray( ' Stop: Ctrl+C' ) ); + console.log(); + } + } ); + + proc.on( 'close', code => { + if ( code !== 0 && code !== null ) { + reject( new Error( `Playground exited with code ${ code }` ) ); + } else { + resolve(); + } + } ); + + proc.on( 'error', reject ); + } ); + } finally { + console.log( chalk.gray( 'Cleaning up temporary files...' ) ); + fs.rmSync( tmpDir, { recursive: true, force: true } ); + } } /** @@ -50,23 +180,18 @@ export function playgroundDefine( yargs ) { * runs in offline mode without needing a WordPress.com connection. If the plugin * ships its own blueprint, the steps are merged. * - * @param {string} pluginPath - Absolute path to the plugin source directory. - * @param {string} mountPath - Absolute path to the resolved plugin copy in the temp dir. - * @param {object} options - CLI options (may include a custom blueprint path). + * @param {string} pluginPath - Absolute path to the plugin source directory. + * @param {string} tmpDir - Temporary directory to write the blueprint into. + * @param {object} options - CLI options (may include a custom blueprint path and plugin name). + * @param {string} wpPluginSlug - The WordPress plugin slug (used for activation). * @return {string} Path to the generated blueprint file. */ -function buildBlueprint( pluginPath, mountPath, options ) { - const tmpDir = path.dirname( mountPath ); - +function buildBlueprint( pluginPath, tmpDir, options, wpPluginSlug ) { // Start with a base blueprint. let blueprint = { $schema: 'https://playground.wordpress.net/blueprint-schema.json', landingPage: '/wp-admin/', login: true, - preferredVersions: { - php: '8.0', - wp: 'latest', - }, features: { networking: true, }, @@ -77,6 +202,10 @@ function buildBlueprint( pluginPath, mountPath, options ) { let sourceBlueprint = null; if ( options.blueprint ) { sourceBlueprint = path.resolve( options.blueprint ); + if ( ! fs.existsSync( sourceBlueprint ) ) { + console.error( chalk.red( `Blueprint file not found: ${ sourceBlueprint }` ) ); + process.exit( 1 ); + } } else { const pluginBlueprintPath = path.join( pluginPath, @@ -91,15 +220,41 @@ function buildBlueprint( pluginPath, mountPath, options ) { if ( sourceBlueprint ) { console.log( chalk.gray( `Using blueprint: ${ path.relative( '.', sourceBlueprint ) }` ) ); - const custom = JSON.parse( fs.readFileSync( sourceBlueprint, 'utf8' ) ); + let custom; + try { + custom = JSON.parse( fs.readFileSync( sourceBlueprint, 'utf8' ) ); + } catch ( err ) { + console.error( chalk.red( `Failed to parse blueprint: ${ err.message }` ) ); + process.exit( 1 ); + } blueprint = { ...blueprint, ...custom, // Merge steps — custom steps run first, then ours. - steps: [ ...( custom.steps || [] ) ], + steps: [ ...( custom.steps || [] ), ...blueprint.steps ], }; } + // The vendor symlinks resolve to /wordpress/wp-content/packages/ which is + // outside WP_PLUGIN_DIR, so plugins_url() generates broken URLs like + // /wp-content/plugins/wordpress/wp-content/packages/... Install a mu-plugin + // that rewrites these URLs to the correct /wp-content/packages/ path. + blueprint.steps.push( { + step: 'writeFile', + path: '/wordpress/wp-content/mu-plugins/playground-fix-symlinks.php', + data: ` 0 ) { - console.error( `\nAvailable plugins: ${ available.join( ', ' ) }` ); - } - process.exit( 1 ); - } - - // Check if the plugin has been built (vendor directories must exist). - const vendorPath = path.join( pluginPath, 'vendor' ); - const jetpackVendorPath = path.join( pluginPath, 'jetpack_vendor' ); - if ( ! fs.existsSync( vendorPath ) || ! fs.existsSync( jetpackVendorPath ) ) { - console.log( - chalk.yellow( - `Plugin "${ options.plugin }" has not been built yet. Running install and build...` - ) - ); - - const install = child_process.spawnSync( - 'jetpack', - [ 'install', `plugins/${ options.plugin }` ], - { shell: true, stdio: 'inherit' } - ); - if ( install.status !== 0 ) { - console.error( chalk.red( 'Install failed. Please run "jetpack install" manually.' ) ); - process.exit( 1 ); +function findMainPluginFile( pluginPath ) { + const entries = fs.readdirSync( pluginPath, { withFileTypes: true } ); + for ( const entry of entries ) { + if ( ! entry.isFile() || ! entry.name.endsWith( '.php' ) ) { + continue; } - - const build = child_process.spawnSync( 'jetpack', [ 'build', `plugins/${ options.plugin }` ], { - shell: true, - stdio: 'inherit', - } ); - if ( build.status !== 0 ) { - console.error( chalk.red( 'Build failed. Please run "jetpack build" manually.' ) ); - process.exit( 1 ); + const content = fs.readFileSync( path.join( pluginPath, entry.name ), 'utf8' ); + if ( /^\s*\*?\s*Plugin Name:/m.test( content ) ) { + return entry.name; } } - - // Use `jetpack rsync` to create a resolved copy in a temp dir. - // This reuses the existing rsync logic which properly resolves monorepo - // symlinks and only includes production files. - const tmpDir = fs.mkdtempSync( - path.join( os.tmpdir(), `jetpack-playground-${ options.plugin }-` ) - ); - const mountPath = path.join( tmpDir, options.plugin ); - - console.log( chalk.gray( 'Syncing plugin files (resolving monorepo symlinks)...' ) ); - - const rsyncResult = child_process.spawnSync( - 'jetpack', - [ 'rsync', options.plugin, mountPath, '--non-interactive' ], - { shell: true, stdio: 'inherit' } - ); - if ( rsyncResult.status !== 0 ) { - console.error( chalk.red( 'Failed to sync plugin files.' ) ); - fs.rmSync( tmpDir, { recursive: true, force: true } ); - process.exit( 1 ); - } - - // Build the blueprint. We always inject a step to enable Jetpack offline mode - // (JETPACK_DEV_DEBUG) so the plugin works without a WordPress.com connection. - const blueprintPath = buildBlueprint( pluginPath, mountPath, options ); - - console.log( - chalkJetpackGreen( `Starting WordPress Playground with plugins/${ options.plugin }...` ) - ); - - const args = [ '@wp-playground/cli', 'server', '--auto-mount', `--blueprint=${ blueprintPath }` ]; - - if ( options.port ) { - args.push( `--port=${ options.port }` ); - } - - if ( options.verbose ) { - console.log( chalk.gray( `Running: npx ${ args.join( ' ' ) }` ) ); - console.log( chalk.gray( `Synced copy: ${ mountPath }` ) ); - } - - try { - child_process.spawnSync( 'npx', args, { - cwd: mountPath, - shell: true, - stdio: 'inherit', - } ); - } finally { - // Clean up the temporary copy. - console.log( chalk.gray( 'Cleaning up temporary files...' ) ); - fs.rmSync( tmpDir, { recursive: true, force: true } ); - } + return null; }