inup 1.6.0 → 1.6.3
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/README.md +81 -4
- package/dist/cli.js +23 -3
- package/dist/config/constants.js +6 -3
- package/dist/core/package-detector.js +20 -0
- package/dist/core/upgrade-runner.js +8 -0
- package/dist/core/upgrader.js +36 -15
- package/dist/features/debug/index.js +1 -0
- package/dist/features/debug/renderer/performance-modal.js +17 -1
- package/dist/features/debug/services/perf-logger.js +129 -0
- package/dist/features/debug/services/performance-tracker.js +15 -3
- package/dist/features/headless/headless-runner.js +89 -5
- package/dist/services/http/adaptive-controller.js +153 -0
- package/dist/services/http/etag-store.js +91 -0
- package/dist/services/http/resizable-semaphore.js +71 -0
- package/dist/services/http/retry.js +23 -1
- package/dist/services/npm-registry.js +195 -86
- package/dist/services/package-manager-detector.js +4 -0
- package/dist/utils/debug-logger.js +8 -1
- package/dist/utils/filesystem/scan.js +13 -2
- package/dist/utils/index.js +1 -0
- package/dist/utils/local-env.js +81 -0
- package/package.json +7 -10
- package/dist/services/cache-manager.js +0 -95
- package/dist/services/persistent-cache.js +0 -237
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.fetchPackageVersions = fetchPackageVersions;
|
|
4
4
|
exports.clearPackageCache = clearPackageCache;
|
|
5
|
-
exports.closeRegistryPool = closeRegistryPool;
|
|
6
5
|
const undici_1 = require("undici");
|
|
7
6
|
const node_zlib_1 = require("node:zlib");
|
|
8
7
|
const node_util_1 = require("node:util");
|
|
@@ -13,27 +12,41 @@ const config_1 = require("../config");
|
|
|
13
12
|
const version_1 = require("../utils/version");
|
|
14
13
|
const retry_1 = require("./http/retry");
|
|
15
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");
|
|
16
18
|
const inFlightLookups = new inflight_1.InflightMap();
|
|
17
19
|
const registryOrigin = new URL(config_1.NPM_REGISTRY_URL).origin;
|
|
18
20
|
const registryPathPrefix = new URL(config_1.NPM_REGISTRY_URL).pathname.replace(/\/$/, '');
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
21
|
+
// Connection count is kept == the adaptive controller's ceiling (POOL_CONNECTIONS)
|
|
22
|
+
// so the controller is never silently throttled below its chosen limit. Idle
|
|
23
|
+
// keep-alive connections are cheap.
|
|
24
|
+
//
|
|
25
|
+
// `headersTimeout` is intentionally non-zero (unlike the rest, where we tolerate
|
|
26
|
+
// slow bodies): a stalled connection that never sends headers would otherwise be
|
|
27
|
+
// consumed forever and stay invisible to the completion-based adaptive
|
|
28
|
+
// controller. With a headers timeout, a stall surfaces as a transient error the
|
|
29
|
+
// controller can react to (and retry handles). `bodyTimeout` stays 0 — large
|
|
30
|
+
// packuments legitimately stream slowly.
|
|
22
31
|
const registryPool = new undici_1.Pool(registryOrigin, {
|
|
23
|
-
connections:
|
|
32
|
+
connections: config_1.POOL_CONNECTIONS,
|
|
24
33
|
pipelining: 1,
|
|
25
34
|
keepAliveTimeout: 30_000,
|
|
26
35
|
keepAliveMaxTimeout: 600_000,
|
|
27
|
-
headersTimeout:
|
|
36
|
+
headersTimeout: 30_000,
|
|
28
37
|
bodyTimeout: 0,
|
|
29
38
|
connectTimeout: 15_000,
|
|
30
39
|
allowH2: false,
|
|
31
40
|
});
|
|
32
41
|
const MAX_REGISTRY_ATTEMPTS = 3;
|
|
33
42
|
const RETRY_BACKOFF_MS = [500, 1500, 3000];
|
|
34
|
-
|
|
43
|
+
// Fixed concurrency used when adaptive is disabled (INUP_ADAPTIVE=0, the A/B
|
|
44
|
+
// control arm). Matches the production caller (PackageDetector) so the fixed path
|
|
45
|
+
// reproduces the legacy behavior exactly.
|
|
46
|
+
const DEFAULT_FIXED_CONCURRENCY = 10;
|
|
47
|
+
async function getFreshPackageData(packageName, currentVersion, onAttempt) {
|
|
35
48
|
const cacheKey = `${packageName}@${currentVersion ?? ''}`;
|
|
36
|
-
return inFlightLookups.dedupe(cacheKey, () => fetchPackageFromRegistry(packageName));
|
|
49
|
+
return inFlightLookups.dedupe(cacheKey, () => fetchPackageFromRegistry(packageName, onAttempt));
|
|
37
50
|
}
|
|
38
51
|
const encodeRegistryPath = (packageName) => {
|
|
39
52
|
const encodedName = packageName.startsWith('@')
|
|
@@ -42,20 +55,40 @@ const encodeRegistryPath = (packageName) => {
|
|
|
42
55
|
return `${registryPathPrefix}/${encodedName}`;
|
|
43
56
|
};
|
|
44
57
|
async function attemptRegistryFetch(path) {
|
|
58
|
+
const startedAt = Date.now();
|
|
59
|
+
// Conditional request: if we have a stored ETag for this packument, ask the
|
|
60
|
+
// registry to validate it. Unchanged → 304 (no body) and we reuse stored data.
|
|
61
|
+
// This still hits the registry every run, so data is never served stale.
|
|
62
|
+
const cached = (0, etag_store_1.readEtag)(path);
|
|
45
63
|
try {
|
|
64
|
+
const requestHeaders = {
|
|
65
|
+
accept: 'application/vnd.npm.install-v1+json',
|
|
66
|
+
'accept-encoding': 'gzip, deflate, br',
|
|
67
|
+
};
|
|
68
|
+
if (cached) {
|
|
69
|
+
requestHeaders['if-none-match'] = cached.etag;
|
|
70
|
+
}
|
|
46
71
|
const { statusCode, headers, body } = await registryPool.request({
|
|
47
72
|
path,
|
|
48
73
|
method: 'GET',
|
|
49
|
-
headers:
|
|
50
|
-
|
|
51
|
-
'accept-encoding': 'gzip, deflate, br',
|
|
52
|
-
},
|
|
53
|
-
headersTimeout: 0,
|
|
74
|
+
headers: requestHeaders,
|
|
75
|
+
headersTimeout: 30_000,
|
|
54
76
|
bodyTimeout: 0,
|
|
55
77
|
blocking: false,
|
|
56
78
|
});
|
|
79
|
+
// Registry confirmed our cached copy is current — reuse it, skip the download.
|
|
80
|
+
if (statusCode === 304 && cached) {
|
|
81
|
+
await body.dump().catch(() => undefined);
|
|
82
|
+
return { kind: 'success', data: cached.data, latencyMs: Date.now() - startedAt };
|
|
83
|
+
}
|
|
57
84
|
if (statusCode < 200 || statusCode >= 300) {
|
|
58
85
|
await body.dump().catch(() => undefined);
|
|
86
|
+
if ((0, retry_1.isCongestionStatus)(statusCode)) {
|
|
87
|
+
return {
|
|
88
|
+
kind: 'congested',
|
|
89
|
+
retryAfterMs: (0, retry_1.parseRetryAfterMs)(headers['retry-after']),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
59
92
|
if ((0, retry_1.isRetryableStatus)(statusCode)) {
|
|
60
93
|
return { kind: 'retryable' };
|
|
61
94
|
}
|
|
@@ -79,7 +112,18 @@ async function attemptRegistryFetch(path) {
|
|
|
79
112
|
else {
|
|
80
113
|
decoded = raw;
|
|
81
114
|
}
|
|
82
|
-
|
|
115
|
+
const data = (0, version_1.parseVersions)(decoded.toString('utf8'));
|
|
116
|
+
// Persist the ETag for next run's conditional request.
|
|
117
|
+
const etagHeader = headers['etag'];
|
|
118
|
+
const etag = Array.isArray(etagHeader) ? etagHeader[0] : etagHeader;
|
|
119
|
+
if (etag) {
|
|
120
|
+
(0, etag_store_1.writeEtag)(path, etag.toString(), data);
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
kind: 'success',
|
|
124
|
+
data,
|
|
125
|
+
latencyMs: Date.now() - startedAt,
|
|
126
|
+
};
|
|
83
127
|
}
|
|
84
128
|
catch (error) {
|
|
85
129
|
if ((0, retry_1.isTransientNetworkError)(error)) {
|
|
@@ -90,24 +134,28 @@ async function attemptRegistryFetch(path) {
|
|
|
90
134
|
return { kind: 'transient' };
|
|
91
135
|
}
|
|
92
136
|
}
|
|
93
|
-
async function fetchFromRegistryWithRetries(path) {
|
|
137
|
+
async function fetchFromRegistryWithRetries(path, onAttempt) {
|
|
94
138
|
let lastOutcome = { kind: 'transient' };
|
|
95
139
|
for (let attempt = 0; attempt < MAX_REGISTRY_ATTEMPTS; attempt++) {
|
|
96
140
|
const outcome = await attemptRegistryFetch(path);
|
|
141
|
+
onAttempt?.(outcome);
|
|
97
142
|
if (outcome.kind === 'success' || outcome.kind === 'not-found') {
|
|
98
143
|
return outcome;
|
|
99
144
|
}
|
|
100
145
|
lastOutcome = outcome;
|
|
101
146
|
if (attempt < MAX_REGISTRY_ATTEMPTS - 1) {
|
|
102
|
-
|
|
147
|
+
// Honor Retry-After on congestion; otherwise exponential backoff. These
|
|
148
|
+
// sleeps are deliberately NOT timed into the controller's latency EWMA.
|
|
149
|
+
const congestedWait = outcome.kind === 'congested' && outcome.retryAfterMs !== null ? outcome.retryAfterMs : null;
|
|
150
|
+
const backoff = congestedWait ?? RETRY_BACKOFF_MS[Math.min(attempt, RETRY_BACKOFF_MS.length - 1)];
|
|
103
151
|
await (0, retry_1.sleep)(backoff);
|
|
104
152
|
}
|
|
105
153
|
}
|
|
106
154
|
return lastOutcome;
|
|
107
155
|
}
|
|
108
|
-
async function fetchPackageFromRegistry(packageName) {
|
|
156
|
+
async function fetchPackageFromRegistry(packageName, onAttempt) {
|
|
109
157
|
const path = encodeRegistryPath(packageName);
|
|
110
|
-
const outcome = await fetchFromRegistryWithRetries(path);
|
|
158
|
+
const outcome = await fetchFromRegistryWithRetries(path, onAttempt);
|
|
111
159
|
if (outcome.kind === 'success') {
|
|
112
160
|
return outcome.data;
|
|
113
161
|
}
|
|
@@ -119,29 +167,48 @@ async function fetchPackageFromRegistry(packageName) {
|
|
|
119
167
|
* Fetches version data for a list of packages from the npm registry.
|
|
120
168
|
*
|
|
121
169
|
* Concurrency model:
|
|
122
|
-
* -
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
* -
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
170
|
+
* - A single resizable semaphore caps in-flight fetches. Package names are
|
|
171
|
+
* pulled from a work queue and dispatched as slots free up (a lazy pump),
|
|
172
|
+
* rather than pre-sliced into fixed batches.
|
|
173
|
+
* - `adaptive` (default true) enables an AIMD controller that ramps the limit to
|
|
174
|
+
* the ceiling on a healthy link and backs off on congestion (429/503) or
|
|
175
|
+
* errors. With `adaptive:false` the limit is fixed at `maxConcurrency` (the A/B
|
|
176
|
+
* control arm), reproducing the legacy fixed path.
|
|
177
|
+
* - Tiny runs (<= ceil packages) skip the controller and run at a fixed
|
|
178
|
+
* `min(ceil, count)` so they never crawl up from the floor and lose to fixed.
|
|
179
|
+
* - No body timeout: slow responses finish. Real network errors and header
|
|
180
|
+
* stalls are retried with backoff; after the retry budget is exhausted the
|
|
181
|
+
* package is reported unavailable (`latestVersion: 'unknown'`).
|
|
182
|
+
* - Unchanged packuments are revalidated via ETag (304), skipping re-download.
|
|
130
183
|
*
|
|
131
184
|
* Callbacks:
|
|
132
|
-
* - `onBatchReady` fires once
|
|
133
|
-
*
|
|
185
|
+
* - `onBatchReady` fires once an emission window has resolved, in original order.
|
|
186
|
+
* Emission windows are fixed-size groupings for UI progress only; they do not
|
|
187
|
+
* gate concurrency.
|
|
188
|
+
* - `onControlTick` (optional) reports each adaptive control decision for
|
|
189
|
+
* instrumentation.
|
|
134
190
|
*/
|
|
135
191
|
async function fetchPackageVersions(packageNames, options = {}) {
|
|
136
192
|
const packageData = new Map();
|
|
137
|
-
|
|
193
|
+
const total = packageNames.length;
|
|
194
|
+
if (total === 0) {
|
|
138
195
|
return packageData;
|
|
139
196
|
}
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const
|
|
197
|
+
const adaptive = options.adaptive ?? true;
|
|
198
|
+
// `maxConcurrency` is the fixed cap used only when adaptive is off; it never
|
|
199
|
+
// caps the adaptive start (the controller smart-starts near the work size and
|
|
200
|
+
// ramps to the ceiling, which beats a low fixed start on large runs).
|
|
201
|
+
const fixedConcurrency = Math.max(1, options.maxConcurrency ?? DEFAULT_FIXED_CONCURRENCY);
|
|
202
|
+
const controller = adaptive && adaptive_controller_1.AdaptiveController.shouldControl(total)
|
|
203
|
+
? new adaptive_controller_1.AdaptiveController(total, options.onControlTick)
|
|
204
|
+
: null;
|
|
205
|
+
const initialLimit = controller
|
|
206
|
+
? controller.getLimit()
|
|
207
|
+
: adaptive
|
|
208
|
+
? Math.min(config_1.POOL_CONNECTIONS, total) // too small to control: smart fixed start
|
|
209
|
+
: fixedConcurrency;
|
|
210
|
+
const semaphore = new resizable_semaphore_1.ResizableSemaphore(initialLimit);
|
|
211
|
+
// --- emission ordering (unchanged contract) ---------------------------------
|
|
145
212
|
let completedCount = 0;
|
|
146
213
|
const pendingEmissions = new Map();
|
|
147
214
|
let nextEmitIndex = 0;
|
|
@@ -153,68 +220,110 @@ async function fetchPackageVersions(packageNames, options = {}) {
|
|
|
153
220
|
nextEmitIndex++;
|
|
154
221
|
}
|
|
155
222
|
};
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
223
|
+
// Emission windows group results for UI progress only (decoupled from
|
|
224
|
+
// concurrency). Sizes come from `batchSizes` (a sequence, last value repeats)
|
|
225
|
+
// or a uniform `batchSize`. We precompute, per package index, which window it
|
|
226
|
+
// belongs to and its position within that window, so a window can flush as soon
|
|
227
|
+
// as all its items resolve — preserving original order via `flushPending`.
|
|
228
|
+
const windowSizes = options.batchSizes && options.batchSizes.length > 0
|
|
229
|
+
? options.batchSizes.map((size) => Math.max(1, size))
|
|
230
|
+
: [Math.max(1, options.batchSize ?? 25)];
|
|
231
|
+
const windowIdByIndex = new Array(total);
|
|
232
|
+
const itemIndexByIndex = new Array(total);
|
|
233
|
+
const windowRemaining = [];
|
|
234
|
+
{
|
|
235
|
+
let cursorIndex = 0;
|
|
236
|
+
let windowId = 0;
|
|
237
|
+
while (cursorIndex < total) {
|
|
238
|
+
const size = windowSizes[Math.min(windowId, windowSizes.length - 1)];
|
|
239
|
+
const end = Math.min(cursorIndex + size, total);
|
|
240
|
+
windowRemaining[windowId] = end - cursorIndex;
|
|
241
|
+
for (let i = cursorIndex; i < end; i++) {
|
|
242
|
+
windowIdByIndex[i] = windowId;
|
|
243
|
+
itemIndexByIndex[i] = i - cursorIndex;
|
|
244
|
+
}
|
|
245
|
+
cursorIndex = end;
|
|
246
|
+
windowId++;
|
|
164
247
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
if (
|
|
172
|
-
|
|
248
|
+
}
|
|
249
|
+
const windowResults = windowRemaining.map(() => []);
|
|
250
|
+
// --- per-attempt observer ---------------------------------------------------
|
|
251
|
+
// Feeds the adaptive controller AND (optionally) reports per-package latency
|
|
252
|
+
// for diagnostics. Built per package so the timing callback knows the name.
|
|
253
|
+
const observerFor = (packageName) => {
|
|
254
|
+
if (!controller && !options.onPackageTiming)
|
|
255
|
+
return undefined;
|
|
256
|
+
return (outcome) => {
|
|
257
|
+
if (outcome.kind === 'success') {
|
|
258
|
+
controller?.record('success', outcome.latencyMs);
|
|
259
|
+
options.onPackageTiming?.(packageName, outcome.latencyMs);
|
|
260
|
+
}
|
|
261
|
+
else if (outcome.kind === 'congested') {
|
|
262
|
+
const next = controller?.record('congested');
|
|
263
|
+
if (next != null)
|
|
264
|
+
semaphore.setLimit(next);
|
|
265
|
+
}
|
|
266
|
+
else if (outcome.kind === 'retryable') {
|
|
267
|
+
controller?.record('retryable');
|
|
268
|
+
}
|
|
269
|
+
else if (outcome.kind === 'transient') {
|
|
270
|
+
controller?.record('transient');
|
|
271
|
+
}
|
|
272
|
+
};
|
|
173
273
|
};
|
|
174
|
-
|
|
175
|
-
let
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
274
|
+
// --- worker: pull from the queue until exhausted ----------------------------
|
|
275
|
+
let cursor = 0;
|
|
276
|
+
const runOne = async (index) => {
|
|
277
|
+
const packageName = packageNames[index];
|
|
278
|
+
await semaphore.acquire();
|
|
279
|
+
try {
|
|
280
|
+
const data = await getFreshPackageData(packageName, options.currentVersions?.get(packageName), observerFor(packageName));
|
|
281
|
+
packageData.set(packageName, data);
|
|
282
|
+
completedCount++;
|
|
283
|
+
const w = windowIdByIndex[index];
|
|
284
|
+
const itemIndex = itemIndexByIndex[index];
|
|
285
|
+
// Index by position (not push) so items keep their original in-window order
|
|
286
|
+
// even when they resolve out of order.
|
|
287
|
+
windowResults[w][itemIndex] = {
|
|
288
|
+
packageName,
|
|
289
|
+
data,
|
|
290
|
+
completed: completedCount,
|
|
291
|
+
total,
|
|
292
|
+
batchIndex: w,
|
|
293
|
+
itemIndex,
|
|
294
|
+
};
|
|
295
|
+
if (--windowRemaining[w] === 0) {
|
|
296
|
+
pendingEmissions.set(w, windowResults[w]);
|
|
297
|
+
flushPending();
|
|
196
298
|
}
|
|
197
|
-
|
|
198
|
-
|
|
299
|
+
if (controller) {
|
|
300
|
+
const next = controller.maybeTick();
|
|
301
|
+
if (next !== null)
|
|
302
|
+
semaphore.setLimit(next);
|
|
199
303
|
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
304
|
+
}
|
|
305
|
+
finally {
|
|
306
|
+
semaphore.release();
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
// The pump: keep enough workers running to saturate the (possibly growing)
|
|
310
|
+
// limit. We dispatch all indices as promises but each one waits on the
|
|
311
|
+
// semaphore before doing work, so the semaphore — not the dispatch loop —
|
|
312
|
+
// enforces the limit. Growing the limit lets queued acquirers through.
|
|
313
|
+
const workers = [];
|
|
314
|
+
while (cursor < total) {
|
|
315
|
+
workers.push(runOne(cursor));
|
|
316
|
+
cursor++;
|
|
207
317
|
}
|
|
208
|
-
await Promise.all(
|
|
318
|
+
await Promise.all(workers);
|
|
209
319
|
return packageData;
|
|
210
320
|
}
|
|
211
321
|
/**
|
|
212
|
-
*
|
|
322
|
+
* Clears the in-run in-flight dedupe map. Only meaningful within a single run
|
|
323
|
+
* (the map collapses duplicate concurrent lookups); registry data itself is
|
|
324
|
+
* never cached in memory across runs. Used by tests to isolate fetches.
|
|
213
325
|
*/
|
|
214
326
|
function clearPackageCache() {
|
|
215
327
|
inFlightLookups.clear();
|
|
216
328
|
}
|
|
217
|
-
async function closeRegistryPool() {
|
|
218
|
-
await registryPool.close();
|
|
219
|
-
}
|
|
220
329
|
//# sourceMappingURL=npm-registry.js.map
|
|
@@ -22,6 +22,8 @@ const PACKAGE_MANAGERS = {
|
|
|
22
22
|
lockFile: 'yarn.lock',
|
|
23
23
|
workspaceFile: null, // Uses package.json workspaces field
|
|
24
24
|
installCommand: 'yarn install',
|
|
25
|
+
// Yarn Berry defaults to immutable installs in CI; --no-immutable lets the lockfile update.
|
|
26
|
+
writeInstallCommand: 'yarn install --no-immutable',
|
|
25
27
|
color: chalk_1.default.blue,
|
|
26
28
|
},
|
|
27
29
|
pnpm: {
|
|
@@ -30,6 +32,8 @@ const PACKAGE_MANAGERS = {
|
|
|
30
32
|
lockFile: 'pnpm-lock.yaml',
|
|
31
33
|
workspaceFile: 'pnpm-workspace.yaml',
|
|
32
34
|
installCommand: 'pnpm install',
|
|
35
|
+
// pnpm defaults to --frozen-lockfile in CI; --no-frozen-lockfile lets the lockfile update.
|
|
36
|
+
writeInstallCommand: 'pnpm install --no-frozen-lockfile',
|
|
33
37
|
color: chalk_1.default.yellow,
|
|
34
38
|
},
|
|
35
39
|
bun: {
|
|
@@ -5,6 +5,7 @@ exports.enableDebugLogging = enableDebugLogging;
|
|
|
5
5
|
exports.isDebugEnabled = isDebugEnabled;
|
|
6
6
|
exports.getDebugLogPath = getDebugLogPath;
|
|
7
7
|
const fs_1 = require("fs");
|
|
8
|
+
const os_1 = require("os");
|
|
8
9
|
const path_1 = require("path");
|
|
9
10
|
let _enabled = false;
|
|
10
11
|
let _logFile = null;
|
|
@@ -18,7 +19,13 @@ function getLogFile() {
|
|
|
18
19
|
if (!_logFile) {
|
|
19
20
|
const d = new Date();
|
|
20
21
|
const dateStr = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
21
|
-
|
|
22
|
+
// Write to the OS temp dir (matches the path advertised in --help), not the
|
|
23
|
+
// current working directory — a debug log must never litter the user's repo.
|
|
24
|
+
const dir = (0, path_1.join)((0, os_1.tmpdir)(), 'inup');
|
|
25
|
+
if (!(0, fs_1.existsSync)(dir)) {
|
|
26
|
+
(0, fs_1.mkdirSync)(dir, { recursive: true });
|
|
27
|
+
}
|
|
28
|
+
_logFile = (0, path_1.join)(dir, `inup-debug-${dateStr}.log`);
|
|
22
29
|
// Write a header so the file is easy to identify
|
|
23
30
|
(0, fs_1.writeFileSync)(_logFile, `=== inup debug log started at ${timestamp()} ===\n`, { flag: 'a' });
|
|
24
31
|
}
|
|
@@ -5,6 +5,15 @@ exports.findAllPackageJsonFilesAsync = findAllPackageJsonFilesAsync;
|
|
|
5
5
|
const fs_1 = require("fs");
|
|
6
6
|
const fs_2 = require("fs");
|
|
7
7
|
const path_1 = require("path");
|
|
8
|
+
/**
|
|
9
|
+
* Normalize a relative path to forward slashes before matching `.inuprc` exclude patterns.
|
|
10
|
+
* On Windows `path.relative` yields backslash separators (e.g. `packages\skipme`), but users write
|
|
11
|
+
* exclude regexes with `/` (e.g. `^packages/skipme(?:/|$)`). Without this, excludes silently fail
|
|
12
|
+
* on Windows and a path the user meant to skip gets scanned and upgraded.
|
|
13
|
+
*/
|
|
14
|
+
function toPosixPath(p) {
|
|
15
|
+
return p.replace(/\\/g, '/');
|
|
16
|
+
}
|
|
8
17
|
const SKIP_DIRS = new Set([
|
|
9
18
|
'node_modules',
|
|
10
19
|
'dist',
|
|
@@ -97,7 +106,8 @@ function findAllPackageJsonFiles(rootDir = process.cwd(), excludePatterns = [],
|
|
|
97
106
|
const skipSet = buildSkipSet(options.scanDirs);
|
|
98
107
|
const excludeRegexes = excludePatterns.map((pattern) => new RegExp(pattern, 'i'));
|
|
99
108
|
function shouldExcludePath(relativePath) {
|
|
100
|
-
|
|
109
|
+
const posix = toPosixPath(relativePath);
|
|
110
|
+
return excludeRegexes.some((regex) => regex.test(posix));
|
|
101
111
|
}
|
|
102
112
|
function reportProgress(currentDir, force = false) {
|
|
103
113
|
if (!onProgress)
|
|
@@ -169,7 +179,8 @@ async function findAllPackageJsonFilesAsync(rootDir = process.cwd(), excludePatt
|
|
|
169
179
|
const skipSet = buildSkipSet(options.scanDirs);
|
|
170
180
|
const excludeRegexes = excludePatterns.map((pattern) => new RegExp(pattern, 'i'));
|
|
171
181
|
function shouldExcludePath(relativePath) {
|
|
172
|
-
|
|
182
|
+
const posix = toPosixPath(relativePath);
|
|
183
|
+
return excludeRegexes.some((regex) => regex.test(posix));
|
|
173
184
|
}
|
|
174
185
|
function reportProgress(currentDir, force = false) {
|
|
175
186
|
if (!onProgress)
|
package/dist/utils/index.js
CHANGED
|
@@ -23,6 +23,7 @@ __exportStar(require("./exec"), exports);
|
|
|
23
23
|
__exportStar(require("./git"), exports);
|
|
24
24
|
__exportStar(require("./version"), exports);
|
|
25
25
|
__exportStar(require("./debug-logger"), exports);
|
|
26
|
+
__exportStar(require("./local-env"), exports);
|
|
26
27
|
__exportStar(require("./color"), exports);
|
|
27
28
|
__exportStar(require("./engines"), exports);
|
|
28
29
|
__exportStar(require("./manifest"), exports);
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.loadInupLocalEnv = loadInupLocalEnv;
|
|
4
|
+
const fs_1 = require("fs");
|
|
5
|
+
const path_1 = require("path");
|
|
6
|
+
/**
|
|
7
|
+
* Loads a gitignored `.env.local` from the inup repo itself (NOT the cwd), so
|
|
8
|
+
* developer-only toggles can be "set once" and apply to every `inup` run in any
|
|
9
|
+
* project — without committing anything or touching the shell profile.
|
|
10
|
+
*
|
|
11
|
+
* The repo is located by walking UP from this file's own directory until a
|
|
12
|
+
* `.env.local` is found, so it works regardless of nesting depth (src/utils when
|
|
13
|
+
* type-stripped, dist/utils when compiled) and resolves the inup repo even when
|
|
14
|
+
* the binary is linked and invoked from a different project directory.
|
|
15
|
+
*
|
|
16
|
+
* Fully optional and best-effort: if the file is absent or unreadable, nothing
|
|
17
|
+
* happens and the run proceeds exactly as before. Existing process env always
|
|
18
|
+
* wins, so a one-off `INUP_PERF=0 inup` still overrides the file.
|
|
19
|
+
*/
|
|
20
|
+
const ENV_FILE_NAME = '.env.local';
|
|
21
|
+
/** Walk upward from this file's dir to find the .env.local; null if none. */
|
|
22
|
+
function findEnvFile() {
|
|
23
|
+
let dir = __dirname;
|
|
24
|
+
const root = (0, path_1.parse)(dir).root;
|
|
25
|
+
// Cap the walk so a missing file can never loop unbounded.
|
|
26
|
+
for (let i = 0; i < 12; i++) {
|
|
27
|
+
const candidate = (0, path_1.join)(dir, ENV_FILE_NAME);
|
|
28
|
+
if ((0, fs_1.existsSync)(candidate))
|
|
29
|
+
return candidate;
|
|
30
|
+
if (dir === root)
|
|
31
|
+
break;
|
|
32
|
+
dir = (0, path_1.dirname)(dir);
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
/** Parse a minimal KEY=VALUE env file. Ignores blanks and `#` comments. */
|
|
37
|
+
function parseEnv(contents) {
|
|
38
|
+
const out = {};
|
|
39
|
+
for (const rawLine of contents.split(/\r?\n/)) {
|
|
40
|
+
const line = rawLine.trim();
|
|
41
|
+
if (!line || line.startsWith('#'))
|
|
42
|
+
continue;
|
|
43
|
+
const eq = line.indexOf('=');
|
|
44
|
+
if (eq === -1)
|
|
45
|
+
continue;
|
|
46
|
+
const key = line.slice(0, eq).trim();
|
|
47
|
+
if (!key)
|
|
48
|
+
continue;
|
|
49
|
+
let value = line.slice(eq + 1).trim();
|
|
50
|
+
// Strip a single layer of matching quotes.
|
|
51
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
52
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
53
|
+
value = value.slice(1, -1);
|
|
54
|
+
}
|
|
55
|
+
out[key] = value;
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Apply `<inup-repo>/.env.local` into process.env. Existing values are never
|
|
61
|
+
* overwritten (real env / one-off overrides win). Returns the path if loaded.
|
|
62
|
+
*/
|
|
63
|
+
function loadInupLocalEnv() {
|
|
64
|
+
try {
|
|
65
|
+
const file = findEnvFile();
|
|
66
|
+
if (!file)
|
|
67
|
+
return null;
|
|
68
|
+
const parsed = parseEnv((0, fs_1.readFileSync)(file, 'utf8'));
|
|
69
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
70
|
+
if (process.env[key] === undefined) {
|
|
71
|
+
process.env[key] = value;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return file;
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// Best-effort: never let a dev convenience break a real run.
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=local-env.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "inup",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.3",
|
|
4
4
|
"description": "Interactive dependency upgrader for npm, yarn, pnpm & bun. Zero-config, monorepo-ready. Upgrade-interactive for every package manager.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -43,10 +43,10 @@
|
|
|
43
43
|
"README.md"
|
|
44
44
|
],
|
|
45
45
|
"devDependencies": {
|
|
46
|
-
"@types/node": "^25.9.
|
|
46
|
+
"@types/node": "^25.9.4",
|
|
47
47
|
"@types/semver": "^7.7.1",
|
|
48
48
|
"@vitest/coverage-v8": "^4.1.7",
|
|
49
|
-
"prettier": "^3.
|
|
49
|
+
"prettier": "^3.9.4",
|
|
50
50
|
"typescript": "^6.0.3",
|
|
51
51
|
"vitest": "^4.1.7"
|
|
52
52
|
},
|
|
@@ -55,20 +55,17 @@
|
|
|
55
55
|
"commander": "^15.0.0",
|
|
56
56
|
"env-paths": "^4.0.0",
|
|
57
57
|
"nanospinner": "^1.2.2",
|
|
58
|
-
"semver": "^7.8.
|
|
59
|
-
"undici": "^8.
|
|
58
|
+
"semver": "^7.8.5",
|
|
59
|
+
"undici": "^8.5.0"
|
|
60
60
|
},
|
|
61
61
|
"engines": {
|
|
62
|
-
"node": ">=
|
|
62
|
+
"node": ">=22.19.0"
|
|
63
63
|
},
|
|
64
64
|
"scripts": {
|
|
65
65
|
"build": "rm -rf dist && tsc",
|
|
66
66
|
"dev": "tsc --watch",
|
|
67
67
|
"start": "node dist/cli.js",
|
|
68
|
-
"link": "pnpm build && pnpm
|
|
69
|
-
"version:patch": "pnpm version patch && git push && git push --tags",
|
|
70
|
-
"version:minor": "pnpm version minor && git push && git push --tags",
|
|
71
|
-
"version:major": "pnpm version major && git push && git push --tags",
|
|
68
|
+
"link": "pnpm build && pnpm add -g .",
|
|
72
69
|
"format": "prettier --write src/**/*.ts",
|
|
73
70
|
"format:check": "prettier --check src/**/*.ts",
|
|
74
71
|
"demo:record": "bash docs/demo/record-demo.sh",
|