From 6fb516236af3bb2418c7d90d5dd03b58e413dbfa Mon Sep 17 00:00:00 2001 From: Volodymyr Kolesnykov Date: Mon, 11 May 2026 05:58:15 +0300 Subject: [PATCH 1/2] fix(cli): allow vip wp --help without requiring separator Adjust wp separator validation so it only applies when wp command payload arguments are present, not for option-only invocations like --help. Add regression coverage in command parser tests to ensure `vip wp --help` dispatches to the wp subcommand correctly. --- __tests__/lib/cli/command.js | 15 +++++++++++++++ src/lib/cli/command.js | 3 ++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/__tests__/lib/cli/command.js b/__tests__/lib/cli/command.js index 02f36bfe1..19e67b959 100644 --- a/__tests__/lib/cli/command.js +++ b/__tests__/lib/cli/command.js @@ -41,6 +41,21 @@ describe( 'utils/cli/command', () => { } ); describe( 'subcommand dispatch', () => { + it( 'allows wp option-only invocations without separator', async () => { + const parentScript = path.join( process.cwd(), 'src/bin/vip.js' ); + const childScript = path.join( process.cwd(), 'src/bin/vip-wp.js' ); + + const cmd = command( { requiredArgs: 0 } ).command( 'wp', 'Run WP-CLI commands.' ); + + await cmd.argv( [ process.execPath, parentScript, 'wp', '--help' ] ); + + expect( spawnSync ).toHaveBeenCalledWith( process.execPath, [ childScript, '--help' ], { + stdio: 'inherit', + env: process.env, + } ); + expect( process.exit ).toHaveBeenCalledWith( 0 ); + } ); + it( 'dispatches when child options appear before -- and subcommand follows separator', async () => { const parentScript = path.join( process.cwd(), 'src/bin/vip.js' ); const childScript = path.join( process.cwd(), 'src/bin/vip-wp.js' ); diff --git a/src/lib/cli/command.js b/src/lib/cli/command.js index 85cfbaf32..f7403a6a2 100644 --- a/src/lib/cli/command.js +++ b/src/lib/cli/command.js @@ -407,8 +407,9 @@ class CommanderArgsCompat { const hasSeparatorBeforeSubcommand = rawArgsBeforeSubcommand.includes( '--' ); const argsBeforeSubcommand = rawArgsBeforeSubcommand.filter( arg => arg !== '--' ); const subcommandArgs = parsedAlias.argv.slice( subcommand.index + 1 ); + const hasWpCommandPayload = subcommandArgs.some( arg => ! isOptionToken( arg ) ); const hasSeparator = argv.includes( '--' ); - if ( subcommandName === 'wp' && subcommandArgs.length && ! hasSeparator ) { + if ( subcommandName === 'wp' && hasWpCommandPayload && ! hasSeparator ) { exit.withError( 'A double dash ("--") must separate the arguments of "vip" from those of "wp". Run "vip wp --help" for examples.' ); From 69fad8a189d693b6494610b5f46c6e3303b847c9 Mon Sep 17 00:00:00 2001 From: Volodymyr Kolesnykov Date: Mon, 11 May 2026 14:57:56 +0300 Subject: [PATCH 2/2] fix: address code review comments --- __tests__/lib/cli/command.js | 19 ++++++++++++++ src/lib/cli/command.js | 48 +++++++++++++++++++++++++++++++++++- 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/__tests__/lib/cli/command.js b/__tests__/lib/cli/command.js index 19e67b959..2a42541e5 100644 --- a/__tests__/lib/cli/command.js +++ b/__tests__/lib/cli/command.js @@ -56,6 +56,25 @@ describe( 'utils/cli/command', () => { expect( process.exit ).toHaveBeenCalledWith( 0 ); } ); + it( 'allows wp option-only invocations with option values without separator', async () => { + const parentScript = path.join( process.cwd(), 'src/bin/vip.js' ); + const childScript = path.join( process.cwd(), 'src/bin/vip-wp.js' ); + + const cmd = command( { requiredArgs: 0 } ).command( 'wp', 'Run WP-CLI commands.' ); + + await cmd.argv( [ process.execPath, parentScript, 'wp', '--path', '/tmp/wp' ] ); + + expect( spawnSync ).toHaveBeenCalledWith( + process.execPath, + [ childScript, '--path', '/tmp/wp' ], + { + stdio: 'inherit', + env: process.env, + } + ); + expect( process.exit ).toHaveBeenCalledWith( 0 ); + } ); + it( 'dispatches when child options appear before -- and subcommand follows separator', async () => { const parentScript = path.join( process.cwd(), 'src/bin/vip.js' ); const childScript = path.join( process.cwd(), 'src/bin/vip-wp.js' ); diff --git a/src/lib/cli/command.js b/src/lib/cli/command.js index f7403a6a2..97a0a0ee3 100644 --- a/src/lib/cli/command.js +++ b/src/lib/cli/command.js @@ -394,6 +394,52 @@ class CommanderArgsCompat { return null; } + hasWpCommandPayload( subcommandArgs ) { + const wpFlagsWithoutValue = new Set( [ '--help', '-h', '--version', '-v', '--yes', '-y' ] ); + let expectOptionValue = false; + + for ( const arg of subcommandArgs ) { + if ( expectOptionValue ) { + if ( ! isOptionToken( arg ) ) { + expectOptionValue = false; + continue; + } + + expectOptionValue = false; + } + + if ( ! isOptionToken( arg ) ) { + return true; + } + + if ( arg.startsWith( '--' ) ) { + if ( arg.includes( '=' ) || wpFlagsWithoutValue.has( arg ) ) { + continue; + } + + expectOptionValue = true; + continue; + } + + if ( /^-[A-Za-z0-9]$/.test( arg ) ) { + if ( wpFlagsWithoutValue.has( arg ) ) { + continue; + } + + expectOptionValue = true; + continue; + } + + if ( /^-[A-Za-z0-9]+=/.test( arg ) ) { + continue; + } + + return true; + } + + return false; + } + async executeSubcommand( argv, parsedAlias, subcommand ) { const currentScript = argv[ 1 ]; const subcommandName = subcommand.name; @@ -407,7 +453,7 @@ class CommanderArgsCompat { const hasSeparatorBeforeSubcommand = rawArgsBeforeSubcommand.includes( '--' ); const argsBeforeSubcommand = rawArgsBeforeSubcommand.filter( arg => arg !== '--' ); const subcommandArgs = parsedAlias.argv.slice( subcommand.index + 1 ); - const hasWpCommandPayload = subcommandArgs.some( arg => ! isOptionToken( arg ) ); + const hasWpCommandPayload = this.hasWpCommandPayload( subcommandArgs ); const hasSeparator = argv.includes( '--' ); if ( subcommandName === 'wp' && hasWpCommandPayload && ! hasSeparator ) { exit.withError(