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
@@ -20,6 +20,7 @@ class InteractiveUI {
20
20
  renderer;
21
21
  packageManager;
22
22
  options;
23
+ saveExact;
23
24
  vulnerabilityAuditController = new controllers_1.VulnerabilityAuditController();
24
25
  packageInfoModalController = new controllers_1.PackageInfoModalController();
25
26
  refreshView;
@@ -27,6 +28,7 @@ class InteractiveUI {
27
28
  this.renderer = new ui_1.UIRenderer();
28
29
  this.packageManager = packageManager;
29
30
  this.options = normalizeVulnerabilityDisplayOptions(options);
31
+ this.saveExact = options?.saveExact ?? false;
30
32
  }
31
33
  async displayPackagesTable(packages) {
32
34
  console.log(this.renderer.renderPackagesTable(packages));
@@ -37,7 +39,7 @@ class InteractiveUI {
37
39
  return [];
38
40
  }
39
41
  const selectedStates = await (0, session_1.runInteractiveSession)(selectionStates, this.packageManager, this.renderer, this.packageInfoModalController, this.vulnerabilityAuditController, this.options, (refresh) => { this.refreshView = refresh; });
40
- return (0, session_1.createUpgradeChoices)(selectedStates);
42
+ return (0, session_1.createUpgradeChoices)(selectedStates, this.saveExact);
41
43
  }
42
44
  createSelectionStates(packages, previousSelections, includeUpToDate = true) {
43
45
  return (0, session_1.createSelectionStates)(packages, (name, version, type) => this.vulnerabilityAuditController.getCachedSummary(name, version, type), previousSelections, includeUpToDate);
@@ -63,7 +65,7 @@ class InteractiveUI {
63
65
  async selectPackagesToUpgradeProgressive(selectionStates, progress, attachRefresh) {
64
66
  this.enqueueSecurityAudit(selectionStates);
65
67
  const selectedStates = await (0, session_1.runInteractiveSession)(selectionStates, this.packageManager, this.renderer, this.packageInfoModalController, this.vulnerabilityAuditController, this.options, (refresh) => { this.refreshView = refresh; }, progress, attachRefresh);
66
- return (0, session_1.createUpgradeChoices)(selectedStates);
68
+ return (0, session_1.createUpgradeChoices)(selectedStates, this.saveExact);
67
69
  }
68
70
  enqueueSecurityAudit(selectionStates) {
69
71
  this.vulnerabilityAuditController.enqueueStates(selectionStates, () => this.refreshView?.());
@@ -18,7 +18,6 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
18
18
  };
19
19
  Object.defineProperty(exports, "__esModule", { value: true });
20
20
  __exportStar(require("./npm-registry"), exports);
21
- __exportStar(require("./jsdelivr-registry"), exports);
22
21
  __exportStar(require("../features/changelog"), exports);
23
22
  __exportStar(require("./version-checker"), exports);
24
23
  __exportStar(require("./vulnerability-checker"), exports);
@@ -10,7 +10,6 @@ const gunzipAsync = (0, node_util_1.promisify)(node_zlib_1.gunzip);
10
10
  const inflateAsync = (0, node_util_1.promisify)(node_zlib_1.inflate);
11
11
  const brotliDecompressAsync = (0, node_util_1.promisify)(node_zlib_1.brotliDecompress);
12
12
  const config_1 = require("../config");
13
- const jsdelivr_registry_1 = require("./jsdelivr-registry");
14
13
  const version_1 = require("../utils/version");
15
14
  const retry_1 = require("./http/retry");
16
15
  const inflight_1 = require("./http/inflight");
@@ -32,13 +31,9 @@ const registryPool = new undici_1.Pool(registryOrigin, {
32
31
  });
33
32
  const MAX_REGISTRY_ATTEMPTS = 3;
34
33
  const RETRY_BACKOFF_MS = [500, 1500, 3000];
35
- const fetchFromJsdelivrFallback = async (packageName, currentVersion) => {
36
- const jsdelivrData = await (0, jsdelivr_registry_1.getAllPackageDataFromJsdelivr)([packageName], currentVersion ? new Map([[packageName, currentVersion]]) : undefined);
37
- return jsdelivrData.get(packageName) ?? { latestVersion: 'unknown', allVersions: [] };
38
- };
39
34
  async function getFreshPackageData(packageName, currentVersion) {
40
35
  const cacheKey = `${packageName}@${currentVersion ?? ''}`;
41
- return inFlightLookups.dedupe(cacheKey, () => fetchPackageFromRegistryWithFallback(packageName, currentVersion));
36
+ return inFlightLookups.dedupe(cacheKey, () => fetchPackageFromRegistry(packageName));
42
37
  }
43
38
  const encodeRegistryPath = (packageName) => {
44
39
  const encodedName = packageName.startsWith('@')
@@ -110,19 +105,15 @@ async function fetchFromRegistryWithRetries(path) {
110
105
  }
111
106
  return lastOutcome;
112
107
  }
113
- async function fetchPackageFromRegistryWithFallback(packageName, currentVersion) {
108
+ async function fetchPackageFromRegistry(packageName) {
114
109
  const path = encodeRegistryPath(packageName);
115
110
  const outcome = await fetchFromRegistryWithRetries(path);
116
111
  if (outcome.kind === 'success') {
117
112
  return outcome.data;
118
113
  }
119
- if (outcome.kind === 'not-found') {
120
- return { latestVersion: 'unknown', allVersions: [] };
121
- }
122
- // Only reach here after exhausted retries against real errors — try jsdelivr
123
- // as last-resort safety net so we don't silently return 'unknown'.
124
- const fallback = await fetchFromJsdelivrFallback(packageName, currentVersion).catch(() => null);
125
- return fallback ?? { latestVersion: 'unknown', allVersions: [] };
114
+ // Not found, or exhausted retries against real errors: report unavailable.
115
+ // The registry is the single source of truth — there is no secondary fetch.
116
+ return { latestVersion: 'unknown', allVersions: [] };
126
117
  }
127
118
  /**
128
119
  * Fetches version data for a list of packages from the npm registry.
@@ -132,9 +123,10 @@ async function fetchPackageFromRegistryWithFallback(packageName, currentVersion)
132
123
  * It doesn't interact with batch size — batches exist only to group
133
124
  * emissions for the UI.
134
125
  * - No per-request timeouts: slow responses are allowed to finish. Real
135
- * network errors are retried with exponential backoff; after that, we
136
- * fall back to jsdelivr as a last resort. A result is never silently
137
- * dropped due to slowness.
126
+ * network errors are retried with exponential backoff; after the retry
127
+ * budget is exhausted the package is reported as unavailable
128
+ * (`latestVersion: 'unknown'`). A result is never silently dropped due to
129
+ * slowness.
138
130
  *
139
131
  * Callbacks:
140
132
  * - `onBatchReady` fires once a whole emission batch has resolved, in
@@ -56,8 +56,8 @@ class PackageManagerDetector {
56
56
  if (fromLockFile) {
57
57
  return fromLockFile;
58
58
  }
59
- // 3. Fallback to npm
60
- console.log(chalk_1.default.yellow('⚠️ No package manager detected. Defaulting to npm. Consider adding a "packageManager" field to your package.json.'));
59
+ // 3. Fallback to npm. Warn on stderr so it never corrupts --json output on stdout.
60
+ console.error(chalk_1.default.yellow('⚠️ No package manager detected. Defaulting to npm. Consider adding a "packageManager" field to your package.json.'));
61
61
  return PACKAGE_MANAGERS.npm;
62
62
  }
63
63
  /**
@@ -92,6 +92,8 @@ class PackageManagerDetector {
92
92
  const lockFileChecks = [
93
93
  { pm: PACKAGE_MANAGERS.pnpm, path: (0, path_1.join)(cwd, 'pnpm-lock.yaml') },
94
94
  { pm: PACKAGE_MANAGERS.bun, path: (0, path_1.join)(cwd, 'bun.lockb') },
95
+ // Bun >= 1.2 writes a text `bun.lock` instead of the binary `bun.lockb`.
96
+ { pm: PACKAGE_MANAGERS.bun, path: (0, path_1.join)(cwd, 'bun.lock') },
95
97
  { pm: PACKAGE_MANAGERS.yarn, path: (0, path_1.join)(cwd, 'yarn.lock') },
96
98
  { pm: PACKAGE_MANAGERS.npm, path: (0, path_1.join)(cwd, 'package-lock.json') },
97
99
  ];
@@ -107,7 +109,8 @@ class PackageManagerDetector {
107
109
  }
108
110
  // If multiple lock files, use most recently modified
109
111
  if (existingLocks.length > 1) {
110
- console.log(chalk_1.default.yellow('⚠️ Multiple lock files detected. Using most recently modified. Consider cleaning up unused lock files.'));
112
+ // stderr so it never corrupts --json output on stdout.
113
+ console.error(chalk_1.default.yellow('⚠️ Multiple lock files detected. Using most recently modified. Consider cleaning up unused lock files.'));
111
114
  existingLocks.sort((a, b) => b.mtime - a.mtime);
112
115
  }
113
116
  return existingLocks[0].pm;
@@ -10,6 +10,7 @@ const chalk_1 = __importDefault(require("chalk"));
10
10
  const themes_colors_1 = require("../../themes-colors");
11
11
  const vulnerability_1 = require("../../presenters/vulnerability");
12
12
  const utils_1 = require("../../utils");
13
+ const utils_2 = require("../../../utils");
13
14
  const release_notes_1 = require("./release-notes");
14
15
  function formatNumber(num) {
15
16
  if (!num)
@@ -88,6 +89,29 @@ function buildPackageInfoSections(state, modalWidth, activeTab) {
88
89
  required: true,
89
90
  behavior: 'pinned',
90
91
  });
92
+ const warningRows = [];
93
+ const warningContentWidth = Math.max(10, modalWidth - 4);
94
+ if (state.deprecated) {
95
+ // Wrap (don't truncate) so a deprecation URL stays whole and clickable —
96
+ // truncation with "..." produces a dead link. `wrapPlainText` breaks on
97
+ // spaces only, so the URL keeps its own intact line. No emoji marker:
98
+ // `getVisualLength` scores glyphs like ⚠ as width 2 while many terminals
99
+ // render them as width 1, which throws off the modal's border alignment.
100
+ for (const line of (0, utils_1.wrapPlainText)(`Deprecated: ${state.deprecated}`, warningContentWidth)) {
101
+ warningRows.push((0, themes_colors_1.getThemeColor)('warning')(line));
102
+ }
103
+ }
104
+ const engineWarning = (0, utils_2.checkNodeEngineCompatibility)(state.enginesNode);
105
+ if (engineWarning) {
106
+ warningRows.push((0, themes_colors_1.getThemeColor)('warning')(`Hold: ${engineWarning}`));
107
+ }
108
+ if (warningRows.length > 0) {
109
+ sections.push({
110
+ key: 'warnings',
111
+ rows: warningRows,
112
+ behavior: 'pinned',
113
+ });
114
+ }
91
115
  if (state.homepage) {
92
116
  sections.push({
93
117
  key: 'homepage',
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getHealthBadge = getHealthBadge;
4
+ const utils_1 = require("../../utils");
5
+ const themes_colors_1 = require("../themes-colors");
6
+ /**
7
+ * A compact badge flagging a package's health in the list:
8
+ * - `[DEPR]` when the latest version is deprecated (highest priority), or
9
+ * - `[ENG]` when its `engines.node` is incompatible with the running Node.
10
+ *
11
+ * Returns an empty string when neither applies. Deprecation wins because an
12
+ * engines mismatch on an abandoned package is moot. Both render in the theme's
13
+ * (amber) warning color — a caution signal, not an alarm.
14
+ */
15
+ function getHealthBadge(state) {
16
+ if (state.deprecated) {
17
+ return (0, themes_colors_1.getThemeColor)('warning')('[DEPR]');
18
+ }
19
+ if ((0, utils_1.checkNodeEngineCompatibility)(state.enginesNode)) {
20
+ return (0, themes_colors_1.getThemeColor)('warning')('[ENG]');
21
+ }
22
+ return '';
23
+ }
24
+ //# sourceMappingURL=health.js.map
@@ -11,6 +11,7 @@ const chalk_1 = __importDefault(require("chalk"));
11
11
  const utils_1 = require("../../utils");
12
12
  const themes_colors_1 = require("../../themes-colors");
13
13
  const vulnerability_1 = require("../../presenters/vulnerability");
14
+ const health_1 = require("../../presenters/health");
14
15
  function padLineToWidth(line, terminalWidth) {
15
16
  const padding = Math.max(0, terminalWidth - utils_1.VersionUtils.getVisualLength(line));
16
17
  return line + ' '.repeat(padding);
@@ -118,15 +119,19 @@ function renderPackageLine(state, _index, isCurrentRow, terminalWidth = 80, opti
118
119
  const shouldShowVulnerability = (0, vulnerability_1.shouldDisplayVulnerabilityForDependency)(state.type, options);
119
120
  const vulnBadge = shouldShowVulnerability ? (0, vulnerability_1.getVulnerabilityBadge)(state.vulnerability) : '';
120
121
  const vulnBadgeWidth = vulnBadge ? utils_1.VersionUtils.getVisualLength(vulnBadge) + 1 : 0;
122
+ // Deprecation / engines-incompatibility marker (independent of dep type).
123
+ const healthBadge = (0, health_1.getHealthBadge)(state);
124
+ const healthBadgeWidth = healthBadge ? utils_1.VersionUtils.getVisualLength(healthBadge) + 1 : 0;
121
125
  const nameLength = utils_1.VersionUtils.getVisualLength(truncatedName);
122
- const namePadding = Math.max(0, packageNameWidth - nameLength - 1 - badgeWidth - vulnBadgeWidth);
126
+ const namePadding = Math.max(0, packageNameWidth - nameLength - 1 - badgeWidth - vulnBadgeWidth - healthBadgeWidth);
123
127
  const nameDashes = shouldShowDashes(namePadding)
124
128
  ? dashColor('-').repeat(namePadding)
125
129
  : ' '.repeat(namePadding);
126
130
  const vulnSuffix = vulnBadge ? ` ${vulnBadge}` : '';
131
+ const healthSuffix = healthBadge ? ` ${healthBadge}` : '';
127
132
  const packageNameSection = typeBadge
128
- ? `${displayName} ${nameDashes}${vulnSuffix}${typeBadge}`
129
- : `${displayName} ${nameDashes}${vulnSuffix}`;
133
+ ? `${displayName} ${nameDashes}${vulnSuffix}${healthSuffix}${typeBadge}`
134
+ : `${displayName} ${nameDashes}${vulnSuffix}${healthSuffix}`;
130
135
  const currentSection = `${currentDot} ${currentVersion}`;
131
136
  const currentSectionLength = utils_1.VersionUtils.getVisualLength(currentSection) + 1;
132
137
  const currentPadding = Math.max(0, currentColumnWidth - currentSectionLength);
@@ -85,6 +85,8 @@ function createSelectionStates(packages, getCachedSummary, previousSelections, i
85
85
  hasRangeUpdate: pkg.hasRangeUpdate,
86
86
  hasMajorUpdate: pkg.hasMajorUpdate,
87
87
  type: pkg.type,
88
+ deprecated: pkg.deprecated,
89
+ enginesNode: pkg.enginesNode,
88
90
  vulnerability: getCachedSummary(pkg.name, pkg.currentVersion, pkg.type),
89
91
  allVersions: pkg.allVersions,
90
92
  };
@@ -120,13 +122,16 @@ function createPendingSelectionStates(packages, getCachedSummary, previousSelect
120
122
  };
121
123
  });
122
124
  }
123
- function createUpgradeChoices(selectedStates) {
125
+ function createUpgradeChoices(selectedStates, saveExact = false) {
124
126
  const choices = [];
125
127
  selectedStates
126
128
  .filter((state) => state.loadState === 'ready' && state.selectedOption !== 'none')
127
129
  .forEach((state) => {
128
130
  const targetVersion = state.selectedOption === 'range' ? state.rangeVersion : state.latestVersion;
129
- const targetVersionWithPrefix = (0, utils_1.applyVersionPrefix)(state.currentVersionSpecifier, targetVersion);
131
+ // Preserve the original range prefix (^/~) by default; --save-exact writes the bare version.
132
+ const targetVersionWithPrefix = saveExact
133
+ ? targetVersion
134
+ : (0, utils_1.applyVersionPrefix)(state.currentVersionSpecifier, targetVersion);
130
135
  const pathsToUpdate = state.packageJsonPaths || [state.packageJsonPath];
131
136
  pathsToUpdate.forEach((packageJsonPath) => {
132
137
  choices.push({
@@ -182,18 +182,27 @@ function hexToRgb(hex) {
182
182
  : { r: 0, g: 0, b: 0 };
183
183
  }
184
184
  /**
185
- * Get ANSI escape code to set terminal background color
185
+ * Get ANSI escape code to set terminal background color.
186
+ *
187
+ * Returns an empty string when color output is disabled (`--no-color`/`NO_COLOR`
188
+ * set `chalk.level` to 0), so the TUI inherits the user's own terminal
189
+ * background instead of painting over it. This escape is raw (not produced by
190
+ * chalk), so it must be gated explicitly.
186
191
  */
187
192
  function getTerminalBgColorCode() {
193
+ if (chalk_1.default.level === 0) {
194
+ return '';
195
+ }
188
196
  const hex = getThemeBgColor();
189
197
  const rgb = hexToRgb(hex);
190
198
  return `\x1b[48;2;${rgb.r};${rgb.g};${rgb.b}m`;
191
199
  }
192
200
  /**
193
- * Get ANSI escape code to reset terminal colors
201
+ * Get ANSI escape code to reset terminal colors. Empty when color is disabled,
202
+ * so a `--no-color` run emits no escape sequences at all.
194
203
  */
195
204
  function getTerminalResetCode() {
196
- return '\x1b[0m';
205
+ return chalk_1.default.level === 0 ? '' : '\x1b[0m';
197
206
  }
198
207
  const BRAND_COLORS = [chalk_1.default.red, chalk_1.default.yellow, chalk_1.default.blue, chalk_1.default.magenta];
199
208
  function coloredInupLogo() {
@@ -67,16 +67,22 @@ exports.ConsoleUtils = {
67
67
  */
68
68
  LINE_WIDTH: 80,
69
69
  /**
70
- * Show a progress message on the current line (overwrites previous content)
70
+ * Show a progress message on the current line (overwrites previous content).
71
+ * Written to stderr so stdout stays clean for --json / piped output, and only
72
+ * when stderr is a TTY — the \r animation is just noise in a redirected log.
71
73
  */
72
74
  showProgress(message) {
73
- process.stdout.write(`\r${' '.repeat(exports.ConsoleUtils.LINE_WIDTH)}\r${message}`);
75
+ if (!process.stderr.isTTY)
76
+ return;
77
+ process.stderr.write(`\r${' '.repeat(exports.ConsoleUtils.LINE_WIDTH)}\r${message}`);
74
78
  },
75
79
  /**
76
80
  * Clear the current progress line
77
81
  */
78
82
  clearProgress() {
79
- process.stdout.write('\r' + ' '.repeat(exports.ConsoleUtils.LINE_WIDTH) + '\r');
83
+ if (!process.stderr.isTTY)
84
+ return;
85
+ process.stderr.write('\r' + ' '.repeat(exports.ConsoleUtils.LINE_WIDTH) + '\r');
80
86
  },
81
87
  };
82
88
  //# sourceMappingURL=cursor.js.map
@@ -0,0 +1,38 @@
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.shouldDisableColor = shouldDisableColor;
7
+ exports.applyColorSetting = applyColorSetting;
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ /**
10
+ * Decide whether colored output should be disabled.
11
+ *
12
+ * Precedence (highest first):
13
+ * 1. An explicit `--no-color` flag (`colorFlag === false`) always wins.
14
+ * 2. `FORCE_COLOR` keeps colors on.
15
+ * 3. `NO_COLOR` (any non-empty value) disables them — the de-facto standard.
16
+ *
17
+ * chalk v5 already honors the env vars on its own, but the explicit flag does
18
+ * not flow through automatically, so we resolve the final intent here.
19
+ */
20
+ function shouldDisableColor(colorFlag, env = process.env) {
21
+ if (colorFlag === false) {
22
+ return true;
23
+ }
24
+ if (env.FORCE_COLOR) {
25
+ return false;
26
+ }
27
+ return Boolean(env.NO_COLOR);
28
+ }
29
+ /**
30
+ * Apply the resolved color intent to chalk's global level. Call once at startup
31
+ * before anything renders.
32
+ */
33
+ function applyColorSetting(colorFlag, env = process.env) {
34
+ if (shouldDisableColor(colorFlag, env)) {
35
+ chalk_1.default.level = 0;
36
+ }
37
+ }
38
+ //# sourceMappingURL=color.js.map
@@ -0,0 +1,63 @@
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.checkNodeEngineCompatibility = checkNodeEngineCompatibility;
37
+ const semver = __importStar(require("semver"));
38
+ /**
39
+ * Check whether the running Node version satisfies a package's declared
40
+ * `engines.node` range. Returns a short human-readable warning when it does
41
+ * not, or `null` when compatible (or when the inputs are unusable).
42
+ *
43
+ * Best-effort: an unparseable range is treated as "no opinion" (null) rather
44
+ * than a false warning.
45
+ */
46
+ function checkNodeEngineCompatibility(requiredRange, currentNodeVersion = process.versions.node) {
47
+ if (!requiredRange) {
48
+ return null;
49
+ }
50
+ const range = semver.validRange(requiredRange, { loose: true });
51
+ if (!range) {
52
+ return null;
53
+ }
54
+ const current = semver.coerce(currentNodeVersion);
55
+ if (!current) {
56
+ return null;
57
+ }
58
+ if (semver.satisfies(current, range, { includePrerelease: true })) {
59
+ return null;
60
+ }
61
+ return `requires Node ${requiredRange}, you're on ${current.version}`;
62
+ }
63
+ //# sourceMappingURL=engines.js.map
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.readPackageJson = readPackageJson;
4
+ exports.detectJsonFormat = detectJsonFormat;
4
5
  exports.readPackageJsonAsync = readPackageJsonAsync;
5
6
  exports.collectAllDependencies = collectAllDependencies;
6
7
  exports.collectAllDependenciesAsync = collectAllDependenciesAsync;
@@ -15,6 +16,21 @@ function readPackageJson(path) {
15
16
  throw new Error(`Failed to read package.json: ${error}`);
16
17
  }
17
18
  }
19
+ /**
20
+ * Detect the indentation and trailing-newline style of a raw JSON document so a
21
+ * re-serialized version can preserve the original formatting instead of normalizing it.
22
+ *
23
+ * The first indented line's leading whitespace is exactly one indent unit; using it verbatim
24
+ * as the JSON.stringify indent round-trips tabs, 2-space, and 4-space without branching on type.
25
+ * Minified/single-line files (no indented line) fall back to 2 spaces, matching prior behavior.
26
+ */
27
+ function detectJsonFormat(raw) {
28
+ const match = raw.match(/\n([ \t]+)\S/);
29
+ return {
30
+ indent: match ? match[1] : 2,
31
+ trailingNewline: raw.endsWith('\n'),
32
+ };
33
+ }
18
34
  async function readPackageJsonAsync(path) {
19
35
  try {
20
36
  const content = await fs_2.promises.readFile(path, 'utf-8');
@@ -16,15 +16,85 @@ const SKIP_DIRS = new Set([
16
16
  'esm',
17
17
  'cjs',
18
18
  ]);
19
- function shouldSkipDirectory(name) {
20
- return name.startsWith('.') || SKIP_DIRS.has(name);
19
+ /**
20
+ * Skip dirs that are ambiguous source-vs-build directories where a real package may legitimately
21
+ * live. Only these trigger the "silently skipped a package" warning — node_modules and build-output
22
+ * dirs (dist/build/coverage/out) routinely contain package.json files and would be pure noise.
23
+ */
24
+ const WARN_SKIP_DIRS = new Set(['lib', 'es', 'esm', 'cjs']);
25
+ /** Effective skip set: the defaults minus any directory the caller opted back into via `scanDirs`. */
26
+ function buildSkipSet(scanDirs) {
27
+ if (!scanDirs || scanDirs.length === 0) {
28
+ return SKIP_DIRS;
29
+ }
30
+ const skip = new Set(SKIP_DIRS);
31
+ for (const dir of scanDirs) {
32
+ skip.delete(dir);
33
+ }
34
+ return skip;
35
+ }
36
+ function classifyDirectory(name, skipSet) {
37
+ if (name.startsWith('.'))
38
+ return 'hidden';
39
+ if (skipSet.has(name))
40
+ return 'skip-dir';
41
+ return null;
21
42
  }
22
- function findAllPackageJsonFiles(rootDir = process.cwd(), excludePatterns = [], maxDepth = 10, onProgress) {
43
+ /**
44
+ * Cheaply decide whether a pruned directory looks like it holds a real package — a package.json
45
+ * directly inside it, or inside any immediate child (the common `lib/<pkg>/package.json` monorepo
46
+ * layout). Stays shallow (depth 1) so detecting a skip doesn't re-walk the subtree we just pruned.
47
+ */
48
+ function prunedDirHoldsPackage(dir) {
49
+ if ((0, fs_1.existsSync)((0, path_1.join)(dir, 'package.json'))) {
50
+ return true;
51
+ }
52
+ let entries;
53
+ try {
54
+ entries = (0, fs_1.readdirSync)(dir);
55
+ }
56
+ catch {
57
+ return false;
58
+ }
59
+ for (const entry of entries) {
60
+ if (entry.startsWith('.'))
61
+ continue;
62
+ const child = (0, path_1.join)(dir, entry);
63
+ try {
64
+ if ((0, fs_1.statSync)(child).isDirectory() && (0, fs_1.existsSync)((0, path_1.join)(child, 'package.json'))) {
65
+ return true;
66
+ }
67
+ }
68
+ catch {
69
+ // Skip children we can't stat
70
+ }
71
+ }
72
+ return false;
73
+ }
74
+ /**
75
+ * Decide whether to descend into a directory, and notify when one is pruned by the default skip
76
+ * list despite containing a package.json (so the caller can surface a "silently skipped" warning).
77
+ */
78
+ function shouldTraverse(name, fullPath, relativePath, skipSet, onSkippedPackageDir) {
79
+ const reason = classifyDirectory(name, skipSet);
80
+ if (reason === null) {
81
+ return true;
82
+ }
83
+ if (reason === 'skip-dir' &&
84
+ WARN_SKIP_DIRS.has(name) &&
85
+ onSkippedPackageDir &&
86
+ prunedDirHoldsPackage(fullPath)) {
87
+ onSkippedPackageDir(relativePath);
88
+ }
89
+ return false;
90
+ }
91
+ function findAllPackageJsonFiles(rootDir = process.cwd(), excludePatterns = [], maxDepth = 10, onProgress, options = {}) {
23
92
  const packageJsonFiles = [];
24
93
  const visitedPaths = new Set();
25
94
  let directoriesScanned = 0;
26
95
  let lastProgressAt = 0;
27
96
  const progressIntervalMs = 250;
97
+ const skipSet = buildSkipSet(options.scanDirs);
28
98
  const excludeRegexes = excludePatterns.map((pattern) => new RegExp(pattern, 'i'));
29
99
  function shouldExcludePath(relativePath) {
30
100
  return excludeRegexes.some((regex) => regex.test(relativePath));
@@ -72,8 +142,10 @@ function findAllPackageJsonFiles(rootDir = process.cwd(), excludePatterns = [],
72
142
  // Skip files/dirs we can't stat (broken symlinks, permission issues)
73
143
  continue;
74
144
  }
75
- if (stat.isDirectory() && !shouldSkipDirectory(file)) {
76
- traverseDirectory(fullPath, depth + 1);
145
+ if (stat.isDirectory()) {
146
+ if (shouldTraverse(file, fullPath, relativePath, skipSet, options.onSkippedPackageDir)) {
147
+ traverseDirectory(fullPath, depth + 1);
148
+ }
77
149
  }
78
150
  else if (file === 'package.json' && stat.isFile()) {
79
151
  packageJsonFiles.push(fullPath);
@@ -94,6 +166,7 @@ async function findAllPackageJsonFilesAsync(rootDir = process.cwd(), excludePatt
94
166
  let lastProgressAt = 0;
95
167
  const progressIntervalMs = 250;
96
168
  const concurrency = Math.max(1, Math.min(options.concurrency ?? 16, 64));
169
+ const skipSet = buildSkipSet(options.scanDirs);
97
170
  const excludeRegexes = excludePatterns.map((pattern) => new RegExp(pattern, 'i'));
98
171
  function shouldExcludePath(relativePath) {
99
172
  return excludeRegexes.some((regex) => regex.test(relativePath));
@@ -167,8 +240,10 @@ async function findAllPackageJsonFilesAsync(rootDir = process.cwd(), excludePatt
167
240
  catch {
168
241
  continue;
169
242
  }
170
- if (stat.isDirectory() && !shouldSkipDirectory(file)) {
171
- schedule(fullPath, depth + 1);
243
+ if (stat.isDirectory()) {
244
+ if (shouldTraverse(file, fullPath, relativePath, skipSet, options.onSkippedPackageDir)) {
245
+ schedule(fullPath, depth + 1);
246
+ }
172
247
  }
173
248
  else if (file === 'package.json' && stat.isFile()) {
174
249
  packageJsonFiles.push(fullPath);
@@ -23,6 +23,9 @@ __exportStar(require("./exec"), exports);
23
23
  __exportStar(require("./git"), exports);
24
24
  __exportStar(require("./version"), exports);
25
25
  __exportStar(require("./debug-logger"), exports);
26
+ __exportStar(require("./color"), exports);
27
+ __exportStar(require("./engines"), exports);
28
+ __exportStar(require("./manifest"), exports);
26
29
  // Re-export async functions for convenience
27
30
  var filesystem_1 = require("./filesystem");
28
31
  Object.defineProperty(exports, "readPackageJsonAsync", { enumerable: true, get: function () { return filesystem_1.readPackageJsonAsync; } });
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ /**
3
+ * Helpers for reading optional health signals out of an npm version manifest
4
+ * (the per-version object inside an abbreviated packument, or a full version
5
+ * document). Both fields are advisory and frequently absent.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.normalizeDeprecatedMessage = normalizeDeprecatedMessage;
9
+ exports.extractEnginesNode = extractEnginesNode;
10
+ /**
11
+ * npm represents deprecation as either a string message or the boolean `true`.
12
+ * Normalize both into a displayable message, or `undefined` when not deprecated.
13
+ */
14
+ function normalizeDeprecatedMessage(value) {
15
+ if (typeof value === 'string' && value.trim()) {
16
+ return value;
17
+ }
18
+ if (value === true) {
19
+ return 'This version is deprecated.';
20
+ }
21
+ return undefined;
22
+ }
23
+ /**
24
+ * Extract the `engines.node` range from a manifest's `engines` object, if any.
25
+ */
26
+ function extractEnginesNode(engines) {
27
+ if (typeof engines === 'object' && engines !== null) {
28
+ const node = engines.node;
29
+ if (typeof node === 'string' && node.trim()) {
30
+ return node;
31
+ }
32
+ }
33
+ return undefined;
34
+ }
35
+ //# sourceMappingURL=manifest.js.map
@@ -41,6 +41,7 @@ exports.isVersionOutdated = isVersionOutdated;
41
41
  exports.getOptimizedRangeVersion = getOptimizedRangeVersion;
42
42
  exports.findClosestMinorVersion = findClosestMinorVersion;
43
43
  const semver = __importStar(require("semver"));
44
+ const manifest_1 = require("./manifest");
44
45
  function extractMajorVersion(version) {
45
46
  if (!version)
46
47
  return null;
@@ -62,10 +63,16 @@ function versionIdentity(version) {
62
63
  }
63
64
  function parseVersions(raw) {
64
65
  const data = JSON.parse(raw);
65
- const allVersions = Object.keys(data.versions || {}).filter((v) => /^[0-9]+\.[0-9]+\.[0-9]+$/.test(v));
66
+ const versions = data.versions || {};
67
+ const allVersions = Object.keys(versions).filter((v) => /^[0-9]+\.[0-9]+\.[0-9]+$/.test(v));
66
68
  const sortedVersions = allVersions.sort(semver.rcompare);
67
69
  const latestVersion = sortedVersions.length > 0 ? sortedVersions[0] : 'unknown';
68
- return { latestVersion, allVersions };
70
+ // Surface health signals for the latest version straight from the abbreviated
71
+ // packument we already fetched — no extra request. Both fields are optional.
72
+ const latestManifest = versions[latestVersion];
73
+ const deprecated = (0, manifest_1.normalizeDeprecatedMessage)(latestManifest?.deprecated);
74
+ const enginesNode = (0, manifest_1.extractEnginesNode)(latestManifest?.engines);
75
+ return { latestVersion, allVersions, deprecated, enginesNode };
69
76
  }
70
77
  /**
71
78
  * Checks if a version is outdated compared to the latest version.