inup 1.5.2 → 1.5.4
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/core/package-detector.js +42 -40
- package/dist/interactive-ui.js +13 -3
- package/dist/services/npm-registry.js +113 -75
- package/dist/ui/input-handler.js +9 -2
- package/dist/ui/modal/package-info-sections.js +58 -13
- package/dist/ui/modal/package-info.js +2 -2
- package/dist/ui/renderer/index.js +2 -2
- package/dist/ui/state/modal-manager.js +13 -0
- package/dist/ui/state/state-manager.js +11 -0
- package/package.json +1 -1
|
@@ -45,8 +45,8 @@ class PackageDetector {
|
|
|
45
45
|
constructor(options) {
|
|
46
46
|
this.packageJsonPath = null;
|
|
47
47
|
this.packageJson = null;
|
|
48
|
-
this.batchSize =
|
|
49
|
-
this.
|
|
48
|
+
this.batchSize = 10;
|
|
49
|
+
this.maxConcurrency = 10;
|
|
50
50
|
this.cwd = options?.cwd || process.cwd();
|
|
51
51
|
this.excludePatterns = options?.excludePatterns || [];
|
|
52
52
|
this.ignorePackages = options?.ignorePackages || [];
|
|
@@ -95,45 +95,47 @@ class PackageDetector {
|
|
|
95
95
|
let lastBatchEndAt = Date.now();
|
|
96
96
|
const tFetch = Date.now();
|
|
97
97
|
utils_3.debugLog.info('PackageDetector', 'fetching version data via npm registry in batches');
|
|
98
|
-
await (0, services_1.
|
|
99
|
-
|
|
100
|
-
let batchFailedCount = 0;
|
|
101
|
-
const batchItems = batch.map((batchItem) => {
|
|
102
|
-
const packageInfo = this.resolvePackageGroup(batchItem.packageName, prepared.allDependencies, batchItem.data);
|
|
103
|
-
packageLookup.set(batchItem.packageName, packageInfo);
|
|
104
|
-
resolved++;
|
|
105
|
-
const isFailed = batchItem.data.latestVersion === 'unknown';
|
|
106
|
-
if (isFailed) {
|
|
107
|
-
failed++;
|
|
108
|
-
batchFailedCount++;
|
|
109
|
-
performanceTracker.recordFailedPackage(batchItem.packageName);
|
|
110
|
-
}
|
|
111
|
-
return {
|
|
112
|
-
packageName: batchItem.packageName,
|
|
113
|
-
packageInfo,
|
|
114
|
-
failed: isFailed,
|
|
115
|
-
};
|
|
116
|
-
});
|
|
117
|
-
const batchEnd = Date.now();
|
|
118
|
-
performanceTracker.recordBatch({
|
|
119
|
-
index: batchIndex++,
|
|
120
|
-
size: batch.length,
|
|
121
|
-
durationMs: batchEnd - batchStart,
|
|
122
|
-
failedCount: batchFailedCount,
|
|
123
|
-
});
|
|
124
|
-
lastBatchEndAt = batchEnd;
|
|
125
|
-
performanceTracker.recordCounts({ resolved, failed });
|
|
126
|
-
const progress = this.createProgressSnapshot(prepared.uniquePackages.length, resolved, failed, resolved < prepared.uniquePackages.length);
|
|
127
|
-
onEvent({
|
|
128
|
-
type: 'batch',
|
|
129
|
-
payload: {
|
|
130
|
-
batch: batchItems,
|
|
131
|
-
progress,
|
|
132
|
-
},
|
|
133
|
-
});
|
|
134
|
-
}, prepared.currentVersions, {
|
|
98
|
+
await (0, services_1.fetchPackageVersions)(prepared.uniquePackages, {
|
|
99
|
+
currentVersions: prepared.currentVersions,
|
|
135
100
|
batchSize: this.batchSize,
|
|
136
|
-
|
|
101
|
+
maxConcurrency: this.maxConcurrency,
|
|
102
|
+
onBatchReady: (batch) => {
|
|
103
|
+
const batchStart = lastBatchEndAt;
|
|
104
|
+
let batchFailedCount = 0;
|
|
105
|
+
const batchItems = batch.map((batchItem) => {
|
|
106
|
+
const packageInfo = this.resolvePackageGroup(batchItem.packageName, prepared.allDependencies, batchItem.data);
|
|
107
|
+
packageLookup.set(batchItem.packageName, packageInfo);
|
|
108
|
+
resolved++;
|
|
109
|
+
const isFailed = batchItem.data.latestVersion === 'unknown';
|
|
110
|
+
if (isFailed) {
|
|
111
|
+
failed++;
|
|
112
|
+
batchFailedCount++;
|
|
113
|
+
performanceTracker.recordFailedPackage(batchItem.packageName);
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
packageName: batchItem.packageName,
|
|
117
|
+
packageInfo,
|
|
118
|
+
failed: isFailed,
|
|
119
|
+
};
|
|
120
|
+
});
|
|
121
|
+
const batchEnd = Date.now();
|
|
122
|
+
performanceTracker.recordBatch({
|
|
123
|
+
index: batchIndex++,
|
|
124
|
+
size: batch.length,
|
|
125
|
+
durationMs: batchEnd - batchStart,
|
|
126
|
+
failedCount: batchFailedCount,
|
|
127
|
+
});
|
|
128
|
+
lastBatchEndAt = batchEnd;
|
|
129
|
+
performanceTracker.recordCounts({ resolved, failed });
|
|
130
|
+
const progress = this.createProgressSnapshot(prepared.uniquePackages.length, resolved, failed, resolved < prepared.uniquePackages.length);
|
|
131
|
+
onEvent({
|
|
132
|
+
type: 'batch',
|
|
133
|
+
payload: {
|
|
134
|
+
batch: batchItems,
|
|
135
|
+
progress,
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
},
|
|
137
139
|
});
|
|
138
140
|
utils_3.debugLog.perf('PackageDetector', `registry fetch (${resolved}/${prepared.uniquePackages.length} resolved)`, tFetch);
|
|
139
141
|
performanceTracker.recordPhaseDuration('registryFetch', Date.now() - tFetch);
|
package/dist/interactive-ui.js
CHANGED
|
@@ -348,6 +348,11 @@ class InteractiveUI {
|
|
|
348
348
|
return;
|
|
349
349
|
}
|
|
350
350
|
break;
|
|
351
|
+
case 'switch_info_modal_tab': {
|
|
352
|
+
const nextTab = stateManager.getInfoModalTab() === 'info' ? 'usedBy' : 'info';
|
|
353
|
+
stateManager.setInfoModalTab(nextTab);
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
351
356
|
case 'navigate_info_modal_version':
|
|
352
357
|
{
|
|
353
358
|
if (uiState.infoModalRow >= 0 && uiState.infoModalRow < filteredStates.length) {
|
|
@@ -553,15 +558,20 @@ class InteractiveUI {
|
|
|
553
558
|
}
|
|
554
559
|
else {
|
|
555
560
|
// Show full info with scroll support
|
|
556
|
-
const
|
|
561
|
+
const activeTab = uiState.infoModalTab;
|
|
562
|
+
const result = this.renderer.renderPackageInfoModal(selectedState, terminalWidth, Math.max(8, terminalHeight - 4), uiState.infoModalScrollOffset, activeTab);
|
|
557
563
|
infoModalMaxScrollOffset = result.maxScrollOffset;
|
|
558
564
|
stateManager.clampInfoModalScrollOffset(infoModalMaxScrollOffset);
|
|
559
565
|
const scrollHint = result.usesInternalScroll && result.maxScrollOffset > 0
|
|
560
566
|
? chalk_1.default.bold.white('↑/↓ ') + chalk_1.default.gray('Scroll · ')
|
|
561
567
|
: '';
|
|
568
|
+
const versionHint = activeTab === 'info'
|
|
569
|
+
? chalk_1.default.bold.white('←/→ ') + chalk_1.default.gray('Version · ')
|
|
570
|
+
: '';
|
|
562
571
|
renderModalViewport('info-modal', scrollHint +
|
|
563
|
-
|
|
564
|
-
chalk_1.default.
|
|
572
|
+
versionHint +
|
|
573
|
+
chalk_1.default.bold.white('Tab ') +
|
|
574
|
+
chalk_1.default.gray('Switch tab · ') +
|
|
565
575
|
chalk_1.default.bold.white('I / Esc ') +
|
|
566
576
|
chalk_1.default.gray('Exit this view'), result.lines, terminalWidth, terminalHeight, bgCode);
|
|
567
577
|
}
|
|
@@ -33,8 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.
|
|
37
|
-
exports.getAllPackageDataBatched = getAllPackageDataBatched;
|
|
36
|
+
exports.fetchPackageVersions = fetchPackageVersions;
|
|
38
37
|
exports.clearPackageCache = clearPackageCache;
|
|
39
38
|
exports.closeRegistryPool = closeRegistryPool;
|
|
40
39
|
const semver = __importStar(require("semver"));
|
|
@@ -46,19 +45,25 @@ const inflateAsync = (0, node_util_1.promisify)(node_zlib_1.inflate);
|
|
|
46
45
|
const brotliDecompressAsync = (0, node_util_1.promisify)(node_zlib_1.brotliDecompress);
|
|
47
46
|
const config_1 = require("../config");
|
|
48
47
|
const jsdelivr_registry_1 = require("./jsdelivr-registry");
|
|
49
|
-
const utils_1 = require("../ui/utils");
|
|
50
48
|
const inFlightLookups = new Map();
|
|
51
49
|
const registryOrigin = new URL(config_1.NPM_REGISTRY_URL).origin;
|
|
52
50
|
const registryPathPrefix = new URL(config_1.NPM_REGISTRY_URL).pathname.replace(/\/$/, '');
|
|
51
|
+
// Few connections + many requests per connection = maximum keep-alive reuse.
|
|
52
|
+
// No per-request timeouts: correctness matters more than speed for a CLI that
|
|
53
|
+
// runs on demand. Slow responses are tolerated; only true errors cause retry.
|
|
53
54
|
const registryPool = new undici_1.Pool(registryOrigin, {
|
|
54
|
-
connections:
|
|
55
|
-
pipelining:
|
|
55
|
+
connections: 6,
|
|
56
|
+
pipelining: 1,
|
|
56
57
|
keepAliveTimeout: 30000,
|
|
57
58
|
keepAliveMaxTimeout: 600000,
|
|
58
|
-
headersTimeout:
|
|
59
|
-
bodyTimeout:
|
|
59
|
+
headersTimeout: 0,
|
|
60
|
+
bodyTimeout: 0,
|
|
61
|
+
connectTimeout: 15000,
|
|
60
62
|
allowH2: false,
|
|
61
63
|
});
|
|
64
|
+
const MAX_REGISTRY_ATTEMPTS = 3;
|
|
65
|
+
const RETRY_BACKOFF_MS = [500, 1500, 3000];
|
|
66
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
62
67
|
const isRetryableStatus = (statusCode) => statusCode === 408 || statusCode === 429 || statusCode >= 500;
|
|
63
68
|
const isTransientNetworkError = (error) => {
|
|
64
69
|
if (!(error instanceof Error)) {
|
|
@@ -104,12 +109,14 @@ function parseVersions(raw) {
|
|
|
104
109
|
const latestVersion = sortedVersions.length > 0 ? sortedVersions[0] : 'unknown';
|
|
105
110
|
return { latestVersion, allVersions };
|
|
106
111
|
}
|
|
107
|
-
|
|
112
|
+
const encodeRegistryPath = (packageName) => {
|
|
113
|
+
const encodedName = packageName.startsWith('@')
|
|
114
|
+
? `@${encodeURIComponent(packageName.slice(1).split('/')[0])}/${encodeURIComponent(packageName.slice(packageName.indexOf('/') + 1))}`
|
|
115
|
+
: encodeURIComponent(packageName);
|
|
116
|
+
return `${registryPathPrefix}/${encodedName}`;
|
|
117
|
+
};
|
|
118
|
+
async function attemptRegistryFetch(path) {
|
|
108
119
|
try {
|
|
109
|
-
const encodedName = packageName.startsWith('@')
|
|
110
|
-
? `@${encodeURIComponent(packageName.slice(1).split('/')[0])}/${encodeURIComponent(packageName.slice(packageName.indexOf('/') + 1))}`
|
|
111
|
-
: encodeURIComponent(packageName);
|
|
112
|
-
const path = `${registryPathPrefix}/${encodedName}`;
|
|
113
120
|
const { statusCode, headers, body } = await registryPool.request({
|
|
114
121
|
path,
|
|
115
122
|
method: 'GET',
|
|
@@ -117,16 +124,16 @@ async function fetchPackageFromRegistryWithFallback(packageName, currentVersion)
|
|
|
117
124
|
accept: 'application/vnd.npm.install-v1+json',
|
|
118
125
|
'accept-encoding': 'gzip, deflate, br',
|
|
119
126
|
},
|
|
120
|
-
headersTimeout:
|
|
121
|
-
bodyTimeout:
|
|
127
|
+
headersTimeout: 0,
|
|
128
|
+
bodyTimeout: 0,
|
|
122
129
|
blocking: false,
|
|
123
130
|
});
|
|
124
131
|
if (statusCode < 200 || statusCode >= 300) {
|
|
125
|
-
await body.dump();
|
|
132
|
+
await body.dump().catch(() => undefined);
|
|
126
133
|
if (isRetryableStatus(statusCode)) {
|
|
127
|
-
return
|
|
134
|
+
return { kind: 'retryable' };
|
|
128
135
|
}
|
|
129
|
-
return {
|
|
136
|
+
return { kind: 'not-found' };
|
|
130
137
|
}
|
|
131
138
|
const raw = Buffer.from(await body.arrayBuffer());
|
|
132
139
|
const encodingHeader = headers['content-encoding'];
|
|
@@ -146,97 +153,128 @@ async function fetchPackageFromRegistryWithFallback(packageName, currentVersion)
|
|
|
146
153
|
else {
|
|
147
154
|
decoded = raw;
|
|
148
155
|
}
|
|
149
|
-
return parseVersions(decoded.toString('utf8'));
|
|
156
|
+
return { kind: 'success', data: parseVersions(decoded.toString('utf8')) };
|
|
150
157
|
}
|
|
151
158
|
catch (error) {
|
|
152
159
|
if (isTransientNetworkError(error)) {
|
|
153
|
-
return
|
|
160
|
+
return { kind: 'transient' };
|
|
154
161
|
}
|
|
155
|
-
|
|
162
|
+
// Unknown error: treat as transient so we try the fallback rather than
|
|
163
|
+
// silently returning 'unknown'.
|
|
164
|
+
return { kind: 'transient' };
|
|
156
165
|
}
|
|
157
166
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
let completedCount = 0;
|
|
170
|
-
const allPromises = packageNames.map(async (packageName) => {
|
|
171
|
-
const data = await getFreshPackageData(packageName, currentVersions?.get(packageName));
|
|
172
|
-
packageData.set(packageName, data);
|
|
173
|
-
completedCount++;
|
|
174
|
-
if (onProgress) {
|
|
175
|
-
onProgress(packageName, completedCount, total);
|
|
167
|
+
async function fetchFromRegistryWithRetries(path) {
|
|
168
|
+
let lastOutcome = { kind: 'transient' };
|
|
169
|
+
for (let attempt = 0; attempt < MAX_REGISTRY_ATTEMPTS; attempt++) {
|
|
170
|
+
const outcome = await attemptRegistryFetch(path);
|
|
171
|
+
if (outcome.kind === 'success' || outcome.kind === 'not-found') {
|
|
172
|
+
return outcome;
|
|
173
|
+
}
|
|
174
|
+
lastOutcome = outcome;
|
|
175
|
+
if (attempt < MAX_REGISTRY_ATTEMPTS - 1) {
|
|
176
|
+
const backoff = RETRY_BACKOFF_MS[Math.min(attempt, RETRY_BACKOFF_MS.length - 1)];
|
|
177
|
+
await sleep(backoff);
|
|
176
178
|
}
|
|
177
|
-
});
|
|
178
|
-
await Promise.all(allPromises);
|
|
179
|
-
if (!onProgress) {
|
|
180
|
-
utils_1.ConsoleUtils.clearProgress();
|
|
181
179
|
}
|
|
182
|
-
return
|
|
180
|
+
return lastOutcome;
|
|
183
181
|
}
|
|
184
|
-
async function
|
|
185
|
-
|
|
186
|
-
|
|
182
|
+
async function fetchPackageFromRegistryWithFallback(packageName, currentVersion) {
|
|
183
|
+
const path = encodeRegistryPath(packageName);
|
|
184
|
+
const outcome = await fetchFromRegistryWithRetries(path);
|
|
185
|
+
if (outcome.kind === 'success') {
|
|
186
|
+
return outcome.data;
|
|
187
187
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
};
|
|
196
|
-
await Promise.all(Array.from({ length: limit }, () => runWorker()));
|
|
188
|
+
if (outcome.kind === 'not-found') {
|
|
189
|
+
return { latestVersion: 'unknown', allVersions: [] };
|
|
190
|
+
}
|
|
191
|
+
// Only reach here after exhausted retries against real errors — try jsdelivr
|
|
192
|
+
// as last-resort safety net so we don't silently return 'unknown'.
|
|
193
|
+
const fallback = await fetchFromJsdelivrFallback(packageName, currentVersion).catch(() => null);
|
|
194
|
+
return fallback ?? { latestVersion: 'unknown', allVersions: [] };
|
|
197
195
|
}
|
|
198
|
-
|
|
196
|
+
/**
|
|
197
|
+
* Fetches version data for a list of packages from the npm registry.
|
|
198
|
+
*
|
|
199
|
+
* Concurrency model:
|
|
200
|
+
* - `maxConcurrency` is a global cap on in-flight fetches at any moment.
|
|
201
|
+
* It doesn't interact with batch size — batches exist only to group
|
|
202
|
+
* emissions for the UI.
|
|
203
|
+
* - No per-request timeouts: slow responses are allowed to finish. Real
|
|
204
|
+
* network errors are retried with exponential backoff; after that, we
|
|
205
|
+
* fall back to jsdelivr as a last resort. A result is never silently
|
|
206
|
+
* dropped due to slowness.
|
|
207
|
+
*
|
|
208
|
+
* Callbacks:
|
|
209
|
+
* - `onBatchReady` fires once a whole emission batch has resolved, in
|
|
210
|
+
* original batch order.
|
|
211
|
+
*/
|
|
212
|
+
async function fetchPackageVersions(packageNames, options = {}) {
|
|
199
213
|
const packageData = new Map();
|
|
200
214
|
if (packageNames.length === 0) {
|
|
201
215
|
return packageData;
|
|
202
216
|
}
|
|
203
217
|
const batchSizes = options.batchSizes && options.batchSizes.length > 0
|
|
204
218
|
? options.batchSizes.map((size) => Math.max(1, size))
|
|
205
|
-
: [Math.max(1, options.batchSize ??
|
|
206
|
-
const
|
|
219
|
+
: [Math.max(1, options.batchSize ?? 25)];
|
|
220
|
+
const maxConcurrency = Math.max(1, options.maxConcurrency ?? 10);
|
|
207
221
|
const total = packageNames.length;
|
|
208
222
|
let completedCount = 0;
|
|
209
|
-
let batchStart = 0;
|
|
210
|
-
let batchIndex = 0;
|
|
211
|
-
const batchPromises = [];
|
|
212
223
|
const pendingEmissions = new Map();
|
|
213
224
|
let nextEmitIndex = 0;
|
|
214
225
|
const flushPending = () => {
|
|
215
226
|
while (pendingEmissions.has(nextEmitIndex)) {
|
|
216
227
|
const ready = pendingEmissions.get(nextEmitIndex);
|
|
217
228
|
pendingEmissions.delete(nextEmitIndex);
|
|
218
|
-
onBatchReady?.(ready);
|
|
229
|
+
options.onBatchReady?.(ready);
|
|
219
230
|
nextEmitIndex++;
|
|
220
231
|
}
|
|
221
232
|
};
|
|
233
|
+
// Global semaphore: `maxConcurrency` is the total in-flight cap across
|
|
234
|
+
// all batches. Batches don't gate concurrency — only emission order.
|
|
235
|
+
let inFlight = 0;
|
|
236
|
+
const waiters = [];
|
|
237
|
+
const acquire = async () => {
|
|
238
|
+
if (inFlight < maxConcurrency) {
|
|
239
|
+
inFlight++;
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
await new Promise((resolve) => waiters.push(resolve));
|
|
243
|
+
inFlight++;
|
|
244
|
+
};
|
|
245
|
+
const release = () => {
|
|
246
|
+
inFlight--;
|
|
247
|
+
const next = waiters.shift();
|
|
248
|
+
if (next)
|
|
249
|
+
next();
|
|
250
|
+
};
|
|
251
|
+
const batchPromises = [];
|
|
252
|
+
let batchStart = 0;
|
|
253
|
+
let batchIndex = 0;
|
|
222
254
|
while (batchStart < packageNames.length) {
|
|
223
255
|
const batchSize = batchSizes[Math.min(batchIndex, batchSizes.length - 1)];
|
|
224
256
|
const batchNames = packageNames.slice(batchStart, batchStart + batchSize);
|
|
225
257
|
const capturedBatchIndex = batchIndex;
|
|
226
258
|
const batchResults = new Array(batchNames.length);
|
|
227
|
-
const batchPromise =
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
259
|
+
const batchPromise = Promise.all(batchNames.map(async (packageName, itemIndex) => {
|
|
260
|
+
await acquire();
|
|
261
|
+
try {
|
|
262
|
+
const data = await getFreshPackageData(packageName, options.currentVersions?.get(packageName));
|
|
263
|
+
packageData.set(packageName, data);
|
|
264
|
+
completedCount++;
|
|
265
|
+
batchResults[itemIndex] = {
|
|
266
|
+
packageName,
|
|
267
|
+
data,
|
|
268
|
+
completed: completedCount,
|
|
269
|
+
total,
|
|
270
|
+
batchIndex: capturedBatchIndex,
|
|
271
|
+
itemIndex,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
finally {
|
|
275
|
+
release();
|
|
276
|
+
}
|
|
277
|
+
})).then(() => {
|
|
240
278
|
pendingEmissions.set(capturedBatchIndex, batchResults.filter(Boolean));
|
|
241
279
|
flushPending();
|
|
242
280
|
});
|
package/dist/ui/input-handler.js
CHANGED
|
@@ -81,6 +81,9 @@ class InputHandler {
|
|
|
81
81
|
case 'I':
|
|
82
82
|
this.onAction({ type: 'toggle_info_modal' });
|
|
83
83
|
return;
|
|
84
|
+
case 'tab':
|
|
85
|
+
this.onAction({ type: 'switch_info_modal_tab' });
|
|
86
|
+
return;
|
|
84
87
|
case 'up':
|
|
85
88
|
this.onAction({ type: 'scroll_info_modal_up' });
|
|
86
89
|
return;
|
|
@@ -88,10 +91,14 @@ class InputHandler {
|
|
|
88
91
|
this.onAction({ type: 'scroll_info_modal_down' });
|
|
89
92
|
return;
|
|
90
93
|
case 'left':
|
|
91
|
-
|
|
94
|
+
if (uiState.infoModalTab === 'info') {
|
|
95
|
+
this.onAction({ type: 'navigate_info_modal_version', direction: 'newer' });
|
|
96
|
+
}
|
|
92
97
|
return;
|
|
93
98
|
case 'right':
|
|
94
|
-
|
|
99
|
+
if (uiState.infoModalTab === 'info') {
|
|
100
|
+
this.onAction({ type: 'navigate_info_modal_version', direction: 'older' });
|
|
101
|
+
}
|
|
95
102
|
return;
|
|
96
103
|
default:
|
|
97
104
|
return; // Consume all other keys while modal is open
|
|
@@ -4,7 +4,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.buildReleaseNotesSections = buildReleaseNotesSections;
|
|
7
|
+
exports.buildUsedBySections = buildUsedBySections;
|
|
7
8
|
exports.buildPackageInfoSections = buildPackageInfoSections;
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
8
10
|
const chalk_1 = __importDefault(require("chalk"));
|
|
9
11
|
const themes_colors_1 = require("../themes-colors");
|
|
10
12
|
const vulnerability_1 = require("../presenters/vulnerability");
|
|
@@ -240,8 +242,47 @@ function formatNumber(num) {
|
|
|
240
242
|
return (num / 1000).toFixed(1) + 'K';
|
|
241
243
|
return num.toString();
|
|
242
244
|
}
|
|
243
|
-
function
|
|
244
|
-
|
|
245
|
+
function getUsedByPaths(state) {
|
|
246
|
+
return state.packageJsonPaths ?? [state.packageJsonPath];
|
|
247
|
+
}
|
|
248
|
+
function buildTabBarSuffix(activeTab, usedByCount) {
|
|
249
|
+
const styleFor = (tab) => tab === activeTab ? chalk_1.default.bold.underline : chalk_1.default.gray;
|
|
250
|
+
const usedByLabel = `Used by${usedByCount > 0 ? ` (${usedByCount})` : ''}`;
|
|
251
|
+
return (' ' +
|
|
252
|
+
chalk_1.default.gray('[ ') +
|
|
253
|
+
styleFor('info')('Info') +
|
|
254
|
+
chalk_1.default.gray(' │ ') +
|
|
255
|
+
styleFor('usedBy')(usedByLabel) +
|
|
256
|
+
chalk_1.default.gray(' ]'));
|
|
257
|
+
}
|
|
258
|
+
function buildUsedBySections(state, modalWidth) {
|
|
259
|
+
const paths = getUsedByPaths(state);
|
|
260
|
+
const cwd = process.cwd();
|
|
261
|
+
const contentWidth = Math.max(10, modalWidth - 4);
|
|
262
|
+
const formatRelative = (absolutePath) => {
|
|
263
|
+
const display = node_path_1.default.relative(cwd, absolutePath) || absolutePath;
|
|
264
|
+
return (0, utils_1.truncatePlainText)(display, contentWidth);
|
|
265
|
+
};
|
|
266
|
+
return [
|
|
267
|
+
{
|
|
268
|
+
key: 'used-by-summary',
|
|
269
|
+
rows: [
|
|
270
|
+
chalk_1.default.bold(`${paths.length} package.json file${paths.length === 1 ? '' : 's'} depend on ${state.name}`),
|
|
271
|
+
chalk_1.default.gray(`Type: ${state.type}`),
|
|
272
|
+
],
|
|
273
|
+
required: true,
|
|
274
|
+
behavior: 'pinned',
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
key: 'used-by-list',
|
|
278
|
+
rows: paths.map((p) => `${chalk_1.default.gray('•')} ${formatRelative(p)}`),
|
|
279
|
+
behavior: 'body',
|
|
280
|
+
},
|
|
281
|
+
];
|
|
282
|
+
}
|
|
283
|
+
function buildPackageInfoSections(state, modalWidth, activeTab) {
|
|
284
|
+
const title = chalk_1.default.cyan.bold(`Package: ${state.name}`) +
|
|
285
|
+
buildTabBarSuffix(activeTab, getUsedByPaths(state).length);
|
|
245
286
|
const authorLicense = chalk_1.default.gray(`${state.author || 'Unknown'} • ${state.license || 'MIT'}`);
|
|
246
287
|
const currentVersion = chalk_1.default.yellow(state.currentVersionSpecifier);
|
|
247
288
|
const targetVersion = chalk_1.default.green(state.selectedOption === 'range' ? state.rangeVersion : state.latestVersion);
|
|
@@ -252,18 +293,22 @@ function buildPackageInfoSections(state, modalWidth) {
|
|
|
252
293
|
required: true,
|
|
253
294
|
behavior: 'pinned',
|
|
254
295
|
},
|
|
255
|
-
{
|
|
256
|
-
key: 'meta',
|
|
257
|
-
rows: [
|
|
258
|
-
`Current: ${currentVersion} Target: ${targetVersion}`,
|
|
259
|
-
...(state.weeklyDownloads !== undefined
|
|
260
|
-
? [(0, themes_colors_1.getThemeColor)('primary')(`Downloads/week: ${formatNumber(state.weeklyDownloads)}`)]
|
|
261
|
-
: []),
|
|
262
|
-
],
|
|
263
|
-
required: true,
|
|
264
|
-
behavior: 'pinned',
|
|
265
|
-
},
|
|
266
296
|
];
|
|
297
|
+
if (activeTab === 'usedBy') {
|
|
298
|
+
sections.push(...buildUsedBySections(state, modalWidth));
|
|
299
|
+
return sections;
|
|
300
|
+
}
|
|
301
|
+
sections.push({
|
|
302
|
+
key: 'meta',
|
|
303
|
+
rows: [
|
|
304
|
+
`Current: ${currentVersion} Target: ${targetVersion}`,
|
|
305
|
+
...(state.weeklyDownloads !== undefined
|
|
306
|
+
? [(0, themes_colors_1.getThemeColor)('primary')(`Downloads/week: ${formatNumber(state.weeklyDownloads)}`)]
|
|
307
|
+
: []),
|
|
308
|
+
],
|
|
309
|
+
required: true,
|
|
310
|
+
behavior: 'pinned',
|
|
311
|
+
});
|
|
267
312
|
if (state.homepage) {
|
|
268
313
|
sections.push({
|
|
269
314
|
key: 'homepage',
|
|
@@ -29,9 +29,9 @@ function renderPackageInfoLoading(state, terminalWidth = 80, terminalHeight = 24
|
|
|
29
29
|
usesInternalScroll: false,
|
|
30
30
|
};
|
|
31
31
|
}
|
|
32
|
-
function renderPackageInfoModal(state, terminalWidth = 80, terminalHeight = 24, scrollOffset = 0) {
|
|
32
|
+
function renderPackageInfoModal(state, terminalWidth = 80, terminalHeight = 24, scrollOffset = 0, activeTab = 'info') {
|
|
33
33
|
const modalWidth = (0, layout_1.getModalWidth)(terminalWidth, 60, 120);
|
|
34
|
-
const allSections = (0, package_info_sections_1.buildPackageInfoSections)(state, modalWidth);
|
|
34
|
+
const allSections = (0, package_info_sections_1.buildPackageInfoSections)(state, modalWidth, activeTab);
|
|
35
35
|
const maxHeight = Math.max(10, terminalHeight - 2);
|
|
36
36
|
const trimOrder = ['homepage', 'changelog', 'description'];
|
|
37
37
|
const compactSections = (0, layout_1.fitModalSections)(allSections, maxHeight, trimOrder);
|
|
@@ -65,8 +65,8 @@ class UIRenderer {
|
|
|
65
65
|
renderPackageInfoLoading(state, terminalWidth = 80, terminalHeight = 24) {
|
|
66
66
|
return Modal.renderPackageInfoLoading(state, terminalWidth, terminalHeight);
|
|
67
67
|
}
|
|
68
|
-
renderPackageInfoModal(state, terminalWidth = 80, terminalHeight = 24, scrollOffset = 0) {
|
|
69
|
-
return Modal.renderPackageInfoModal(state, terminalWidth, terminalHeight, scrollOffset);
|
|
68
|
+
renderPackageInfoModal(state, terminalWidth = 80, terminalHeight = 24, scrollOffset = 0, activeTab = 'info') {
|
|
69
|
+
return Modal.renderPackageInfoModal(state, terminalWidth, terminalHeight, scrollOffset, activeTab);
|
|
70
70
|
}
|
|
71
71
|
renderThemeSelectorModal(currentTheme, previewTheme, terminalWidth = 80, terminalHeight = 24) {
|
|
72
72
|
return Modal.renderThemeSelectorModal(currentTheme, previewTheme, terminalWidth, terminalHeight);
|
|
@@ -9,10 +9,21 @@ class ModalManager {
|
|
|
9
9
|
isLoadingModalInfo: false,
|
|
10
10
|
infoModalScrollOffset: 0,
|
|
11
11
|
infoModalSessionId: 0,
|
|
12
|
+
infoModalTab: 'info',
|
|
12
13
|
showDebugModal: false,
|
|
13
14
|
debugModalScrollOffset: 0,
|
|
14
15
|
};
|
|
15
16
|
}
|
|
17
|
+
getInfoModalTab() {
|
|
18
|
+
return this.state.infoModalTab;
|
|
19
|
+
}
|
|
20
|
+
setInfoModalTab(tab) {
|
|
21
|
+
if (this.state.infoModalTab === tab)
|
|
22
|
+
return false;
|
|
23
|
+
this.state.infoModalTab = tab;
|
|
24
|
+
this.state.infoModalScrollOffset = 0;
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
16
27
|
isDebugModalOpen() {
|
|
17
28
|
return this.state.showDebugModal;
|
|
18
29
|
}
|
|
@@ -99,6 +110,7 @@ class ModalManager {
|
|
|
99
110
|
this.state.infoModalRow = currentRow;
|
|
100
111
|
this.state.infoModalScrollOffset = 0;
|
|
101
112
|
this.state.isLoadingModalInfo = false;
|
|
113
|
+
this.state.infoModalTab = 'info';
|
|
102
114
|
this.state.infoModalSessionId += 1;
|
|
103
115
|
return this.state.infoModalSessionId;
|
|
104
116
|
}
|
|
@@ -107,6 +119,7 @@ class ModalManager {
|
|
|
107
119
|
this.state.infoModalRow = -1;
|
|
108
120
|
this.state.isLoadingModalInfo = false;
|
|
109
121
|
this.state.infoModalScrollOffset = 0;
|
|
122
|
+
this.state.infoModalTab = 'info';
|
|
110
123
|
this.state.infoModalSessionId += 1;
|
|
111
124
|
}
|
|
112
125
|
setModalLoading(isLoading, sessionId) {
|
|
@@ -43,6 +43,7 @@ class StateManager {
|
|
|
43
43
|
infoModalRow: modalState.infoModalRow,
|
|
44
44
|
isLoadingModalInfo: modalState.isLoadingModalInfo,
|
|
45
45
|
infoModalScrollOffset: modalState.infoModalScrollOffset,
|
|
46
|
+
infoModalTab: modalState.infoModalTab,
|
|
46
47
|
showDebugModal: modalState.showDebugModal,
|
|
47
48
|
debugModalScrollOffset: modalState.debugModalScrollOffset,
|
|
48
49
|
filterMode: filterState.filterMode,
|
|
@@ -189,6 +190,16 @@ class StateManager {
|
|
|
189
190
|
clampInfoModalScrollOffset(maxOffset) {
|
|
190
191
|
return this.modalManager.clampScrollOffset(maxOffset);
|
|
191
192
|
}
|
|
193
|
+
setInfoModalTab(tab) {
|
|
194
|
+
const changed = this.modalManager.setInfoModalTab(tab);
|
|
195
|
+
if (changed) {
|
|
196
|
+
this.renderState.forceFullRender = true;
|
|
197
|
+
}
|
|
198
|
+
return changed;
|
|
199
|
+
}
|
|
200
|
+
getInfoModalTab() {
|
|
201
|
+
return this.modalManager.getInfoModalTab();
|
|
202
|
+
}
|
|
192
203
|
toggleDebugModal() {
|
|
193
204
|
this.modalManager.toggleDebugModal();
|
|
194
205
|
}
|
package/package.json
CHANGED