@dependably/npm-check 1.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/LICENSE +201 -0
- package/README.md +71 -0
- package/bin/cli.js +1577 -0
- package/package.json +103 -0
- package/src/audit-config.js +331 -0
- package/src/audit.js +805 -0
- package/src/backup.js +235 -0
- package/src/checker.js +771 -0
- package/src/checksum-fixer.js +419 -0
- package/src/deprecation.js +325 -0
- package/src/fixer.js +207 -0
- package/src/format-library.js +132 -0
- package/src/index.js +138 -0
- package/src/integrity.js +519 -0
- package/src/migrator.js +263 -0
- package/src/npmrc-validator.js +205 -0
- package/src/overrides.js +71 -0
- package/src/package-json-validator.js +356 -0
- package/src/parallel-processor.js +374 -0
- package/src/parser.js +133 -0
- package/src/performance.js +283 -0
- package/src/pinner.js +248 -0
- package/src/pnpm-format.js +229 -0
- package/src/pnpm-workspace-validator.js +121 -0
- package/src/progress-reporter.js +245 -0
- package/src/pruner.js +177 -0
- package/src/remediate.js +389 -0
- package/src/report.js +663 -0
- package/src/schema.js +76 -0
- package/src/streaming-parser.js +251 -0
- package/src/updater.js +251 -0
- package/src/usage-scanner.js +215 -0
- package/src/validator.js +282 -0
- package/src/vuln.js +618 -0
- package/src/workers/dedupe-worker.js +20 -0
- package/src/workers/hash-upgrade-worker.js +20 -0
- package/src/workers/migration-worker.js +21 -0
- package/src/workers/validation-worker.js +20 -0
package/bin/cli.js
ADDED
|
@@ -0,0 +1,1577 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { parseLockfile } from '../src/parser.js';
|
|
7
|
+
import { validatePackageLock } from '../src/validator.js';
|
|
8
|
+
import { validatePackageJson } from '../src/package-json-validator.js';
|
|
9
|
+
import { validateNpmrc } from '../src/npmrc-validator.js';
|
|
10
|
+
import { migrateToVersion } from '../src/migrator.js';
|
|
11
|
+
import { upgradeIntegrityHashes, deduplicatePackages } from '../src/updater.js';
|
|
12
|
+
import { fixPackageLock } from '../src/fixer.js';
|
|
13
|
+
import { createBackup, listBackups, restoreFromLatestBackup, cleanOldBackups, BackupError } from '../src/backup.js';
|
|
14
|
+
import { createProgressBar } from '../src/progress-reporter.js';
|
|
15
|
+
import { checkIntegrity, checkLicenses } from '../src/checker.js';
|
|
16
|
+
import { checkVulnerabilities, vulnEnvelope } from '../src/vuln.js';
|
|
17
|
+
import { checkDeprecations, deprecationEnvelope } from '../src/deprecation.js';
|
|
18
|
+
import { remediateDependencies, remediateEnvelope } from '../src/remediate.js';
|
|
19
|
+
import { detectLockfileVersion, detectLockfileFlavor } from '../src/format-library.js';
|
|
20
|
+
import { validatePnpmWorkspace } from '../src/pnpm-workspace-validator.js';
|
|
21
|
+
import { fixChecksums } from '../src/checksum-fixer.js';
|
|
22
|
+
import { pinVersions, detectIndent } from '../src/pinner.js';
|
|
23
|
+
import { runAudit, formatAuditReport } from '../src/audit.js';
|
|
24
|
+
import { loadAuditConfig, mergeConfig } from '../src/audit-config.js';
|
|
25
|
+
import { runReport, formatReport } from '../src/report.js';
|
|
26
|
+
import { prunePackages } from '../src/pruner.js';
|
|
27
|
+
import { findUnusedDependencies } from '../src/usage-scanner.js';
|
|
28
|
+
|
|
29
|
+
const argv = process.argv.slice(2);
|
|
30
|
+
|
|
31
|
+
function printHelp() {
|
|
32
|
+
console.log(`
|
|
33
|
+
npm-check — npm lockfile toolkit
|
|
34
|
+
|
|
35
|
+
Usage:
|
|
36
|
+
npm-check [command] [file] [options]
|
|
37
|
+
|
|
38
|
+
With no command, npm-check runs the full report (all checks) on ./package-lock.json
|
|
39
|
+
(or ./pnpm-lock.yaml when present). pnpm lockfiles support the read-only checks
|
|
40
|
+
(report/integrity/vuln/deprecated); the write/transform commands are npm-only.
|
|
41
|
+
|
|
42
|
+
Commands:
|
|
43
|
+
|
|
44
|
+
Read & report (inspect; never mutate the lockfile):
|
|
45
|
+
report [file] Run ALL checks and print one grouped report (default)
|
|
46
|
+
validate [file] Validate package-lock.json, package.json, and .npmrc
|
|
47
|
+
vuln [file] Scan locked packages for known vulnerabilities (registry advisories)
|
|
48
|
+
deprecated [file] Scan locked packages for deprecation notices (the npm ci warnings)
|
|
49
|
+
check [file] Verify integrity hashes and licenses
|
|
50
|
+
audit [file] Lint lockfile for best practices (non-zero exit on failure)
|
|
51
|
+
unused [dir] Flag declared dependencies the application never imports
|
|
52
|
+
|
|
53
|
+
Fix & transform (npm-only; mutate the lockfile with --write):
|
|
54
|
+
fix [file] [--write] Run automated fixer with optional write
|
|
55
|
+
fix-checksums [file] Fill missing/placeholder/sha1 hashes from the registry
|
|
56
|
+
upgrade-hashes [file] Upgrade integrity hashes sha1→sha512
|
|
57
|
+
migrate [file] [target] Migrate to target version (1, 2, or 3; default: 3)
|
|
58
|
+
(alias: upgrade — \`npm-check upgrade\` == \`migrate 3\`)
|
|
59
|
+
pin [dir] Pin ^/~ ranges in package.json to lockfile versions
|
|
60
|
+
prune [file] Remove orphaned packages unreachable from the dependency graph
|
|
61
|
+
dedupe [file] Deduplicate packages in lockfile
|
|
62
|
+
remediate [dir] Bump direct deps that are deprecated/vulnerable to latest (then npm install)
|
|
63
|
+
|
|
64
|
+
Backups:
|
|
65
|
+
backups [file] List all backups for a file
|
|
66
|
+
restore [file] Restore from latest backup
|
|
67
|
+
clean-backups [file] Clean old backup files with optional --keep N
|
|
68
|
+
|
|
69
|
+
CI Gate (the one mechanism — repeatable):
|
|
70
|
+
--fail-on severity=<level> Fail if any finding is at/above the level
|
|
71
|
+
(info|low|moderate|high|critical)
|
|
72
|
+
--fail-on count=<N> Fail if the total finding count exceeds N
|
|
73
|
+
(replaces --max-warnings; count=0 == old --strict)
|
|
74
|
+
Default gate (no --fail-on): vulnerabilities trip the run; the fail-closed
|
|
75
|
+
default on packages that could not be scanned stays on (see --allow-unresolved).
|
|
76
|
+
Deprecated aliases (still work, will be removed): --min-severity → --fail-on severity=,
|
|
77
|
+
--max-warnings → --fail-on count=, --strict → --fail-on count=0, --fail-on-deprecated.
|
|
78
|
+
|
|
79
|
+
Report Options:
|
|
80
|
+
--offline Skip all network checks (integrity + vuln + deprecated); offline rules only
|
|
81
|
+
--no-integrity Skip the integrity check
|
|
82
|
+
--no-vuln Skip the known-vulnerability scan
|
|
83
|
+
--no-deprecated Skip the deprecation scan
|
|
84
|
+
--no-license Skip the license check
|
|
85
|
+
--config <file> Suite config (.dependably-check), discovered by walking
|
|
86
|
+
up to the repo root; .npm-checkrc.json is a fallback
|
|
87
|
+
--format human|json Output format (default: human; json emits the shared finding schema)
|
|
88
|
+
--allow-unresolved Don't fail when a registry-backed scan can't complete
|
|
89
|
+
(registry down / endpoint unsupported). Default: FAIL CLOSED
|
|
90
|
+
--concurrency / --timeout / --registry / --licenses-csv (as in Check Options)
|
|
91
|
+
|
|
92
|
+
Check Options:
|
|
93
|
+
--check hash Verify locked integrity hashes against the registry
|
|
94
|
+
--check license Only verify licenses against approved list
|
|
95
|
+
--check all Run both checks (default)
|
|
96
|
+
--licenses-csv <path> Path to approved licenses CSV
|
|
97
|
+
--strict Treat warnings as errors
|
|
98
|
+
--concurrency N Parallel registry requests for hash check (default: 8)
|
|
99
|
+
--timeout MS Per-request timeout in milliseconds (default: 10000)
|
|
100
|
+
--registry <url> Registry for entries without a derivable base
|
|
101
|
+
--allow-unresolved Don't fail on entries that can't be verified (default: FAIL CLOSED)
|
|
102
|
+
|
|
103
|
+
Vuln Options:
|
|
104
|
+
--fail-on severity=<level> Severity that fails the run (default: high)
|
|
105
|
+
--format human|json Output format (default: human; json emits the shared finding schema)
|
|
106
|
+
--offline Skip the scan (report everything as skipped)
|
|
107
|
+
--allow-unresolved Don't fail on packages that can't be checked
|
|
108
|
+
(registry down/unsupported). Default: FAIL CLOSED — a scan
|
|
109
|
+
that couldn't run never reports "clean"
|
|
110
|
+
--concurrency N Parallel registry requests (default: 8)
|
|
111
|
+
--timeout MS Per-request timeout in milliseconds (default: 10000)
|
|
112
|
+
--registry <url> Registry for entries without a derivable base
|
|
113
|
+
|
|
114
|
+
Deprecated Options:
|
|
115
|
+
--format human|json Output format (default: human; json emits the shared finding schema)
|
|
116
|
+
--offline Skip the scan (report everything as skipped)
|
|
117
|
+
--fail-on ... Fail the run when a locked package is deprecated
|
|
118
|
+
(--fail-on-deprecated is a deprecated alias; default: warn)
|
|
119
|
+
--allow-unresolved Don't fail on packages that can't be checked (default: FAIL CLOSED)
|
|
120
|
+
--concurrency N Parallel registry requests (default: 8)
|
|
121
|
+
--timeout MS Per-request timeout in milliseconds (default: 10000)
|
|
122
|
+
--registry <url> Registry for entries without a derivable base
|
|
123
|
+
|
|
124
|
+
Fix-Checksums Options:
|
|
125
|
+
--concurrency N Parallel registry requests (default: 8)
|
|
126
|
+
--timeout MS Per-request timeout in milliseconds (default: 10000)
|
|
127
|
+
--registry <url> Default registry for entries without a resolved URL
|
|
128
|
+
--local-fallback Hash node_modules copies when the registry fails
|
|
129
|
+
(flagged: these are NOT npm tarball hashes)
|
|
130
|
+
|
|
131
|
+
Remediate Options:
|
|
132
|
+
--write Apply the bumps to package.json + lockfile root (backs up first)
|
|
133
|
+
--fail-on severity=<level> Advisory level that counts a dep as vulnerable (default: high)
|
|
134
|
+
--no-deprecated Don't treat deprecated direct deps as remediation targets
|
|
135
|
+
--format human|json Output format (default: human; json emits the shared finding schema)
|
|
136
|
+
--registry <url> Registry for entries without a derivable base
|
|
137
|
+
(only DIRECT deps are bumped; transitive findings are reported as guidance.
|
|
138
|
+
Run 'npm install' afterward to re-resolve the tree.)
|
|
139
|
+
|
|
140
|
+
Pin Options:
|
|
141
|
+
--include-peer Also pin peerDependencies (off by default)
|
|
142
|
+
|
|
143
|
+
Unused Options:
|
|
144
|
+
--include-dev Also check devDependencies (off by default)
|
|
145
|
+
--format human|json Output format (default: human; json is machine-readable)
|
|
146
|
+
|
|
147
|
+
Audit Options:
|
|
148
|
+
--config <file> Suite config (.dependably-check), discovered by walking
|
|
149
|
+
up to the repo root; .npm-checkrc.json is a fallback
|
|
150
|
+
--rule <id>:<severity> Override a rule severity (error|warn|off); repeatable
|
|
151
|
+
--fail-on count=<N> Fail when the warning count exceeds N (count=0 fails on
|
|
152
|
+
any warning; --max-warnings / --strict are deprecated aliases)
|
|
153
|
+
--format stylish|json Report format (default: stylish)
|
|
154
|
+
|
|
155
|
+
General Options:
|
|
156
|
+
--write Write changes to file (creates backup)
|
|
157
|
+
-h, --help Show this help
|
|
158
|
+
--version Show version (long-only; -v is NOT version)
|
|
159
|
+
|
|
160
|
+
Exit Codes:
|
|
161
|
+
0 success — clean run, or --help / --version
|
|
162
|
+
1 findings — vulnerabilities / audit problems / checks failed (a blocking result)
|
|
163
|
+
2 usage or operational error — unknown command/flag, invalid flag value,
|
|
164
|
+
missing/unreadable lockfile, unsupported input, or an internal failure
|
|
165
|
+
|
|
166
|
+
Examples:
|
|
167
|
+
npm-check validate
|
|
168
|
+
npm-check upgrade --write # Lockfile v2 → v3
|
|
169
|
+
npm-check fix-checksums --write # Real integrity hashes from registry
|
|
170
|
+
npm-check pin --write # Lock down ^/~ versions
|
|
171
|
+
npm-check prune --write # Remove orphaned lockfile entries
|
|
172
|
+
npm-check unused # Flag never-imported dependencies
|
|
173
|
+
npm-check audit # Lint with default rules
|
|
174
|
+
npm-check audit --fail-on count=0 --format json # Any warning fails the run
|
|
175
|
+
npm-check audit --rule pinned-versions:error
|
|
176
|
+
npm-check vuln # Scan for known vulnerabilities
|
|
177
|
+
npm-check vuln --fail-on severity=critical --format json
|
|
178
|
+
npm-check deprecated # Scan for deprecated packages (npm ci warnings)
|
|
179
|
+
npm-check deprecated --fail-on count=0 # Fail CI when any locked package is deprecated
|
|
180
|
+
npm-check check --check hash # Only verify integrity
|
|
181
|
+
npm-check restore
|
|
182
|
+
npm-check clean-backups --keep 5
|
|
183
|
+
|
|
184
|
+
Default file: ./package-lock.json
|
|
185
|
+
`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function getVersion() {
|
|
189
|
+
try {
|
|
190
|
+
const packageJsonPath = path.join(path.dirname(fileURLToPath(import.meta.url)), '../package.json');
|
|
191
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
192
|
+
return packageJson.version;
|
|
193
|
+
} catch {
|
|
194
|
+
return 'unknown';
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Pick the default lockfile when none is given: prefer an existing
|
|
199
|
+
// package-lock.json, else an existing pnpm-lock.yaml, else the npm default
|
|
200
|
+
// (whose absence is reported downstream).
|
|
201
|
+
function resolveDefaultLockfile() {
|
|
202
|
+
const npmLock = path.resolve('package-lock.json');
|
|
203
|
+
if (fs.existsSync(npmLock)) return npmLock;
|
|
204
|
+
const pnpmLock = path.resolve('pnpm-lock.yaml');
|
|
205
|
+
if (fs.existsSync(pnpmLock)) return pnpmLock;
|
|
206
|
+
return npmLock;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function getFilePath(arg1) {
|
|
210
|
+
// Determine if arg1 is a file path or needs to use default
|
|
211
|
+
if (!arg1 || arg1.startsWith('-')) {
|
|
212
|
+
return resolveDefaultLockfile();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Check if arg1 looks like a numeric target version or other command arg
|
|
216
|
+
if (arg1.match(/^\d+$/)) {
|
|
217
|
+
return resolveDefaultLockfile();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// arg1 is a file path
|
|
221
|
+
return path.resolve(arg1);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// True when a path points at a pnpm (YAML) lockfile.
|
|
225
|
+
function isPnpmLockPath(filePath) {
|
|
226
|
+
const base = path.basename(filePath).toLowerCase();
|
|
227
|
+
return base === 'pnpm-lock.yaml' || base.endsWith('.yaml') || base.endsWith('.yml');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Guard the write/transform commands: a pnpm-lock.yaml is machine-generated and
|
|
231
|
+
// must never be hand-patched. Refuse with guidance instead of corrupting it.
|
|
232
|
+
// Exit 2: this is a usage error (the command was given input it cannot accept).
|
|
233
|
+
function refuseIfPnpm(filePath, command) {
|
|
234
|
+
if (isPnpmLockPath(filePath)) {
|
|
235
|
+
console.error(`\n\`${command}\` does not support pnpm-lock.yaml.`);
|
|
236
|
+
console.error(' pnpm lockfiles are machine-generated — regenerate with `pnpm install` instead.');
|
|
237
|
+
process.exit(2);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Exit 2: a missing required input (the lockfile/file the command operates on) is
|
|
242
|
+
// a usage error, per the suite convention — not a "findings" failure (exit 1).
|
|
243
|
+
function ensureFileExists(filePath) {
|
|
244
|
+
if (!fs.existsSync(filePath)) {
|
|
245
|
+
console.error(`Error: File not found: ${filePath}`);
|
|
246
|
+
process.exit(2);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Operational / internal error handler. Exit 2 per the suite convention: a thrown
|
|
251
|
+
// exception (bad input, unsupported lockfile, registry/scan failure, internal bug)
|
|
252
|
+
// is an operational error, distinct from a clean "findings found" run (exit 1).
|
|
253
|
+
function handleError(error, context = '') {
|
|
254
|
+
console.error(`\n${context || 'Error'}:`);
|
|
255
|
+
if (error instanceof BackupError) {
|
|
256
|
+
console.error(` Backup Error: ${error.message}`);
|
|
257
|
+
} else if (error.fixes) {
|
|
258
|
+
console.error(` ${error.message}`);
|
|
259
|
+
if (error.fixes.length > 0) {
|
|
260
|
+
console.error(' Partial fixes attempted:');
|
|
261
|
+
error.fixes.forEach(fix => console.error(` • ${fix}`));
|
|
262
|
+
}
|
|
263
|
+
} else {
|
|
264
|
+
console.error(` ${error.message}`);
|
|
265
|
+
}
|
|
266
|
+
process.exit(2);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Build a progress callback that redraws the bar only when the percentage
|
|
270
|
+
// changes, to avoid flicker. Each caller keeps its own `lastProgress` state.
|
|
271
|
+
function makeProgressReporter() {
|
|
272
|
+
let lastProgress = null;
|
|
273
|
+
return (progress) => {
|
|
274
|
+
if (!lastProgress || progress.percentage !== lastProgress.percentage) {
|
|
275
|
+
process.stderr.write(`\r${createProgressBar(progress)} ${progress.stage}`);
|
|
276
|
+
lastProgress = progress;
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Read the value that follows a flag in argv, or undefined when absent.
|
|
282
|
+
function flagValue(name) {
|
|
283
|
+
const i = argv.indexOf(name);
|
|
284
|
+
return i !== -1 && argv[i + 1] ? argv[i + 1] : undefined;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Parse a flag's value as a positive integer, exiting with `code` on a bad value.
|
|
288
|
+
// Returns `fallback` when the flag is absent.
|
|
289
|
+
function parsePositiveIntFlag(name, fallback, label, code = 2) {
|
|
290
|
+
const raw = flagValue(name);
|
|
291
|
+
if (raw === undefined) return fallback;
|
|
292
|
+
const parsed = parseInt(raw, 10);
|
|
293
|
+
if (isNaN(parsed) || parsed < 1) {
|
|
294
|
+
console.error(`Invalid ${label} value. Must be a positive number`);
|
|
295
|
+
process.exit(code);
|
|
296
|
+
}
|
|
297
|
+
return parsed;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Validate a --format flag against the allowed values, exiting with `code` on a bad value.
|
|
301
|
+
function parseFormatFlag(allowed, fallback, code = 2) {
|
|
302
|
+
const raw = flagValue('--format');
|
|
303
|
+
if (raw === undefined) return fallback;
|
|
304
|
+
if (!allowed.includes(raw)) {
|
|
305
|
+
console.error(`Invalid --format value. Use: ${allowed.join(' or ')}`);
|
|
306
|
+
process.exit(code);
|
|
307
|
+
}
|
|
308
|
+
return raw;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Every option token the CLI recognizes, split by whether it consumes the
|
|
312
|
+
// following token as its value. Used by rejectUnknownOptions() to fail closed
|
|
313
|
+
// on an unrecognized `-`/`--` token (a typo'd flag must never be silently
|
|
314
|
+
// dropped — that is a fail-open that can disable a CI gate).
|
|
315
|
+
//
|
|
316
|
+
// VALUED flags consume the next argv token (`--format json`); its value must
|
|
317
|
+
// NOT itself be treated as an unknown option. BOOLEAN flags stand alone.
|
|
318
|
+
// Keep these in sync with the flags parsed throughout this file.
|
|
319
|
+
const VALUED_OPTIONS = new Set([
|
|
320
|
+
'--format', '--config', '--fail-on', '--concurrency', '--timeout',
|
|
321
|
+
'--registry', '--licenses-csv', '--check', '--rule', '--keep',
|
|
322
|
+
// deprecated valued aliases (still parsed)
|
|
323
|
+
'--min-severity', '--max-warnings'
|
|
324
|
+
]);
|
|
325
|
+
const BOOLEAN_OPTIONS = new Set([
|
|
326
|
+
'--offline', '--allow-unresolved',
|
|
327
|
+
'--no-integrity', '--no-vuln', '--no-deprecated', '--no-license',
|
|
328
|
+
'--include-dev', '--include-peer', '--write', '--local-fallback',
|
|
329
|
+
'--version', '--help', '-h',
|
|
330
|
+
// deprecated boolean aliases (still parsed)
|
|
331
|
+
'--strict', '--fail-on-deprecated'
|
|
332
|
+
]);
|
|
333
|
+
|
|
334
|
+
// Collect the positional arguments after the command word (argv[0]), correctly
|
|
335
|
+
// skipping flags AND the value token that follows a valued flag. Reading a fixed
|
|
336
|
+
// slot like argv[1] is a footgun: `pin --write <path>` puts the flag in argv[1],
|
|
337
|
+
// so the path is never seen and a dir-oriented command silently falls back to
|
|
338
|
+
// cwd — which once wrote to the wrong project. Always resolve a target through
|
|
339
|
+
// this so flag order never changes which file is touched.
|
|
340
|
+
function positionals() {
|
|
341
|
+
const out = [];
|
|
342
|
+
let i = 1; // skip argv[0], the command word
|
|
343
|
+
while (i < argv.length) {
|
|
344
|
+
const tok = argv[i];
|
|
345
|
+
if (typeof tok === 'string' && tok.startsWith('-') && tok !== '-') {
|
|
346
|
+
if (VALUED_OPTIONS.has(tok)) i++; // also skip this flag's value
|
|
347
|
+
i++;
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
out.push(tok);
|
|
351
|
+
i++;
|
|
352
|
+
}
|
|
353
|
+
return out;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Reject any unrecognized option token, matching the unknown-command behavior
|
|
357
|
+
// (and the .NET suite tools): print `unknown option: '<x>'` to stderr and exit 2
|
|
358
|
+
// (a usage error). Positionals (the lockfile/dir/target), the VALUE that follows
|
|
359
|
+
// a valued flag (`--format json`, `--registry <url>`), and recognized flags all
|
|
360
|
+
// pass through untouched.
|
|
361
|
+
function rejectUnknownOptions() {
|
|
362
|
+
let i = 0;
|
|
363
|
+
while (i < argv.length) {
|
|
364
|
+
const tok = argv[i];
|
|
365
|
+
i++;
|
|
366
|
+
if (typeof tok !== 'string' || !tok.startsWith('-') || tok === '-') {
|
|
367
|
+
continue; // positional / command / a bare '-'
|
|
368
|
+
}
|
|
369
|
+
if (VALUED_OPTIONS.has(tok)) {
|
|
370
|
+
i++; // skip this flag's value so a value like `-1` is never mis-read as a flag
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
if (BOOLEAN_OPTIONS.has(tok)) continue;
|
|
374
|
+
console.error(`unknown option: '${tok}'`);
|
|
375
|
+
process.exit(2);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const SEVERITIES = ['info', 'low', 'moderate', 'high', 'critical'];
|
|
380
|
+
|
|
381
|
+
// Emit a one-line deprecation notice to stderr (never stdout, so machine output
|
|
382
|
+
// stays clean). Each retired flag is a thin alias that still maps onto --fail-on.
|
|
383
|
+
function warnDeprecated(oldFlag, replacement) {
|
|
384
|
+
console.error(`${oldFlag} is deprecated; use \`${replacement}\` instead.`);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Parse the unified, repeatable CI gate: `--fail-on <key>=<value>`.
|
|
388
|
+
// severity=<info|low|moderate|high|critical> trip if any finding is at/above
|
|
389
|
+
// count=<N> trip if total findings exceed N
|
|
390
|
+
// Returns { severity, count } with each null when unset. Bad input is exit 2.
|
|
391
|
+
function parseFailOn(code = 2) {
|
|
392
|
+
const out = { severity: null, count: null };
|
|
393
|
+
argv.forEach((arg, i) => {
|
|
394
|
+
if (arg !== '--fail-on') return;
|
|
395
|
+
const spec = argv[i + 1];
|
|
396
|
+
if (!spec || !spec.includes('=')) {
|
|
397
|
+
console.error('Invalid --fail-on. Use --fail-on <key>=<value> (severity=<level> or count=<N>)');
|
|
398
|
+
process.exit(code);
|
|
399
|
+
}
|
|
400
|
+
const eq = spec.indexOf('=');
|
|
401
|
+
const key = spec.slice(0, eq);
|
|
402
|
+
const value = spec.slice(eq + 1);
|
|
403
|
+
if (key === 'severity') {
|
|
404
|
+
if (!SEVERITIES.includes(value)) {
|
|
405
|
+
console.error(`Invalid --fail-on severity value. Use: ${SEVERITIES.join(', ')}`);
|
|
406
|
+
process.exit(code);
|
|
407
|
+
}
|
|
408
|
+
out.severity = value;
|
|
409
|
+
} else if (key === 'count') {
|
|
410
|
+
const n = parseInt(value, 10);
|
|
411
|
+
if (isNaN(n) || n < 0 || String(n) !== value.trim()) {
|
|
412
|
+
console.error('Invalid --fail-on count value. Must be a non-negative integer');
|
|
413
|
+
process.exit(code);
|
|
414
|
+
}
|
|
415
|
+
out.count = n;
|
|
416
|
+
} else {
|
|
417
|
+
console.error(`Unknown --fail-on key "${key}". Use: severity or count`);
|
|
418
|
+
process.exit(code);
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
return out;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// `--fail-on-deprecated` is retired in favour of the unified `--fail-on`, but
|
|
425
|
+
// kept as a thin deprecated alias (deprecation has no place on the severity
|
|
426
|
+
// ladder, so it stays a distinct toggle that still fails the run when set).
|
|
427
|
+
function resolveFailOnDeprecated() {
|
|
428
|
+
if (!argv.includes('--fail-on-deprecated')) return false;
|
|
429
|
+
warnDeprecated('--fail-on-deprecated', '--fail-on (deprecations fail when this is set)');
|
|
430
|
+
return true;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Assert that --fail-on specs are applicable for the calling command.
|
|
434
|
+
// `supportedKeys` shapes:
|
|
435
|
+
// { severity: true|false, count: true|false|'zero-only' }
|
|
436
|
+
// When a key was supplied in argv but is not supported (or only count=0 is
|
|
437
|
+
// supported and a non-zero value was given), exit 2 (usage error) with a
|
|
438
|
+
// clear message — a silently-dropped gate key is a fail-open CI footgun.
|
|
439
|
+
function assertFailOnSupported(supportedKeys, commandName) {
|
|
440
|
+
const failOn = parseFailOn();
|
|
441
|
+
if (failOn.severity !== null && !supportedKeys.severity) {
|
|
442
|
+
console.error(`--fail-on severity= is not supported by the "${commandName}" command`);
|
|
443
|
+
if (supportedKeys.count === 'zero-only') {
|
|
444
|
+
console.error(' Use --fail-on count=0 to fail when any deprecated package is found');
|
|
445
|
+
} else if (supportedKeys.count) {
|
|
446
|
+
console.error(' Use --fail-on count=<N> to set a finding count budget');
|
|
447
|
+
}
|
|
448
|
+
process.exit(2);
|
|
449
|
+
}
|
|
450
|
+
if (failOn.count !== null) {
|
|
451
|
+
if (!supportedKeys.count) {
|
|
452
|
+
console.error(`--fail-on count= is not supported by the "${commandName}" command`);
|
|
453
|
+
if (supportedKeys.severity) {
|
|
454
|
+
console.error(' Use --fail-on severity=<level> to set the minimum severity threshold');
|
|
455
|
+
}
|
|
456
|
+
process.exit(2);
|
|
457
|
+
}
|
|
458
|
+
if (supportedKeys.count === 'zero-only' && failOn.count > 0) {
|
|
459
|
+
console.error(`--fail-on count=${failOn.count} is not supported by the "${commandName}" command`);
|
|
460
|
+
console.error(' Use --fail-on count=0 to fail when any deprecated package is found');
|
|
461
|
+
process.exit(2);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Resolve the severity gate (the level at/above which a finding fails the run).
|
|
467
|
+
// Precedence: `--fail-on severity=` > the deprecated `--min-severity` alias > fallback.
|
|
468
|
+
function resolveSeverityGate(fallback = 'high', code = 2) {
|
|
469
|
+
const failOn = parseFailOn(code);
|
|
470
|
+
if (failOn.severity) return failOn.severity;
|
|
471
|
+
|
|
472
|
+
const legacy = flagValue('--min-severity');
|
|
473
|
+
if (legacy !== undefined) {
|
|
474
|
+
if (!SEVERITIES.includes(legacy)) {
|
|
475
|
+
console.error(`Invalid --min-severity value. Use: ${SEVERITIES.join(', ')}`);
|
|
476
|
+
process.exit(code);
|
|
477
|
+
}
|
|
478
|
+
warnDeprecated('--min-severity', '--fail-on severity=<level>');
|
|
479
|
+
return legacy;
|
|
480
|
+
}
|
|
481
|
+
return fallback;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// The registry-verification flags shared by the network-backed commands.
|
|
485
|
+
// Invalid flag values are usage errors (exit 2) by default.
|
|
486
|
+
function parseNetworkFlags(code = 2) {
|
|
487
|
+
return {
|
|
488
|
+
concurrency: parsePositiveIntFlag('--concurrency', 8, '--concurrency', code),
|
|
489
|
+
timeoutMs: parsePositiveIntFlag('--timeout', 10000, '--timeout', code),
|
|
490
|
+
defaultRegistry: flagValue('--registry')
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Spread the optional registry override only when one was supplied.
|
|
495
|
+
function registryOption(defaultRegistry) {
|
|
496
|
+
return defaultRegistry ? { defaultRegistry } : {};
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Clear the in-progress progress bar line.
|
|
500
|
+
function clearProgressLine() {
|
|
501
|
+
process.stderr.write('\r' + ' '.repeat(80) + '\r');
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Resolve the directory argument for dir-oriented commands (pin/unused/remediate).
|
|
505
|
+
// Accepts either a directory or a lockfile/package.json file path (deriving the
|
|
506
|
+
// directory from it), so `pin path/to/package-lock.json` targets the right
|
|
507
|
+
// project the same way the file-oriented commands do.
|
|
508
|
+
function getDirArg() {
|
|
509
|
+
const pos = positionals()[0];
|
|
510
|
+
if (!pos) return process.cwd();
|
|
511
|
+
const resolved = path.resolve(pos);
|
|
512
|
+
if (fs.existsSync(resolved) && fs.statSync(resolved).isFile()) {
|
|
513
|
+
return path.dirname(resolved);
|
|
514
|
+
}
|
|
515
|
+
return resolved;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Write `data` as pretty JSON to a file, creating a backup first.
|
|
519
|
+
function writeJsonFile(targetPath, data, indent = 2) {
|
|
520
|
+
createBackup(targetPath);
|
|
521
|
+
fs.writeFileSync(targetPath, JSON.stringify(data, null, indent) + '\n', 'utf8');
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Guard for the report/vuln/deprecated commands: a missing lockfile is exit 2.
|
|
525
|
+
function requireLockfileOrExit2(filePath, command) {
|
|
526
|
+
if (!fs.existsSync(filePath)) {
|
|
527
|
+
console.error(`No lockfile found at ${filePath}`);
|
|
528
|
+
console.error(` Run \`npm-check ${command} <path>\` or \`npm-check --help\`.`);
|
|
529
|
+
process.exit(2);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Apply repeatable --rule <id>:<severity> overrides onto an audit config.
|
|
534
|
+
// Exits 2 when the spec is missing a colon or has an empty severity part —
|
|
535
|
+
// a bare `--rule <id>` would silently set severity=undefined and be dropped
|
|
536
|
+
// by mergeConfig (a fail-open that could leave a CI gate disabled).
|
|
537
|
+
function applyRuleOverrides(config) {
|
|
538
|
+
const ruleOverrides = {};
|
|
539
|
+
argv.forEach((arg, i) => {
|
|
540
|
+
if (arg === '--rule' && argv[i + 1]) {
|
|
541
|
+
const spec = argv[i + 1];
|
|
542
|
+
const colonIdx = spec.indexOf(':');
|
|
543
|
+
if (colonIdx === -1 || colonIdx === spec.length - 1) {
|
|
544
|
+
console.error(`Invalid --rule spec "${spec}". Use --rule <id>:<severity> (error|warn|off)`);
|
|
545
|
+
process.exit(2);
|
|
546
|
+
}
|
|
547
|
+
const ruleId = spec.slice(0, colonIdx);
|
|
548
|
+
const severity = spec.slice(colonIdx + 1);
|
|
549
|
+
ruleOverrides[ruleId] = severity;
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
if (Object.keys(ruleOverrides).length === 0) return;
|
|
553
|
+
const merged = mergeConfig({ maxWarnings: config.maxWarnings, rules: ruleOverrides });
|
|
554
|
+
for (const ruleId of Object.keys(ruleOverrides)) {
|
|
555
|
+
if (!merged.rules[ruleId]) continue;
|
|
556
|
+
config.rules[ruleId] = {
|
|
557
|
+
severity: merged.rules[ruleId].severity,
|
|
558
|
+
options: { ...config.rules[ruleId].options }
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Resolve the finding-count gate onto an audit config.
|
|
564
|
+
// Precedence: `--fail-on count=N` > the deprecated `--strict` / `--max-warnings`
|
|
565
|
+
// aliases. `count=N` (and `--max-warnings N`) trip when warnings exceed N;
|
|
566
|
+
// `--strict` is shorthand for count=0 (any warning fails).
|
|
567
|
+
function applyMaxWarnings(config) {
|
|
568
|
+
const failOn = parseFailOn();
|
|
569
|
+
if (failOn.count !== null) {
|
|
570
|
+
config.maxWarnings = failOn.count;
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
if (argv.includes('--strict')) {
|
|
574
|
+
warnDeprecated('--strict', '--fail-on count=0');
|
|
575
|
+
config.maxWarnings = 0;
|
|
576
|
+
}
|
|
577
|
+
const raw = flagValue('--max-warnings');
|
|
578
|
+
if (raw === undefined) return;
|
|
579
|
+
const parsed = parseInt(raw, 10);
|
|
580
|
+
if (isNaN(parsed)) {
|
|
581
|
+
console.error('Invalid --max-warnings value. Must be a number');
|
|
582
|
+
process.exit(2);
|
|
583
|
+
}
|
|
584
|
+
warnDeprecated('--max-warnings', '--fail-on count=<N>');
|
|
585
|
+
config.maxWarnings = parsed;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Load the package.json sibling to a lockfile, or null when absent/unparseable.
|
|
589
|
+
function loadSiblingPackageJson(filePath, { tolerant = false } = {}) {
|
|
590
|
+
const pkgPath = path.join(path.dirname(filePath), 'package.json');
|
|
591
|
+
if (!fs.existsSync(pkgPath)) return null;
|
|
592
|
+
if (!tolerant) return JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
593
|
+
try { return JSON.parse(fs.readFileSync(pkgPath, 'utf8')); } catch { return null; }
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Resolve every report option from argv (audit config, format, toggles, network).
|
|
597
|
+
function parseReportOptions() {
|
|
598
|
+
const config = loadAuditConfig(process.cwd(), flagValue('--config') || null);
|
|
599
|
+
applyRuleOverrides(config);
|
|
600
|
+
|
|
601
|
+
// The count gate (`--fail-on count=`, or the deprecated `--strict`/`--max-warnings`)
|
|
602
|
+
// lands on config.maxWarnings. `strict` stays a separate signal for the license
|
|
603
|
+
// check (treat unknown-license warnings as failures).
|
|
604
|
+
applyMaxWarnings(config);
|
|
605
|
+
const strict = argv.includes('--strict');
|
|
606
|
+
const maxWarnings = config.maxWarnings;
|
|
607
|
+
|
|
608
|
+
return {
|
|
609
|
+
config,
|
|
610
|
+
strict,
|
|
611
|
+
maxWarnings,
|
|
612
|
+
format: parseFormatFlag(['human', 'json'], 'human'),
|
|
613
|
+
// Network/integrity + license toggles.
|
|
614
|
+
integrity: !argv.includes('--offline') && !argv.includes('--no-integrity'),
|
|
615
|
+
license: !argv.includes('--no-license'),
|
|
616
|
+
vuln: !argv.includes('--offline') && !argv.includes('--no-vuln'),
|
|
617
|
+
deprecated: !argv.includes('--offline') && !argv.includes('--no-deprecated'),
|
|
618
|
+
// Fail closed by default: a registry-backed scan that couldn't complete must not
|
|
619
|
+
// pass the report. `--allow-unresolved` opts back into the old lenient behavior.
|
|
620
|
+
failOnUnresolved: !argv.includes('--allow-unresolved'),
|
|
621
|
+
failOnDeprecated: resolveFailOnDeprecated(),
|
|
622
|
+
minSeverity: resolveSeverityGate(),
|
|
623
|
+
concurrency: parsePositiveIntFlag('--concurrency', 8, '--concurrency', 2),
|
|
624
|
+
timeoutMs: parsePositiveIntFlag('--timeout', 10000, '--timeout', 2),
|
|
625
|
+
defaultRegistry: flagValue('--registry'),
|
|
626
|
+
licensesCsv: flagValue('--licenses-csv')
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
async function runReportCommand() {
|
|
631
|
+
// `report` takes its file positional only when invoked explicitly.
|
|
632
|
+
const filePath = getFilePath(argv[0] === 'report' ? positionals()[0] : undefined);
|
|
633
|
+
requireLockfileOrExit2(filePath, 'report');
|
|
634
|
+
|
|
635
|
+
let report;
|
|
636
|
+
try {
|
|
637
|
+
const opts = parseReportOptions();
|
|
638
|
+
const dir = path.dirname(filePath);
|
|
639
|
+
const lockfile = parseLockfile(filePath);
|
|
640
|
+
const packageJson = loadSiblingPackageJson(filePath);
|
|
641
|
+
|
|
642
|
+
if ((opts.integrity || opts.vuln || opts.deprecated) && opts.format === 'human') {
|
|
643
|
+
console.error('Running all checks (querying the registry)…');
|
|
644
|
+
}
|
|
645
|
+
const onProgress = opts.format === 'human' ? makeProgressReporter() : null;
|
|
646
|
+
|
|
647
|
+
report = await runReport(
|
|
648
|
+
{ lockfile, packageJson, filePath: path.relative(process.cwd(), filePath) || filePath, dir },
|
|
649
|
+
{
|
|
650
|
+
auditConfig: opts.config, integrity: opts.integrity, license: opts.license, vuln: opts.vuln,
|
|
651
|
+
deprecated: opts.deprecated, failOnDeprecated: opts.failOnDeprecated, minSeverity: opts.minSeverity,
|
|
652
|
+
strict: opts.strict, maxWarnings: opts.maxWarnings, concurrency: opts.concurrency,
|
|
653
|
+
timeoutMs: opts.timeoutMs, failOnUnresolved: opts.failOnUnresolved, onProgress,
|
|
654
|
+
...registryOption(opts.defaultRegistry),
|
|
655
|
+
...(opts.licensesCsv ? { licensesCsv: opts.licensesCsv } : {})
|
|
656
|
+
}
|
|
657
|
+
);
|
|
658
|
+
|
|
659
|
+
if (onProgress) clearProgressLine();
|
|
660
|
+
const rendered = formatReport(report, { format: opts.format });
|
|
661
|
+
console.log(opts.format === 'human' ? '\n' + rendered : rendered);
|
|
662
|
+
} catch (error) {
|
|
663
|
+
console.error(`\nReport error: ${error.message}`);
|
|
664
|
+
process.exit(2);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
process.exit(report.summary.pass ? 0 : 1);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// Validate the sibling package.json (if present). A malformed manifest is itself a
|
|
671
|
+
// validation failure to report — not a reason to abort the whole command.
|
|
672
|
+
function loadPackageJsonResult(dir) {
|
|
673
|
+
const pkgPath = path.join(dir, 'package.json');
|
|
674
|
+
if (!fs.existsSync(pkgPath)) return null;
|
|
675
|
+
try {
|
|
676
|
+
return validatePackageJson(JSON.parse(fs.readFileSync(pkgPath, 'utf8')));
|
|
677
|
+
} catch (e) {
|
|
678
|
+
return { valid: false, errors: [{ code: 'PJ_PARSE_ERROR', message: `package.json is not valid JSON: ${e.message}` }], warnings: [] };
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// Validate the project-level .npmrc (if present), threading the flavor through so
|
|
683
|
+
// pnpm-ignored keys are flagged.
|
|
684
|
+
function loadNpmrcResult(dir, isPnpm) {
|
|
685
|
+
const npmrcPath = path.join(dir, '.npmrc');
|
|
686
|
+
if (!fs.existsSync(npmrcPath)) return null;
|
|
687
|
+
return validateNpmrc(fs.readFileSync(npmrcPath, 'utf8'), isPnpm ? { flavor: 'pnpm' } : {});
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Validate pnpm-workspace.yaml (pnpm projects only).
|
|
691
|
+
function loadPnpmWorkspaceResult(dir, isPnpm) {
|
|
692
|
+
const wsPath = path.join(dir, 'pnpm-workspace.yaml');
|
|
693
|
+
if (!isPnpm || !fs.existsSync(wsPath)) return null;
|
|
694
|
+
return validatePnpmWorkspace(fs.readFileSync(wsPath, 'utf8'));
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// Errors are Error subclass instances, whose `message` is non-enumerable and so
|
|
698
|
+
// vanishes under JSON.stringify — normalize to {code, message} so the JSON output
|
|
699
|
+
// is actually readable.
|
|
700
|
+
function normalizeValidationResult(r) {
|
|
701
|
+
if (!r || typeof r !== 'object' || !Array.isArray(r.errors)) return r;
|
|
702
|
+
return { ...r, errors: r.errors.map((e) => ({ code: e.code, message: e.message })) };
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function runValidateCommand() {
|
|
706
|
+
const filePath = getFilePath(positionals()[0]);
|
|
707
|
+
ensureFileExists(filePath);
|
|
708
|
+
const dir = path.dirname(filePath);
|
|
709
|
+
|
|
710
|
+
const lockfile = parseLockfile(filePath);
|
|
711
|
+
const isPnpm = detectLockfileFlavor(lockfile) === 'pnpm';
|
|
712
|
+
|
|
713
|
+
// The npm lockfile validator is npm-shape only; a pnpm-lock.yaml is
|
|
714
|
+
// machine-generated, so we validate the files that govern its install instead.
|
|
715
|
+
const lockResult = isPnpm ? null : validatePackageLock(lockfile);
|
|
716
|
+
const pkgResult = loadPackageJsonResult(dir);
|
|
717
|
+
const npmrcResult = loadNpmrcResult(dir, isPnpm);
|
|
718
|
+
const wsResult = loadPnpmWorkspaceResult(dir, isPnpm);
|
|
719
|
+
|
|
720
|
+
const norm = (r, fallback) => normalizeValidationResult(r) || fallback;
|
|
721
|
+
const out = isPnpm
|
|
722
|
+
? {
|
|
723
|
+
'pnpm-lock.yaml': 'machine-generated (structural validation skipped; regenerate with `pnpm install`)',
|
|
724
|
+
'package.json': norm(pkgResult, 'not found (skipped)'),
|
|
725
|
+
'.npmrc': norm(npmrcResult, 'not found (skipped)'),
|
|
726
|
+
'pnpm-workspace.yaml': norm(wsResult, 'not found (skipped)')
|
|
727
|
+
}
|
|
728
|
+
: {
|
|
729
|
+
'package-lock.json': normalizeValidationResult(lockResult),
|
|
730
|
+
'package.json': norm(pkgResult, 'not found (skipped)'),
|
|
731
|
+
'.npmrc': norm(npmrcResult, 'not found (skipped)')
|
|
732
|
+
};
|
|
733
|
+
|
|
734
|
+
console.log('\nValidation Result:');
|
|
735
|
+
console.log(JSON.stringify(out, null, 2));
|
|
736
|
+
|
|
737
|
+
const valid = [lockResult, pkgResult, npmrcResult, wsResult].every((r) => !r || r.valid);
|
|
738
|
+
process.exit(valid ? 0 : 1);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function runMigrateCommand() {
|
|
742
|
+
// `migrate [target] [file]` — the numeric target and the file path may appear
|
|
743
|
+
// in either order; split the positionals by shape rather than by fixed slot.
|
|
744
|
+
const pos = positionals();
|
|
745
|
+
const numericArg = pos.find((a) => /^\d+$/.test(a));
|
|
746
|
+
const fileArg = pos.find((a) => !/^\d+$/.test(a));
|
|
747
|
+
const filePath = getFilePath(fileArg);
|
|
748
|
+
ensureFileExists(filePath);
|
|
749
|
+
refuseIfPnpm(filePath, 'migrate');
|
|
750
|
+
|
|
751
|
+
// Get target version (default: 3)
|
|
752
|
+
const target = numericArg ? parseInt(numericArg, 10) : 3;
|
|
753
|
+
|
|
754
|
+
const hasWrite = argv.includes('--write');
|
|
755
|
+
|
|
756
|
+
const lockfile = parseLockfile(filePath);
|
|
757
|
+
const migrated = migrateToVersion(lockfile, target);
|
|
758
|
+
|
|
759
|
+
console.log(`\nMigrated lockfile to version ${target}`);
|
|
760
|
+
|
|
761
|
+
if (hasWrite) {
|
|
762
|
+
createBackup(filePath);
|
|
763
|
+
fs.writeFileSync(filePath, JSON.stringify(migrated, null, 2) + '\n', 'utf8');
|
|
764
|
+
console.log(`Changes written to ${filePath}`);
|
|
765
|
+
} else {
|
|
766
|
+
console.log('\nUse --write flag to save changes');
|
|
767
|
+
console.log(JSON.stringify(migrated, null, 2));
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function runUpgradeCommand() {
|
|
772
|
+
const filePath = getFilePath(positionals()[0]);
|
|
773
|
+
ensureFileExists(filePath);
|
|
774
|
+
refuseIfPnpm(filePath, 'upgrade');
|
|
775
|
+
const hasWrite = argv.includes('--write');
|
|
776
|
+
|
|
777
|
+
const lockfile = parseLockfile(filePath);
|
|
778
|
+
const sourceVersion = detectLockfileVersion(lockfile);
|
|
779
|
+
|
|
780
|
+
if (sourceVersion === 3) {
|
|
781
|
+
console.log('\nAlready at version 3, nothing to do');
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
const migrated = migrateToVersion(lockfile, 3);
|
|
786
|
+
console.log(`\nMigrated lockfile v${sourceVersion} → v3`);
|
|
787
|
+
|
|
788
|
+
if (hasWrite) {
|
|
789
|
+
createBackup(filePath);
|
|
790
|
+
fs.writeFileSync(filePath, JSON.stringify(migrated, null, 2) + '\n', 'utf8');
|
|
791
|
+
console.log(`Changes written to ${filePath}`);
|
|
792
|
+
} else {
|
|
793
|
+
console.log('\nUse --write flag to save changes');
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// Print the fix-checksums result summary, changes, unresolved entries and warnings.
|
|
798
|
+
function printChecksumResult(result, hasWrite) {
|
|
799
|
+
clearProgressLine();
|
|
800
|
+
console.log('\nChecksum fix complete');
|
|
801
|
+
console.log(` Candidates: ${result.summary.candidates}`);
|
|
802
|
+
console.log(` Fixed from registry: ${result.summary.fixedFromRegistry}`);
|
|
803
|
+
console.log(` Fixed locally: ${result.summary.fixedFromLocal}${result.summary.fixedFromLocal > 0 ? ' (flagged)' : ''}`);
|
|
804
|
+
console.log(` Unresolved: ${result.summary.unresolved}`);
|
|
805
|
+
console.log(` Skipped: ${result.summary.skipped}`);
|
|
806
|
+
|
|
807
|
+
if (result.changes.length > 0 && !hasWrite) {
|
|
808
|
+
console.log('\n Changes:');
|
|
809
|
+
result.changes.forEach((change) => {
|
|
810
|
+
console.log(` • ${change.packagePath}: ${change.from || '(missing)'} → ${change.to.slice(0, 40)}... [${change.source}]`);
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
if (result.unresolved.length > 0) {
|
|
814
|
+
console.log('\n Unresolved packages:');
|
|
815
|
+
result.unresolved.forEach((item) => {
|
|
816
|
+
console.log(` • ${item.packagePath}: ${item.reason}`);
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
result.warnings.forEach((warning) => console.log(`\n${warning}`));
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
async function runFixChecksumsCommand() {
|
|
823
|
+
const filePath = getFilePath(positionals()[0]);
|
|
824
|
+
ensureFileExists(filePath);
|
|
825
|
+
refuseIfPnpm(filePath, 'fix-checksums');
|
|
826
|
+
const hasWrite = argv.includes('--write');
|
|
827
|
+
const localFallback = argv.includes('--local-fallback');
|
|
828
|
+
const { concurrency, timeoutMs, defaultRegistry } = parseNetworkFlags();
|
|
829
|
+
|
|
830
|
+
const lockfile = parseLockfile(filePath);
|
|
831
|
+
const onProgress = makeProgressReporter();
|
|
832
|
+
|
|
833
|
+
console.log('Fixing integrity checksums...');
|
|
834
|
+
const lockfileDir = path.dirname(filePath);
|
|
835
|
+
const result = await fixChecksums(lockfile, {
|
|
836
|
+
onProgress, concurrency, timeoutMs, localFallback,
|
|
837
|
+
baseDir: lockfileDir,
|
|
838
|
+
nodeModulesPath: path.join(lockfileDir, 'node_modules'),
|
|
839
|
+
...registryOption(defaultRegistry)
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
printChecksumResult(result, hasWrite);
|
|
843
|
+
|
|
844
|
+
if (hasWrite && result.changes.length > 0) {
|
|
845
|
+
writeJsonFile(filePath, result.lockfile);
|
|
846
|
+
console.log(`\nChanges written to ${filePath}`);
|
|
847
|
+
} else if (result.changes.length > 0) {
|
|
848
|
+
console.log('\nUse --write flag to save changes');
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
process.exit(result.unresolved.length > 0 ? 1 : 0);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
function runPinCommand() {
|
|
855
|
+
// pin operates on a directory containing both package.json and the lockfile
|
|
856
|
+
const dir = getDirArg();
|
|
857
|
+
const hasWrite = argv.includes('--write');
|
|
858
|
+
const includePeer = argv.includes('--include-peer');
|
|
859
|
+
|
|
860
|
+
const packageJsonPath = path.join(dir, 'package.json');
|
|
861
|
+
const lockfilePath = path.join(dir, 'package-lock.json');
|
|
862
|
+
ensureFileExists(packageJsonPath);
|
|
863
|
+
ensureFileExists(lockfilePath);
|
|
864
|
+
|
|
865
|
+
const packageJsonRaw = fs.readFileSync(packageJsonPath, 'utf8');
|
|
866
|
+
const packageJson = JSON.parse(packageJsonRaw);
|
|
867
|
+
const lockfile = parseLockfile(lockfilePath);
|
|
868
|
+
|
|
869
|
+
const result = pinVersions(packageJson, lockfile, { includePeer });
|
|
870
|
+
|
|
871
|
+
console.log('\nPin Results:');
|
|
872
|
+
if (result.changes.length === 0) {
|
|
873
|
+
console.log(' Nothing to pin — all ranges already exact (or skipped)');
|
|
874
|
+
} else {
|
|
875
|
+
result.changes.forEach((change) => {
|
|
876
|
+
console.log(` • ${change.section}/${change.name} ${change.from} → ${change.to}`);
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
if (result.skipped.length > 0) {
|
|
881
|
+
console.log('\n Skipped:');
|
|
882
|
+
result.skipped.forEach((skip) => {
|
|
883
|
+
console.log(` • ${skip.section}/${skip.name} (${skip.range}): ${skip.reason}`);
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
result.warnings.forEach((warning) => {
|
|
888
|
+
console.log(`\n${warning}`);
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
if (hasWrite && result.changes.length > 0) {
|
|
892
|
+
const indent = detectIndent(packageJsonRaw);
|
|
893
|
+
createBackup(packageJsonPath);
|
|
894
|
+
createBackup(lockfilePath);
|
|
895
|
+
fs.writeFileSync(packageJsonPath, JSON.stringify(result.packageJson, null, indent) + '\n', 'utf8');
|
|
896
|
+
fs.writeFileSync(lockfilePath, JSON.stringify(result.lockfile, null, 2) + '\n', 'utf8');
|
|
897
|
+
console.log(`\nChanges written to ${packageJsonPath} and ${lockfilePath}`);
|
|
898
|
+
} else if (result.changes.length > 0) {
|
|
899
|
+
console.log('\nUse --write flag to save changes');
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
function runPruneCommand() {
|
|
904
|
+
const filePath = getFilePath(positionals()[0]);
|
|
905
|
+
ensureFileExists(filePath);
|
|
906
|
+
refuseIfPnpm(filePath, 'prune');
|
|
907
|
+
const hasWrite = argv.includes('--write');
|
|
908
|
+
|
|
909
|
+
const lockfile = parseLockfile(filePath);
|
|
910
|
+
const result = prunePackages(lockfile);
|
|
911
|
+
|
|
912
|
+
console.log('\nPrune Results:');
|
|
913
|
+
if (result.removed.length === 0) {
|
|
914
|
+
console.log(' No orphaned packages found — lockfile is fully connected');
|
|
915
|
+
} else {
|
|
916
|
+
console.log(` Removed ${result.removed.length} orphaned package(s):`);
|
|
917
|
+
result.removed.forEach((orphan) => {
|
|
918
|
+
const detail = orphan.version ? ` (${orphan.name}@${orphan.version})` : '';
|
|
919
|
+
console.log(` • ${orphan.key}${detail}`);
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
result.warnings.forEach((warning) => {
|
|
924
|
+
console.log(`\n${warning}`);
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
if (hasWrite && result.removed.length > 0) {
|
|
928
|
+
createBackup(filePath);
|
|
929
|
+
fs.writeFileSync(filePath, JSON.stringify(result.lockfile, null, 2) + '\n', 'utf8');
|
|
930
|
+
console.log(`\nChanges written to ${filePath}`);
|
|
931
|
+
} else if (result.removed.length > 0) {
|
|
932
|
+
console.log('\nUse --write flag to save changes');
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
function runUnusedCommand() {
|
|
937
|
+
const dir = getDirArg();
|
|
938
|
+
const includeDev = argv.includes('--include-dev');
|
|
939
|
+
// Machine output is selected with `--format json` (the suite-wide spelling);
|
|
940
|
+
// the old boolean `--json` switch is retired.
|
|
941
|
+
const asJson = parseFormatFlag(['human', 'json'], 'human') === 'json';
|
|
942
|
+
|
|
943
|
+
const packageJsonPath = path.join(dir, 'package.json');
|
|
944
|
+
ensureFileExists(packageJsonPath);
|
|
945
|
+
|
|
946
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
947
|
+
const result = findUnusedDependencies(packageJson, dir, { includeDev });
|
|
948
|
+
|
|
949
|
+
if (asJson) {
|
|
950
|
+
console.log(JSON.stringify({
|
|
951
|
+
scannedFiles: result.scannedFiles,
|
|
952
|
+
appFiles: result.appFiles,
|
|
953
|
+
buildFiles: result.buildFiles,
|
|
954
|
+
buildDirsScanned: result.buildDirsScanned,
|
|
955
|
+
sectionsChecked: result.sectionsChecked,
|
|
956
|
+
buildOnly: result.buildOnly,
|
|
957
|
+
unused: result.unused
|
|
958
|
+
}, null, 2));
|
|
959
|
+
} else {
|
|
960
|
+
const split = result.buildFiles
|
|
961
|
+
? ` — ${result.appFiles} app, ${result.buildFiles} build [${result.buildDirsScanned.join(', ')}]`
|
|
962
|
+
: '';
|
|
963
|
+
console.log(`\nScanned ${result.scannedFiles} source file(s)${split} (${result.sectionsChecked.join(', ')})`);
|
|
964
|
+
if (result.buildOnly.length > 0) {
|
|
965
|
+
console.log(` ${result.buildOnly.length} package(s) imported only by build tooling (kept): ${result.buildOnly.join(', ')}`);
|
|
966
|
+
}
|
|
967
|
+
if (result.unused.length === 0) {
|
|
968
|
+
console.log(' All declared dependencies are imported by the application');
|
|
969
|
+
} else {
|
|
970
|
+
console.log(` ${result.unused.length} package(s) flagged for removal (never imported):`);
|
|
971
|
+
result.unused.forEach((dep) => {
|
|
972
|
+
console.log(` • ${dep.name} (${dep.section}: ${dep.version})`);
|
|
973
|
+
});
|
|
974
|
+
console.log('\n Heuristic results — packages loaded via config files or CLI-only');
|
|
975
|
+
console.log(' tools can be false positives. Verify before removing, e.g.:');
|
|
976
|
+
result.unused.forEach((dep) => {
|
|
977
|
+
console.log(` npm uninstall ${dep.name}`);
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
function runAuditCommand() {
|
|
984
|
+
const filePath = getFilePath(positionals()[0]);
|
|
985
|
+
// The audit command gates on warning/finding count, not severity level.
|
|
986
|
+
// `--fail-on severity=` is a no-op here — reject it before the run so a CI
|
|
987
|
+
// pipeline that meant `--fail-on count=` never silently passes.
|
|
988
|
+
assertFailOnSupported({ severity: false, count: true }, 'audit');
|
|
989
|
+
|
|
990
|
+
let report;
|
|
991
|
+
try {
|
|
992
|
+
if (!fs.existsSync(filePath)) {
|
|
993
|
+
throw new Error(`File not found: ${filePath}`);
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// Resolve config: file (discovered or --config) + CLI overrides
|
|
997
|
+
const config = loadAuditConfig(process.cwd(), flagValue('--config') || null);
|
|
998
|
+
applyRuleOverrides(config);
|
|
999
|
+
applyMaxWarnings(config);
|
|
1000
|
+
const format = parseFormatFlag(['stylish', 'json'], 'stylish');
|
|
1001
|
+
|
|
1002
|
+
const lockfile = parseLockfile(filePath);
|
|
1003
|
+
// package.json is optional; the pinned-versions rule degrades gracefully
|
|
1004
|
+
const packageJson = loadSiblingPackageJson(filePath);
|
|
1005
|
+
|
|
1006
|
+
report = runAudit({ lockfile, packageJson, filePath: path.relative(process.cwd(), filePath) || filePath }, config);
|
|
1007
|
+
console.log('\n' + formatAuditReport(report, { format }));
|
|
1008
|
+
} catch (error) {
|
|
1009
|
+
console.error(`\nAudit error: ${error.message}`);
|
|
1010
|
+
process.exit(2);
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
process.exit(report.pass ? 0 : 1);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
function runUpgradeHashesCommand() {
|
|
1017
|
+
const filePath = getFilePath(positionals()[0]);
|
|
1018
|
+
ensureFileExists(filePath);
|
|
1019
|
+
refuseIfPnpm(filePath, 'upgrade-hashes');
|
|
1020
|
+
const hasWrite = argv.includes('--write');
|
|
1021
|
+
|
|
1022
|
+
const lockfile = parseLockfile(filePath);
|
|
1023
|
+
|
|
1024
|
+
// Setup progress reporting
|
|
1025
|
+
const onProgress = makeProgressReporter();
|
|
1026
|
+
|
|
1027
|
+
const upgraded = upgradeIntegrityHashes(lockfile, { onProgress });
|
|
1028
|
+
|
|
1029
|
+
// Clear progress line and show completion
|
|
1030
|
+
clearProgressLine();
|
|
1031
|
+
console.log('\nUpgraded integrity hashes');
|
|
1032
|
+
|
|
1033
|
+
if (hasWrite) {
|
|
1034
|
+
createBackup(filePath);
|
|
1035
|
+
fs.writeFileSync(filePath, JSON.stringify(upgraded, null, 2) + '\n', 'utf8');
|
|
1036
|
+
console.log(`Changes written to ${filePath}`);
|
|
1037
|
+
} else {
|
|
1038
|
+
console.log('\nUse --write flag to save changes');
|
|
1039
|
+
console.log(JSON.stringify(upgraded, null, 2));
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
function runDedupeCommand() {
|
|
1044
|
+
const filePath = getFilePath(positionals()[0]);
|
|
1045
|
+
ensureFileExists(filePath);
|
|
1046
|
+
refuseIfPnpm(filePath, 'dedupe');
|
|
1047
|
+
const hasWrite = argv.includes('--write');
|
|
1048
|
+
|
|
1049
|
+
const lockfile = parseLockfile(filePath);
|
|
1050
|
+
const beforeCount = lockfile.packages ? Object.keys(lockfile.packages).length : 0;
|
|
1051
|
+
|
|
1052
|
+
// Setup progress reporting
|
|
1053
|
+
const onProgress = makeProgressReporter();
|
|
1054
|
+
|
|
1055
|
+
const deduped = deduplicatePackages(lockfile, { onProgress });
|
|
1056
|
+
const afterCount = deduped.packages ? Object.keys(deduped.packages).length : 0;
|
|
1057
|
+
|
|
1058
|
+
// Clear progress line and show completion
|
|
1059
|
+
clearProgressLine();
|
|
1060
|
+
console.log(`\nDeduplication complete`);
|
|
1061
|
+
console.log(` Packages: ${beforeCount} → ${afterCount} (removed ${beforeCount - afterCount})`);
|
|
1062
|
+
if (afterCount === beforeCount && beforeCount > 0) {
|
|
1063
|
+
console.log(' (a v2/v3 packages map is keyed by install path — every entry is required,');
|
|
1064
|
+
console.log(' so there is nothing to collapse. Use `npm-check prune` to remove orphaned entries.)');
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
if (hasWrite) {
|
|
1068
|
+
createBackup(filePath);
|
|
1069
|
+
fs.writeFileSync(filePath, JSON.stringify(deduped, null, 2) + '\n', 'utf8');
|
|
1070
|
+
console.log(`Changes written to ${filePath}`);
|
|
1071
|
+
} else {
|
|
1072
|
+
console.log('\nUse --write flag to save changes');
|
|
1073
|
+
console.log(JSON.stringify(deduped, null, 2));
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
function runFixCommand() {
|
|
1078
|
+
const filePath = getFilePath(positionals()[0]);
|
|
1079
|
+
ensureFileExists(filePath);
|
|
1080
|
+
refuseIfPnpm(filePath, 'fix');
|
|
1081
|
+
const hasWrite = argv.includes('--write');
|
|
1082
|
+
|
|
1083
|
+
const lockfile = parseLockfile(filePath);
|
|
1084
|
+
// Load the sibling package.json (if present) so the fixer can sync the
|
|
1085
|
+
// lockfile's stale root name/version — the "Structure & format" errors.
|
|
1086
|
+
let pkgJson = null;
|
|
1087
|
+
const pkgJsonPath = path.join(path.dirname(filePath), 'package.json');
|
|
1088
|
+
if (fs.existsSync(pkgJsonPath)) {
|
|
1089
|
+
try { pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8')); } catch { /* ignore malformed package.json */ }
|
|
1090
|
+
}
|
|
1091
|
+
const { fixedLockfile, fixes } = fixPackageLock(lockfile, { packageJson: pkgJson });
|
|
1092
|
+
|
|
1093
|
+
console.log('\nFixer Results:');
|
|
1094
|
+
if (fixes.length === 0) {
|
|
1095
|
+
console.log(' • Nothing to fix — lockfile structure is already consistent');
|
|
1096
|
+
} else {
|
|
1097
|
+
fixes.forEach(fix => console.log(` • ${fix}`));
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
if (hasWrite) {
|
|
1101
|
+
createBackup(filePath);
|
|
1102
|
+
fs.writeFileSync(filePath, JSON.stringify(fixedLockfile, null, 2) + '\n', 'utf8');
|
|
1103
|
+
console.log(`Changes written to ${filePath}`);
|
|
1104
|
+
} else {
|
|
1105
|
+
console.log('\nUse --write flag to save changes');
|
|
1106
|
+
console.log(JSON.stringify(fixedLockfile, null, 2));
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
function runBackupsCommand() {
|
|
1111
|
+
const filePath = getFilePath(positionals()[0]);
|
|
1112
|
+
const fileName = path.basename(filePath);
|
|
1113
|
+
|
|
1114
|
+
const backups = listBackups(filePath);
|
|
1115
|
+
|
|
1116
|
+
if (backups.length === 0) {
|
|
1117
|
+
console.log(`\nNo backups found for ${fileName}`);
|
|
1118
|
+
} else {
|
|
1119
|
+
console.log(`\nBackups for ${fileName}:`);
|
|
1120
|
+
backups.forEach((backup, i) => {
|
|
1121
|
+
console.log(` ${i + 1}. ${backup.name} (${backup.created.toLocaleString()})`);
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
function runRestoreCommand() {
|
|
1127
|
+
const filePath = getFilePath(positionals()[0]);
|
|
1128
|
+
ensureFileExists(filePath);
|
|
1129
|
+
|
|
1130
|
+
restoreFromLatestBackup(filePath);
|
|
1131
|
+
console.log(`\nFile restored successfully`);
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
function runCleanBackupsCommand() {
|
|
1135
|
+
const filePath = getFilePath(positionals()[0]);
|
|
1136
|
+
|
|
1137
|
+
// Parse --keep flag
|
|
1138
|
+
let keepCount = 5;
|
|
1139
|
+
const keepIndex = argv.indexOf('--keep');
|
|
1140
|
+
if (keepIndex !== -1 && argv[keepIndex + 1]) {
|
|
1141
|
+
const parsed = parseInt(argv[keepIndex + 1], 10);
|
|
1142
|
+
if (!isNaN(parsed) && parsed > 0) {
|
|
1143
|
+
keepCount = parsed;
|
|
1144
|
+
} else {
|
|
1145
|
+
console.error('Invalid --keep value. Must be a positive number');
|
|
1146
|
+
process.exit(2);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
const deleted = cleanOldBackups(filePath, keepCount);
|
|
1151
|
+
if (deleted === 0) {
|
|
1152
|
+
console.log(`\nNo old backups to clean (keeping ${keepCount})`);
|
|
1153
|
+
} else {
|
|
1154
|
+
console.log(`\nCleaned ${deleted} old backup(s), keeping ${keepCount}`);
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
// Validate the --check flag, exiting on a bad value.
|
|
1159
|
+
function parseCheckType() {
|
|
1160
|
+
const raw = flagValue('--check');
|
|
1161
|
+
if (raw === undefined) return 'all';
|
|
1162
|
+
if (!['hash', 'license', 'all'].includes(raw)) {
|
|
1163
|
+
console.error('Invalid check type. Use: hash, license, or all');
|
|
1164
|
+
process.exit(2);
|
|
1165
|
+
}
|
|
1166
|
+
return raw;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
// Print the integrity (hash) check result: counts, mismatches, unresolved entries.
|
|
1170
|
+
function printHashResult(hashResult, failOnUnresolved) {
|
|
1171
|
+
clearProgressLine();
|
|
1172
|
+
console.log(` Checked: ${hashResult.checked}`);
|
|
1173
|
+
console.log(` Passed: ${hashResult.passed}`);
|
|
1174
|
+
console.log(` Failed: ${hashResult.failed}`);
|
|
1175
|
+
console.log(` Skipped: ${hashResult.skipped}`);
|
|
1176
|
+
console.log(` Unresolved: ${hashResult.unresolved}`);
|
|
1177
|
+
|
|
1178
|
+
const mismatches = hashResult.errors.filter(e => e.expected && e.actual);
|
|
1179
|
+
if (mismatches.length > 0) {
|
|
1180
|
+
console.log('\n Hash mismatches (lockfile differs from registry):');
|
|
1181
|
+
mismatches.forEach(err => {
|
|
1182
|
+
console.log(` • ${err.package}`);
|
|
1183
|
+
console.log(` Registry: ${err.expected.slice(0, 50)}...`);
|
|
1184
|
+
console.log(` Lockfile: ${err.actual.slice(0, 50)}...`);
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
if (hashResult.unresolvedItems.length > 0) {
|
|
1188
|
+
console.log('\n Unresolved (could not verify against the registry):');
|
|
1189
|
+
hashResult.unresolvedItems.forEach(item => {
|
|
1190
|
+
console.log(` • ${item.package}@${item.version}: ${item.reason}`);
|
|
1191
|
+
});
|
|
1192
|
+
if (failOnUnresolved) {
|
|
1193
|
+
console.log(' (unresolved entries fail the check — they could not be verified; pass --allow-unresolved to treat them as non-fatal)');
|
|
1194
|
+
} else {
|
|
1195
|
+
console.log(' (unresolved entries are NOT failing the check because --allow-unresolved is set)');
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
// Print the license check result: counts, unapproved licenses, warnings.
|
|
1201
|
+
function printLicenseResult(licenseResult, strict) {
|
|
1202
|
+
clearProgressLine();
|
|
1203
|
+
console.log(` Checked: ${licenseResult.checked}`);
|
|
1204
|
+
console.log(` Approved: ${licenseResult.approved}`);
|
|
1205
|
+
console.log(` Rejected: ${licenseResult.rejected}`);
|
|
1206
|
+
console.log(` Unknown: ${licenseResult.unknown}`);
|
|
1207
|
+
|
|
1208
|
+
if (licenseResult.errors.length > 0) {
|
|
1209
|
+
console.log('\n Unapproved/Unknown licenses:');
|
|
1210
|
+
licenseResult.errors.forEach(err => {
|
|
1211
|
+
console.log(` • ${err.package}: ${err.license || 'UNKNOWN'}`);
|
|
1212
|
+
});
|
|
1213
|
+
}
|
|
1214
|
+
if (licenseResult.warnings.length > 0 && !strict) {
|
|
1215
|
+
console.log('\n Warnings (unknown licenses):');
|
|
1216
|
+
licenseResult.warnings.forEach(warn => {
|
|
1217
|
+
console.log(` • ${warn.package}: ${warn.license || 'UNKNOWN'}`);
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
async function runCheckCommand(command) {
|
|
1223
|
+
const filePath = getFilePath(positionals()[0]);
|
|
1224
|
+
ensureFileExists(filePath);
|
|
1225
|
+
|
|
1226
|
+
const checkType = parseCheckType();
|
|
1227
|
+
const strict = argv.includes('--strict');
|
|
1228
|
+
const licensesCsv = flagValue('--licenses-csv') || './approved-licenses.csv';
|
|
1229
|
+
// Fail closed by default: an entry we couldn't verify against the registry must
|
|
1230
|
+
// not pass as "verified". `--allow-unresolved` opts back into lenient behavior.
|
|
1231
|
+
const failOnUnresolved = !argv.includes('--allow-unresolved');
|
|
1232
|
+
// Registry-verification flags (hash check)
|
|
1233
|
+
const { concurrency, timeoutMs, defaultRegistry } = parseNetworkFlags();
|
|
1234
|
+
|
|
1235
|
+
const lockfile = parseLockfile(filePath);
|
|
1236
|
+
const onProgress = makeProgressReporter();
|
|
1237
|
+
const options = { onProgress, strict, csvPath: licensesCsv };
|
|
1238
|
+
|
|
1239
|
+
let allValid = true;
|
|
1240
|
+
try {
|
|
1241
|
+
// Run hash check (verifies locked integrity against the registry)
|
|
1242
|
+
if (checkType === 'hash' || checkType === 'all') {
|
|
1243
|
+
console.log('Verifying integrity against the registry...');
|
|
1244
|
+
const hashResult = await checkIntegrity(lockfile, {
|
|
1245
|
+
...options, concurrency, timeoutMs, failOnUnresolved, ...registryOption(defaultRegistry)
|
|
1246
|
+
});
|
|
1247
|
+
printHashResult(hashResult, failOnUnresolved);
|
|
1248
|
+
allValid = allValid && hashResult.valid;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
// Run license check
|
|
1252
|
+
if (checkType === 'license' || checkType === 'all') {
|
|
1253
|
+
console.log('\nChecking licenses...');
|
|
1254
|
+
const licenseResult = await checkLicenses(lockfile, options);
|
|
1255
|
+
printLicenseResult(licenseResult, strict);
|
|
1256
|
+
allValid = allValid && licenseResult.valid;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
console.log(allValid ? '\nAll checks passed' : '\nSome checks failed');
|
|
1260
|
+
process.exit(allValid ? 0 : 1);
|
|
1261
|
+
} catch (error) {
|
|
1262
|
+
handleError(error, `${command} command failed`);
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
// Print the vuln-scan result in pretty form: counts, advisories, unresolved entries.
|
|
1267
|
+
function printVulnResult(result, minSeverity, failOnUnresolved) {
|
|
1268
|
+
console.log(` Scanned: ${result.scanned}`);
|
|
1269
|
+
console.log(` Vulnerable: ${result.vulnerable}`);
|
|
1270
|
+
console.log(` Skipped: ${result.skipped}`);
|
|
1271
|
+
console.log(` Unresolved: ${result.unresolved}`);
|
|
1272
|
+
|
|
1273
|
+
if (result.errors.length > 0) {
|
|
1274
|
+
console.log(`\n Vulnerabilities at/above ${minSeverity} (fail the run):`);
|
|
1275
|
+
result.errors.forEach(e => {
|
|
1276
|
+
// Skip only reason-bearing (unresolved) errors — rendered separately below.
|
|
1277
|
+
// A genuine advisory, even one lacking an `id`, must still be printed.
|
|
1278
|
+
if (e.reason) return;
|
|
1279
|
+
console.log(` • ${e.package}@${e.version}: ${e.title} (${e.severity})`);
|
|
1280
|
+
if (e.url) console.log(` ${e.url}`);
|
|
1281
|
+
});
|
|
1282
|
+
}
|
|
1283
|
+
if (result.warnings.length > 0) {
|
|
1284
|
+
console.log(`\n Advisories below ${minSeverity} (warnings):`);
|
|
1285
|
+
result.warnings.forEach(w => {
|
|
1286
|
+
console.log(` • ${w.package}@${w.version}: ${w.title} (${w.severity})`);
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
1289
|
+
if (result.unresolvedItems.length > 0) {
|
|
1290
|
+
console.log('\n Unresolved (could not check against the registry):');
|
|
1291
|
+
result.unresolvedItems.forEach(item => {
|
|
1292
|
+
console.log(` • ${item.package}@${item.version}: ${item.reason}`);
|
|
1293
|
+
});
|
|
1294
|
+
if (failOnUnresolved) {
|
|
1295
|
+
console.log(' (unresolved entries FAIL the scan — these packages could not be checked; pass --allow-unresolved to treat them as non-fatal)');
|
|
1296
|
+
} else {
|
|
1297
|
+
console.log(' (unresolved entries are NOT failing the scan because --allow-unresolved is set)');
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
console.log(result.valid ? '\nNo known vulnerabilities at/above the threshold' : '\nScan failed (vulnerabilities found or packages could not be scanned)');
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
async function runVulnCommand(command) {
|
|
1304
|
+
const filePath = getFilePath(positionals()[0]);
|
|
1305
|
+
// `count=` has no meaning for a per-package severity scanner — reject it early
|
|
1306
|
+
// so a CI pipeline that meant `--fail-on severity=` never silently passes.
|
|
1307
|
+
assertFailOnSupported({ severity: true, count: false }, 'vuln');
|
|
1308
|
+
requireLockfileOrExit2(filePath, 'vuln');
|
|
1309
|
+
|
|
1310
|
+
const format = parseFormatFlag(['human', 'json'], 'human');
|
|
1311
|
+
const minSeverity = resolveSeverityGate();
|
|
1312
|
+
const offline = argv.includes('--offline');
|
|
1313
|
+
// Fail closed by default: a package the scan couldn't check (registry down /
|
|
1314
|
+
// endpoint unsupported) must not pass as "no vulnerabilities". Opt out with
|
|
1315
|
+
// `--allow-unresolved` (or skip the network entirely with `--offline`).
|
|
1316
|
+
const failOnUnresolved = !argv.includes('--allow-unresolved');
|
|
1317
|
+
const { concurrency, timeoutMs, defaultRegistry } = parseNetworkFlags(2);
|
|
1318
|
+
|
|
1319
|
+
const lockfile = parseLockfile(filePath);
|
|
1320
|
+
const onProgress = format === 'human' ? makeProgressReporter() : null;
|
|
1321
|
+
|
|
1322
|
+
try {
|
|
1323
|
+
if (format === 'human' && !offline) console.log('Scanning locked packages for known vulnerabilities…');
|
|
1324
|
+
const result = await checkVulnerabilities(lockfile, {
|
|
1325
|
+
concurrency, timeoutMs, minSeverity, offline, failOnUnresolved, onProgress,
|
|
1326
|
+
...registryOption(defaultRegistry)
|
|
1327
|
+
});
|
|
1328
|
+
|
|
1329
|
+
if (onProgress) clearProgressLine();
|
|
1330
|
+
|
|
1331
|
+
const exitCode = result.valid ? 0 : 1;
|
|
1332
|
+
if (format === 'json') {
|
|
1333
|
+
// The shared finding-schema envelope is the ONLY thing on stdout in json mode.
|
|
1334
|
+
const target = path.relative(process.cwd(), filePath) || filePath;
|
|
1335
|
+
console.log(JSON.stringify(vulnEnvelope(result, { target, exitCode }), null, 2));
|
|
1336
|
+
} else {
|
|
1337
|
+
printVulnResult(result, minSeverity, failOnUnresolved);
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
process.exit(exitCode);
|
|
1341
|
+
} catch (error) {
|
|
1342
|
+
handleError(error, `${command} command failed`);
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
// Print the deprecation-scan result in pretty form: counts, notices, unresolved, verdict.
|
|
1347
|
+
function printDeprecatedResult(result, failOnUnresolved) {
|
|
1348
|
+
console.log(` Scanned: ${result.scanned}`);
|
|
1349
|
+
console.log(` Deprecated: ${result.deprecated}`);
|
|
1350
|
+
console.log(` Skipped: ${result.skipped}`);
|
|
1351
|
+
console.log(` Unresolved: ${result.unresolved}`);
|
|
1352
|
+
|
|
1353
|
+
if (result.errors.some(e => e.message)) {
|
|
1354
|
+
console.log('\n Deprecated (fail the run):');
|
|
1355
|
+
result.errors.forEach(e => {
|
|
1356
|
+
if (!e.message) return;
|
|
1357
|
+
console.log(` • ${e.package}@${e.version}: ${e.message}`);
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
if (result.warnings.length > 0) {
|
|
1361
|
+
console.log('\n Deprecated (warnings):');
|
|
1362
|
+
result.warnings.forEach(w => {
|
|
1363
|
+
console.log(` • ${w.package}@${w.version}: ${w.message}`);
|
|
1364
|
+
});
|
|
1365
|
+
}
|
|
1366
|
+
if (result.unresolvedItems.length > 0) {
|
|
1367
|
+
console.log('\n Unresolved (could not check against the registry):');
|
|
1368
|
+
result.unresolvedItems.forEach(item => {
|
|
1369
|
+
console.log(` • ${item.package}@${item.version}: ${item.reason}`);
|
|
1370
|
+
});
|
|
1371
|
+
if (failOnUnresolved) {
|
|
1372
|
+
console.log(' (unresolved entries FAIL the scan — these packages could not be checked; pass --allow-unresolved to treat them as non-fatal)');
|
|
1373
|
+
} else {
|
|
1374
|
+
console.log(' (unresolved entries are NOT failing the scan because --allow-unresolved is set)');
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
if (!result.valid && result.deprecated === 0) {
|
|
1378
|
+
console.log('\nScan incomplete — some packages could not be checked (see unresolved above)');
|
|
1379
|
+
} else if (result.deprecated === 0) {
|
|
1380
|
+
console.log('\nNo deprecated packages found');
|
|
1381
|
+
} else if (result.valid) {
|
|
1382
|
+
console.log('\nDeprecated packages found (warnings; pass --fail-on count=0 to fail the run)');
|
|
1383
|
+
} else {
|
|
1384
|
+
console.log('\nDeprecated packages found');
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
async function runDeprecatedCommand(command) {
|
|
1389
|
+
const filePath = getFilePath(positionals()[0]);
|
|
1390
|
+
// Deprecation is not on the severity ladder (`severity=` is a no-op here).
|
|
1391
|
+
// Only `count=0` (fail on ANY deprecation) is supported; a non-zero budget
|
|
1392
|
+
// (count=3) has no defined semantics for this command — reject both early.
|
|
1393
|
+
assertFailOnSupported({ severity: false, count: 'zero-only' }, 'deprecated');
|
|
1394
|
+
requireLockfileOrExit2(filePath, 'deprecated');
|
|
1395
|
+
|
|
1396
|
+
const format = parseFormatFlag(['human', 'json'], 'human');
|
|
1397
|
+
const offline = argv.includes('--offline');
|
|
1398
|
+
// Deprecation isn't on the severity ladder, so the canonical gate spelling is
|
|
1399
|
+
// `--fail-on count=0` (fail on any deprecation); `--fail-on-deprecated` is the
|
|
1400
|
+
// deprecated alias for the same effect.
|
|
1401
|
+
const failOnDeprecated = resolveFailOnDeprecated() || parseFailOn().count === 0;
|
|
1402
|
+
// Fail closed by default: a package the scan couldn't check must not pass as
|
|
1403
|
+
// clean. `--allow-unresolved` (or `--offline`) opts back into lenient behavior.
|
|
1404
|
+
const failOnUnresolved = !argv.includes('--allow-unresolved');
|
|
1405
|
+
const { concurrency, timeoutMs, defaultRegistry } = parseNetworkFlags(2);
|
|
1406
|
+
|
|
1407
|
+
const lockfile = parseLockfile(filePath);
|
|
1408
|
+
const onProgress = format === 'human' ? makeProgressReporter() : null;
|
|
1409
|
+
|
|
1410
|
+
try {
|
|
1411
|
+
if (format === 'human' && !offline) console.log('Scanning locked packages for deprecation notices…');
|
|
1412
|
+
const result = await checkDeprecations(lockfile, {
|
|
1413
|
+
concurrency, timeoutMs, offline, failOnDeprecated, failOnUnresolved, onProgress,
|
|
1414
|
+
...registryOption(defaultRegistry)
|
|
1415
|
+
});
|
|
1416
|
+
|
|
1417
|
+
if (onProgress) clearProgressLine();
|
|
1418
|
+
|
|
1419
|
+
const exitCode = result.valid ? 0 : 1;
|
|
1420
|
+
if (format === 'json') {
|
|
1421
|
+
// The shared finding-schema envelope is the ONLY thing on stdout in json mode.
|
|
1422
|
+
const target = path.relative(process.cwd(), filePath) || filePath;
|
|
1423
|
+
console.log(JSON.stringify(deprecationEnvelope(result, { target, exitCode }), null, 2));
|
|
1424
|
+
} else {
|
|
1425
|
+
printDeprecatedResult(result, failOnUnresolved);
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
process.exit(exitCode);
|
|
1429
|
+
} catch (error) {
|
|
1430
|
+
handleError(error, `${command} command failed`);
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
// Print the remediation result in pretty form: bumps, guidance, skips, warnings.
|
|
1435
|
+
function printRemediateResult(result) {
|
|
1436
|
+
console.log('\nRemediation Results:');
|
|
1437
|
+
if (result.bumped.length === 0) {
|
|
1438
|
+
console.log(' • No direct dependencies to bump');
|
|
1439
|
+
} else {
|
|
1440
|
+
result.bumped.forEach((b) => {
|
|
1441
|
+
console.log(` • ${b.section}/${b.package} ${b.from} → ${b.to} (${b.reasons.join(', ')})`);
|
|
1442
|
+
});
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
if (result.guidance.length > 0) {
|
|
1446
|
+
console.log('\n Transitive / manual (not a direct dep — bump the parent or add an npm override):');
|
|
1447
|
+
result.guidance.forEach((g) => {
|
|
1448
|
+
const note = g.kind === 'latest-still-affected' ? `${g.reasons.join(', ')}; latest still affected` : g.reasons.join(', ');
|
|
1449
|
+
console.log(` • ${g.package}: ${note}`);
|
|
1450
|
+
});
|
|
1451
|
+
}
|
|
1452
|
+
if (result.skipped.length > 0) {
|
|
1453
|
+
console.log('\n Skipped:');
|
|
1454
|
+
result.skipped.forEach((s) => console.log(` • ${s.section}/${s.package} (${s.range}): ${s.reason}`));
|
|
1455
|
+
}
|
|
1456
|
+
result.warnings.forEach((w) => console.log(`\n${w.package}: ${w.reason}`));
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
async function runRemediateCommand(command) {
|
|
1460
|
+
// Operates on a directory containing both package.json and the lockfile.
|
|
1461
|
+
const dir = getDirArg();
|
|
1462
|
+
const hasWrite = argv.includes('--write');
|
|
1463
|
+
|
|
1464
|
+
const packageJsonPath = path.join(dir, 'package.json');
|
|
1465
|
+
const lockfilePath = path.join(dir, 'package-lock.json');
|
|
1466
|
+
ensureFileExists(packageJsonPath);
|
|
1467
|
+
ensureFileExists(lockfilePath);
|
|
1468
|
+
|
|
1469
|
+
const format = parseFormatFlag(['human', 'json'], 'human');
|
|
1470
|
+
const minSeverity = resolveSeverityGate();
|
|
1471
|
+
const includeDeprecated = !argv.includes('--no-deprecated');
|
|
1472
|
+
const defaultRegistry = flagValue('--registry');
|
|
1473
|
+
|
|
1474
|
+
const packageJsonRaw = fs.readFileSync(packageJsonPath, 'utf8');
|
|
1475
|
+
const packageJson = JSON.parse(packageJsonRaw);
|
|
1476
|
+
const lockfile = parseLockfile(lockfilePath);
|
|
1477
|
+
|
|
1478
|
+
const onProgress = format === 'human' ? makeProgressReporter() : null;
|
|
1479
|
+
|
|
1480
|
+
try {
|
|
1481
|
+
if (format === 'human') console.log('Scanning for remediable direct dependencies…');
|
|
1482
|
+
const result = await remediateDependencies(lockfile, packageJson, {
|
|
1483
|
+
minSeverity, includeDeprecated, onProgress,
|
|
1484
|
+
...registryOption(defaultRegistry)
|
|
1485
|
+
});
|
|
1486
|
+
if (onProgress) clearProgressLine();
|
|
1487
|
+
|
|
1488
|
+
if (format === 'json') {
|
|
1489
|
+
// The shared finding-schema envelope is the ONLY thing on stdout in json mode.
|
|
1490
|
+
// remediate is an action command, not a CI gate — it always exits 0.
|
|
1491
|
+
const target = path.relative(process.cwd(), dir) || dir;
|
|
1492
|
+
const scanned = lockfile && lockfile.packages && typeof lockfile.packages === 'object'
|
|
1493
|
+
? Object.keys(lockfile.packages).filter((k) => k !== '').length : 0;
|
|
1494
|
+
console.log(JSON.stringify(remediateEnvelope(result, { target, scanned, exitCode: 0 }), null, 2));
|
|
1495
|
+
process.exit(0);
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
printRemediateResult(result);
|
|
1499
|
+
|
|
1500
|
+
if (hasWrite && result.changed) {
|
|
1501
|
+
const indent = detectIndent(packageJsonRaw);
|
|
1502
|
+
writeJsonFile(packageJsonPath, result.packageJson, indent);
|
|
1503
|
+
writeJsonFile(lockfilePath, result.lockfile);
|
|
1504
|
+
console.log(`\nChanges written to ${packageJsonPath} and ${lockfilePath}`);
|
|
1505
|
+
console.log(' Run `npm install` to re-resolve the dependency tree, then re-run `npm-check`.');
|
|
1506
|
+
} else if (result.changed) {
|
|
1507
|
+
console.log('\nUse --write to apply these bumps (then run `npm install`)');
|
|
1508
|
+
}
|
|
1509
|
+
process.exit(0);
|
|
1510
|
+
} catch (error) {
|
|
1511
|
+
handleError(error, `${command} command failed`);
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
// Dispatch table: command name → handler. Handlers that report their own
|
|
1516
|
+
// errors with the originating command name take it as an argument.
|
|
1517
|
+
const COMMAND_HANDLERS = {
|
|
1518
|
+
report: () => runReportCommand(),
|
|
1519
|
+
validate: () => runValidateCommand(),
|
|
1520
|
+
migrate: () => runMigrateCommand(),
|
|
1521
|
+
upgrade: () => runUpgradeCommand(),
|
|
1522
|
+
'fix-checksums': () => runFixChecksumsCommand(),
|
|
1523
|
+
pin: () => runPinCommand(),
|
|
1524
|
+
prune: () => runPruneCommand(),
|
|
1525
|
+
unused: () => runUnusedCommand(),
|
|
1526
|
+
audit: () => runAuditCommand(),
|
|
1527
|
+
'upgrade-hashes': () => runUpgradeHashesCommand(),
|
|
1528
|
+
dedupe: () => runDedupeCommand(),
|
|
1529
|
+
fix: () => runFixCommand(),
|
|
1530
|
+
backups: () => runBackupsCommand(),
|
|
1531
|
+
restore: () => runRestoreCommand(),
|
|
1532
|
+
'clean-backups': () => runCleanBackupsCommand(),
|
|
1533
|
+
check: (command) => runCheckCommand(command),
|
|
1534
|
+
vuln: (command) => runVulnCommand(command),
|
|
1535
|
+
deprecated: (command) => runDeprecatedCommand(command),
|
|
1536
|
+
remediate: (command) => runRemediateCommand(command)
|
|
1537
|
+
};
|
|
1538
|
+
|
|
1539
|
+
async function main() {
|
|
1540
|
+
if (argv.includes('-h') || argv.includes('--help')) {
|
|
1541
|
+
printHelp();
|
|
1542
|
+
return;
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
// Version is long-only (`--version`); `-v` is intentionally NOT a version alias
|
|
1546
|
+
// (canonical suite vocabulary: `-v` is never version).
|
|
1547
|
+
if (argv.includes('--version')) {
|
|
1548
|
+
console.log(`npm-check version ${getVersion()}`);
|
|
1549
|
+
return;
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
// Fail closed on a typo'd/unknown flag (exit 2) — a silently-dropped option
|
|
1553
|
+
// could disable the CI gate. Runs after --help/--version so those still work.
|
|
1554
|
+
rejectUnknownOptions();
|
|
1555
|
+
|
|
1556
|
+
// Bare `npm-check` (no command, or only flags) runs the full report.
|
|
1557
|
+
const command = (!argv[0] || argv[0].startsWith('-')) ? 'report' : argv[0];
|
|
1558
|
+
|
|
1559
|
+
const handler = COMMAND_HANDLERS[command];
|
|
1560
|
+
if (!handler) {
|
|
1561
|
+
// Unknown command is a usage error → exit 2 (suite convention).
|
|
1562
|
+
console.error(`Unknown command: ${command}`);
|
|
1563
|
+
printHelp();
|
|
1564
|
+
process.exit(2);
|
|
1565
|
+
return;
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
try {
|
|
1569
|
+
await handler(command);
|
|
1570
|
+
} catch (error) {
|
|
1571
|
+
handleError(error, `${command} command failed`);
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
main().catch(error => {
|
|
1576
|
+
handleError(error, 'Fatal error');
|
|
1577
|
+
});
|