inup 1.5.1 → 1.5.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/dist/core/package-detector.js +31 -1
- 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 +28 -0
- package/dist/services/npm-registry.js +94 -38
- package/dist/ui/input-handler.js +28 -0
- package/dist/ui/state/modal-manager.js +34 -0
- package/dist/ui/state/state-manager.js +17 -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
48
|
this.batchSize = 25;
|
|
48
|
-
this.batchConcurrency =
|
|
49
|
+
this.batchConcurrency = 25;
|
|
49
50
|
this.cwd = options?.cwd || process.cwd();
|
|
50
51
|
this.excludePatterns = options?.excludePatterns || [];
|
|
51
52
|
this.ignorePackages = options?.ignorePackages || [];
|
|
@@ -89,9 +90,14 @@ 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
98
|
await (0, services_1.getAllPackageDataBatched)(prepared.uniquePackages, (batch) => {
|
|
99
|
+
const batchStart = lastBatchEndAt;
|
|
100
|
+
let batchFailedCount = 0;
|
|
95
101
|
const batchItems = batch.map((batchItem) => {
|
|
96
102
|
const packageInfo = this.resolvePackageGroup(batchItem.packageName, prepared.allDependencies, batchItem.data);
|
|
97
103
|
packageLookup.set(batchItem.packageName, packageInfo);
|
|
@@ -99,6 +105,8 @@ class PackageDetector {
|
|
|
99
105
|
const isFailed = batchItem.data.latestVersion === 'unknown';
|
|
100
106
|
if (isFailed) {
|
|
101
107
|
failed++;
|
|
108
|
+
batchFailedCount++;
|
|
109
|
+
performanceTracker.recordFailedPackage(batchItem.packageName);
|
|
102
110
|
}
|
|
103
111
|
return {
|
|
104
112
|
packageName: batchItem.packageName,
|
|
@@ -106,6 +114,15 @@ class PackageDetector {
|
|
|
106
114
|
failed: isFailed,
|
|
107
115
|
};
|
|
108
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 });
|
|
109
126
|
const progress = this.createProgressSnapshot(prepared.uniquePackages.length, resolved, failed, resolved < prepared.uniquePackages.length);
|
|
110
127
|
onEvent({
|
|
111
128
|
type: 'batch',
|
|
@@ -119,6 +136,7 @@ class PackageDetector {
|
|
|
119
136
|
concurrency: this.batchConcurrency,
|
|
120
137
|
});
|
|
121
138
|
utils_3.debugLog.perf('PackageDetector', `registry fetch (${resolved}/${prepared.uniquePackages.length} resolved)`, tFetch);
|
|
139
|
+
performanceTracker.recordPhaseDuration('registryFetch', Date.now() - tFetch);
|
|
122
140
|
const finalPackages = prepared.uniquePackages.flatMap((packageName) => packageLookup.get(packageName) ?? []);
|
|
123
141
|
const progress = this.createProgressSnapshot(prepared.uniquePackages.length, resolved, failed, false);
|
|
124
142
|
utils_3.debugLog.perf('PackageDetector', `total scan complete (${finalPackages.filter((p) => p.isOutdated).length} outdated of ${finalPackages.length} deps)`, t0);
|
|
@@ -133,12 +151,15 @@ class PackageDetector {
|
|
|
133
151
|
return finalPackages;
|
|
134
152
|
}
|
|
135
153
|
async prepareDependencies() {
|
|
154
|
+
const performanceTracker = (0, debug_1.getPerformanceTracker)();
|
|
136
155
|
this.showProgress('🔍 Scanning repository for package.json files...');
|
|
137
156
|
const tScan = Date.now();
|
|
138
157
|
const allPackageJsonFiles = await this.findPackageJsonFilesWithTimeout(30000);
|
|
139
158
|
utils_3.debugLog.perf('PackageDetector', `file scan (${allPackageJsonFiles.length} files)`, tScan, {
|
|
140
159
|
files: allPackageJsonFiles,
|
|
141
160
|
});
|
|
161
|
+
performanceTracker.recordPhaseDuration('discovery', Date.now() - tScan);
|
|
162
|
+
performanceTracker.recordCounts({ packageJsonFiles: allPackageJsonFiles.length });
|
|
142
163
|
this.showProgress(`🔍 Found ${allPackageJsonFiles.length} package.json file${allPackageJsonFiles.length === 1 ? '' : 's'}`);
|
|
143
164
|
this.showProgress('🔍 Reading dependencies from package.json files...');
|
|
144
165
|
const tDeps = Date.now();
|
|
@@ -147,7 +168,10 @@ class PackageDetector {
|
|
|
147
168
|
includeOptionalDeps: true,
|
|
148
169
|
});
|
|
149
170
|
utils_3.debugLog.perf('PackageDetector', `dependency collection (${allDepsRaw.length} raw deps)`, tDeps);
|
|
171
|
+
performanceTracker.recordPhaseDuration('depCollection', Date.now() - tDeps);
|
|
172
|
+
performanceTracker.recordCounts({ rawDependencies: allDepsRaw.length });
|
|
150
173
|
this.showProgress('🔍 Identifying unique packages...');
|
|
174
|
+
const tFilter = Date.now();
|
|
151
175
|
const uniquePackageNames = new Set();
|
|
152
176
|
const allDependencies = [];
|
|
153
177
|
let ignoredCount = 0;
|
|
@@ -191,6 +215,12 @@ class PackageDetector {
|
|
|
191
215
|
return a.localeCompare(b);
|
|
192
216
|
});
|
|
193
217
|
utils_3.debugLog.info('PackageDetector', `${uniquePackages.length} unique packages to check, ${ignoredCount} ignored`);
|
|
218
|
+
performanceTracker.recordPhaseDuration('filter', Date.now() - tFilter);
|
|
219
|
+
performanceTracker.recordCounts({
|
|
220
|
+
uniquePackages: uniquePackages.length,
|
|
221
|
+
ignoredPackages: ignoredCount,
|
|
222
|
+
workspaceRefsSkipped: seenWorkspaceRefs.size,
|
|
223
|
+
});
|
|
194
224
|
const currentVersions = new Map();
|
|
195
225
|
for (const dep of allDependencies) {
|
|
196
226
|
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,19 @@ 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;
|
|
335
351
|
case 'navigate_info_modal_version':
|
|
336
352
|
{
|
|
337
353
|
if (uiState.infoModalRow >= 0 && uiState.infoModalRow < filteredStates.length) {
|
|
@@ -511,6 +527,18 @@ class InteractiveUI {
|
|
|
511
527
|
const modalLines = this.renderer.renderThemeSelectorModal(themeManager.getCurrentTheme(), themeManager.getPreviewTheme(), terminalWidth, terminalHeight);
|
|
512
528
|
renderModalViewport('theme-modal', chalk_1.default.bold.white('T ') + chalk_1.default.gray('/ Esc Exit theme selector'), modalLines, terminalWidth, terminalHeight, bgCode);
|
|
513
529
|
}
|
|
530
|
+
else if (uiState.showDebugModal) {
|
|
531
|
+
const terminalWidth = process.stdout.columns || 80;
|
|
532
|
+
const terminalHeight = this.getTerminalHeight();
|
|
533
|
+
const snapshot = (0, debug_1.getPerformanceTracker)().snapshot();
|
|
534
|
+
const result = (0, debug_1.renderPerformanceModal)(snapshot, terminalWidth, Math.max(8, terminalHeight - 4), uiState.debugModalScrollOffset);
|
|
535
|
+
debugModalMaxScrollOffset = result.maxScrollOffset;
|
|
536
|
+
stateManager.clampDebugModalScrollOffset(debugModalMaxScrollOffset);
|
|
537
|
+
const scrollHint = result.usesInternalScroll && result.maxScrollOffset > 0
|
|
538
|
+
? chalk_1.default.bold.white('↑/↓ ') + chalk_1.default.gray('Scroll · ')
|
|
539
|
+
: '';
|
|
540
|
+
renderModalViewport('info-modal', scrollHint + chalk_1.default.bold.white('! / Esc ') + chalk_1.default.gray('Close'), result.lines, terminalWidth, terminalHeight, bgCode);
|
|
541
|
+
}
|
|
514
542
|
else if (uiState.showInfoModal &&
|
|
515
543
|
uiState.infoModalRow >= 0 &&
|
|
516
544
|
uiState.infoModalRow < filteredStates.length) {
|
|
@@ -36,11 +36,29 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.getAllPackageData = getAllPackageData;
|
|
37
37
|
exports.getAllPackageDataBatched = getAllPackageDataBatched;
|
|
38
38
|
exports.clearPackageCache = clearPackageCache;
|
|
39
|
+
exports.closeRegistryPool = closeRegistryPool;
|
|
39
40
|
const semver = __importStar(require("semver"));
|
|
41
|
+
const undici_1 = require("undici");
|
|
42
|
+
const node_zlib_1 = require("node:zlib");
|
|
43
|
+
const node_util_1 = require("node:util");
|
|
44
|
+
const gunzipAsync = (0, node_util_1.promisify)(node_zlib_1.gunzip);
|
|
45
|
+
const inflateAsync = (0, node_util_1.promisify)(node_zlib_1.inflate);
|
|
46
|
+
const brotliDecompressAsync = (0, node_util_1.promisify)(node_zlib_1.brotliDecompress);
|
|
40
47
|
const config_1 = require("../config");
|
|
41
48
|
const jsdelivr_registry_1 = require("./jsdelivr-registry");
|
|
42
49
|
const utils_1 = require("../ui/utils");
|
|
43
50
|
const inFlightLookups = new Map();
|
|
51
|
+
const registryOrigin = new URL(config_1.NPM_REGISTRY_URL).origin;
|
|
52
|
+
const registryPathPrefix = new URL(config_1.NPM_REGISTRY_URL).pathname.replace(/\/$/, '');
|
|
53
|
+
const registryPool = new undici_1.Pool(registryOrigin, {
|
|
54
|
+
connections: 64,
|
|
55
|
+
pipelining: 10,
|
|
56
|
+
keepAliveTimeout: 30000,
|
|
57
|
+
keepAliveMaxTimeout: 600000,
|
|
58
|
+
headersTimeout: config_1.REQUEST_TIMEOUT,
|
|
59
|
+
bodyTimeout: config_1.REQUEST_TIMEOUT,
|
|
60
|
+
allowH2: false,
|
|
61
|
+
});
|
|
44
62
|
const isRetryableStatus = (statusCode) => statusCode === 408 || statusCode === 429 || statusCode >= 500;
|
|
45
63
|
const isTransientNetworkError = (error) => {
|
|
46
64
|
if (!(error instanceof Error)) {
|
|
@@ -48,6 +66,14 @@ const isTransientNetworkError = (error) => {
|
|
|
48
66
|
}
|
|
49
67
|
const maybeCode = error.code;
|
|
50
68
|
return (error.name === 'AbortError' ||
|
|
69
|
+
error.name === 'HeadersTimeoutError' ||
|
|
70
|
+
error.name === 'BodyTimeoutError' ||
|
|
71
|
+
error.name === 'ConnectTimeoutError' ||
|
|
72
|
+
error.name === 'SocketError' ||
|
|
73
|
+
maybeCode === 'UND_ERR_HEADERS_TIMEOUT' ||
|
|
74
|
+
maybeCode === 'UND_ERR_BODY_TIMEOUT' ||
|
|
75
|
+
maybeCode === 'UND_ERR_CONNECT_TIMEOUT' ||
|
|
76
|
+
maybeCode === 'UND_ERR_SOCKET' ||
|
|
51
77
|
maybeCode === 'ENOTFOUND' ||
|
|
52
78
|
maybeCode === 'EAI_AGAIN' ||
|
|
53
79
|
maybeCode === 'ECONNRESET' ||
|
|
@@ -71,43 +97,56 @@ async function getFreshPackageData(packageName, currentVersion) {
|
|
|
71
97
|
inFlightLookups.set(cacheKey, lookupPromise);
|
|
72
98
|
return await lookupPromise;
|
|
73
99
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
100
|
+
function parseVersions(raw) {
|
|
101
|
+
const data = JSON.parse(raw);
|
|
102
|
+
const allVersions = Object.keys(data.versions || {}).filter((v) => /^[0-9]+\.[0-9]+\.[0-9]+$/.test(v));
|
|
103
|
+
const sortedVersions = allVersions.sort(semver.rcompare);
|
|
104
|
+
const latestVersion = sortedVersions.length > 0 ? sortedVersions[0] : 'unknown';
|
|
105
|
+
return { latestVersion, allVersions };
|
|
106
|
+
}
|
|
78
107
|
async function fetchPackageFromRegistryWithFallback(packageName, currentVersion) {
|
|
79
108
|
try {
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
+
const { statusCode, headers, body } = await registryPool.request({
|
|
114
|
+
path,
|
|
115
|
+
method: 'GET',
|
|
116
|
+
headers: {
|
|
117
|
+
accept: 'application/vnd.npm.install-v1+json',
|
|
118
|
+
'accept-encoding': 'gzip, deflate, br',
|
|
119
|
+
},
|
|
120
|
+
headersTimeout: config_1.REQUEST_TIMEOUT,
|
|
121
|
+
bodyTimeout: config_1.REQUEST_TIMEOUT,
|
|
122
|
+
blocking: false,
|
|
123
|
+
});
|
|
124
|
+
if (statusCode < 200 || statusCode >= 300) {
|
|
125
|
+
await body.dump();
|
|
126
|
+
if (isRetryableStatus(statusCode)) {
|
|
127
|
+
return await fetchFromJsdelivrFallback(packageName, currentVersion);
|
|
97
128
|
}
|
|
98
|
-
|
|
99
|
-
const data = JSON.parse(text);
|
|
100
|
-
const allVersions = Object.keys(data.versions || {}).filter((version) => /^[0-9]+\.[0-9]+\.[0-9]+$/.test(version));
|
|
101
|
-
const sortedVersions = allVersions.sort(semver.rcompare);
|
|
102
|
-
const latestVersion = sortedVersions.length > 0 ? sortedVersions[0] : 'unknown';
|
|
103
|
-
return {
|
|
104
|
-
latestVersion,
|
|
105
|
-
allVersions,
|
|
106
|
-
};
|
|
129
|
+
return { latestVersion: 'unknown', allVersions: [] };
|
|
107
130
|
}
|
|
108
|
-
|
|
109
|
-
|
|
131
|
+
const raw = Buffer.from(await body.arrayBuffer());
|
|
132
|
+
const encodingHeader = headers['content-encoding'];
|
|
133
|
+
const encoding = (Array.isArray(encodingHeader) ? encodingHeader[0] : encodingHeader)
|
|
134
|
+
?.toString()
|
|
135
|
+
.toLowerCase();
|
|
136
|
+
let decoded;
|
|
137
|
+
if (encoding === 'gzip') {
|
|
138
|
+
decoded = await gunzipAsync(raw);
|
|
110
139
|
}
|
|
140
|
+
else if (encoding === 'br') {
|
|
141
|
+
decoded = await brotliDecompressAsync(raw);
|
|
142
|
+
}
|
|
143
|
+
else if (encoding === 'deflate') {
|
|
144
|
+
decoded = await inflateAsync(raw);
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
decoded = raw;
|
|
148
|
+
}
|
|
149
|
+
return parseVersions(decoded.toString('utf8'));
|
|
111
150
|
}
|
|
112
151
|
catch (error) {
|
|
113
152
|
if (isTransientNetworkError(error)) {
|
|
@@ -118,7 +157,7 @@ async function fetchPackageFromRegistryWithFallback(packageName, currentVersion)
|
|
|
118
157
|
}
|
|
119
158
|
/**
|
|
120
159
|
* Fetches package version data from npm registry for multiple packages.
|
|
121
|
-
* Uses
|
|
160
|
+
* Uses an undici Pool with HTTP/1.1 pipelining for high throughput.
|
|
122
161
|
* Only returns valid semantic versions (X.Y.Z format, excluding pre-releases).
|
|
123
162
|
*/
|
|
124
163
|
async function getAllPackageData(packageNames, onProgress, currentVersions) {
|
|
@@ -136,9 +175,7 @@ async function getAllPackageData(packageNames, onProgress, currentVersions) {
|
|
|
136
175
|
onProgress(packageName, completedCount, total);
|
|
137
176
|
}
|
|
138
177
|
});
|
|
139
|
-
// Wait for all requests to complete
|
|
140
178
|
await Promise.all(allPromises);
|
|
141
|
-
// Clear the progress line if no custom progress handler
|
|
142
179
|
if (!onProgress) {
|
|
143
180
|
utils_1.ConsoleUtils.clearProgress();
|
|
144
181
|
}
|
|
@@ -171,11 +208,23 @@ async function getAllPackageDataBatched(packageNames, onBatchReady, currentVersi
|
|
|
171
208
|
let completedCount = 0;
|
|
172
209
|
let batchStart = 0;
|
|
173
210
|
let batchIndex = 0;
|
|
211
|
+
const batchPromises = [];
|
|
212
|
+
const pendingEmissions = new Map();
|
|
213
|
+
let nextEmitIndex = 0;
|
|
214
|
+
const flushPending = () => {
|
|
215
|
+
while (pendingEmissions.has(nextEmitIndex)) {
|
|
216
|
+
const ready = pendingEmissions.get(nextEmitIndex);
|
|
217
|
+
pendingEmissions.delete(nextEmitIndex);
|
|
218
|
+
onBatchReady?.(ready);
|
|
219
|
+
nextEmitIndex++;
|
|
220
|
+
}
|
|
221
|
+
};
|
|
174
222
|
while (batchStart < packageNames.length) {
|
|
175
223
|
const batchSize = batchSizes[Math.min(batchIndex, batchSizes.length - 1)];
|
|
176
224
|
const batchNames = packageNames.slice(batchStart, batchStart + batchSize);
|
|
225
|
+
const capturedBatchIndex = batchIndex;
|
|
177
226
|
const batchResults = new Array(batchNames.length);
|
|
178
|
-
|
|
227
|
+
const batchPromise = runWithConcurrencyLimit(batchNames, concurrency, async (packageName, itemIndex) => {
|
|
179
228
|
const data = await getFreshPackageData(packageName, currentVersions?.get(packageName));
|
|
180
229
|
packageData.set(packageName, data);
|
|
181
230
|
completedCount++;
|
|
@@ -184,14 +233,18 @@ async function getAllPackageDataBatched(packageNames, onBatchReady, currentVersi
|
|
|
184
233
|
data,
|
|
185
234
|
completed: completedCount,
|
|
186
235
|
total,
|
|
187
|
-
batchIndex,
|
|
236
|
+
batchIndex: capturedBatchIndex,
|
|
188
237
|
itemIndex,
|
|
189
238
|
};
|
|
239
|
+
}).then(() => {
|
|
240
|
+
pendingEmissions.set(capturedBatchIndex, batchResults.filter(Boolean));
|
|
241
|
+
flushPending();
|
|
190
242
|
});
|
|
191
|
-
|
|
243
|
+
batchPromises.push(batchPromise);
|
|
192
244
|
batchStart += batchSize;
|
|
193
245
|
batchIndex++;
|
|
194
246
|
}
|
|
247
|
+
await Promise.all(batchPromises);
|
|
195
248
|
return packageData;
|
|
196
249
|
}
|
|
197
250
|
/**
|
|
@@ -200,4 +253,7 @@ async function getAllPackageDataBatched(packageNames, onBatchReady, currentVersi
|
|
|
200
253
|
function clearPackageCache() {
|
|
201
254
|
inFlightLookups.clear();
|
|
202
255
|
}
|
|
256
|
+
async function closeRegistryPool() {
|
|
257
|
+
await registryPool.close();
|
|
258
|
+
}
|
|
203
259
|
//# 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) {
|
|
@@ -76,6 +99,11 @@ class InputHandler {
|
|
|
76
99
|
}
|
|
77
100
|
return;
|
|
78
101
|
}
|
|
102
|
+
// '!' toggles the debug/performance modal (outside of filter mode)
|
|
103
|
+
if (str === '!' && !uiState.filterMode) {
|
|
104
|
+
this.onAction({ type: 'toggle_debug_modal' });
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
79
107
|
// Check for '/' character to handle filter mode (only when not in modal)
|
|
80
108
|
if (str === '/') {
|
|
81
109
|
if (uiState.filterMode) {
|
|
@@ -9,8 +9,42 @@ class ModalManager {
|
|
|
9
9
|
isLoadingModalInfo: false,
|
|
10
10
|
infoModalScrollOffset: 0,
|
|
11
11
|
infoModalSessionId: 0,
|
|
12
|
+
showDebugModal: false,
|
|
13
|
+
debugModalScrollOffset: 0,
|
|
12
14
|
};
|
|
13
15
|
}
|
|
16
|
+
isDebugModalOpen() {
|
|
17
|
+
return this.state.showDebugModal;
|
|
18
|
+
}
|
|
19
|
+
toggleDebugModal() {
|
|
20
|
+
this.state.showDebugModal = !this.state.showDebugModal;
|
|
21
|
+
this.state.debugModalScrollOffset = 0;
|
|
22
|
+
}
|
|
23
|
+
closeDebugModal() {
|
|
24
|
+
this.state.showDebugModal = false;
|
|
25
|
+
this.state.debugModalScrollOffset = 0;
|
|
26
|
+
}
|
|
27
|
+
scrollDebugModalUp() {
|
|
28
|
+
if (this.state.debugModalScrollOffset > 0) {
|
|
29
|
+
this.state.debugModalScrollOffset--;
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
scrollDebugModalDown(maxOffset) {
|
|
35
|
+
if (this.state.debugModalScrollOffset < maxOffset) {
|
|
36
|
+
this.state.debugModalScrollOffset++;
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
clampDebugModalScrollOffset(maxOffset) {
|
|
42
|
+
const nextOffset = Math.max(0, Math.min(this.state.debugModalScrollOffset, maxOffset));
|
|
43
|
+
if (nextOffset === this.state.debugModalScrollOffset)
|
|
44
|
+
return false;
|
|
45
|
+
this.state.debugModalScrollOffset = nextOffset;
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
14
48
|
getState() {
|
|
15
49
|
return { ...this.state };
|
|
16
50
|
}
|
|
@@ -43,6 +43,8 @@ class StateManager {
|
|
|
43
43
|
infoModalRow: modalState.infoModalRow,
|
|
44
44
|
isLoadingModalInfo: modalState.isLoadingModalInfo,
|
|
45
45
|
infoModalScrollOffset: modalState.infoModalScrollOffset,
|
|
46
|
+
showDebugModal: modalState.showDebugModal,
|
|
47
|
+
debugModalScrollOffset: modalState.debugModalScrollOffset,
|
|
46
48
|
filterMode: filterState.filterMode,
|
|
47
49
|
filterQuery: filterState.filterQuery,
|
|
48
50
|
showThemeModal: themeState.showThemeModal,
|
|
@@ -187,6 +189,21 @@ class StateManager {
|
|
|
187
189
|
clampInfoModalScrollOffset(maxOffset) {
|
|
188
190
|
return this.modalManager.clampScrollOffset(maxOffset);
|
|
189
191
|
}
|
|
192
|
+
toggleDebugModal() {
|
|
193
|
+
this.modalManager.toggleDebugModal();
|
|
194
|
+
}
|
|
195
|
+
closeDebugModal() {
|
|
196
|
+
this.modalManager.closeDebugModal();
|
|
197
|
+
}
|
|
198
|
+
scrollDebugModalUp() {
|
|
199
|
+
return this.modalManager.scrollDebugModalUp();
|
|
200
|
+
}
|
|
201
|
+
scrollDebugModalDown(maxOffset) {
|
|
202
|
+
return this.modalManager.scrollDebugModalDown(maxOffset);
|
|
203
|
+
}
|
|
204
|
+
clampDebugModalScrollOffset(maxOffset) {
|
|
205
|
+
return this.modalManager.clampDebugModalScrollOffset(maxOffset);
|
|
206
|
+
}
|
|
190
207
|
// Filter delegation
|
|
191
208
|
enterFilterMode(preserveQuery = false) {
|
|
192
209
|
this.filterManager.enterFilterMode(preserveQuery);
|
package/package.json
CHANGED