Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions src/cli/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.`);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The backticks around cve-lite --help aren't escaped here, so the template literal terminates early — tsc --noEmit produces 12 errors across all six lines (19, 29, 30, 79, 81 too).

Also worth pulling the hint into a constant since it's repeated six times:

const HELP_HINT = `\\nRun \`cve-lite --help\` to see supported options.`;
// then: throw new Error(`Unknown option: ${arg}${HELP_HINT}`);

throw new Error(`Unexpected argument: ${arg}\nRun `cve-lite --help` to see supported options.`);
}

return { command: "advisories-sync", options };
Expand All @@ -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 };
}
Expand Down Expand Up @@ -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) {
Expand Down
18 changes: 12 additions & 6 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This works, but it ties the catch block to the text content of errors thrown in args.ts. If the wording ever changes the deduplication silently breaks. Cleaner to keep args.ts throwing plain messages and always append the hint here unconditionally.

console.error(chalk.gray("Run `cve-lite --help` to see supported options."));
}
process.exit(1);
}

Expand Down Expand Up @@ -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) {
Expand All @@ -121,11 +123,11 @@ if (parsedArgs) {
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The improved flag-conflict messages here don't have test coverage yet. The existing cli-integration.test.ts already mocks these option combinations, so it'd be a natural home for assertions on the new message text.


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;
Expand All @@ -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;
}
Expand Down
38 changes: 38 additions & 0 deletions tests/args.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { jest } from "@jest/globals";
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jest is imported but never used — safe to remove. Also, there's already a describe("parseArgs") block in helpers.test.ts — could you fold these in there to keep everything in one place?

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."
);
});
});