inup 1.5.5 → 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 (75) hide show
  1. package/README.md +53 -10
  2. package/dist/cli.js +53 -31
  3. package/dist/config/constants.js +4 -6
  4. package/dist/config/package-meta.js +10 -0
  5. package/dist/config/project-config.js +11 -1
  6. package/dist/core/package-detector.js +37 -5
  7. package/dist/core/upgrade-runner.js +8 -10
  8. package/dist/core/upgrader.js +15 -11
  9. package/dist/features/changelog/clients/github-client.js +5 -6
  10. package/dist/features/changelog/services/changelog-service.js +2 -4
  11. package/dist/features/changelog/services/package-metadata-service.js +7 -23
  12. package/dist/features/changelog/services/release-notes-service.js +4 -29
  13. package/dist/features/debug/services/performance-tracker.js +6 -8
  14. package/dist/features/headless/headless-runner.js +53 -0
  15. package/dist/features/headless/index.js +27 -0
  16. package/dist/features/headless/report.js +64 -0
  17. package/dist/features/headless/types.js +6 -0
  18. package/dist/features/headless/vulnerability-audit.js +106 -0
  19. package/dist/index.js +1 -2
  20. package/dist/interactive-ui.js +15 -606
  21. package/dist/services/background-audit.js +3 -5
  22. package/dist/services/cache-manager.js +5 -7
  23. package/dist/services/http/inflight.js +22 -0
  24. package/dist/services/http/retry.js +30 -0
  25. package/dist/services/index.js +0 -1
  26. package/dist/services/npm-registry.js +20 -97
  27. package/dist/services/package-manager-detector.js +6 -3
  28. package/dist/services/persistent-cache.js +8 -13
  29. package/dist/types/domain.js +3 -0
  30. package/dist/types/streaming.js +3 -0
  31. package/dist/types/ui.js +3 -0
  32. package/dist/types.js +17 -0
  33. package/dist/ui/controllers/package-info-modal-controller.js +22 -31
  34. package/dist/ui/controllers/vulnerability-audit-controller.js +17 -8
  35. package/dist/ui/index.js +3 -0
  36. package/dist/ui/input-handler.js +58 -75
  37. package/dist/ui/keymap.js +208 -0
  38. package/dist/ui/modal/package-info-sections/release-notes.js +161 -0
  39. package/dist/ui/modal/package-info-sections/sections.js +174 -0
  40. package/dist/ui/modal/package-info-sections/text.js +75 -0
  41. package/dist/ui/modal/package-info-sections.js +15 -369
  42. package/dist/ui/modal/package-info.js +1 -1
  43. package/dist/ui/presenters/health.js +24 -0
  44. package/dist/ui/renderer/help-modal.js +75 -0
  45. package/dist/ui/renderer/index.js +2 -5
  46. package/dist/ui/renderer/package-list/interface.js +184 -0
  47. package/dist/ui/renderer/package-list/rows.js +177 -0
  48. package/dist/ui/renderer/package-list.js +15 -455
  49. package/dist/ui/session/action-dispatcher.js +233 -0
  50. package/dist/ui/session/index.js +13 -0
  51. package/dist/ui/session/interactive-session.js +280 -0
  52. package/dist/ui/session/selection-state-builder.js +149 -0
  53. package/dist/ui/state/filter-manager.js +17 -6
  54. package/dist/ui/state/modal-manager.js +35 -0
  55. package/dist/ui/state/navigation-manager.js +33 -4
  56. package/dist/ui/state/state-manager.js +68 -3
  57. package/dist/ui/state/theme-manager.js +1 -0
  58. package/dist/ui/themes-colors.js +16 -4
  59. package/dist/ui/themes.js +11 -19
  60. package/dist/ui/utils/cursor.js +12 -7
  61. package/dist/ui/utils/index.js +6 -1
  62. package/dist/ui/utils/text.js +3 -2
  63. package/dist/ui/utils/version.js +60 -86
  64. package/dist/utils/color.js +38 -0
  65. package/dist/utils/config.js +13 -1
  66. package/dist/utils/engines.js +63 -0
  67. package/dist/utils/filesystem/io.js +103 -0
  68. package/dist/utils/filesystem/paths.js +19 -0
  69. package/dist/utils/filesystem/scan.js +280 -0
  70. package/dist/utils/filesystem.js +17 -335
  71. package/dist/utils/index.js +3 -0
  72. package/dist/utils/manifest.js +35 -0
  73. package/dist/utils/version.js +38 -1
  74. package/package.json +8 -7
  75. package/dist/services/jsdelivr-registry.js +0 -395
package/README.md CHANGED
@@ -49,23 +49,66 @@ 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
 
86
+ <!-- KEYS:START -->
57
87
  | Key | Action |
58
88
  |-----|--------|
59
- | `↑` `↓` | Navigate packages |
60
- | `←` `→` | Select version (current, patch, minor, major) |
61
- | `Space` | Toggle selection |
62
- | `m` | Select all minor updates |
63
- | `l` | Select all latest updates |
64
- | `u` | Unselect all |
65
- | `/` | Search packages |
66
- | `i` | View package info |
67
- | `t` | Change theme |
68
- | `Enter` | Confirm and upgrade |
89
+ | `↑ / k` | Move up |
90
+ | `↓ / j` | Move down |
91
+ | `g` | Jump to the first package |
92
+ | `G` | Jump to the last package |
93
+ | `←` | Cycle selection left (none → range → latest) |
94
+ | `→` | Cycle selection right (none → range → latest) |
95
+ | `Space` | Toggle the current package on/off |
96
+ | `m` | Select all minor/patch updates |
97
+ | `l` | Select all latest updates (including major) |
98
+ | `u` | Unselect all packages |
99
+ | `Enter` | Confirm selection and upgrade |
100
+ | `/` | Search packages by name |
101
+ | `d` | Toggle devDependencies |
102
+ | `p` | Toggle peerDependencies |
103
+ | `o` | Toggle optionalDependencies |
104
+ | `s` | Run the vulnerability audit |
105
+ | `v` | Show only vulnerable packages |
106
+ | `Esc` | Clear the active search filter |
107
+ | `i` | View package details and changelog |
108
+ | `t` | Change the color theme |
109
+ | `?` | Show this help |
110
+ | `!` | Show the performance/debug panel |
111
+ <!-- KEYS:END -->
69
112
 
70
113
  ## Privacy
71
114
 
package/dist/cli.js CHANGED
@@ -7,27 +7,34 @@ Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.runCli = runCli;
8
8
  const commander_1 = require("commander");
9
9
  const chalk_1 = __importDefault(require("chalk"));
10
- const fs_1 = require("fs");
11
10
  const path_1 = require("path");
12
11
  const index_1 = require("./index");
12
+ const headless_1 = require("./features/headless");
13
13
  const services_1 = require("./services");
14
14
  const config_1 = require("./config");
15
15
  const utils_1 = require("./utils");
16
16
  const git_1 = require("./utils/git");
17
- const terminal_input_1 = require("./ui/utils/terminal-input");
18
- const packageJson = JSON.parse((0, fs_1.readFileSync)((0, path_1.join)(__dirname, '../package.json'), 'utf-8'));
17
+ const ui_1 = require("./ui");
19
18
  const program = new commander_1.Command();
20
19
  async function runCli(options) {
20
+ // Resolve colored-output intent before anything renders.
21
+ (0, utils_1.applyColorSetting)(options.color);
21
22
  const cwd = (0, path_1.resolve)(options.dir);
22
23
  if (options.debug || process.env.INUP_DEBUG === '1') {
23
24
  (0, utils_1.enableDebugLogging)();
24
25
  }
25
- const gitState = (0, git_1.getGitWorkingTreeState)(cwd);
26
- if (gitState.isRepo && gitState.isDirty) {
27
- const shouldProceed = await terminal_input_1.TerminalInput.promptForImmediateConfirmation(`${chalk_1.default.yellow('Warning:')} dirty working tree. Proceed anyway? ${chalk_1.default.dim('[y/N]')} `, false);
28
- if (!shouldProceed) {
29
- console.log(chalk_1.default.yellow('Upgrade cancelled.'));
30
- 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
+ }
31
38
  }
32
39
  }
33
40
  // Load project config from .inuprc
@@ -54,8 +61,11 @@ async function runCli(options) {
54
61
  console.error(chalk_1.default.yellow('Expected a non-negative integer, for example: --max-depth 10'));
55
62
  process.exit(1);
56
63
  }
57
- // Check for updates in the background (non-blocking)
58
- const updateCheckPromise = (0, services_1.checkForUpdateAsync)('inup', packageJson.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;
59
69
  // Validate package manager if provided
60
70
  let packageManager;
61
71
  if (options.packageManager) {
@@ -67,50 +77,62 @@ async function runCli(options) {
67
77
  }
68
78
  packageManager = options.packageManager;
69
79
  }
70
- const upgrader = new index_1.UpgradeRunner({
80
+ const runnerOptions = {
71
81
  cwd,
72
82
  excludePatterns,
83
+ scanDirs: projectConfig.scanDirs,
73
84
  maxDepth,
74
85
  ignorePackages,
75
86
  packageManager,
76
87
  showPeerDependencyVulnerabilities: projectConfig.showPeerDependencyVulnerabilities ?? false,
77
88
  showOptionalDependencyVulnerabilities: projectConfig.showOptionalDependencyVulnerabilities ?? false,
78
89
  debug: options.debug || process.env.INUP_DEBUG === '1',
79
- });
80
- 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();
81
99
  // After the main flow completes, check if there's an update available
82
100
  const updateCheck = await updateCheckPromise;
83
101
  if (updateCheck?.isOutdated) {
84
- console.log('');
85
- console.log(chalk_1.default.yellow('┌' + '─'.repeat(78) + '┐'));
86
- console.log(chalk_1.default.yellow('│') +
87
- ' ' +
102
+ const columns = process.stdout.columns && process.stdout.columns > 0 ? process.stdout.columns : 80;
103
+ const innerWidth = Math.max(40, Math.min(columns, 100) - 2); // chars between the │ borders
104
+ const border = chalk_1.default.yellow;
105
+ const padTo = (visibleLength) => ' '.repeat(Math.max(0, innerWidth - visibleLength));
106
+ const line1Plain = ` Update available! ${updateCheck.currentVersion} → ${updateCheck.latestVersion}`;
107
+ const line1 = ' ' +
88
108
  chalk_1.default.bold.yellow('Update available! ') +
89
- chalk_1.default.gray(`${updateCheck.currentVersion}`) +
109
+ chalk_1.default.gray(updateCheck.currentVersion) +
90
110
  ' → ' +
91
- chalk_1.default.green(`${updateCheck.latestVersion}`) +
92
- ' '.repeat(78 - 19 - updateCheck.currentVersion.length - 3 - updateCheck.latestVersion.length - 1) +
93
- chalk_1.default.yellow(''));
94
- console.log(chalk_1.default.yellow('') +
95
- ' ' +
96
- chalk_1.default.gray('Run: ') +
97
- chalk_1.default.cyan(updateCheck.updateCommand) +
98
- ' '.repeat(78 - 6 - updateCheck.updateCommand.length - 1) +
99
- chalk_1.default.yellow('│'));
100
- console.log(chalk_1.default.yellow('└' + '─'.repeat(78) + '┘'));
111
+ chalk_1.default.green(updateCheck.latestVersion);
112
+ const line2Plain = ` Run: ${updateCheck.updateCommand}`;
113
+ const line2 = ' ' + chalk_1.default.gray('Run: ') + chalk_1.default.cyan(updateCheck.updateCommand);
114
+ console.log('');
115
+ console.log(border('┌' + '─'.repeat(innerWidth) + '┐'));
116
+ console.log(border('') + line1 + padTo(line1Plain.length) + border('│'));
117
+ console.log(border('│') + line2 + padTo(line2Plain.length) + border('│'));
118
+ console.log(border('└' + '─'.repeat(innerWidth) + '┘'));
101
119
  console.log('');
102
120
  }
103
121
  }
104
122
  program
105
- .name('inup')
123
+ .name(config_1.PACKAGE_NAME)
106
124
  .description('Interactive upgrade tool for package managers. Auto-detects and works with npm, yarn, pnpm, and bun.')
107
- .version(packageJson.version)
125
+ .version(config_1.PACKAGE_VERSION)
108
126
  .option('-d, --dir <directory>', 'specify directory to run in', process.cwd())
109
127
  .option('-e, --exclude <patterns>', 'exclude paths matching regex patterns (comma-separated)', '')
110
128
  .option('-i, --ignore <packages>', 'ignore packages (comma-separated, supports glob patterns like @babel/*)')
111
129
  .option('--max-depth <number>', 'maximum directory depth for package.json discovery', '10')
112
130
  .option('--package-manager <name>', 'manually specify package manager (npm, yarn, pnpm, bun)')
113
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)')
114
136
  .action(runCli);
115
137
  // Handle uncaught errors gracefully
116
138
  process.on('uncaughtException', (error) => {
@@ -1,13 +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_NAME = void 0;
4
- exports.PACKAGE_NAME = 'inup';
3
+ exports.REQUEST_TIMEOUT = exports.CACHE_TTL = exports.MAX_CONCURRENT_REQUESTS = exports.NPM_REGISTRY_URL = exports.PACKAGE_VERSION = exports.PACKAGE_NAME = void 0;
4
+ var package_meta_1 = require("./package-meta");
5
+ Object.defineProperty(exports, "PACKAGE_NAME", { enumerable: true, get: function () { return package_meta_1.PACKAGE_NAME; } });
6
+ Object.defineProperty(exports, "PACKAGE_VERSION", { enumerable: true, get: function () { return package_meta_1.PACKAGE_VERSION; } });
5
7
  exports.NPM_REGISTRY_URL = 'https://registry.npmjs.org';
6
- exports.JSDELIVR_CDN_URL = 'https://cdn.jsdelivr.net/npm';
7
8
  exports.MAX_CONCURRENT_REQUESTS = 150;
8
9
  exports.CACHE_TTL = 5 * 60 * 1000; // 5 minutes in milliseconds
9
10
  exports.REQUEST_TIMEOUT = 60000; // 60 seconds in milliseconds
10
- exports.JSDELIVR_RETRY_TIMEOUTS = [2000, 3500]; // short retry budget to keep fallback fast
11
- exports.JSDELIVR_RETRY_DELAYS = [150]; // tiny backoff between jsDelivr retries in ms
12
- exports.JSDELIVR_POOL_TIMEOUT = 60000; // keep-alive/connect lifecycle should be looser than per-request timeouts
13
11
  //# sourceMappingURL=constants.js.map
@@ -0,0 +1,10 @@
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.PACKAGE_VERSION = exports.PACKAGE_NAME = void 0;
7
+ const package_json_1 = __importDefault(require("../../package.json"));
8
+ exports.PACKAGE_NAME = package_json_1.default.name;
9
+ exports.PACKAGE_VERSION = package_json_1.default.version;
10
+ //# sourceMappingURL=package-meta.js.map
@@ -4,7 +4,12 @@ exports.loadProjectConfig = loadProjectConfig;
4
4
  exports.isPackageIgnored = isPackageIgnored;
5
5
  const fs_1 = require("fs");
6
6
  const path_1 = require("path");
7
- const CONFIG_FILES = ['.inuprc', '.inuprc.json', 'inup.config.json'];
7
+ const package_meta_1 = require("./package-meta");
8
+ const CONFIG_FILES = [
9
+ `.${package_meta_1.PACKAGE_NAME}rc`,
10
+ `.${package_meta_1.PACKAGE_NAME}rc.json`,
11
+ `${package_meta_1.PACKAGE_NAME}.config.json`,
12
+ ];
8
13
  /**
9
14
  * Load project configuration from .inuprc, .inuprc.json, or inup.config.json
10
15
  * Searches in the specified directory and parent directories up to root
@@ -49,6 +54,11 @@ function normalizeConfig(config) {
49
54
  normalized.exclude = config.exclude.filter((item) => typeof item === 'string');
50
55
  }
51
56
  }
57
+ if (config.scanDirs) {
58
+ if (Array.isArray(config.scanDirs)) {
59
+ normalized.scanDirs = config.scanDirs.filter((item) => typeof item === 'string');
60
+ }
61
+ }
52
62
  if (typeof config.showPeerDependencyVulnerabilities === 'boolean') {
53
63
  normalized.showPeerDependencyVulnerabilities = config.showPeerDependencyVulnerabilities;
54
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");
@@ -42,13 +46,19 @@ const utils_2 = require("../ui/utils");
42
46
  const utils_3 = require("../utils");
43
47
  const debug_1 = require("../features/debug");
44
48
  class PackageDetector {
49
+ packageJsonPath = null;
50
+ packageJson = null;
51
+ cwd;
52
+ excludePatterns;
53
+ scanDirs;
54
+ ignorePackages;
55
+ maxDepth;
56
+ batchSize = 10;
57
+ maxConcurrency = 10;
45
58
  constructor(options) {
46
- this.packageJsonPath = null;
47
- this.packageJson = null;
48
- this.batchSize = 10;
49
- this.maxConcurrency = 10;
50
59
  this.cwd = options?.cwd || process.cwd();
51
60
  this.excludePatterns = options?.excludePatterns || [];
61
+ this.scanDirs = options?.scanDirs || [];
52
62
  this.ignorePackages = options?.ignorePackages || [];
53
63
  this.maxDepth = options?.maxDepth ?? 10;
54
64
  this.packageJsonPath = (0, utils_1.findPackageJson)(this.cwd);
@@ -278,6 +288,8 @@ class PackageDetector {
278
288
  hasRangeUpdate,
279
289
  hasMajorUpdate,
280
290
  allVersions,
291
+ deprecated: packageData.deprecated,
292
+ enginesNode: packageData.enginesNode,
281
293
  };
282
294
  }
283
295
  catch (error) {
@@ -309,13 +321,17 @@ class PackageDetector {
309
321
  };
310
322
  }
311
323
  async findPackageJsonFilesWithTimeout(timeoutMs) {
324
+ const skippedPackageDirs = new Set();
312
325
  try {
313
326
  let timeoutId;
314
327
  try {
315
- return await Promise.race([
328
+ const files = await Promise.race([
316
329
  (0, utils_1.findAllPackageJsonFilesAsync)(this.cwd, this.excludePatterns, this.maxDepth, (currentDir, foundCount) => {
317
330
  const truncatedDir = currentDir.length > 50 ? '...' + currentDir.slice(-47) : currentDir;
318
331
  this.showProgress(`🔍 Scanning ${truncatedDir} (found ${foundCount})`);
332
+ }, {
333
+ scanDirs: this.scanDirs,
334
+ onSkippedPackageDir: (relativePath) => skippedPackageDirs.add(relativePath),
319
335
  }),
320
336
  new Promise((_, reject) => {
321
337
  timeoutId = setTimeout(() => {
@@ -324,6 +340,8 @@ class PackageDetector {
324
340
  timeoutId.unref?.();
325
341
  }),
326
342
  ]);
343
+ this.warnSkippedPackageDirs(skippedPackageDirs);
344
+ return files;
327
345
  }
328
346
  finally {
329
347
  if (timeoutId) {
@@ -335,6 +353,20 @@ class PackageDetector {
335
353
  throw new Error(`Failed to scan for package.json files: ${err}. Try using --exclude patterns to skip problematic directories.`);
336
354
  }
337
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
+ }
338
370
  isWorkspaceReference(version) {
339
371
  return (version.includes('workspace:') ||
340
372
  version === '*' ||
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.PnpmUpgradeInteractive = exports.UpgradeRunner = void 0;
6
+ exports.UpgradeRunner = void 0;
7
7
  const chalk_1 = __importDefault(require("chalk"));
8
8
  const package_detector_1 = require("./package-detector");
9
9
  const interactive_ui_1 = require("../interactive-ui");
@@ -15,8 +15,11 @@ const debug_1 = require("../features/debug");
15
15
  * Main orchestrator for the inup upgrade process
16
16
  */
17
17
  class UpgradeRunner {
18
+ detector;
19
+ ui;
20
+ upgrader;
21
+ packageManager;
18
22
  constructor(options) {
19
- this.options = options;
20
23
  // Detect package manager
21
24
  const cwd = options?.cwd || process.cwd();
22
25
  if (options?.packageManager) {
@@ -29,6 +32,7 @@ class UpgradeRunner {
29
32
  this.ui = new interactive_ui_1.InteractiveUI(this.packageManager, {
30
33
  showPeerDependencyVulnerabilities: options?.showPeerDependencyVulnerabilities ?? false,
31
34
  showOptionalDependencyVulnerabilities: options?.showOptionalDependencyVulnerabilities ?? false,
35
+ saveExact: options?.saveExact ?? false,
32
36
  });
33
37
  this.upgrader = new upgrader_1.PackageUpgrader(this.packageManager);
34
38
  }
@@ -96,14 +100,14 @@ class UpgradeRunner {
96
100
  let selectedChoices = await selectionPromise;
97
101
  const outdatedPackages = this.detector.getOutdatedPackagesOnly(latestPackages);
98
102
  if (outdatedPackages.length === 0 && selectedChoices.length === 0) {
99
- console.log(chalk_1.default.green('✅ All packages are up to date!'));
103
+ console.log(chalk_1.default.green('✅ Everything is up to date — no upgrades needed.'));
100
104
  return;
101
105
  }
102
106
  // Interactive selection and confirmation loop
103
107
  let shouldProceed = false;
104
108
  while (true) {
105
109
  if (selectedChoices.length === 0) {
106
- console.log(chalk_1.default.yellow('No packages selected. Exiting...'));
110
+ console.log(chalk_1.default.yellow('Nothing selected — no changes made.'));
107
111
  return;
108
112
  }
109
113
  // Validate selected choices before confirmation
@@ -184,10 +188,4 @@ class UpgradeRunner {
184
188
  }
185
189
  }
186
190
  exports.UpgradeRunner = UpgradeRunner;
187
- /**
188
- * @deprecated Use UpgradeRunner instead
189
- */
190
- class PnpmUpgradeInteractive extends UpgradeRunner {
191
- }
192
- exports.PnpmUpgradeInteractive = PnpmUpgradeInteractive;
193
191
  //# sourceMappingURL=upgrade-runner.js.map
@@ -11,16 +11,17 @@ const path_1 = require("path");
11
11
  const child_process_1 = require("child_process");
12
12
  const utils_1 = require("../utils");
13
13
  class PackageUpgrader {
14
+ packageManager;
14
15
  constructor(packageManager) {
15
16
  this.packageManager = packageManager;
16
17
  }
17
- async upgradePackages(choices, packageInfos) {
18
+ async upgradePackages(choices, _packageInfos) {
18
19
  if (choices.length === 0) {
19
20
  console.log(chalk_1.default.yellow('No packages to upgrade.'));
20
21
  return;
21
22
  }
22
23
  // Group choices by package.json path and dependency type
23
- const choicesByFileAndType = this.groupChoicesByFileAndType(choices, packageInfos);
24
+ const choicesByFileAndType = this.groupChoicesByFileAndType(choices);
24
25
  for (const [fileAndType, choiceList] of Object.entries(choicesByFileAndType)) {
25
26
  if (choiceList.length === 0)
26
27
  continue;
@@ -32,9 +33,9 @@ class PackageUpgrader {
32
33
  const uniquePackages = new Set(choices.map((c) => c.name));
33
34
  console.log(chalk_1.default.green(`\n✅ Successfully upgraded ${uniquePackages.size} package(s)!`));
34
35
  // Execute package manager install after all upgrades are complete
35
- await this.runInstall(choices, packageInfos);
36
+ await this.runInstall(choices);
36
37
  }
37
- async runInstall(choices, packageInfos) {
38
+ async runInstall(choices) {
38
39
  if (choices.length === 0) {
39
40
  return;
40
41
  }
@@ -71,7 +72,7 @@ class PackageUpgrader {
71
72
  throw new Error(`${this.packageManager.installCommand} exited with code ${result.status}`);
72
73
  }
73
74
  }
74
- groupChoicesByFileAndType(choices, _packageInfos) {
75
+ groupChoicesByFileAndType(choices) {
75
76
  const groups = {};
76
77
  choices.forEach((choice) => {
77
78
  const key = `${choice.packageJsonPath}|${choice.dependencyType}`;
@@ -91,11 +92,9 @@ class PackageUpgrader {
91
92
  const packageDir = packageJsonPath.replace('/package.json', '');
92
93
  const spinner = (0, nanospinner_1.createSpinner)(`Upgrading ${type} in ${packageDir}...`).start();
93
94
  try {
94
- // 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');
95
97
  const packageJson = (0, utils_1.readPackageJson)(packageJsonPath);
96
- // Find workspace root
97
- const workspaceRoot = (0, utils_1.findWorkspaceRoot)(packageDir, this.packageManager.name);
98
- const isWorkspaceRoot = packageDir === workspaceRoot;
99
98
  // Group by upgrade type (range vs latest)
100
99
  const rangeChoices = choices.filter((c) => c.upgradeType === 'range');
101
100
  const latestChoices = choices.filter((c) => c.upgradeType === 'latest');
@@ -120,9 +119,14 @@ class PackageUpgrader {
120
119
  modified = true;
121
120
  });
122
121
  }
123
- // 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.
124
124
  if (modified) {
125
- (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
+ }
126
130
  }
127
131
  spinner.success({ text: `Upgraded ${choices.length} ${type} in ${packageDir}` });
128
132
  // Show which packages were upgraded
@@ -2,12 +2,11 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.GitHubClient = void 0;
4
4
  const repository_ref_1 = require("../parsers/repository-ref");
5
+ const config_1 = require("../../../config");
5
6
  const GITHUB_RELEASES_PAGE_LIMIT = 3;
6
7
  class GitHubClient {
7
- constructor() {
8
- this.releasesCache = new Map();
9
- this.rawChangelogCache = new Map();
10
- }
8
+ releasesCache = new Map();
9
+ rawChangelogCache = new Map();
11
10
  clearCache() {
12
11
  this.releasesCache.clear();
13
12
  this.rawChangelogCache.clear();
@@ -21,7 +20,7 @@ class GitHubClient {
21
20
  method: 'GET',
22
21
  headers: {
23
22
  accept: 'application/vnd.github.v3+json',
24
- 'user-agent': 'inup-cli',
23
+ 'user-agent': `${config_1.PACKAGE_NAME}-cli`,
25
24
  },
26
25
  signal,
27
26
  });
@@ -72,7 +71,7 @@ class GitHubClient {
72
71
  method: 'GET',
73
72
  headers: {
74
73
  accept: 'application/vnd.github.v3+json',
75
- 'user-agent': 'inup-cli',
74
+ 'user-agent': `${config_1.PACKAGE_NAME}-cli`,
76
75
  },
77
76
  signal,
78
77
  });
@@ -4,10 +4,8 @@ exports.changelogFetcher = exports.ChangelogFetcher = void 0;
4
4
  const package_metadata_service_1 = require("./package-metadata-service");
5
5
  const release_notes_service_1 = require("./release-notes-service");
6
6
  class ChangelogFetcher {
7
- constructor() {
8
- this.metadataService = new package_metadata_service_1.PackageMetadataService();
9
- this.releaseNotesService = new release_notes_service_1.ReleaseNotesService(this.metadataService);
10
- }
7
+ metadataService = new package_metadata_service_1.PackageMetadataService();
8
+ releaseNotesService = new release_notes_service_1.ReleaseNotesService(this.metadataService);
11
9
  async fetchPackageMetadata(packageName, version, signal) {
12
10
  return await this.metadataService.fetchPackageMetadata(packageName, version, signal);
13
11
  }
@@ -3,13 +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");
6
+ const inflight_1 = require("../../../services/http/inflight");
7
7
  class PackageMetadataService {
8
- constructor(npmRegistryClient = new npm_registry_client_1.NpmRegistryClient(), exactManifestFetcher = jsdelivr_registry_1.fetchExactPackageManifest) {
8
+ npmRegistryClient;
9
+ cache = new Map();
10
+ inFlight = new inflight_1.InflightMap();
11
+ constructor(npmRegistryClient = new npm_registry_client_1.NpmRegistryClient()) {
9
12
  this.npmRegistryClient = npmRegistryClient;
10
- this.exactManifestFetcher = exactManifestFetcher;
11
- this.cache = new Map();
12
- this.inFlight = new Map();
13
13
  }
14
14
  clearCache() {
15
15
  this.cache.clear();
@@ -28,15 +28,7 @@ class PackageMetadataService {
28
28
  if (this.cache.has(cacheKey)) {
29
29
  return this.cache.get(cacheKey) ?? null;
30
30
  }
31
- const inFlight = this.inFlight.get(cacheKey);
32
- if (inFlight) {
33
- return await inFlight;
34
- }
35
- const lookupPromise = this.fetchAndCachePackageMetadata(packageName, version, signal).finally(() => {
36
- this.inFlight.delete(cacheKey);
37
- });
38
- this.inFlight.set(cacheKey, lookupPromise);
39
- return await lookupPromise;
31
+ return this.inFlight.dedupe(cacheKey, () => this.fetchAndCachePackageMetadata(packageName, version, signal));
40
32
  }
41
33
  cacheMetadata(packageName, rawData) {
42
34
  const metadata = this.buildMetadata(packageName, rawData);
@@ -90,15 +82,7 @@ class PackageMetadataService {
90
82
  }
91
83
  async fetchPackageManifest(packageName, version, signal) {
92
84
  signal?.throwIfAborted();
93
- const normalizedVersion = version?.trim();
94
- if (normalizedVersion) {
95
- const jsdelivrManifest = await this.exactManifestFetcher(packageName, normalizedVersion);
96
- if (jsdelivrManifest) {
97
- return jsdelivrManifest;
98
- }
99
- }
100
- signal?.throwIfAborted();
101
- return await this.npmRegistryClient.fetchPackageManifest(packageName, normalizedVersion || 'latest', signal);
85
+ return await this.npmRegistryClient.fetchPackageManifest(packageName, version?.trim() || 'latest', signal);
102
86
  }
103
87
  buildMetadata(packageName, rawData) {
104
88
  return (0, package_metadata_1.mapPackageManifestToMetadata)(packageName, rawData);