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..6e32700 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); } @@ -105,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) { @@ -121,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; @@ -150,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; } 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." + ); + }); +});