inup 1.6.6 → 1.6.8

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 (110) hide show
  1. package/README.md +25 -2
  2. package/dist/{interactive-ui.js → app/interactive-ui.js} +34 -21
  3. package/dist/{core → app}/upgrade-runner.js +18 -22
  4. package/dist/cli.js +19 -18
  5. package/dist/{services → features/audit}/background-audit.js +2 -2
  6. package/dist/features/{headless/vulnerability-audit.js → audit/headless-audit.js} +8 -11
  7. package/dist/features/audit/index.js +36 -0
  8. package/dist/{ui/presenters/vulnerability.js → features/audit/presenter.js} +1 -3
  9. package/dist/{ui/controllers → features/audit}/vulnerability-audit-controller.js +14 -8
  10. package/dist/{services → features/audit}/vulnerability-checker.js +9 -10
  11. package/dist/features/changelog/clients/github-client.js +41 -21
  12. package/dist/features/changelog/clients/npm-registry-client.js +16 -2
  13. package/dist/features/changelog/index.js +1 -1
  14. package/dist/features/changelog/services/package-metadata-service.js +6 -2
  15. package/dist/features/changelog/services/release-notes-service.js +9 -12
  16. package/dist/features/changelog/types.js +3 -0
  17. package/dist/features/debug/index.js +3 -4
  18. package/dist/features/debug/{services/perf-logger.js → perf-logger.js} +15 -15
  19. package/dist/features/debug/types.js +3 -0
  20. package/dist/features/headless/headless-runner.js +32 -19
  21. package/dist/features/headless/index.js +2 -5
  22. package/dist/features/headless/report.js +2 -0
  23. package/dist/features/interactive/controllers/index.js +6 -0
  24. package/dist/{ui → features/interactive}/controllers/package-info-modal-controller.js +3 -3
  25. package/dist/features/interactive/index.js +21 -0
  26. package/dist/{ui → features/interactive}/input-handler.js +6 -6
  27. package/dist/{ui → features/interactive}/modal/index.js +1 -1
  28. package/dist/{ui → features/interactive}/modal/layout.js +5 -5
  29. package/dist/{ui/renderer/package-list.js → features/interactive/modal/package-info-sections/index.js} +3 -3
  30. package/dist/{ui → features/interactive}/modal/package-info-sections/release-notes.js +1 -1
  31. package/dist/{ui → features/interactive}/modal/package-info-sections/sections.js +55 -24
  32. package/dist/{ui → features/interactive}/modal/package-info-sections/text.js +6 -6
  33. package/dist/{ui → features/interactive}/modal/package-info.js +33 -18
  34. package/dist/features/interactive/modal/types.js +3 -0
  35. package/dist/{ui → features/interactive}/presenters/health.js +2 -2
  36. package/dist/{ui → features/interactive}/renderer/confirmation.js +6 -3
  37. package/dist/{ui → features/interactive}/renderer/help-modal.js +3 -3
  38. package/dist/{ui → features/interactive}/renderer/index.js +3 -4
  39. package/dist/{types.js → features/interactive/renderer/package-list/index.js} +3 -4
  40. package/dist/{ui → features/interactive}/renderer/package-list/interface.js +15 -16
  41. package/dist/{ui → features/interactive}/renderer/package-list/rows.js +31 -25
  42. package/dist/features/{debug → interactive}/renderer/performance-modal.js +3 -3
  43. package/dist/{ui/utils/version.js → features/interactive/renderer/version-format.js} +9 -13
  44. package/dist/{ui → features/interactive}/session/action-dispatcher.js +2 -2
  45. package/dist/{ui → features/interactive}/session/index.js +5 -4
  46. package/dist/{ui → features/interactive}/session/interactive-session.js +44 -23
  47. package/dist/{ui → features/interactive}/session/selection-state-builder.js +25 -9
  48. package/dist/{ui → features/interactive}/state/filter-manager.js +3 -3
  49. package/dist/{ui → features/interactive}/state/index.js +7 -7
  50. package/dist/{ui → features/interactive}/state/state-manager.js +10 -8
  51. package/dist/{ui → features/interactive}/state/theme-manager.js +7 -5
  52. package/dist/{ui → features/interactive}/themes-colors.js +3 -1
  53. package/dist/features/upgrade/index.js +8 -0
  54. package/dist/{core → features/upgrade}/package-detector.js +96 -37
  55. package/dist/{core → features/upgrade}/upgrader.js +84 -35
  56. package/dist/index.js +2 -2
  57. package/dist/{config → shared/config}/package-meta.js +1 -1
  58. package/dist/{config → shared/config}/project-config.js +10 -8
  59. package/dist/{utils/config.js → shared/config/user-config.js} +11 -11
  60. package/dist/{utils → shared}/debug-logger.js +9 -9
  61. package/dist/{utils → shared}/exec.js +4 -4
  62. package/dist/{ui/modal/package-info-sections.js → shared/fs/index.js} +4 -3
  63. package/dist/{utils/filesystem → shared/fs}/io.js +3 -4
  64. package/dist/shared/fs/paths.js +19 -0
  65. package/dist/{utils/filesystem → shared/fs}/scan.js +23 -20
  66. package/dist/{utils → shared}/git.js +3 -3
  67. package/dist/{services → shared}/http/etag-store.js +56 -4
  68. package/dist/{services → shared}/http/resizable-semaphore.js +3 -1
  69. package/dist/{utils → shared}/local-env.js +8 -8
  70. package/dist/{services/package-manager-detector.js → shared/package-manager.js} +26 -25
  71. package/dist/shared/pnpm-catalogs.js +138 -0
  72. package/dist/{services → shared/registry}/npm-registry.js +54 -33
  73. package/dist/shared/registry/registry-config.js +76 -0
  74. package/dist/{services → shared/registry}/version-checker.js +25 -26
  75. package/dist/{ui/utils → shared/terminal}/cursor.js +1 -1
  76. package/dist/{ui/utils → shared/terminal}/index.js +5 -7
  77. package/dist/{ui/utils → shared/terminal}/terminal-input.js +5 -0
  78. package/dist/shared/terminal/text.js +48 -0
  79. package/dist/shared/types/index.js +20 -0
  80. package/dist/{utils/version.js → shared/versions.js} +45 -30
  81. package/package.json +21 -8
  82. package/dist/core/index.js +0 -23
  83. package/dist/features/changelog/types/changelog.types.js +0 -3
  84. package/dist/features/debug/types/debug.types.js +0 -3
  85. package/dist/services/index.js +0 -25
  86. package/dist/ui/controllers/index.js +0 -8
  87. package/dist/ui/index.js +0 -27
  88. package/dist/ui/presenters/index.js +0 -11
  89. package/dist/ui/utils/text.js +0 -76
  90. package/dist/utils/filesystem/paths.js +0 -19
  91. package/dist/utils/filesystem.js +0 -20
  92. package/dist/utils/index.js +0 -34
  93. /package/dist/{ui/modal → features/audit}/types.js +0 -0
  94. /package/dist/features/debug/{services/performance-tracker.js → performance-tracker.js} +0 -0
  95. /package/dist/{ui → features/interactive}/keymap.js +0 -0
  96. /package/dist/{ui → features/interactive}/modal/theme-selector.js +0 -0
  97. /package/dist/{ui → features/interactive}/state/modal-manager.js +0 -0
  98. /package/dist/{ui → features/interactive}/state/navigation-manager.js +0 -0
  99. /package/dist/{ui → features/interactive}/themes.js +0 -0
  100. /package/dist/{config → shared/config}/constants.js +0 -0
  101. /package/dist/{config → shared/config}/index.js +0 -0
  102. /package/dist/{utils → shared}/engines.js +0 -0
  103. /package/dist/{services → shared}/http/adaptive-controller.js +0 -0
  104. /package/dist/{services → shared}/http/inflight.js +0 -0
  105. /package/dist/{services → shared}/http/retry.js +0 -0
  106. /package/dist/{utils → shared}/manifest.js +0 -0
  107. /package/dist/{utils → shared/terminal}/color.js +0 -0
  108. /package/dist/{types → shared/types}/domain.js +0 -0
  109. /package/dist/{types → shared/types}/streaming.js +0 -0
  110. /package/dist/{types → shared/types}/ui.js +0 -0
@@ -4,8 +4,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.PackageManagerDetector = void 0;
7
- const fs_1 = require("fs");
8
- const path_1 = require("path");
7
+ const node_fs_1 = require("node:fs");
8
+ const node_path_1 = require("node:path");
9
9
  const chalk_1 = __importDefault(require("chalk"));
10
10
  const PACKAGE_MANAGERS = {
11
11
  npm: {
@@ -45,18 +45,19 @@ const PACKAGE_MANAGERS = {
45
45
  color: chalk_1.default.magenta,
46
46
  },
47
47
  };
48
+ // biome-ignore lint/complexity/noStaticOnlyClass: intentional namespace-style API used throughout the codebase
48
49
  class PackageManagerDetector {
49
50
  /**
50
51
  * Detect package manager from packageManager field or lock files
51
52
  */
52
53
  static detect(cwd = process.cwd()) {
53
54
  // 1. Check packageManager field in package.json
54
- const fromPackageJson = this.detectFromPackageJson(cwd);
55
+ const fromPackageJson = PackageManagerDetector.detectFromPackageJson(cwd);
55
56
  if (fromPackageJson) {
56
57
  return fromPackageJson;
57
58
  }
58
59
  // 2. Check for lock files
59
- const fromLockFile = this.detectFromLockFiles(cwd);
60
+ const fromLockFile = PackageManagerDetector.detectFromLockFiles(cwd);
60
61
  if (fromLockFile) {
61
62
  return fromLockFile;
62
63
  }
@@ -68,12 +69,12 @@ class PackageManagerDetector {
68
69
  * Detect from package.json packageManager field
69
70
  */
70
71
  static detectFromPackageJson(cwd) {
71
- const packageJsonPath = (0, path_1.join)(cwd, 'package.json');
72
- if (!(0, fs_1.existsSync)(packageJsonPath)) {
72
+ const packageJsonPath = (0, node_path_1.join)(cwd, 'package.json');
73
+ if (!(0, node_fs_1.existsSync)(packageJsonPath)) {
73
74
  return null;
74
75
  }
75
76
  try {
76
- const content = (0, fs_1.readFileSync)(packageJsonPath, 'utf-8');
77
+ const content = (0, node_fs_1.readFileSync)(packageJsonPath, 'utf-8');
77
78
  const packageJson = JSON.parse(content);
78
79
  if (packageJson.packageManager) {
79
80
  // Parse format: "pnpm@10.28.1" or "npm@9.0.0+sha512.abc..."
@@ -84,7 +85,7 @@ class PackageManagerDetector {
84
85
  }
85
86
  }
86
87
  }
87
- catch (error) {
88
+ catch {
88
89
  // Invalid package.json, continue to lock file detection
89
90
  }
90
91
  return null;
@@ -94,19 +95,19 @@ class PackageManagerDetector {
94
95
  */
95
96
  static detectFromLockFiles(cwd) {
96
97
  const lockFileChecks = [
97
- { pm: PACKAGE_MANAGERS.pnpm, path: (0, path_1.join)(cwd, 'pnpm-lock.yaml') },
98
- { pm: PACKAGE_MANAGERS.bun, path: (0, path_1.join)(cwd, 'bun.lockb') },
98
+ { pm: PACKAGE_MANAGERS.pnpm, path: (0, node_path_1.join)(cwd, 'pnpm-lock.yaml') },
99
+ { pm: PACKAGE_MANAGERS.bun, path: (0, node_path_1.join)(cwd, 'bun.lockb') },
99
100
  // Bun >= 1.2 writes a text `bun.lock` instead of the binary `bun.lockb`.
100
- { pm: PACKAGE_MANAGERS.bun, path: (0, path_1.join)(cwd, 'bun.lock') },
101
- { pm: PACKAGE_MANAGERS.yarn, path: (0, path_1.join)(cwd, 'yarn.lock') },
102
- { pm: PACKAGE_MANAGERS.npm, path: (0, path_1.join)(cwd, 'package-lock.json') },
101
+ { pm: PACKAGE_MANAGERS.bun, path: (0, node_path_1.join)(cwd, 'bun.lock') },
102
+ { pm: PACKAGE_MANAGERS.yarn, path: (0, node_path_1.join)(cwd, 'yarn.lock') },
103
+ { pm: PACKAGE_MANAGERS.npm, path: (0, node_path_1.join)(cwd, 'package-lock.json') },
103
104
  ];
104
105
  const existingLocks = lockFileChecks
105
- .filter(({ path }) => (0, fs_1.existsSync)(path))
106
+ .filter(({ path }) => (0, node_fs_1.existsSync)(path))
106
107
  .map(({ pm, path }) => ({
107
108
  pm,
108
109
  path,
109
- mtime: (0, fs_1.statSync)(path).mtime.getTime(),
110
+ mtime: (0, node_fs_1.statSync)(path).mtime.getTime(),
110
111
  }));
111
112
  if (existingLocks.length === 0) {
112
113
  return null;
@@ -131,20 +132,20 @@ class PackageManagerDetector {
131
132
  static findWorkspaceRoot(cwd = process.cwd(), packageManager) {
132
133
  const pmInfo = PACKAGE_MANAGERS[packageManager];
133
134
  let currentDir = cwd;
134
- while (currentDir !== (0, path_1.join)(currentDir, '..')) {
135
+ while (currentDir !== (0, node_path_1.join)(currentDir, '..')) {
135
136
  // Check for package manager-specific workspace file
136
137
  if (pmInfo.workspaceFile) {
137
- const workspaceFilePath = (0, path_1.join)(currentDir, pmInfo.workspaceFile);
138
- if ((0, fs_1.existsSync)(workspaceFilePath)) {
138
+ const workspaceFilePath = (0, node_path_1.join)(currentDir, pmInfo.workspaceFile);
139
+ if ((0, node_fs_1.existsSync)(workspaceFilePath)) {
139
140
  return currentDir;
140
141
  }
141
142
  }
142
143
  else {
143
144
  // Check for package.json with workspaces field
144
- const packageJsonPath = (0, path_1.join)(currentDir, 'package.json');
145
- if ((0, fs_1.existsSync)(packageJsonPath)) {
145
+ const packageJsonPath = (0, node_path_1.join)(currentDir, 'package.json');
146
+ if ((0, node_fs_1.existsSync)(packageJsonPath)) {
146
147
  try {
147
- const content = (0, fs_1.readFileSync)(packageJsonPath, 'utf-8');
148
+ const content = (0, node_fs_1.readFileSync)(packageJsonPath, 'utf-8');
148
149
  const packageJson = JSON.parse(content);
149
150
  // Check if workspaces field exists and is non-empty
150
151
  if (packageJson.workspaces) {
@@ -159,12 +160,12 @@ class PackageManagerDetector {
159
160
  }
160
161
  }
161
162
  }
162
- catch (error) {
163
+ catch {
163
164
  // Invalid package.json, continue searching
164
165
  }
165
166
  }
166
167
  }
167
- currentDir = (0, path_1.join)(currentDir, '..');
168
+ currentDir = (0, node_path_1.join)(currentDir, '..');
168
169
  }
169
170
  return null;
170
171
  }
@@ -172,8 +173,8 @@ class PackageManagerDetector {
172
173
  * Check if directory is in a workspace
173
174
  */
174
175
  static isInWorkspace(cwd, packageManager) {
175
- return this.findWorkspaceRoot(cwd, packageManager) !== null;
176
+ return PackageManagerDetector.findWorkspaceRoot(cwd, packageManager) !== null;
176
177
  }
177
178
  }
178
179
  exports.PackageManagerDetector = PackageManagerDetector;
179
- //# sourceMappingURL=package-manager-detector.js.map
180
+ //# sourceMappingURL=package-manager.js.map
@@ -0,0 +1,138 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PnpmCatalogs = exports.DEFAULT_CATALOG = exports.PNPM_WORKSPACE_FILE = void 0;
4
+ exports.isCatalogReference = isCatalogReference;
5
+ exports.writeCatalogUpdates = writeCatalogUpdates;
6
+ const node_fs_1 = require("node:fs");
7
+ const node_path_1 = require("node:path");
8
+ const yaml_1 = require("yaml");
9
+ const debug_logger_1 = require("./debug-logger");
10
+ exports.PNPM_WORKSPACE_FILE = 'pnpm-workspace.yaml';
11
+ /** Name pnpm gives the top-level `catalog:` map (also addressable as `catalogs.default`). */
12
+ exports.DEFAULT_CATALOG = 'default';
13
+ /** `catalog:` (default) or `catalog:<name>` — the pnpm catalog protocol. */
14
+ function isCatalogReference(version) {
15
+ return version.startsWith('catalog:');
16
+ }
17
+ /**
18
+ * The catalog definitions of the nearest `pnpm-workspace.yaml`.
19
+ *
20
+ * Catalogs (pnpm ≥ 9.5) hoist shared version ranges out of the member
21
+ * package.json files: a dependency declared as `"react": "catalog:"` gets its
22
+ * actual range from this file's `catalog:` map (or `catalogs.<name>` for
23
+ * `"catalog:<name>"` references). Upgrading such a dependency therefore means
24
+ * editing pnpm-workspace.yaml, not the referencing package.json.
25
+ */
26
+ class PnpmCatalogs {
27
+ path;
28
+ catalogs;
29
+ constructor(
30
+ /** Absolute path of the pnpm-workspace.yaml the catalogs were read from. */
31
+ path,
32
+ /** catalog name → (package name → range). */
33
+ catalogs) {
34
+ this.path = path;
35
+ this.catalogs = catalogs;
36
+ }
37
+ /**
38
+ * Load catalogs from the nearest pnpm-workspace.yaml at or above `startDir`.
39
+ * Returns null when there is no workspace file, it is unreadable, or it
40
+ * defines no catalogs — a broken file must never break a scan.
41
+ */
42
+ static load(startDir) {
43
+ const path = findPnpmWorkspaceFile(startDir);
44
+ if (!path)
45
+ return null;
46
+ try {
47
+ const raw = (0, yaml_1.parse)((0, node_fs_1.readFileSync)(path, 'utf8'));
48
+ if (!raw || typeof raw !== 'object')
49
+ return null;
50
+ const catalogs = new Map();
51
+ const addCatalog = (name, value) => {
52
+ if (!value || typeof value !== 'object' || Array.isArray(value))
53
+ return;
54
+ const entries = new Map();
55
+ for (const [pkg, range] of Object.entries(value)) {
56
+ if (typeof range === 'string')
57
+ entries.set(pkg, range);
58
+ }
59
+ if (entries.size > 0)
60
+ catalogs.set(name, entries);
61
+ };
62
+ addCatalog(exports.DEFAULT_CATALOG, raw.catalog);
63
+ if (raw.catalogs && typeof raw.catalogs === 'object' && !Array.isArray(raw.catalogs)) {
64
+ for (const [name, value] of Object.entries(raw.catalogs)) {
65
+ addCatalog(name, value);
66
+ }
67
+ }
68
+ if (catalogs.size === 0)
69
+ return null;
70
+ return new PnpmCatalogs(path, catalogs);
71
+ }
72
+ catch (error) {
73
+ debug_logger_1.debugLog.warn('PnpmCatalogs', `failed to parse ${path}: ${error}`);
74
+ return null;
75
+ }
76
+ }
77
+ /**
78
+ * Resolve a `catalog:` / `catalog:<name>` version specifier for `packageName`
79
+ * to the range declared in the workspace file, or null when the reference
80
+ * points at a catalog (or entry) that does not exist.
81
+ */
82
+ resolve(packageName, versionSpecifier) {
83
+ if (!isCatalogReference(versionSpecifier))
84
+ return null;
85
+ const name = versionSpecifier.slice('catalog:'.length).trim() || exports.DEFAULT_CATALOG;
86
+ const range = this.catalogs.get(name)?.get(packageName);
87
+ return range === undefined ? null : { catalog: name, range };
88
+ }
89
+ /** Every entry declared in the given catalog (empty for unknown catalogs). */
90
+ entriesOf(catalogName) {
91
+ const entries = this.catalogs.get(catalogName);
92
+ if (!entries)
93
+ return [];
94
+ return Array.from(entries, ([name, range]) => ({ name, range }));
95
+ }
96
+ }
97
+ exports.PnpmCatalogs = PnpmCatalogs;
98
+ /**
99
+ * Write new ranges for catalog entries back into pnpm-workspace.yaml.
100
+ *
101
+ * Edits go through the yaml Document API so comments, key order, and the
102
+ * formatting of untouched nodes survive the round-trip. Entries that no longer
103
+ * exist in the file (edited since the scan) are skipped, never invented.
104
+ */
105
+ function writeCatalogUpdates(workspaceFilePath, updates) {
106
+ const raw = (0, node_fs_1.readFileSync)(workspaceFilePath, 'utf8');
107
+ const doc = (0, yaml_1.parseDocument)(raw);
108
+ for (const update of updates) {
109
+ // The default catalog may live under `catalog:` or `catalogs.default:`.
110
+ const candidatePaths = update.catalog === exports.DEFAULT_CATALOG
111
+ ? [
112
+ ['catalog', update.name],
113
+ ['catalogs', exports.DEFAULT_CATALOG, update.name],
114
+ ]
115
+ : [['catalogs', update.catalog, update.name]];
116
+ const keyPath = candidatePaths.find((path) => doc.hasIn(path));
117
+ if (!keyPath) {
118
+ debug_logger_1.debugLog.warn('PnpmCatalogs', `catalog entry ${update.catalog}:${update.name} no longer exists in ${workspaceFilePath} — skipping`);
119
+ continue;
120
+ }
121
+ doc.setIn(keyPath, update.range);
122
+ }
123
+ (0, node_fs_1.writeFileSync)(workspaceFilePath, doc.toString());
124
+ }
125
+ /** Nearest pnpm-workspace.yaml at or above `startDir`, or null. */
126
+ function findPnpmWorkspaceFile(startDir) {
127
+ let dir = (0, node_path_1.resolve)(startDir);
128
+ for (;;) {
129
+ const candidate = (0, node_path_1.join)(dir, exports.PNPM_WORKSPACE_FILE);
130
+ if ((0, node_fs_1.existsSync)(candidate))
131
+ return candidate;
132
+ const parent = (0, node_path_1.dirname)(dir);
133
+ if (parent === dir)
134
+ return null;
135
+ dir = parent;
136
+ }
137
+ }
138
+ //# sourceMappingURL=pnpm-catalogs.js.map
@@ -2,22 +2,25 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.fetchPackageVersions = fetchPackageVersions;
4
4
  exports.clearPackageCache = clearPackageCache;
5
- const undici_1 = require("undici");
6
- const node_zlib_1 = require("node:zlib");
7
5
  const node_util_1 = require("node:util");
6
+ const node_zlib_1 = require("node:zlib");
7
+ const undici_1 = require("undici");
8
8
  const gunzipAsync = (0, node_util_1.promisify)(node_zlib_1.gunzip);
9
9
  const inflateAsync = (0, node_util_1.promisify)(node_zlib_1.inflate);
10
10
  const brotliDecompressAsync = (0, node_util_1.promisify)(node_zlib_1.brotliDecompress);
11
11
  const config_1 = require("../config");
12
- const version_1 = require("../utils/version");
13
- const retry_1 = require("./http/retry");
14
- const inflight_1 = require("./http/inflight");
15
- const resizable_semaphore_1 = require("./http/resizable-semaphore");
16
- const adaptive_controller_1 = require("./http/adaptive-controller");
17
- const etag_store_1 = require("./http/etag-store");
12
+ const adaptive_controller_1 = require("../http/adaptive-controller");
13
+ const etag_store_1 = require("../http/etag-store");
14
+ const inflight_1 = require("../http/inflight");
15
+ const resizable_semaphore_1 = require("../http/resizable-semaphore");
16
+ const retry_1 = require("../http/retry");
17
+ const versions_1 = require("../versions");
18
+ const registry_config_1 = require("./registry-config");
18
19
  const inFlightLookups = new inflight_1.InflightMap();
19
- const registryOrigin = new URL(config_1.NPM_REGISTRY_URL).origin;
20
- const registryPathPrefix = new URL(config_1.NPM_REGISTRY_URL).pathname.replace(/\/$/, '');
20
+ // One pool per registry origin: scoped packages may resolve to different
21
+ // registries (`@scope:registry` in .npmrc), and each origin keeps its own
22
+ // keep-alive connections. Most runs still touch a single origin.
23
+ //
21
24
  // Connection count is kept == the adaptive controller's ceiling (POOL_CONNECTIONS)
22
25
  // so the controller is never silently throttled below its chosen limit. Idle
23
26
  // keep-alive connections are cheap.
@@ -28,16 +31,24 @@ const registryPathPrefix = new URL(config_1.NPM_REGISTRY_URL).pathname.replace(/
28
31
  // controller. With a headers timeout, a stall surfaces as a transient error the
29
32
  // controller can react to (and retry handles). `bodyTimeout` stays 0 — large
30
33
  // packuments legitimately stream slowly.
31
- const registryPool = new undici_1.Pool(registryOrigin, {
32
- connections: config_1.POOL_CONNECTIONS,
33
- pipelining: 1,
34
- keepAliveTimeout: 30_000,
35
- keepAliveMaxTimeout: 600_000,
36
- headersTimeout: 30_000,
37
- bodyTimeout: 0,
38
- connectTimeout: 15_000,
39
- allowH2: false,
40
- });
34
+ const poolByOrigin = new Map();
35
+ function poolFor(origin) {
36
+ let pool = poolByOrigin.get(origin);
37
+ if (!pool) {
38
+ pool = new undici_1.Pool(origin, {
39
+ connections: config_1.POOL_CONNECTIONS,
40
+ pipelining: 1,
41
+ keepAliveTimeout: 30_000,
42
+ keepAliveMaxTimeout: 600_000,
43
+ headersTimeout: 30_000,
44
+ bodyTimeout: 0,
45
+ connectTimeout: 15_000,
46
+ allowH2: false,
47
+ });
48
+ poolByOrigin.set(origin, pool);
49
+ }
50
+ return pool;
51
+ }
41
52
  const MAX_REGISTRY_ATTEMPTS = 3;
42
53
  const RETRY_BACKOFF_MS = [500, 1500, 3000];
43
54
  // Fixed concurrency used when adaptive is disabled (INUP_ADAPTIVE=0, the A/B
@@ -48,27 +59,32 @@ async function getFreshPackageData(packageName, currentVersion, onAttempt) {
48
59
  const cacheKey = `${packageName}@${currentVersion ?? ''}`;
49
60
  return inFlightLookups.dedupe(cacheKey, () => fetchPackageFromRegistry(packageName, onAttempt));
50
61
  }
51
- const encodeRegistryPath = (packageName) => {
62
+ const encodeRegistryPath = (packageName, pathPrefix) => {
52
63
  const encodedName = packageName.startsWith('@')
53
64
  ? `@${encodeURIComponent(packageName.slice(1).split('/')[0])}/${encodeURIComponent(packageName.slice(packageName.indexOf('/') + 1))}`
54
65
  : encodeURIComponent(packageName);
55
- return `${registryPathPrefix}/${encodedName}`;
66
+ return `${pathPrefix}/${encodedName}`;
56
67
  };
57
- async function attemptRegistryFetch(path) {
68
+ async function attemptRegistryFetch(target, path) {
58
69
  const startedAt = Date.now();
59
70
  // Conditional request: if we have a stored ETag for this packument, ask the
60
71
  // registry to validate it. Unchanged → 304 (no body) and we reuse stored data.
61
72
  // This still hits the registry every run, so data is never served stale.
62
- const cached = (0, etag_store_1.readEtag)(path);
73
+ // Keys are origin-qualified so two registries can never collide on a path.
74
+ const cacheKey = `${target.origin}${path}`;
75
+ const cached = (0, etag_store_1.readEtag)(cacheKey);
63
76
  try {
64
77
  const requestHeaders = {
65
78
  accept: 'application/vnd.npm.install-v1+json',
66
79
  'accept-encoding': 'gzip, deflate, br',
67
80
  };
81
+ if (target.authHeader) {
82
+ requestHeaders.authorization = target.authHeader;
83
+ }
68
84
  if (cached) {
69
85
  requestHeaders['if-none-match'] = cached.etag;
70
86
  }
71
- const { statusCode, headers, body } = await registryPool.request({
87
+ const { statusCode, headers, body } = await poolFor(target.origin).request({
72
88
  path,
73
89
  method: 'GET',
74
90
  headers: requestHeaders,
@@ -112,12 +128,12 @@ async function attemptRegistryFetch(path) {
112
128
  else {
113
129
  decoded = raw;
114
130
  }
115
- const data = (0, version_1.parseVersions)(decoded.toString('utf8'));
131
+ const data = (0, versions_1.parseVersions)(decoded.toString('utf8'));
116
132
  // Persist the ETag for next run's conditional request.
117
- const etagHeader = headers['etag'];
133
+ const etagHeader = headers.etag;
118
134
  const etag = Array.isArray(etagHeader) ? etagHeader[0] : etagHeader;
119
135
  if (etag) {
120
- (0, etag_store_1.writeEtag)(path, etag.toString(), data);
136
+ (0, etag_store_1.writeEtag)(cacheKey, etag.toString(), data);
121
137
  }
122
138
  return {
123
139
  kind: 'success',
@@ -134,10 +150,10 @@ async function attemptRegistryFetch(path) {
134
150
  return { kind: 'transient' };
135
151
  }
136
152
  }
137
- async function fetchFromRegistryWithRetries(path, onAttempt) {
153
+ async function fetchFromRegistryWithRetries(target, path, onAttempt) {
138
154
  let lastOutcome = { kind: 'transient' };
139
155
  for (let attempt = 0; attempt < MAX_REGISTRY_ATTEMPTS; attempt++) {
140
- const outcome = await attemptRegistryFetch(path);
156
+ const outcome = await attemptRegistryFetch(target, path);
141
157
  onAttempt?.(outcome);
142
158
  if (outcome.kind === 'success' || outcome.kind === 'not-found') {
143
159
  return outcome;
@@ -154,8 +170,11 @@ async function fetchFromRegistryWithRetries(path, onAttempt) {
154
170
  return lastOutcome;
155
171
  }
156
172
  async function fetchPackageFromRegistry(packageName, onAttempt) {
157
- const path = encodeRegistryPath(packageName);
158
- const outcome = await fetchFromRegistryWithRetries(path, onAttempt);
173
+ // Scoped packages may live on a different registry (with credentials) than
174
+ // unscoped ones resolved from the npm config chain, memoized per scope.
175
+ const target = (0, registry_config_1.registryTargetFor)(packageName);
176
+ const path = encodeRegistryPath(packageName, target.pathPrefix);
177
+ const outcome = await fetchFromRegistryWithRetries(target, path, onAttempt);
159
178
  if (outcome.kind === 'success') {
160
179
  return outcome.data;
161
180
  }
@@ -213,8 +232,10 @@ async function fetchPackageVersions(packageNames, options = {}) {
213
232
  const pendingEmissions = new Map();
214
233
  let nextEmitIndex = 0;
215
234
  const flushPending = () => {
216
- while (pendingEmissions.has(nextEmitIndex)) {
235
+ while (true) {
217
236
  const ready = pendingEmissions.get(nextEmitIndex);
237
+ if (!ready)
238
+ break;
218
239
  pendingEmissions.delete(nextEmitIndex);
219
240
  options.onBatchReady?.(ready);
220
241
  nextEmitIndex++;
@@ -0,0 +1,76 @@
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.scopeOfPackage = scopeOfPackage;
7
+ exports.registryTargetFor = registryTargetFor;
8
+ exports.clearRegistryTargetCache = clearRegistryTargetCache;
9
+ const registry_auth_token_1 = __importDefault(require("registry-auth-token"));
10
+ const registry_url_1 = __importDefault(require("registry-auth-token/registry-url"));
11
+ const config_1 = require("../config");
12
+ // The submodule's .d.ts declares the second parameter as `{ npmrc?: … }`, but the
13
+ // implementation consumes the plain npmrc key/value record directly.
14
+ const getRegistryUrl = registry_url_1.default;
15
+ /** Scope of a package name (`@scope/pkg` → `@scope`), or undefined when unscoped. */
16
+ function scopeOfPackage(packageName) {
17
+ if (!packageName.startsWith('@'))
18
+ return undefined;
19
+ const slash = packageName.indexOf('/');
20
+ return slash > 0 ? packageName.slice(0, slash) : undefined;
21
+ }
22
+ // Config files are re-read on every resolution inside registry-auth-token, so
23
+ // memoize per scope: one resolution per distinct scope per process.
24
+ const targetByScope = new Map();
25
+ function registryTargetFor(packageName, npmrc) {
26
+ const scope = scopeOfPackage(packageName) ?? '';
27
+ if (!npmrc) {
28
+ const cached = targetByScope.get(scope);
29
+ if (cached)
30
+ return cached;
31
+ }
32
+ const target = resolveTarget(scope || undefined, npmrc);
33
+ if (!npmrc) {
34
+ targetByScope.set(scope, target);
35
+ }
36
+ return target;
37
+ }
38
+ function resolveTarget(scope, npmrc) {
39
+ let registryHref = config_1.NPM_REGISTRY_URL;
40
+ try {
41
+ registryHref = getRegistryUrl(scope, npmrc);
42
+ }
43
+ catch {
44
+ // Unreadable npm config must never break a run — fall back to the public registry.
45
+ }
46
+ let parsed;
47
+ try {
48
+ parsed = new URL(registryHref);
49
+ }
50
+ catch {
51
+ parsed = new URL(config_1.NPM_REGISTRY_URL);
52
+ }
53
+ let authHeader;
54
+ try {
55
+ const auth = (0, registry_auth_token_1.default)(parsed.href, { recursive: true, ...(npmrc ? { npmrc } : {}) });
56
+ // registry-auth-token stringifies an unset ${ENV_VAR} reference into the
57
+ // literal "undefined". Sending `Bearer undefined` guarantees a 401 where an
58
+ // anonymous request might succeed, so treat it as no credentials.
59
+ if (auth?.token && auth.token !== 'undefined') {
60
+ authHeader = `${auth.type} ${auth.token}`;
61
+ }
62
+ }
63
+ catch {
64
+ // No credentials (or unreadable config) → anonymous requests, matching npm.
65
+ }
66
+ return {
67
+ origin: parsed.origin,
68
+ pathPrefix: parsed.pathname.replace(/\/$/, ''),
69
+ authHeader,
70
+ };
71
+ }
72
+ /** Test helper: forget memoized resolutions so npmrc overrides take effect. */
73
+ function clearRegistryTargetCache() {
74
+ targetByScope.clear();
75
+ }
76
+ //# sourceMappingURL=registry-config.js.map
@@ -35,21 +35,32 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.checkForUpdate = checkForUpdate;
37
37
  exports.checkForUpdateAsync = checkForUpdateAsync;
38
- const child_process_1 = require("child_process");
39
38
  const semver = __importStar(require("semver"));
40
39
  const config_1 = require("../config");
40
+ /** Cap for the self-update check; it must never hold up an interactive session. */
41
+ const UPDATE_CHECK_TIMEOUT_MS = 5000;
41
42
  /**
42
- * Check if the current package version is outdated compared to npm registry
43
+ * Check if the current package version is outdated compared to npm registry.
44
+ *
45
+ * One small registry request (`{registry}/{name}/latest`) instead of spawning
46
+ * the npm CLI — `npm view` paid a few hundred ms of process startup for the
47
+ * same answer.
43
48
  */
44
49
  async function checkForUpdate(packageName, currentVersion) {
45
50
  try {
46
- // Use npm view to get the latest version from registry
47
- const result = (0, child_process_1.execSync)(`npm view ${packageName} version`, {
48
- encoding: 'utf-8',
49
- timeout: config_1.REQUEST_TIMEOUT,
50
- stdio: ['pipe', 'pipe', 'pipe'],
51
+ const response = await fetch(`${config_1.NPM_REGISTRY_URL}/${encodeURIComponent(packageName)}/latest`, {
52
+ method: 'GET',
53
+ headers: { accept: 'application/json' },
54
+ signal: AbortSignal.timeout(UPDATE_CHECK_TIMEOUT_MS),
51
55
  });
52
- const latestVersion = result.trim();
56
+ if (!response.ok) {
57
+ return null;
58
+ }
59
+ const manifest = (await response.json());
60
+ const latestVersion = typeof manifest.version === 'string' ? manifest.version.trim() : '';
61
+ if (!semver.valid(latestVersion)) {
62
+ return null;
63
+ }
53
64
  // Compare versions
54
65
  const isOutdated = semver.lt(currentVersion, latestVersion);
55
66
  // Determine update command based on how the tool was likely invoked
@@ -63,30 +74,18 @@ async function checkForUpdate(packageName, currentVersion) {
63
74
  updateCommand,
64
75
  };
65
76
  }
66
- catch (error) {
77
+ catch {
67
78
  // Silently fail - don't interrupt the user experience
68
79
  return null;
69
80
  }
70
81
  }
71
82
  /**
72
- * Check for updates in the background without blocking
73
- * Resolves immediately, result available via promise
83
+ * Check for updates in the background without blocking.
84
+ *
85
+ * `checkForUpdate` self-caps at UPDATE_CHECK_TIMEOUT_MS and never rejects
86
+ * (every failure resolves to null), so no extra timeout race is needed here.
74
87
  */
75
88
  function checkForUpdateAsync(packageName, currentVersion) {
76
- return new Promise((resolve) => {
77
- // Set a timeout to prevent hanging
78
- const timeout = setTimeout(() => {
79
- resolve(null);
80
- }, 5000);
81
- checkForUpdate(packageName, currentVersion)
82
- .then((result) => {
83
- clearTimeout(timeout);
84
- resolve(result);
85
- })
86
- .catch(() => {
87
- clearTimeout(timeout);
88
- resolve(null);
89
- });
90
- });
89
+ return checkForUpdate(packageName, currentVersion);
91
90
  }
92
91
  //# sourceMappingURL=version-checker.js.map
@@ -82,7 +82,7 @@ exports.ConsoleUtils = {
82
82
  clearProgress() {
83
83
  if (!process.stderr.isTTY)
84
84
  return;
85
- process.stderr.write('\r' + ' '.repeat(exports.ConsoleUtils.LINE_WIDTH) + '\r');
85
+ process.stderr.write(`\r${' '.repeat(exports.ConsoleUtils.LINE_WIDTH)}\r`);
86
86
  },
87
87
  };
88
88
  //# sourceMappingURL=cursor.js.map
@@ -1,14 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.wrapPlainText = exports.truncatePlainText = exports.stripAnsi = exports.getVisualLength = exports.TerminalInput = exports.RAW_SHOW_CURSOR = exports.RAW_EXIT_ALT_SCREEN = exports.ConsoleUtils = exports.CursorUtils = exports.formatVersionDiff = exports.truncateMiddle = exports.applyVersionPrefix = exports.VersionUtils = void 0;
4
- var version_1 = require("./version");
5
- Object.defineProperty(exports, "VersionUtils", { enumerable: true, get: function () { return version_1.VersionUtils; } });
6
- Object.defineProperty(exports, "applyVersionPrefix", { enumerable: true, get: function () { return version_1.applyVersionPrefix; } });
7
- Object.defineProperty(exports, "truncateMiddle", { enumerable: true, get: function () { return version_1.truncateMiddle; } });
8
- Object.defineProperty(exports, "formatVersionDiff", { enumerable: true, get: function () { return version_1.formatVersionDiff; } });
3
+ exports.wrapPlainText = exports.truncatePlainText = exports.stripAnsi = exports.getVisualLength = exports.TerminalInput = exports.RAW_SHOW_CURSOR = exports.RAW_EXIT_ALT_SCREEN = exports.CursorUtils = exports.ConsoleUtils = exports.shouldDisableColor = exports.applyColorSetting = void 0;
4
+ var color_1 = require("./color");
5
+ Object.defineProperty(exports, "applyColorSetting", { enumerable: true, get: function () { return color_1.applyColorSetting; } });
6
+ Object.defineProperty(exports, "shouldDisableColor", { enumerable: true, get: function () { return color_1.shouldDisableColor; } });
9
7
  var cursor_1 = require("./cursor");
10
- Object.defineProperty(exports, "CursorUtils", { enumerable: true, get: function () { return cursor_1.CursorUtils; } });
11
8
  Object.defineProperty(exports, "ConsoleUtils", { enumerable: true, get: function () { return cursor_1.ConsoleUtils; } });
9
+ Object.defineProperty(exports, "CursorUtils", { enumerable: true, get: function () { return cursor_1.CursorUtils; } });
12
10
  Object.defineProperty(exports, "RAW_EXIT_ALT_SCREEN", { enumerable: true, get: function () { return cursor_1.RAW_EXIT_ALT_SCREEN; } });
13
11
  Object.defineProperty(exports, "RAW_SHOW_CURSOR", { enumerable: true, get: function () { return cursor_1.RAW_SHOW_CURSOR; } });
14
12
  var terminal_input_1 = require("./terminal-input");
@@ -85,7 +85,12 @@ exports.TerminalInput = {
85
85
  promptForImmediateConfirmation(prompt, defaultValue = true) {
86
86
  return new Promise((resolve) => {
87
87
  process.stdout.write(prompt);
88
+ // Replaced synchronously below before any keypress can call finish(),
89
+ // so the placeholder body is unreachable. (The `v8 ignore next` form
90
+ // is not honored by coverage-v8 here; the block form is.)
91
+ /* v8 ignore start */
88
92
  let cleanup = () => { };
93
+ /* v8 ignore stop */
89
94
  const finish = (value) => {
90
95
  cleanup();
91
96
  process.stdout.write('\n');