diff --git a/.changeset/quiet-chairs-report.md b/.changeset/quiet-chairs-report.md new file mode 100644 index 000000000..1b693f996 --- /dev/null +++ b/.changeset/quiet-chairs-report.md @@ -0,0 +1,5 @@ +--- +"agent-bundle": patch +--- + +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) diff --git a/packages/agent-bundle/src/cli.ts b/packages/agent-bundle/src/cli.ts index 4f935deba..21b098b96 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 c979aa081..832ff0bc2 100644 --- a/packages/agent-bundle/tests/cli.test.ts +++ b/packages/agent-bundle/tests/cli.test.ts @@ -1141,8 +1141,54 @@ 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 CLI 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', + }, + ]; + + 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'); + } + + 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'); +}); diff --git a/website/docs/en/reference/cli.mdx b/website/docs/en/reference/cli.mdx index c6326c88b..1dd0c84c3 100644 --- a/website/docs/en/reference/cli.mdx +++ b/website/docs/en/reference/cli.mdx @@ -296,8 +296,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 22c8fb645..53a09f619 100644 --- a/website/docs/zh/reference/cli.mdx +++ b/website/docs/zh/reference/cli.mdx @@ -274,8 +274,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` 退出——结论不明的试验没有产生证据,因此它同样不能 报告成功。