inup 1.5.6 → 1.6.2

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 (47) hide show
  1. package/README.md +30 -1
  2. package/dist/cli.js +42 -11
  3. package/dist/config/constants.js +6 -7
  4. package/dist/config/project-config.js +5 -0
  5. package/dist/core/package-detector.js +49 -1
  6. package/dist/core/upgrade-runner.js +9 -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/debug/index.js +1 -0
  11. package/dist/features/debug/renderer/performance-modal.js +17 -1
  12. package/dist/features/debug/services/perf-logger.js +129 -0
  13. package/dist/features/debug/services/performance-tracker.js +15 -3
  14. package/dist/features/headless/headless-runner.js +69 -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/interactive-ui.js +4 -2
  20. package/dist/services/http/adaptive-controller.js +153 -0
  21. package/dist/services/http/etag-store.js +91 -0
  22. package/dist/services/http/resizable-semaphore.js +71 -0
  23. package/dist/services/http/retry.js +23 -1
  24. package/dist/services/index.js +0 -1
  25. package/dist/services/npm-registry.js +198 -97
  26. package/dist/services/package-manager-detector.js +6 -3
  27. package/dist/ui/modal/package-info-sections/sections.js +24 -0
  28. package/dist/ui/presenters/health.js +24 -0
  29. package/dist/ui/renderer/package-list/rows.js +8 -3
  30. package/dist/ui/session/selection-state-builder.js +7 -2
  31. package/dist/ui/themes-colors.js +12 -3
  32. package/dist/ui/utils/cursor.js +9 -3
  33. package/dist/utils/color.js +38 -0
  34. package/dist/utils/debug-logger.js +8 -1
  35. package/dist/utils/engines.js +63 -0
  36. package/dist/utils/filesystem/io.js +16 -0
  37. package/dist/utils/filesystem/scan.js +82 -7
  38. package/dist/utils/index.js +4 -0
  39. package/dist/utils/local-env.js +81 -0
  40. package/dist/utils/manifest.js +35 -0
  41. package/dist/utils/version.js +9 -2
  42. package/package.json +8 -8
  43. package/dist/services/cache-manager.js +0 -95
  44. package/dist/services/jsdelivr/client.js +0 -191
  45. package/dist/services/jsdelivr/manifest.js +0 -136
  46. package/dist/services/jsdelivr-registry.js +0 -9
  47. package/dist/services/persistent-cache.js +0 -237
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 -->
@@ -83,7 +112,7 @@ inup [options]
83
112
 
84
113
  ## Privacy
85
114
 
86
- No tracking, no telemetry, no data collection. Package metadata is fetched directly from the npm registry. Download counts come from the npm downloads API. When needed for exact-version manifests, inup may fetch a pinned `package.json` from jsDelivr.
115
+ No tracking, no telemetry, no data collection. Package metadata is fetched directly from the npm registry. Download counts come from the npm downloads API. Changelog and release notes are fetched from GitHub.
87
116
 
88
117
  ## License
89
118
 
package/dist/cli.js CHANGED
@@ -9,23 +9,36 @@ 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");
15
16
  const git_1 = require("./utils/git");
16
17
  const ui_1 = require("./ui");
18
+ // Load developer-only toggles from <inup-repo>/.env.local before anything reads
19
+ // env. Best-effort, gitignored, never overrides real env. Lets perf/debug be
20
+ // "set once" across every project without shell config.
21
+ (0, utils_1.loadInupLocalEnv)();
17
22
  const program = new commander_1.Command();
18
23
  async function runCli(options) {
24
+ // Resolve colored-output intent before anything renders.
25
+ (0, utils_1.applyColorSetting)(options.color);
19
26
  const cwd = (0, path_1.resolve)(options.dir);
20
27
  if (options.debug || process.env.INUP_DEBUG === '1') {
21
28
  (0, utils_1.enableDebugLogging)();
22
29
  }
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;
30
+ // Headless when piped, in CI, or when a non-interactive flag is set. The TUI only renders in
31
+ // interactive mode; everything else routes through the read-only headless report path.
32
+ const interactive = !!process.stdout.isTTY && !process.env.CI && !options.json && !options.check;
33
+ // The dirty-tree prompt would hang without a TTY; headless is read-only anyway, so skip it.
34
+ if (interactive) {
35
+ const gitState = (0, git_1.getGitWorkingTreeState)(cwd);
36
+ if (gitState.isRepo && gitState.isDirty) {
37
+ const shouldProceed = await ui_1.TerminalInput.promptForImmediateConfirmation(`${chalk_1.default.yellow('Warning:')} dirty working tree. Proceed anyway? ${chalk_1.default.dim('[y/N]')} `, false);
38
+ if (!shouldProceed) {
39
+ console.log(chalk_1.default.yellow('Upgrade cancelled.'));
40
+ return;
41
+ }
29
42
  }
30
43
  }
31
44
  // Load project config from .inuprc
@@ -52,8 +65,11 @@ async function runCli(options) {
52
65
  console.error(chalk_1.default.yellow('Expected a non-negative integer, for example: --max-depth 10'));
53
66
  process.exit(1);
54
67
  }
55
- // Check for updates in the background (non-blocking)
56
- const updateCheckPromise = (0, services_1.checkForUpdateAsync)(config_1.PACKAGE_NAME, config_1.PACKAGE_VERSION);
68
+ // Check for updates in the background (non-blocking). Interactive only — keeps headless stdout
69
+ // clean and avoids a lingering fetch handle in CI.
70
+ const updateCheckPromise = interactive
71
+ ? (0, services_1.checkForUpdateAsync)(config_1.PACKAGE_NAME, config_1.PACKAGE_VERSION)
72
+ : undefined;
57
73
  // Validate package manager if provided
58
74
  let packageManager;
59
75
  if (options.packageManager) {
@@ -65,17 +81,28 @@ async function runCli(options) {
65
81
  }
66
82
  packageManager = options.packageManager;
67
83
  }
68
- const upgrader = new index_1.UpgradeRunner({
84
+ const runnerOptions = {
69
85
  cwd,
70
86
  excludePatterns,
87
+ scanDirs: projectConfig.scanDirs,
71
88
  maxDepth,
72
89
  ignorePackages,
73
90
  packageManager,
74
91
  showPeerDependencyVulnerabilities: projectConfig.showPeerDependencyVulnerabilities ?? false,
75
92
  showOptionalDependencyVulnerabilities: projectConfig.showOptionalDependencyVulnerabilities ?? false,
76
93
  debug: options.debug || process.env.INUP_DEBUG === '1',
77
- });
78
- await upgrader.run();
94
+ saveExact: options.saveExact ?? false,
95
+ // Adaptive concurrency defaults ON; it's an internal/dev toggle with no public
96
+ // flag. Set INUP_ADAPTIVE=0 to disable (e.g. for A/B perf comparisons).
97
+ adaptive: process.env.INUP_ADAPTIVE !== '0',
98
+ };
99
+ // Non-interactive (piped / CI / --json / --check) routes to the read-only headless feature;
100
+ // only the interactive path builds the full TUI runner.
101
+ if (!interactive) {
102
+ await new headless_1.HeadlessRunner(runnerOptions).run({ json: options.json, check: options.check });
103
+ return;
104
+ }
105
+ await new index_1.UpgradeRunner(runnerOptions).run();
79
106
  // After the main flow completes, check if there's an update available
80
107
  const updateCheck = await updateCheckPromise;
81
108
  if (updateCheck?.isOutdated) {
@@ -109,6 +136,10 @@ program
109
136
  .option('--max-depth <number>', 'maximum directory depth for package.json discovery', '10')
110
137
  .option('--package-manager <name>', 'manually specify package manager (npm, yarn, pnpm, bun)')
111
138
  .option('--debug', 'write verbose debug log to /tmp/inup-debug-YYYY-MM-DD.log')
139
+ .option('--no-color', 'disable colored output (also respects NO_COLOR / FORCE_COLOR)')
140
+ .option('--save-exact', 'write exact versions instead of preserving the range prefix (^/~)')
141
+ .option('--json', 'print a machine-readable JSON report and exit (non-interactive, read-only)')
142
+ .option('-c, --check', 'exit non-zero if updates exist, without writing (for CI; read-only)')
112
143
  .action(runCli);
113
144
  // Handle uncaught errors gracefully
114
145
  process.on('uncaughtException', (error) => {
@@ -1,15 +1,14 @@
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.POOL_CONNECTIONS = exports.REQUEST_TIMEOUT = 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
- exports.MAX_CONCURRENT_REQUESTS = 150;
10
- exports.CACHE_TTL = 5 * 60 * 1000; // 5 minutes in milliseconds
11
8
  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
9
+ // Upper bound for both the undici Pool's connection count and the adaptive
10
+ // concurrency controller's ceiling, kept as one const so they never drift apart.
11
+ // With pipelining:1, effective in-flight requests are capped by the pool's
12
+ // connection count, so the controller must never ramp past it.
13
+ exports.POOL_CONNECTIONS = 24;
15
14
  //# 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,15 +50,19 @@ class PackageDetector {
46
50
  packageJson = null;
47
51
  cwd;
48
52
  excludePatterns;
53
+ scanDirs;
49
54
  ignorePackages;
50
55
  maxDepth;
51
56
  batchSize = 10;
52
57
  maxConcurrency = 10;
58
+ adaptive;
53
59
  constructor(options) {
54
60
  this.cwd = options?.cwd || process.cwd();
55
61
  this.excludePatterns = options?.excludePatterns || [];
62
+ this.scanDirs = options?.scanDirs || [];
56
63
  this.ignorePackages = options?.ignorePackages || [];
57
64
  this.maxDepth = options?.maxDepth ?? 10;
65
+ this.adaptive = options?.adaptive ?? true;
58
66
  this.packageJsonPath = (0, utils_1.findPackageJson)(this.cwd);
59
67
  if (this.packageJsonPath) {
60
68
  this.packageJson = (0, utils_1.readPackageJson)(this.packageJsonPath);
@@ -63,6 +71,19 @@ class PackageDetector {
63
71
  hasPackageJson() {
64
72
  return this.packageJsonPath !== null && this.packageJson !== null;
65
73
  }
74
+ /**
75
+ * The resolved fetch configuration for this run, for perf logging. Exposes the
76
+ * exact values handed to the registry fetcher so a logged run is reproducible.
77
+ */
78
+ getPerfConfig() {
79
+ return {
80
+ cwd: this.cwd,
81
+ adaptive: this.adaptive,
82
+ maxConcurrency: this.maxConcurrency,
83
+ batchSize: this.batchSize,
84
+ poolConnections: config_1.POOL_CONNECTIONS,
85
+ };
86
+ }
66
87
  async getOutdatedPackages() {
67
88
  const packages = [];
68
89
  await this.streamOutdatedPackages((event) => {
@@ -103,6 +124,11 @@ class PackageDetector {
103
124
  currentVersions: prepared.currentVersions,
104
125
  batchSize: this.batchSize,
105
126
  maxConcurrency: this.maxConcurrency,
127
+ adaptive: this.adaptive,
128
+ onControlTick: (tick) => performanceTracker.recordControlTick(tick),
129
+ onPackageTiming: (0, debug_1.isPerfLoggingEnabled)()
130
+ ? (name, latencyMs) => performanceTracker.recordPackageTiming({ name, latencyMs })
131
+ : undefined,
106
132
  onBatchReady: (batch) => {
107
133
  const batchStart = lastBatchEndAt;
108
134
  let batchFailedCount = 0;
@@ -282,6 +308,8 @@ class PackageDetector {
282
308
  hasRangeUpdate,
283
309
  hasMajorUpdate,
284
310
  allVersions,
311
+ deprecated: packageData.deprecated,
312
+ enginesNode: packageData.enginesNode,
285
313
  };
286
314
  }
287
315
  catch (error) {
@@ -313,13 +341,17 @@ class PackageDetector {
313
341
  };
314
342
  }
315
343
  async findPackageJsonFilesWithTimeout(timeoutMs) {
344
+ const skippedPackageDirs = new Set();
316
345
  try {
317
346
  let timeoutId;
318
347
  try {
319
- return await Promise.race([
348
+ const files = await Promise.race([
320
349
  (0, utils_1.findAllPackageJsonFilesAsync)(this.cwd, this.excludePatterns, this.maxDepth, (currentDir, foundCount) => {
321
350
  const truncatedDir = currentDir.length > 50 ? '...' + currentDir.slice(-47) : currentDir;
322
351
  this.showProgress(`🔍 Scanning ${truncatedDir} (found ${foundCount})`);
352
+ }, {
353
+ scanDirs: this.scanDirs,
354
+ onSkippedPackageDir: (relativePath) => skippedPackageDirs.add(relativePath),
323
355
  }),
324
356
  new Promise((_, reject) => {
325
357
  timeoutId = setTimeout(() => {
@@ -328,6 +360,8 @@ class PackageDetector {
328
360
  timeoutId.unref?.();
329
361
  }),
330
362
  ]);
363
+ this.warnSkippedPackageDirs(skippedPackageDirs);
364
+ return files;
331
365
  }
332
366
  finally {
333
367
  if (timeoutId) {
@@ -339,6 +373,20 @@ class PackageDetector {
339
373
  throw new Error(`Failed to scan for package.json files: ${err}. Try using --exclude patterns to skip problematic directories.`);
340
374
  }
341
375
  }
376
+ /**
377
+ * Warn (once per directory, after the scan) about package.json-bearing directories that the
378
+ * default skip list pruned, so the user can re-include them via `.inuprc`'s `scanDirs`.
379
+ * Emitted after scanning so it does not corrupt the progress spinner output.
380
+ */
381
+ warnSkippedPackageDirs(skippedPackageDirs) {
382
+ if (skippedPackageDirs.size === 0) {
383
+ return;
384
+ }
385
+ const list = Array.from(skippedPackageDirs).sort();
386
+ console.warn(chalk_1.default.yellow(`⚠️ Skipped ${list.length} package.json-bearing director${list.length === 1 ? 'y' : 'ies'} matching the default ignore list:\n` +
387
+ list.map((dir) => ` - ${dir}`).join('\n') +
388
+ `\n Add the directory name(s) to "scanDirs" in .inuprc to include them.`));
389
+ }
342
390
  isWorkspaceReference(version) {
343
391
  return (version.includes('workspace:') ||
344
392
  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
  }
@@ -91,6 +92,14 @@ class UpgradeRunner {
91
92
  progress.isLoading = event.payload.progress.isLoading;
92
93
  performanceTracker.mark('firstBatch');
93
94
  performanceTracker.mark('allLoaded');
95
+ if ((0, debug_1.isPerfLoggingEnabled)()) {
96
+ (0, debug_1.writePerfLog)({
97
+ ...this.detector.getPerfConfig(),
98
+ packageManager: this.packageManager.name,
99
+ mode: 'interactive',
100
+ env: (0, debug_1.perfEnv)(),
101
+ }, performanceTracker.snapshot());
102
+ }
94
103
  refreshUI?.();
95
104
  }
96
105
  });
@@ -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
@@ -16,5 +16,6 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./types/debug.types"), exports);
18
18
  __exportStar(require("./services/performance-tracker"), exports);
19
+ __exportStar(require("./services/perf-logger"), exports);
19
20
  __exportStar(require("./renderer/performance-modal"), exports);
20
21
  //# sourceMappingURL=index.js.map
@@ -21,7 +21,7 @@ function labelValue(label, value, labelWidth = 22) {
21
21
  return `${chalk_1.default.white(padded)} ${value}`;
22
22
  }
23
23
  function buildSections(snapshot) {
24
- const { phases, counts, batches, failedPackages, packageManager, totalMs } = snapshot;
24
+ const { phases, counts, batches, controlTicks, failedPackages, packageManager, totalMs } = snapshot;
25
25
  const pinned = [
26
26
  {
27
27
  key: 'header',
@@ -67,6 +67,22 @@ function buildSections(snapshot) {
67
67
  bodyRows.push(chalk_1.default.gray(' (no batches recorded)'));
68
68
  }
69
69
  bodyRows.push('');
70
+ bodyRows.push(chalk_1.default.bold('Concurrency'));
71
+ if (controlTicks.length > 0) {
72
+ const limits = controlTicks.map((t) => t.limit);
73
+ const finalTick = controlTicks[controlTicks.length - 1];
74
+ const hardDowns = controlTicks.filter((t) => t.reason === 'hard-down').length;
75
+ bodyRows.push(labelValue('Start limit', formatCount(controlTicks[0].limit)));
76
+ bodyRows.push(labelValue('Peak limit', formatCount(Math.max(...limits))));
77
+ bodyRows.push(labelValue('Final limit', formatCount(finalTick.limit)));
78
+ bodyRows.push(labelValue('Final EWMA', formatMs(finalTick.ewmaMs)));
79
+ bodyRows.push(labelValue('Control ticks', formatCount(controlTicks.length)));
80
+ bodyRows.push(labelValue('Hard back-offs', formatCount(hardDowns)));
81
+ }
82
+ else {
83
+ bodyRows.push(chalk_1.default.gray(' (fixed — adaptive off or run too small)'));
84
+ }
85
+ bodyRows.push('');
70
86
  bodyRows.push(chalk_1.default.bold('Failures'));
71
87
  if (failedPackages.length > 0) {
72
88
  for (const name of failedPackages) {
@@ -0,0 +1,129 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isPerfLoggingEnabled = isPerfLoggingEnabled;
4
+ exports.perfEnv = perfEnv;
5
+ exports.getPerfDir = getPerfDir;
6
+ exports.writePerfLog = writePerfLog;
7
+ const fs_1 = require("fs");
8
+ const path_1 = require("path");
9
+ const adaptive_controller_1 = require("../../../services/http/adaptive-controller");
10
+ /**
11
+ * Performance debug logger.
12
+ *
13
+ * When INUP_PERF=1, every run writes ONE self-contained JSON file capturing the
14
+ * full configuration plus the performance snapshot (phases, batches, adaptive
15
+ * control ticks, counts). The files accumulate in a gitignored directory so a
16
+ * series of runs can be diffed to find the best-performing configuration.
17
+ *
18
+ * Output location:
19
+ * - Default: `<cwd>/.inup-perf` (logs next to the project being scanned).
20
+ * - INUP_PERF_DIR=<path>: a single centralized directory (e.g. the inup repo)
21
+ * so runs from many different projects collect in one place for analysis.
22
+ * The project name is encoded into each filename so they stay distinct.
23
+ *
24
+ * Each file is independent: it records every input that could affect timing, so
25
+ * a run can be understood (and reproduced) without external context.
26
+ */
27
+ const PERF_DIR_NAME = '.inup-perf';
28
+ /** Filesystem-safe slug of a project name for use in filenames. */
29
+ function projectSlug(cwd) {
30
+ const name = (0, path_1.basename)((0, path_1.resolve)(cwd)) || 'root';
31
+ return name.replace(/[^a-zA-Z0-9._-]/g, '-').slice(0, 40);
32
+ }
33
+ function isPerfLoggingEnabled() {
34
+ return process.env.INUP_PERF === '1';
35
+ }
36
+ /** Capture the env toggles that influence a run, for reproducibility. */
37
+ function perfEnv() {
38
+ return {
39
+ INUP_ADAPTIVE: process.env.INUP_ADAPTIVE,
40
+ INUP_PERF: process.env.INUP_PERF,
41
+ INUP_DEBUG: process.env.INUP_DEBUG,
42
+ CI: process.env.CI,
43
+ NODE_ENV: process.env.NODE_ENV,
44
+ };
45
+ }
46
+ /**
47
+ * Resolve (and lazily create) the perf-log directory.
48
+ *
49
+ * - INUP_PERF_DIR set → that directory verbatim (centralized collection point,
50
+ * e.g. the inup repo's .inup-perf). Relative paths resolve from cwd.
51
+ * - otherwise → `<cwd>/.inup-perf` (logs sit next to the scanned project).
52
+ */
53
+ function getPerfDir(cwd = process.cwd()) {
54
+ const override = process.env.INUP_PERF_DIR;
55
+ const dir = override
56
+ ? (0, path_1.isAbsolute)(override)
57
+ ? override
58
+ : (0, path_1.resolve)(cwd, override)
59
+ : (0, path_1.join)(cwd, PERF_DIR_NAME);
60
+ if (!(0, fs_1.existsSync)(dir)) {
61
+ (0, fs_1.mkdirSync)(dir, { recursive: true });
62
+ }
63
+ return dir;
64
+ }
65
+ const pad = (n, width = 2) => String(n).padStart(width, '0');
66
+ function fileStamp(d) {
67
+ return (`${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}` +
68
+ `-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}` +
69
+ `-${pad(d.getMilliseconds(), 3)}`);
70
+ }
71
+ /**
72
+ * Write one perf record to its own JSON file. Best-effort: never throws into the
73
+ * caller (a debugging aid must not break a real run). Returns the path written,
74
+ * or null if disabled or on failure.
75
+ */
76
+ function writePerfLog(config, snapshot) {
77
+ if (!isPerfLoggingEnabled())
78
+ return null;
79
+ try {
80
+ const now = new Date();
81
+ const record = {
82
+ schemaVersion: 1,
83
+ timestamp: now.toISOString(),
84
+ wallMs: snapshot.totalMs,
85
+ config,
86
+ tuning: adaptive_controller_1.DEFAULT_TUNING,
87
+ snapshot,
88
+ };
89
+ const dir = getPerfDir(config.cwd);
90
+ const modeTag = config.mode;
91
+ const adaptiveTag = config.adaptive ? 'adaptive' : 'fixed';
92
+ // Project name in the filename keeps multi-project runs distinct when many
93
+ // collect into one centralized INUP_PERF_DIR.
94
+ const fileName = `run-${fileStamp(now)}-${projectSlug(config.cwd)}-${adaptiveTag}-${modeTag}.json`;
95
+ const filePath = (0, path_1.join)(dir, fileName);
96
+ (0, fs_1.writeFileSync)(filePath, JSON.stringify(record, null, 2));
97
+ // Maintain a stable pointer to the most recent run for quick reads.
98
+ (0, fs_1.writeFileSync)((0, path_1.join)(dir, 'latest.json'), JSON.stringify(record, null, 2));
99
+ // And a cheap one-line-per-run index for fast scanning.
100
+ appendPerfIndex(dir, record);
101
+ if (process.env.INUP_DEBUG === '1') {
102
+ process.stderr.write(`[inup] perf log → ${filePath}\n`);
103
+ }
104
+ return filePath;
105
+ }
106
+ catch {
107
+ // Never let perf logging affect the run.
108
+ return null;
109
+ }
110
+ }
111
+ /** Append a single line to a NDJSON index for ultra-cheap scanning, best-effort. */
112
+ function appendPerfIndex(dir, record) {
113
+ try {
114
+ const line = JSON.stringify({
115
+ timestamp: record.timestamp,
116
+ wallMs: record.wallMs,
117
+ adaptive: record.config.adaptive,
118
+ mode: record.config.mode,
119
+ uniquePackages: record.snapshot.counts.uniquePackages,
120
+ registryFetch: record.snapshot.phases.registryFetch,
121
+ controlTicks: record.snapshot.controlTicks.length,
122
+ }) + '\n';
123
+ (0, fs_1.appendFileSync)((0, path_1.join)(dir, 'index.ndjson'), line);
124
+ }
125
+ catch {
126
+ /* best-effort */
127
+ }
128
+ }
129
+ //# sourceMappingURL=perf-logger.js.map