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.
@@ -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 = 25;
48
- this.batchConcurrency = 5;
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.getAllPackageDataBatched)(prepared.uniquePackages, (batch) => {
95
- const batchItems = batch.map((batchItem) => {
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
- concurrency: this.batchConcurrency,
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
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=debug.types.js.map
@@ -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 result = this.renderer.renderPackageInfoModal(selectedState, terminalWidth, Math.max(8, terminalHeight - 4), uiState.infoModalScrollOffset);
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
- chalk_1.default.bold.white('←/→ ') +
536
- chalk_1.default.gray('Version · ') +
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.getAllPackageData = getAllPackageData;
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
- * Fetches package data from npm registry.
76
- * Falls back to jsDelivr when npm is temporarily unavailable.
77
- */
78
- async function fetchPackageFromRegistryWithFallback(packageName, currentVersion) {
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 url = `${config_1.NPM_REGISTRY_URL}/${encodeURIComponent(packageName)}`;
81
- const controller = new AbortController();
82
- const timeoutId = setTimeout(() => controller.abort(), config_1.REQUEST_TIMEOUT);
83
- try {
84
- const response = await fetch(url, {
85
- method: 'GET',
86
- headers: {
87
- accept: 'application/vnd.npm.install-v1+json',
88
- },
89
- signal: controller.signal,
90
- });
91
- clearTimeout(timeoutId);
92
- if (!response.ok) {
93
- if (isRetryableStatus(response.status)) {
94
- return await fetchFromJsdelivrFallback(packageName, currentVersion);
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
- const text = await response.text();
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
- };
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
- finally {
109
- clearTimeout(timeoutId);
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 await fetchFromJsdelivrFallback(packageName, currentVersion);
160
+ return { kind: 'transient' };
115
161
  }
116
- return { latestVersion: 'unknown', allVersions: [] };
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
- * Fetches package version data from npm registry for multiple packages.
121
- * Uses native fetch with timeout support for reliable performance.
122
- * Only returns valid semantic versions (X.Y.Z format, excluding pre-releases).
123
- */
124
- async function getAllPackageData(packageNames, onProgress, currentVersions) {
125
- const packageData = new Map();
126
- if (packageNames.length === 0) {
127
- return packageData;
128
- }
129
- const total = packageNames.length;
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 packageData;
180
+ return lastOutcome;
146
181
  }
147
- async function runWithConcurrencyLimit(items, concurrency, worker) {
148
- if (items.length === 0) {
149
- return;
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
- const limit = Math.max(1, Math.min(concurrency, items.length));
152
- let nextIndex = 0;
153
- const runWorker = async () => {
154
- while (nextIndex < items.length) {
155
- const currentIndex = nextIndex++;
156
- await worker(items[currentIndex], currentIndex);
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
- async function getAllPackageDataBatched(packageNames, onBatchReady, currentVersions, options = {}) {
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 ?? 20)];
169
- const concurrency = Math.max(1, options.concurrency ?? 5);
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
- await runWithConcurrencyLimit(batchNames, concurrency, async (packageName, itemIndex) => {
179
- const data = await getFreshPackageData(packageName, currentVersions?.get(packageName));
180
- packageData.set(packageName, data);
181
- completedCount++;
182
- batchResults[itemIndex] = {
183
- packageName,
184
- data,
185
- completed: completedCount,
186
- total,
187
- batchIndex,
188
- itemIndex,
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
- onBatchReady?.(batchResults.filter(Boolean));
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
@@ -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
- this.onAction({ type: 'navigate_info_modal_version', direction: 'newer' });
94
+ if (uiState.infoModalTab === 'info') {
95
+ this.onAction({ type: 'navigate_info_modal_version', direction: 'newer' });
96
+ }
69
97
  return;
70
98
  case 'right':
71
- this.onAction({ type: 'navigate_info_modal_version', direction: 'older' });
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 buildPackageInfoSections(state, modalWidth) {
244
- const title = chalk_1.default.cyan.bold(`Package: ${state.name}`);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "inup",
3
- "version": "1.5.1",
3
+ "version": "1.5.4",
4
4
  "description": "Interactive dependency upgrader for npm, yarn, pnpm & bun. Zero-config, monorepo-ready. Upgrade-interactive for every package manager.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {