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.
Files changed (35) hide show
  1. package/README.md +29 -0
  2. package/dist/cli.js +35 -11
  3. package/dist/config/constants.js +1 -5
  4. package/dist/config/project-config.js +5 -0
  5. package/dist/core/package-detector.js +29 -1
  6. package/dist/core/upgrade-runner.js +1 -0
  7. package/dist/core/upgrader.js +9 -3
  8. package/dist/features/changelog/services/package-metadata-service.js +2 -13
  9. package/dist/features/changelog/services/release-notes-service.js +0 -27
  10. package/dist/features/headless/headless-runner.js +53 -0
  11. package/dist/features/headless/index.js +27 -0
  12. package/dist/features/headless/report.js +64 -0
  13. package/dist/features/headless/types.js +6 -0
  14. package/dist/features/headless/vulnerability-audit.js +106 -0
  15. package/dist/interactive-ui.js +4 -2
  16. package/dist/services/index.js +0 -1
  17. package/dist/services/npm-registry.js +9 -17
  18. package/dist/services/package-manager-detector.js +6 -3
  19. package/dist/ui/modal/package-info-sections/sections.js +24 -0
  20. package/dist/ui/presenters/health.js +24 -0
  21. package/dist/ui/renderer/package-list/rows.js +8 -3
  22. package/dist/ui/session/selection-state-builder.js +7 -2
  23. package/dist/ui/themes-colors.js +12 -3
  24. package/dist/ui/utils/cursor.js +9 -3
  25. package/dist/utils/color.js +38 -0
  26. package/dist/utils/engines.js +63 -0
  27. package/dist/utils/filesystem/io.js +16 -0
  28. package/dist/utils/filesystem/scan.js +82 -7
  29. package/dist/utils/index.js +3 -0
  30. package/dist/utils/manifest.js +35 -0
  31. package/dist/utils/version.js +9 -2
  32. package/package.json +3 -3
  33. package/dist/services/jsdelivr/client.js +0 -191
  34. package/dist/services/jsdelivr/manifest.js +0 -136
  35. 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
- const gitState = (0, git_1.getGitWorkingTreeState)(cwd);
24
- if (gitState.isRepo && gitState.isDirty) {
25
- const shouldProceed = await ui_1.TerminalInput.promptForImmediateConfirmation(`${chalk_1.default.yellow('Warning:')} dirty working tree. Proceed anyway? ${chalk_1.default.dim('[y/N]')} `, false);
26
- if (!shouldProceed) {
27
- console.log(chalk_1.default.yellow('Upgrade cancelled.'));
28
- return;
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
- const updateCheckPromise = (0, services_1.checkForUpdateAsync)(config_1.PACKAGE_NAME, config_1.PACKAGE_VERSION);
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 upgrader = new index_1.UpgradeRunner({
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
- await upgrader.run();
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) => {
@@ -1,15 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.JSDELIVR_POOL_TIMEOUT = exports.JSDELIVR_RETRY_DELAYS = exports.JSDELIVR_RETRY_TIMEOUTS = exports.REQUEST_TIMEOUT = exports.CACHE_TTL = exports.MAX_CONCURRENT_REQUESTS = exports.JSDELIVR_CDN_URL = exports.NPM_REGISTRY_URL = exports.PACKAGE_VERSION = exports.PACKAGE_NAME = void 0;
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
- return await Promise.race([
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
  }
@@ -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, fs_1.writeFileSync)(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n');
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(), exactManifestFetcher = jsdelivr_registry_1.fetchExactPackageManifest) {
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
- const normalizedVersion = version?.trim();
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