diff --git a/sdk/typescript/src/cli.ts b/sdk/typescript/src/cli.ts index eac5b514..2995039d 100644 --- a/sdk/typescript/src/cli.ts +++ b/sdk/typescript/src/cli.ts @@ -1382,6 +1382,11 @@ export async function main( let outputDir: string; let githubHost: string | undefined; if (args.input === undefined) { + if (options.outputDir !== undefined) { + throw new Error( + "--output-dir can only be used with a repository CSV; omit it to choose an output directory interactively.", + ); + } let optionIndex = 1; while (optionIndex < argv.length) { const argument = argv[optionIndex]!; @@ -1390,7 +1395,12 @@ export async function main( argument === "--effort" || argument === "--provider" || argument === "--codex" || - argument === "--knowledge-base" + argument === "--knowledge-base" || + argument === "--workers" || + argument === "--mode" || + argument === "--max-attempts" || + argument === "--plugin-path" || + argument === "--python" ) { optionIndex += 2; } else if ( @@ -1398,7 +1408,12 @@ export async function main( argument.startsWith("--effort=") || argument.startsWith("--provider=") || argument.startsWith("--codex=") || - argument.startsWith("--knowledge-base=") + argument.startsWith("--knowledge-base=") || + argument.startsWith("--workers=") || + argument.startsWith("--mode=") || + argument.startsWith("--max-attempts=") || + argument.startsWith("--plugin-path=") || + argument.startsWith("--python=") ) { optionIndex += 1; } else { @@ -1407,7 +1422,7 @@ export async function main( } if (argv[0] !== "bulk-scan" || optionIndex !== argv.length) { throw new Error( - "Run 'codex-security bulk-scan [--provider PROVIDER] [--model MODEL] [--effort EFFORT] [--codex KEY=VALUE] [--knowledge-base PATH]' to discover repositories, or provide a CSV and --output-dir.", + "Run 'codex-security bulk-scan [--provider PROVIDER] [--model MODEL] [--effort EFFORT] [--workers COUNT] [--mode MODE] [--max-attempts COUNT] [--plugin-path PATH] [--python PATH] [--codex KEY=VALUE] [--knowledge-base PATH]' to discover repositories, or provide a CSV and --output-dir.", ); } const wizard = await runBulkScanWizard( diff --git a/sdk/typescript/tests-ts/cli.test.ts b/sdk/typescript/tests-ts/cli.test.ts index 5475ef49..2c0ce446 100644 --- a/sdk/typescript/tests-ts/cli.test.ts +++ b/sdk/typescript/tests-ts/cli.test.ts @@ -888,6 +888,12 @@ describe("CLI", () => { ["bulk-scan", "--codex", 'model_reasoning_effort="high"'], ["bulk-scan", '--codex=model_reasoning_effort="high"'], ["bulk-scan", "--model", "gpt-5.6-terra", "--effort", "high"], + ["bulk-scan", "--workers", "8"], + ["bulk-scan", "--workers=8"], + ["bulk-scan", "--mode", "deep"], + ["bulk-scan", "--max-attempts=3"], + ["bulk-scan", "--plugin-path", "./plugin"], + ["bulk-scan", "--python=python3"], ["bulk-scan", "--knowledge-base", "/shared/threat-models"], [ "bulk-scan", @@ -928,6 +934,22 @@ describe("CLI", () => { } }); + test("rejects prefixed interactive bulk scans before discovery", async () => { + const stdout = capture(); + const stderr = capture(); + + expect( + await main( + ["--format", "toon", "bulk-scan", "--workers", "8"], + stdout.stream, + stderr.stream, + dependencies(), + ), + ).toBe(2); + expect(stdout.text()).toBe(""); + expect(stderr.text()).toContain("Run 'codex-security bulk-scan"); + }); + test("requires an output directory for a supplied bulk scan CSV", async () => { const stdout = capture(); const stderr = capture(); @@ -944,6 +966,24 @@ describe("CLI", () => { expect(stdout.text()).toBe(""); }); + test("lets the discovery wizard choose its output directory", async () => { + const stdout = capture(); + const stderr = capture(); + + expect( + await main( + ["bulk-scan", "--output-dir", "results"], + stdout.stream, + stderr.stream, + dependencies(), + ), + ).toBe(2); + expect(stderr.text()).toContain( + "--output-dir can only be used with a repository CSV", + ); + expect(stdout.text()).toBe(""); + }); + test("exposes only typed, read-only SDK metadata over MCP", () => { const child = spawnSync( process.execPath,