inup 1.5.6 → 1.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -1
- package/dist/cli.js +42 -11
- package/dist/config/constants.js +6 -7
- package/dist/config/project-config.js +5 -0
- package/dist/core/package-detector.js +49 -1
- package/dist/core/upgrade-runner.js +9 -0
- package/dist/core/upgrader.js +9 -3
- package/dist/features/changelog/services/package-metadata-service.js +2 -13
- package/dist/features/changelog/services/release-notes-service.js +0 -27
- 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 +69 -0
- package/dist/features/headless/index.js +27 -0
- package/dist/features/headless/report.js +64 -0
- package/dist/features/headless/types.js +6 -0
- package/dist/features/headless/vulnerability-audit.js +106 -0
- package/dist/interactive-ui.js +4 -2
- 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/index.js +0 -1
- package/dist/services/npm-registry.js +198 -97
- package/dist/services/package-manager-detector.js +6 -3
- package/dist/ui/modal/package-info-sections/sections.js +24 -0
- package/dist/ui/presenters/health.js +24 -0
- package/dist/ui/renderer/package-list/rows.js +8 -3
- package/dist/ui/session/selection-state-builder.js +7 -2
- package/dist/ui/themes-colors.js +12 -3
- package/dist/ui/utils/cursor.js +9 -3
- package/dist/utils/color.js +38 -0
- package/dist/utils/debug-logger.js +8 -1
- package/dist/utils/engines.js +63 -0
- package/dist/utils/filesystem/io.js +16 -0
- package/dist/utils/filesystem/scan.js +82 -7
- package/dist/utils/index.js +4 -0
- package/dist/utils/local-env.js +81 -0
- package/dist/utils/manifest.js +35 -0
- package/dist/utils/version.js +9 -2
- package/package.json +8 -8
- package/dist/services/cache-manager.js +0 -95
- package/dist/services/jsdelivr/client.js +0 -191
- package/dist/services/jsdelivr/manifest.js +0 -136
- package/dist/services/jsdelivr-registry.js +0 -9
- package/dist/services/persistent-cache.js +0 -237
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ResizableSemaphore = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* A counting semaphore whose limit can change at runtime.
|
|
6
|
+
*
|
|
7
|
+
* - `acquire()` resolves immediately if there is headroom, otherwise queues a
|
|
8
|
+
* waiter (FIFO) until a `release()` opens a slot.
|
|
9
|
+
* - `setLimit()` may grow or shrink the limit at any time. Growing wakes as many
|
|
10
|
+
* queued waiters as the new headroom allows. Shrinking never aborts in-flight
|
|
11
|
+
* holders — it simply stops admitting new ones until natural releases drain
|
|
12
|
+
* `inFlight` back below the new limit.
|
|
13
|
+
* - `release()` is guarded: it only wakes a waiter when `inFlight < limit`. This
|
|
14
|
+
* is what makes a shrink actually take effect (an unconditional wake would
|
|
15
|
+
* re-admit immediately and ignore the smaller limit).
|
|
16
|
+
*
|
|
17
|
+
* Invariant: `inFlight` never exceeds `limit` at the moment of admission. A prior
|
|
18
|
+
* shrink can leave `inFlight > limit` transiently; no new holder is admitted
|
|
19
|
+
* until it drains.
|
|
20
|
+
*/
|
|
21
|
+
class ResizableSemaphore {
|
|
22
|
+
limit;
|
|
23
|
+
inFlight = 0;
|
|
24
|
+
waiters = [];
|
|
25
|
+
constructor(initialLimit) {
|
|
26
|
+
this.limit = Math.max(1, Math.floor(initialLimit));
|
|
27
|
+
}
|
|
28
|
+
getLimit() {
|
|
29
|
+
return this.limit;
|
|
30
|
+
}
|
|
31
|
+
getInFlight() {
|
|
32
|
+
return this.inFlight;
|
|
33
|
+
}
|
|
34
|
+
getWaiterCount() {
|
|
35
|
+
return this.waiters.length;
|
|
36
|
+
}
|
|
37
|
+
setLimit(next) {
|
|
38
|
+
this.limit = Math.max(1, Math.floor(next));
|
|
39
|
+
// Growing may have opened headroom for queued waiters.
|
|
40
|
+
this.drainWaiters();
|
|
41
|
+
}
|
|
42
|
+
async acquire() {
|
|
43
|
+
// `inFlight` is the single authoritative counter and is incremented exactly
|
|
44
|
+
// once per admission, synchronously at the moment admission is decided —
|
|
45
|
+
// either here (fast path) or in `drainWaiters` (when a slot frees up). A
|
|
46
|
+
// woken waiter must NOT increment again, or a concurrent fast-path acquire
|
|
47
|
+
// reading a stale count could over-admit past the limit.
|
|
48
|
+
if (this.inFlight < this.limit) {
|
|
49
|
+
this.inFlight++;
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
await new Promise((resolve) => this.waiters.push(resolve));
|
|
53
|
+
}
|
|
54
|
+
release() {
|
|
55
|
+
this.inFlight--;
|
|
56
|
+
this.drainWaiters();
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Wakes queued waiters while there is headroom, incrementing `inFlight` for
|
|
60
|
+
* each as it is admitted so the count stays authoritative and synchronous.
|
|
61
|
+
*/
|
|
62
|
+
drainWaiters() {
|
|
63
|
+
while (this.inFlight < this.limit && this.waiters.length > 0) {
|
|
64
|
+
const next = this.waiters.shift();
|
|
65
|
+
this.inFlight++;
|
|
66
|
+
next();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
exports.ResizableSemaphore = ResizableSemaphore;
|
|
71
|
+
//# sourceMappingURL=resizable-semaphore.js.map
|
|
@@ -1,10 +1,32 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.isTransientNetworkError = exports.isRetryableStatus = exports.sleep = void 0;
|
|
3
|
+
exports.isTransientNetworkError = exports.parseRetryAfterMs = exports.isCongestionStatus = exports.isRetryableStatus = exports.sleep = void 0;
|
|
4
4
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
5
5
|
exports.sleep = sleep;
|
|
6
6
|
const isRetryableStatus = (statusCode) => statusCode === 408 || statusCode === 429 || statusCode >= 500;
|
|
7
7
|
exports.isRetryableStatus = isRetryableStatus;
|
|
8
|
+
// Registry explicitly signaling congestion: the highest-quality back-off signal.
|
|
9
|
+
const isCongestionStatus = (statusCode) => statusCode === 429 || statusCode === 503;
|
|
10
|
+
exports.isCongestionStatus = isCongestionStatus;
|
|
11
|
+
// Parse a Retry-After header (delta-seconds or HTTP-date) into milliseconds.
|
|
12
|
+
// Returns null when absent or unparseable.
|
|
13
|
+
const parseRetryAfterMs = (header, now = Date.now()) => {
|
|
14
|
+
if (header === undefined)
|
|
15
|
+
return null;
|
|
16
|
+
const value = (Array.isArray(header) ? header[0] : header)?.toString().trim();
|
|
17
|
+
if (!value)
|
|
18
|
+
return null;
|
|
19
|
+
const asSeconds = Number(value);
|
|
20
|
+
if (Number.isFinite(asSeconds)) {
|
|
21
|
+
return asSeconds >= 0 ? Math.round(asSeconds * 1000) : null;
|
|
22
|
+
}
|
|
23
|
+
const asDate = Date.parse(value);
|
|
24
|
+
if (Number.isFinite(asDate)) {
|
|
25
|
+
return Math.max(0, asDate - now);
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
};
|
|
29
|
+
exports.parseRetryAfterMs = parseRetryAfterMs;
|
|
8
30
|
const isTransientNetworkError = (error) => {
|
|
9
31
|
if (!(error instanceof Error)) {
|
|
10
32
|
return false;
|
package/dist/services/index.js
CHANGED
|
@@ -18,7 +18,6 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
18
18
|
};
|
|
19
19
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
20
|
__exportStar(require("./npm-registry"), exports);
|
|
21
|
-
__exportStar(require("./jsdelivr-registry"), exports);
|
|
22
21
|
__exportStar(require("../features/changelog"), exports);
|
|
23
22
|
__exportStar(require("./version-checker"), exports);
|
|
24
23
|
__exportStar(require("./vulnerability-checker"), exports);
|
|
@@ -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");
|
|
@@ -10,35 +9,44 @@ const gunzipAsync = (0, node_util_1.promisify)(node_zlib_1.gunzip);
|
|
|
10
9
|
const inflateAsync = (0, node_util_1.promisify)(node_zlib_1.inflate);
|
|
11
10
|
const brotliDecompressAsync = (0, node_util_1.promisify)(node_zlib_1.brotliDecompress);
|
|
12
11
|
const config_1 = require("../config");
|
|
13
|
-
const jsdelivr_registry_1 = require("./jsdelivr-registry");
|
|
14
12
|
const version_1 = require("../utils/version");
|
|
15
13
|
const retry_1 = require("./http/retry");
|
|
16
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");
|
|
17
18
|
const inFlightLookups = new inflight_1.InflightMap();
|
|
18
19
|
const registryOrigin = new URL(config_1.NPM_REGISTRY_URL).origin;
|
|
19
20
|
const registryPathPrefix = new URL(config_1.NPM_REGISTRY_URL).pathname.replace(/\/$/, '');
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
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.
|
|
23
31
|
const registryPool = new undici_1.Pool(registryOrigin, {
|
|
24
|
-
connections:
|
|
32
|
+
connections: config_1.POOL_CONNECTIONS,
|
|
25
33
|
pipelining: 1,
|
|
26
34
|
keepAliveTimeout: 30_000,
|
|
27
35
|
keepAliveMaxTimeout: 600_000,
|
|
28
|
-
headersTimeout:
|
|
36
|
+
headersTimeout: 30_000,
|
|
29
37
|
bodyTimeout: 0,
|
|
30
38
|
connectTimeout: 15_000,
|
|
31
39
|
allowH2: false,
|
|
32
40
|
});
|
|
33
41
|
const MAX_REGISTRY_ATTEMPTS = 3;
|
|
34
42
|
const RETRY_BACKOFF_MS = [500, 1500, 3000];
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
async function getFreshPackageData(packageName, currentVersion) {
|
|
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) {
|
|
40
48
|
const cacheKey = `${packageName}@${currentVersion ?? ''}`;
|
|
41
|
-
return inFlightLookups.dedupe(cacheKey, () =>
|
|
49
|
+
return inFlightLookups.dedupe(cacheKey, () => fetchPackageFromRegistry(packageName, onAttempt));
|
|
42
50
|
}
|
|
43
51
|
const encodeRegistryPath = (packageName) => {
|
|
44
52
|
const encodedName = packageName.startsWith('@')
|
|
@@ -47,20 +55,40 @@ const encodeRegistryPath = (packageName) => {
|
|
|
47
55
|
return `${registryPathPrefix}/${encodedName}`;
|
|
48
56
|
};
|
|
49
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);
|
|
50
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
|
+
}
|
|
51
71
|
const { statusCode, headers, body } = await registryPool.request({
|
|
52
72
|
path,
|
|
53
73
|
method: 'GET',
|
|
54
|
-
headers:
|
|
55
|
-
|
|
56
|
-
'accept-encoding': 'gzip, deflate, br',
|
|
57
|
-
},
|
|
58
|
-
headersTimeout: 0,
|
|
74
|
+
headers: requestHeaders,
|
|
75
|
+
headersTimeout: 30_000,
|
|
59
76
|
bodyTimeout: 0,
|
|
60
77
|
blocking: false,
|
|
61
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
|
+
}
|
|
62
84
|
if (statusCode < 200 || statusCode >= 300) {
|
|
63
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
|
+
}
|
|
64
92
|
if ((0, retry_1.isRetryableStatus)(statusCode)) {
|
|
65
93
|
return { kind: 'retryable' };
|
|
66
94
|
}
|
|
@@ -84,7 +112,18 @@ async function attemptRegistryFetch(path) {
|
|
|
84
112
|
else {
|
|
85
113
|
decoded = raw;
|
|
86
114
|
}
|
|
87
|
-
|
|
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
|
+
};
|
|
88
127
|
}
|
|
89
128
|
catch (error) {
|
|
90
129
|
if ((0, retry_1.isTransientNetworkError)(error)) {
|
|
@@ -95,61 +134,81 @@ async function attemptRegistryFetch(path) {
|
|
|
95
134
|
return { kind: 'transient' };
|
|
96
135
|
}
|
|
97
136
|
}
|
|
98
|
-
async function fetchFromRegistryWithRetries(path) {
|
|
137
|
+
async function fetchFromRegistryWithRetries(path, onAttempt) {
|
|
99
138
|
let lastOutcome = { kind: 'transient' };
|
|
100
139
|
for (let attempt = 0; attempt < MAX_REGISTRY_ATTEMPTS; attempt++) {
|
|
101
140
|
const outcome = await attemptRegistryFetch(path);
|
|
141
|
+
onAttempt?.(outcome);
|
|
102
142
|
if (outcome.kind === 'success' || outcome.kind === 'not-found') {
|
|
103
143
|
return outcome;
|
|
104
144
|
}
|
|
105
145
|
lastOutcome = outcome;
|
|
106
146
|
if (attempt < MAX_REGISTRY_ATTEMPTS - 1) {
|
|
107
|
-
|
|
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)];
|
|
108
151
|
await (0, retry_1.sleep)(backoff);
|
|
109
152
|
}
|
|
110
153
|
}
|
|
111
154
|
return lastOutcome;
|
|
112
155
|
}
|
|
113
|
-
async function
|
|
156
|
+
async function fetchPackageFromRegistry(packageName, onAttempt) {
|
|
114
157
|
const path = encodeRegistryPath(packageName);
|
|
115
|
-
const outcome = await fetchFromRegistryWithRetries(path);
|
|
158
|
+
const outcome = await fetchFromRegistryWithRetries(path, onAttempt);
|
|
116
159
|
if (outcome.kind === 'success') {
|
|
117
160
|
return outcome.data;
|
|
118
161
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
}
|
|
122
|
-
// Only reach here after exhausted retries against real errors — try jsdelivr
|
|
123
|
-
// as last-resort safety net so we don't silently return 'unknown'.
|
|
124
|
-
const fallback = await fetchFromJsdelivrFallback(packageName, currentVersion).catch(() => null);
|
|
125
|
-
return fallback ?? { latestVersion: 'unknown', allVersions: [] };
|
|
162
|
+
// Not found, or exhausted retries against real errors: report unavailable.
|
|
163
|
+
// The registry is the single source of truth — there is no secondary fetch.
|
|
164
|
+
return { latestVersion: 'unknown', allVersions: [] };
|
|
126
165
|
}
|
|
127
166
|
/**
|
|
128
167
|
* Fetches version data for a list of packages from the npm registry.
|
|
129
168
|
*
|
|
130
169
|
* Concurrency model:
|
|
131
|
-
* -
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
* -
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
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.
|
|
138
183
|
*
|
|
139
184
|
* Callbacks:
|
|
140
|
-
* - `onBatchReady` fires once
|
|
141
|
-
*
|
|
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.
|
|
142
190
|
*/
|
|
143
191
|
async function fetchPackageVersions(packageNames, options = {}) {
|
|
144
192
|
const packageData = new Map();
|
|
145
|
-
|
|
193
|
+
const total = packageNames.length;
|
|
194
|
+
if (total === 0) {
|
|
146
195
|
return packageData;
|
|
147
196
|
}
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
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) ---------------------------------
|
|
153
212
|
let completedCount = 0;
|
|
154
213
|
const pendingEmissions = new Map();
|
|
155
214
|
let nextEmitIndex = 0;
|
|
@@ -161,68 +220,110 @@ async function fetchPackageVersions(packageNames, options = {}) {
|
|
|
161
220
|
nextEmitIndex++;
|
|
162
221
|
}
|
|
163
222
|
};
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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++;
|
|
172
247
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
if (
|
|
180
|
-
|
|
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
|
+
};
|
|
181
273
|
};
|
|
182
|
-
|
|
183
|
-
let
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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();
|
|
204
298
|
}
|
|
205
|
-
|
|
206
|
-
|
|
299
|
+
if (controller) {
|
|
300
|
+
const next = controller.maybeTick();
|
|
301
|
+
if (next !== null)
|
|
302
|
+
semaphore.setLimit(next);
|
|
207
303
|
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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++;
|
|
215
317
|
}
|
|
216
|
-
await Promise.all(
|
|
318
|
+
await Promise.all(workers);
|
|
217
319
|
return packageData;
|
|
218
320
|
}
|
|
219
321
|
/**
|
|
220
|
-
*
|
|
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.
|
|
221
325
|
*/
|
|
222
326
|
function clearPackageCache() {
|
|
223
327
|
inFlightLookups.clear();
|
|
224
328
|
}
|
|
225
|
-
async function closeRegistryPool() {
|
|
226
|
-
await registryPool.close();
|
|
227
|
-
}
|
|
228
329
|
//# sourceMappingURL=npm-registry.js.map
|
|
@@ -56,8 +56,8 @@ class PackageManagerDetector {
|
|
|
56
56
|
if (fromLockFile) {
|
|
57
57
|
return fromLockFile;
|
|
58
58
|
}
|
|
59
|
-
// 3. Fallback to npm
|
|
60
|
-
console.
|
|
59
|
+
// 3. Fallback to npm. Warn on stderr so it never corrupts --json output on stdout.
|
|
60
|
+
console.error(chalk_1.default.yellow('⚠️ No package manager detected. Defaulting to npm. Consider adding a "packageManager" field to your package.json.'));
|
|
61
61
|
return PACKAGE_MANAGERS.npm;
|
|
62
62
|
}
|
|
63
63
|
/**
|
|
@@ -92,6 +92,8 @@ class PackageManagerDetector {
|
|
|
92
92
|
const lockFileChecks = [
|
|
93
93
|
{ pm: PACKAGE_MANAGERS.pnpm, path: (0, path_1.join)(cwd, 'pnpm-lock.yaml') },
|
|
94
94
|
{ pm: PACKAGE_MANAGERS.bun, path: (0, path_1.join)(cwd, 'bun.lockb') },
|
|
95
|
+
// Bun >= 1.2 writes a text `bun.lock` instead of the binary `bun.lockb`.
|
|
96
|
+
{ pm: PACKAGE_MANAGERS.bun, path: (0, path_1.join)(cwd, 'bun.lock') },
|
|
95
97
|
{ pm: PACKAGE_MANAGERS.yarn, path: (0, path_1.join)(cwd, 'yarn.lock') },
|
|
96
98
|
{ pm: PACKAGE_MANAGERS.npm, path: (0, path_1.join)(cwd, 'package-lock.json') },
|
|
97
99
|
];
|
|
@@ -107,7 +109,8 @@ class PackageManagerDetector {
|
|
|
107
109
|
}
|
|
108
110
|
// If multiple lock files, use most recently modified
|
|
109
111
|
if (existingLocks.length > 1) {
|
|
110
|
-
|
|
112
|
+
// stderr so it never corrupts --json output on stdout.
|
|
113
|
+
console.error(chalk_1.default.yellow('⚠️ Multiple lock files detected. Using most recently modified. Consider cleaning up unused lock files.'));
|
|
111
114
|
existingLocks.sort((a, b) => b.mtime - a.mtime);
|
|
112
115
|
}
|
|
113
116
|
return existingLocks[0].pm;
|
|
@@ -10,6 +10,7 @@ const chalk_1 = __importDefault(require("chalk"));
|
|
|
10
10
|
const themes_colors_1 = require("../../themes-colors");
|
|
11
11
|
const vulnerability_1 = require("../../presenters/vulnerability");
|
|
12
12
|
const utils_1 = require("../../utils");
|
|
13
|
+
const utils_2 = require("../../../utils");
|
|
13
14
|
const release_notes_1 = require("./release-notes");
|
|
14
15
|
function formatNumber(num) {
|
|
15
16
|
if (!num)
|
|
@@ -88,6 +89,29 @@ function buildPackageInfoSections(state, modalWidth, activeTab) {
|
|
|
88
89
|
required: true,
|
|
89
90
|
behavior: 'pinned',
|
|
90
91
|
});
|
|
92
|
+
const warningRows = [];
|
|
93
|
+
const warningContentWidth = Math.max(10, modalWidth - 4);
|
|
94
|
+
if (state.deprecated) {
|
|
95
|
+
// Wrap (don't truncate) so a deprecation URL stays whole and clickable —
|
|
96
|
+
// truncation with "..." produces a dead link. `wrapPlainText` breaks on
|
|
97
|
+
// spaces only, so the URL keeps its own intact line. No emoji marker:
|
|
98
|
+
// `getVisualLength` scores glyphs like ⚠ as width 2 while many terminals
|
|
99
|
+
// render them as width 1, which throws off the modal's border alignment.
|
|
100
|
+
for (const line of (0, utils_1.wrapPlainText)(`Deprecated: ${state.deprecated}`, warningContentWidth)) {
|
|
101
|
+
warningRows.push((0, themes_colors_1.getThemeColor)('warning')(line));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const engineWarning = (0, utils_2.checkNodeEngineCompatibility)(state.enginesNode);
|
|
105
|
+
if (engineWarning) {
|
|
106
|
+
warningRows.push((0, themes_colors_1.getThemeColor)('warning')(`Hold: ${engineWarning}`));
|
|
107
|
+
}
|
|
108
|
+
if (warningRows.length > 0) {
|
|
109
|
+
sections.push({
|
|
110
|
+
key: 'warnings',
|
|
111
|
+
rows: warningRows,
|
|
112
|
+
behavior: 'pinned',
|
|
113
|
+
});
|
|
114
|
+
}
|
|
91
115
|
if (state.homepage) {
|
|
92
116
|
sections.push({
|
|
93
117
|
key: 'homepage',
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getHealthBadge = getHealthBadge;
|
|
4
|
+
const utils_1 = require("../../utils");
|
|
5
|
+
const themes_colors_1 = require("../themes-colors");
|
|
6
|
+
/**
|
|
7
|
+
* A compact badge flagging a package's health in the list:
|
|
8
|
+
* - `[DEPR]` when the latest version is deprecated (highest priority), or
|
|
9
|
+
* - `[ENG]` when its `engines.node` is incompatible with the running Node.
|
|
10
|
+
*
|
|
11
|
+
* Returns an empty string when neither applies. Deprecation wins because an
|
|
12
|
+
* engines mismatch on an abandoned package is moot. Both render in the theme's
|
|
13
|
+
* (amber) warning color — a caution signal, not an alarm.
|
|
14
|
+
*/
|
|
15
|
+
function getHealthBadge(state) {
|
|
16
|
+
if (state.deprecated) {
|
|
17
|
+
return (0, themes_colors_1.getThemeColor)('warning')('[DEPR]');
|
|
18
|
+
}
|
|
19
|
+
if ((0, utils_1.checkNodeEngineCompatibility)(state.enginesNode)) {
|
|
20
|
+
return (0, themes_colors_1.getThemeColor)('warning')('[ENG]');
|
|
21
|
+
}
|
|
22
|
+
return '';
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=health.js.map
|
|
@@ -11,6 +11,7 @@ const chalk_1 = __importDefault(require("chalk"));
|
|
|
11
11
|
const utils_1 = require("../../utils");
|
|
12
12
|
const themes_colors_1 = require("../../themes-colors");
|
|
13
13
|
const vulnerability_1 = require("../../presenters/vulnerability");
|
|
14
|
+
const health_1 = require("../../presenters/health");
|
|
14
15
|
function padLineToWidth(line, terminalWidth) {
|
|
15
16
|
const padding = Math.max(0, terminalWidth - utils_1.VersionUtils.getVisualLength(line));
|
|
16
17
|
return line + ' '.repeat(padding);
|
|
@@ -118,15 +119,19 @@ function renderPackageLine(state, _index, isCurrentRow, terminalWidth = 80, opti
|
|
|
118
119
|
const shouldShowVulnerability = (0, vulnerability_1.shouldDisplayVulnerabilityForDependency)(state.type, options);
|
|
119
120
|
const vulnBadge = shouldShowVulnerability ? (0, vulnerability_1.getVulnerabilityBadge)(state.vulnerability) : '';
|
|
120
121
|
const vulnBadgeWidth = vulnBadge ? utils_1.VersionUtils.getVisualLength(vulnBadge) + 1 : 0;
|
|
122
|
+
// Deprecation / engines-incompatibility marker (independent of dep type).
|
|
123
|
+
const healthBadge = (0, health_1.getHealthBadge)(state);
|
|
124
|
+
const healthBadgeWidth = healthBadge ? utils_1.VersionUtils.getVisualLength(healthBadge) + 1 : 0;
|
|
121
125
|
const nameLength = utils_1.VersionUtils.getVisualLength(truncatedName);
|
|
122
|
-
const namePadding = Math.max(0, packageNameWidth - nameLength - 1 - badgeWidth - vulnBadgeWidth);
|
|
126
|
+
const namePadding = Math.max(0, packageNameWidth - nameLength - 1 - badgeWidth - vulnBadgeWidth - healthBadgeWidth);
|
|
123
127
|
const nameDashes = shouldShowDashes(namePadding)
|
|
124
128
|
? dashColor('-').repeat(namePadding)
|
|
125
129
|
: ' '.repeat(namePadding);
|
|
126
130
|
const vulnSuffix = vulnBadge ? ` ${vulnBadge}` : '';
|
|
131
|
+
const healthSuffix = healthBadge ? ` ${healthBadge}` : '';
|
|
127
132
|
const packageNameSection = typeBadge
|
|
128
|
-
? `${displayName} ${nameDashes}${vulnSuffix}${typeBadge}`
|
|
129
|
-
: `${displayName} ${nameDashes}${vulnSuffix}`;
|
|
133
|
+
? `${displayName} ${nameDashes}${vulnSuffix}${healthSuffix}${typeBadge}`
|
|
134
|
+
: `${displayName} ${nameDashes}${vulnSuffix}${healthSuffix}`;
|
|
130
135
|
const currentSection = `${currentDot} ${currentVersion}`;
|
|
131
136
|
const currentSectionLength = utils_1.VersionUtils.getVisualLength(currentSection) + 1;
|
|
132
137
|
const currentPadding = Math.max(0, currentColumnWidth - currentSectionLength);
|