From 9079bab03eebc318ca12216e5cb9e10711326020 Mon Sep 17 00:00:00 2001 From: dagangtj <2285648311@qq.com> Date: Sat, 23 May 2026 06:08:26 +0800 Subject: [PATCH 1/2] fix(cli): suggest --help when unknown or unexpected CLI argument is passed When parseArgs encounters an unknown option or unexpected argument, the error message previously only stated what went wrong without telling the user they can run --help to see valid options. This change: - Appends 'Run cve-lite --help to see supported options.' to every Unknown option and Unexpected argument error thrown by args.ts - Updates index.ts to avoid printing the hint a second time when the message already contains it - Adds dedicated unit tests covering all three CLI entry points Closes #400 --- src/cli/args.ts | 12 ++++++------ src/index.ts | 4 +++- tests/args.test.ts | 38 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 7 deletions(-) create mode 100644 tests/args.test.ts diff --git a/src/cli/args.ts b/src/cli/args.ts index 2a14071..b0e9c52 100644 --- a/src/cli/args.ts +++ b/src/cli/args.ts @@ -15,8 +15,8 @@ export function parseArgs(argv: string[]): { command: CliCommand; options: Parse if (arg === "-v" || arg === "--version") { options.version = true; continue; } if (arg === "--output") { options.output = argv[++i]; continue; } if (arg.startsWith("--output=")) { options.output = arg.slice("--output=".length); continue; } - if (arg.startsWith("-")) throw new Error(`Unknown option: ${arg}`); - throw new Error(`Unexpected argument: ${arg}`); + if (arg.startsWith("-")) throw new Error(`Unknown option: ${arg}\nRun `cve-lite --help` to see supported options.`); + throw new Error(`Unexpected argument: ${arg}\nRun `cve-lite --help` to see supported options.`); } return { command: "advisories-sync", options }; @@ -26,8 +26,8 @@ export function parseArgs(argv: string[]): { command: CliCommand; options: Parse for (let i = 1; i < argv.length; i++) { const arg = argv[i]; if (arg === "-h" || arg === "--help") { options.help = true; continue; } - if (arg.startsWith("-")) throw new Error(`Unknown option: ${arg}`); - throw new Error(`Unexpected argument: ${arg}`); + if (arg.startsWith("-")) throw new Error(`Unknown option: ${arg}\nRun `cve-lite --help` to see supported options.`); + throw new Error(`Unexpected argument: ${arg}\nRun `cve-lite --help` to see supported options.`); } return { command: "install-skill", options }; } @@ -76,9 +76,9 @@ export function parseArgs(argv: string[]): { command: CliCommand; options: Parse if (arg === "--no-cache") { options.noCache = true; continue; } if (arg === "--sarif") { options.sarif = true; continue; } if (arg === "--cdx") { options.cdx = true; continue; } - if (arg.startsWith("-")) throw new Error(`Unknown option: ${arg}`); + if (arg.startsWith("-")) throw new Error(`Unknown option: ${arg}\nRun `cve-lite --help` to see supported options.`); if (!projectArg) { projectArg = arg; continue; } - throw new Error(`Unexpected argument: ${arg}`); + throw new Error(`Unexpected argument: ${arg}\nRun `cve-lite --help` to see supported options.`); } if (options.sarif && options.report) { diff --git a/src/index.ts b/src/index.ts index d01afde..aeccf73 100644 --- a/src/index.ts +++ b/src/index.ts @@ -46,7 +46,9 @@ try { } catch (error) { const message = error instanceof Error ? error.message : String(error); console.error(chalk.red(`Error: ${message}`)); - console.error(chalk.gray("Run `cve-lite --help` to see supported options.")); + if (!message.includes("--help")) { + console.error(chalk.gray("Run `cve-lite --help` to see supported options.")); + } process.exit(1); } diff --git a/tests/args.test.ts b/tests/args.test.ts new file mode 100644 index 0000000..87c7081 --- /dev/null +++ b/tests/args.test.ts @@ -0,0 +1,38 @@ +import { jest } from "@jest/globals"; +import { parseArgs } from "../src/cli/args.js"; + +describe("parseArgs", () => { + it("includes --help hint in unknown option error", () => { + expect(() => parseArgs(["--verbosse"])).toThrow( + "Unknown option: --verbosse\nRun `cve-lite --help` to see supported options." + ); + }); + + it("includes --help hint in unexpected argument error", () => { + expect(() => parseArgs([".", "extra-arg"])).toThrow( + "Unexpected argument: extra-arg\nRun `cve-lite --help` to see supported options." + ); + }); + + it("parses valid arguments without error", () => { + expect(() => parseArgs(["--json", "--verbose"])).not.toThrow(); + }); + + it("includes --help hint for unknown option in advisories-sync command", () => { + expect(() => parseArgs(["advisories", "sync", "--bad-flag"])).toThrow( + "Unknown option: --bad-flag\nRun `cve-lite --help` to see supported options." + ); + }); + + it("includes --help hint for unexpected argument in advisories-sync command", () => { + expect(() => parseArgs(["advisories", "sync", "unexpected"])).toThrow( + "Unexpected argument: unexpected\nRun `cve-lite --help` to see supported options." + ); + }); + + it("includes --help hint for unknown option in install-skill command", () => { + expect(() => parseArgs(["install-skill", "--bad-flag"])).toThrow( + "Unknown option: --bad-flag\nRun `cve-lite --help` to see supported options." + ); + }); +}); From 3ccaae0e898af5df411c56f5426cd6a0bc567df2 Mon Sep 17 00:00:00 2001 From: dagangtj <2285648311@qq.com> Date: Sat, 23 May 2026 06:11:35 +0800 Subject: [PATCH 2/2] fix(cli): improve error messages for incompatible flags and offline DB unavailability Issue #401: Flag incompatibility errors now explain what each flag does and guide the user on which to remove: - --fix vs --json: explains interactive fixes vs JSON output - --offline/--offline-db vs --osv-url: explains local DB vs custom endpoint - --no-cache vs --offline: explains cache behavior in offline mode - --report vs --json: explains HTML report vs JSON output Issue #402: Offline advisory database unavailable error now includes the sync command hint: - Without --offline-db: 'To build it, run: cve-lite advisories sync' - With --offline-db: includes --output in the hint Closes #401 Closes #402 --- src/index.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/index.ts b/src/index.ts index aeccf73..6e32700 100644 --- a/src/index.ts +++ b/src/index.ts @@ -107,11 +107,11 @@ if (parsedArgs) { } if ((options.offline || options.offlineDb) && options.osvUrl) { - throw new Error("--offline/--offline-db cannot be used with --osv-url"); + throw new Error("--offline/--offline-db cannot be used with --osv-url. Choose offline mode (local DB) or online mode (custom OSV endpoint), not both."); } if (options.noCache && (options.offline || options.offlineDb)) { - throw new Error("--no-cache cannot be used with --offline or --offline-db"); + throw new Error("--no-cache cannot be used with --offline or --offline-db. In offline mode the local advisory DB is used directly; --no-cache only applies to online scans."); } if (options.osvUrl) { @@ -123,11 +123,11 @@ if (parsedArgs) { } if (options.fix && options.json) { - throw new Error("--fix cannot be used with --json"); + throw new Error("--fix cannot be used with --json. Use --fix to apply fixes interactively, or --json to output scan results as JSON — not both at once."); } if (options.report && options.json) { - throw new Error("--report cannot be used with --json"); + throw new Error("--report cannot be used with --json. Use --report to generate an HTML report, or --json to output scan results as JSON — not both at once."); } let advisorySourceLine: string; @@ -152,7 +152,11 @@ if (parsedArgs) { advisorySource.cleanup(); } catch (error) { if (options.offline || options.offlineDb) { - throw new Error(`Offline advisory database is not available: ${error instanceof Error ? error.message : String(error)}`); + const reason = error instanceof Error ? error.message : String(error); + const hint = options.offlineDb + ? `To build it, run: cve-lite advisories sync --output ${options.offlineDb}` + : 'To build it, run: cve-lite advisories sync'; + throw new Error(`Offline advisory database is not available: ${reason}\n${hint}`); } throw error; }