inup 1.4.12 → 1.5.1

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/LICENSE +21 -0
  2. package/README.md +39 -30
  3. package/dist/cli.js +29 -14
  4. package/dist/config/project-config.js +6 -0
  5. package/dist/core/package-detector.js +3 -2
  6. package/dist/core/upgrade-runner.js +6 -3
  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 +242 -114
  19. package/dist/services/background-audit.js +60 -0
  20. package/dist/services/index.js +3 -1
  21. package/dist/services/vulnerability-checker.js +133 -0
  22. package/dist/ui/controllers/index.js +8 -0
  23. package/dist/ui/controllers/package-info-modal-controller.js +237 -0
  24. package/dist/ui/controllers/vulnerability-audit-controller.js +82 -0
  25. package/dist/ui/index.js +3 -0
  26. package/dist/ui/input-handler.js +40 -9
  27. package/dist/ui/modal/index.js +22 -0
  28. package/dist/ui/modal/layout.js +84 -0
  29. package/dist/ui/modal/package-info-sections.js +327 -0
  30. package/dist/ui/modal/package-info.js +147 -0
  31. package/dist/ui/modal/theme-selector.js +46 -0
  32. package/dist/ui/modal/types.js +3 -0
  33. package/dist/ui/presenters/index.js +11 -0
  34. package/dist/ui/presenters/vulnerability.js +76 -0
  35. package/dist/ui/renderer/index.js +9 -11
  36. package/dist/ui/renderer/package-list.js +135 -62
  37. package/dist/ui/state/filter-manager.js +17 -2
  38. package/dist/ui/state/modal-manager.js +48 -6
  39. package/dist/ui/state/state-manager.js +42 -7
  40. package/dist/ui/utils/cursor.js +18 -0
  41. package/dist/ui/utils/index.js +8 -1
  42. package/dist/ui/utils/terminal-input.js +125 -0
  43. package/dist/ui/utils/text.js +75 -0
  44. package/dist/ui/utils/version.js +3 -2
  45. package/dist/utils/git.js +33 -0
  46. package/dist/utils/index.js +1 -0
  47. package/package.json +22 -19
  48. package/dist/services/changelog-fetcher.js +0 -215
  49. package/dist/ui/renderer/modal.js +0 -190
  50. package/dist/ui/renderer/theme-selector.js +0 -83
@@ -0,0 +1,133 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fetchVulnerabilities = fetchVulnerabilities;
4
+ const config_1 = require("../config");
5
+ const utils_1 = require("../utils");
6
+ const SEVERITY_ORDER = {
7
+ info: 0,
8
+ low: 1,
9
+ moderate: 2,
10
+ high: 3,
11
+ critical: 4,
12
+ };
13
+ function isKnownSeverity(severity) {
14
+ return severity in SEVERITY_ORDER;
15
+ }
16
+ function getHighestSeverity(vulnerabilities) {
17
+ if (vulnerabilities.length === 0)
18
+ return null;
19
+ let highest = 'info';
20
+ for (const vuln of vulnerabilities) {
21
+ if (SEVERITY_ORDER[vuln.severity] > SEVERITY_ORDER[highest]) {
22
+ highest = vuln.severity;
23
+ }
24
+ }
25
+ return highest;
26
+ }
27
+ /**
28
+ * Fetches security advisories for a set of packages using the npm bulk advisory API.
29
+ * This is the same endpoint that `npm audit` uses under the hood.
30
+ *
31
+ * POST https://registry.npmjs.org/-/npm/v1/security/advisories/bulk
32
+ * Body: { "package-name": ["version1"], ... }
33
+ * Response: { "package-name": [{ id, title, severity, url, vulnerable_versions }], ... }
34
+ */
35
+ async function fetchVulnerabilities(packages) {
36
+ const results = new Map();
37
+ if (packages.size === 0) {
38
+ return results;
39
+ }
40
+ // Build the request body: { "package-name": ["version"], ... }
41
+ const body = {};
42
+ for (const [name, version] of packages) {
43
+ const cleanVersion = version.replace(/^[^0-9]*/, '');
44
+ if (cleanVersion) {
45
+ body[name] = [cleanVersion];
46
+ }
47
+ }
48
+ if (Object.keys(body).length === 0) {
49
+ return results;
50
+ }
51
+ const url = `${config_1.NPM_REGISTRY_URL}/-/npm/v1/security/advisories/bulk`;
52
+ utils_1.debugLog.info('vulnerability-checker', `checking ${Object.keys(body).length} packages`);
53
+ const controller = new AbortController();
54
+ const timeoutId = setTimeout(() => controller.abort(), config_1.REQUEST_TIMEOUT);
55
+ try {
56
+ const response = await fetch(url, {
57
+ method: 'POST',
58
+ headers: {
59
+ 'content-type': 'application/json',
60
+ accept: 'application/json',
61
+ },
62
+ body: JSON.stringify(body),
63
+ signal: controller.signal,
64
+ });
65
+ clearTimeout(timeoutId);
66
+ if (!response.ok) {
67
+ utils_1.debugLog.warn('vulnerability-checker', `API returned HTTP ${response.status}`);
68
+ return results;
69
+ }
70
+ // Response is keyed by package name, values are arrays of advisories
71
+ const data = (await response.json());
72
+ if (!data || typeof data !== 'object' || Array.isArray(data)) {
73
+ utils_1.debugLog.warn('vulnerability-checker', 'unexpected API payload shape');
74
+ return results;
75
+ }
76
+ for (const [packageName, advisories] of Object.entries(data)) {
77
+ if (!Array.isArray(advisories) || advisories.length === 0)
78
+ continue;
79
+ const seen = new Set();
80
+ const unique = [];
81
+ for (const advisory of advisories) {
82
+ if (!advisory ||
83
+ typeof advisory.id !== 'number' ||
84
+ typeof advisory.title !== 'string' ||
85
+ typeof advisory.url !== 'string' ||
86
+ typeof advisory.vulnerable_versions !== 'string' ||
87
+ typeof advisory.severity !== 'string' ||
88
+ !isKnownSeverity(advisory.severity)) {
89
+ continue;
90
+ }
91
+ if (seen.has(advisory.id))
92
+ continue;
93
+ seen.add(advisory.id);
94
+ unique.push({
95
+ id: advisory.id,
96
+ title: advisory.title,
97
+ severity: advisory.severity,
98
+ url: advisory.url,
99
+ vulnerable_versions: advisory.vulnerable_versions,
100
+ });
101
+ }
102
+ unique.sort((left, right) => {
103
+ const severityDelta = SEVERITY_ORDER[right.severity] - SEVERITY_ORDER[left.severity];
104
+ if (severityDelta !== 0) {
105
+ return severityDelta;
106
+ }
107
+ return left.id - right.id;
108
+ });
109
+ if (unique.length === 0)
110
+ continue;
111
+ results.set(packageName, {
112
+ packageName,
113
+ vulnerabilities: unique,
114
+ highestSeverity: getHighestSeverity(unique),
115
+ });
116
+ }
117
+ utils_1.debugLog.info('vulnerability-checker', `found ${results.size} packages with vulnerabilities`);
118
+ return results;
119
+ }
120
+ catch (error) {
121
+ if (error instanceof Error && error.name === 'AbortError') {
122
+ utils_1.debugLog.warn('vulnerability-checker', 'request timed out');
123
+ }
124
+ else {
125
+ utils_1.debugLog.warn('vulnerability-checker', 'failed to fetch vulnerabilities', error);
126
+ }
127
+ return results;
128
+ }
129
+ finally {
130
+ clearTimeout(timeoutId);
131
+ }
132
+ }
133
+ //# sourceMappingURL=vulnerability-checker.js.map
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PackageInfoModalController = exports.VulnerabilityAuditController = void 0;
4
+ var vulnerability_audit_controller_1 = require("./vulnerability-audit-controller");
5
+ Object.defineProperty(exports, "VulnerabilityAuditController", { enumerable: true, get: function () { return vulnerability_audit_controller_1.VulnerabilityAuditController; } });
6
+ var package_info_modal_controller_1 = require("./package-info-modal-controller");
7
+ Object.defineProperty(exports, "PackageInfoModalController", { enumerable: true, get: function () { return package_info_modal_controller_1.PackageInfoModalController; } });
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,237 @@
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
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.PackageInfoModalController = void 0;
37
+ const semver = __importStar(require("semver"));
38
+ const services_1 = require("../../services");
39
+ const RELEASE_NOTES_LOAD_DEBOUNCE_MS = 120;
40
+ class PackageInfoModalController {
41
+ constructor() {
42
+ this.abortController = null;
43
+ this.pendingReleaseNotesVersion = null;
44
+ this.releaseNotesDebounceTimer = null;
45
+ this.resolveDebouncedLoad = null;
46
+ }
47
+ /**
48
+ * Cancel any in-flight hydrate/release-notes fetches.
49
+ * Safe to call multiple times or when nothing is in flight.
50
+ */
51
+ cancel() {
52
+ this.abortController?.abort();
53
+ this.abortController = null;
54
+ this.pendingReleaseNotesVersion = null;
55
+ if (this.releaseNotesDebounceTimer) {
56
+ clearTimeout(this.releaseNotesDebounceTimer);
57
+ this.releaseNotesDebounceTimer = null;
58
+ }
59
+ this.resolveDebouncedLoad?.(false);
60
+ this.resolveDebouncedLoad = null;
61
+ }
62
+ async hydrate(state) {
63
+ // Abort any previous session
64
+ this.cancel();
65
+ const controller = new AbortController();
66
+ this.abortController = controller;
67
+ const metadata = await services_1.changelogFetcher.fetchPackageMetadata(state.name, state.latestVersion, controller.signal);
68
+ if (!metadata) {
69
+ return null;
70
+ }
71
+ const result = {
72
+ description: metadata.description,
73
+ homepage: metadata.homepage,
74
+ repository: metadata.releaseNotes,
75
+ weeklyDownloads: metadata.weeklyDownloads,
76
+ author: metadata.author,
77
+ license: metadata.license,
78
+ };
79
+ state.description = result.description;
80
+ state.homepage = result.homepage;
81
+ state.repository = result.repository;
82
+ state.weeklyDownloads = result.weeklyDownloads;
83
+ state.author = result.author;
84
+ state.license = result.license;
85
+ // Compute the version range for release notes
86
+ const targetVersion = state.selectedOption === 'range' ? state.rangeVersion : state.latestVersion;
87
+ if (state.allVersions && state.allVersions.length > 0) {
88
+ state.releaseNotesVersions = this.buildReleaseNotesVersionQueue(state.allVersions, state.currentVersion, targetVersion);
89
+ }
90
+ else {
91
+ // No allVersions available — just show the target version
92
+ state.releaseNotesVersions = [targetVersion];
93
+ }
94
+ state.releaseNotesLoaded = new Map();
95
+ state.releaseNotesViewIndex = 0;
96
+ state.releaseNotesLoadingVersion = undefined;
97
+ this.pendingReleaseNotesVersion = null;
98
+ if (this.releaseNotesDebounceTimer) {
99
+ clearTimeout(this.releaseNotesDebounceTimer);
100
+ this.releaseNotesDebounceTimer = null;
101
+ }
102
+ this.resolveDebouncedLoad?.(false);
103
+ this.resolveDebouncedLoad = null;
104
+ return result;
105
+ }
106
+ /**
107
+ * Load release notes for a specific version by index.
108
+ * Returns true if a load was triggered, false if nothing to load.
109
+ */
110
+ async loadVersionAtIndex(state, index, onLoaded) {
111
+ if (!state.releaseNotesVersions || !state.releaseNotesLoaded)
112
+ return false;
113
+ if (index < 0 || index >= state.releaseNotesVersions.length)
114
+ return false;
115
+ const version = state.releaseNotesVersions[index];
116
+ // Already loaded
117
+ if (state.releaseNotesLoaded.has(version))
118
+ return false;
119
+ if (state.releaseNotesLoadingVersion) {
120
+ this.pendingReleaseNotesVersion = version;
121
+ return false;
122
+ }
123
+ return await this.scheduleVersionLoad(state, version, onLoaded);
124
+ }
125
+ scheduleVersionLoad(state, version, onLoaded) {
126
+ if (this.releaseNotesDebounceTimer) {
127
+ clearTimeout(this.releaseNotesDebounceTimer);
128
+ this.releaseNotesDebounceTimer = null;
129
+ }
130
+ this.resolveDebouncedLoad?.(false);
131
+ this.pendingReleaseNotesVersion = version;
132
+ return new Promise((resolve) => {
133
+ this.resolveDebouncedLoad = resolve;
134
+ this.releaseNotesDebounceTimer = setTimeout(() => {
135
+ this.releaseNotesDebounceTimer = null;
136
+ this.resolveDebouncedLoad = null;
137
+ const nextVersion = this.pendingReleaseNotesVersion;
138
+ this.pendingReleaseNotesVersion = null;
139
+ if (!nextVersion || state.releaseNotesLoadingVersion) {
140
+ resolve(false);
141
+ return;
142
+ }
143
+ void this.loadVersion(state, nextVersion, onLoaded).then(resolve);
144
+ }, RELEASE_NOTES_LOAD_DEBOUNCE_MS);
145
+ });
146
+ }
147
+ async loadVersion(state, version, onLoaded) {
148
+ const loadedNotes = state.releaseNotesLoaded;
149
+ if (!loadedNotes)
150
+ return false;
151
+ state.releaseNotesLoadingVersion = version;
152
+ onLoaded(); // Re-render to show loading indicator
153
+ try {
154
+ const notes = await services_1.changelogFetcher.fetchReleaseNotesForVersion(state.name, version, this.abortController?.signal);
155
+ loadedNotes.set(version, notes);
156
+ return true;
157
+ }
158
+ catch (error) {
159
+ if (error instanceof DOMException && error.name === 'AbortError') {
160
+ return false;
161
+ }
162
+ loadedNotes.set(version, null);
163
+ return true;
164
+ }
165
+ finally {
166
+ state.releaseNotesLoadingVersion = undefined;
167
+ onLoaded(); // Re-render with new content or recovered state
168
+ const pendingVersion = this.pendingReleaseNotesVersion;
169
+ this.pendingReleaseNotesVersion = null;
170
+ if (pendingVersion &&
171
+ pendingVersion !== version &&
172
+ loadedNotes &&
173
+ !loadedNotes.has(pendingVersion)) {
174
+ void this.loadVersion(state, pendingVersion, onLoaded);
175
+ }
176
+ }
177
+ }
178
+ /**
179
+ * Navigate to the next or previous version in the release notes list.
180
+ * Returns the new view index, or -1 if navigation is not possible.
181
+ */
182
+ navigateVersion(state, direction) {
183
+ if (!state.releaseNotesVersions || state.releaseNotesVersions.length === 0)
184
+ return -1;
185
+ const currentIndex = state.releaseNotesViewIndex ?? 0;
186
+ const newIndex = direction === 'older' ? currentIndex + 1 : currentIndex - 1;
187
+ if (newIndex < 0 || newIndex >= state.releaseNotesVersions.length)
188
+ return -1;
189
+ state.releaseNotesViewIndex = newIndex;
190
+ return newIndex;
191
+ }
192
+ /**
193
+ * Check if the version at the given index is already loaded.
194
+ */
195
+ isVersionLoaded(state, index) {
196
+ if (!state.releaseNotesVersions || !state.releaseNotesLoaded)
197
+ return false;
198
+ if (index < 0 || index >= state.releaseNotesVersions.length)
199
+ return false;
200
+ return state.releaseNotesLoaded.has(state.releaseNotesVersions[index]);
201
+ }
202
+ /**
203
+ * Get the total number of versions available.
204
+ */
205
+ getVersionCount(state) {
206
+ return state.releaseNotesVersions?.length ?? 0;
207
+ }
208
+ /**
209
+ * Check if navigation in a direction is possible.
210
+ */
211
+ canNavigate(state, direction) {
212
+ if (!state.releaseNotesVersions || state.releaseNotesVersions.length === 0)
213
+ return false;
214
+ const currentIndex = state.releaseNotesViewIndex ?? 0;
215
+ if (direction === 'newer')
216
+ return currentIndex > 0;
217
+ return currentIndex < state.releaseNotesVersions.length - 1;
218
+ }
219
+ buildReleaseNotesVersionQueue(allVersions, currentVersion, targetVersion) {
220
+ const cleanTarget = semver.clean(targetVersion);
221
+ if (!cleanTarget) {
222
+ return [];
223
+ }
224
+ const cleanCurrent = semver.clean(currentVersion);
225
+ const versionsAtOrBelowTarget = Array.from(new Set(allVersions
226
+ .map((version) => semver.clean(version))
227
+ .filter((version) => version !== null)
228
+ .filter((version) => semver.lte(version, cleanTarget)))).sort(semver.rcompare);
229
+ if (!cleanCurrent) {
230
+ return versionsAtOrBelowTarget;
231
+ }
232
+ const relevantVersions = versionsAtOrBelowTarget.filter((version) => semver.gte(version, cleanCurrent));
233
+ return relevantVersions;
234
+ }
235
+ }
236
+ exports.PackageInfoModalController = PackageInfoModalController;
237
+ //# sourceMappingURL=package-info-modal-controller.js.map
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.VulnerabilityAuditController = void 0;
4
+ const services_1 = require("../../services");
5
+ const vulnerability_1 = require("../presenters/vulnerability");
6
+ class VulnerabilityAuditController {
7
+ constructor() {
8
+ this.tracker = new services_1.BackgroundAuditTracker();
9
+ this.cache = new Map();
10
+ this.drainPromise = null;
11
+ }
12
+ getCachedSummary(packageName, currentVersionSpecifier, type) {
13
+ return this.cache.get(this.getCacheKey(packageName, currentVersionSpecifier, type));
14
+ }
15
+ getProgress() {
16
+ return this.tracker.getProgress();
17
+ }
18
+ enqueueStates(selectionStates, onUpdate) {
19
+ const packages = selectionStates.map((state) => ({
20
+ name: state.name,
21
+ version: state.currentVersionSpecifier,
22
+ }));
23
+ const added = this.tracker.enqueue(packages);
24
+ if (added > 0) {
25
+ this.drain(selectionStates, onUpdate);
26
+ }
27
+ }
28
+ drain(selectionStates, onUpdate) {
29
+ if (this.drainPromise) {
30
+ return;
31
+ }
32
+ this.drainPromise = (async () => {
33
+ while (true) {
34
+ const batch = this.tracker.reserveNextBatch(20);
35
+ if (batch.packageNames.length === 0) {
36
+ break;
37
+ }
38
+ try {
39
+ const vulnerabilityData = await (0, services_1.fetchVulnerabilities)(batch.packages);
40
+ const batchNames = new Set(batch.packageNames);
41
+ for (const state of selectionStates) {
42
+ if (!batchNames.has(state.name)) {
43
+ continue;
44
+ }
45
+ const vulnerability = vulnerabilityData.get(state.name);
46
+ if (!vulnerability ||
47
+ vulnerability.vulnerabilities.length === 0 ||
48
+ !vulnerability.highestSeverity) {
49
+ continue;
50
+ }
51
+ const summary = (0, vulnerability_1.createVulnerabilitySummary)(state.vulnerability, vulnerability.vulnerabilities.map((item) => ({
52
+ id: item.id,
53
+ title: item.title,
54
+ severity: item.severity,
55
+ url: item.url,
56
+ })), vulnerability.highestSeverity);
57
+ const merged = (0, vulnerability_1.mergeVulnerabilitySummary)(state.vulnerability, summary);
58
+ state.vulnerability = merged;
59
+ this.cache.set(this.getCacheKey(state.name, state.currentVersionSpecifier, state.type), merged);
60
+ }
61
+ }
62
+ catch {
63
+ // Best-effort enrichment only. Keep the UI responsive on audit failures.
64
+ }
65
+ finally {
66
+ this.tracker.markCompleted(batch.packageNames);
67
+ onUpdate?.();
68
+ }
69
+ }
70
+ })().finally(() => {
71
+ this.drainPromise = null;
72
+ if (this.tracker.getProgress().isRunning) {
73
+ this.drain(selectionStates, onUpdate);
74
+ }
75
+ });
76
+ }
77
+ getCacheKey(packageName, currentVersionSpecifier, type) {
78
+ return `${packageName}@${currentVersionSpecifier}@${type}`;
79
+ }
80
+ }
81
+ exports.VulnerabilityAuditController = VulnerabilityAuditController;
82
+ //# sourceMappingURL=vulnerability-audit-controller.js.map
package/dist/ui/index.js CHANGED
@@ -17,5 +17,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./utils"), exports);
18
18
  __exportStar(require("./state"), exports);
19
19
  __exportStar(require("./renderer"), exports);
20
+ __exportStar(require("./modal"), exports);
21
+ __exportStar(require("./presenters"), exports);
22
+ __exportStar(require("./controllers"), exports);
20
23
  __exportStar(require("./input-handler"), exports);
21
24
  //# sourceMappingURL=index.js.map
@@ -47,8 +47,37 @@ class InputHandler {
47
47
  }
48
48
  return;
49
49
  }
50
+ // Handle info modal input (scroll and close)
51
+ if (uiState.showInfoModal) {
52
+ if (key) {
53
+ switch (key.name) {
54
+ case 'escape':
55
+ this.onAction({ type: 'toggle_info_modal' });
56
+ return;
57
+ case 'i':
58
+ case 'I':
59
+ this.onAction({ type: 'toggle_info_modal' });
60
+ return;
61
+ case 'up':
62
+ this.onAction({ type: 'scroll_info_modal_up' });
63
+ return;
64
+ case 'down':
65
+ this.onAction({ type: 'scroll_info_modal_down' });
66
+ return;
67
+ case 'left':
68
+ this.onAction({ type: 'navigate_info_modal_version', direction: 'newer' });
69
+ return;
70
+ case 'right':
71
+ this.onAction({ type: 'navigate_info_modal_version', direction: 'older' });
72
+ return;
73
+ default:
74
+ return; // Consume all other keys while modal is open
75
+ }
76
+ }
77
+ return;
78
+ }
50
79
  // Check for '/' character to handle filter mode (only when not in modal)
51
- if (str === '/' && !uiState.showInfoModal) {
80
+ if (str === '/') {
52
81
  if (uiState.filterMode) {
53
82
  // Apply search (exit filter mode but keep the filter)
54
83
  this.onAction({ type: 'exit_filter_mode' });
@@ -146,19 +175,19 @@ class InputHandler {
146
175
  break;
147
176
  case 'd':
148
177
  case 'D':
149
- if (!uiState.showInfoModal && !uiState.showThemeModal && !uiState.filterMode) {
178
+ if (!uiState.showThemeModal && !uiState.filterMode) {
150
179
  this.onAction({ type: 'toggle_dep_type_filter', depType: 'devDependencies' });
151
180
  }
152
181
  break;
153
182
  case 'p':
154
183
  case 'P':
155
- if (!uiState.showInfoModal && !uiState.showThemeModal && !uiState.filterMode) {
184
+ if (!uiState.showThemeModal && !uiState.filterMode) {
156
185
  this.onAction({ type: 'toggle_dep_type_filter', depType: 'peerDependencies' });
157
186
  }
158
187
  break;
159
188
  case 'o':
160
189
  case 'O':
161
- if (!uiState.showInfoModal && !uiState.showThemeModal && !uiState.filterMode) {
190
+ if (!uiState.showThemeModal && !uiState.filterMode) {
162
191
  this.onAction({ type: 'toggle_dep_type_filter', depType: 'optionalDependencies' });
163
192
  }
164
193
  break;
@@ -166,16 +195,18 @@ class InputHandler {
166
195
  case 'I':
167
196
  this.onAction({ type: 'toggle_info_modal' });
168
197
  break;
198
+ case 's':
199
+ case 'S':
200
+ if (!uiState.showThemeModal) {
201
+ this.onAction({ type: 'trigger_audit_scan' });
202
+ }
203
+ break;
169
204
  case 't':
170
205
  case 'T':
171
206
  this.onAction({ type: 'toggle_theme_modal' });
172
207
  break;
173
208
  case 'escape':
174
- // Close modal if open
175
- if (uiState.showInfoModal) {
176
- this.onAction({ type: 'toggle_info_modal' });
177
- }
178
- else if (uiState.filterQuery) {
209
+ if (uiState.filterQuery) {
179
210
  // Clear filter if one is applied
180
211
  this.onAction({ type: 'exit_filter_mode', clearQuery: true });
181
212
  }
@@ -0,0 +1,22 @@
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"), exports);
18
+ __exportStar(require("./layout"), exports);
19
+ __exportStar(require("./package-info"), exports);
20
+ __exportStar(require("./package-info-sections"), exports);
21
+ __exportStar(require("./theme-selector"), exports);
22
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,84 @@
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.getModalWidth = getModalWidth;
7
+ exports.renderModalRow = renderModalRow;
8
+ exports.renderModalSeparator = renderModalSeparator;
9
+ exports.fitModalSections = fitModalSections;
10
+ exports.getModalSectionRowCount = getModalSectionRowCount;
11
+ exports.getModalFrameHeight = getModalFrameHeight;
12
+ exports.renderModalFrame = renderModalFrame;
13
+ const chalk_1 = __importDefault(require("chalk"));
14
+ const utils_1 = require("../utils");
15
+ function getModalWidth(terminalWidth, minWidth, maxWidth) {
16
+ return Math.min(Math.max(minWidth, terminalWidth - 6), maxWidth);
17
+ }
18
+ function renderModalRow(padding, modalWidth, text) {
19
+ const rowLength = (0, utils_1.getVisualLength)(text);
20
+ const rowPadding = Math.max(0, modalWidth - 4 - rowLength);
21
+ return (' '.repeat(padding) +
22
+ chalk_1.default.gray('│') +
23
+ ' ' +
24
+ text +
25
+ ' '.repeat(rowPadding) +
26
+ ' ' +
27
+ chalk_1.default.gray('│'));
28
+ }
29
+ function renderModalSeparator(padding, modalWidth) {
30
+ return ' '.repeat(padding) + chalk_1.default.gray('├' + '─'.repeat(modalWidth - 2) + '┤');
31
+ }
32
+ function fitModalSections(sections, maxHeight, trimOrder) {
33
+ const activeSections = sections.map((section) => ({ ...section, rows: [...section.rows] }));
34
+ while (getModalFrameHeight(activeSections) > maxHeight) {
35
+ let trimmed = false;
36
+ for (const key of trimOrder) {
37
+ const section = activeSections.find((candidate) => candidate.key === key && candidate.rows.length > 0 && !candidate.required);
38
+ if (!section) {
39
+ continue;
40
+ }
41
+ if (section.rows.length === 1) {
42
+ section.rows = [];
43
+ }
44
+ else {
45
+ section.rows = section.rows.slice(0, -1);
46
+ }
47
+ trimmed = true;
48
+ break;
49
+ }
50
+ if (!trimmed) {
51
+ break;
52
+ }
53
+ }
54
+ return activeSections.filter((section) => section.rows.length > 0);
55
+ }
56
+ function getModalSectionRowCount(sections) {
57
+ const visible = sections.filter((section) => section.rows.length > 0);
58
+ return visible.reduce((sum, section, index) => sum + section.rows.length + (index > 0 ? 1 : 0), 0);
59
+ }
60
+ function getModalFrameHeight(sections) {
61
+ return 2 + getModalSectionRowCount(sections);
62
+ }
63
+ function renderModalFrame(sections, options) {
64
+ const modalWidth = getModalWidth(options.terminalWidth, options.minWidth, options.maxWidth);
65
+ const padding = Math.floor((options.terminalWidth - modalWidth) / 2);
66
+ const contentHeight = getModalFrameHeight(sections);
67
+ const topPadding = Math.max(0, Math.floor((options.terminalHeight - contentHeight) / 2));
68
+ const lines = [];
69
+ for (let i = 0; i < topPadding; i++) {
70
+ lines.push('');
71
+ }
72
+ lines.push(' '.repeat(padding) + chalk_1.default.gray('╭' + '─'.repeat(modalWidth - 2) + '╮'));
73
+ sections.forEach((section, index) => {
74
+ if (index > 0) {
75
+ lines.push(renderModalSeparator(padding, modalWidth));
76
+ }
77
+ section.rows.forEach((row) => {
78
+ lines.push(renderModalRow(padding, modalWidth, row));
79
+ });
80
+ });
81
+ lines.push(' '.repeat(padding) + chalk_1.default.gray('╰' + '─'.repeat(modalWidth - 2) + '╯'));
82
+ return lines;
83
+ }
84
+ //# sourceMappingURL=layout.js.map