inup 1.5.1 → 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 +59 -27
- package/dist/core/upgrade-runner.js +7 -0
- package/dist/features/debug/index.js +20 -0
- package/dist/features/debug/renderer/performance-modal.js +141 -0
- package/dist/features/debug/services/performance-tracker.js +72 -0
- package/dist/features/debug/types/debug.types.js +3 -0
- package/dist/interactive-ui.js +41 -3
- package/dist/services/npm-registry.js +185 -91
- package/dist/ui/input-handler.js +37 -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 +47 -0
- package/dist/ui/state/state-manager.js +28 -0
- package/package.json +1 -1
|
@@ -40,12 +40,13 @@ const services_1 = require("../services");
|
|
|
40
40
|
const config_1 = require("../config");
|
|
41
41
|
const utils_2 = require("../ui/utils");
|
|
42
42
|
const utils_3 = require("../utils");
|
|
43
|
+
const debug_1 = require("../features/debug");
|
|
43
44
|
class PackageDetector {
|
|
44
45
|
constructor(options) {
|
|
45
46
|
this.packageJsonPath = null;
|
|
46
47
|
this.packageJson = null;
|
|
47
|
-
this.batchSize =
|
|
48
|
-
this.
|
|
48
|
+
this.batchSize = 10;
|
|
49
|
+
this.maxConcurrency = 10;
|
|
49
50
|
this.cwd = options?.cwd || process.cwd();
|
|
50
51
|
this.excludePatterns = options?.excludePatterns || [];
|
|
51
52
|
this.ignorePackages = options?.ignorePackages || [];
|
|
@@ -89,36 +90,55 @@ class PackageDetector {
|
|
|
89
90
|
const packageLookup = new Map();
|
|
90
91
|
let resolved = 0;
|
|
91
92
|
let failed = 0;
|
|
93
|
+
const performanceTracker = (0, debug_1.getPerformanceTracker)();
|
|
94
|
+
let batchIndex = 0;
|
|
95
|
+
let lastBatchEndAt = Date.now();
|
|
92
96
|
const tFetch = Date.now();
|
|
93
97
|
utils_3.debugLog.info('PackageDetector', 'fetching version data via npm registry in batches');
|
|
94
|
-
await (0, services_1.
|
|
95
|
-
|
|
96
|
-
const packageInfo = this.resolvePackageGroup(batchItem.packageName, prepared.allDependencies, batchItem.data);
|
|
97
|
-
packageLookup.set(batchItem.packageName, packageInfo);
|
|
98
|
-
resolved++;
|
|
99
|
-
const isFailed = batchItem.data.latestVersion === 'unknown';
|
|
100
|
-
if (isFailed) {
|
|
101
|
-
failed++;
|
|
102
|
-
}
|
|
103
|
-
return {
|
|
104
|
-
packageName: batchItem.packageName,
|
|
105
|
-
packageInfo,
|
|
106
|
-
failed: isFailed,
|
|
107
|
-
};
|
|
108
|
-
});
|
|
109
|
-
const progress = this.createProgressSnapshot(prepared.uniquePackages.length, resolved, failed, resolved < prepared.uniquePackages.length);
|
|
110
|
-
onEvent({
|
|
111
|
-
type: 'batch',
|
|
112
|
-
payload: {
|
|
113
|
-
batch: batchItems,
|
|
114
|
-
progress,
|
|
115
|
-
},
|
|
116
|
-
});
|
|
117
|
-
}, prepared.currentVersions, {
|
|
98
|
+
await (0, services_1.fetchPackageVersions)(prepared.uniquePackages, {
|
|
99
|
+
currentVersions: prepared.currentVersions,
|
|
118
100
|
batchSize: this.batchSize,
|
|
119
|
-
|
|
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
|
+
},
|
|
120
139
|
});
|
|
121
140
|
utils_3.debugLog.perf('PackageDetector', `registry fetch (${resolved}/${prepared.uniquePackages.length} resolved)`, tFetch);
|
|
141
|
+
performanceTracker.recordPhaseDuration('registryFetch', Date.now() - tFetch);
|
|
122
142
|
const finalPackages = prepared.uniquePackages.flatMap((packageName) => packageLookup.get(packageName) ?? []);
|
|
123
143
|
const progress = this.createProgressSnapshot(prepared.uniquePackages.length, resolved, failed, false);
|
|
124
144
|
utils_3.debugLog.perf('PackageDetector', `total scan complete (${finalPackages.filter((p) => p.isOutdated).length} outdated of ${finalPackages.length} deps)`, t0);
|
|
@@ -133,12 +153,15 @@ class PackageDetector {
|
|
|
133
153
|
return finalPackages;
|
|
134
154
|
}
|
|
135
155
|
async prepareDependencies() {
|
|
156
|
+
const performanceTracker = (0, debug_1.getPerformanceTracker)();
|
|
136
157
|
this.showProgress('🔍 Scanning repository for package.json files...');
|
|
137
158
|
const tScan = Date.now();
|
|
138
159
|
const allPackageJsonFiles = await this.findPackageJsonFilesWithTimeout(30000);
|
|
139
160
|
utils_3.debugLog.perf('PackageDetector', `file scan (${allPackageJsonFiles.length} files)`, tScan, {
|
|
140
161
|
files: allPackageJsonFiles,
|
|
141
162
|
});
|
|
163
|
+
performanceTracker.recordPhaseDuration('discovery', Date.now() - tScan);
|
|
164
|
+
performanceTracker.recordCounts({ packageJsonFiles: allPackageJsonFiles.length });
|
|
142
165
|
this.showProgress(`🔍 Found ${allPackageJsonFiles.length} package.json file${allPackageJsonFiles.length === 1 ? '' : 's'}`);
|
|
143
166
|
this.showProgress('🔍 Reading dependencies from package.json files...');
|
|
144
167
|
const tDeps = Date.now();
|
|
@@ -147,7 +170,10 @@ class PackageDetector {
|
|
|
147
170
|
includeOptionalDeps: true,
|
|
148
171
|
});
|
|
149
172
|
utils_3.debugLog.perf('PackageDetector', `dependency collection (${allDepsRaw.length} raw deps)`, tDeps);
|
|
173
|
+
performanceTracker.recordPhaseDuration('depCollection', Date.now() - tDeps);
|
|
174
|
+
performanceTracker.recordCounts({ rawDependencies: allDepsRaw.length });
|
|
150
175
|
this.showProgress('🔍 Identifying unique packages...');
|
|
176
|
+
const tFilter = Date.now();
|
|
151
177
|
const uniquePackageNames = new Set();
|
|
152
178
|
const allDependencies = [];
|
|
153
179
|
let ignoredCount = 0;
|
|
@@ -191,6 +217,12 @@ class PackageDetector {
|
|
|
191
217
|
return a.localeCompare(b);
|
|
192
218
|
});
|
|
193
219
|
utils_3.debugLog.info('PackageDetector', `${uniquePackages.length} unique packages to check, ${ignoredCount} ignored`);
|
|
220
|
+
performanceTracker.recordPhaseDuration('filter', Date.now() - tFilter);
|
|
221
|
+
performanceTracker.recordCounts({
|
|
222
|
+
uniquePackages: uniquePackages.length,
|
|
223
|
+
ignoredPackages: ignoredCount,
|
|
224
|
+
workspaceRefsSkipped: seenWorkspaceRefs.size,
|
|
225
|
+
});
|
|
194
226
|
const currentVersions = new Map();
|
|
195
227
|
for (const dep of allDependencies) {
|
|
196
228
|
if (!currentVersions.has(dep.name)) {
|
|
@@ -10,6 +10,7 @@ const interactive_ui_1 = require("../interactive-ui");
|
|
|
10
10
|
const upgrader_1 = require("./upgrader");
|
|
11
11
|
const package_manager_detector_1 = require("../services/package-manager-detector");
|
|
12
12
|
const utils_1 = require("../ui/utils");
|
|
13
|
+
const debug_1 = require("../features/debug");
|
|
13
14
|
/**
|
|
14
15
|
* Main orchestrator for the inup upgrade process
|
|
15
16
|
*/
|
|
@@ -35,6 +36,9 @@ class UpgradeRunner {
|
|
|
35
36
|
try {
|
|
36
37
|
// Check prerequisites
|
|
37
38
|
this.checkPrerequisites();
|
|
39
|
+
const performanceTracker = (0, debug_1.getPerformanceTracker)();
|
|
40
|
+
performanceTracker.start();
|
|
41
|
+
performanceTracker.setPackageManager(this.packageManager.name);
|
|
38
42
|
const progress = {
|
|
39
43
|
discovered: 0,
|
|
40
44
|
resolved: 0,
|
|
@@ -71,6 +75,7 @@ class UpgradeRunner {
|
|
|
71
75
|
progress.total = event.payload.progress.total;
|
|
72
76
|
progress.failed = event.payload.progress.failed;
|
|
73
77
|
progress.isLoading = event.payload.progress.isLoading;
|
|
78
|
+
performanceTracker.mark('firstBatch');
|
|
74
79
|
this.ui.appendOutdatedBatchToSelectionStates(selectionStates, event.payload.batch, previousSelections);
|
|
75
80
|
refreshUI?.();
|
|
76
81
|
}
|
|
@@ -81,6 +86,8 @@ class UpgradeRunner {
|
|
|
81
86
|
progress.total = event.payload.progress.total;
|
|
82
87
|
progress.failed = event.payload.progress.failed;
|
|
83
88
|
progress.isLoading = event.payload.progress.isLoading;
|
|
89
|
+
performanceTracker.mark('firstBatch');
|
|
90
|
+
performanceTracker.mark('allLoaded');
|
|
84
91
|
refreshUI?.();
|
|
85
92
|
}
|
|
86
93
|
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./types/debug.types"), exports);
|
|
18
|
+
__exportStar(require("./services/performance-tracker"), exports);
|
|
19
|
+
__exportStar(require("./renderer/performance-modal"), exports);
|
|
20
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.renderPerformanceModal = renderPerformanceModal;
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
const modal_1 = require("../../../ui/modal");
|
|
9
|
+
function formatMs(value) {
|
|
10
|
+
if (value === undefined || value === null)
|
|
11
|
+
return chalk_1.default.gray('—');
|
|
12
|
+
return chalk_1.default.yellow(`${value} ms`);
|
|
13
|
+
}
|
|
14
|
+
function formatCount(value) {
|
|
15
|
+
if (value === undefined)
|
|
16
|
+
return chalk_1.default.gray('—');
|
|
17
|
+
return chalk_1.default.cyan(String(value));
|
|
18
|
+
}
|
|
19
|
+
function labelValue(label, value, labelWidth = 22) {
|
|
20
|
+
const padded = label.padEnd(labelWidth, ' ');
|
|
21
|
+
return `${chalk_1.default.white(padded)} ${value}`;
|
|
22
|
+
}
|
|
23
|
+
function buildSections(snapshot) {
|
|
24
|
+
const { phases, counts, batches, failedPackages, packageManager, totalMs } = snapshot;
|
|
25
|
+
const pinned = [
|
|
26
|
+
{
|
|
27
|
+
key: 'header',
|
|
28
|
+
rows: [
|
|
29
|
+
chalk_1.default.cyan('⚡ Performance'),
|
|
30
|
+
chalk_1.default.gray(`Package manager: ${packageManager ? chalk_1.default.white(packageManager) : chalk_1.default.gray('unknown')}`),
|
|
31
|
+
],
|
|
32
|
+
required: true,
|
|
33
|
+
behavior: 'pinned',
|
|
34
|
+
},
|
|
35
|
+
];
|
|
36
|
+
const bodyRows = [];
|
|
37
|
+
bodyRows.push(chalk_1.default.bold('Timings'));
|
|
38
|
+
bodyRows.push(labelValue('Discovery', formatMs(phases.discovery)));
|
|
39
|
+
bodyRows.push(labelValue('Dep collection', formatMs(phases.depCollection)));
|
|
40
|
+
bodyRows.push(labelValue('Filter', formatMs(phases.filter)));
|
|
41
|
+
bodyRows.push(labelValue('Registry fetch', formatMs(phases.registryFetch)));
|
|
42
|
+
bodyRows.push(labelValue('First batch ready', formatMs(phases.firstBatch)));
|
|
43
|
+
bodyRows.push(labelValue('All packages loaded', formatMs(phases.allLoaded)));
|
|
44
|
+
bodyRows.push(labelValue('Elapsed total', formatMs(totalMs ?? undefined)));
|
|
45
|
+
bodyRows.push('');
|
|
46
|
+
bodyRows.push(chalk_1.default.bold('Counts'));
|
|
47
|
+
bodyRows.push(labelValue('package.json files', formatCount(counts.packageJsonFiles)));
|
|
48
|
+
bodyRows.push(labelValue('Raw dependencies', formatCount(counts.rawDependencies)));
|
|
49
|
+
bodyRows.push(labelValue('Unique packages', formatCount(counts.uniquePackages)));
|
|
50
|
+
bodyRows.push(labelValue('Ignored', formatCount(counts.ignoredPackages)));
|
|
51
|
+
bodyRows.push(labelValue('Workspace refs', formatCount(counts.workspaceRefsSkipped)));
|
|
52
|
+
bodyRows.push(labelValue('Resolved', formatCount(counts.resolved)));
|
|
53
|
+
bodyRows.push(labelValue('Failed', formatCount(counts.failed)));
|
|
54
|
+
bodyRows.push('');
|
|
55
|
+
bodyRows.push(chalk_1.default.bold('Batches'));
|
|
56
|
+
if (batches.length > 0) {
|
|
57
|
+
const durations = batches.map((b) => b.durationMs);
|
|
58
|
+
const total = durations.reduce((a, b) => a + b, 0);
|
|
59
|
+
const avg = Math.round(total / batches.length);
|
|
60
|
+
const slowest = Math.max(...durations);
|
|
61
|
+
const slowestBatch = batches.find((b) => b.durationMs === slowest);
|
|
62
|
+
bodyRows.push(labelValue('Batch count', formatCount(batches.length)));
|
|
63
|
+
bodyRows.push(labelValue('Avg batch', formatMs(avg)));
|
|
64
|
+
bodyRows.push(labelValue('Slowest batch', `${formatMs(slowest)} ${chalk_1.default.gray(`(#${slowestBatch?.index})`)}`));
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
bodyRows.push(chalk_1.default.gray(' (no batches recorded)'));
|
|
68
|
+
}
|
|
69
|
+
bodyRows.push('');
|
|
70
|
+
bodyRows.push(chalk_1.default.bold('Failures'));
|
|
71
|
+
if (failedPackages.length > 0) {
|
|
72
|
+
for (const name of failedPackages) {
|
|
73
|
+
bodyRows.push(` ${chalk_1.default.red('✗')} ${name}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
bodyRows.push(chalk_1.default.gray(' (none)'));
|
|
78
|
+
}
|
|
79
|
+
const body = [
|
|
80
|
+
{
|
|
81
|
+
key: 'body',
|
|
82
|
+
rows: bodyRows,
|
|
83
|
+
required: true,
|
|
84
|
+
behavior: 'body',
|
|
85
|
+
},
|
|
86
|
+
];
|
|
87
|
+
return { pinned, body };
|
|
88
|
+
}
|
|
89
|
+
function renderPerformanceModal(snapshot, terminalWidth = 80, terminalHeight = 24, scrollOffset = 0) {
|
|
90
|
+
const modalWidth = (0, modal_1.getModalWidth)(terminalWidth, 60, 84);
|
|
91
|
+
const padding = Math.floor((terminalWidth - modalWidth) / 2);
|
|
92
|
+
const fixedModalHeight = Math.max(10, terminalHeight - 2);
|
|
93
|
+
const { pinned, body } = buildSections(snapshot);
|
|
94
|
+
const pinnedRowCount = pinned.reduce((sum, s) => sum + s.rows.length, 0);
|
|
95
|
+
// frame: top border + pinned rows + separator + body rows + bottom border
|
|
96
|
+
const availableForBody = Math.max(3, fixedModalHeight - 2 - pinnedRowCount - 1);
|
|
97
|
+
const bodyRows = body[0].rows;
|
|
98
|
+
const totalScrollableRows = bodyRows.length;
|
|
99
|
+
const needsScroll = totalScrollableRows > availableForBody;
|
|
100
|
+
const visibleBodyRows = needsScroll ? Math.max(1, availableForBody - 1) : availableForBody;
|
|
101
|
+
const maxScroll = Math.max(0, totalScrollableRows - visibleBodyRows);
|
|
102
|
+
const clampedOffset = Math.min(Math.max(0, scrollOffset), maxScroll);
|
|
103
|
+
const visibleSlice = bodyRows.slice(clampedOffset, clampedOffset + visibleBodyRows);
|
|
104
|
+
const lines = [];
|
|
105
|
+
const topPadding = Math.max(0, Math.floor((terminalHeight - fixedModalHeight) / 2));
|
|
106
|
+
for (let i = 0; i < topPadding; i++) {
|
|
107
|
+
lines.push('');
|
|
108
|
+
}
|
|
109
|
+
lines.push(' '.repeat(padding) + chalk_1.default.gray('╭' + '─'.repeat(modalWidth - 2) + '╮'));
|
|
110
|
+
for (const section of pinned) {
|
|
111
|
+
for (const row of section.rows) {
|
|
112
|
+
lines.push((0, modal_1.renderModalRow)(padding, modalWidth, row));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
lines.push((0, modal_1.renderModalSeparator)(padding, modalWidth));
|
|
116
|
+
let renderedBodyRows = 0;
|
|
117
|
+
for (const row of visibleSlice) {
|
|
118
|
+
lines.push((0, modal_1.renderModalRow)(padding, modalWidth, row));
|
|
119
|
+
renderedBodyRows++;
|
|
120
|
+
}
|
|
121
|
+
const footer = needsScroll
|
|
122
|
+
? chalk_1.default.gray(`Lines ${clampedOffset + 1}-${Math.min(clampedOffset + visibleBodyRows, totalScrollableRows)} of ${totalScrollableRows}`)
|
|
123
|
+
: null;
|
|
124
|
+
const usedContentRows = pinnedRowCount + 1 + renderedBodyRows + (footer ? 1 : 0);
|
|
125
|
+
const totalContentSlots = fixedModalHeight - 2;
|
|
126
|
+
const emptyRows = Math.max(0, totalContentSlots - usedContentRows);
|
|
127
|
+
for (let i = 0; i < emptyRows; i++) {
|
|
128
|
+
lines.push((0, modal_1.renderModalRow)(padding, modalWidth, ''));
|
|
129
|
+
}
|
|
130
|
+
if (footer) {
|
|
131
|
+
lines.push((0, modal_1.renderModalRow)(padding, modalWidth, footer));
|
|
132
|
+
}
|
|
133
|
+
lines.push(' '.repeat(padding) + chalk_1.default.gray('╰' + '─'.repeat(modalWidth - 2) + '╯'));
|
|
134
|
+
return {
|
|
135
|
+
lines,
|
|
136
|
+
maxScrollOffset: maxScroll,
|
|
137
|
+
totalContentRows: totalScrollableRows,
|
|
138
|
+
usesInternalScroll: needsScroll,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=performance-modal.js.map
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getPerformanceTracker = getPerformanceTracker;
|
|
4
|
+
class PerformanceTracker {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.startedAt = null;
|
|
7
|
+
this.phases = {};
|
|
8
|
+
this.counts = {};
|
|
9
|
+
this.batches = [];
|
|
10
|
+
this.failedPackages = [];
|
|
11
|
+
this.packageManager = null;
|
|
12
|
+
}
|
|
13
|
+
start() {
|
|
14
|
+
this.startedAt = Date.now();
|
|
15
|
+
this.phases = {};
|
|
16
|
+
this.counts = {};
|
|
17
|
+
this.batches = [];
|
|
18
|
+
this.failedPackages = [];
|
|
19
|
+
this.packageManager = null;
|
|
20
|
+
}
|
|
21
|
+
mark(phase) {
|
|
22
|
+
if (this.startedAt === null || this.phases[phase] !== undefined)
|
|
23
|
+
return;
|
|
24
|
+
this.phases[phase] = Date.now() - this.startedAt;
|
|
25
|
+
}
|
|
26
|
+
recordPhaseDuration(phase, durationMs) {
|
|
27
|
+
this.phases[phase] = durationMs;
|
|
28
|
+
}
|
|
29
|
+
recordCounts(partial) {
|
|
30
|
+
this.counts = { ...this.counts, ...partial };
|
|
31
|
+
}
|
|
32
|
+
recordBatch(batch) {
|
|
33
|
+
this.batches.push(batch);
|
|
34
|
+
}
|
|
35
|
+
recordFailedPackage(name) {
|
|
36
|
+
if (!this.failedPackages.includes(name)) {
|
|
37
|
+
this.failedPackages.push(name);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
setPackageManager(name) {
|
|
41
|
+
this.packageManager = name;
|
|
42
|
+
}
|
|
43
|
+
snapshot() {
|
|
44
|
+
const totalMs = this.startedAt === null
|
|
45
|
+
? null
|
|
46
|
+
: (this.phases.allLoaded ?? Date.now() - this.startedAt);
|
|
47
|
+
return {
|
|
48
|
+
startedAt: this.startedAt,
|
|
49
|
+
phases: { ...this.phases },
|
|
50
|
+
totalMs,
|
|
51
|
+
counts: { ...this.counts },
|
|
52
|
+
batches: [...this.batches],
|
|
53
|
+
failedPackages: [...this.failedPackages],
|
|
54
|
+
packageManager: this.packageManager,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
reset() {
|
|
58
|
+
this.startedAt = null;
|
|
59
|
+
this.phases = {};
|
|
60
|
+
this.counts = {};
|
|
61
|
+
this.batches = [];
|
|
62
|
+
this.failedPackages = [];
|
|
63
|
+
this.packageManager = null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
let instance = null;
|
|
67
|
+
function getPerformanceTracker() {
|
|
68
|
+
if (!instance)
|
|
69
|
+
instance = new PerformanceTracker();
|
|
70
|
+
return instance;
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=performance-tracker.js.map
|
package/dist/interactive-ui.js
CHANGED
|
@@ -43,6 +43,7 @@ const ui_1 = require("./ui");
|
|
|
43
43
|
const controllers_1 = require("./ui/controllers");
|
|
44
44
|
const themes_1 = require("./ui/themes");
|
|
45
45
|
const themes_colors_1 = require("./ui/themes-colors");
|
|
46
|
+
const debug_1 = require("./features/debug");
|
|
46
47
|
const DEFAULT_VULNERABILITY_DISPLAY_OPTIONS = {
|
|
47
48
|
showPeerDependencyVulnerabilities: false,
|
|
48
49
|
showOptionalDependencyVulnerabilities: false,
|
|
@@ -236,6 +237,8 @@ class InteractiveUI {
|
|
|
236
237
|
stateManager.setRenderableItems([]);
|
|
237
238
|
// Track the current max scroll offset for the info modal
|
|
238
239
|
let infoModalMaxScrollOffset = 0;
|
|
240
|
+
// Track the current max scroll offset for the debug modal
|
|
241
|
+
let debugModalMaxScrollOffset = 0;
|
|
239
242
|
let previousViewportMode = null;
|
|
240
243
|
let previousModalViewportLineCount = null;
|
|
241
244
|
const handleAction = (action) => {
|
|
@@ -332,6 +335,24 @@ class InteractiveUI {
|
|
|
332
335
|
return;
|
|
333
336
|
}
|
|
334
337
|
break;
|
|
338
|
+
case 'toggle_debug_modal':
|
|
339
|
+
stateManager.toggleDebugModal();
|
|
340
|
+
break;
|
|
341
|
+
case 'scroll_debug_modal_up':
|
|
342
|
+
if (!stateManager.scrollDebugModalUp()) {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
break;
|
|
346
|
+
case 'scroll_debug_modal_down':
|
|
347
|
+
if (!stateManager.scrollDebugModalDown(debugModalMaxScrollOffset)) {
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
break;
|
|
351
|
+
case 'switch_info_modal_tab': {
|
|
352
|
+
const nextTab = stateManager.getInfoModalTab() === 'info' ? 'usedBy' : 'info';
|
|
353
|
+
stateManager.setInfoModalTab(nextTab);
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
335
356
|
case 'navigate_info_modal_version':
|
|
336
357
|
{
|
|
337
358
|
if (uiState.infoModalRow >= 0 && uiState.infoModalRow < filteredStates.length) {
|
|
@@ -511,6 +532,18 @@ class InteractiveUI {
|
|
|
511
532
|
const modalLines = this.renderer.renderThemeSelectorModal(themeManager.getCurrentTheme(), themeManager.getPreviewTheme(), terminalWidth, terminalHeight);
|
|
512
533
|
renderModalViewport('theme-modal', chalk_1.default.bold.white('T ') + chalk_1.default.gray('/ Esc Exit theme selector'), modalLines, terminalWidth, terminalHeight, bgCode);
|
|
513
534
|
}
|
|
535
|
+
else if (uiState.showDebugModal) {
|
|
536
|
+
const terminalWidth = process.stdout.columns || 80;
|
|
537
|
+
const terminalHeight = this.getTerminalHeight();
|
|
538
|
+
const snapshot = (0, debug_1.getPerformanceTracker)().snapshot();
|
|
539
|
+
const result = (0, debug_1.renderPerformanceModal)(snapshot, terminalWidth, Math.max(8, terminalHeight - 4), uiState.debugModalScrollOffset);
|
|
540
|
+
debugModalMaxScrollOffset = result.maxScrollOffset;
|
|
541
|
+
stateManager.clampDebugModalScrollOffset(debugModalMaxScrollOffset);
|
|
542
|
+
const scrollHint = result.usesInternalScroll && result.maxScrollOffset > 0
|
|
543
|
+
? chalk_1.default.bold.white('↑/↓ ') + chalk_1.default.gray('Scroll · ')
|
|
544
|
+
: '';
|
|
545
|
+
renderModalViewport('info-modal', scrollHint + chalk_1.default.bold.white('! / Esc ') + chalk_1.default.gray('Close'), result.lines, terminalWidth, terminalHeight, bgCode);
|
|
546
|
+
}
|
|
514
547
|
else if (uiState.showInfoModal &&
|
|
515
548
|
uiState.infoModalRow >= 0 &&
|
|
516
549
|
uiState.infoModalRow < filteredStates.length) {
|
|
@@ -525,15 +558,20 @@ class InteractiveUI {
|
|
|
525
558
|
}
|
|
526
559
|
else {
|
|
527
560
|
// Show full info with scroll support
|
|
528
|
-
const
|
|
561
|
+
const activeTab = uiState.infoModalTab;
|
|
562
|
+
const result = this.renderer.renderPackageInfoModal(selectedState, terminalWidth, Math.max(8, terminalHeight - 4), uiState.infoModalScrollOffset, activeTab);
|
|
529
563
|
infoModalMaxScrollOffset = result.maxScrollOffset;
|
|
530
564
|
stateManager.clampInfoModalScrollOffset(infoModalMaxScrollOffset);
|
|
531
565
|
const scrollHint = result.usesInternalScroll && result.maxScrollOffset > 0
|
|
532
566
|
? chalk_1.default.bold.white('↑/↓ ') + chalk_1.default.gray('Scroll · ')
|
|
533
567
|
: '';
|
|
568
|
+
const versionHint = activeTab === 'info'
|
|
569
|
+
? chalk_1.default.bold.white('←/→ ') + chalk_1.default.gray('Version · ')
|
|
570
|
+
: '';
|
|
534
571
|
renderModalViewport('info-modal', scrollHint +
|
|
535
|
-
|
|
536
|
-
chalk_1.default.
|
|
572
|
+
versionHint +
|
|
573
|
+
chalk_1.default.bold.white('Tab ') +
|
|
574
|
+
chalk_1.default.gray('Switch tab · ') +
|
|
537
575
|
chalk_1.default.bold.white('I / Esc ') +
|
|
538
576
|
chalk_1.default.gray('Exit this view'), result.lines, terminalWidth, terminalHeight, bgCode);
|
|
539
577
|
}
|
|
@@ -33,14 +33,37 @@ 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;
|
|
38
|
+
exports.closeRegistryPool = closeRegistryPool;
|
|
39
39
|
const semver = __importStar(require("semver"));
|
|
40
|
+
const undici_1 = require("undici");
|
|
41
|
+
const node_zlib_1 = require("node:zlib");
|
|
42
|
+
const node_util_1 = require("node:util");
|
|
43
|
+
const gunzipAsync = (0, node_util_1.promisify)(node_zlib_1.gunzip);
|
|
44
|
+
const inflateAsync = (0, node_util_1.promisify)(node_zlib_1.inflate);
|
|
45
|
+
const brotliDecompressAsync = (0, node_util_1.promisify)(node_zlib_1.brotliDecompress);
|
|
40
46
|
const config_1 = require("../config");
|
|
41
47
|
const jsdelivr_registry_1 = require("./jsdelivr-registry");
|
|
42
|
-
const utils_1 = require("../ui/utils");
|
|
43
48
|
const inFlightLookups = new Map();
|
|
49
|
+
const registryOrigin = new URL(config_1.NPM_REGISTRY_URL).origin;
|
|
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.
|
|
54
|
+
const registryPool = new undici_1.Pool(registryOrigin, {
|
|
55
|
+
connections: 6,
|
|
56
|
+
pipelining: 1,
|
|
57
|
+
keepAliveTimeout: 30000,
|
|
58
|
+
keepAliveMaxTimeout: 600000,
|
|
59
|
+
headersTimeout: 0,
|
|
60
|
+
bodyTimeout: 0,
|
|
61
|
+
connectTimeout: 15000,
|
|
62
|
+
allowH2: false,
|
|
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));
|
|
44
67
|
const isRetryableStatus = (statusCode) => statusCode === 408 || statusCode === 429 || statusCode >= 500;
|
|
45
68
|
const isTransientNetworkError = (error) => {
|
|
46
69
|
if (!(error instanceof Error)) {
|
|
@@ -48,6 +71,14 @@ const isTransientNetworkError = (error) => {
|
|
|
48
71
|
}
|
|
49
72
|
const maybeCode = error.code;
|
|
50
73
|
return (error.name === 'AbortError' ||
|
|
74
|
+
error.name === 'HeadersTimeoutError' ||
|
|
75
|
+
error.name === 'BodyTimeoutError' ||
|
|
76
|
+
error.name === 'ConnectTimeoutError' ||
|
|
77
|
+
error.name === 'SocketError' ||
|
|
78
|
+
maybeCode === 'UND_ERR_HEADERS_TIMEOUT' ||
|
|
79
|
+
maybeCode === 'UND_ERR_BODY_TIMEOUT' ||
|
|
80
|
+
maybeCode === 'UND_ERR_CONNECT_TIMEOUT' ||
|
|
81
|
+
maybeCode === 'UND_ERR_SOCKET' ||
|
|
51
82
|
maybeCode === 'ENOTFOUND' ||
|
|
52
83
|
maybeCode === 'EAI_AGAIN' ||
|
|
53
84
|
maybeCode === 'ECONNRESET' ||
|
|
@@ -71,127 +102,187 @@ async function getFreshPackageData(packageName, currentVersion) {
|
|
|
71
102
|
inFlightLookups.set(cacheKey, lookupPromise);
|
|
72
103
|
return await lookupPromise;
|
|
73
104
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
105
|
+
function parseVersions(raw) {
|
|
106
|
+
const data = JSON.parse(raw);
|
|
107
|
+
const allVersions = Object.keys(data.versions || {}).filter((v) => /^[0-9]+\.[0-9]+\.[0-9]+$/.test(v));
|
|
108
|
+
const sortedVersions = allVersions.sort(semver.rcompare);
|
|
109
|
+
const latestVersion = sortedVersions.length > 0 ? sortedVersions[0] : 'unknown';
|
|
110
|
+
return { latestVersion, allVersions };
|
|
111
|
+
}
|
|
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) {
|
|
79
119
|
try {
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
throw new Error(`HTTP ${response.status}`);
|
|
120
|
+
const { statusCode, headers, body } = await registryPool.request({
|
|
121
|
+
path,
|
|
122
|
+
method: 'GET',
|
|
123
|
+
headers: {
|
|
124
|
+
accept: 'application/vnd.npm.install-v1+json',
|
|
125
|
+
'accept-encoding': 'gzip, deflate, br',
|
|
126
|
+
},
|
|
127
|
+
headersTimeout: 0,
|
|
128
|
+
bodyTimeout: 0,
|
|
129
|
+
blocking: false,
|
|
130
|
+
});
|
|
131
|
+
if (statusCode < 200 || statusCode >= 300) {
|
|
132
|
+
await body.dump().catch(() => undefined);
|
|
133
|
+
if (isRetryableStatus(statusCode)) {
|
|
134
|
+
return { kind: 'retryable' };
|
|
97
135
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
136
|
+
return { kind: 'not-found' };
|
|
137
|
+
}
|
|
138
|
+
const raw = Buffer.from(await body.arrayBuffer());
|
|
139
|
+
const encodingHeader = headers['content-encoding'];
|
|
140
|
+
const encoding = (Array.isArray(encodingHeader) ? encodingHeader[0] : encodingHeader)
|
|
141
|
+
?.toString()
|
|
142
|
+
.toLowerCase();
|
|
143
|
+
let decoded;
|
|
144
|
+
if (encoding === 'gzip') {
|
|
145
|
+
decoded = await gunzipAsync(raw);
|
|
146
|
+
}
|
|
147
|
+
else if (encoding === 'br') {
|
|
148
|
+
decoded = await brotliDecompressAsync(raw);
|
|
149
|
+
}
|
|
150
|
+
else if (encoding === 'deflate') {
|
|
151
|
+
decoded = await inflateAsync(raw);
|
|
107
152
|
}
|
|
108
|
-
|
|
109
|
-
|
|
153
|
+
else {
|
|
154
|
+
decoded = raw;
|
|
110
155
|
}
|
|
156
|
+
return { kind: 'success', data: parseVersions(decoded.toString('utf8')) };
|
|
111
157
|
}
|
|
112
158
|
catch (error) {
|
|
113
159
|
if (isTransientNetworkError(error)) {
|
|
114
|
-
return
|
|
160
|
+
return { kind: 'transient' };
|
|
115
161
|
}
|
|
116
|
-
|
|
162
|
+
// Unknown error: treat as transient so we try the fallback rather than
|
|
163
|
+
// silently returning 'unknown'.
|
|
164
|
+
return { kind: 'transient' };
|
|
117
165
|
}
|
|
118
166
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
let completedCount = 0;
|
|
131
|
-
const allPromises = packageNames.map(async (packageName) => {
|
|
132
|
-
const data = await getFreshPackageData(packageName, currentVersions?.get(packageName));
|
|
133
|
-
packageData.set(packageName, data);
|
|
134
|
-
completedCount++;
|
|
135
|
-
if (onProgress) {
|
|
136
|
-
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);
|
|
137
178
|
}
|
|
138
|
-
});
|
|
139
|
-
// Wait for all requests to complete
|
|
140
|
-
await Promise.all(allPromises);
|
|
141
|
-
// Clear the progress line if no custom progress handler
|
|
142
|
-
if (!onProgress) {
|
|
143
|
-
utils_1.ConsoleUtils.clearProgress();
|
|
144
179
|
}
|
|
145
|
-
return
|
|
180
|
+
return lastOutcome;
|
|
146
181
|
}
|
|
147
|
-
async function
|
|
148
|
-
|
|
149
|
-
|
|
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;
|
|
150
187
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
};
|
|
159
|
-
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: [] };
|
|
160
195
|
}
|
|
161
|
-
|
|
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 = {}) {
|
|
162
213
|
const packageData = new Map();
|
|
163
214
|
if (packageNames.length === 0) {
|
|
164
215
|
return packageData;
|
|
165
216
|
}
|
|
166
217
|
const batchSizes = options.batchSizes && options.batchSizes.length > 0
|
|
167
218
|
? options.batchSizes.map((size) => Math.max(1, size))
|
|
168
|
-
: [Math.max(1, options.batchSize ??
|
|
169
|
-
const
|
|
219
|
+
: [Math.max(1, options.batchSize ?? 25)];
|
|
220
|
+
const maxConcurrency = Math.max(1, options.maxConcurrency ?? 10);
|
|
170
221
|
const total = packageNames.length;
|
|
171
222
|
let completedCount = 0;
|
|
223
|
+
const pendingEmissions = new Map();
|
|
224
|
+
let nextEmitIndex = 0;
|
|
225
|
+
const flushPending = () => {
|
|
226
|
+
while (pendingEmissions.has(nextEmitIndex)) {
|
|
227
|
+
const ready = pendingEmissions.get(nextEmitIndex);
|
|
228
|
+
pendingEmissions.delete(nextEmitIndex);
|
|
229
|
+
options.onBatchReady?.(ready);
|
|
230
|
+
nextEmitIndex++;
|
|
231
|
+
}
|
|
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 = [];
|
|
172
252
|
let batchStart = 0;
|
|
173
253
|
let batchIndex = 0;
|
|
174
254
|
while (batchStart < packageNames.length) {
|
|
175
255
|
const batchSize = batchSizes[Math.min(batchIndex, batchSizes.length - 1)];
|
|
176
256
|
const batchNames = packageNames.slice(batchStart, batchStart + batchSize);
|
|
257
|
+
const capturedBatchIndex = batchIndex;
|
|
177
258
|
const batchResults = new Array(batchNames.length);
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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(() => {
|
|
278
|
+
pendingEmissions.set(capturedBatchIndex, batchResults.filter(Boolean));
|
|
279
|
+
flushPending();
|
|
190
280
|
});
|
|
191
|
-
|
|
281
|
+
batchPromises.push(batchPromise);
|
|
192
282
|
batchStart += batchSize;
|
|
193
283
|
batchIndex++;
|
|
194
284
|
}
|
|
285
|
+
await Promise.all(batchPromises);
|
|
195
286
|
return packageData;
|
|
196
287
|
}
|
|
197
288
|
/**
|
|
@@ -200,4 +291,7 @@ async function getAllPackageDataBatched(packageNames, onBatchReady, currentVersi
|
|
|
200
291
|
function clearPackageCache() {
|
|
201
292
|
inFlightLookups.clear();
|
|
202
293
|
}
|
|
294
|
+
async function closeRegistryPool() {
|
|
295
|
+
await registryPool.close();
|
|
296
|
+
}
|
|
203
297
|
//# sourceMappingURL=npm-registry.js.map
|
package/dist/ui/input-handler.js
CHANGED
|
@@ -47,6 +47,29 @@ class InputHandler {
|
|
|
47
47
|
}
|
|
48
48
|
return;
|
|
49
49
|
}
|
|
50
|
+
// Handle debug modal input (scroll and close)
|
|
51
|
+
if (uiState.showDebugModal) {
|
|
52
|
+
if (str === '!') {
|
|
53
|
+
this.onAction({ type: 'toggle_debug_modal' });
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (key) {
|
|
57
|
+
switch (key.name) {
|
|
58
|
+
case 'escape':
|
|
59
|
+
this.onAction({ type: 'toggle_debug_modal' });
|
|
60
|
+
return;
|
|
61
|
+
case 'up':
|
|
62
|
+
this.onAction({ type: 'scroll_debug_modal_up' });
|
|
63
|
+
return;
|
|
64
|
+
case 'down':
|
|
65
|
+
this.onAction({ type: 'scroll_debug_modal_down' });
|
|
66
|
+
return;
|
|
67
|
+
default:
|
|
68
|
+
return; // consume other keys while modal is open
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
50
73
|
// Handle info modal input (scroll and close)
|
|
51
74
|
if (uiState.showInfoModal) {
|
|
52
75
|
if (key) {
|
|
@@ -58,6 +81,9 @@ class InputHandler {
|
|
|
58
81
|
case 'I':
|
|
59
82
|
this.onAction({ type: 'toggle_info_modal' });
|
|
60
83
|
return;
|
|
84
|
+
case 'tab':
|
|
85
|
+
this.onAction({ type: 'switch_info_modal_tab' });
|
|
86
|
+
return;
|
|
61
87
|
case 'up':
|
|
62
88
|
this.onAction({ type: 'scroll_info_modal_up' });
|
|
63
89
|
return;
|
|
@@ -65,10 +91,14 @@ class InputHandler {
|
|
|
65
91
|
this.onAction({ type: 'scroll_info_modal_down' });
|
|
66
92
|
return;
|
|
67
93
|
case 'left':
|
|
68
|
-
|
|
94
|
+
if (uiState.infoModalTab === 'info') {
|
|
95
|
+
this.onAction({ type: 'navigate_info_modal_version', direction: 'newer' });
|
|
96
|
+
}
|
|
69
97
|
return;
|
|
70
98
|
case 'right':
|
|
71
|
-
|
|
99
|
+
if (uiState.infoModalTab === 'info') {
|
|
100
|
+
this.onAction({ type: 'navigate_info_modal_version', direction: 'older' });
|
|
101
|
+
}
|
|
72
102
|
return;
|
|
73
103
|
default:
|
|
74
104
|
return; // Consume all other keys while modal is open
|
|
@@ -76,6 +106,11 @@ class InputHandler {
|
|
|
76
106
|
}
|
|
77
107
|
return;
|
|
78
108
|
}
|
|
109
|
+
// '!' toggles the debug/performance modal (outside of filter mode)
|
|
110
|
+
if (str === '!' && !uiState.filterMode) {
|
|
111
|
+
this.onAction({ type: 'toggle_debug_modal' });
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
79
114
|
// Check for '/' character to handle filter mode (only when not in modal)
|
|
80
115
|
if (str === '/') {
|
|
81
116
|
if (uiState.filterMode) {
|
|
@@ -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,8 +9,53 @@ class ModalManager {
|
|
|
9
9
|
isLoadingModalInfo: false,
|
|
10
10
|
infoModalScrollOffset: 0,
|
|
11
11
|
infoModalSessionId: 0,
|
|
12
|
+
infoModalTab: 'info',
|
|
13
|
+
showDebugModal: false,
|
|
14
|
+
debugModalScrollOffset: 0,
|
|
12
15
|
};
|
|
13
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
|
+
}
|
|
27
|
+
isDebugModalOpen() {
|
|
28
|
+
return this.state.showDebugModal;
|
|
29
|
+
}
|
|
30
|
+
toggleDebugModal() {
|
|
31
|
+
this.state.showDebugModal = !this.state.showDebugModal;
|
|
32
|
+
this.state.debugModalScrollOffset = 0;
|
|
33
|
+
}
|
|
34
|
+
closeDebugModal() {
|
|
35
|
+
this.state.showDebugModal = false;
|
|
36
|
+
this.state.debugModalScrollOffset = 0;
|
|
37
|
+
}
|
|
38
|
+
scrollDebugModalUp() {
|
|
39
|
+
if (this.state.debugModalScrollOffset > 0) {
|
|
40
|
+
this.state.debugModalScrollOffset--;
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
scrollDebugModalDown(maxOffset) {
|
|
46
|
+
if (this.state.debugModalScrollOffset < maxOffset) {
|
|
47
|
+
this.state.debugModalScrollOffset++;
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
clampDebugModalScrollOffset(maxOffset) {
|
|
53
|
+
const nextOffset = Math.max(0, Math.min(this.state.debugModalScrollOffset, maxOffset));
|
|
54
|
+
if (nextOffset === this.state.debugModalScrollOffset)
|
|
55
|
+
return false;
|
|
56
|
+
this.state.debugModalScrollOffset = nextOffset;
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
14
59
|
getState() {
|
|
15
60
|
return { ...this.state };
|
|
16
61
|
}
|
|
@@ -65,6 +110,7 @@ class ModalManager {
|
|
|
65
110
|
this.state.infoModalRow = currentRow;
|
|
66
111
|
this.state.infoModalScrollOffset = 0;
|
|
67
112
|
this.state.isLoadingModalInfo = false;
|
|
113
|
+
this.state.infoModalTab = 'info';
|
|
68
114
|
this.state.infoModalSessionId += 1;
|
|
69
115
|
return this.state.infoModalSessionId;
|
|
70
116
|
}
|
|
@@ -73,6 +119,7 @@ class ModalManager {
|
|
|
73
119
|
this.state.infoModalRow = -1;
|
|
74
120
|
this.state.isLoadingModalInfo = false;
|
|
75
121
|
this.state.infoModalScrollOffset = 0;
|
|
122
|
+
this.state.infoModalTab = 'info';
|
|
76
123
|
this.state.infoModalSessionId += 1;
|
|
77
124
|
}
|
|
78
125
|
setModalLoading(isLoading, sessionId) {
|
|
@@ -43,6 +43,9 @@ class StateManager {
|
|
|
43
43
|
infoModalRow: modalState.infoModalRow,
|
|
44
44
|
isLoadingModalInfo: modalState.isLoadingModalInfo,
|
|
45
45
|
infoModalScrollOffset: modalState.infoModalScrollOffset,
|
|
46
|
+
infoModalTab: modalState.infoModalTab,
|
|
47
|
+
showDebugModal: modalState.showDebugModal,
|
|
48
|
+
debugModalScrollOffset: modalState.debugModalScrollOffset,
|
|
46
49
|
filterMode: filterState.filterMode,
|
|
47
50
|
filterQuery: filterState.filterQuery,
|
|
48
51
|
showThemeModal: themeState.showThemeModal,
|
|
@@ -187,6 +190,31 @@ class StateManager {
|
|
|
187
190
|
clampInfoModalScrollOffset(maxOffset) {
|
|
188
191
|
return this.modalManager.clampScrollOffset(maxOffset);
|
|
189
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
|
+
}
|
|
203
|
+
toggleDebugModal() {
|
|
204
|
+
this.modalManager.toggleDebugModal();
|
|
205
|
+
}
|
|
206
|
+
closeDebugModal() {
|
|
207
|
+
this.modalManager.closeDebugModal();
|
|
208
|
+
}
|
|
209
|
+
scrollDebugModalUp() {
|
|
210
|
+
return this.modalManager.scrollDebugModalUp();
|
|
211
|
+
}
|
|
212
|
+
scrollDebugModalDown(maxOffset) {
|
|
213
|
+
return this.modalManager.scrollDebugModalDown(maxOffset);
|
|
214
|
+
}
|
|
215
|
+
clampDebugModalScrollOffset(maxOffset) {
|
|
216
|
+
return this.modalManager.clampDebugModalScrollOffset(maxOffset);
|
|
217
|
+
}
|
|
190
218
|
// Filter delegation
|
|
191
219
|
enterFilterMode(preserveQuery = false) {
|
|
192
220
|
this.filterManager.enterFilterMode(preserveQuery);
|
package/package.json
CHANGED