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 +81 -18
- package/dist/cli.js +169 -79
- package/dist/explain.js +193 -0
- package/dist/report-assets.js +2 -2
- package/dist/report.js +5 -5
- package/dist/runners/lockfileGraph.js +4 -1
- package/package.json +4 -2
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.
|
|
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
|
-
- **
|
|
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
|
-
-
|
|
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
|
|
164
|
-
• Transitive
|
|
165
|
-
• Vulnerable packages:
|
|
166
|
-
•
|
|
167
|
-
•
|
|
168
|
-
• Major upgrade blockers:
|
|
169
|
-
-
|
|
170
|
-
-
|
|
171
|
-
-
|
|
172
|
-
-
|
|
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:
|
|
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
|
-
|
|
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 === "
|
|
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
|
|
1274
|
-
console.log(`${bullet} Transitive
|
|
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}
|
|
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
|
|
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
|
|
1319
|
-
|
|
1320
|
-
|
|
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
|
|
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
|
-
|
|
1403
|
-
|
|
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 =
|
|
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
|
|
1555
|
-
|
|
1556
|
-
|
|
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
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1582
|
-
|
|
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
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
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
|
-
|
|
1589
|
-
|
|
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
|
-
|
|
1592
|
-
|
|
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
|
-
|
|
1595
|
-
|
|
1671
|
+
try {
|
|
1672
|
+
if (opts.command === "explain") {
|
|
1673
|
+
await runExplainCommand(opts);
|
|
1674
|
+
return;
|
|
1675
|
+
}
|
|
1676
|
+
await runScanCommand(opts);
|
|
1596
1677
|
}
|
|
1597
|
-
|
|
1598
|
-
|
|
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
|
*
|