inup 1.5.6 → 1.6.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 +29 -0
- package/dist/cli.js +35 -11
- package/dist/config/constants.js +1 -5
- package/dist/config/project-config.js +5 -0
- package/dist/core/package-detector.js +29 -1
- package/dist/core/upgrade-runner.js +1 -0
- package/dist/core/upgrader.js +9 -3
- package/dist/features/changelog/services/package-metadata-service.js +2 -13
- package/dist/features/changelog/services/release-notes-service.js +0 -27
- package/dist/features/headless/headless-runner.js +53 -0
- package/dist/features/headless/index.js +27 -0
- package/dist/features/headless/report.js +64 -0
- package/dist/features/headless/types.js +6 -0
- package/dist/features/headless/vulnerability-audit.js +106 -0
- package/dist/interactive-ui.js +4 -2
- package/dist/services/index.js +0 -1
- package/dist/services/npm-registry.js +9 -17
- package/dist/services/package-manager-detector.js +6 -3
- package/dist/ui/modal/package-info-sections/sections.js +24 -0
- package/dist/ui/presenters/health.js +24 -0
- package/dist/ui/renderer/package-list/rows.js +8 -3
- package/dist/ui/session/selection-state-builder.js +7 -2
- package/dist/ui/themes-colors.js +12 -3
- package/dist/ui/utils/cursor.js +9 -3
- package/dist/utils/color.js +38 -0
- package/dist/utils/engines.js +63 -0
- package/dist/utils/filesystem/io.js +16 -0
- package/dist/utils/filesystem/scan.js +82 -7
- package/dist/utils/index.js +3 -0
- package/dist/utils/manifest.js +35 -0
- package/dist/utils/version.js +9 -2
- package/package.json +3 -3
- package/dist/services/jsdelivr/client.js +0 -191
- package/dist/services/jsdelivr/manifest.js +0 -136
- package/dist/services/jsdelivr-registry.js +0 -9
package/README.md
CHANGED
|
@@ -49,9 +49,38 @@ inup [options]
|
|
|
49
49
|
-i, --ignore <packages> Ignore packages (comma-separated, glob supported)
|
|
50
50
|
--max-depth <number> Maximum scan depth for package discovery (default: 10)
|
|
51
51
|
--package-manager <name> Force package manager (npm, yarn, pnpm, bun)
|
|
52
|
+
--json Print a machine-readable JSON report and exit (read-only)
|
|
53
|
+
-c, --check Exit non-zero if updates exist, without writing (for CI)
|
|
52
54
|
--debug Write verbose debug logs
|
|
53
55
|
```
|
|
54
56
|
|
|
57
|
+
## CI & Scripting
|
|
58
|
+
|
|
59
|
+
`inup` runs headless automatically when stdout isn't a TTY or `$CI` is set, so it never hangs in a
|
|
60
|
+
pipeline waiting on the interactive UI. Both `--json` and `--check` are **read-only** — they report,
|
|
61
|
+
they never edit `package.json` or install.
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
inup --check # exit 1 if anything is outdated → fails the build
|
|
65
|
+
inup --json | jq # structured drift report for dashboards/bots
|
|
66
|
+
inup | cat # plain line-based report when piped to a log
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Each reported package carries its health signals: `deprecated` (npm deprecation message), `enginesNode`
|
|
70
|
+
(declared `engines.node`), and `vulnerability` (known advisories on the currently-installed version,
|
|
71
|
+
from one bulk `npm audit`-style request). Every advisory is **cross-referenced against the upgrade
|
|
72
|
+
targets**, so you know whether the upgrade actually fixes it:
|
|
73
|
+
|
|
74
|
+
- `vulnerability.advisories[].fixedByRange` / `fixedByLatest` — does the in-range / latest target escape
|
|
75
|
+
this advisory's affected range?
|
|
76
|
+
- `vulnerability.fixedByRange` / `fixedByLatest` — does the target clear **every** advisory?
|
|
77
|
+
|
|
78
|
+
The summary includes a `vulnerable` count, and the payload carries a `schemaVersion` so scripts and
|
|
79
|
+
agents can pin to a known shape.
|
|
80
|
+
|
|
81
|
+
Output hygiene: with `--json`, stdout carries **only** the JSON document; all progress and warnings go
|
|
82
|
+
to stderr. Exit codes: `0` up to date, `1` updates exist (`--check`), `2` error.
|
|
83
|
+
|
|
55
84
|
## Keyboard Shortcuts
|
|
56
85
|
|
|
57
86
|
<!-- KEYS:START -->
|
package/dist/cli.js
CHANGED
|
@@ -9,6 +9,7 @@ const commander_1 = require("commander");
|
|
|
9
9
|
const chalk_1 = __importDefault(require("chalk"));
|
|
10
10
|
const path_1 = require("path");
|
|
11
11
|
const index_1 = require("./index");
|
|
12
|
+
const headless_1 = require("./features/headless");
|
|
12
13
|
const services_1 = require("./services");
|
|
13
14
|
const config_1 = require("./config");
|
|
14
15
|
const utils_1 = require("./utils");
|
|
@@ -16,16 +17,24 @@ const git_1 = require("./utils/git");
|
|
|
16
17
|
const ui_1 = require("./ui");
|
|
17
18
|
const program = new commander_1.Command();
|
|
18
19
|
async function runCli(options) {
|
|
20
|
+
// Resolve colored-output intent before anything renders.
|
|
21
|
+
(0, utils_1.applyColorSetting)(options.color);
|
|
19
22
|
const cwd = (0, path_1.resolve)(options.dir);
|
|
20
23
|
if (options.debug || process.env.INUP_DEBUG === '1') {
|
|
21
24
|
(0, utils_1.enableDebugLogging)();
|
|
22
25
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
// Headless when piped, in CI, or when a non-interactive flag is set. The TUI only renders in
|
|
27
|
+
// interactive mode; everything else routes through the read-only headless report path.
|
|
28
|
+
const interactive = !!process.stdout.isTTY && !process.env.CI && !options.json && !options.check;
|
|
29
|
+
// The dirty-tree prompt would hang without a TTY; headless is read-only anyway, so skip it.
|
|
30
|
+
if (interactive) {
|
|
31
|
+
const gitState = (0, git_1.getGitWorkingTreeState)(cwd);
|
|
32
|
+
if (gitState.isRepo && gitState.isDirty) {
|
|
33
|
+
const shouldProceed = await ui_1.TerminalInput.promptForImmediateConfirmation(`${chalk_1.default.yellow('Warning:')} dirty working tree. Proceed anyway? ${chalk_1.default.dim('[y/N]')} `, false);
|
|
34
|
+
if (!shouldProceed) {
|
|
35
|
+
console.log(chalk_1.default.yellow('Upgrade cancelled.'));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
29
38
|
}
|
|
30
39
|
}
|
|
31
40
|
// Load project config from .inuprc
|
|
@@ -52,8 +61,11 @@ async function runCli(options) {
|
|
|
52
61
|
console.error(chalk_1.default.yellow('Expected a non-negative integer, for example: --max-depth 10'));
|
|
53
62
|
process.exit(1);
|
|
54
63
|
}
|
|
55
|
-
// Check for updates in the background (non-blocking)
|
|
56
|
-
|
|
64
|
+
// Check for updates in the background (non-blocking). Interactive only — keeps headless stdout
|
|
65
|
+
// clean and avoids a lingering fetch handle in CI.
|
|
66
|
+
const updateCheckPromise = interactive
|
|
67
|
+
? (0, services_1.checkForUpdateAsync)(config_1.PACKAGE_NAME, config_1.PACKAGE_VERSION)
|
|
68
|
+
: undefined;
|
|
57
69
|
// Validate package manager if provided
|
|
58
70
|
let packageManager;
|
|
59
71
|
if (options.packageManager) {
|
|
@@ -65,17 +77,25 @@ async function runCli(options) {
|
|
|
65
77
|
}
|
|
66
78
|
packageManager = options.packageManager;
|
|
67
79
|
}
|
|
68
|
-
const
|
|
80
|
+
const runnerOptions = {
|
|
69
81
|
cwd,
|
|
70
82
|
excludePatterns,
|
|
83
|
+
scanDirs: projectConfig.scanDirs,
|
|
71
84
|
maxDepth,
|
|
72
85
|
ignorePackages,
|
|
73
86
|
packageManager,
|
|
74
87
|
showPeerDependencyVulnerabilities: projectConfig.showPeerDependencyVulnerabilities ?? false,
|
|
75
88
|
showOptionalDependencyVulnerabilities: projectConfig.showOptionalDependencyVulnerabilities ?? false,
|
|
76
89
|
debug: options.debug || process.env.INUP_DEBUG === '1',
|
|
77
|
-
|
|
78
|
-
|
|
90
|
+
saveExact: options.saveExact ?? false,
|
|
91
|
+
};
|
|
92
|
+
// Non-interactive (piped / CI / --json / --check) routes to the read-only headless feature;
|
|
93
|
+
// only the interactive path builds the full TUI runner.
|
|
94
|
+
if (!interactive) {
|
|
95
|
+
await new headless_1.HeadlessRunner(runnerOptions).run({ json: options.json, check: options.check });
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
await new index_1.UpgradeRunner(runnerOptions).run();
|
|
79
99
|
// After the main flow completes, check if there's an update available
|
|
80
100
|
const updateCheck = await updateCheckPromise;
|
|
81
101
|
if (updateCheck?.isOutdated) {
|
|
@@ -109,6 +129,10 @@ program
|
|
|
109
129
|
.option('--max-depth <number>', 'maximum directory depth for package.json discovery', '10')
|
|
110
130
|
.option('--package-manager <name>', 'manually specify package manager (npm, yarn, pnpm, bun)')
|
|
111
131
|
.option('--debug', 'write verbose debug log to /tmp/inup-debug-YYYY-MM-DD.log')
|
|
132
|
+
.option('--no-color', 'disable colored output (also respects NO_COLOR / FORCE_COLOR)')
|
|
133
|
+
.option('--save-exact', 'write exact versions instead of preserving the range prefix (^/~)')
|
|
134
|
+
.option('--json', 'print a machine-readable JSON report and exit (non-interactive, read-only)')
|
|
135
|
+
.option('-c, --check', 'exit non-zero if updates exist, without writing (for CI; read-only)')
|
|
112
136
|
.action(runCli);
|
|
113
137
|
// Handle uncaught errors gracefully
|
|
114
138
|
process.on('uncaughtException', (error) => {
|
package/dist/config/constants.js
CHANGED
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.REQUEST_TIMEOUT = exports.CACHE_TTL = exports.MAX_CONCURRENT_REQUESTS = exports.NPM_REGISTRY_URL = exports.PACKAGE_VERSION = exports.PACKAGE_NAME = void 0;
|
|
4
4
|
var package_meta_1 = require("./package-meta");
|
|
5
5
|
Object.defineProperty(exports, "PACKAGE_NAME", { enumerable: true, get: function () { return package_meta_1.PACKAGE_NAME; } });
|
|
6
6
|
Object.defineProperty(exports, "PACKAGE_VERSION", { enumerable: true, get: function () { return package_meta_1.PACKAGE_VERSION; } });
|
|
7
7
|
exports.NPM_REGISTRY_URL = 'https://registry.npmjs.org';
|
|
8
|
-
exports.JSDELIVR_CDN_URL = 'https://cdn.jsdelivr.net/npm';
|
|
9
8
|
exports.MAX_CONCURRENT_REQUESTS = 150;
|
|
10
9
|
exports.CACHE_TTL = 5 * 60 * 1000; // 5 minutes in milliseconds
|
|
11
10
|
exports.REQUEST_TIMEOUT = 60000; // 60 seconds in milliseconds
|
|
12
|
-
exports.JSDELIVR_RETRY_TIMEOUTS = [2000, 3500]; // short retry budget to keep fallback fast
|
|
13
|
-
exports.JSDELIVR_RETRY_DELAYS = [150]; // tiny backoff between jsDelivr retries in ms
|
|
14
|
-
exports.JSDELIVR_POOL_TIMEOUT = 60000; // keep-alive/connect lifecycle should be looser than per-request timeouts
|
|
15
11
|
//# sourceMappingURL=constants.js.map
|
|
@@ -54,6 +54,11 @@ function normalizeConfig(config) {
|
|
|
54
54
|
normalized.exclude = config.exclude.filter((item) => typeof item === 'string');
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
|
+
if (config.scanDirs) {
|
|
58
|
+
if (Array.isArray(config.scanDirs)) {
|
|
59
|
+
normalized.scanDirs = config.scanDirs.filter((item) => typeof item === 'string');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
57
62
|
if (typeof config.showPeerDependencyVulnerabilities === 'boolean') {
|
|
58
63
|
normalized.showPeerDependencyVulnerabilities = config.showPeerDependencyVulnerabilities;
|
|
59
64
|
}
|
|
@@ -32,8 +32,12 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
32
32
|
return result;
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
35
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
39
|
exports.PackageDetector = void 0;
|
|
40
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
37
41
|
const semver = __importStar(require("semver"));
|
|
38
42
|
const utils_1 = require("../utils");
|
|
39
43
|
const services_1 = require("../services");
|
|
@@ -46,6 +50,7 @@ class PackageDetector {
|
|
|
46
50
|
packageJson = null;
|
|
47
51
|
cwd;
|
|
48
52
|
excludePatterns;
|
|
53
|
+
scanDirs;
|
|
49
54
|
ignorePackages;
|
|
50
55
|
maxDepth;
|
|
51
56
|
batchSize = 10;
|
|
@@ -53,6 +58,7 @@ class PackageDetector {
|
|
|
53
58
|
constructor(options) {
|
|
54
59
|
this.cwd = options?.cwd || process.cwd();
|
|
55
60
|
this.excludePatterns = options?.excludePatterns || [];
|
|
61
|
+
this.scanDirs = options?.scanDirs || [];
|
|
56
62
|
this.ignorePackages = options?.ignorePackages || [];
|
|
57
63
|
this.maxDepth = options?.maxDepth ?? 10;
|
|
58
64
|
this.packageJsonPath = (0, utils_1.findPackageJson)(this.cwd);
|
|
@@ -282,6 +288,8 @@ class PackageDetector {
|
|
|
282
288
|
hasRangeUpdate,
|
|
283
289
|
hasMajorUpdate,
|
|
284
290
|
allVersions,
|
|
291
|
+
deprecated: packageData.deprecated,
|
|
292
|
+
enginesNode: packageData.enginesNode,
|
|
285
293
|
};
|
|
286
294
|
}
|
|
287
295
|
catch (error) {
|
|
@@ -313,13 +321,17 @@ class PackageDetector {
|
|
|
313
321
|
};
|
|
314
322
|
}
|
|
315
323
|
async findPackageJsonFilesWithTimeout(timeoutMs) {
|
|
324
|
+
const skippedPackageDirs = new Set();
|
|
316
325
|
try {
|
|
317
326
|
let timeoutId;
|
|
318
327
|
try {
|
|
319
|
-
|
|
328
|
+
const files = await Promise.race([
|
|
320
329
|
(0, utils_1.findAllPackageJsonFilesAsync)(this.cwd, this.excludePatterns, this.maxDepth, (currentDir, foundCount) => {
|
|
321
330
|
const truncatedDir = currentDir.length > 50 ? '...' + currentDir.slice(-47) : currentDir;
|
|
322
331
|
this.showProgress(`🔍 Scanning ${truncatedDir} (found ${foundCount})`);
|
|
332
|
+
}, {
|
|
333
|
+
scanDirs: this.scanDirs,
|
|
334
|
+
onSkippedPackageDir: (relativePath) => skippedPackageDirs.add(relativePath),
|
|
323
335
|
}),
|
|
324
336
|
new Promise((_, reject) => {
|
|
325
337
|
timeoutId = setTimeout(() => {
|
|
@@ -328,6 +340,8 @@ class PackageDetector {
|
|
|
328
340
|
timeoutId.unref?.();
|
|
329
341
|
}),
|
|
330
342
|
]);
|
|
343
|
+
this.warnSkippedPackageDirs(skippedPackageDirs);
|
|
344
|
+
return files;
|
|
331
345
|
}
|
|
332
346
|
finally {
|
|
333
347
|
if (timeoutId) {
|
|
@@ -339,6 +353,20 @@ class PackageDetector {
|
|
|
339
353
|
throw new Error(`Failed to scan for package.json files: ${err}. Try using --exclude patterns to skip problematic directories.`);
|
|
340
354
|
}
|
|
341
355
|
}
|
|
356
|
+
/**
|
|
357
|
+
* Warn (once per directory, after the scan) about package.json-bearing directories that the
|
|
358
|
+
* default skip list pruned, so the user can re-include them via `.inuprc`'s `scanDirs`.
|
|
359
|
+
* Emitted after scanning so it does not corrupt the progress spinner output.
|
|
360
|
+
*/
|
|
361
|
+
warnSkippedPackageDirs(skippedPackageDirs) {
|
|
362
|
+
if (skippedPackageDirs.size === 0) {
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
const list = Array.from(skippedPackageDirs).sort();
|
|
366
|
+
console.warn(chalk_1.default.yellow(`⚠️ Skipped ${list.length} package.json-bearing director${list.length === 1 ? 'y' : 'ies'} matching the default ignore list:\n` +
|
|
367
|
+
list.map((dir) => ` - ${dir}`).join('\n') +
|
|
368
|
+
`\n Add the directory name(s) to "scanDirs" in .inuprc to include them.`));
|
|
369
|
+
}
|
|
342
370
|
isWorkspaceReference(version) {
|
|
343
371
|
return (version.includes('workspace:') ||
|
|
344
372
|
version === '*' ||
|
|
@@ -32,6 +32,7 @@ class UpgradeRunner {
|
|
|
32
32
|
this.ui = new interactive_ui_1.InteractiveUI(this.packageManager, {
|
|
33
33
|
showPeerDependencyVulnerabilities: options?.showPeerDependencyVulnerabilities ?? false,
|
|
34
34
|
showOptionalDependencyVulnerabilities: options?.showOptionalDependencyVulnerabilities ?? false,
|
|
35
|
+
saveExact: options?.saveExact ?? false,
|
|
35
36
|
});
|
|
36
37
|
this.upgrader = new upgrader_1.PackageUpgrader(this.packageManager);
|
|
37
38
|
}
|
package/dist/core/upgrader.js
CHANGED
|
@@ -92,7 +92,8 @@ class PackageUpgrader {
|
|
|
92
92
|
const packageDir = packageJsonPath.replace('/package.json', '');
|
|
93
93
|
const spinner = (0, nanospinner_1.createSpinner)(`Upgrading ${type} in ${packageDir}...`).start();
|
|
94
94
|
try {
|
|
95
|
-
// Read the current package.json
|
|
95
|
+
// Read the current package.json — keep the raw text so we can round-trip its formatting
|
|
96
|
+
const rawContent = (0, fs_1.readFileSync)(packageJsonPath, 'utf-8');
|
|
96
97
|
const packageJson = (0, utils_1.readPackageJson)(packageJsonPath);
|
|
97
98
|
// Group by upgrade type (range vs latest)
|
|
98
99
|
const rangeChoices = choices.filter((c) => c.upgradeType === 'range');
|
|
@@ -118,9 +119,14 @@ class PackageUpgrader {
|
|
|
118
119
|
modified = true;
|
|
119
120
|
});
|
|
120
121
|
}
|
|
121
|
-
// Write back the modified package.json
|
|
122
|
+
// Write back the modified package.json, preserving the original indentation and
|
|
123
|
+
// trailing-newline style. Skip the write entirely when nothing actually changed.
|
|
122
124
|
if (modified) {
|
|
123
|
-
(0,
|
|
125
|
+
const format = (0, utils_1.detectJsonFormat)(rawContent);
|
|
126
|
+
const nextContent = JSON.stringify(packageJson, null, format.indent) + (format.trailingNewline ? '\n' : '');
|
|
127
|
+
if (nextContent !== rawContent) {
|
|
128
|
+
(0, fs_1.writeFileSync)(packageJsonPath, nextContent);
|
|
129
|
+
}
|
|
124
130
|
}
|
|
125
131
|
spinner.success({ text: `Upgraded ${choices.length} ${type} in ${packageDir}` });
|
|
126
132
|
// Show which packages were upgraded
|
|
@@ -3,16 +3,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.PackageMetadataService = void 0;
|
|
4
4
|
const npm_registry_client_1 = require("../clients/npm-registry-client");
|
|
5
5
|
const package_metadata_1 = require("../parsers/package-metadata");
|
|
6
|
-
const jsdelivr_registry_1 = require("../../../services/jsdelivr-registry");
|
|
7
6
|
const inflight_1 = require("../../../services/http/inflight");
|
|
8
7
|
class PackageMetadataService {
|
|
9
8
|
npmRegistryClient;
|
|
10
|
-
exactManifestFetcher;
|
|
11
9
|
cache = new Map();
|
|
12
10
|
inFlight = new inflight_1.InflightMap();
|
|
13
|
-
constructor(npmRegistryClient = new npm_registry_client_1.NpmRegistryClient()
|
|
11
|
+
constructor(npmRegistryClient = new npm_registry_client_1.NpmRegistryClient()) {
|
|
14
12
|
this.npmRegistryClient = npmRegistryClient;
|
|
15
|
-
this.exactManifestFetcher = exactManifestFetcher;
|
|
16
13
|
}
|
|
17
14
|
clearCache() {
|
|
18
15
|
this.cache.clear();
|
|
@@ -85,15 +82,7 @@ class PackageMetadataService {
|
|
|
85
82
|
}
|
|
86
83
|
async fetchPackageManifest(packageName, version, signal) {
|
|
87
84
|
signal?.throwIfAborted();
|
|
88
|
-
|
|
89
|
-
if (normalizedVersion) {
|
|
90
|
-
const jsdelivrManifest = await this.exactManifestFetcher(packageName, normalizedVersion);
|
|
91
|
-
if (jsdelivrManifest) {
|
|
92
|
-
return jsdelivrManifest;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
signal?.throwIfAborted();
|
|
96
|
-
return await this.npmRegistryClient.fetchPackageManifest(packageName, normalizedVersion || 'latest', signal);
|
|
85
|
+
return await this.npmRegistryClient.fetchPackageManifest(packageName, version?.trim() || 'latest', signal);
|
|
97
86
|
}
|
|
98
87
|
buildMetadata(packageName, rawData) {
|
|
99
88
|
return (0, package_metadata_1.mapPackageManifestToMetadata)(packageName, rawData);
|
|
@@ -38,7 +38,6 @@ const semver = __importStar(require("semver"));
|
|
|
38
38
|
const github_client_1 = require("../clients/github-client");
|
|
39
39
|
const changelog_parser_1 = require("../parsers/changelog-parser");
|
|
40
40
|
const github_release_html_parser_1 = require("../parsers/github-release-html-parser");
|
|
41
|
-
const constants_1 = require("../../../config/constants");
|
|
42
41
|
const RELEASE_NOTES_FETCH_TIMEOUT_MS = 5000;
|
|
43
42
|
const PREFER_GITHUB_RELEASE_PAGE = true;
|
|
44
43
|
class ReleaseNotesService {
|
|
@@ -89,9 +88,6 @@ class ReleaseNotesService {
|
|
|
89
88
|
return notes;
|
|
90
89
|
}
|
|
91
90
|
}
|
|
92
|
-
const changelogNotes = await this.fetchJsdelivrChangelog(packageName, version, signal);
|
|
93
|
-
if (changelogNotes)
|
|
94
|
-
return changelogNotes;
|
|
95
91
|
return null;
|
|
96
92
|
}
|
|
97
93
|
getGitHubSources(repoUrl, version, signal) {
|
|
@@ -154,29 +150,6 @@ class ReleaseNotesService {
|
|
|
154
150
|
return null;
|
|
155
151
|
return (0, changelog_parser_1.extractVersionSection)(fullText, version);
|
|
156
152
|
}
|
|
157
|
-
async fetchJsdelivrChangelog(packageName, version, signal) {
|
|
158
|
-
const fullText = await this.fetchPublishedPackageChangelog(packageName, version, signal);
|
|
159
|
-
if (!fullText)
|
|
160
|
-
return null;
|
|
161
|
-
return (0, changelog_parser_1.extractVersionSection)(fullText, version);
|
|
162
|
-
}
|
|
163
|
-
async fetchPublishedPackageChangelog(packageName, version, signal) {
|
|
164
|
-
try {
|
|
165
|
-
const response = await fetch(`${constants_1.JSDELIVR_CDN_URL}/${encodeURIComponent(packageName)}@${version}/CHANGELOG.md`, {
|
|
166
|
-
method: 'GET',
|
|
167
|
-
signal,
|
|
168
|
-
});
|
|
169
|
-
if (!response.ok)
|
|
170
|
-
return null;
|
|
171
|
-
return await response.text();
|
|
172
|
-
}
|
|
173
|
-
catch (error) {
|
|
174
|
-
if (error instanceof DOMException && error.name === 'AbortError') {
|
|
175
|
-
throw error;
|
|
176
|
-
}
|
|
177
|
-
return null;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
153
|
}
|
|
181
154
|
exports.ReleaseNotesService = ReleaseNotesService;
|
|
182
155
|
//# sourceMappingURL=release-notes-service.js.map
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.HeadlessRunner = void 0;
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
const package_detector_1 = require("../../core/package-detector");
|
|
9
|
+
const vulnerability_audit_1 = require("./vulnerability-audit");
|
|
10
|
+
const report_1 = require("./report");
|
|
11
|
+
/**
|
|
12
|
+
* Non-interactive entry point. Resolves the outdated list without rendering the TUI, then either
|
|
13
|
+
* emits a JSON report (--json) or a plain line-based report. With --check, sets a non-zero exit
|
|
14
|
+
* code when updates exist so CI can gate on it. Read-only: never writes package.json or installs.
|
|
15
|
+
*
|
|
16
|
+
* This is the headless counterpart to the interactive `UpgradeRunner`; it shares only the
|
|
17
|
+
* `PackageDetector` (the scan/resolve layer), not the UI/upgrade machinery.
|
|
18
|
+
*/
|
|
19
|
+
class HeadlessRunner {
|
|
20
|
+
detector;
|
|
21
|
+
constructor(options) {
|
|
22
|
+
this.detector = new package_detector_1.PackageDetector(options);
|
|
23
|
+
}
|
|
24
|
+
async run(options) {
|
|
25
|
+
try {
|
|
26
|
+
if (!this.detector.hasPackageJson()) {
|
|
27
|
+
throw new Error('No package.json found in current directory');
|
|
28
|
+
}
|
|
29
|
+
const packages = await this.detector.getOutdatedPackages();
|
|
30
|
+
const outdated = this.detector.getOutdatedPackagesOnly(packages);
|
|
31
|
+
// Audit the current versions (one bulk request, best-effort) and cross-reference each
|
|
32
|
+
// advisory against the upgrade targets, so the report says whether upgrading *fixes* it.
|
|
33
|
+
const vulnerabilities = await (0, vulnerability_audit_1.auditVulnerabilities)(outdated);
|
|
34
|
+
if (options.json) {
|
|
35
|
+
// stdout is reserved for the JSON document only.
|
|
36
|
+
console.log(JSON.stringify((0, report_1.buildHeadlessReport)(packages, outdated, vulnerabilities), null, 2));
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
console.log((0, report_1.renderPlainReport)(outdated, vulnerabilities));
|
|
40
|
+
}
|
|
41
|
+
// Exit 1 only means "updates exist" (like `prettier --check`); 2 is reserved for errors.
|
|
42
|
+
if (options.check && outdated.length > 0) {
|
|
43
|
+
process.exitCode = 1;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
console.error(chalk_1.default.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
48
|
+
process.exit(2);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
exports.HeadlessRunner = HeadlessRunner;
|
|
53
|
+
//# sourceMappingURL=headless-runner.js.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.renderPlainReport = exports.buildHeadlessReport = exports.upgradeClears = exports.auditVulnerabilities = exports.HeadlessRunner = void 0;
|
|
18
|
+
__exportStar(require("./types"), exports);
|
|
19
|
+
var headless_runner_1 = require("./headless-runner");
|
|
20
|
+
Object.defineProperty(exports, "HeadlessRunner", { enumerable: true, get: function () { return headless_runner_1.HeadlessRunner; } });
|
|
21
|
+
var vulnerability_audit_1 = require("./vulnerability-audit");
|
|
22
|
+
Object.defineProperty(exports, "auditVulnerabilities", { enumerable: true, get: function () { return vulnerability_audit_1.auditVulnerabilities; } });
|
|
23
|
+
Object.defineProperty(exports, "upgradeClears", { enumerable: true, get: function () { return vulnerability_audit_1.upgradeClears; } });
|
|
24
|
+
var report_1 = require("./report");
|
|
25
|
+
Object.defineProperty(exports, "buildHeadlessReport", { enumerable: true, get: function () { return report_1.buildHeadlessReport; } });
|
|
26
|
+
Object.defineProperty(exports, "renderPlainReport", { enumerable: true, get: function () { return report_1.renderPlainReport; } });
|
|
27
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildHeadlessReport = buildHeadlessReport;
|
|
4
|
+
exports.renderPlainReport = renderPlainReport;
|
|
5
|
+
const types_1 = require("./types");
|
|
6
|
+
/** Build the machine-readable `--json` payload from the scanned + outdated package sets. */
|
|
7
|
+
function buildHeadlessReport(all, outdated, vulnerabilities) {
|
|
8
|
+
return {
|
|
9
|
+
schemaVersion: types_1.HEADLESS_SCHEMA_VERSION,
|
|
10
|
+
summary: {
|
|
11
|
+
total: all.length,
|
|
12
|
+
outdated: outdated.length,
|
|
13
|
+
major: outdated.filter((pkg) => pkg.hasMajorUpdate).length,
|
|
14
|
+
vulnerable: vulnerabilities.size,
|
|
15
|
+
},
|
|
16
|
+
outdated: outdated.map((pkg) => {
|
|
17
|
+
const entry = {
|
|
18
|
+
name: pkg.name,
|
|
19
|
+
current: pkg.currentVersion,
|
|
20
|
+
range: pkg.rangeVersion,
|
|
21
|
+
latest: pkg.latestVersion,
|
|
22
|
+
type: pkg.type,
|
|
23
|
+
packageJsonPath: pkg.packageJsonPath,
|
|
24
|
+
hasMajorUpdate: pkg.hasMajorUpdate,
|
|
25
|
+
};
|
|
26
|
+
if (pkg.deprecated)
|
|
27
|
+
entry.deprecated = pkg.deprecated;
|
|
28
|
+
if (pkg.enginesNode)
|
|
29
|
+
entry.enginesNode = pkg.enginesNode;
|
|
30
|
+
const vulnerability = vulnerabilities.get(pkg);
|
|
31
|
+
if (vulnerability)
|
|
32
|
+
entry.vulnerability = vulnerability;
|
|
33
|
+
return entry;
|
|
34
|
+
}),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/** Render the plain, line-based report (one line per package + a recap) as a single string. */
|
|
38
|
+
function renderPlainReport(outdated, vulnerabilities) {
|
|
39
|
+
if (outdated.length === 0) {
|
|
40
|
+
return 'All dependencies are up to date — no upgrades needed.';
|
|
41
|
+
}
|
|
42
|
+
const lines = outdated.map((pkg) => {
|
|
43
|
+
const major = pkg.hasMajorUpdate ? ' (major)' : '';
|
|
44
|
+
const deprecated = pkg.deprecated ? ' [deprecated]' : '';
|
|
45
|
+
return `${pkg.name} ${pkg.currentVersion} → ${pkg.latestVersion} [${pkg.type}]${major}${vulnMarker(vulnerabilities.get(pkg))}${deprecated}`;
|
|
46
|
+
});
|
|
47
|
+
const fileCount = new Set(outdated.map((pkg) => pkg.packageJsonPath)).size;
|
|
48
|
+
const vulnNote = vulnerabilities.size > 0 ? ` — ${vulnerabilities.size} with known vulnerabilities` : '';
|
|
49
|
+
lines.push('', `${outdated.length} package(s) outdated across ${fileCount} file(s)${vulnNote}.`);
|
|
50
|
+
return lines.join('\n');
|
|
51
|
+
}
|
|
52
|
+
/** A compact `[vuln: N sev → verdict]` tag for the plain report; '' when there are none. */
|
|
53
|
+
function vulnMarker(vulnerability) {
|
|
54
|
+
if (!vulnerability)
|
|
55
|
+
return '';
|
|
56
|
+
// Prefer the cheaper fix: if the in-range bump already clears it, that's the safer action.
|
|
57
|
+
const verdict = vulnerability.fixedByRange
|
|
58
|
+
? 'fixed by range upgrade'
|
|
59
|
+
: vulnerability.fixedByLatest
|
|
60
|
+
? 'fixed by latest only'
|
|
61
|
+
: 'not fixed by upgrade';
|
|
62
|
+
return ` [vuln: ${vulnerability.count} ${vulnerability.highestSeverity} → ${verdict}]`;
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=report.js.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HEADLESS_SCHEMA_VERSION = void 0;
|
|
4
|
+
/** Bump when the `--json` shape changes in a way consumers (scripts, agents) must adapt to. */
|
|
5
|
+
exports.HEADLESS_SCHEMA_VERSION = 1;
|
|
6
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.auditVulnerabilities = auditVulnerabilities;
|
|
37
|
+
exports.upgradeClears = upgradeClears;
|
|
38
|
+
const semver = __importStar(require("semver"));
|
|
39
|
+
const services_1 = require("../../services");
|
|
40
|
+
const utils_1 = require("../../utils");
|
|
41
|
+
/**
|
|
42
|
+
* Audit the outdated packages' currently-installed versions (one bulk request, matching the
|
|
43
|
+
* interactive audit) and, for each advisory, cross-reference its affected range against the
|
|
44
|
+
* upgrade targets — so callers can state whether upgrading actually *fixes* the issue.
|
|
45
|
+
*
|
|
46
|
+
* Best-effort: `fetchVulnerabilities` swallows network errors and returns an empty map, so a
|
|
47
|
+
* failed audit never blocks the report. Returns only the vulnerable packages, keyed by package.
|
|
48
|
+
*/
|
|
49
|
+
async function auditVulnerabilities(outdated) {
|
|
50
|
+
const result = new Map();
|
|
51
|
+
if (outdated.length === 0)
|
|
52
|
+
return result;
|
|
53
|
+
// The bulk advisory API is keyed by package name (one version per name), so dedupe by name.
|
|
54
|
+
const versions = new Map();
|
|
55
|
+
for (const pkg of outdated) {
|
|
56
|
+
if (!versions.has(pkg.name))
|
|
57
|
+
versions.set(pkg.name, pkg.currentVersion);
|
|
58
|
+
}
|
|
59
|
+
const advisories = await (0, services_1.fetchVulnerabilities)(versions);
|
|
60
|
+
if (advisories.size === 0)
|
|
61
|
+
return result;
|
|
62
|
+
for (const pkg of outdated) {
|
|
63
|
+
const found = advisories.get(pkg.name);
|
|
64
|
+
if (!found || found.vulnerabilities.length === 0 || !found.highestSeverity)
|
|
65
|
+
continue;
|
|
66
|
+
result.set(pkg, summarizeVulnerability(pkg, found.vulnerabilities, found.highestSeverity));
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
function summarizeVulnerability(pkg, vulnerabilities, highestSeverity) {
|
|
71
|
+
const advisories = vulnerabilities.map((vuln) => ({
|
|
72
|
+
id: vuln.id,
|
|
73
|
+
title: vuln.title,
|
|
74
|
+
severity: vuln.severity,
|
|
75
|
+
url: vuln.url,
|
|
76
|
+
vulnerableVersions: vuln.vulnerable_versions,
|
|
77
|
+
fixedByRange: upgradeClears(pkg.rangeVersion, vuln.vulnerable_versions),
|
|
78
|
+
fixedByLatest: upgradeClears(pkg.latestVersion, vuln.vulnerable_versions),
|
|
79
|
+
}));
|
|
80
|
+
return {
|
|
81
|
+
count: advisories.length,
|
|
82
|
+
highestSeverity,
|
|
83
|
+
fixedByRange: advisories.every((advisory) => advisory.fixedByRange),
|
|
84
|
+
fixedByLatest: advisories.every((advisory) => advisory.fixedByLatest),
|
|
85
|
+
advisories,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* True when upgrading to `target` escapes an advisory's affected range. Conservative: if either
|
|
90
|
+
* the target or the advisory range can't be parsed, we do NOT claim a fix — `semver.satisfies`
|
|
91
|
+
* treats an invalid range as "matches nothing", which would otherwise read as a false "fixed".
|
|
92
|
+
*/
|
|
93
|
+
function upgradeClears(target, vulnerableVersions) {
|
|
94
|
+
const comparable = (0, utils_1.toComparableVersion)(target);
|
|
95
|
+
if (!comparable)
|
|
96
|
+
return false;
|
|
97
|
+
if (semver.validRange(vulnerableVersions) === null)
|
|
98
|
+
return false;
|
|
99
|
+
try {
|
|
100
|
+
return !semver.satisfies(comparable, vulnerableVersions, { includePrerelease: true });
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=vulnerability-audit.js.map
|