inup 1.4.6 → 1.4.7

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.
package/dist/cli.js CHANGED
@@ -11,6 +11,7 @@ const path_1 = require("path");
11
11
  const index_1 = require("./index");
12
12
  const services_1 = require("./services");
13
13
  const config_1 = require("./config");
14
+ const utils_1 = require("./utils");
14
15
  const packageJson = JSON.parse((0, fs_1.readFileSync)((0, path_1.join)(__dirname, '../package.json'), 'utf-8'));
15
16
  const program = new commander_1.Command();
16
17
  program
@@ -21,11 +22,15 @@ program
21
22
  .option('-e, --exclude <patterns>', 'exclude paths matching regex patterns (comma-separated)', '')
22
23
  .option('-i, --ignore <packages>', 'ignore packages (comma-separated, supports glob patterns like @babel/*)')
23
24
  .option('--package-manager <name>', 'manually specify package manager (npm, yarn, pnpm, bun)')
25
+ .option('--debug', 'write verbose debug log to /tmp/inup-debug-YYYY-MM-DD.log')
24
26
  .action(async (options) => {
25
27
  console.log(chalk_1.default.bold.blue(`🚀 `) + chalk_1.default.bold.red(`i`) + chalk_1.default.bold.yellow(`n`) + chalk_1.default.bold.blue(`u`) + chalk_1.default.bold.magenta(`p`) + `\n`);
26
28
  // Check for updates in the background (non-blocking)
27
29
  const updateCheckPromise = (0, services_1.checkForUpdateAsync)('inup', packageJson.version);
28
30
  const cwd = (0, path_1.resolve)(options.dir);
31
+ if (options.debug || process.env.INUP_DEBUG === '1') {
32
+ (0, utils_1.enableDebugLogging)();
33
+ }
29
34
  // Load project config from .inuprc
30
35
  const projectConfig = (0, config_1.loadProjectConfig)(cwd);
31
36
  // Merge CLI exclude patterns with config
@@ -60,6 +65,7 @@ program
60
65
  excludePatterns,
61
66
  ignorePackages,
62
67
  packageManager,
68
+ debug: options.debug || process.env.INUP_DEBUG === '1',
63
69
  });
64
70
  await upgrader.run();
65
71
  // After the main flow completes, check if there's an update available
@@ -1,11 +1,14 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DEFAULT_REGISTRY = exports.REQUEST_TIMEOUT = exports.CACHE_TTL = exports.MAX_CONCURRENT_REQUESTS = exports.JSDELIVR_CDN_URL = exports.NPM_REGISTRY_URL = exports.PACKAGE_NAME = void 0;
3
+ exports.DEFAULT_REGISTRY = 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
4
  exports.PACKAGE_NAME = 'inup';
5
5
  exports.NPM_REGISTRY_URL = 'https://registry.npmjs.org';
6
6
  exports.JSDELIVR_CDN_URL = 'https://cdn.jsdelivr.net/npm';
7
7
  exports.MAX_CONCURRENT_REQUESTS = 150;
8
8
  exports.CACHE_TTL = 5 * 60 * 1000; // 5 minutes in milliseconds
9
9
  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
10
13
  exports.DEFAULT_REGISTRY = 'jsdelivr';
11
14
  //# sourceMappingURL=constants.js.map
@@ -39,6 +39,7 @@ const utils_1 = require("../utils");
39
39
  const services_1 = require("../services");
40
40
  const config_1 = require("../config");
41
41
  const utils_2 = require("../ui/utils");
42
+ const utils_3 = require("../utils");
42
43
  class PackageDetector {
43
44
  constructor(options) {
44
45
  this.packageJsonPath = null;
@@ -59,27 +60,46 @@ class PackageDetector {
59
60
  throw new Error('No package.json found in current directory');
60
61
  }
61
62
  const packages = [];
63
+ const t0 = Date.now();
64
+ utils_3.debugLog.info('PackageDetector', `Starting scan in ${this.cwd}`);
62
65
  // Always check all package.json files recursively with timeout protection
63
66
  this.showProgress('🔍 Scanning repository for package.json files...');
67
+ const tScan = Date.now();
64
68
  const allPackageJsonFiles = this.findPackageJsonFilesWithTimeout(30000); // 30 second timeout
69
+ utils_3.debugLog.perf('PackageDetector', `file scan (${allPackageJsonFiles.length} files)`, tScan, {
70
+ files: allPackageJsonFiles,
71
+ });
65
72
  this.showProgress(`🔍 Found ${allPackageJsonFiles.length} package.json file${allPackageJsonFiles.length === 1 ? '' : 's'}`);
66
73
  // Step 2: Collect all dependencies from package.json files (parallelized)
67
74
  this.showProgress('🔍 Reading dependencies from package.json files...');
75
+ const tDeps = Date.now();
68
76
  const allDepsRaw = await (0, utils_1.collectAllDependenciesAsync)(allPackageJsonFiles, {
69
77
  includePeerDeps: true,
70
78
  includeOptionalDeps: true,
71
79
  });
80
+ utils_3.debugLog.perf('PackageDetector', `dependency collection (${allDepsRaw.length} raw deps)`, tDeps);
72
81
  // Step 3: Get unique package names while filtering out workspace references and ignored packages
73
82
  this.showProgress('🔍 Identifying unique packages...');
74
83
  const uniquePackageNames = new Set();
75
84
  const allDeps = [];
76
85
  let ignoredCount = 0;
86
+ const seenWorkspaceRefs = new Set();
87
+ const seenIgnored = new Set();
77
88
  for (const dep of allDepsRaw) {
78
89
  if (this.isWorkspaceReference(dep.version)) {
90
+ const key = `${dep.name}@${dep.version}`;
91
+ if (!seenWorkspaceRefs.has(key)) {
92
+ seenWorkspaceRefs.add(key);
93
+ utils_3.debugLog.info('PackageDetector', `skipping workspace ref: ${key}`);
94
+ }
79
95
  continue;
80
96
  }
81
97
  if (this.ignorePackages.length > 0 && (0, config_1.isPackageIgnored)(dep.name, this.ignorePackages)) {
82
98
  ignoredCount++;
99
+ if (!seenIgnored.has(dep.name)) {
100
+ seenIgnored.add(dep.name);
101
+ utils_3.debugLog.info('PackageDetector', `ignoring package: ${dep.name}`);
102
+ }
83
103
  continue;
84
104
  }
85
105
  allDeps.push(dep);
@@ -89,6 +109,7 @@ class PackageDetector {
89
109
  this.showProgress(`🔍 Skipped ${ignoredCount} ignored package(s)`);
90
110
  }
91
111
  const packageNames = Array.from(uniquePackageNames);
112
+ utils_3.debugLog.info('PackageDetector', `${packageNames.length} unique packages to check, ${ignoredCount} ignored`);
92
113
  // Step 4: Fetch all package data in one call per package
93
114
  // Create a map of package names to their current versions for major version optimization
94
115
  const currentVersions = new Map();
@@ -98,6 +119,8 @@ class PackageDetector {
98
119
  currentVersions.set(dep.name, dep.version);
99
120
  }
100
121
  }
122
+ const tFetch = Date.now();
123
+ utils_3.debugLog.info('PackageDetector', `fetching version data via ${config_1.DEFAULT_REGISTRY}`);
101
124
  const allPackageData = config_1.DEFAULT_REGISTRY === 'jsdelivr'
102
125
  ? await (0, services_1.getAllPackageDataFromJsdelivr)(packageNames, currentVersions, (_currentPackage, completed, total) => {
103
126
  this.showProgress(`🌐 Checking versions... (${completed}/${total} packages)`);
@@ -105,12 +128,20 @@ class PackageDetector {
105
128
  : await (0, services_1.getAllPackageData)(packageNames, (_currentPackage, completed, total) => {
106
129
  this.showProgress(`🌐 Checking versions... (${completed}/${total} packages)`);
107
130
  });
131
+ utils_3.debugLog.perf('PackageDetector', `registry fetch (${allPackageData.size}/${packageNames.length} resolved)`, tFetch);
132
+ const loggedOutdated = new Set();
133
+ const loggedNoData = new Set();
108
134
  try {
109
135
  for (const dep of allDeps) {
110
136
  try {
111
137
  const packageData = allPackageData.get(dep.name);
112
- if (!packageData)
138
+ if (!packageData) {
139
+ if (!loggedNoData.has(dep.name)) {
140
+ loggedNoData.add(dep.name);
141
+ utils_3.debugLog.warn('PackageDetector', `no data returned for ${dep.name} — skipping`);
142
+ }
113
143
  continue;
144
+ }
114
145
  const { latestVersion, allVersions } = packageData;
115
146
  // Find closest minor version (same major, higher minor) that satisfies the current range
116
147
  // Falls back to patch updates if no minor updates are available
@@ -123,6 +154,13 @@ class PackageDetector {
123
154
  const hasRangeUpdate = minorClean !== null && minorClean !== installedClean;
124
155
  const hasMajorUpdate = semver.major(latestClean) > semver.major(installedClean);
125
156
  const isOutdated = hasRangeUpdate || hasMajorUpdate;
157
+ if (isOutdated) {
158
+ const outdatedKey = `${dep.name}@${dep.version}`;
159
+ if (!loggedOutdated.has(outdatedKey)) {
160
+ loggedOutdated.add(outdatedKey);
161
+ utils_3.debugLog.info('PackageDetector', `outdated: ${dep.name} ${dep.version} → range:${closestMinorVersion ?? '-'} latest:${latestVersion}`);
162
+ }
163
+ }
126
164
  packages.push({
127
165
  name: dep.name,
128
166
  currentVersion: dep.version, // Keep original version specifier with prefix
@@ -136,6 +174,7 @@ class PackageDetector {
136
174
  });
137
175
  }
138
176
  catch (error) {
177
+ utils_3.debugLog.error('PackageDetector', `error processing ${dep.name}`, error);
139
178
  // Skip packages that can't be checked (private packages, etc.)
140
179
  packages.push({
141
180
  name: dep.name,
@@ -150,10 +189,13 @@ class PackageDetector {
150
189
  });
151
190
  }
152
191
  }
192
+ const outdatedCount = packages.filter((p) => p.isOutdated).length;
193
+ utils_3.debugLog.perf('PackageDetector', `total scan complete (${outdatedCount} outdated of ${packages.length} deps)`, t0);
153
194
  return packages;
154
195
  }
155
196
  catch (error) {
156
197
  this.showProgress('❌ Failed to check packages\n');
198
+ utils_3.debugLog.error('PackageDetector', 'fatal error during package check', error);
157
199
  throw error;
158
200
  }
159
201
  }
@@ -42,50 +42,250 @@ const config_1 = require("../config");
42
42
  const npm_registry_1 = require("./npm-registry");
43
43
  const cache_manager_1 = require("./cache-manager");
44
44
  const utils_1 = require("../ui/utils");
45
+ const utils_2 = require("../utils");
46
+ // Batch configuration for progressive loading
47
+ const BATCH_SIZE = 5;
48
+ const BATCH_TIMEOUT_MS = 500;
49
+ const DEFAULT_JSDELIVR_RETRY_TIMEOUT_MS = 2000;
50
+ const DEFAULT_JSDELIVR_POOL_TIMEOUT_MS = 60000;
51
+ const MIN_JSDELIVR_CONNECT_TIMEOUT_MS = 500;
52
+ const toPositiveInteger = (value) => {
53
+ if (!Number.isFinite(value) || value <= 0) {
54
+ return null;
55
+ }
56
+ const normalized = Math.floor(value);
57
+ return normalized > 0 ? normalized : null;
58
+ };
59
+ const RETRY_TIMEOUTS = (() => {
60
+ const configured = Array.from(new Set(config_1.JSDELIVR_RETRY_TIMEOUTS.map(toPositiveInteger).filter((value) => value !== null))).sort((a, b) => a - b);
61
+ return configured.length > 0 ? configured : [DEFAULT_JSDELIVR_RETRY_TIMEOUT_MS];
62
+ })();
63
+ const RETRY_DELAYS = config_1.JSDELIVR_RETRY_DELAYS.map(toPositiveInteger).filter((value) => value !== null);
64
+ const MAX_RETRY_AFTER_DELAY_MS = RETRY_TIMEOUTS[RETRY_TIMEOUTS.length - 1];
65
+ const RETRY_AFTER_HEADER = 'retry-after';
66
+ const parseRetryAfterMs = (value) => {
67
+ const trimmed = value.trim();
68
+ if (!trimmed) {
69
+ return null;
70
+ }
71
+ const seconds = Number(trimmed);
72
+ if (Number.isFinite(seconds)) {
73
+ if (seconds <= 0) {
74
+ return null;
75
+ }
76
+ const delayMs = Math.floor(seconds * 1000);
77
+ return delayMs > 0 ? delayMs : null;
78
+ }
79
+ const dateMs = Date.parse(trimmed);
80
+ if (Number.isNaN(dateMs)) {
81
+ return null;
82
+ }
83
+ const delayMs = dateMs - Date.now();
84
+ return delayMs > 0 ? delayMs : null;
85
+ };
86
+ const getHeaderValue = (headers, name) => {
87
+ if (!headers) {
88
+ return null;
89
+ }
90
+ const direct = headers[name];
91
+ if (typeof direct === 'string') {
92
+ return direct;
93
+ }
94
+ if (Array.isArray(direct)) {
95
+ return direct.find((value) => typeof value === 'string') ?? null;
96
+ }
97
+ const headerEntry = Object.entries(headers).find(([headerName]) => headerName.toLowerCase() === name);
98
+ if (!headerEntry) {
99
+ return null;
100
+ }
101
+ const [, rawValue] = headerEntry;
102
+ if (typeof rawValue === 'string') {
103
+ return rawValue;
104
+ }
105
+ if (Array.isArray(rawValue)) {
106
+ return rawValue.find((value) => typeof value === 'string') ?? null;
107
+ }
108
+ return null;
109
+ };
110
+ const getRetryAfterDelay = (headers) => {
111
+ const retryAfterValue = getHeaderValue(headers, RETRY_AFTER_HEADER);
112
+ if (!retryAfterValue) {
113
+ return null;
114
+ }
115
+ const parsedDelay = parseRetryAfterMs(retryAfterValue);
116
+ if (parsedDelay === null) {
117
+ return null;
118
+ }
119
+ return Math.min(parsedDelay, MAX_RETRY_AFTER_DELAY_MS);
120
+ };
121
+ const getRetryDelay = (attempt, headers) => {
122
+ const configuredDelay = RETRY_DELAYS.length === 0 ? 0 : RETRY_DELAYS[Math.min(attempt, RETRY_DELAYS.length - 1)];
123
+ const retryAfterDelay = getRetryAfterDelay(headers);
124
+ return retryAfterDelay === null ? configuredDelay : Math.max(configuredDelay, retryAfterDelay);
125
+ };
126
+ // Keep connection setup bounded by retry budget so fallback stays responsive.
127
+ const JSDELIVR_CONNECT_TIMEOUT_MS = Math.max(RETRY_TIMEOUTS[0], MIN_JSDELIVR_CONNECT_TIMEOUT_MS);
128
+ const JSDELIVR_POOL_TIMEOUT_MS = toPositiveInteger(config_1.JSDELIVR_POOL_TIMEOUT) ?? DEFAULT_JSDELIVR_POOL_TIMEOUT_MS;
129
+ const JSDELIVR_CONNECTIONS = toPositiveInteger(config_1.MAX_CONCURRENT_REQUESTS) ?? 1;
45
130
  // Create a persistent connection pool for jsDelivr CDN with optimal settings
46
131
  // This enables connection reuse and HTTP/1.1 keep-alive for blazing fast requests
47
132
  const jsdelivrPool = new undici_1.Pool('https://cdn.jsdelivr.net', {
48
- connections: config_1.MAX_CONCURRENT_REQUESTS, // Maximum concurrent connections
49
- pipelining: 10, // Enable request pipelining for even better performance
50
- keepAliveTimeout: config_1.REQUEST_TIMEOUT, // Keep connections alive for 60 seconds
51
- keepAliveMaxTimeout: config_1.REQUEST_TIMEOUT, // Maximum keep-alive timeout
52
- connectTimeout: config_1.REQUEST_TIMEOUT, // 60 seconds connect timeout
133
+ connections: JSDELIVR_CONNECTIONS,
134
+ pipelining: 10,
135
+ keepAliveTimeout: JSDELIVR_POOL_TIMEOUT_MS,
136
+ keepAliveMaxTimeout: JSDELIVR_POOL_TIMEOUT_MS,
137
+ connectTimeout: JSDELIVR_CONNECT_TIMEOUT_MS,
53
138
  });
54
- // Batch configuration for progressive loading
55
- const BATCH_SIZE = 5;
56
- const BATCH_TIMEOUT_MS = 500;
139
+ const isTimeoutError = (error) => {
140
+ if (!(error instanceof Error)) {
141
+ return false;
142
+ }
143
+ const maybeCode = error.code;
144
+ const message = error.message.toLowerCase();
145
+ return (maybeCode === 'UND_ERR_HEADERS_TIMEOUT' ||
146
+ maybeCode === 'UND_ERR_BODY_TIMEOUT' ||
147
+ maybeCode === 'UND_ERR_CONNECT_TIMEOUT' ||
148
+ error.name === 'HeadersTimeoutError' ||
149
+ error.name === 'BodyTimeoutError' ||
150
+ error.name === 'ConnectTimeoutError' ||
151
+ message.includes('timeout'));
152
+ };
153
+ const isTransientNetworkError = (error) => {
154
+ if (!(error instanceof Error)) {
155
+ return false;
156
+ }
157
+ const maybeCode = error.code;
158
+ return (maybeCode === 'UND_ERR_SOCKET' ||
159
+ maybeCode === 'ENOTFOUND' ||
160
+ maybeCode === 'EAI_AGAIN' ||
161
+ maybeCode === 'ECONNRESET' ||
162
+ maybeCode === 'ECONNREFUSED' ||
163
+ maybeCode === 'ETIMEDOUT' ||
164
+ maybeCode === 'EPIPE');
165
+ };
166
+ const isRetryableStatus = (statusCode) => statusCode === 408 || statusCode === 429 || statusCode >= 500;
167
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
168
+ const consumeBodySafely = async (body) => {
169
+ try {
170
+ await body.text();
171
+ }
172
+ catch {
173
+ // Ignore body read errors on non-200 responses because request will be retried/fallback.
174
+ }
175
+ };
176
+ const extractMajorVersion = (version) => {
177
+ if (!version) {
178
+ return null;
179
+ }
180
+ const coerced = semver.coerce(version);
181
+ if (!coerced) {
182
+ return null;
183
+ }
184
+ return semver.major(coerced).toString();
185
+ };
186
+ const toComparableVersion = (version) => {
187
+ const validVersion = semver.valid(version);
188
+ if (validVersion) {
189
+ return validVersion;
190
+ }
191
+ const coerced = semver.coerce(version);
192
+ return coerced ? coerced.version : null;
193
+ };
194
+ const versionIdentity = (version) => {
195
+ const comparable = toComparableVersion(version);
196
+ return comparable ?? `raw:${version}`;
197
+ };
198
+ const sortVersionsDescending = (versions) => {
199
+ const uniqueVersions = [];
200
+ const seenVersions = new Set();
201
+ for (const version of versions) {
202
+ const identity = versionIdentity(version);
203
+ if (!seenVersions.has(identity)) {
204
+ seenVersions.add(identity);
205
+ uniqueVersions.push(version);
206
+ }
207
+ }
208
+ return uniqueVersions.sort((a, b) => {
209
+ const comparableA = toComparableVersion(a);
210
+ const comparableB = toComparableVersion(b);
211
+ if (comparableA && comparableB) {
212
+ return semver.rcompare(comparableA, comparableB);
213
+ }
214
+ if (comparableA) {
215
+ return -1;
216
+ }
217
+ if (comparableB) {
218
+ return 1;
219
+ }
220
+ return b.localeCompare(a);
221
+ });
222
+ };
223
+ const isExpectedTransientError = (error) => isTimeoutError(error) || isTransientNetworkError(error);
57
224
  /**
58
225
  * Fetches package.json from jsdelivr CDN for a specific version tag using undici pool.
59
226
  * Uses connection pooling and keep-alive for maximum performance.
227
+ * Retries on transient failures while keeping a short fallback budget.
60
228
  * @param packageName - The npm package name
61
229
  * @param versionTag - The version tag (e.g., '14', 'latest')
62
230
  * @returns The package.json content or null if not found
63
231
  */
64
232
  async function fetchPackageJsonFromJsdelivr(packageName, versionTag) {
65
- try {
66
- const url = `${config_1.JSDELIVR_CDN_URL}/${encodeURIComponent(packageName)}@${versionTag}/package.json`;
67
- const { statusCode, body } = await (0, undici_1.request)(url, {
68
- dispatcher: jsdelivrPool,
69
- method: 'GET',
70
- headers: {
71
- accept: 'application/json',
72
- },
73
- headersTimeout: config_1.REQUEST_TIMEOUT,
74
- bodyTimeout: config_1.REQUEST_TIMEOUT,
75
- });
76
- if (statusCode !== 200) {
77
- // Consume body to prevent memory leaks
78
- await body.text();
233
+ const url = `${config_1.JSDELIVR_CDN_URL}/${encodeURIComponent(packageName)}@${versionTag}/package.json`;
234
+ for (let attempt = 0; attempt < RETRY_TIMEOUTS.length; attempt++) {
235
+ const timeout = RETRY_TIMEOUTS[attempt];
236
+ const tReq = Date.now();
237
+ try {
238
+ const { statusCode, headers, body } = await (0, undici_1.request)(url, {
239
+ dispatcher: jsdelivrPool,
240
+ method: 'GET',
241
+ headers: {
242
+ accept: 'application/json',
243
+ },
244
+ headersTimeout: timeout,
245
+ bodyTimeout: timeout,
246
+ });
247
+ if (statusCode !== 200) {
248
+ // Consume body to prevent memory leaks
249
+ await consumeBodySafely(body);
250
+ if (isRetryableStatus(statusCode) && attempt < RETRY_TIMEOUTS.length - 1) {
251
+ const delay = getRetryDelay(attempt, headers);
252
+ utils_2.debugLog.warn('jsdelivr', `${packageName}@${versionTag} HTTP ${statusCode}, retry ${attempt + 1} in ${delay}ms`);
253
+ if (delay > 0) {
254
+ await sleep(delay);
255
+ }
256
+ continue;
257
+ }
258
+ utils_2.debugLog.warn('jsdelivr', `${packageName}@${versionTag} HTTP ${statusCode}, no more retries`);
259
+ return null;
260
+ }
261
+ const text = await body.text();
262
+ const data = JSON.parse(text);
263
+ const version = typeof data.version === 'string' ? data.version.trim() : '';
264
+ utils_2.debugLog.perf('jsdelivr', `fetch ${packageName}@${versionTag} → ${version || 'no version'}`, tReq);
265
+ return version ? { version } : null;
266
+ }
267
+ catch (error) {
268
+ if ((isTimeoutError(error) || isTransientNetworkError(error)) &&
269
+ attempt < RETRY_TIMEOUTS.length - 1) {
270
+ const delay = getRetryDelay(attempt);
271
+ utils_2.debugLog.warn('jsdelivr', `${packageName}@${versionTag} transient error on attempt ${attempt + 1}, retry in ${delay}ms`, error);
272
+ if (delay > 0) {
273
+ await sleep(delay);
274
+ }
275
+ continue;
276
+ }
277
+ if (!isExpectedTransientError(error)) {
278
+ // Unexpected errors are logged for observability.
279
+ console.error(`jsDelivr fetch failed for ${packageName}@${versionTag} on attempt ${attempt + 1}/${RETRY_TIMEOUTS.length}`, error);
280
+ utils_2.debugLog.error('jsdelivr', `unexpected error for ${packageName}@${versionTag} attempt ${attempt + 1}`, error);
281
+ }
282
+ else {
283
+ utils_2.debugLog.warn('jsdelivr', `${packageName}@${versionTag} exhausted retries`, error);
284
+ }
79
285
  return null;
80
286
  }
81
- const text = await body.text();
82
- const data = JSON.parse(text);
83
- return data.version ? { version: data.version } : null;
84
- }
85
- catch (error) {
86
- console.error(`Error fetching from jsdelivr for package: ${packageName}@${versionTag}`, error);
87
- return null;
88
287
  }
288
+ return null;
89
289
  }
90
290
  /**
91
291
  * Fetches package version data from jsdelivr CDN for multiple packages.
@@ -105,14 +305,41 @@ async function getAllPackageDataFromJsdelivr(packageNames, currentVersions, onPr
105
305
  }
106
306
  const total = packageNames.length;
107
307
  let completedCount = 0;
308
+ let progressCallback = onProgress;
309
+ let batchReadyCallback = onBatchReady;
108
310
  // Batch buffer for progressive updates
109
311
  let batchBuffer = [];
110
312
  let batchTimer = null;
313
+ const emitProgress = (packageName, completed, packageTotal) => {
314
+ if (!progressCallback) {
315
+ return;
316
+ }
317
+ try {
318
+ progressCallback(packageName, completed, packageTotal);
319
+ }
320
+ catch (error) {
321
+ console.error('Progress callback failed, disabling progress updates for this run.', error);
322
+ progressCallback = undefined;
323
+ }
324
+ };
325
+ const emitBatch = (batch) => {
326
+ if (!batchReadyCallback) {
327
+ return;
328
+ }
329
+ try {
330
+ batchReadyCallback(batch);
331
+ }
332
+ catch (error) {
333
+ console.error('Batch callback failed, disabling batch updates for this run.', error);
334
+ batchReadyCallback = undefined;
335
+ }
336
+ };
111
337
  // Helper to flush the current batch
112
338
  const flushBatch = () => {
113
- if (batchBuffer.length > 0 && onBatchReady) {
114
- onBatchReady([...batchBuffer]);
339
+ if (batchBuffer.length > 0) {
340
+ const batch = [...batchBuffer];
115
341
  batchBuffer = [];
342
+ emitBatch(batch);
116
343
  }
117
344
  if (batchTimer) {
118
345
  clearTimeout(batchTimer);
@@ -121,113 +348,119 @@ async function getAllPackageDataFromJsdelivr(packageNames, currentVersions, onPr
121
348
  };
122
349
  // Helper to add package to batch and flush if needed
123
350
  const addToBatch = (packageName, data) => {
124
- if (onBatchReady) {
125
- batchBuffer.push({ name: packageName, data });
126
- // Flush if batch is full
127
- if (batchBuffer.length >= BATCH_SIZE) {
128
- flushBatch();
129
- }
130
- else if (!batchTimer) {
131
- // Set timer to flush batch after timeout
132
- batchTimer = setTimeout(flushBatch, BATCH_TIMEOUT_MS);
133
- }
351
+ if (!batchReadyCallback) {
352
+ return;
353
+ }
354
+ batchBuffer.push({ name: packageName, data });
355
+ // Flush if batch is full
356
+ if (batchBuffer.length >= BATCH_SIZE) {
357
+ flushBatch();
358
+ }
359
+ else if (!batchTimer) {
360
+ // Set timer to flush batch after timeout
361
+ batchTimer = setTimeout(flushBatch, BATCH_TIMEOUT_MS);
134
362
  }
135
363
  };
136
364
  // Process individual package fetch with immediate npm fallback on failure
137
- const fetchPackageWithFallback = async (packageName) => {
138
- const currentVersion = currentVersions?.get(packageName);
139
- // Use CacheManager for unified caching (memory + disk)
140
- const cached = cache_manager_1.packageCache.get(packageName);
141
- if (cached) {
142
- packageData.set(packageName, cached);
143
- completedCount++;
144
- if (onProgress) {
145
- onProgress(packageName, completedCount, total);
365
+ const inFlightLookups = new Map();
366
+ const fetchFromNpmFallback = async (packageName) => {
367
+ const tFallback = Date.now();
368
+ utils_2.debugLog.info('jsdelivr', `falling back to npm registry for ${packageName}`);
369
+ try {
370
+ const npmData = await (0, npm_registry_1.getAllPackageData)([packageName]);
371
+ const result = npmData.get(packageName) ?? null;
372
+ if (result) {
373
+ cache_manager_1.packageCache.set(packageName, result);
374
+ utils_2.debugLog.perf('jsdelivr', `npm fallback resolved ${packageName} → ${result.latestVersion}`, tFallback);
146
375
  }
147
- addToBatch(packageName, cached);
148
- return;
376
+ else {
377
+ utils_2.debugLog.warn('jsdelivr', `npm fallback returned no data for ${packageName}`);
378
+ }
379
+ return result;
149
380
  }
381
+ catch (error) {
382
+ utils_2.debugLog.error('jsdelivr', `npm fallback failed for ${packageName}`, error);
383
+ return null;
384
+ }
385
+ };
386
+ const fetchFreshPackageData = async (packageName, currentVersion) => {
150
387
  try {
151
- // Determine major version from current version if provided
152
- const majorVersion = currentVersion
153
- ? semver.major(semver.coerce(currentVersion) || '0.0.0').toString()
154
- : null;
155
- // Prepare requests: always fetch @latest, @major if we have a current version
156
- const requests = [
157
- fetchPackageJsonFromJsdelivr(packageName, 'latest'),
158
- ];
159
- if (majorVersion) {
160
- requests.push(fetchPackageJsonFromJsdelivr(packageName, majorVersion));
161
- }
162
- // Execute all requests simultaneously
163
- const results = await Promise.all(requests);
164
- const latestResult = results[0];
165
- const majorResult = results[1];
388
+ const majorVersion = extractMajorVersion(currentVersion);
389
+ const latestResult = await fetchPackageJsonFromJsdelivr(packageName, 'latest');
166
390
  if (!latestResult) {
167
- // Package not on jsDelivr, immediately try npm fallback
168
- const npmData = await (0, npm_registry_1.getAllPackageData)([packageName]);
169
- const result = npmData.get(packageName);
170
- if (result) {
171
- packageData.set(packageName, result);
172
- // CacheManager handles both memory and disk caching
173
- cache_manager_1.packageCache.set(packageName, result);
174
- addToBatch(packageName, result);
175
- }
176
- completedCount++;
177
- if (onProgress) {
178
- onProgress(packageName, completedCount, total);
179
- }
180
- return;
391
+ return await fetchFromNpmFallback(packageName);
181
392
  }
182
393
  const latestVersion = latestResult.version;
394
+ const latestMajorVersion = extractMajorVersion(latestVersion);
395
+ const shouldFetchMajorVersion = Boolean(majorVersion && (latestMajorVersion === null || majorVersion !== latestMajorVersion));
396
+ const majorResult = shouldFetchMajorVersion
397
+ ? await fetchPackageJsonFromJsdelivr(packageName, majorVersion)
398
+ : null;
183
399
  const allVersions = [latestVersion];
184
- // Add the major version result if different from latest
185
400
  if (majorResult && majorResult.version !== latestVersion) {
186
401
  allVersions.push(majorResult.version);
187
402
  }
403
+ const sortedVersions = sortVersionsDescending(allVersions);
404
+ const orderedVersions = sortedVersions[0] === latestVersion
405
+ ? sortedVersions
406
+ : [latestVersion, ...sortedVersions.filter((version) => version !== latestVersion)];
188
407
  const result = {
189
408
  latestVersion,
190
- allVersions: allVersions.sort(semver.rcompare),
409
+ allVersions: orderedVersions,
191
410
  };
192
- // Cache the result using CacheManager (handles both memory and disk)
193
411
  cache_manager_1.packageCache.set(packageName, result);
194
- packageData.set(packageName, result);
195
- completedCount++;
196
- if (onProgress) {
197
- onProgress(packageName, completedCount, total);
412
+ return result;
413
+ }
414
+ catch {
415
+ return await fetchFromNpmFallback(packageName);
416
+ }
417
+ };
418
+ const getPackageData = async (packageName, currentVersion) => {
419
+ const cached = cache_manager_1.packageCache.get(packageName);
420
+ if (cached) {
421
+ utils_2.debugLog.info('jsdelivr', `cache hit: ${packageName} → ${cached.latestVersion}`);
422
+ return cached;
423
+ }
424
+ const inFlight = inFlightLookups.get(packageName);
425
+ if (inFlight) {
426
+ return await inFlight;
427
+ }
428
+ const lookupPromise = fetchFreshPackageData(packageName, currentVersion).finally(() => {
429
+ inFlightLookups.delete(packageName);
430
+ });
431
+ inFlightLookups.set(packageName, lookupPromise);
432
+ return await lookupPromise;
433
+ };
434
+ const fetchPackageWithFallback = async (packageName) => {
435
+ try {
436
+ const currentVersion = currentVersions?.get(packageName);
437
+ const result = await getPackageData(packageName, currentVersion);
438
+ if (result) {
439
+ packageData.set(packageName, result);
440
+ addToBatch(packageName, result);
198
441
  }
199
- addToBatch(packageName, result);
200
442
  }
201
443
  catch (error) {
202
- // On error, immediately try npm fallback
203
- try {
204
- const npmData = await (0, npm_registry_1.getAllPackageData)([packageName]);
205
- const result = npmData.get(packageName);
206
- if (result) {
207
- packageData.set(packageName, result);
208
- // CacheManager handles both memory and disk caching
209
- cache_manager_1.packageCache.set(packageName, result);
210
- addToBatch(packageName, result);
211
- }
212
- }
213
- catch (npmError) {
214
- // If both fail, just continue
215
- }
444
+ console.error(`Failed to resolve package data for ${packageName}; continuing with others.`, error);
445
+ }
446
+ finally {
216
447
  completedCount++;
217
- if (onProgress) {
218
- onProgress(packageName, completedCount, total);
219
- }
448
+ emitProgress(packageName, completedCount, total);
220
449
  }
221
450
  };
222
- // Fire all requests simultaneously - they handle fallback internally and immediately
223
- await Promise.all(packageNames.map(fetchPackageWithFallback));
224
- // Flush any remaining batch items
225
- flushBatch();
226
- // Flush persistent cache to disk
227
- cache_manager_1.packageCache.flush();
228
- // Clear the progress line if no custom progress handler
229
- if (!onProgress) {
230
- utils_1.ConsoleUtils.clearProgress();
451
+ try {
452
+ // Fire all requests simultaneously - each request internally handles retries/fallback.
453
+ await Promise.all(packageNames.map(fetchPackageWithFallback));
454
+ }
455
+ finally {
456
+ // Flush any remaining batch items
457
+ flushBatch();
458
+ // Flush persistent cache to disk
459
+ cache_manager_1.packageCache.flush();
460
+ // Clear the progress line if no custom progress handler
461
+ if (!onProgress) {
462
+ utils_1.ConsoleUtils.clearProgress();
463
+ }
231
464
  }
232
465
  return packageData;
233
466
  }
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.debugLog = void 0;
4
+ exports.enableDebugLogging = enableDebugLogging;
5
+ exports.isDebugEnabled = isDebugEnabled;
6
+ exports.getDebugLogPath = getDebugLogPath;
7
+ const fs_1 = require("fs");
8
+ const path_1 = require("path");
9
+ let _enabled = false;
10
+ let _logFile = null;
11
+ const pad = (n, width = 2) => String(n).padStart(width, '0');
12
+ function timestamp() {
13
+ const d = new Date();
14
+ return (`${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ` +
15
+ `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.${pad(d.getMilliseconds(), 3)}`);
16
+ }
17
+ function getLogFile() {
18
+ if (!_logFile) {
19
+ const d = new Date();
20
+ const dateStr = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
21
+ _logFile = (0, path_1.join)(`inup-debug-${dateStr}.log`);
22
+ // Write a header so the file is easy to identify
23
+ (0, fs_1.writeFileSync)(_logFile, `=== inup debug log started at ${timestamp()} ===\n`, { flag: 'a' });
24
+ }
25
+ return _logFile;
26
+ }
27
+ function enableDebugLogging() {
28
+ _enabled = true;
29
+ const file = getLogFile();
30
+ // Print the path so the user knows where to look
31
+ process.stderr.write(`[inup] debug logging enabled → ${file}\n`);
32
+ }
33
+ function isDebugEnabled() {
34
+ return _enabled;
35
+ }
36
+ function getDebugLogPath() {
37
+ return _logFile;
38
+ }
39
+ function write(level, context, message, extra) {
40
+ if (!_enabled)
41
+ return;
42
+ let line = `[${timestamp()}] [${level}] [${context}] ${message}`;
43
+ if (extra !== undefined) {
44
+ if (extra instanceof Error) {
45
+ line += ` | ${extra.name}: ${extra.message}`;
46
+ if (extra.stack) {
47
+ const stackLines = extra.stack.split('\n').slice(1, 4).join(' | ');
48
+ line += ` | ${stackLines}`;
49
+ }
50
+ }
51
+ else if (typeof extra === 'object') {
52
+ try {
53
+ line += ` | ${JSON.stringify(extra)}`;
54
+ }
55
+ catch {
56
+ line += ` | [unserializable]`;
57
+ }
58
+ }
59
+ else {
60
+ line += ` | ${extra}`;
61
+ }
62
+ }
63
+ line += '\n';
64
+ try {
65
+ (0, fs_1.appendFileSync)(getLogFile(), line);
66
+ }
67
+ catch {
68
+ // Never crash the app because of debug logging
69
+ }
70
+ }
71
+ exports.debugLog = {
72
+ info: (context, message, extra) => write('INFO', context, message, extra),
73
+ warn: (context, message, extra) => write('WARN', context, message, extra),
74
+ error: (context, message, extra) => write('ERROR', context, message, extra),
75
+ /** Log elapsed time since a start timestamp obtained via Date.now() */
76
+ perf: (context, label, startMs, extra) => {
77
+ const elapsed = Date.now() - startMs;
78
+ write('PERF', context, `${label} — ${elapsed}ms`, extra);
79
+ },
80
+ };
81
+ //# sourceMappingURL=debug-logger.js.map
@@ -21,6 +21,7 @@ exports.collectAllDependenciesAsync = exports.readPackageJsonAsync = void 0;
21
21
  __exportStar(require("./filesystem"), exports);
22
22
  __exportStar(require("./exec"), exports);
23
23
  __exportStar(require("./version"), exports);
24
+ __exportStar(require("./debug-logger"), exports);
24
25
  // Re-export async functions for convenience
25
26
  var filesystem_1 = require("./filesystem");
26
27
  Object.defineProperty(exports, "readPackageJsonAsync", { enumerable: true, get: function () { return filesystem_1.readPackageJsonAsync; } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "inup",
3
- "version": "1.4.6",
3
+ "version": "1.4.7",
4
4
  "description": "Interactive CLI tool for upgrading dependencies with ease. Auto-detects and works with npm, yarn, pnpm, and bun. Inspired by yarn upgrade-interactive. Supports monorepos, workspaces, and batch upgrades.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {