inup 1.5.4 → 1.5.6

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.
Files changed (65) hide show
  1. package/README.md +24 -10
  2. package/dist/cli.js +20 -22
  3. package/dist/config/constants.js +4 -2
  4. package/dist/config/package-meta.js +10 -0
  5. package/dist/config/project-config.js +6 -1
  6. package/dist/core/package-detector.js +8 -4
  7. package/dist/core/upgrade-runner.js +7 -10
  8. package/dist/core/upgrader.js +6 -8
  9. package/dist/features/changelog/clients/github-client.js +5 -6
  10. package/dist/features/changelog/services/changelog-service.js +2 -4
  11. package/dist/features/changelog/services/package-metadata-service.js +6 -11
  12. package/dist/features/changelog/services/release-notes-service.js +4 -2
  13. package/dist/features/debug/services/performance-tracker.js +6 -8
  14. package/dist/index.js +1 -2
  15. package/dist/interactive-ui.js +30 -587
  16. package/dist/services/background-audit.js +3 -5
  17. package/dist/services/cache-manager.js +5 -7
  18. package/dist/services/http/inflight.js +22 -0
  19. package/dist/services/http/retry.js +30 -0
  20. package/dist/services/jsdelivr/client.js +191 -0
  21. package/dist/services/jsdelivr/manifest.js +136 -0
  22. package/dist/services/jsdelivr-registry.js +6 -392
  23. package/dist/services/npm-registry.js +12 -81
  24. package/dist/services/persistent-cache.js +8 -13
  25. package/dist/types/domain.js +3 -0
  26. package/dist/types/streaming.js +3 -0
  27. package/dist/types/ui.js +3 -0
  28. package/dist/types.js +17 -0
  29. package/dist/ui/controllers/package-info-modal-controller.js +22 -31
  30. package/dist/ui/controllers/vulnerability-audit-controller.js +17 -8
  31. package/dist/ui/index.js +3 -0
  32. package/dist/ui/input-handler.js +62 -77
  33. package/dist/ui/keymap.js +208 -0
  34. package/dist/ui/modal/package-info-sections/release-notes.js +161 -0
  35. package/dist/ui/modal/package-info-sections/sections.js +150 -0
  36. package/dist/ui/modal/package-info-sections/text.js +75 -0
  37. package/dist/ui/modal/package-info-sections.js +15 -368
  38. package/dist/ui/modal/package-info.js +1 -1
  39. package/dist/ui/renderer/help-modal.js +75 -0
  40. package/dist/ui/renderer/index.js +2 -5
  41. package/dist/ui/renderer/package-list/interface.js +184 -0
  42. package/dist/ui/renderer/package-list/rows.js +172 -0
  43. package/dist/ui/renderer/package-list.js +15 -462
  44. package/dist/ui/session/action-dispatcher.js +233 -0
  45. package/dist/ui/session/index.js +13 -0
  46. package/dist/ui/session/interactive-session.js +280 -0
  47. package/dist/ui/session/selection-state-builder.js +144 -0
  48. package/dist/ui/state/filter-manager.js +17 -6
  49. package/dist/ui/state/modal-manager.js +35 -0
  50. package/dist/ui/state/navigation-manager.js +33 -4
  51. package/dist/ui/state/state-manager.js +68 -3
  52. package/dist/ui/state/theme-manager.js +1 -0
  53. package/dist/ui/themes-colors.js +8 -0
  54. package/dist/ui/themes.js +11 -19
  55. package/dist/ui/utils/cursor.js +3 -4
  56. package/dist/ui/utils/index.js +6 -1
  57. package/dist/ui/utils/text.js +3 -2
  58. package/dist/ui/utils/version.js +60 -86
  59. package/dist/utils/config.js +13 -1
  60. package/dist/utils/filesystem/io.js +87 -0
  61. package/dist/utils/filesystem/paths.js +19 -0
  62. package/dist/utils/filesystem/scan.js +205 -0
  63. package/dist/utils/filesystem.js +17 -335
  64. package/dist/utils/version.js +31 -1
  65. package/package.json +10 -9
@@ -1,49 +1,9 @@
1
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 __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
- var __importDefault = (this && this.__importDefault) || function (mod) {
36
- return (mod && mod.__esModule) ? mod : { "default": mod };
37
- };
38
2
  Object.defineProperty(exports, "__esModule", { value: true });
39
3
  exports.InteractiveUI = void 0;
40
- const chalk_1 = __importDefault(require("chalk"));
41
- const semver = __importStar(require("semver"));
42
4
  const ui_1 = require("./ui");
43
5
  const controllers_1 = require("./ui/controllers");
44
- const themes_1 = require("./ui/themes");
45
- const themes_colors_1 = require("./ui/themes-colors");
46
- const debug_1 = require("./features/debug");
6
+ const session_1 = require("./ui/session");
47
7
  const DEFAULT_VULNERABILITY_DISPLAY_OPTIONS = {
48
8
  showPeerDependencyVulnerabilities: false,
49
9
  showOptionalDependencyVulnerabilities: false,
@@ -57,9 +17,13 @@ function normalizeVulnerabilityDisplayOptions(options) {
57
17
  };
58
18
  }
59
19
  class InteractiveUI {
20
+ renderer;
21
+ packageManager;
22
+ options;
23
+ vulnerabilityAuditController = new controllers_1.VulnerabilityAuditController();
24
+ packageInfoModalController = new controllers_1.PackageInfoModalController();
25
+ refreshView;
60
26
  constructor(packageManager, options) {
61
- this.vulnerabilityAuditController = new controllers_1.VulnerabilityAuditController();
62
- this.packageInfoModalController = new controllers_1.PackageInfoModalController();
63
27
  this.renderer = new ui_1.UIRenderer();
64
28
  this.packageManager = packageManager;
65
29
  this.options = normalizeVulnerabilityDisplayOptions(options);
@@ -72,65 +36,14 @@ class InteractiveUI {
72
36
  if (selectionStates.length === 0) {
73
37
  return [];
74
38
  }
75
- const selectedStates = await this.interactiveTableSelector(selectionStates);
76
- return this.createUpgradeChoices(selectedStates);
39
+ const selectedStates = await (0, session_1.runInteractiveSession)(selectionStates, this.packageManager, this.renderer, this.packageInfoModalController, this.vulnerabilityAuditController, this.options, (refresh) => { this.refreshView = refresh; });
40
+ return (0, session_1.createUpgradeChoices)(selectedStates);
77
41
  }
78
42
  createSelectionStates(packages, previousSelections, includeUpToDate = true) {
79
- const relevantPackages = includeUpToDate ? packages : packages.filter((p) => p.isOutdated);
80
- const uniquePackages = this.deduplicatePackages(relevantPackages);
81
- return Array.from(uniquePackages.values()).map(({ pkg, packageJsonPaths }) => {
82
- const currentClean = semver.coerce(pkg.currentVersion)?.version || pkg.currentVersion;
83
- const rangeClean = semver.coerce(pkg.rangeVersion)?.version || pkg.rangeVersion;
84
- const latestClean = semver.coerce(pkg.latestVersion)?.version || pkg.latestVersion;
85
- const key = `${pkg.name}@${pkg.currentVersion}@${pkg.type}`;
86
- const previousSelection = previousSelections?.get(key) || 'none';
87
- return {
88
- name: pkg.name,
89
- packageJsonPath: pkg.packageJsonPath,
90
- packageJsonPaths: Array.from(packageJsonPaths),
91
- currentVersionSpecifier: pkg.currentVersion,
92
- currentVersion: currentClean,
93
- rangeVersion: rangeClean,
94
- latestVersion: latestClean,
95
- selectedOption: previousSelection,
96
- loadState: 'ready',
97
- hasRangeUpdate: pkg.hasRangeUpdate,
98
- hasMajorUpdate: pkg.hasMajorUpdate,
99
- type: pkg.type,
100
- vulnerability: this.vulnerabilityAuditController.getCachedSummary(pkg.name, pkg.currentVersion, pkg.type),
101
- allVersions: pkg.allVersions,
102
- };
103
- });
43
+ return (0, session_1.createSelectionStates)(packages, (name, version, type) => this.vulnerabilityAuditController.getCachedSummary(name, version, type), previousSelections, includeUpToDate);
104
44
  }
105
45
  createPendingSelectionStates(packages, previousSelections) {
106
- const uniquePackages = this.deduplicatePackages(packages.map((pkg) => ({
107
- ...pkg,
108
- rangeVersion: pkg.currentVersion,
109
- latestVersion: pkg.currentVersion,
110
- isOutdated: false,
111
- hasRangeUpdate: false,
112
- hasMajorUpdate: false,
113
- })));
114
- return Array.from(uniquePackages.values()).map(({ pkg, packageJsonPaths }) => {
115
- const currentClean = semver.coerce(pkg.currentVersion)?.version || pkg.currentVersion;
116
- const key = `${pkg.name}@${pkg.currentVersion}@${pkg.type}`;
117
- const previousSelection = previousSelections?.get(key) || 'none';
118
- return {
119
- name: pkg.name,
120
- packageJsonPath: pkg.packageJsonPath,
121
- packageJsonPaths: Array.from(packageJsonPaths),
122
- currentVersionSpecifier: pkg.currentVersion,
123
- currentVersion: currentClean,
124
- rangeVersion: 'loading',
125
- latestVersion: 'loading',
126
- selectedOption: previousSelection,
127
- loadState: 'pending',
128
- hasRangeUpdate: false,
129
- hasMajorUpdate: false,
130
- type: pkg.type,
131
- vulnerability: this.vulnerabilityAuditController.getCachedSummary(pkg.name, pkg.currentVersion, pkg.type),
132
- };
133
- });
46
+ return (0, session_1.createPendingSelectionStates)(packages, (name, version, type) => this.vulnerabilityAuditController.getCachedSummary(name, version, type), previousSelections);
134
47
  }
135
48
  appendOutdatedBatchToSelectionStates(selectionStates, batch, previousSelections) {
136
49
  const outdatedStates = this.createSelectionStates(batch.flatMap((batchItem) => batchItem.packageInfo).filter((pkg) => pkg.isOutdated), previousSelections, false);
@@ -149,498 +62,12 @@ class InteractiveUI {
149
62
  }
150
63
  async selectPackagesToUpgradeProgressive(selectionStates, progress, attachRefresh) {
151
64
  this.enqueueSecurityAudit(selectionStates);
152
- const selectedStates = await this.interactiveTableSelector(selectionStates, progress, attachRefresh);
153
- return this.createUpgradeChoices(selectedStates);
65
+ const selectedStates = await (0, session_1.runInteractiveSession)(selectionStates, this.packageManager, this.renderer, this.packageInfoModalController, this.vulnerabilityAuditController, this.options, (refresh) => { this.refreshView = refresh; }, progress, attachRefresh);
66
+ return (0, session_1.createUpgradeChoices)(selectedStates);
154
67
  }
155
68
  enqueueSecurityAudit(selectionStates) {
156
69
  this.vulnerabilityAuditController.enqueueStates(selectionStates, () => this.refreshView?.());
157
70
  }
158
- deduplicatePackages(packages) {
159
- const uniquePackages = new Map();
160
- for (const pkg of packages) {
161
- const key = `${pkg.name}@${pkg.currentVersion}@${pkg.type}`;
162
- if (!uniquePackages.has(key)) {
163
- uniquePackages.set(key, {
164
- pkg,
165
- packageJsonPaths: new Set([pkg.packageJsonPath]),
166
- });
167
- }
168
- else {
169
- uniquePackages.get(key).packageJsonPaths.add(pkg.packageJsonPath);
170
- }
171
- }
172
- return new Map(Array.from(uniquePackages.entries()).sort(([, a], [, b]) => {
173
- const aIsScoped = a.pkg.name.startsWith('@');
174
- const bIsScoped = b.pkg.name.startsWith('@');
175
- if (aIsScoped && !bIsScoped)
176
- return -1;
177
- if (!aIsScoped && bIsScoped)
178
- return 1;
179
- return a.pkg.name.localeCompare(b.pkg.name);
180
- }));
181
- }
182
- createUpgradeChoices(selectedStates) {
183
- const choices = [];
184
- selectedStates
185
- .filter((state) => state.loadState === 'ready' && state.selectedOption !== 'none')
186
- .forEach((state) => {
187
- const targetVersion = state.selectedOption === 'range' ? state.rangeVersion : state.latestVersion;
188
- const targetVersionWithPrefix = ui_1.VersionUtils.applyVersionPrefix(state.currentVersionSpecifier, targetVersion);
189
- const pathsToUpdate = state.packageJsonPaths || [state.packageJsonPath];
190
- pathsToUpdate.forEach((packageJsonPath) => {
191
- choices.push({
192
- name: state.name,
193
- packageJsonPath,
194
- dependencyType: state.type,
195
- upgradeType: state.selectedOption,
196
- targetVersion: targetVersionWithPrefix,
197
- currentVersionSpecifier: state.currentVersionSpecifier,
198
- });
199
- });
200
- });
201
- return choices;
202
- }
203
- getTerminalHeight() {
204
- // Check if stdout is a TTY and has rows property
205
- if (process.stdout.isTTY &&
206
- typeof process.stdout.rows === 'number' &&
207
- process.stdout.rows > 0) {
208
- return process.stdout.rows;
209
- }
210
- return 24; // Fallback default
211
- }
212
- async interactiveTableSelector(selectionStates, loadingProgress, attachRefresh) {
213
- return new Promise((resolve) => {
214
- const states = selectionStates;
215
- const stateManager = new ui_1.StateManager(0, this.getTerminalHeight());
216
- let isResolved = false;
217
- let ownsAlternateScreen = false;
218
- const vulnerabilityDisplayOptions = this.options;
219
- const claimInteractiveScreen = () => {
220
- if (ownsAlternateScreen) {
221
- return;
222
- }
223
- ui_1.ConsoleUtils.clearProgress();
224
- ui_1.CursorUtils.enterAlternateScreen();
225
- ui_1.CursorUtils.clearScreen();
226
- ownsAlternateScreen = true;
227
- };
228
- const releaseInteractiveScreen = () => {
229
- if (!ownsAlternateScreen) {
230
- return;
231
- }
232
- ui_1.CursorUtils.exitAlternateScreen();
233
- ownsAlternateScreen = false;
234
- };
235
- // No grouping needed - packages are already filtered by type
236
- // This simplifies scrolling and avoids rendering issues
237
- stateManager.setRenderableItems([]);
238
- // Track the current max scroll offset for the info modal
239
- let infoModalMaxScrollOffset = 0;
240
- // Track the current max scroll offset for the debug modal
241
- let debugModalMaxScrollOffset = 0;
242
- let previousViewportMode = null;
243
- let previousModalViewportLineCount = null;
244
- const handleAction = (action) => {
245
- const uiState = stateManager.getUIState();
246
- const filteredStates = stateManager.getFilteredStates(states, vulnerabilityDisplayOptions);
247
- switch (action.type) {
248
- case 'navigate_up':
249
- if (!uiState.showThemeModal) {
250
- stateManager.navigateUp(filteredStates.length);
251
- }
252
- break;
253
- case 'navigate_down':
254
- if (!uiState.showThemeModal) {
255
- stateManager.navigateDown(filteredStates.length);
256
- }
257
- break;
258
- case 'select_left':
259
- if (!uiState.showThemeModal) {
260
- stateManager.updateSelection(filteredStates, 'left');
261
- }
262
- break;
263
- case 'select_right':
264
- if (!uiState.showThemeModal) {
265
- stateManager.updateSelection(filteredStates, 'right');
266
- }
267
- break;
268
- case 'bulk_select_minor':
269
- if (!uiState.showThemeModal) {
270
- stateManager.bulkSelectMinor(filteredStates);
271
- }
272
- break;
273
- case 'bulk_select_latest':
274
- if (!uiState.showThemeModal) {
275
- stateManager.bulkSelectLatest(filteredStates);
276
- }
277
- break;
278
- case 'bulk_unselect_all':
279
- if (!uiState.showThemeModal) {
280
- stateManager.bulkUnselectAll(filteredStates);
281
- }
282
- break;
283
- case 'toggle_dep_type_filter':
284
- if (!uiState.showThemeModal) {
285
- stateManager.toggleDependencyTypeFilter(action.depType);
286
- }
287
- break;
288
- case 'toggle_info_modal':
289
- if (!uiState.showInfoModal) {
290
- // Opening modal - load package info asynchronously
291
- const modalSessionId = stateManager.toggleInfoModal();
292
- const currentState = filteredStates[uiState.currentRow];
293
- const canFetchMetadata = currentState?.loadState === 'ready';
294
- stateManager.setModalLoading(canFetchMetadata, modalSessionId);
295
- renderInterface();
296
- if (currentState && canFetchMetadata) {
297
- this.packageInfoModalController
298
- .hydrate(currentState)
299
- .then(() => {
300
- if (isResolved || stateManager.getInfoModalSessionId() !== modalSessionId)
301
- return;
302
- stateManager.setModalLoading(false, modalSessionId);
303
- renderInterface();
304
- // Auto-load the first version's release notes
305
- if (stateManager.getInfoModalSessionId() === modalSessionId &&
306
- this.packageInfoModalController.getVersionCount(currentState) > 0) {
307
- this.packageInfoModalController.loadVersionAtIndex(currentState, 0, () => {
308
- if (!isResolved)
309
- renderInterface();
310
- });
311
- }
312
- })
313
- .catch(() => {
314
- if (isResolved || stateManager.getInfoModalSessionId() !== modalSessionId)
315
- return;
316
- stateManager.setModalLoading(false, modalSessionId);
317
- renderInterface();
318
- });
319
- }
320
- }
321
- else {
322
- // Closing modal - cancel in-flight fetches
323
- this.packageInfoModalController.cancel();
324
- stateManager.toggleInfoModal();
325
- renderInterface();
326
- }
327
- break;
328
- case 'scroll_info_modal_up':
329
- if (!stateManager.scrollInfoModalUp()) {
330
- return;
331
- }
332
- break;
333
- case 'scroll_info_modal_down':
334
- if (!stateManager.scrollInfoModalDown(infoModalMaxScrollOffset)) {
335
- return;
336
- }
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
- }
356
- case 'navigate_info_modal_version':
357
- {
358
- if (uiState.infoModalRow >= 0 && uiState.infoModalRow < filteredStates.length) {
359
- const currentState = filteredStates[uiState.infoModalRow];
360
- const newIndex = this.packageInfoModalController.navigateVersion(currentState, action.direction);
361
- if (newIndex >= 0) {
362
- // Reset scroll to top when switching versions
363
- stateManager.resetInfoModalScroll();
364
- // Load the version if not already loaded
365
- if (!this.packageInfoModalController.isVersionLoaded(currentState, newIndex)) {
366
- this.packageInfoModalController.loadVersionAtIndex(currentState, newIndex, () => {
367
- if (!isResolved)
368
- renderInterface();
369
- });
370
- }
371
- }
372
- else {
373
- return;
374
- }
375
- }
376
- else {
377
- return;
378
- }
379
- }
380
- break;
381
- case 'enter_filter_mode':
382
- stateManager.enterFilterMode(action.preserveQuery);
383
- break;
384
- case 'exit_filter_mode':
385
- stateManager.exitFilterMode(action.clearQuery);
386
- break;
387
- case 'filter_input':
388
- stateManager.appendToFilterQuery(action.char);
389
- // Re-calculate filtered states after input
390
- break;
391
- case 'filter_backspace':
392
- stateManager.deleteFromFilterQuery();
393
- // Re-calculate filtered states after backspace
394
- break;
395
- case 'resize':
396
- const heightChanged = stateManager.updateTerminalHeight(action.height);
397
- if (heightChanged) {
398
- stateManager.resetForResize(filteredStates.length);
399
- }
400
- else {
401
- // Even if height didn't change, width might have changed
402
- // Force a full re-render to clear any wrapping issues
403
- stateManager.setInitialRender(true);
404
- }
405
- break;
406
- case 'toggle_theme_modal':
407
- stateManager.toggleThemeModal();
408
- break;
409
- case 'theme_navigate_up': {
410
- const themeManager = stateManager.getThemeManager();
411
- const currentIndex = themes_1.themeNames.indexOf(themeManager.getPreviewTheme());
412
- const themeArray = Object.keys(themes_1.themes);
413
- const nextIndex = currentIndex > 0 ? currentIndex - 1 : themeArray.length - 1;
414
- stateManager.previewTheme(themeArray[nextIndex]);
415
- break;
416
- }
417
- case 'theme_navigate_down': {
418
- const themeManager = stateManager.getThemeManager();
419
- const currentIndex = themes_1.themeNames.indexOf(themeManager.getPreviewTheme());
420
- const themeArray = Object.keys(themes_1.themes);
421
- const nextIndex = currentIndex < themeArray.length - 1 ? currentIndex + 1 : 0;
422
- stateManager.previewTheme(themeArray[nextIndex]);
423
- break;
424
- }
425
- case 'theme_confirm':
426
- stateManager.confirmTheme();
427
- break;
428
- case 'trigger_audit_scan':
429
- if (!uiState.showThemeModal) {
430
- const auditProgress = this.vulnerabilityAuditController.getProgress();
431
- if (auditProgress.hasData) {
432
- stateManager.toggleVulnerableFilter();
433
- }
434
- else if (!auditProgress.isRunning) {
435
- this.enqueueSecurityAudit(states);
436
- }
437
- }
438
- break;
439
- case 'cancel':
440
- this.packageInfoModalController.cancel();
441
- handleCancel();
442
- return;
443
- }
444
- if (action.type !== 'toggle_info_modal') {
445
- renderInterface();
446
- }
447
- };
448
- const handleConfirm = (selectedStates) => {
449
- finalizeSelection(selectedStates);
450
- };
451
- const handleCancel = () => {
452
- finalizeSelection(states.map((s) => ({ ...s, selectedOption: 'none' })));
453
- };
454
- const inputHandler = new ui_1.InputHandler(stateManager, handleAction, handleConfirm, handleCancel);
455
- const resetAnsiPattern = /\x1b\[(?:0|49)m/g;
456
- const packageListRenderOptions = {
457
- showPeerDependencyVulnerabilities: this.options.showPeerDependencyVulnerabilities,
458
- showOptionalDependencyVulnerabilities: this.options.showOptionalDependencyVulnerabilities,
459
- };
460
- const keypressHandler = (str, key) => inputHandler.handleKeypress(str, key, states);
461
- const buildRemainingViewport = (terminalWidth, terminalHeight, usedLines) => {
462
- const remainingLines = Math.max(0, terminalHeight - usedLines);
463
- const blankLine = ' '.repeat(terminalWidth);
464
- return Array.from({ length: remainingLines }, () => blankLine);
465
- };
466
- const applyBackgroundToLine = (line, bgCode) => `${bgCode}${line.replace(resetAnsiPattern, (match) => `${match}${bgCode}`)}${(0, themes_colors_1.getTerminalResetCode)()}`;
467
- const writeFrame = (lines, bgCode) => {
468
- if (lines.length === 0) {
469
- return;
470
- }
471
- process.stdout.write(lines.map((line) => applyBackgroundToLine(line, bgCode)).join('\n'));
472
- };
473
- const buildModalHeaderLines = (shortcutLabel) => [
474
- ' ' + chalk_1.default.bold.magenta('🚀 inup'),
475
- '',
476
- ' ' + shortcutLabel,
477
- '',
478
- ];
479
- const renderViewport = (lines, terminalWidth, terminalHeight, bgCode) => {
480
- const viewportLines = [
481
- ...lines,
482
- ...buildRemainingViewport(terminalWidth, terminalHeight, lines.length),
483
- ];
484
- writeFrame(viewportLines, bgCode);
485
- };
486
- const renderModalViewport = (mode, shortcutLabel, modalLines, terminalWidth, terminalHeight, bgCode) => {
487
- const viewportLineCount = buildModalHeaderLines(shortcutLabel).length + modalLines.length;
488
- const shouldClearBeforeRender = previousViewportMode !== mode || previousModalViewportLineCount !== viewportLineCount;
489
- if (shouldClearBeforeRender) {
490
- ui_1.CursorUtils.clearScreen();
491
- ui_1.CursorUtils.hide();
492
- }
493
- renderViewport([...buildModalHeaderLines(shortcutLabel), ...modalLines], terminalWidth, terminalHeight, bgCode);
494
- previousViewportMode = mode;
495
- previousModalViewportLineCount = viewportLineCount;
496
- stateManager.markRendered([]);
497
- };
498
- let cleanupInteractiveSession = () => {
499
- process.stdout.write((0, themes_colors_1.getTerminalResetCode)());
500
- ui_1.CursorUtils.show();
501
- process.stdin.off('keypress', keypressHandler);
502
- process.stdin.pause();
503
- process.off('SIGWINCH', handleResize);
504
- this.refreshView = undefined;
505
- };
506
- const finalizeSelection = (selectedStates) => {
507
- isResolved = true;
508
- this.packageInfoModalController.cancel();
509
- releaseInteractiveScreen();
510
- cleanupInteractiveSession();
511
- resolve(selectedStates);
512
- };
513
- const renderInterface = () => {
514
- const uiState = stateManager.getUIState();
515
- const filteredStates = stateManager.getFilteredStates(states, vulnerabilityDisplayOptions);
516
- const auditProgress = this.vulnerabilityAuditController.getProgress();
517
- // Apply terminal background color
518
- const bgCode = (0, themes_colors_1.getTerminalBgColorCode)();
519
- process.stdout.write(bgCode);
520
- if (uiState.forceFullRender) {
521
- ui_1.CursorUtils.clearScreen();
522
- ui_1.CursorUtils.hide();
523
- }
524
- else {
525
- ui_1.CursorUtils.moveToHome();
526
- }
527
- // If theme modal is open, render only the theme selector
528
- if (uiState.showThemeModal) {
529
- const terminalWidth = process.stdout.columns || 80;
530
- const terminalHeight = this.getTerminalHeight();
531
- const themeManager = stateManager.getThemeManager();
532
- const modalLines = this.renderer.renderThemeSelectorModal(themeManager.getCurrentTheme(), themeManager.getPreviewTheme(), terminalWidth, terminalHeight);
533
- renderModalViewport('theme-modal', chalk_1.default.bold.white('T ') + chalk_1.default.gray('/ Esc Exit theme selector'), modalLines, terminalWidth, terminalHeight, bgCode);
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
- }
547
- else if (uiState.showInfoModal &&
548
- uiState.infoModalRow >= 0 &&
549
- uiState.infoModalRow < filteredStates.length) {
550
- const selectedState = filteredStates[uiState.infoModalRow];
551
- const terminalWidth = process.stdout.columns || 80;
552
- const terminalHeight = this.getTerminalHeight();
553
- if (uiState.isLoadingModalInfo) {
554
- // Show loading state
555
- const result = this.renderer.renderPackageInfoLoading(selectedState, terminalWidth, Math.max(8, terminalHeight - 4));
556
- infoModalMaxScrollOffset = result.maxScrollOffset;
557
- renderModalViewport('info-modal', chalk_1.default.bold.white('I / Esc ') + chalk_1.default.gray('Exit this view'), result.lines, terminalWidth, terminalHeight, bgCode);
558
- }
559
- else {
560
- // Show full info with scroll support
561
- const activeTab = uiState.infoModalTab;
562
- const result = this.renderer.renderPackageInfoModal(selectedState, terminalWidth, Math.max(8, terminalHeight - 4), uiState.infoModalScrollOffset, activeTab);
563
- infoModalMaxScrollOffset = result.maxScrollOffset;
564
- stateManager.clampInfoModalScrollOffset(infoModalMaxScrollOffset);
565
- const scrollHint = result.usesInternalScroll && result.maxScrollOffset > 0
566
- ? chalk_1.default.bold.white('↑/↓ ') + chalk_1.default.gray('Scroll · ')
567
- : '';
568
- const versionHint = activeTab === 'info'
569
- ? chalk_1.default.bold.white('←/→ ') + chalk_1.default.gray('Version · ')
570
- : '';
571
- renderModalViewport('info-modal', scrollHint +
572
- versionHint +
573
- chalk_1.default.bold.white('Tab ') +
574
- chalk_1.default.gray('Switch tab · ') +
575
- chalk_1.default.bold.white('I / Esc ') +
576
- chalk_1.default.gray('Exit this view'), result.lines, terminalWidth, terminalHeight, bgCode);
577
- }
578
- }
579
- else {
580
- // Normal list view (flat rendering - no grouping)
581
- const terminalWidth = process.stdout.columns || 80;
582
- const terminalHeight = this.getTerminalHeight();
583
- const activeFilterLabel = stateManager.getActiveFilterLabel();
584
- const lines = this.renderer.renderInterface(filteredStates, uiState.currentRow, uiState.scrollOffset, uiState.maxVisibleItems, uiState.forceFullRender, [], // No renderable items - use flat rendering
585
- activeFilterLabel, // Show current dependency type filter state
586
- this.packageManager, // Pass package manager info for header
587
- uiState.filterMode, uiState.filterQuery, states.length, terminalWidth, loadingProgress, auditProgress, packageListRenderOptions);
588
- renderViewport(lines, terminalWidth, terminalHeight, bgCode);
589
- previousViewportMode = 'list';
590
- previousModalViewportLineCount = null;
591
- stateManager.markRendered(lines);
592
- }
593
- stateManager.setInitialRender(false);
594
- };
595
- const handleResize = () => {
596
- // On resize (width or height change), always trigger a re-render
597
- // This prevents layout breaking when terminal width changes
598
- // The action handler will update height and force a full re-render
599
- inputHandler.handleResize(this.getTerminalHeight());
600
- };
601
- // Setup keypress handling
602
- try {
603
- claimInteractiveScreen();
604
- this.refreshView = () => {
605
- if (!isResolved) {
606
- renderInterface();
607
- }
608
- };
609
- attachRefresh?.(() => {
610
- if (!isResolved) {
611
- renderInterface();
612
- }
613
- });
614
- const keypressSession = ui_1.TerminalInput.startKeypressSession(keypressHandler);
615
- const previousCleanup = cleanupInteractiveSession;
616
- cleanupInteractiveSession = () => {
617
- keypressSession.close();
618
- previousCleanup();
619
- };
620
- // Setup resize handler
621
- process.on('SIGWINCH', handleResize);
622
- // Update terminal height directly before initial render to ensure correct dimensions
623
- // This handles cases where process.stdout.rows might not be accurate at startup
624
- const currentHeight = this.getTerminalHeight();
625
- if (stateManager.updateTerminalHeight(currentHeight)) {
626
- const initialFiltered = stateManager.getFilteredStates(states, vulnerabilityDisplayOptions);
627
- stateManager.resetForResize(initialFiltered.length);
628
- }
629
- // Initial render
630
- renderInterface();
631
- this.enqueueSecurityAudit(states);
632
- }
633
- catch (error) {
634
- releaseInteractiveScreen();
635
- // Reset terminal colors
636
- process.stdout.write((0, themes_colors_1.getTerminalResetCode)());
637
- this.refreshView = undefined;
638
- // Fallback to simple interface if raw mode fails
639
- console.log(chalk_1.default.yellow('Raw mode not available, using fallback interface...'));
640
- resolve(states);
641
- }
642
- });
643
- }
644
71
  async confirmUpgrade(choices) {
645
72
  console.log(this.renderer.renderConfirmation(choices));
646
73
  return new Promise((resolve) => {
@@ -651,7 +78,22 @@ class InteractiveUI {
651
78
  cleanupConfirmationSession();
652
79
  resolve(confirmed);
653
80
  };
654
- const inputHandler = new ui_1.ConfirmationInputHandler(handleConfirm);
81
+ // Safety net for the same reason as selectPackages — synchronous restore on exit.
82
+ // The confirmation screen does not enter the alternate screen, so alt-screen
83
+ // restoration is intentionally omitted here. If that ever changes, mirror the
84
+ // ownsAlternateScreen-gated pattern from selectPackages.
85
+ const confirmEmergencyCleanup = () => {
86
+ process.stdout.write('\x1b[?25h');
87
+ if (process.stdin.setRawMode) {
88
+ process.stdin.setRawMode(false);
89
+ }
90
+ };
91
+ process.on('exit', confirmEmergencyCleanup);
92
+ const handleConfirmWithCleanup = (confirmed) => {
93
+ handleConfirm(confirmed);
94
+ process.off('exit', confirmEmergencyCleanup);
95
+ };
96
+ const inputHandler = new ui_1.ConfirmationInputHandler(handleConfirmWithCleanup);
655
97
  const keypressHandler = (str, key) => inputHandler.handleKeypress(str, key);
656
98
  // Setup keypress handling
657
99
  try {
@@ -663,6 +105,7 @@ class InteractiveUI {
663
105
  ui_1.CursorUtils.hide();
664
106
  }
665
107
  catch (error) {
108
+ process.off('exit', confirmEmergencyCleanup);
666
109
  ui_1.TerminalInput.promptForConfirmation('Proceed with upgrade? [Y/n] ')
667
110
  .then(resolve)
668
111
  .catch(() => resolve(false));