From fdcf42ad890fb68f92470f64b678886de84681fd Mon Sep 17 00:00:00 2001 From: Sonu Kapoor Date: Fri, 26 Jun 2026 00:30:50 +0200 Subject: [PATCH 1/6] feat: add hasOverrideEntries utility to detect override fields in package.json Returns true if package.json at a given directory contains at least one entry in overrides (npm/Bun), pnpm.overrides, or resolutions (Yarn). Returns false if the file is missing, unreadable, or all containers are empty. --- src/utils/package-json.ts | 16 ++++++++ tests/utils/package-json.test.ts | 67 ++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 tests/utils/package-json.test.ts diff --git a/src/utils/package-json.ts b/src/utils/package-json.ts index 5374013..87e85eb 100644 --- a/src/utils/package-json.ts +++ b/src/utils/package-json.ts @@ -141,3 +141,19 @@ function expandWorkspacePattern(projectPath: string, segments: string[], current export function isRecord(value: unknown): value is Record { return !!value && typeof value === "object" && !Array.isArray(value); } + +export function hasOverrideEntries(dir: string): boolean { + const pkgPath = path.join(dir, "package.json"); + if (!fs.existsSync(pkgPath)) return false; + try { + const raw = JSON.parse(fs.readFileSync(pkgPath, "utf8")) as Record; + if (!raw || typeof raw !== "object") return false; + if (isRecord(raw.overrides) && Object.keys(raw.overrides).length > 0) return true; + const pnpm = raw.pnpm; + if (isRecord(pnpm) && isRecord(pnpm.overrides) && Object.keys(pnpm.overrides).length > 0) return true; + if (isRecord(raw.resolutions) && Object.keys(raw.resolutions).length > 0) return true; + return false; + } catch { + return false; + } +} diff --git a/tests/utils/package-json.test.ts b/tests/utils/package-json.test.ts new file mode 100644 index 0000000..b460c5c --- /dev/null +++ b/tests/utils/package-json.test.ts @@ -0,0 +1,67 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { hasOverrideEntries } from "../../src/utils/package-json.js"; + +function makeTempDir(): string { + return fs.mkdtempSync(path.join(os.tmpdir(), "pkg-json-test-")); +} + +function writePackageJson(dir: string, content: Record): void { + fs.writeFileSync(path.join(dir, "package.json"), JSON.stringify(content)); +} + +describe("hasOverrideEntries", () => { + it("returns false when package.json does not exist", () => { + const dir = makeTempDir(); + expect(hasOverrideEntries(dir)).toBe(false); + }); + + it("returns false when package.json has no override fields", () => { + const dir = makeTempDir(); + writePackageJson(dir, { name: "test", version: "1.0.0" }); + expect(hasOverrideEntries(dir)).toBe(false); + }); + + it("returns false when overrides is an empty object", () => { + const dir = makeTempDir(); + writePackageJson(dir, { overrides: {} }); + expect(hasOverrideEntries(dir)).toBe(false); + }); + + it("returns false when pnpm.overrides is an empty object", () => { + const dir = makeTempDir(); + writePackageJson(dir, { pnpm: { overrides: {} } }); + expect(hasOverrideEntries(dir)).toBe(false); + }); + + it("returns false when resolutions is an empty object", () => { + const dir = makeTempDir(); + writePackageJson(dir, { resolutions: {} }); + expect(hasOverrideEntries(dir)).toBe(false); + }); + + it("returns true when overrides has entries (npm/Bun)", () => { + const dir = makeTempDir(); + writePackageJson(dir, { overrides: { "lodash": "4.17.21" } }); + expect(hasOverrideEntries(dir)).toBe(true); + }); + + it("returns true when pnpm.overrides has entries", () => { + const dir = makeTempDir(); + writePackageJson(dir, { pnpm: { overrides: { "semver": "7.6.0" } } }); + expect(hasOverrideEntries(dir)).toBe(true); + }); + + it("returns true when resolutions has entries (Yarn)", () => { + const dir = makeTempDir(); + writePackageJson(dir, { resolutions: { "minimatch": "3.1.2" } }); + expect(hasOverrideEntries(dir)).toBe(true); + }); + + it("returns false when package.json is malformed JSON", () => { + const dir = makeTempDir(); + fs.writeFileSync(path.join(dir, "package.json"), "not json {"); + expect(hasOverrideEntries(dir)).toBe(false); + }); +}); From 8cdf5026f1e6bb4d647df753b5ba45e1fc7b99a4 Mon Sep 17 00:00:00 2001 From: Sonu Kapoor Date: Fri, 26 Jun 2026 00:33:59 +0200 Subject: [PATCH 2/6] feat: add printOverrideHint to surface discovery tip in terminal output --- src/output/printers.ts | 4 ++++ tests/output.test.ts | 12 ++++++++++++ 2 files changed, 16 insertions(+) diff --git a/src/output/printers.ts b/src/output/printers.ts index 5adcf5b..2495463 100644 --- a/src/output/printers.ts +++ b/src/output/printers.ts @@ -348,6 +348,10 @@ export function printFinalStatus(findings: Finding[], overrideCount = 0) { ); } +export function printOverrideHint(): void { + console.log(chalk.gray("Tip:") + " override entries detected - run with --check-overrides to audit them for stale or broken overrides."); +} + function renderRow(cells: string[], widths: number[]) { const formatted = cells.map((cell, i) => { const truncated = truncate(cell, widths[i]); diff --git a/tests/output.test.ts b/tests/output.test.ts index fa06f9a..48127ba 100644 --- a/tests/output.test.ts +++ b/tests/output.test.ts @@ -22,6 +22,7 @@ import { printActionSummary, printCompactOutput, printFinalStatus, + printOverrideHint, printSuggestedFixCommands, printSuggestedFixCommandSkips, printSummary, @@ -2423,3 +2424,14 @@ describe("printCompactOutput - packageManager option", () => { consoleSpy.mockRestore(); }); }); + +describe("printOverrideHint", () => { + it("prints the hint line", () => { + const consoleSpy = jest.spyOn(console, "log").mockImplementation(() => {}); + printOverrideHint(); + const out = consoleSpy.mock.calls.map(c => stripAnsi(String(c[0]))).join("\n"); + expect(out).toContain("--check-overrides"); + expect(out).toContain("override entries detected"); + consoleSpy.mockRestore(); + }); +}); From 2d762c261d814c23c0d356df1dbdc5c37cb2b325 Mon Sep 17 00:00:00 2001 From: Sonu Kapoor Date: Fri, 26 Jun 2026 00:39:52 +0200 Subject: [PATCH 3/6] feat: wire override hint into verbose and compact output paths Import hasOverrideEntries and printOverrideHint into src/index.ts and call printOverrideHint after printFinalStatus (verbose) and after the renderOverrideFindings block (compact) when override entries are detected but --check-overrides and --ratchet are not active. Update jest.unstable_mockModule factories in cli-integration and multi-folder-printer tests to include printOverrideHint so ESM module linking does not fail on the new named import. --- src/index.ts | 19 +++++++++++++------ tests/cli-integration.test.ts | 2 ++ tests/multi-folder-printer.test.ts | 2 ++ 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/index.ts b/src/index.ts index 7eb713d..c77986d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -44,21 +44,22 @@ import { buildReportData, writeHtmlReport } from "./output/html-reporter.js"; import { writeOutputs } from "./output/write-outputs.js"; import { selectFindingsForTable } from "./output/finding-display.js"; import { - printSummary, printActionSummary, - printSuggestedFixCommands, - printSuggestedFixCommandSkips, + printCompactOutput, printCoverage, + printFinalStatus, + printOverrideHint, printSkippedDependencies, + printSuggestedFixCommands, + printSuggestedFixCommandSkips, + printSummary, printTable, - printFinalStatus, - printCompactOutput } from "./output/printers.js"; import { renderOverrideFindings } from "./output/override-findings-terminal.js"; import { installSkill } from "./skills/install.js"; import { readConfig, validateCaCertFile } from "./cli/config.js"; import { runConfigCommand } from "./cli/config-command.js"; -import { readDirectDependencyNames } from "./utils/package-json.js"; +import { readDirectDependencyNames, hasOverrideEntries } from "./utils/package-json.js"; import { applyFixesIfRequested, FixExecutionResult, @@ -643,6 +644,9 @@ if (parsedArgs) { } printCoverage([...scanInput.notes, ...scanState.coverage]); printFinalStatus(scanState.sorted, overrideCount); + if (!options.checkOverrides && !options.ratchet && hasOverrideEntries(projectPath)) { + printOverrideHint(); + } } else { const compactPmLabel = scanState.suggestedFixCommands ? `${chalk.cyan(scanState.suggestedFixCommands.packageManager)} ${chalk.gray(`(${scanState.suggestedFixCommands.sourceLabel})`)}` @@ -651,6 +655,9 @@ if (parsedArgs) { if (options.checkOverrides) { console.log(renderOverrideFindings(overrideFindings, { verbose: false })); } + if (!options.checkOverrides && !options.ratchet && hasOverrideEntries(projectPath)) { + printOverrideHint(); + } } } } diff --git a/tests/cli-integration.test.ts b/tests/cli-integration.test.ts index 272c46b..def7728 100644 --- a/tests/cli-integration.test.ts +++ b/tests/cli-integration.test.ts @@ -28,6 +28,7 @@ const printSkippedDependenciesMock = jest.fn(); const printTableMock = jest.fn(); const printFinalStatusMock = jest.fn(); const printCompactOutputMock = jest.fn(); +const printOverrideHintMock = jest.fn(); const buildSuggestedFixCommandPlanMock = jest.fn(); const spawnMock = jest.fn(); const buildReportDataMock = jest.fn(); @@ -101,6 +102,7 @@ jest.unstable_mockModule("../src/output/printers.js", () => ({ printTable: printTableMock, printFinalStatus: printFinalStatusMock, printCompactOutput: printCompactOutputMock, + printOverrideHint: printOverrideHintMock, })); jest.unstable_mockModule("../src/remediation/fix-commands.js", () => ({ diff --git a/tests/multi-folder-printer.test.ts b/tests/multi-folder-printer.test.ts index 3f49361..52ce65e 100644 --- a/tests/multi-folder-printer.test.ts +++ b/tests/multi-folder-printer.test.ts @@ -9,6 +9,7 @@ const printSkippedDependenciesMock = jest.fn(); const printTableMock = jest.fn(); const printFinalStatusMock = jest.fn(); const printCompactOutputMock = jest.fn(); +const printOverrideHintMock = jest.fn(); const logInfoMock = jest.fn(); const logWarnMock = jest.fn(); @@ -22,6 +23,7 @@ jest.unstable_mockModule("../src/output/printers.js", () => ({ printTable: printTableMock, printFinalStatus: printFinalStatusMock, printCompactOutput: printCompactOutputMock, + printOverrideHint: printOverrideHintMock, })); jest.unstable_mockModule("../src/output/formatters.js", () => ({ From ea12f05cb3e694e6e32ca40486e4a22d37f95340 Mon Sep 17 00:00:00 2001 From: Sonu Kapoor Date: Fri, 26 Jun 2026 00:43:40 +0200 Subject: [PATCH 4/6] feat: wire override hint into multi-folder scan output path When running in multi-folder mode, print the override hygiene hint once if any subfolder has override entries and --check-overrides / --ratchet are not active. Mocks for hasOverrideEntries and printOverrideHint added to the multi-folder-scan test. --- src/scan/multi-folder-scan.ts | 12 +++++++++++- tests/multi-folder-scan.test.ts | 7 +++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/scan/multi-folder-scan.ts b/src/scan/multi-folder-scan.ts index c3ae2b5..ba5f0cf 100644 --- a/src/scan/multi-folder-scan.ts +++ b/src/scan/multi-folder-scan.ts @@ -9,9 +9,10 @@ import { sortFindingsForOutput } from "../output/formatters.js"; import { normalizeSeverity } from "../osv/severity.js"; import { selectFindingsForTable } from "../output/finding-display.js"; import { buildSuggestedFixCommandPlan } from "../remediation/fix-commands.js"; -import { readDirectDependencyNames } from "../utils/package-json.js"; +import { readDirectDependencyNames, hasOverrideEntries } from "../utils/package-json.js"; import { DEFAULT_BATCH_SIZE, DEFAULT_SEARCH_DEPTH, severityOrder } from "../constants.js"; import { printMultiFolderResults } from "../output/multi-folder-printer.js"; +import { printOverrideHint } from "../output/printers.js"; import { writeMultiFolderHtmlReport } from "../output/multi-folder-html-reporter.js"; import { chalk } from "../utils/chalk.js"; import { getCliVersion } from "../utils/version-info.js"; @@ -143,6 +144,15 @@ export async function handleMultiFolderScan(params: { console.log(renderOverrideFindings(r.overrideFindings, { verbose: !!params.options.verbose })); } } + if (!params.options.checkOverrides && !params.options.ratchet) { + for (const r of results) { + const subfolderPath = path.join(params.projectRoot, r.subfolder); + if (hasOverrideEntries(subfolderPath)) { + printOverrideHint(); + break; // one hint is enough even if multiple subfolders have overrides + } + } + } } if (params.options.report) { diff --git a/tests/multi-folder-scan.test.ts b/tests/multi-folder-scan.test.ts index e57ad3d..2641159 100644 --- a/tests/multi-folder-scan.test.ts +++ b/tests/multi-folder-scan.test.ts @@ -42,8 +42,11 @@ jest.unstable_mockModule("../src/remediation/fix-commands.js", () => ({ findSuggestedCommandForFinding: jest.fn(() => null), })); +const hasOverrideEntriesMock = jest.fn(() => false); + jest.unstable_mockModule("../src/utils/package-json.js", () => ({ readDirectDependencyNames: readDirectDependencyNamesMock, + hasOverrideEntries: hasOverrideEntriesMock, })); jest.unstable_mockModule("../src/overrides/index.js", () => ({ @@ -60,6 +63,10 @@ jest.unstable_mockModule("../src/output/multi-folder-printer.js", () => ({ printMultiFolderResults: jest.fn(), })); +jest.unstable_mockModule("../src/output/printers.js", () => ({ + printOverrideHint: jest.fn(), +})); + jest.unstable_mockModule("../src/output/multi-folder-html-reporter.js", () => ({ writeMultiFolderHtmlReport: jest.fn(() => Promise.resolve({ reportPath: "/tmp/report/index.html" })), })); From edff9f7b578333906ce1b4a4df8b2f3fc981f858 Mon Sep 17 00:00:00 2001 From: Sonu Kapoor Date: Fri, 26 Jun 2026 00:47:26 +0200 Subject: [PATCH 5/6] refactor: hoist hasOverrideEntries call to avoid reading package.json twice --- src/index.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/index.ts b/src/index.ts index c77986d..c778e3f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -617,6 +617,7 @@ if (parsedArgs) { if (!(options.json || options.sarif || options.cdx) || options.verbose) { const offline = !!options.offline || !!options.offlineDb; + const showOverrideHint = !options.checkOverrides && !options.ratchet && hasOverrideEntries(projectPath); if (options.verbose) { const overrideCount = options.checkOverrides ? overrideFindings.length : 0; printSummary(scanState.sorted, packages.length, scanInput); @@ -644,9 +645,7 @@ if (parsedArgs) { } printCoverage([...scanInput.notes, ...scanState.coverage]); printFinalStatus(scanState.sorted, overrideCount); - if (!options.checkOverrides && !options.ratchet && hasOverrideEntries(projectPath)) { - printOverrideHint(); - } + if (showOverrideHint) printOverrideHint(); } else { const compactPmLabel = scanState.suggestedFixCommands ? `${chalk.cyan(scanState.suggestedFixCommands.packageManager)} ${chalk.gray(`(${scanState.suggestedFixCommands.sourceLabel})`)}` @@ -655,9 +654,7 @@ if (parsedArgs) { if (options.checkOverrides) { console.log(renderOverrideFindings(overrideFindings, { verbose: false })); } - if (!options.checkOverrides && !options.ratchet && hasOverrideEntries(projectPath)) { - printOverrideHint(); - } + if (showOverrideHint) printOverrideHint(); } } } From 74d65f8193b2a2e6e52a786be094121c33c3f802 Mon Sep 17 00:00:00 2001 From: Sonu Kapoor Date: Fri, 26 Jun 2026 00:48:52 +0200 Subject: [PATCH 6/6] feat: add bulb icon to override hint tip line --- src/output/printers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/output/printers.ts b/src/output/printers.ts index 2495463..ab63bfd 100644 --- a/src/output/printers.ts +++ b/src/output/printers.ts @@ -349,7 +349,7 @@ export function printFinalStatus(findings: Finding[], overrideCount = 0) { } export function printOverrideHint(): void { - console.log(chalk.gray("Tip:") + " override entries detected - run with --check-overrides to audit them for stale or broken overrides."); + console.log("💡 " + chalk.gray("Tip:") + " override entries detected - run with --check-overrides to audit them for stale or broken overrides."); } function renderRow(cells: string[], widths: number[]) {