diff --git a/packages/playground/cli/src/run-cli.ts b/packages/playground/cli/src/run-cli.ts index 150b5aae44..b3639bfa1d 100644 --- a/packages/playground/cli/src/run-cli.ts +++ b/packages/playground/cli/src/run-cli.ts @@ -45,6 +45,7 @@ import { BlueprintsV1Handler } from './blueprints-v1/blueprints-v1-handler'; import { startBridge } from '@php-wasm/xdebug-bridge'; import path from 'path'; import os from 'os'; +import { exec } from 'child_process'; import { cleanupStalePlaygroundTempDirs, createPlaygroundCliTempDir, @@ -280,6 +281,81 @@ export async function parseOptionsAndRunCLI(argsToParse: string[]) { }, }; + /** + * Options for the high-level `start` command. + * This command provides a simplified, opinionated interface for common use cases, + * similar to wp-now. It auto-detects project type and uses sensible defaults. + */ + const startCommandOptions: Record = { + path: { + describe: + 'Path to the project directory. Playground will auto-detect if this is a plugin, theme, wp-content, or WordPress directory.', + type: 'string', + default: process.cwd(), + }, + php: { + describe: 'PHP version to use.', + type: 'string', + default: RecommendedPHPVersion, + choices: SupportedPHPVersions, + }, + wp: { + describe: 'WordPress version to use.', + type: 'string', + default: 'latest', + }, + port: { + describe: 'Port to listen on.', + type: 'number', + default: 9400, + }, + blueprint: { + describe: + 'Path to a Blueprint JSON file to execute on startup.', + type: 'string', + }, + login: { + describe: 'Auto-login as the admin user.', + type: 'boolean', + default: true, + }, + xdebug: { + describe: 'Enable Xdebug for debugging.', + type: 'boolean', + default: false, + }, + 'skip-browser': { + describe: + 'Do not open the site in your default browser on startup.', + type: 'boolean', + default: false, + }, + quiet: { + describe: 'Suppress non-essential output.', + type: 'boolean', + default: false, + }, + // Advanced options for power users who need more control + 'site-url': { + describe: + 'Override the site URL. By default, derived from the port (http://127.0.0.1:).', + type: 'string', + }, + mount: { + describe: + 'Mount a directory to the PHP runtime (can be used multiple times). Format: /host/path:/vfs/path. Use this for additional mounts beyond auto-detection.', + type: 'array', + string: true, + coerce: parseMountWithDelimiterArguments, + }, + 'no-auto-mount': { + describe: + 'Disable automatic project type detection. Use --mount to manually specify mounts instead.', + type: 'boolean', + default: false, + }, + }; + const buildSnapshotOnlyOptions: Record = { outfile: { describe: 'When building, write to this output file.', @@ -290,9 +366,28 @@ export async function parseOptionsAndRunCLI(argsToParse: string[]) { const yargsObject = yargs(argsToParse) .usage('Usage: wp-playground [options]') + .command( + 'start', + 'Start a local WordPress server with automatic project detection (recommended)', + (yargsInstance: Argv) => + yargsInstance + .usage( + 'Usage: wp-playground start [options]\n\n' + + 'The easiest way to run WordPress locally. Automatically detects\n' + + 'if your directory contains a plugin, theme, wp-content, or\n' + + 'WordPress installation and configures everything for you.\n\n' + + 'Examples:\n' + + ' wp-playground start # Start in current directory\n' + + ' wp-playground start --path=./my-plugin # Start with a specific path\n' + + ' wp-playground start --wp=6.7 --php=8.3 # Use specific versions\n' + + ' wp-playground start --skip-browser # Skip opening browser\n' + + ' wp-playground start --no-auto-mount # Disable auto-detection' + ) + .options(startCommandOptions) + ) .command( 'server', - 'Start a local WordPress server', + 'Start a local WordPress server (advanced, low-level)', (yargsInstance: Argv) => yargsInstance.options({ ...sharedOptions, @@ -463,14 +558,40 @@ export async function parseOptionsAndRunCLI(argsToParse: string[]) { const command = args._[0] as string; - if (!['run-blueprint', 'server', 'build-snapshot'].includes(command)) { + if ( + !['start', 'run-blueprint', 'server', 'build-snapshot'].includes( + command + ) + ) { yargsObject.showHelp(); process.exit(1); } + // Track whether to open browser (only for 'start' command) + let shouldOpenBrowser = false; + + // Transform 'start' command args to server-compatible args + if (command === 'start') { + shouldOpenBrowser = args['skip-browser'] !== true; + + // Enable auto-mount unless explicitly disabled + if (!args['no-auto-mount']) { + args['auto-mount'] = (args['path'] as string) || process.cwd(); + } + + // Verbosity handling + if (args['quiet']) { + args['verbosity'] = 'quiet'; + } + + // Intl is always enabled for the start command + args['intl'] = true; + } + const cliArgs = { ...args, - command, + // The 'start' command internally runs as 'server' + command: command === 'start' ? 'server' : command, mount: [ ...((args['mount'] as Mount[]) || []), ...((args['mount-dir'] as Mount[]) || []), @@ -487,6 +608,11 @@ export async function parseOptionsAndRunCLI(argsToParse: string[]) { process.exit(0); } + // Open browser for the 'start' command + if (shouldOpenBrowser) { + openInBrowser(cliServer.serverUrl); + } + const cleanUpCliAndExit = (() => { // Remember we are already cleaning up to preclude the possibility // of multiple, conflicting cleanup attempts. @@ -535,7 +661,7 @@ export interface RunCLIArgs { | BlueprintV1Declaration | BlueprintV2Declaration | BlueprintBundle; - command: 'server' | 'run-blueprint' | 'build-snapshot'; + command: 'start' | 'server' | 'run-blueprint' | 'build-snapshot'; debug?: boolean; login?: boolean; mount?: Mount[]; @@ -576,6 +702,11 @@ export interface RunCLIArgs { 'db-path'?: string; 'truncate-new-site-directory'?: boolean; allow?: string; + + // --------- Start command args ----------- + path?: string; + skipBrowser?: boolean; + noAutoMount?: boolean; } type PlaygroundCliWorker = @@ -1305,6 +1436,35 @@ async function exposeFileLockManager(fileLockManager: FileLockManagerForNode) { return port2; } +/** + * Open a URL in the user's default browser. + * Works cross-platform: macOS, Windows, and Linux. + */ +function openInBrowser(url: string): void { + const platform = os.platform(); + let command: string; + + switch (platform) { + case 'darwin': + command = `open "${url}"`; + break; + case 'win32': + command = `start "" "${url}"`; + break; + default: + // Linux and other Unix-like systems + command = `xdg-open "${url}"`; + break; + } + + exec(command, (error) => { + if (error) { + // Don't fail the CLI if browser opening fails, just log a debug message + logger.debug(`Could not open browser: ${error.message}`); + } + }); +} + async function zipSite( playground: RemoteAPI, outfile: string diff --git a/packages/playground/cli/tests/run-cli.spec.ts b/packages/playground/cli/tests/run-cli.spec.ts index 3ce0a71dd1..99a6425cff 100644 --- a/packages/playground/cli/tests/run-cli.spec.ts +++ b/packages/playground/cli/tests/run-cli.spec.ts @@ -740,6 +740,28 @@ describe.each(blueprintVersions)( 60_000 * 5 ); +describe('start command', () => { + test('should work with default options', async () => { + // The start command internally runs as 'server' with auto-mount enabled + await using cliServer = await runCLI({ + command: 'server', + // Simulating what 'start' command does: + // - enables auto-mount with current directory + // - enables login by default + // - enables intl + login: true, + intl: true, + // Skip WordPress setup for speed since we're just testing the command structure + wordpressInstallMode: 'do-not-attempt-installing', + skipSqliteSetup: true, + blueprint: undefined, + }); + + // Verify server started successfully + expect(cliServer.serverUrl).toMatch(/^http:\/\/127\.0\.0\.1:\d+$/); + }); +}); + describe('other run-cli behaviors', () => { describe('auto-login', () => { test('should clear old auto-login cookie', async () => {