dependency-radar 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,7 +4,7 @@ Dependency Radar inspects your Node.js dependency graph and makes structural ris
4
4
 
5
5
  Unlike basic audit tools, it builds the graph from lockfiles, understands PNPM workspaces, validates declared vs inferred licences, and highlights structural risks before they become production problems.
6
6
 
7
- No accounts. No uploads. Runs entirely on your machine.
7
+ No accounts. No uploads. Nothing leaves your machine.
8
8
 
9
9
  The simplest way to get started is to go to your project root and run:
10
10
 
@@ -27,7 +27,7 @@ This runs a scan against the current project and writes a self-contained `depend
27
27
  ## What you get
28
28
 
29
29
  - **Vulnerability scanning** — runs `npm audit` / `pnpm audit` / `yarn audit` and surfaces advisories with severity, fix availability, and reachability heuristics
30
- - **Licence analysis** — validates SPDX declarations, infers licences from `LICENSE` files, and flags mismatches, unknown licences, and strong copyleft
30
+ - **License analysis** — validates SPDX declarations, infers licences from `LICENSE` files, and flags mismatches, unknown licences, and strong copyleft
31
31
  - **Interactive dependency graph** — explore your full dependency tree visually, including direct, dev, and transitive relationships
32
32
  - **Upgrade friction analysis** — identifies upgrade blockers: peer constraints, engine ranges, native bindings, install scripts, deprecated packages
33
33
  - **Import usage heuristics** — classifies each dependency's runtime impact (`runtime`, `build`, `testing`, `tooling`, `mixed`) based on where it's imported in your source
@@ -57,7 +57,7 @@ This runs a scan against the current project and writes a self-contained `depend
57
57
  Modern Node projects pull in hundreds (or thousands) of transitive dependencies, and most of the risk is structural, not obvious.
58
58
 
59
59
  - `npm audit` tells you about known vulnerabilities, but it does not explain how a dependency got there, whether it is reachable at runtime, or how deep it sits in your graph.
60
- - Licence tooling often trusts `package.json` declarations, even though they can be missing, invalid, or wrong, and rarely checks what is actually in the installed `LICENSE` file.
60
+ - License tooling often trusts `package.json` declarations, even though they can be missing, invalid, or wrong, and rarely checks what is actually in the installed `LICENSE` file.
61
61
  - Monorepos and PNPM workspaces make the tree harder to reason about, especially when package manager outputs include optional platform variants that are not installed on your machine.
62
62
  - Upgrade pain usually shows up late, when a Node major bump or a package update breaks due to peer dependency constraints, engine ranges, native bindings, or install scripts.
63
63
 
@@ -92,6 +92,7 @@ The `scan` command is the default and can also be run explicitly as `npx depende
92
92
  | Flag | Description |
93
93
  |---|---|
94
94
  | `--project <path>` | Path to the project to scan (defaults to current directory) |
95
+ | `--quiet` | Suppress progress/info logs, browser opening, and footer messaging while keeping the final summary and failures visible |
95
96
  | `--out <path>` | Output path for the report file |
96
97
  | `--offline` | Skip `npm audit` and `npm outdated` (useful for offline/air-gapped scans) |
97
98
  | `--json` | Output JSON instead of HTML (`dependency-radar.json`) |
@@ -101,6 +102,49 @@ The `scan` command is the default and can also be run explicitly as `npx depende
101
102
  | `--fail-on <rules>` | Fail with exit code 1 when selected policy rules are violated (see below) |
102
103
  | `--help` | Show all options |
103
104
 
105
+ ### Explain one dependency in the terminal
106
+
107
+ Use `explain` when you want a fast terminal view for one package without generating HTML or JSON output:
108
+
109
+ ```bash
110
+ npx dependency-radar explain lodash
111
+ ```
112
+
113
+ This reuses the normal scan model and then filters it in memory. `explain` does not add its own extra lookup pipeline and does not write `dependency-radar.html`, but it can still trigger the same network-dependent `audit` and `outdated` steps as a normal scan unless you pass `--offline`.
114
+
115
+ `explain` shows the signals already present in Dependency Radar's scan model, including:
116
+
117
+ - direct vs transitive
118
+ - scope and introduction classification
119
+ - runtime impact heuristics
120
+ - root packages and direct parents
121
+ - static import evidence and top import locations
122
+ - vulnerability summary when audit data is available
123
+ - licence status
124
+ - upgrade blockers
125
+ - other detected versions of the same package
126
+
127
+ Examples:
128
+
129
+ ```bash
130
+ npx dependency-radar explain lodash
131
+ ```
132
+
133
+ ```bash
134
+ npx dependency-radar explain lodash --project ./my-app
135
+ ```
136
+
137
+ ```bash
138
+ npx dependency-radar explain lodash --project ./my-app --offline
139
+ ```
140
+
141
+ Notes:
142
+
143
+ - `explain` matches by package name only. If multiple installed versions exist, each version is shown in its own block.
144
+ - Vulnerabilities are reported only when audit data is available. With `--offline`, the command prints `not available (--offline)` instead of implying `none`.
145
+ - "Static import evidence" means Dependency Radar found local source imports for that package. It is a code-usage heuristic, not exploit reachability analysis.
146
+ - "Introduced via root packages" and "Direct parents" are shown from the current scan model. The command does not currently print full ancestry chains.
147
+
104
148
  ### CI policy enforcement (`--fail-on`)
105
149
 
106
150
  ```
@@ -149,6 +193,20 @@ npx dependency-radar --offline
149
193
  npx dependency-radar --no-report --fail-on reachable-vuln,licence-mismatch
150
194
  ```
151
195
 
196
+ ### Example: quiet mode for CI or scripting
197
+
198
+ ```bash
199
+ npx dependency-radar scan --quiet --no-report
200
+ ```
201
+
202
+ `--quiet` is quiet, not silent:
203
+
204
+ - the scan still runs fully
205
+ - reports are still generated unless `--no-report` is set
206
+ - the final summary block is still printed
207
+ - policy failures are still printed
208
+ - progress/info logs, automatic browser opening, and the promotional footer are suppressed
209
+
152
210
  __Note:__ When used with `--no-report`, the `--keep-temp` flag is ignored.
153
211
  Temporary files are normally deleted automatically.
154
212
  If you intentionally use `--keep-temp` (without `--no-report`) for debugging,
@@ -160,16 +218,16 @@ At the end of each scan, the CLI prints a summary block with high-level counts,
160
218
 
161
219
  ```text
162
220
  Summary:
163
- • Direct deps scanned: 8
164
- • Transitive deps scanned: 65
165
- • Vulnerable packages: 5 (1 reachable)
166
- Unused installed deps: 0
167
- Licence mismatches: 3
168
- • Major upgrade blockers: 28
169
- - 14 strict peer dependency constraints
170
- - 6 narrow engine ranges
171
- - 4 deprecated packages
172
- - 4 native bindings
221
+ • Direct dependencies scanned: 6
222
+ • Transitive dependencies scanned: 62
223
+ • Vulnerable packages: 1 (0 reachable)
224
+ Dependencies with no static import reference: 0
225
+ License mismatches: 3
226
+ • Major upgrade blockers: 24
227
+ - 1 strict peer dependency constraint
228
+ - 22 narrow engine ranges
229
+ - 2 native bindings
230
+ - 1 install lifecycle script
173
231
  ```
174
232
 
175
233
  The blocker detail counts can overlap: a single package may contribute to multiple blocker categories.
@@ -187,7 +245,7 @@ The blocker detail counts can overlap: a single package may contribute to multip
187
245
 
188
246
  ## Requirements
189
247
 
190
- - Node.js 14.14+
248
+ - Node.js 14.21.3 is currently the oldest version verified by our Docker release smoke test (`node:14.21.3-bullseye`)
191
249
  - Dependencies must be installed (`npm install` / `pnpm install` / `yarn install`) before scanning
192
250
 
193
251
  ## How a scan works
@@ -227,6 +285,8 @@ When you run `npx dependency-radar` (or `dependency-radar scan`), the CLI execut
227
285
 
228
286
  The scan is local-first: package metadata is read from `node_modules`; only audit/outdated commands require registry access.
229
287
 
288
+ The `explain` command reuses this same pipeline with report writing disabled, then filters the in-memory model down to a single package for terminal output.
289
+
230
290
  ### `node_modules` crawling details
231
291
 
232
292
  - Dependency metadata is read from installed package directories, not from registry documents.
@@ -536,9 +596,6 @@ export interface DependencyRecord {
536
596
 
537
597
  For full details and any future changes, see `src/types.ts`.
538
598
 
539
- Environment data includes Node.js version, OS platform, CPU architecture, and package manager versions.
540
- No personal information, usernames, paths, or environment variables are collected.
541
-
542
599
  ## Notes
543
600
 
544
601
  - The target project must have dependencies installed (run `npm install`, `pnpm install`, or `yarn install` first).
@@ -548,6 +605,8 @@ No personal information, usernames, paths, or environment variables are collecte
548
605
  - A temporary `.dependency-radar/` folder is created during the scan to store intermediate tool output.
549
606
  - Use `--keep-temp` to retain this folder for debugging; otherwise it is deleted automatically.
550
607
  - If some per-package tools fail (common in large workspaces), the scan continues and reports warnings; missing sections are marked unavailable where applicable.
608
+ - Environment data includes Node.js version, OS platform, CPU architecture, and package manager versions.
609
+ - No personal information, usernames, paths, or environment variables are collected.
551
610
 
552
611
  ---
553
612
 
@@ -576,11 +635,15 @@ npm run build
576
635
  | `npm run test:fixtures` | Run curated fixture integration tests (mostly offline scans) |
577
636
  | `npm run test:fixtures:online` | Run online fixture checks (audit/outdated regression coverage) |
578
637
  | `npm run test:fixtures:all` | Run all fixture integration tests |
579
- | `npm run test:release` | Full pre-release gate (`build` + unit + fixture + package dry run) |
638
+ | `npm run test:docker:node14` | Pack the published artifact and smoke-test it in Docker on Node `14.21.3` |
639
+ | `npm run test:docker` | Alias for the Node `14.21.3` Docker compatibility smoke test |
640
+ | `npm run test:release` | Full pre-release gate (`build` + unit + fixture + Docker Node 14 smoke test + package dry run) |
580
641
 
581
642
 
582
643
  Fixture orchestration lives in `/test-fixtures/package.json` with helper scripts under `/test-fixtures/scripts`.
583
644
 
645
+ The Docker smoke test uses the packed tarball, installs it inside `node:14.21.3-bullseye`, and runs an offline scan against `test-fixtures/license-edge-cases`. This verifies the published CLI on the oldest Node version we currently exercise in automation without requiring local Node 14 installation.
646
+
584
647
  ### Report UI Development
585
648
 
586
649
  The HTML report UI is developed in a separate Vite project located in `report-ui/`. This provides a proper development environment with hot reload, TypeScript support, and sample data.
package/dist/cli.js CHANGED
@@ -8,6 +8,7 @@ const path_1 = __importDefault(require("path"));
8
8
  const child_process_1 = require("child_process");
9
9
  const os_1 = require("os");
10
10
  const aggregator_1 = require("./aggregator");
11
+ const explain_1 = require("./explain");
11
12
  const importGraphRunner_1 = require("./runners/importGraphRunner");
12
13
  const npmAudit_1 = require("./runners/npmAudit");
13
14
  const npmLs_1 = require("./runners/npmLs");
@@ -923,6 +924,7 @@ function parseArgs(argv) {
923
924
  const opts = {
924
925
  command: "scan",
925
926
  project: process.cwd(),
927
+ quiet: false,
926
928
  out: "dependency-radar.html",
927
929
  keepTemp: false,
928
930
  audit: true,
@@ -934,14 +936,26 @@ function parseArgs(argv) {
934
936
  };
935
937
  const args = [...argv];
936
938
  if (args[0] && !args[0].startsWith("-")) {
937
- opts.command = args.shift();
939
+ const command = args.shift();
940
+ if (command === "scan" || command === "explain") {
941
+ opts.command = command;
942
+ }
943
+ else {
944
+ opts.invalidCommand = command;
945
+ return opts;
946
+ }
938
947
  }
939
948
  while (args.length) {
940
949
  const arg = args.shift();
941
950
  if (!arg)
942
951
  break;
943
- if (arg === "--project" && args[0])
952
+ if (!arg.startsWith("-") && opts.command === "explain" && !opts.packageName) {
953
+ opts.packageName = arg;
954
+ }
955
+ else if (arg === "--project" && args[0])
944
956
  opts.project = args.shift();
957
+ else if (arg === "--quiet")
958
+ opts.quiet = true;
945
959
  else if (arg === "--out" && args[0])
946
960
  opts.out = args.shift();
947
961
  else if (arg === "--keep-temp")
@@ -990,11 +1004,13 @@ function parseArgs(argv) {
990
1004
  */
991
1005
  function printHelp() {
992
1006
  console.log(`dependency-radar [scan] [options]
1007
+ dependency-radar explain <package-name> [options]
993
1008
 
994
1009
  If no command is provided, \`scan\` is run by default.
995
1010
 
996
1011
  Options:
997
1012
  --project <path> Project folder (default: cwd)
1013
+ --quiet Suppress progress/info logs but keep summary and failures
998
1014
  --out <path> Output HTML file (default: dependency-radar.html)
999
1015
  --json Write aggregated data to JSON (default filename: dependency-radar.json)
1000
1016
  --no-report Do not write HTML/JSON report files or temp artifacts to disk
@@ -1004,6 +1020,8 @@ Options:
1004
1020
  --fail-on <rules> Fail with exit code 1 when selected rules are violated
1005
1021
  Supported: reachable-vuln, production-vuln, high-severity-vuln,
1006
1022
  licence-mismatch, copyleft-detected, unknown-licence
1023
+
1024
+ \`explain\` reuses the same local scan model and prints a terminal view for one package.
1007
1025
  `);
1008
1026
  }
1009
1027
  /**
@@ -1070,6 +1088,20 @@ function shouldUseColor() {
1070
1088
  return Boolean(process.stdout.isTTY);
1071
1089
  }
1072
1090
  const COLOR_ENABLED = shouldUseColor();
1091
+ function supportsTerminalHyperlinks() {
1092
+ if (!process.stdout.isTTY)
1093
+ return false;
1094
+ if (process.env.NO_COLOR !== undefined)
1095
+ return false;
1096
+ if (process.env.TERM === "dumb")
1097
+ return false;
1098
+ return true;
1099
+ }
1100
+ function formatTerminalLink(label, url) {
1101
+ if (!supportsTerminalHyperlinks())
1102
+ return label;
1103
+ return `\u001B]8;;${url}\u0007${label}\u001B]8;;\u0007`;
1104
+ }
1073
1105
  /**
1074
1106
  * Wraps text with ANSI color or style escape sequences when terminal coloring is enabled.
1075
1107
  *
@@ -1270,10 +1302,10 @@ function printCliSummary(summary) {
1270
1302
  const bullet = "•";
1271
1303
  console.log("");
1272
1304
  console.log("Summary:");
1273
- console.log(`${bullet} Direct deps scanned: ${summary.directDeps}`);
1274
- console.log(`${bullet} Transitive deps scanned: ${summary.transitiveDeps}`);
1305
+ console.log(`${bullet} Direct dependencies scanned: ${summary.directDeps}`);
1306
+ console.log(`${bullet} Transitive dependencies scanned: ${summary.transitiveDeps}`);
1275
1307
  console.log(`${bullet} Vulnerable packages: ${summary.vulnerablePackages} (${summary.reachableVulnerablePackages} reachable)`);
1276
- console.log(`${bullet} Unused installed deps: ${summary.unusedInstalledDeps}`);
1308
+ console.log(`${bullet} Dependencies with no static import reference: ${summary.unusedInstalledDeps}`);
1277
1309
  console.log(`${bullet} License mismatches: ${summary.licenseMismatches}`);
1278
1310
  console.log(`${bullet} Major upgrade blockers: ${summary.majorUpgradeBlockers}`);
1279
1311
  const blockerDetails = [];
@@ -1297,36 +1329,21 @@ function printCliSummary(summary) {
1297
1329
  }
1298
1330
  console.log("");
1299
1331
  }
1300
- /**
1301
- * Run the CLI "scan" command to collect and aggregate dependency data for a project or workspace.
1302
- *
1303
- * Detects workspace type and package manager, runs per-package collectors (audit, dependency tree, import graph, outdated),
1304
- * merges collected signals into a workspace-level model, and writes a JSON or HTML report according to CLI options.
1305
- * Manages a temporary working directory and optionally opens the generated report. Exits the process with a non-zero code
1306
- * on fatal errors or when configured policy violations are detected.
1307
- */
1308
- async function run() {
1332
+ async function executeAnalysis(opts, options) {
1309
1333
  var _a;
1310
- const opts = parseArgs(process.argv.slice(2));
1311
- if (opts.command !== "scan") {
1312
- printHelp();
1313
- process.exit(1);
1314
- return;
1315
- }
1316
- const shouldWriteArtifacts = !opts.noReport;
1334
+ const shouldWriteArtifacts = options.shouldWriteArtifacts;
1317
1335
  const projectPath = path_1.default.resolve(opts.project);
1318
- let summary;
1319
- let policyViolations = [];
1320
- if (opts.noReport && opts.keepTemp) {
1336
+ let outputPath = path_1.default.resolve(opts.out);
1337
+ const startTime = Date.now();
1338
+ let dependencyCount = 0;
1339
+ let outputCreated = false;
1340
+ if (opts.command === "scan" && opts.noReport && opts.keepTemp && !opts.quiet) {
1321
1341
  console.log(statusLine("⚠", "--keep-temp is ignored when --no-report is enabled."));
1322
1342
  }
1323
1343
  if (opts.json && opts.out === "dependency-radar.html") {
1324
1344
  opts.out = "dependency-radar.json";
1345
+ outputPath = path_1.default.resolve(opts.out);
1325
1346
  }
1326
- let outputPath = path_1.default.resolve(opts.out);
1327
- const startTime = Date.now();
1328
- let dependencyCount = 0;
1329
- let outputCreated = false;
1330
1347
  if (shouldWriteArtifacts) {
1331
1348
  try {
1332
1349
  const stat = await promises_1.default.stat(outputPath).catch(() => undefined);
@@ -1338,19 +1355,17 @@ async function run() {
1338
1355
  outputPath = path_1.default.join(outputPath, opts.json ? "dependency-radar.json" : "dependency-radar.html");
1339
1356
  }
1340
1357
  }
1341
- catch (e) {
1358
+ catch {
1342
1359
  // ignore, best-effort path normalization
1343
1360
  }
1344
1361
  }
1345
1362
  const tempDir = path_1.default.join(projectPath, ".dependency-radar");
1346
- // Stage 1: detect workspace/package-manager context and collect tool versions.
1347
1363
  const workspace = await detectWorkspace(projectPath);
1348
1364
  const yarnPnP = await detectYarnPnP(projectPath);
1349
1365
  if (workspace.type === "yarn" && workspace.packagePaths.length === 0) {
1350
1366
  console.error("Yarn Plug'n'Play (nodeLinker: pnp) detected. This is not supported yet.");
1351
1367
  console.error("Switch to nodeLinker: node-modules or run in a non-PnP environment.");
1352
1368
  process.exit(1);
1353
- return;
1354
1369
  }
1355
1370
  const hasProjectNodeModules = await (0, utils_1.pathExists)(path_1.default.join(projectPath, "node_modules"));
1356
1371
  if (!hasProjectNodeModules) {
@@ -1393,22 +1408,22 @@ async function run() {
1393
1408
  console.error("Yarn Plug'n'Play (nodeLinker: pnp) detected. This is not supported yet.");
1394
1409
  console.error("Switch to nodeLinker: node-modules or run in a non-PnP environment.");
1395
1410
  process.exit(1);
1396
- return;
1397
1411
  }
1398
1412
  const packagePaths = workspace.packagePaths;
1399
1413
  const workspaceLabel = workspace.type === "none"
1400
1414
  ? "Single project"
1401
1415
  : `${workspace.type.toUpperCase()} workspace`;
1402
- console.log(statusLine("✔", `${workspaceLabel} detected`));
1403
- if (workspace.type !== "none" && scanManager !== workspace.type) {
1416
+ if (!opts.quiet) {
1417
+ console.log(statusLine("", `${workspaceLabel} detected`));
1418
+ }
1419
+ if (!opts.quiet && workspace.type !== "none" && scanManager !== workspace.type) {
1404
1420
  console.log(statusLine("✔", `Using ${scanManager.toUpperCase()} for dependency data (lockfile detected)`));
1405
1421
  }
1406
- const spinner = startSpinner(`Scanning ${workspaceLabel} at ${projectPath}`);
1422
+ const spinner = createProgressReporter(`Scanning ${workspaceLabel} at ${projectPath}`, opts.quiet);
1407
1423
  try {
1408
1424
  if (shouldWriteArtifacts) {
1409
1425
  await (0, utils_1.ensureDir)(tempDir);
1410
1426
  }
1411
- // Stage 2: run per-package collectors and persist raw tool outputs.
1412
1427
  const packageMetas = await readWorkspacePackageMeta(projectPath, packagePaths);
1413
1428
  const workspaceClassification = buildWorkspaceClassification(projectPath, packageMetas);
1414
1429
  const perPackageAudit = [];
@@ -1443,23 +1458,16 @@ async function run() {
1443
1458
  perPackageImportGraph.push(ig);
1444
1459
  perPackageOutdated.push({ attempted: Boolean(opts.outdated), result: o });
1445
1460
  }
1446
- // Stage 3: merge per-package results into a workspace-level view.
1447
1461
  if (opts.audit) {
1448
1462
  const auditOk = perPackageAudit.every((r) => r && r.ok);
1449
- if (auditOk) {
1450
- spinner.log(statusLine("✔", `${scanManager.toUpperCase()} audit data collected`));
1451
- }
1452
- else {
1453
- spinner.log(statusLine("✖", `${scanManager.toUpperCase()} audit data unavailable`));
1463
+ if (!opts.quiet || !auditOk) {
1464
+ spinner.log(statusLine(auditOk ? "✔" : "✖", `${scanManager.toUpperCase()} audit data ${auditOk ? "collected" : "unavailable"}`));
1454
1465
  }
1455
1466
  }
1456
1467
  if (opts.outdated) {
1457
1468
  const outdatedOk = perPackageOutdated.every((r) => r.result && r.result.ok);
1458
- if (outdatedOk) {
1459
- spinner.log(statusLine("✔", `${scanManager.toUpperCase()} outdated data collected`));
1460
- }
1461
- else {
1462
- spinner.log(statusLine("✖", `${scanManager.toUpperCase()} outdated data unavailable`));
1469
+ if (!opts.quiet || !outdatedOk) {
1470
+ spinner.log(statusLine(outdatedOk ? "✔" : "✖", `${scanManager.toUpperCase()} outdated data ${outdatedOk ? "collected" : "unavailable"}`));
1463
1471
  }
1464
1472
  }
1465
1473
  const mergedAuditData = mergeAuditResults(perPackageAudit.map((r) => (r && r.ok ? r.data : undefined)));
@@ -1476,7 +1484,6 @@ async function run() {
1476
1484
  : undefined;
1477
1485
  const npmLsResult = { ok: true, data: mergedGraphData };
1478
1486
  const importGraphResult = { ok: true, data: mergedImportGraphData };
1479
- // Build a merged package.json view for aggregator direct-dep checks.
1480
1487
  const mergedPkgForAggregator = mergeDepsFromWorkspace(packageMetas, workspaceClassification.workspacePackageNames, workspaceClassification.localDependencyNames);
1481
1488
  const auditFailure = opts.audit
1482
1489
  ? perPackageAudit.find((r) => r && !r.ok)
@@ -1496,7 +1503,6 @@ async function run() {
1496
1503
  if (importFailures.length > 0) {
1497
1504
  spinner.log(`Import graph warning: ${importFailures.length} package${importFailures.length === 1 ? "" : "s"} failed (${importFailures[0].error || "import graph failed"})`);
1498
1505
  }
1499
- // Stage 4: aggregate all signals into the final report model.
1500
1506
  const aggregated = await (0, aggregator_1.aggregateData)({
1501
1507
  projectPath,
1502
1508
  auditResult,
@@ -1533,11 +1539,11 @@ async function run() {
1533
1539
  });
1534
1540
  dependencyCount = Object.keys(aggregated.dependencies).length;
1535
1541
  const importGraphComplete = perPackageImportGraph.every((result) => result.ok);
1536
- summary = buildCliSummary(aggregated, {
1542
+ const summary = buildCliSummary(aggregated, {
1537
1543
  importGraphComplete,
1538
1544
  });
1539
- policyViolations = (0, failOn_1.evaluatePolicyViolations)(aggregated, opts.failOn);
1540
- if (workspace.type !== "none") {
1545
+ const policyViolations = (0, failOn_1.evaluatePolicyViolations)(aggregated, opts.failOn);
1546
+ if (!opts.quiet && options.emitWorkspacePackageSummary && workspace.type !== "none") {
1541
1547
  console.log(`Detected ${workspace.type.toUpperCase()} workspace with ${packagePaths.length} package${packagePaths.length === 1 ? "" : "s"}.`);
1542
1548
  }
1543
1549
  if (dependencyCount > 0 && shouldWriteArtifacts) {
@@ -1551,57 +1557,141 @@ async function run() {
1551
1557
  outputCreated = true;
1552
1558
  }
1553
1559
  spinner.stop(true);
1554
- const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
1555
- console.log(statusLine("✔", `Scan complete: ${dependencyCount} dependencies analysed in ${elapsed}s`));
1556
- if (!shouldWriteArtifacts) {
1557
- console.log(statusLine("ℹ", "Report output disabled (--no-report); no report artifacts written."));
1560
+ const elapsedSeconds = ((Date.now() - startTime) / 1000).toFixed(1);
1561
+ if (!opts.quiet) {
1562
+ console.log(statusLine("✔", `Scan complete: ${dependencyCount} dependencies analysed in ${elapsedSeconds}s`));
1558
1563
  }
1559
- else if (outputCreated) {
1560
- console.log(statusLine("✔", `${opts.json ? "JSON" : "Report"} written to ${outputPath}`));
1561
- }
1562
- else {
1563
- console.log(statusLine("✖", `No dependencies were found - ${opts.json ? "JSON file" : "Report"} not created`));
1564
+ if (!opts.quiet && options.emitArtifactSummary) {
1565
+ if (!shouldWriteArtifacts) {
1566
+ console.log(statusLine("ℹ", "Report output disabled (--no-report); no report artifacts written."));
1567
+ }
1568
+ else if (outputCreated) {
1569
+ console.log(statusLine("✔", `${opts.json ? "JSON" : "Report"} written to ${outputPath}`));
1570
+ }
1571
+ else {
1572
+ console.log(statusLine("✖", `No dependencies were found - ${opts.json ? "JSON file" : "Report"} not created`));
1573
+ }
1564
1574
  }
1575
+ return {
1576
+ aggregated,
1577
+ summary,
1578
+ policyViolations,
1579
+ dependencyCount,
1580
+ elapsedSeconds,
1581
+ outputCreated,
1582
+ outputPath,
1583
+ shouldWriteArtifacts,
1584
+ collectorAvailability: {
1585
+ audit: !opts.audit
1586
+ ? "skipped"
1587
+ : perPackageAudit.every((result) => result && result.ok)
1588
+ ? "available"
1589
+ : "unavailable",
1590
+ importGraphComplete,
1591
+ },
1592
+ workspace,
1593
+ packagePaths,
1594
+ };
1565
1595
  }
1566
1596
  catch (err) {
1567
1597
  spinner.stop(false);
1568
- console.error("Failed to generate report:", err);
1569
- process.exit(1);
1598
+ throw err;
1570
1599
  }
1571
1600
  finally {
1572
1601
  if (shouldWriteArtifacts) {
1573
1602
  if (!opts.keepTemp) {
1574
1603
  await (0, utils_1.removeDir)(tempDir);
1575
1604
  }
1576
- else {
1605
+ else if (!opts.quiet) {
1577
1606
  console.log(statusLine("✔", `Temporary data kept at ${tempDir}`));
1578
1607
  }
1579
1608
  }
1580
1609
  }
1581
- if (opts.open && !shouldWriteArtifacts) {
1582
- console.log(statusLine("✖", "Skipping auto-open because --no-report is enabled."));
1610
+ }
1611
+ async function runScanCommand(opts) {
1612
+ const result = await executeAnalysis(opts, {
1613
+ shouldWriteArtifacts: !opts.noReport,
1614
+ emitArtifactSummary: true,
1615
+ emitWorkspacePackageSummary: true,
1616
+ });
1617
+ if (!opts.quiet) {
1618
+ if (opts.open && !result.shouldWriteArtifacts) {
1619
+ console.log(statusLine("✖", "Skipping auto-open because --no-report is enabled."));
1620
+ }
1621
+ else if (opts.open && result.outputCreated && !isCI()) {
1622
+ console.log(statusLine("↗", `Opening ${path_1.default.basename(result.outputPath)} using system default ${opts.json ? "application" : "browser"}.`));
1623
+ openInBrowser(result.outputPath);
1624
+ }
1625
+ else if (opts.open && result.outputCreated && isCI()) {
1626
+ console.log(statusLine("✖", "Skipping auto-open in CI environment."));
1627
+ }
1628
+ }
1629
+ printCliSummary(result.summary);
1630
+ printPolicyViolations(result.policyViolations);
1631
+ if (!opts.quiet) {
1632
+ console.log(`Enrich this scan with maintenance signals, upgrade readiness, and risk modelling at ${formatTerminalLink("https://www.dependency-radar.com", "https://www.dependency-radar.com")}`);
1633
+ }
1634
+ if (result.policyViolations.length > 0) {
1635
+ process.exit(1);
1583
1636
  }
1584
- else if (opts.open && outputCreated && !isCI()) {
1585
- console.log(statusLine("↗", `Opening ${path_1.default.basename(outputPath)} using system default ${opts.json ? "application" : "browser"}.`));
1586
- openInBrowser(outputPath);
1637
+ }
1638
+ async function runExplainCommand(opts) {
1639
+ var _a;
1640
+ const packageName = (_a = opts.packageName) === null || _a === void 0 ? void 0 : _a.trim();
1641
+ if (!packageName) {
1642
+ console.error("Missing package name for explain. Usage: dependency-radar explain <package-name>");
1643
+ process.exit(1);
1644
+ return;
1587
1645
  }
1588
- else if (opts.open && outputCreated && isCI()) {
1589
- console.log(statusLine("✖", "Skipping auto-open in CI environment."));
1646
+ const result = await executeAnalysis(opts, {
1647
+ shouldWriteArtifacts: false,
1648
+ emitArtifactSummary: false,
1649
+ emitWorkspacePackageSummary: false,
1650
+ });
1651
+ const matches = (0, explain_1.findDependenciesByPackageName)(result.aggregated, packageName);
1652
+ console.log("");
1653
+ console.log((0, explain_1.formatExplainOutput)(packageName, matches, {
1654
+ audit: result.collectorAvailability.audit,
1655
+ importGraphComplete: result.collectorAvailability.importGraphComplete,
1656
+ }));
1657
+ if (matches.length === 0) {
1658
+ process.exit(1);
1590
1659
  }
1591
- if (summary) {
1592
- printCliSummary(summary);
1660
+ }
1661
+ /**
1662
+ * Run the CLI entrypoint and dispatch to the selected command.
1663
+ */
1664
+ async function run() {
1665
+ const opts = parseArgs(process.argv.slice(2));
1666
+ if (opts.invalidCommand) {
1667
+ printHelp();
1668
+ process.exit(1);
1669
+ return;
1593
1670
  }
1594
- else {
1595
- console.log("");
1671
+ try {
1672
+ if (opts.command === "explain") {
1673
+ await runExplainCommand(opts);
1674
+ return;
1675
+ }
1676
+ await runScanCommand(opts);
1596
1677
  }
1597
- printPolicyViolations(policyViolations);
1598
- // Always show CTA as the last output
1599
- console.log("Enrich this scan with maintenance signals, upgrade readiness, and risk modelling at dependency-radar.com");
1600
- if (policyViolations.length > 0) {
1678
+ catch (err) {
1679
+ console.error("Failed to generate report:", err);
1601
1680
  process.exit(1);
1602
1681
  }
1603
1682
  }
1604
1683
  run();
1684
+ function createProgressReporter(text, quiet) {
1685
+ if (!quiet)
1686
+ return startSpinner(text);
1687
+ return {
1688
+ stop: () => { },
1689
+ update: () => { },
1690
+ log: (line) => {
1691
+ process.stdout.write(`${colorLeadingSymbol(line)}\n`);
1692
+ },
1693
+ };
1694
+ }
1605
1695
  /**
1606
1696
  * Displays a rotating CLI spinner with a message and returns controls to stop, update, or log lines.
1607
1697
  *