inup 1.4.10 → 1.5.0

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 (50) hide show
  1. package/README.md +1 -7
  2. package/dist/cli.js +2 -1
  3. package/dist/config/constants.js +1 -2
  4. package/dist/config/project-config.js +6 -0
  5. package/dist/core/package-detector.js +163 -89
  6. package/dist/core/upgrade-runner.js +68 -16
  7. package/dist/features/changelog/clients/github-client.js +134 -0
  8. package/dist/features/changelog/clients/npm-registry-client.js +53 -0
  9. package/dist/features/changelog/index.js +19 -0
  10. package/dist/features/changelog/parsers/changelog-parser.js +68 -0
  11. package/dist/features/changelog/parsers/github-release-html-parser.js +61 -0
  12. package/dist/features/changelog/parsers/package-metadata.js +34 -0
  13. package/dist/features/changelog/parsers/repository-ref.js +26 -0
  14. package/dist/features/changelog/services/changelog-service.js +30 -0
  15. package/dist/features/changelog/services/package-metadata-service.js +108 -0
  16. package/dist/features/changelog/services/release-notes-service.js +180 -0
  17. package/dist/features/changelog/types/changelog.types.js +3 -0
  18. package/dist/interactive-ui.js +343 -161
  19. package/dist/services/background-audit.js +60 -0
  20. package/dist/services/index.js +3 -3
  21. package/dist/services/jsdelivr-registry.js +92 -176
  22. package/dist/services/npm-registry.js +97 -27
  23. package/dist/services/vulnerability-checker.js +133 -0
  24. package/dist/ui/controllers/index.js +8 -0
  25. package/dist/ui/controllers/package-info-modal-controller.js +237 -0
  26. package/dist/ui/controllers/vulnerability-audit-controller.js +82 -0
  27. package/dist/ui/index.js +3 -0
  28. package/dist/ui/input-handler.js +41 -10
  29. package/dist/ui/modal/index.js +22 -0
  30. package/dist/ui/modal/layout.js +84 -0
  31. package/dist/ui/modal/package-info-sections.js +327 -0
  32. package/dist/ui/modal/package-info.js +147 -0
  33. package/dist/ui/modal/theme-selector.js +46 -0
  34. package/dist/ui/modal/types.js +3 -0
  35. package/dist/ui/presenters/index.js +11 -0
  36. package/dist/ui/presenters/vulnerability.js +76 -0
  37. package/dist/ui/renderer/index.js +9 -11
  38. package/dist/ui/renderer/package-list.js +166 -66
  39. package/dist/ui/state/filter-manager.js +17 -2
  40. package/dist/ui/state/modal-manager.js +48 -6
  41. package/dist/ui/state/state-manager.js +49 -12
  42. package/dist/ui/utils/cursor.js +18 -0
  43. package/dist/ui/utils/index.js +8 -1
  44. package/dist/ui/utils/terminal-input.js +82 -0
  45. package/dist/ui/utils/text.js +75 -0
  46. package/dist/ui/utils/version.js +3 -2
  47. package/package.json +7 -11
  48. package/dist/services/changelog-fetcher.js +0 -190
  49. package/dist/ui/renderer/modal.js +0 -190
  50. package/dist/ui/renderer/theme-selector.js +0 -83
@@ -37,91 +37,154 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.InteractiveUI = void 0;
40
- const inquirer_1 = __importDefault(require("inquirer"));
41
40
  const chalk_1 = __importDefault(require("chalk"));
42
41
  const semver = __importStar(require("semver"));
43
- const keypress = require('keypress');
44
42
  const ui_1 = require("./ui");
45
- const services_1 = require("./services");
43
+ const controllers_1 = require("./ui/controllers");
46
44
  const themes_1 = require("./ui/themes");
47
45
  const themes_colors_1 = require("./ui/themes-colors");
46
+ const DEFAULT_VULNERABILITY_DISPLAY_OPTIONS = {
47
+ showPeerDependencyVulnerabilities: false,
48
+ showOptionalDependencyVulnerabilities: false,
49
+ };
50
+ function normalizeVulnerabilityDisplayOptions(options) {
51
+ return {
52
+ showPeerDependencyVulnerabilities: options?.showPeerDependencyVulnerabilities ??
53
+ DEFAULT_VULNERABILITY_DISPLAY_OPTIONS.showPeerDependencyVulnerabilities,
54
+ showOptionalDependencyVulnerabilities: options?.showOptionalDependencyVulnerabilities ??
55
+ DEFAULT_VULNERABILITY_DISPLAY_OPTIONS.showOptionalDependencyVulnerabilities,
56
+ };
57
+ }
48
58
  class InteractiveUI {
49
- constructor(packageManager) {
59
+ constructor(packageManager, options) {
60
+ this.vulnerabilityAuditController = new controllers_1.VulnerabilityAuditController();
61
+ this.packageInfoModalController = new controllers_1.PackageInfoModalController();
50
62
  this.renderer = new ui_1.UIRenderer();
51
63
  this.packageManager = packageManager;
64
+ this.options = normalizeVulnerabilityDisplayOptions(options);
52
65
  }
53
66
  async displayPackagesTable(packages) {
54
67
  console.log(this.renderer.renderPackagesTable(packages));
55
68
  }
56
69
  async selectPackagesToUpgrade(packages, previousSelections) {
57
- const outdatedPackages = packages.filter((p) => p.isOutdated);
58
- if (outdatedPackages.length === 0) {
70
+ const selectionStates = this.createSelectionStates(packages, previousSelections, false);
71
+ if (selectionStates.length === 0) {
59
72
  return [];
60
73
  }
61
- // Deduplicate packages by name and version specifier, but track all package.json paths
62
- const uniquePackages = new Map();
63
- for (const pkg of outdatedPackages) {
64
- const key = `${pkg.name}@${pkg.currentVersion}@${pkg.type}`;
65
- if (!uniquePackages.has(key)) {
66
- uniquePackages.set(key, {
67
- pkg,
68
- packageJsonPaths: new Set([pkg.packageJsonPath]),
69
- type: pkg.type,
70
- });
71
- }
72
- else {
73
- uniquePackages.get(key).packageJsonPaths.add(pkg.packageJsonPath);
74
- }
75
- }
76
- // Convert to array and sort alphabetically by name (@scoped packages first, then unscoped)
77
- const deduplicatedPackages = Array.from(uniquePackages.values()).map(({ pkg, packageJsonPaths, type }) => ({
78
- ...pkg,
79
- packageJsonPaths: Array.from(packageJsonPaths),
80
- type,
81
- }));
82
- deduplicatedPackages.sort((a, b) => {
83
- const aIsScoped = a.name.startsWith('@');
84
- const bIsScoped = b.name.startsWith('@');
85
- // If one is scoped and the other isn't, scoped comes first
86
- if (aIsScoped && !bIsScoped)
87
- return -1;
88
- if (!aIsScoped && bIsScoped)
89
- return 1;
90
- // Both scoped or both unscoped - sort alphabetically
91
- return a.name.localeCompare(b.name);
92
- });
93
- // Create selection states for each unique package
94
- const selectionStates = deduplicatedPackages.map((pkg) => {
74
+ const selectedStates = await this.interactiveTableSelector(selectionStates);
75
+ return this.createUpgradeChoices(selectedStates);
76
+ }
77
+ createSelectionStates(packages, previousSelections, includeUpToDate = true) {
78
+ const relevantPackages = includeUpToDate ? packages : packages.filter((p) => p.isOutdated);
79
+ const uniquePackages = this.deduplicatePackages(relevantPackages);
80
+ return Array.from(uniquePackages.values()).map(({ pkg, packageJsonPaths }) => {
95
81
  const currentClean = semver.coerce(pkg.currentVersion)?.version || pkg.currentVersion;
96
82
  const rangeClean = semver.coerce(pkg.rangeVersion)?.version || pkg.rangeVersion;
97
83
  const latestClean = semver.coerce(pkg.latestVersion)?.version || pkg.latestVersion;
98
- // Use previous selection if available, otherwise default to 'none'
99
84
  const key = `${pkg.name}@${pkg.currentVersion}@${pkg.type}`;
100
85
  const previousSelection = previousSelections?.get(key) || 'none';
101
86
  return {
102
87
  name: pkg.name,
103
- packageJsonPath: pkg.packageJsonPaths[0], // Use first path for display
104
- packageJsonPaths: pkg.packageJsonPaths, // Store all paths for upgrading
105
- currentVersionSpecifier: pkg.currentVersion, // Keep original with prefix
88
+ packageJsonPath: pkg.packageJsonPath,
89
+ packageJsonPaths: Array.from(packageJsonPaths),
90
+ currentVersionSpecifier: pkg.currentVersion,
106
91
  currentVersion: currentClean,
107
92
  rangeVersion: rangeClean,
108
93
  latestVersion: latestClean,
109
94
  selectedOption: previousSelection,
95
+ loadState: 'ready',
110
96
  hasRangeUpdate: pkg.hasRangeUpdate,
111
97
  hasMajorUpdate: pkg.hasMajorUpdate,
112
98
  type: pkg.type,
99
+ vulnerability: this.vulnerabilityAuditController.getCachedSummary(pkg.name, pkg.currentVersion, pkg.type),
100
+ allVersions: pkg.allVersions,
113
101
  };
114
102
  });
115
- // Use custom interactive table selector (simplified - no grouping)
116
- const selectedStates = await this.interactiveTableSelector(selectionStates);
117
- // Convert to PackageUpgradeChoice[] - create one choice per package.json path
103
+ }
104
+ createPendingSelectionStates(packages, previousSelections) {
105
+ const uniquePackages = this.deduplicatePackages(packages.map((pkg) => ({
106
+ ...pkg,
107
+ rangeVersion: pkg.currentVersion,
108
+ latestVersion: pkg.currentVersion,
109
+ isOutdated: false,
110
+ hasRangeUpdate: false,
111
+ hasMajorUpdate: false,
112
+ })));
113
+ return Array.from(uniquePackages.values()).map(({ pkg, packageJsonPaths }) => {
114
+ const currentClean = semver.coerce(pkg.currentVersion)?.version || pkg.currentVersion;
115
+ const key = `${pkg.name}@${pkg.currentVersion}@${pkg.type}`;
116
+ const previousSelection = previousSelections?.get(key) || 'none';
117
+ return {
118
+ name: pkg.name,
119
+ packageJsonPath: pkg.packageJsonPath,
120
+ packageJsonPaths: Array.from(packageJsonPaths),
121
+ currentVersionSpecifier: pkg.currentVersion,
122
+ currentVersion: currentClean,
123
+ rangeVersion: 'loading',
124
+ latestVersion: 'loading',
125
+ selectedOption: previousSelection,
126
+ loadState: 'pending',
127
+ hasRangeUpdate: false,
128
+ hasMajorUpdate: false,
129
+ type: pkg.type,
130
+ vulnerability: this.vulnerabilityAuditController.getCachedSummary(pkg.name, pkg.currentVersion, pkg.type),
131
+ };
132
+ });
133
+ }
134
+ appendOutdatedBatchToSelectionStates(selectionStates, batch, previousSelections) {
135
+ const outdatedStates = this.createSelectionStates(batch.flatMap((batchItem) => batchItem.packageInfo).filter((pkg) => pkg.isOutdated), previousSelections, false);
136
+ if (outdatedStates.length === 0) {
137
+ return;
138
+ }
139
+ const seen = new Set(selectionStates.map((state) => `${state.name}@${state.currentVersionSpecifier}@${state.type}`));
140
+ outdatedStates.forEach((state) => {
141
+ const key = `${state.name}@${state.currentVersionSpecifier}@${state.type}`;
142
+ if (!seen.has(key)) {
143
+ selectionStates.push(state);
144
+ seen.add(key);
145
+ }
146
+ });
147
+ this.enqueueSecurityAudit(selectionStates);
148
+ }
149
+ async selectPackagesToUpgradeProgressive(selectionStates, progress, attachRefresh) {
150
+ this.enqueueSecurityAudit(selectionStates);
151
+ const selectedStates = await this.interactiveTableSelector(selectionStates, progress, attachRefresh);
152
+ return this.createUpgradeChoices(selectedStates);
153
+ }
154
+ enqueueSecurityAudit(selectionStates) {
155
+ this.vulnerabilityAuditController.enqueueStates(selectionStates, () => this.refreshView?.());
156
+ }
157
+ deduplicatePackages(packages) {
158
+ const uniquePackages = new Map();
159
+ for (const pkg of packages) {
160
+ const key = `${pkg.name}@${pkg.currentVersion}@${pkg.type}`;
161
+ if (!uniquePackages.has(key)) {
162
+ uniquePackages.set(key, {
163
+ pkg,
164
+ packageJsonPaths: new Set([pkg.packageJsonPath]),
165
+ });
166
+ }
167
+ else {
168
+ uniquePackages.get(key).packageJsonPaths.add(pkg.packageJsonPath);
169
+ }
170
+ }
171
+ return new Map(Array.from(uniquePackages.entries()).sort(([, a], [, b]) => {
172
+ const aIsScoped = a.pkg.name.startsWith('@');
173
+ const bIsScoped = b.pkg.name.startsWith('@');
174
+ if (aIsScoped && !bIsScoped)
175
+ return -1;
176
+ if (!aIsScoped && bIsScoped)
177
+ return 1;
178
+ return a.pkg.name.localeCompare(b.pkg.name);
179
+ }));
180
+ }
181
+ createUpgradeChoices(selectedStates) {
118
182
  const choices = [];
119
183
  selectedStates
120
- .filter((state) => state.selectedOption !== 'none')
184
+ .filter((state) => state.loadState === 'ready' && state.selectedOption !== 'none')
121
185
  .forEach((state) => {
122
186
  const targetVersion = state.selectedOption === 'range' ? state.rangeVersion : state.latestVersion;
123
187
  const targetVersionWithPrefix = ui_1.VersionUtils.applyVersionPrefix(state.currentVersionSpecifier, targetVersion);
124
- // Create a choice for each package.json path where this package appears
125
188
  const pathsToUpdate = state.packageJsonPaths || [state.packageJsonPath];
126
189
  pathsToUpdate.forEach((packageJsonPath) => {
127
190
  choices.push({
@@ -138,89 +201,162 @@ class InteractiveUI {
138
201
  }
139
202
  getTerminalHeight() {
140
203
  // Check if stdout is a TTY and has rows property
141
- if (process.stdout.isTTY && typeof process.stdout.rows === 'number' && process.stdout.rows > 0) {
204
+ if (process.stdout.isTTY &&
205
+ typeof process.stdout.rows === 'number' &&
206
+ process.stdout.rows > 0) {
142
207
  return process.stdout.rows;
143
208
  }
144
209
  return 24; // Fallback default
145
210
  }
146
- async interactiveTableSelector(selectionStates) {
211
+ async interactiveTableSelector(selectionStates, loadingProgress, attachRefresh) {
147
212
  return new Promise((resolve) => {
148
- const states = [...selectionStates];
213
+ const states = selectionStates;
149
214
  const stateManager = new ui_1.StateManager(0, this.getTerminalHeight());
215
+ let isResolved = false;
216
+ let ownsAlternateScreen = false;
217
+ const vulnerabilityDisplayOptions = this.options;
218
+ const claimInteractiveScreen = () => {
219
+ if (ownsAlternateScreen) {
220
+ return;
221
+ }
222
+ ui_1.ConsoleUtils.clearProgress();
223
+ ui_1.CursorUtils.enterAlternateScreen();
224
+ ui_1.CursorUtils.clearScreen();
225
+ ownsAlternateScreen = true;
226
+ };
227
+ const releaseInteractiveScreen = () => {
228
+ if (!ownsAlternateScreen) {
229
+ return;
230
+ }
231
+ ui_1.CursorUtils.exitAlternateScreen();
232
+ ownsAlternateScreen = false;
233
+ };
150
234
  // No grouping needed - packages are already filtered by type
151
235
  // This simplifies scrolling and avoids rendering issues
152
236
  stateManager.setRenderableItems([]);
237
+ // Track the current max scroll offset for the info modal
238
+ let infoModalMaxScrollOffset = 0;
239
+ let previousViewportMode = null;
240
+ let previousModalViewportLineCount = null;
153
241
  const handleAction = (action) => {
154
242
  const uiState = stateManager.getUIState();
155
- const filteredStates = stateManager.getFilteredStates(states);
243
+ const filteredStates = stateManager.getFilteredStates(states, vulnerabilityDisplayOptions);
156
244
  switch (action.type) {
157
245
  case 'navigate_up':
158
- if (!uiState.showInfoModal && !uiState.showThemeModal) {
246
+ if (!uiState.showThemeModal) {
159
247
  stateManager.navigateUp(filteredStates.length);
160
248
  }
161
249
  break;
162
250
  case 'navigate_down':
163
- if (!uiState.showInfoModal && !uiState.showThemeModal) {
251
+ if (!uiState.showThemeModal) {
164
252
  stateManager.navigateDown(filteredStates.length);
165
253
  }
166
254
  break;
167
255
  case 'select_left':
168
- if (!uiState.showInfoModal && !uiState.showThemeModal) {
256
+ if (!uiState.showThemeModal) {
169
257
  stateManager.updateSelection(filteredStates, 'left');
170
258
  }
171
259
  break;
172
260
  case 'select_right':
173
- if (!uiState.showInfoModal && !uiState.showThemeModal) {
261
+ if (!uiState.showThemeModal) {
174
262
  stateManager.updateSelection(filteredStates, 'right');
175
263
  }
176
264
  break;
177
265
  case 'bulk_select_minor':
178
- if (!uiState.showInfoModal && !uiState.showThemeModal) {
266
+ if (!uiState.showThemeModal) {
179
267
  stateManager.bulkSelectMinor(filteredStates);
180
268
  }
181
269
  break;
182
270
  case 'bulk_select_latest':
183
- if (!uiState.showInfoModal && !uiState.showThemeModal) {
271
+ if (!uiState.showThemeModal) {
184
272
  stateManager.bulkSelectLatest(filteredStates);
185
273
  }
186
274
  break;
187
275
  case 'bulk_unselect_all':
188
- if (!uiState.showInfoModal && !uiState.showThemeModal) {
276
+ if (!uiState.showThemeModal) {
189
277
  stateManager.bulkUnselectAll(filteredStates);
190
278
  }
191
279
  break;
192
280
  case 'toggle_dep_type_filter':
193
- if (!uiState.showInfoModal && !uiState.showThemeModal) {
281
+ if (!uiState.showThemeModal) {
194
282
  stateManager.toggleDependencyTypeFilter(action.depType);
195
283
  }
196
284
  break;
197
285
  case 'toggle_info_modal':
198
286
  if (!uiState.showInfoModal) {
199
287
  // Opening modal - load package info asynchronously
200
- stateManager.toggleInfoModal();
288
+ const modalSessionId = stateManager.toggleInfoModal();
201
289
  const currentState = filteredStates[uiState.currentRow];
202
- stateManager.setModalLoading(true);
290
+ const canFetchMetadata = currentState?.loadState === 'ready';
291
+ stateManager.setModalLoading(canFetchMetadata, modalSessionId);
203
292
  renderInterface();
204
- // Fetch metadata asynchronously
205
- services_1.changelogFetcher.fetchPackageMetadata(currentState.name).then((metadata) => {
206
- if (metadata) {
207
- currentState.description = metadata.description;
208
- currentState.homepage = metadata.homepage;
209
- currentState.repository = metadata.releaseNotes;
210
- currentState.weeklyDownloads = metadata.weeklyDownloads;
211
- currentState.author = metadata.author;
212
- currentState.license = metadata.license;
213
- }
214
- stateManager.setModalLoading(false);
215
- renderInterface();
216
- });
293
+ if (currentState && canFetchMetadata) {
294
+ this.packageInfoModalController
295
+ .hydrate(currentState)
296
+ .then(() => {
297
+ if (isResolved || stateManager.getInfoModalSessionId() !== modalSessionId)
298
+ return;
299
+ stateManager.setModalLoading(false, modalSessionId);
300
+ renderInterface();
301
+ // Auto-load the first version's release notes
302
+ if (stateManager.getInfoModalSessionId() === modalSessionId &&
303
+ this.packageInfoModalController.getVersionCount(currentState) > 0) {
304
+ this.packageInfoModalController.loadVersionAtIndex(currentState, 0, () => {
305
+ if (!isResolved)
306
+ renderInterface();
307
+ });
308
+ }
309
+ })
310
+ .catch(() => {
311
+ if (isResolved || stateManager.getInfoModalSessionId() !== modalSessionId)
312
+ return;
313
+ stateManager.setModalLoading(false, modalSessionId);
314
+ renderInterface();
315
+ });
316
+ }
217
317
  }
218
318
  else {
219
- // Closing modal
319
+ // Closing modal - cancel in-flight fetches
320
+ this.packageInfoModalController.cancel();
220
321
  stateManager.toggleInfoModal();
221
322
  renderInterface();
222
323
  }
223
324
  break;
325
+ case 'scroll_info_modal_up':
326
+ if (!stateManager.scrollInfoModalUp()) {
327
+ return;
328
+ }
329
+ break;
330
+ case 'scroll_info_modal_down':
331
+ if (!stateManager.scrollInfoModalDown(infoModalMaxScrollOffset)) {
332
+ return;
333
+ }
334
+ break;
335
+ case 'navigate_info_modal_version':
336
+ {
337
+ if (uiState.infoModalRow >= 0 && uiState.infoModalRow < filteredStates.length) {
338
+ const currentState = filteredStates[uiState.infoModalRow];
339
+ const newIndex = this.packageInfoModalController.navigateVersion(currentState, action.direction);
340
+ if (newIndex >= 0) {
341
+ // Reset scroll to top when switching versions
342
+ stateManager.resetInfoModalScroll();
343
+ // Load the version if not already loaded
344
+ if (!this.packageInfoModalController.isVersionLoaded(currentState, newIndex)) {
345
+ this.packageInfoModalController.loadVersionAtIndex(currentState, newIndex, () => {
346
+ if (!isResolved)
347
+ renderInterface();
348
+ });
349
+ }
350
+ }
351
+ else {
352
+ return;
353
+ }
354
+ }
355
+ else {
356
+ return;
357
+ }
358
+ }
359
+ break;
224
360
  case 'enter_filter_mode':
225
361
  stateManager.enterFilterMode(action.preserveQuery);
226
362
  break;
@@ -268,7 +404,19 @@ class InteractiveUI {
268
404
  case 'theme_confirm':
269
405
  stateManager.confirmTheme();
270
406
  break;
407
+ case 'trigger_audit_scan':
408
+ if (!uiState.showThemeModal) {
409
+ const auditProgress = this.vulnerabilityAuditController.getProgress();
410
+ if (auditProgress.hasData) {
411
+ stateManager.toggleVulnerableFilter();
412
+ }
413
+ else if (!auditProgress.isRunning) {
414
+ this.enqueueSecurityAudit(states);
415
+ }
416
+ }
417
+ break;
271
418
  case 'cancel':
419
+ this.packageInfoModalController.cancel();
272
420
  handleCancel();
273
421
  return;
274
422
  }
@@ -277,40 +425,79 @@ class InteractiveUI {
277
425
  }
278
426
  };
279
427
  const handleConfirm = (selectedStates) => {
280
- // Reset terminal colors
281
- process.stdout.write((0, themes_colors_1.getTerminalResetCode)());
282
- ui_1.CursorUtils.show();
283
- // Clean up listeners
284
- if (process.stdin.setRawMode) {
285
- process.stdin.setRawMode(false);
286
- }
287
- process.stdin.removeAllListeners('keypress');
288
- process.stdin.pause();
289
- process.removeAllListeners('SIGWINCH');
290
- resolve(selectedStates);
428
+ finalizeSelection(selectedStates);
291
429
  };
292
430
  const handleCancel = () => {
293
- // Reset terminal colors
431
+ finalizeSelection(states.map((s) => ({ ...s, selectedOption: 'none' })));
432
+ };
433
+ const inputHandler = new ui_1.InputHandler(stateManager, handleAction, handleConfirm, handleCancel);
434
+ const resetAnsiPattern = /\x1b\[(?:0|49)m/g;
435
+ const packageListRenderOptions = {
436
+ showPeerDependencyVulnerabilities: this.options.showPeerDependencyVulnerabilities,
437
+ showOptionalDependencyVulnerabilities: this.options.showOptionalDependencyVulnerabilities,
438
+ };
439
+ const keypressHandler = (str, key) => inputHandler.handleKeypress(str, key, states);
440
+ const buildRemainingViewport = (terminalWidth, terminalHeight, usedLines) => {
441
+ const remainingLines = Math.max(0, terminalHeight - usedLines);
442
+ const blankLine = ' '.repeat(terminalWidth);
443
+ return Array.from({ length: remainingLines }, () => blankLine);
444
+ };
445
+ const applyBackgroundToLine = (line, bgCode) => `${bgCode}${line.replace(resetAnsiPattern, (match) => `${match}${bgCode}`)}${(0, themes_colors_1.getTerminalResetCode)()}`;
446
+ const writeFrame = (lines, bgCode) => {
447
+ if (lines.length === 0) {
448
+ return;
449
+ }
450
+ process.stdout.write(lines.map((line) => applyBackgroundToLine(line, bgCode)).join('\n'));
451
+ };
452
+ const buildModalHeaderLines = (shortcutLabel) => [
453
+ ' ' + chalk_1.default.bold.magenta('🚀 inup'),
454
+ '',
455
+ ' ' + shortcutLabel,
456
+ '',
457
+ ];
458
+ const renderViewport = (lines, terminalWidth, terminalHeight, bgCode) => {
459
+ const viewportLines = [
460
+ ...lines,
461
+ ...buildRemainingViewport(terminalWidth, terminalHeight, lines.length),
462
+ ];
463
+ writeFrame(viewportLines, bgCode);
464
+ };
465
+ const renderModalViewport = (mode, shortcutLabel, modalLines, terminalWidth, terminalHeight, bgCode) => {
466
+ const viewportLineCount = buildModalHeaderLines(shortcutLabel).length + modalLines.length;
467
+ const shouldClearBeforeRender = previousViewportMode !== mode || previousModalViewportLineCount !== viewportLineCount;
468
+ if (shouldClearBeforeRender) {
469
+ ui_1.CursorUtils.clearScreen();
470
+ ui_1.CursorUtils.hide();
471
+ }
472
+ renderViewport([...buildModalHeaderLines(shortcutLabel), ...modalLines], terminalWidth, terminalHeight, bgCode);
473
+ previousViewportMode = mode;
474
+ previousModalViewportLineCount = viewportLineCount;
475
+ stateManager.markRendered([]);
476
+ };
477
+ let cleanupInteractiveSession = () => {
294
478
  process.stdout.write((0, themes_colors_1.getTerminalResetCode)());
295
479
  ui_1.CursorUtils.show();
296
- // Clean up listeners
297
- if (process.stdin.setRawMode) {
298
- process.stdin.setRawMode(false);
299
- }
300
- process.stdin.removeAllListeners('keypress');
480
+ process.stdin.off('keypress', keypressHandler);
301
481
  process.stdin.pause();
302
- process.removeAllListeners('SIGWINCH');
303
- resolve(states.map((s) => ({ ...s, selectedOption: 'none' })));
482
+ process.off('SIGWINCH', handleResize);
483
+ this.refreshView = undefined;
484
+ };
485
+ const finalizeSelection = (selectedStates) => {
486
+ isResolved = true;
487
+ this.packageInfoModalController.cancel();
488
+ releaseInteractiveScreen();
489
+ cleanupInteractiveSession();
490
+ resolve(selectedStates);
304
491
  };
305
- const inputHandler = new ui_1.InputHandler(stateManager, handleAction, handleConfirm, handleCancel);
306
492
  const renderInterface = () => {
307
493
  const uiState = stateManager.getUIState();
308
- const filteredStates = stateManager.getFilteredStates(states);
494
+ const filteredStates = stateManager.getFilteredStates(states, vulnerabilityDisplayOptions);
495
+ const auditProgress = this.vulnerabilityAuditController.getProgress();
309
496
  // Apply terminal background color
310
497
  const bgCode = (0, themes_colors_1.getTerminalBgColorCode)();
311
498
  process.stdout.write(bgCode);
312
499
  if (uiState.forceFullRender) {
313
- console.clear();
500
+ ui_1.CursorUtils.clearScreen();
314
501
  ui_1.CursorUtils.hide();
315
502
  }
316
503
  else {
@@ -321,62 +508,48 @@ class InteractiveUI {
321
508
  const terminalWidth = process.stdout.columns || 80;
322
509
  const terminalHeight = this.getTerminalHeight();
323
510
  const themeManager = stateManager.getThemeManager();
324
- // Render header
325
- const headerLines = [];
326
- headerLines.push(' ' + chalk_1.default.bold.magenta('🚀 inup'));
327
- headerLines.push('');
328
- headerLines.push(' ' +
329
- chalk_1.default.bold.white('T ') +
330
- chalk_1.default.gray('/ Esc Exit theme selector'));
331
- headerLines.push('');
332
- headerLines.forEach((line) => console.log(line));
333
511
  const modalLines = this.renderer.renderThemeSelectorModal(themeManager.getCurrentTheme(), themeManager.getPreviewTheme(), terminalWidth, terminalHeight);
334
- modalLines.forEach((line) => console.log(line));
335
- // Clear any remaining lines from previous render
336
- ui_1.CursorUtils.clearToEndOfScreen();
337
- stateManager.markRendered([]);
512
+ renderModalViewport('theme-modal', chalk_1.default.bold.white('T ') + chalk_1.default.gray('/ Esc Exit theme selector'), modalLines, terminalWidth, terminalHeight, bgCode);
338
513
  }
339
- else if (uiState.showInfoModal && uiState.infoModalRow >= 0 && uiState.infoModalRow < filteredStates.length) {
514
+ else if (uiState.showInfoModal &&
515
+ uiState.infoModalRow >= 0 &&
516
+ uiState.infoModalRow < filteredStates.length) {
340
517
  const selectedState = filteredStates[uiState.infoModalRow];
341
518
  const terminalWidth = process.stdout.columns || 80;
342
519
  const terminalHeight = this.getTerminalHeight();
343
- // Render header
344
- const headerLines = [];
345
- headerLines.push(' ' + chalk_1.default.bold.magenta('🚀 inup'));
346
- headerLines.push('');
347
- headerLines.push(' ' +
348
- chalk_1.default.bold.white('I / Esc ') +
349
- chalk_1.default.gray('Exit this view'));
350
- headerLines.push('');
351
- headerLines.forEach((line) => console.log(line));
352
520
  if (uiState.isLoadingModalInfo) {
353
521
  // Show loading state
354
- const modalLines = this.renderer.renderPackageInfoLoading(selectedState, terminalWidth, terminalHeight);
355
- modalLines.forEach((line) => console.log(line));
522
+ const result = this.renderer.renderPackageInfoLoading(selectedState, terminalWidth, Math.max(8, terminalHeight - 4));
523
+ infoModalMaxScrollOffset = result.maxScrollOffset;
524
+ renderModalViewport('info-modal', chalk_1.default.bold.white('I / Esc ') + chalk_1.default.gray('Exit this view'), result.lines, terminalWidth, terminalHeight, bgCode);
356
525
  }
357
526
  else {
358
- // Show full info
359
- const modalLines = this.renderer.renderPackageInfoModal(selectedState, terminalWidth, terminalHeight);
360
- modalLines.forEach((line) => console.log(line));
527
+ // Show full info with scroll support
528
+ const result = this.renderer.renderPackageInfoModal(selectedState, terminalWidth, Math.max(8, terminalHeight - 4), uiState.infoModalScrollOffset);
529
+ infoModalMaxScrollOffset = result.maxScrollOffset;
530
+ stateManager.clampInfoModalScrollOffset(infoModalMaxScrollOffset);
531
+ const scrollHint = result.usesInternalScroll && result.maxScrollOffset > 0
532
+ ? chalk_1.default.bold.white('↑/↓ ') + chalk_1.default.gray('Scroll · ')
533
+ : '';
534
+ renderModalViewport('info-modal', scrollHint +
535
+ chalk_1.default.bold.white('←/→ ') +
536
+ chalk_1.default.gray('Version · ') +
537
+ chalk_1.default.bold.white('I / Esc ') +
538
+ chalk_1.default.gray('Exit this view'), result.lines, terminalWidth, terminalHeight, bgCode);
361
539
  }
362
- // Clear any remaining lines from previous render
363
- ui_1.CursorUtils.clearToEndOfScreen();
364
- stateManager.markRendered([]);
365
540
  }
366
541
  else {
367
542
  // Normal list view (flat rendering - no grouping)
368
543
  const terminalWidth = process.stdout.columns || 80;
544
+ const terminalHeight = this.getTerminalHeight();
369
545
  const activeFilterLabel = stateManager.getActiveFilterLabel();
370
546
  const lines = this.renderer.renderInterface(filteredStates, uiState.currentRow, uiState.scrollOffset, uiState.maxVisibleItems, uiState.forceFullRender, [], // No renderable items - use flat rendering
371
547
  activeFilterLabel, // Show current dependency type filter state
372
548
  this.packageManager, // Pass package manager info for header
373
- uiState.filterMode, uiState.filterQuery, states.length, terminalWidth);
374
- // Print all lines
375
- lines.forEach((line) => console.log(line));
376
- // Clear any remaining lines from previous render
377
- if (!uiState.forceFullRender) {
378
- ui_1.CursorUtils.clearToEndOfScreen();
379
- }
549
+ uiState.filterMode, uiState.filterQuery, states.length, terminalWidth, loadingProgress, auditProgress, packageListRenderOptions);
550
+ renderViewport(lines, terminalWidth, terminalHeight, bgCode);
551
+ previousViewportMode = 'list';
552
+ previousModalViewportLineCount = null;
380
553
  stateManager.markRendered(lines);
381
554
  }
382
555
  stateManager.setInitialRender(false);
@@ -389,27 +562,41 @@ class InteractiveUI {
389
562
  };
390
563
  // Setup keypress handling
391
564
  try {
392
- keypress(process.stdin);
393
- if (process.stdin.setRawMode) {
394
- process.stdin.setRawMode(true);
395
- }
396
- process.stdin.resume();
397
- process.stdin.on('keypress', (str, key) => inputHandler.handleKeypress(str, key, states));
565
+ claimInteractiveScreen();
566
+ this.refreshView = () => {
567
+ if (!isResolved) {
568
+ renderInterface();
569
+ }
570
+ };
571
+ attachRefresh?.(() => {
572
+ if (!isResolved) {
573
+ renderInterface();
574
+ }
575
+ });
576
+ const keypressSession = ui_1.TerminalInput.startKeypressSession(keypressHandler);
577
+ const previousCleanup = cleanupInteractiveSession;
578
+ cleanupInteractiveSession = () => {
579
+ keypressSession.close();
580
+ previousCleanup();
581
+ };
398
582
  // Setup resize handler
399
583
  process.on('SIGWINCH', handleResize);
400
584
  // Update terminal height directly before initial render to ensure correct dimensions
401
585
  // This handles cases where process.stdout.rows might not be accurate at startup
402
586
  const currentHeight = this.getTerminalHeight();
403
587
  if (stateManager.updateTerminalHeight(currentHeight)) {
404
- const initialFiltered = stateManager.getFilteredStates(states);
588
+ const initialFiltered = stateManager.getFilteredStates(states, vulnerabilityDisplayOptions);
405
589
  stateManager.resetForResize(initialFiltered.length);
406
590
  }
407
591
  // Initial render
408
592
  renderInterface();
593
+ this.enqueueSecurityAudit(states);
409
594
  }
410
595
  catch (error) {
596
+ releaseInteractiveScreen();
411
597
  // Reset terminal colors
412
598
  process.stdout.write((0, themes_colors_1.getTerminalResetCode)());
599
+ this.refreshView = undefined;
413
600
  // Fallback to simple interface if raw mode fails
414
601
  console.log(chalk_1.default.yellow('Raw mode not available, using fallback interface...'));
415
602
  resolve(states);
@@ -419,32 +606,27 @@ class InteractiveUI {
419
606
  async confirmUpgrade(choices) {
420
607
  console.log(this.renderer.renderConfirmation(choices));
421
608
  return new Promise((resolve) => {
609
+ let cleanupConfirmationSession = () => {
610
+ ui_1.CursorUtils.show();
611
+ };
422
612
  const handleConfirm = (confirmed) => {
613
+ cleanupConfirmationSession();
423
614
  resolve(confirmed);
424
615
  };
425
616
  const inputHandler = new ui_1.ConfirmationInputHandler(handleConfirm);
617
+ const keypressHandler = (str, key) => inputHandler.handleKeypress(str, key);
426
618
  // Setup keypress handling
427
619
  try {
428
- keypress(process.stdin);
429
- if (process.stdin.setRawMode) {
430
- process.stdin.setRawMode(true);
431
- }
620
+ const keypressSession = ui_1.TerminalInput.startKeypressSession(keypressHandler);
621
+ cleanupConfirmationSession = () => {
622
+ keypressSession.close();
623
+ ui_1.CursorUtils.show();
624
+ };
432
625
  ui_1.CursorUtils.hide();
433
- process.stdin.resume();
434
- process.stdin.on('keypress', (str, key) => inputHandler.handleKeypress(str, key));
435
626
  }
436
627
  catch (error) {
437
- // Fallback to inquirer
438
- inquirer_1.default
439
- .prompt([
440
- {
441
- type: 'confirm',
442
- name: 'proceed',
443
- message: 'Proceed with upgrade?',
444
- default: true,
445
- },
446
- ])
447
- .then((answer) => resolve(answer.proceed))
628
+ ui_1.TerminalInput.promptForConfirmation('Proceed with upgrade? [Y/n] ')
629
+ .then(resolve)
448
630
  .catch(() => resolve(false));
449
631
  }
450
632
  });