From 054108ee6bb28d2abc1cc39a5ebc52d26a1af3a4 Mon Sep 17 00:00:00 2001 From: luojiyin Date: Wed, 8 Jul 2026 10:50:26 +0800 Subject: [PATCH 1/4] refactor(cli-errors): extract error hint formatting and network error dispatch --- src/index.ts | 36 +++--------------------------------- src/utils/cli-errors.ts | 29 +++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 33 deletions(-) create mode 100644 src/utils/cli-errors.ts diff --git a/src/index.ts b/src/index.ts index 6af001fd..9569c4c7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,18 +16,8 @@ import { createDebugLogger, type DebugLogger } from "./output/debug.js"; import { buildSuggestedFixCommandPlan } from "./remediation/fix-commands.js"; import { scanProjectForPackageUsage } from "./usage/scanner.js"; import { getCliVersion } from "./utils/version-info.js"; -import { - blockedAdvisoryRequestHint, - fetchErrorCaCertHint, - isLikelyBlockedAdvisoryRequestError, - isRateLimitError, - isServerError, - isSslCertificateError, - offlineDbSyncHint, - rateLimitAdvisoryRequestHint, - serverAdvisoryRequestHint, - sslCertificateErrorHint, -} from "./utils/network.js"; +import { offlineDbSyncHint } from "./utils/network.js"; +import { formatHintLines, getNetworkErrorHint } from "./utils/cli-errors.js"; import { formatAdvisoryDbFreshness } from "./utils/time.js"; import { pluralize, nearestCommand } from "./utils/string.js"; import type { ParsedOptions } from "./types.js"; @@ -765,27 +755,7 @@ if (parsedArgs) { }); auditLogHandle.close(); - if (isSslCertificateError(error)) { - const [hint, ...rest] = sslCertificateErrorHint(); - console.error(chalk.yellow(hint)); - rest.forEach(line => console.error(chalk.gray(line))); - } else if (isRateLimitError(errorMessage)) { - const [hint, ...rest] = rateLimitAdvisoryRequestHint(); - console.error(chalk.yellow(hint)); - rest.forEach(line => console.error(chalk.gray(line))); - } else if (isServerError(errorMessage)) { - const [hint, ...rest] = serverAdvisoryRequestHint(); - console.error(chalk.yellow(hint)); - rest.forEach(line => console.error(chalk.gray(line))); - } else if (isLikelyBlockedAdvisoryRequestError(errorMessage)) { - const [hint, ...rest] = blockedAdvisoryRequestHint(); - console.error(chalk.yellow(hint)); - rest.forEach(line => console.error(chalk.gray(line))); - } else { - const [hint, ...rest] = fetchErrorCaCertHint(); - console.error(chalk.yellow(hint)); - rest.forEach(line => console.error(chalk.gray(line))); - } + console.error(formatHintLines(getNetworkErrorHint(error, errorMessage))); process.exit(1); }); } diff --git a/src/utils/cli-errors.ts b/src/utils/cli-errors.ts new file mode 100644 index 00000000..71eca7fb --- /dev/null +++ b/src/utils/cli-errors.ts @@ -0,0 +1,29 @@ +import { chalk } from "./chalk.js"; +import { + isSslCertificateError, + isRateLimitError, + isServerError, + isLikelyBlockedAdvisoryRequestError, + sslCertificateErrorHint, + rateLimitAdvisoryRequestHint, + serverAdvisoryRequestHint, + blockedAdvisoryRequestHint, + fetchErrorCaCertHint, +} from "./network.js"; + +export function formatHintLines(lines: string[]): string { + if (lines.length === 0) return ""; + const [first, ...rest] = lines; + return [ + chalk.yellow(first), + ...rest.map(line => chalk.gray(line)), + ].join("\n"); +} + +export function getNetworkErrorHint(error: unknown, errorMessage: string): string[] { + if (isSslCertificateError(error)) return sslCertificateErrorHint(); + if (isRateLimitError(errorMessage)) return rateLimitAdvisoryRequestHint(); + if (isServerError(errorMessage)) return serverAdvisoryRequestHint(); + if (isLikelyBlockedAdvisoryRequestError(errorMessage)) return blockedAdvisoryRequestHint(); + return fetchErrorCaCertHint(); +} From 28c739045edbf1b8827b70ea2dac35f845e5c202 Mon Sep 17 00:00:00 2001 From: luojiyin Date: Wed, 8 Jul 2026 11:06:53 +0800 Subject: [PATCH 2/4] refactor(cli-errors): return string[] from formatHintLines, output line by line --- src/index.ts | 3 ++- src/utils/cli-errors.ts | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/index.ts b/src/index.ts index 9569c4c7..dbf95f13 100644 --- a/src/index.ts +++ b/src/index.ts @@ -755,7 +755,8 @@ if (parsedArgs) { }); auditLogHandle.close(); - console.error(formatHintLines(getNetworkErrorHint(error, errorMessage))); + formatHintLines(getNetworkErrorHint(error, errorMessage)) + .forEach(line => console.error(line)); process.exit(1); }); } diff --git a/src/utils/cli-errors.ts b/src/utils/cli-errors.ts index 71eca7fb..fc31be74 100644 --- a/src/utils/cli-errors.ts +++ b/src/utils/cli-errors.ts @@ -11,13 +11,13 @@ import { fetchErrorCaCertHint, } from "./network.js"; -export function formatHintLines(lines: string[]): string { - if (lines.length === 0) return ""; +export function formatHintLines(lines: string[]): string[] { + if (lines.length === 0) return []; const [first, ...rest] = lines; return [ chalk.yellow(first), ...rest.map(line => chalk.gray(line)), - ].join("\n"); + ]; } export function getNetworkErrorHint(error: unknown, errorMessage: string): string[] { From 96292b855ad634cc4b0c98d50b762e8f73480579 Mon Sep 17 00:00:00 2001 From: luojiyin Date: Wed, 8 Jul 2026 20:30:41 +0800 Subject: [PATCH 3/4] refactor: move CLI error helpers to existing modules --- src/index.ts | 5 ++--- src/output/formatters.ts | 10 ++++++++- src/utils/cli-errors.ts | 29 ------------------------- src/utils/network.ts | 8 +++++++ tests/network.test.ts | 46 +++++++++++++++++++++++++++++++++++++++- tests/output.test.ts | 16 ++++++++++++++ 6 files changed, 80 insertions(+), 34 deletions(-) delete mode 100644 src/utils/cli-errors.ts diff --git a/src/index.ts b/src/index.ts index dbf95f13..6eb58b9e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,14 +16,14 @@ import { createDebugLogger, type DebugLogger } from "./output/debug.js"; import { buildSuggestedFixCommandPlan } from "./remediation/fix-commands.js"; import { scanProjectForPackageUsage } from "./usage/scanner.js"; import { getCliVersion } from "./utils/version-info.js"; -import { offlineDbSyncHint } from "./utils/network.js"; -import { formatHintLines, getNetworkErrorHint } from "./utils/cli-errors.js"; +import { getNetworkErrorHint, offlineDbSyncHint } from "./utils/network.js"; import { formatAdvisoryDbFreshness } from "./utils/time.js"; import { pluralize, nearestCommand } from "./utils/string.js"; import type { ParsedOptions } from "./types.js"; import { EXIT_ERROR } from "./types.js"; import { formatAdvisorySourceLine, + formatHintLines, logInfo, logWarn, printCacheSummary, @@ -834,4 +834,3 @@ async function scanProject(params: { }; } - diff --git a/src/output/formatters.ts b/src/output/formatters.ts index 3a67bb03..131736c0 100644 --- a/src/output/formatters.ts +++ b/src/output/formatters.ts @@ -34,6 +34,15 @@ export function formatAdvisorySourceLine(sourceLabel: string): string { return `${match[1]} (${chalk.cyan(match[2])})`; } +export function formatHintLines(lines: string[]): string[] { + if (lines.length === 0) return []; + const [first, ...rest] = lines; + return [ + chalk.yellow(first), + ...rest.map(line => chalk.gray(line)), + ]; +} + // MAL-* is the OSV prefix for malicious code advisories function isMaliciousAdvisory(finding: Finding): boolean { return finding.vulnerabilities.some(v => v.id.startsWith("MAL-")); @@ -338,4 +347,3 @@ export function sortFindingsForOutput(findings: Finding[]): Finding[] { export function countUniqueAdvisories(findings: Finding[]): number { return new Set(findings.flatMap(f => f.vulnerabilities.map(v => v.id))).size; } - diff --git a/src/utils/cli-errors.ts b/src/utils/cli-errors.ts deleted file mode 100644 index fc31be74..00000000 --- a/src/utils/cli-errors.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { chalk } from "./chalk.js"; -import { - isSslCertificateError, - isRateLimitError, - isServerError, - isLikelyBlockedAdvisoryRequestError, - sslCertificateErrorHint, - rateLimitAdvisoryRequestHint, - serverAdvisoryRequestHint, - blockedAdvisoryRequestHint, - fetchErrorCaCertHint, -} from "./network.js"; - -export function formatHintLines(lines: string[]): string[] { - if (lines.length === 0) return []; - const [first, ...rest] = lines; - return [ - chalk.yellow(first), - ...rest.map(line => chalk.gray(line)), - ]; -} - -export function getNetworkErrorHint(error: unknown, errorMessage: string): string[] { - if (isSslCertificateError(error)) return sslCertificateErrorHint(); - if (isRateLimitError(errorMessage)) return rateLimitAdvisoryRequestHint(); - if (isServerError(errorMessage)) return serverAdvisoryRequestHint(); - if (isLikelyBlockedAdvisoryRequestError(errorMessage)) return blockedAdvisoryRequestHint(); - return fetchErrorCaCertHint(); -} diff --git a/src/utils/network.ts b/src/utils/network.ts index 31bb7f90..dc622f22 100644 --- a/src/utils/network.ts +++ b/src/utils/network.ts @@ -158,3 +158,11 @@ export function isLikelyBlockedAdvisoryRequestError(message: string): boolean { return /^(401|403|407|408|451)\b/.test(finalCause); } + +export function getNetworkErrorHint(error: unknown, errorMessage: string): string[] { + if (isSslCertificateError(error)) return sslCertificateErrorHint(); + if (isRateLimitError(errorMessage)) return rateLimitAdvisoryRequestHint(); + if (isServerError(errorMessage)) return serverAdvisoryRequestHint(); + if (isLikelyBlockedAdvisoryRequestError(errorMessage)) return blockedAdvisoryRequestHint(); + return fetchErrorCaCertHint(); +} diff --git a/tests/network.test.ts b/tests/network.test.ts index 4cc5c4f0..094d66f0 100644 --- a/tests/network.test.ts +++ b/tests/network.test.ts @@ -1,4 +1,16 @@ -import { isLikelyBlockedAdvisoryRequestError, isRateLimitError, isServerError, isSslCertificateError, extractErrorMessage } from "../src/utils/network.js"; +import { + blockedAdvisoryRequestHint, + extractErrorMessage, + fetchErrorCaCertHint, + getNetworkErrorHint, + isLikelyBlockedAdvisoryRequestError, + isRateLimitError, + isServerError, + isSslCertificateError, + rateLimitAdvisoryRequestHint, + serverAdvisoryRequestHint, + sslCertificateErrorHint, +} from "../src/utils/network.js"; describe("extractErrorMessage", () => { it("returns the message of a plain Error", () => { @@ -131,3 +143,35 @@ describe("isServerError", () => { expect(isServerError("API failed: 500 Internal Server Error")).toBe(false); }); }); + +describe("getNetworkErrorHint", () => { + it("returns the SSL certificate hint for SSL errors", () => { + const err = Object.assign(new Error("fetch failed"), { code: "SELF_SIGNED_CERT_IN_CHAIN" }); + + expect(getNetworkErrorHint(err, "fetch failed")).toEqual(sslCertificateErrorHint()); + }); + + it("returns the rate-limit hint for OSV 429 errors", () => { + const message = "OSV batch query failed for https://api.osv.dev: OSV batch query failed: 429 Too Many Requests"; + + expect(getNetworkErrorHint(new Error(message), message)).toEqual(rateLimitAdvisoryRequestHint()); + }); + + it("returns the server-error hint for OSV 5xx errors", () => { + const message = "OSV batch query failed for https://api.osv.dev: OSV batch query failed: 503 Service Unavailable"; + + expect(getNetworkErrorHint(new Error(message), message)).toEqual(serverAdvisoryRequestHint()); + }); + + it("returns the blocked-advisory hint for likely blocked OSV requests", () => { + const message = "OSV batch query failed for https://api.osv.dev: fetch failed"; + + expect(getNetworkErrorHint(new Error(message), message)).toEqual(blockedAdvisoryRequestHint()); + }); + + it("returns the CA certificate fallback hint for unrelated errors", () => { + const message = "Unexpected scan failure"; + + expect(getNetworkErrorHint(new Error(message), message)).toEqual(fetchErrorCaCertHint()); + }); +}); diff --git a/tests/output.test.ts b/tests/output.test.ts index 48127baf..63d8b8b2 100644 --- a/tests/output.test.ts +++ b/tests/output.test.ts @@ -14,6 +14,7 @@ import { summarizeNextAction, summarizeRisk, formatRelLabel, + formatHintLines, formatFixCommandWithPublishDates, formatFixVersionPublishDate, } from "../src/output/formatters.js"; @@ -159,6 +160,21 @@ describe("output formatters", () => { } }); + it("formats empty hint lines as empty output", () => { + expect(formatHintLines([])).toEqual([]); + }); + + it("formats the first hint line before gray detail lines", () => { + const lines = formatHintLines(["Hint: retry later", "Run: cve-lite . --offline"]); + + expect(lines.map(line => stripAnsi(line))).toEqual([ + "Hint: retry later", + "Run: cve-lite . --offline", + ]); + expect(lines[0]).toContain("Hint: retry later"); + expect(lines[1]).toContain("Run: cve-lite . --offline"); + }); + it("getPrimaryParent returns null for paths shorter than 3 nodes", () => { const shortPath = createFinding({ dependencyPaths: [["project", "lodash"]], From af0617a624581185c11ce919a80d0d987583ae87 Mon Sep 17 00:00:00 2001 From: luojiyin Date: Wed, 8 Jul 2026 20:42:22 +0800 Subject: [PATCH 4/4] test: mock hint formatter in CLI integration --- tests/cli-integration.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/cli-integration.test.ts b/tests/cli-integration.test.ts index def7728a..196f7b5e 100644 --- a/tests/cli-integration.test.ts +++ b/tests/cli-integration.test.ts @@ -84,6 +84,7 @@ jest.unstable_mockModule("../src/advisory/osv-sync.js", () => ({ jest.unstable_mockModule("../src/output/formatters.js", () => ({ formatAdvisorySourceLine: jest.fn((sourceLabel: string) => sourceLabel), + formatHintLines: jest.fn((lines: string[]) => lines), logInfo: logInfoMock, logWarn: logWarnMock, printCacheSummary: printCacheSummaryMock,