diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 5dbab4fa..308fb061 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -66,7 +66,22 @@ export async function runCli(argv: readonly string[], backend: CommandBackend): process.stderr.write(`${backend.help}\n`); return 1; } - if (command === 'help' || command === '--help' || command === '-h') { + if (command === 'help') { + const path = argv.slice(1); + if ( + path.length === 0 || + (path.length === 1 && (path[0] === 'help' || path[0] === '--help' || path[0] === '-h')) + ) { + process.stdout.write(`${backend.help}\n`); + return 0; + } + if (path.some((argument) => argument.startsWith('-'))) { + process.stderr.write('gcb: help takes a command path, not options\n'); + return 1; + } + return runCli([...path, '--help'], backend); + } + if (command === '--help' || command === '-h') { process.stdout.write(`${backend.help}\n`); return 0; } diff --git a/test/unit/commands.test.ts b/test/unit/commands.test.ts index 05b894b3..cf560849 100644 --- a/test/unit/commands.test.ts +++ b/test/unit/commands.test.ts @@ -1247,3 +1247,19 @@ describe('the anchor flags, at the CLI boundary', () => { assert.doesNotMatch(noNumber.stderr, /--number is required/, 'without being asked for a number'); }); }); + +describe('the help path', () => { + it('routes gcb help pr to gcb pr --help', () => { + const home = fs.mkdtempSync(path.join(os.tmpdir(), 'gcb-help-path-')); + try { + const result = execFileSync('node', ['--conditions=development', 'packages/client/bin/gcb.js', 'help', 'pr'], { + cwd: path.resolve('.'), + encoding: 'utf8', + env: { ...process.env, HOME: home, GIT_BROKER_SOCKET: '', GIT_BROKER_SOCKET_FILE: '' }, + }); + assert.match(result, /Usage: gcb pr/); + } finally { + fs.rmSync(home, { recursive: true, force: true }); + } + }); +}); diff --git a/test/unit/gh-command.test.ts b/test/unit/gh-command.test.ts index 962a799f..1a32e1da 100644 --- a/test/unit/gh-command.test.ts +++ b/test/unit/gh-command.test.ts @@ -418,3 +418,48 @@ describe('the -- terminator', () => { assert.match(result.stderr, /too many positional arguments/); }); }); + +describe('the help path', () => { + it('routes gcb help gh pr to gh pr --help', async () => { + const result = await runClient(['help', 'gh', 'pr']); + assert.equal(result.status, 0); + assert.match(result.stdout, /Usage: gcb gh/); + }); +}); + +describe('a help path that is the help command', () => { + it('prints root help for help --help and help help', async () => { + for (const args of [['help', '--help'], ['help', 'help']]) { + const result = await runClient(args); + assert.equal(result.status, 0, args.join(' ')); + assert.match(result.stdout, /Usage: gcb/); + } + }); +}); + +describe('an invalid help path', () => { + it('does not run a command when the path contains options', async () => { + for (const args of [ + ['help', 'pr', '--'], + ['help', 'pr', '--number'], + ['help', 'pr', '--help'], + ]) { + const result = await runClient(args); + assert.equal(result.status, 1, args.join(' ')); + assert.match(result.stderr, /help takes a command path/); + } + }); +}); + +describe('command-specific help refusal', () => { + it('runs the dispatcher checks for help host', async () => { + const result = await runClient(['help', 'host']); + assert.equal(result.status, 1); + assert.equal(result.stdout, ''); + if (process.platform === 'win32') { + assert.match(result.stderr, /native Windows is not supported/); + } else { + assert.match(result.stderr, /`host` is a host command; install gcb-host on the host/); + } + }); +});