From 606256c4ec314e2a74c3458b5cf84fd48b42e6d8 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sat, 5 Sep 2026 07:36:48 +0000 Subject: [PATCH 1/6] fix(cli): classify invalid option values as usage errors --- .changeset/quiet-chairs-report.md | 5 +++ packages/agent-bundle/src/cli.ts | 14 ++++---- packages/agent-bundle/tests/cli.test.ts | 45 +++++++++++++++++++++++-- website/docs/en/reference/cli.mdx | 4 +-- website/docs/zh/reference/cli.mdx | 4 +-- 5 files changed, 58 insertions(+), 14 deletions(-) create mode 100644 .changeset/quiet-chairs-report.md diff --git a/.changeset/quiet-chairs-report.md b/.changeset/quiet-chairs-report.md new file mode 100644 index 000000000..aeb470403 --- /dev/null +++ b/.changeset/quiet-chairs-report.md @@ -0,0 +1,5 @@ +--- +"agent-bundle": patch +--- + +Treat invalid `--port`, `--trials`, `dev --install-host`, and install or uninstall `--mode` and `--scope` values as CLI usage errors that exit with code 2. (#PR) diff --git a/packages/agent-bundle/src/cli.ts b/packages/agent-bundle/src/cli.ts index 5ad4c9125..d3a8f8994 100644 --- a/packages/agent-bundle/src/cli.ts +++ b/packages/agent-bundle/src/cli.ts @@ -217,22 +217,22 @@ interface ServeAppCommandOptions extends JsonInputOptions { const collect = (value: string, previous: string[]): string[] => [...previous, value]; const port = (value: string): number => { - if (!/^(0|[1-9]\d{0,4})$/u.test(value)) throw new TypeError('Port must be a TCP port number.'); + if (!/^(0|[1-9]\d{0,4})$/u.test(value)) throw new InvalidArgumentError('Port must be a TCP port number.'); const number = Number(value); - if (number > 65_535) throw new TypeError('Port must be a TCP port number.'); + if (number > 65_535) throw new InvalidArgumentError('Port must be a TCP port number.'); return number; }; const trialCount = (value: string): number => { - if (!/^[1-9]\d{0,2}$/u.test(value)) throw new TypeError('Trials must be a positive integer.'); + if (!/^[1-9]\d{0,2}$/u.test(value)) throw new InvalidArgumentError('Trials must be a positive integer.'); const number = Number(value); - if (number > 100) throw new TypeError('Trials must be at most 100.'); + if (number > 100) throw new InvalidArgumentError('Trials must be at most 100.'); return number; }; const installHost = (value: string): InstallHost => { if (value === 'claude' || value === 'codex' || value === 'cursor') return value; - throw new TypeError('Install host must be claude, codex, or cursor.'); + throw new InvalidArgumentError('Install host must be claude, codex, or cursor.'); }; const collectInstallHost = (value: string, previous: readonly InstallHost[]): readonly InstallHost[] => @@ -240,12 +240,12 @@ const collectInstallHost = (value: string, previous: readonly InstallHost[]): re const installMode = (value: string): InstallMode => { if (value === 'local' || value === 'marketplace') return value; - throw new TypeError('Install mode must be local or marketplace.'); + throw new InvalidArgumentError('Install mode must be local or marketplace.'); }; const installScope = (value: string): InstallScope => { if (value === 'user' || value === 'project' || value === 'local') return value; - throw new TypeError('Install scope must be user, project, or local.'); + throw new InvalidArgumentError('Install scope must be user, project, or local.'); }; const mcpAppProfile = (value: string): McpAppProfileId => { diff --git a/packages/agent-bundle/tests/cli.test.ts b/packages/agent-bundle/tests/cli.test.ts index 0df1fde20..c68239fd6 100644 --- a/packages/agent-bundle/tests/cli.test.ts +++ b/packages/agent-bundle/tests/cli.test.ts @@ -1116,8 +1116,47 @@ it('rejects serve-app argv that cannot be served before anything launches', asyn const badCapability = await runSourceCliWithOutput(['serve-app', 'status/status', '--root', '/project', '--allow', 'camera'], dependencies); expect(badCapability.code).toBe(2); expect(badCapability.stderr).toContain('Consent capability must be call-tool, download-file, open-external-link, or request-display-mode.'); - const badPort = await runSourceCliWithOutput(['serve-app', 'status/status', '--root', '/project', '--port', '70000'], dependencies); - expect(badPort.code).toBe(1); - expect(JSON.parse(badPort.stderr)).toEqual([{ code: 'AB5000', message: 'Port must be a TCP port number.', severity: 'error' }]); expect(launched).toEqual([]); }); + +it('reports invalid option arguments as Commander usage errors', async () => { + const cases = [ + { + args: ['dev', '--port', '70000'], + option: '--port ', + reason: 'Port must be a TCP port number.', + value: '70000', + }, + { + args: ['eval', '--trials', '0'], + option: '--trials ', + reason: 'Trials must be a positive integer.', + value: '0', + }, + { + args: ['dev', '--install-host', 'windsurf'], + option: '--install-host ', + reason: 'Install host must be claude, codex, or cursor.', + value: 'windsurf', + }, + { + args: ['install', 'cursor', '--mode', 'remote'], + option: '--mode ', + reason: 'Install mode must be local or marketplace.', + value: 'remote', + }, + { + args: ['install', 'claude', '--scope', 'global'], + option: '--scope ', + reason: 'Install scope must be user, project, or local.', + value: 'global', + }, + ] as const; + + for (const { args, option, reason, value } of cases) { + const result = await runSourceCliWithOutput([...args]); + expect(result.code).toBe(2); + expect(result.stderr).toContain(`error: option '${option}' argument '${value}' is invalid. ${reason}`); + expect(result.stderr).not.toContain('AB5000'); + } +}); diff --git a/website/docs/en/reference/cli.mdx b/website/docs/en/reference/cli.mdx index 4c644ddaa..88d760984 100644 --- a/website/docs/en/reference/cli.mdx +++ b/website/docs/en/reference/cli.mdx @@ -295,8 +295,8 @@ a name) and the same `--input` / `--input-file` pair as `mcp invoke`. | Code | Meaning | | --- | --- | | `0` | Success, including `--help` and `--version`. | -| `1` | A reported failure: an error diagnostic, an invalid model from `inspect`, a failing or inconclusive eval run, or an uncaught error written to stderr as one `AB5000` diagnostic. Option values checked outside the parser land here: an invalid `install` / `uninstall` ``, `--scope`, or `--mode`, an invalid `--port`, `--trials`, or `dev --install-host`, and a malformed `--input` / `--input-file` document. | -| `2` | A parser-level failure from the command-line parser: an unknown command or option, a missing required argument or option, or an invalid `serve-app --profile`, `serve-app --allow`, or `doctor --host` value. | +| `1` | A reported failure: an error diagnostic, an invalid model from `inspect`, a failing or inconclusive eval run, or an uncaught error written to stderr as one `AB5000` diagnostic. A malformed `--input` / `--input-file` document is a runtime input failure and lands here. | +| `2` | A parser-level failure from the command-line parser: an unknown command or option, a missing required argument or option, or an invalid value for `--port`, `eval --trials`, `dev --install-host`, `install` / `uninstall` ``, `--scope`, or `--mode`, `serve-app --profile`, `serve-app --allow`, or `doctor --host`. | An eval run exits `1` when any trial **fails or is inconclusive** — an inconclusive trial produced no evidence, so it cannot report success either. diff --git a/website/docs/zh/reference/cli.mdx b/website/docs/zh/reference/cli.mdx index dcd10ee17..599194a20 100644 --- a/website/docs/zh/reference/cli.mdx +++ b/website/docs/zh/reference/cli.mdx @@ -273,8 +273,8 @@ keep-data 选项)。相对包的安装器 bin 接受带同样标志的 `uninst | 退出码 | 含义 | | --- | --- | | `0` | 成功,包括 `--help` 与 `--version`。 | -| `1` | 一次被报告的失败:一条 error 级诊断、`inspect` 得到的无效模型、失败或结论不明的 eval 运行,或者被作为一条 `AB5000` 诊断写到 stderr 的未捕获错误。 | -| `2` | 仅限命令行解析器(Commander)自身的错误:未知选项、缺少参数,或者 `serve-app` 的 `--profile` 与 `--allow`、`doctor --host` 的无效取值——这三个校验器抛出 `InvalidArgumentError`。`install`/`uninstall` 的 `` 参数、`--scope` 与 `--mode`,以及 `dev --install-host`、`--port` 与 `eval --trials` 的校验器抛出的是普通 `TypeError`,`runCli` 会把它作为一条 `AB5000` 诊断写到 stderr 并以 `1` 退出。 | +| `1` | 一次被报告的失败:一条 error 级诊断、`inspect` 得到的无效模型、失败或结论不明的 eval 运行,或者被作为一条 `AB5000` 诊断写到 stderr 的未捕获错误。格式错误的 `--input` / `--input-file` 文档属于运行时输入失败,因此落在这里。 | +| `2` | 命令行解析器(Commander)自身的错误:未知命令或选项、缺少必需的参数或选项,或者 `--port`、`eval --trials`、`dev --install-host`、`install` / `uninstall` 的 ``、`--scope` 与 `--mode`、`serve-app --profile`、`serve-app --allow` 或 `doctor --host` 的无效取值。 | 当任何一次试验**失败或结论不明**时,eval 运行以 `1` 退出——结论不明的试验没有产生证据,因此它同样不能 报告成功。 From 497cb985ed87c29b10dbb1f69c8f595c2e2f787f Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sat, 5 Sep 2026 07:37:10 +0000 Subject: [PATCH 2/6] docs(changeset): reference pull request 615 --- .changeset/quiet-chairs-report.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/quiet-chairs-report.md b/.changeset/quiet-chairs-report.md index aeb470403..59cd9e811 100644 --- a/.changeset/quiet-chairs-report.md +++ b/.changeset/quiet-chairs-report.md @@ -2,4 +2,4 @@ "agent-bundle": patch --- -Treat invalid `--port`, `--trials`, `dev --install-host`, and install or uninstall `--mode` and `--scope` values as CLI usage errors that exit with code 2. (#PR) +Treat invalid `--port`, `--trials`, `dev --install-host`, and install or uninstall `--mode` and `--scope` values as CLI usage errors that exit with code 2. (#615) From ea2dde01c4a946856c24d47c19516588cbcab48c Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sat, 5 Sep 2026 08:09:39 +0000 Subject: [PATCH 3/6] test(cli): cover positional host usage errors --- .changeset/quiet-chairs-report.md | 2 +- packages/agent-bundle/tests/cli.test.ts | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.changeset/quiet-chairs-report.md b/.changeset/quiet-chairs-report.md index 59cd9e811..50141717d 100644 --- a/.changeset/quiet-chairs-report.md +++ b/.changeset/quiet-chairs-report.md @@ -2,4 +2,4 @@ "agent-bundle": patch --- -Treat invalid `--port`, `--trials`, `dev --install-host`, and install or uninstall `--mode` and `--scope` values as CLI usage errors that exit with code 2. (#615) +Make the `agent-bundle` CLI treat invalid `--port`, `--trials`, `dev --install-host`, and install or uninstall `--mode` and `--scope` values as usage errors that exit with code 2. (#615) diff --git a/packages/agent-bundle/tests/cli.test.ts b/packages/agent-bundle/tests/cli.test.ts index 2d8888949..d0e00c1f6 100644 --- a/packages/agent-bundle/tests/cli.test.ts +++ b/packages/agent-bundle/tests/cli.test.ts @@ -1144,7 +1144,7 @@ it('rejects serve-app argv that cannot be served before anything launches', asyn expect(launched).toEqual([]); }); -it('reports invalid option arguments as Commander usage errors', async () => { +it('reports invalid CLI arguments as Commander usage errors', async () => { const cases = [ { args: ['dev', '--port', '70000'], @@ -1184,4 +1184,11 @@ it('reports invalid option arguments as Commander usage errors', async () => { expect(result.stderr).toContain(`error: option '${option}' argument '${value}' is invalid. ${reason}`); expect(result.stderr).not.toContain('AB5000'); } + + const invalidInstallHost = await runSourceCliWithOutput(['install', 'windsurf']); + expect(invalidInstallHost.code).toBe(2); + expect(invalidInstallHost.stderr).toContain( + "error: command-argument value 'windsurf' is invalid for argument 'host'. Install host must be claude, codex, or cursor.", + ); + expect(invalidInstallHost.stderr).not.toContain('AB5000'); }); From c01c611724d15669c98af48ee2b5e15f220de660 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sat, 5 Sep 2026 08:12:49 +0000 Subject: [PATCH 4/6] docs(changeset): include invalid host arguments --- .changeset/quiet-chairs-report.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/quiet-chairs-report.md b/.changeset/quiet-chairs-report.md index 50141717d..7dc231a27 100644 --- a/.changeset/quiet-chairs-report.md +++ b/.changeset/quiet-chairs-report.md @@ -2,4 +2,4 @@ "agent-bundle": patch --- -Make the `agent-bundle` CLI treat invalid `--port`, `--trials`, `dev --install-host`, and install or uninstall `--mode` and `--scope` values as usage errors that exit with code 2. (#615) +Make the `agent-bundle` CLI treat invalid `--port`, `--trials`, `dev --install-host`, and install or uninstall ``, `--mode`, and `--scope` values as usage errors that exit with code 2. (#615) From ee3b9d771e49f7445cf17990005b89cfd6ff6deb Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sat, 5 Sep 2026 08:33:31 +0000 Subject: [PATCH 5/6] docs(changeset): name AB5000 behavior change --- .changeset/quiet-chairs-report.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/quiet-chairs-report.md b/.changeset/quiet-chairs-report.md index 7dc231a27..1b693f996 100644 --- a/.changeset/quiet-chairs-report.md +++ b/.changeset/quiet-chairs-report.md @@ -2,4 +2,4 @@ "agent-bundle": patch --- -Make the `agent-bundle` CLI treat invalid `--port`, `--trials`, `dev --install-host`, and install or uninstall ``, `--mode`, and `--scope` values as usage errors that exit with code 2. (#615) +Move invalid `--port`, `--trials`, `dev --install-host`, and install or uninstall ``, `--mode`, and `--scope` values in the `agent-bundle` CLI away from `AB5000` diagnostics and exit code 1 to Commander usage errors and exit code 2. (#615) From 9d6c83a3c24e003c3378b7e416efbfaa4897fdab Mon Sep 17 00:00:00 2001 From: Zack Jackson <25274700+ScriptedAlchemy@users.noreply.github.com> Date: Sat, 5 Sep 2026 08:46:36 +0000 Subject: [PATCH 6/6] test(cli): drop needless as-const and spread in usage-error cases Co-authored-by: Zack Jackson --- packages/agent-bundle/tests/cli.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/agent-bundle/tests/cli.test.ts b/packages/agent-bundle/tests/cli.test.ts index d0e00c1f6..832ff0bc2 100644 --- a/packages/agent-bundle/tests/cli.test.ts +++ b/packages/agent-bundle/tests/cli.test.ts @@ -1176,10 +1176,10 @@ it('reports invalid CLI arguments as Commander usage errors', async () => { reason: 'Install scope must be user, project, or local.', value: 'global', }, - ] as const; + ]; for (const { args, option, reason, value } of cases) { - const result = await runSourceCliWithOutput([...args]); + const result = await runSourceCliWithOutput(args); expect(result.code).toBe(2); expect(result.stderr).toContain(`error: option '${option}' argument '${value}' is invalid. ${reason}`); expect(result.stderr).not.toContain('AB5000');