inup 1.5.5 → 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 +13 -606
  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 +58 -75
  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 -369
  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 -455
  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 +4 -1
  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 +6 -5
@@ -38,12 +38,10 @@ const semver = __importStar(require("semver"));
38
38
  const services_1 = require("../../services");
39
39
  const RELEASE_NOTES_LOAD_DEBOUNCE_MS = 120;
40
40
  class PackageInfoModalController {
41
- constructor() {
42
- this.abortController = null;
43
- this.pendingReleaseNotesVersion = null;
44
- this.releaseNotesDebounceTimer = null;
45
- this.resolveDebouncedLoad = null;
46
- }
41
+ abortController = null;
42
+ pendingReleaseNotesVersion = null;
43
+ releaseNotesDebounceTimer = null;
44
+ resolveDebouncedLoad = null;
47
45
  /**
48
46
  * Cancel any in-flight hydrate/release-notes fetches.
49
47
  * Safe to call multiple times or when nothing is in flight.
@@ -68,32 +66,11 @@ class PackageInfoModalController {
68
66
  if (!metadata) {
69
67
  return null;
70
68
  }
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
69
  // Compute the version range for release notes
86
70
  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;
71
+ const releaseNotesVersions = state.allVersions && state.allVersions.length > 0
72
+ ? this.buildReleaseNotesVersionQueue(state.allVersions, state.currentVersion, targetVersion)
73
+ : [targetVersion];
97
74
  this.pendingReleaseNotesVersion = null;
98
75
  if (this.releaseNotesDebounceTimer) {
99
76
  clearTimeout(this.releaseNotesDebounceTimer);
@@ -101,7 +78,21 @@ class PackageInfoModalController {
101
78
  }
102
79
  this.resolveDebouncedLoad?.(false);
103
80
  this.resolveDebouncedLoad = null;
104
- return result;
81
+ return {
82
+ name: state.name,
83
+ patch: {
84
+ description: metadata.description,
85
+ homepage: metadata.homepage,
86
+ repository: metadata.releaseNotes,
87
+ weeklyDownloads: metadata.weeklyDownloads,
88
+ author: metadata.author,
89
+ license: metadata.license,
90
+ releaseNotesVersions,
91
+ releaseNotesLoaded: new Map(),
92
+ releaseNotesViewIndex: 0,
93
+ releaseNotesLoadingVersion: undefined,
94
+ },
95
+ };
105
96
  }
106
97
  /**
107
98
  * Load release notes for a specific version by index.
@@ -4,11 +4,9 @@ exports.VulnerabilityAuditController = void 0;
4
4
  const services_1 = require("../../services");
5
5
  const vulnerability_1 = require("../presenters/vulnerability");
6
6
  class VulnerabilityAuditController {
7
- constructor() {
8
- this.tracker = new services_1.BackgroundAuditTracker();
9
- this.cache = new Map();
10
- this.drainPromise = null;
11
- }
7
+ tracker = new services_1.BackgroundAuditTracker();
8
+ cache = new Map();
9
+ drainPromise = null;
12
10
  getCachedSummary(packageName, currentVersionSpecifier, type) {
13
11
  return this.cache.get(this.getCacheKey(packageName, currentVersionSpecifier, type));
14
12
  }
@@ -25,16 +23,18 @@ class VulnerabilityAuditController {
25
23
  this.drain(selectionStates, onUpdate);
26
24
  }
27
25
  }
28
- drain(selectionStates, onUpdate) {
26
+ async drain(selectionStates, onUpdate) {
29
27
  if (this.drainPromise) {
30
- return;
28
+ return [];
31
29
  }
30
+ const allUpdates = [];
32
31
  this.drainPromise = (async () => {
33
32
  while (true) {
34
33
  const batch = this.tracker.reserveNextBatch(20);
35
34
  if (batch.packageNames.length === 0) {
36
35
  break;
37
36
  }
37
+ const batchUpdates = [];
38
38
  try {
39
39
  const vulnerabilityData = await (0, services_1.fetchVulnerabilities)(batch.packages);
40
40
  const batchNames = new Set(batch.packageNames);
@@ -55,8 +55,8 @@ class VulnerabilityAuditController {
55
55
  url: item.url,
56
56
  })), vulnerability.highestSeverity);
57
57
  const merged = (0, vulnerability_1.mergeVulnerabilitySummary)(state.vulnerability, summary);
58
- state.vulnerability = merged;
59
58
  this.cache.set(this.getCacheKey(state.name, state.currentVersionSpecifier, state.type), merged);
59
+ batchUpdates.push({ name: state.name, patch: { vulnerability: merged } });
60
60
  }
61
61
  }
62
62
  catch {
@@ -64,6 +64,13 @@ class VulnerabilityAuditController {
64
64
  }
65
65
  finally {
66
66
  this.tracker.markCompleted(batch.packageNames);
67
+ for (const update of batchUpdates) {
68
+ for (const state of selectionStates) {
69
+ if (state.name === update.name)
70
+ Object.assign(state, update.patch);
71
+ }
72
+ }
73
+ allUpdates.push(...batchUpdates);
67
74
  onUpdate?.();
68
75
  }
69
76
  }
@@ -73,6 +80,8 @@ class VulnerabilityAuditController {
73
80
  this.drain(selectionStates, onUpdate);
74
81
  }
75
82
  });
83
+ await this.drainPromise;
84
+ return allUpdates;
76
85
  }
77
86
  getCacheKey(packageName, currentVersionSpecifier, type) {
78
87
  return `${packageName}@${currentVersionSpecifier}@${type}`;
package/dist/ui/index.js CHANGED
@@ -21,4 +21,7 @@ __exportStar(require("./modal"), exports);
21
21
  __exportStar(require("./presenters"), exports);
22
22
  __exportStar(require("./controllers"), exports);
23
23
  __exportStar(require("./input-handler"), exports);
24
+ __exportStar(require("./keymap"), exports);
25
+ __exportStar(require("./themes"), exports);
26
+ __exportStar(require("./themes-colors"), exports);
24
27
  //# sourceMappingURL=index.js.map
@@ -2,7 +2,14 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ConfirmationInputHandler = exports.InputHandler = void 0;
4
4
  const utils_1 = require("./utils");
5
+ const keymap_1 = require("./keymap");
5
6
  class InputHandler {
7
+ stateManager;
8
+ onAction;
9
+ onConfirm;
10
+ // Must synchronously release terminal state (alt screen, raw mode, cursor) —
11
+ // it runs immediately before process.exit(0) on Ctrl+C, with no chance to await.
12
+ onCancel;
6
13
  constructor(stateManager, onAction, onConfirm, onCancel) {
7
14
  this.stateManager = stateManager;
8
15
  this.onAction = onAction;
@@ -70,6 +77,24 @@ class InputHandler {
70
77
  }
71
78
  return;
72
79
  }
80
+ // Handle help overlay input (close or scroll)
81
+ if (uiState.showHelpModal) {
82
+ if (str === '?' || (key && key.name === 'escape')) {
83
+ this.onAction({ type: 'toggle_help_modal' });
84
+ return;
85
+ }
86
+ if (key) {
87
+ if (key.name === 'up') {
88
+ this.onAction({ type: 'scroll_help_modal_up' });
89
+ return;
90
+ }
91
+ if (key.name === 'down') {
92
+ this.onAction({ type: 'scroll_help_modal_down' });
93
+ return;
94
+ }
95
+ }
96
+ return; // consume all other keys while help is open
97
+ }
73
98
  // Handle info modal input (scroll and close)
74
99
  if (uiState.showInfoModal) {
75
100
  if (key) {
@@ -111,6 +136,11 @@ class InputHandler {
111
136
  this.onAction({ type: 'toggle_debug_modal' });
112
137
  return;
113
138
  }
139
+ // '?' opens the help overlay (outside of filter mode)
140
+ if (str === '?' && !uiState.filterMode) {
141
+ this.onAction({ type: 'toggle_help_modal' });
142
+ return;
143
+ }
114
144
  // Check for '/' character to handle filter mode (only when not in modal)
115
145
  if (str === '/') {
116
146
  if (uiState.filterMode) {
@@ -169,84 +199,36 @@ class InputHandler {
169
199
  }
170
200
  return;
171
201
  }
172
- // Normal mode (not in filter mode)
173
- if (!key) {
202
+ // Normal mode (not in filter mode). Keys resolve through the keymap so that
203
+ // dispatch, the help overlay, the footer, and the README share one source.
204
+ if (!key || !key.name) {
174
205
  return;
175
206
  }
176
- switch (key.name) {
177
- case 'up':
178
- this.onAction({ type: 'navigate_up' });
179
- break;
180
- case 'down':
181
- this.onAction({ type: 'navigate_down' });
182
- break;
183
- case 'left':
184
- this.onAction({ type: 'select_left' });
185
- break;
186
- case 'right':
187
- this.onAction({ type: 'select_right' });
188
- break;
189
- case 'return':
190
- // Check if any packages are selected
191
- const selectedCount = states.filter((s) => s.loadState === 'ready' && s.selectedOption !== 'none').length;
192
- if (selectedCount === 0) {
193
- // Do nothing if no packages selected
194
- return;
195
- }
196
- this.cleanup();
197
- this.onConfirm(states);
207
+ // Enter confirms the selection (guarding against an empty selection). It runs
208
+ // the cleanup/confirm path directly rather than going through onAction.
209
+ if (key.name === 'return') {
210
+ const selectedCount = states.filter((s) => s.loadState === 'ready' && s.selectedOption !== 'none').length;
211
+ if (selectedCount === 0) {
212
+ this.onAction({ type: 'notify_empty_selection' });
198
213
  return;
199
- case 'm':
200
- case 'M':
201
- this.onAction({ type: 'bulk_select_minor' });
202
- break;
203
- case 'l':
204
- case 'L':
205
- this.onAction({ type: 'bulk_select_latest' });
206
- break;
207
- case 'u':
208
- case 'U':
209
- this.onAction({ type: 'bulk_unselect_all' });
210
- break;
211
- case 'd':
212
- case 'D':
213
- if (!uiState.showThemeModal && !uiState.filterMode) {
214
- this.onAction({ type: 'toggle_dep_type_filter', depType: 'devDependencies' });
215
- }
216
- break;
217
- case 'p':
218
- case 'P':
219
- if (!uiState.showThemeModal && !uiState.filterMode) {
220
- this.onAction({ type: 'toggle_dep_type_filter', depType: 'peerDependencies' });
221
- }
222
- break;
223
- case 'o':
224
- case 'O':
225
- if (!uiState.showThemeModal && !uiState.filterMode) {
226
- this.onAction({ type: 'toggle_dep_type_filter', depType: 'optionalDependencies' });
227
- }
228
- break;
229
- case 'i':
230
- case 'I':
231
- this.onAction({ type: 'toggle_info_modal' });
232
- break;
233
- case 's':
234
- case 'S':
235
- if (!uiState.showThemeModal) {
236
- this.onAction({ type: 'trigger_audit_scan' });
237
- }
238
- break;
239
- case 't':
240
- case 'T':
241
- this.onAction({ type: 'toggle_theme_modal' });
242
- break;
243
- case 'escape':
244
- if (uiState.filterQuery) {
245
- // Clear filter if one is applied
246
- this.onAction({ type: 'exit_filter_mode', clearQuery: true });
247
- }
248
- // Otherwise do nothing - Escape no longer exits the CLI
249
- break;
214
+ }
215
+ this.cleanup();
216
+ this.onConfirm(states);
217
+ return;
218
+ }
219
+ // Escape clears an active search filter; otherwise it is a no-op (it no
220
+ // longer exits the CLI).
221
+ if (key.name === 'escape') {
222
+ if (uiState.filterQuery) {
223
+ this.onAction({ type: 'exit_filter_mode', clearQuery: true });
224
+ }
225
+ return;
226
+ }
227
+ // Shifted letters become uppercase tokens so the vim pair g/G stays distinct.
228
+ const token = key.shift && /^[a-z]$/.test(key.name) ? key.name.toUpperCase() : key.name;
229
+ const binding = (0, keymap_1.findBinding)(token);
230
+ if (binding?.action) {
231
+ this.onAction(binding.action);
250
232
  }
251
233
  }
252
234
  handleResize(height) {
@@ -258,6 +240,7 @@ class InputHandler {
258
240
  }
259
241
  exports.InputHandler = InputHandler;
260
242
  class ConfirmationInputHandler {
243
+ onConfirm;
261
244
  constructor(onConfirm) {
262
245
  this.onConfirm = onConfirm;
263
246
  }
@@ -0,0 +1,208 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.KEY_BINDINGS = exports.KEY_GROUPS = void 0;
4
+ exports.findBinding = findBinding;
5
+ exports.getFooterHints = getFooterHints;
6
+ exports.getHelpGroups = getHelpGroups;
7
+ exports.renderReadmeKeyTable = renderReadmeKeyTable;
8
+ /** Group display order for the help overlay and README. */
9
+ exports.KEY_GROUPS = ['Navigation', 'Selection', 'Filtering', 'View'];
10
+ exports.KEY_BINDINGS = [
11
+ // Navigation
12
+ {
13
+ tokens: ['up', 'k'],
14
+ action: { type: 'navigate_up' },
15
+ displayKeys: '↑ / k',
16
+ help: 'Move up',
17
+ group: 'Navigation',
18
+ footer: { keyLabel: '↑/↓', label: 'Move' },
19
+ },
20
+ {
21
+ tokens: ['down', 'j'],
22
+ action: { type: 'navigate_down' },
23
+ displayKeys: '↓ / j',
24
+ help: 'Move down',
25
+ group: 'Navigation',
26
+ },
27
+ {
28
+ tokens: ['g'],
29
+ action: { type: 'navigate_top' },
30
+ displayKeys: 'g',
31
+ help: 'Jump to the first package',
32
+ group: 'Navigation',
33
+ },
34
+ {
35
+ tokens: ['G'],
36
+ action: { type: 'navigate_bottom' },
37
+ displayKeys: 'G',
38
+ help: 'Jump to the last package',
39
+ group: 'Navigation',
40
+ },
41
+ // Selection
42
+ {
43
+ tokens: ['left'],
44
+ action: { type: 'select_left' },
45
+ displayKeys: '←',
46
+ help: 'Cycle selection left (none → range → latest)',
47
+ group: 'Selection',
48
+ footer: { keyLabel: '←/→', label: 'Select' },
49
+ },
50
+ {
51
+ tokens: ['right'],
52
+ action: { type: 'select_right' },
53
+ displayKeys: '→',
54
+ help: 'Cycle selection right (none → range → latest)',
55
+ group: 'Selection',
56
+ },
57
+ {
58
+ tokens: ['space'],
59
+ action: { type: 'toggle_selection' },
60
+ displayKeys: 'Space',
61
+ help: 'Toggle the current package on/off',
62
+ group: 'Selection',
63
+ },
64
+ {
65
+ tokens: ['m'],
66
+ action: { type: 'bulk_select_minor' },
67
+ displayKeys: 'm',
68
+ help: 'Select all minor/patch updates',
69
+ group: 'Selection',
70
+ footer: { keyLabel: 'M', label: 'Minor' },
71
+ },
72
+ {
73
+ tokens: ['l'],
74
+ action: { type: 'bulk_select_latest' },
75
+ displayKeys: 'l',
76
+ help: 'Select all latest updates (including major)',
77
+ group: 'Selection',
78
+ footer: { keyLabel: 'L', label: 'All' },
79
+ },
80
+ {
81
+ tokens: ['u'],
82
+ action: { type: 'bulk_unselect_all' },
83
+ displayKeys: 'u',
84
+ help: 'Unselect all packages',
85
+ group: 'Selection',
86
+ footer: { keyLabel: 'U', label: 'None' },
87
+ },
88
+ {
89
+ displayKeys: 'Enter',
90
+ help: 'Confirm selection and upgrade',
91
+ group: 'Selection',
92
+ },
93
+ // Filtering
94
+ {
95
+ displayKeys: '/',
96
+ help: 'Search packages by name',
97
+ group: 'Filtering',
98
+ footer: { keyLabel: '/', label: 'Search' },
99
+ },
100
+ {
101
+ tokens: ['d'],
102
+ action: { type: 'toggle_dep_type_filter', depType: 'devDependencies' },
103
+ displayKeys: 'd',
104
+ help: 'Toggle devDependencies',
105
+ group: 'Filtering',
106
+ footer: { keyLabel: 'D/P/O', label: 'Filter' },
107
+ },
108
+ {
109
+ tokens: ['p'],
110
+ action: { type: 'toggle_dep_type_filter', depType: 'peerDependencies' },
111
+ displayKeys: 'p',
112
+ help: 'Toggle peerDependencies',
113
+ group: 'Filtering',
114
+ },
115
+ {
116
+ tokens: ['o'],
117
+ action: { type: 'toggle_dep_type_filter', depType: 'optionalDependencies' },
118
+ displayKeys: 'o',
119
+ help: 'Toggle optionalDependencies',
120
+ group: 'Filtering',
121
+ },
122
+ {
123
+ tokens: ['s'],
124
+ action: { type: 'trigger_audit_scan' },
125
+ displayKeys: 's',
126
+ help: 'Run the vulnerability audit',
127
+ group: 'Filtering',
128
+ footer: { keyLabel: 'S', label: 'Audit' },
129
+ },
130
+ {
131
+ tokens: ['v'],
132
+ action: { type: 'toggle_vulnerable_filter' },
133
+ displayKeys: 'v',
134
+ help: 'Show only vulnerable packages',
135
+ group: 'Filtering',
136
+ },
137
+ {
138
+ displayKeys: 'Esc',
139
+ help: 'Clear the active search filter',
140
+ group: 'Filtering',
141
+ },
142
+ // View
143
+ {
144
+ tokens: ['i'],
145
+ action: { type: 'toggle_info_modal' },
146
+ displayKeys: 'i',
147
+ help: 'View package details and changelog',
148
+ group: 'View',
149
+ footer: { keyLabel: 'I', label: 'Info' },
150
+ },
151
+ {
152
+ tokens: ['t'],
153
+ action: { type: 'toggle_theme_modal' },
154
+ displayKeys: 't',
155
+ help: 'Change the color theme',
156
+ group: 'View',
157
+ },
158
+ {
159
+ displayKeys: '?',
160
+ help: 'Show this help',
161
+ group: 'View',
162
+ footer: { keyLabel: '?', label: 'Help' },
163
+ },
164
+ {
165
+ displayKeys: '!',
166
+ help: 'Show the performance/debug panel',
167
+ group: 'View',
168
+ },
169
+ ];
170
+ /**
171
+ * Resolve a pressed key token to its binding. Tries an exact match first so the
172
+ * case-sensitive vim pair `g`/`G` is honored, then falls back to the lowercase
173
+ * letter so shifted letters (e.g. `M`) still trigger their lowercase binding.
174
+ */
175
+ function findBinding(token) {
176
+ const exact = exports.KEY_BINDINGS.find((binding) => binding.tokens?.includes(token));
177
+ if (exact)
178
+ return exact;
179
+ if (/^[A-Z]$/.test(token)) {
180
+ const lower = token.toLowerCase();
181
+ return exports.KEY_BINDINGS.find((binding) => binding.tokens?.includes(lower));
182
+ }
183
+ return undefined;
184
+ }
185
+ // Computed once at module load — KEY_BINDINGS is static so these never change.
186
+ const _footerHints = exports.KEY_BINDINGS.flatMap((binding) => binding.footer ? [binding.footer] : []);
187
+ const _helpGroups = exports.KEY_GROUPS.map((group) => ({
188
+ group,
189
+ bindings: exports.KEY_BINDINGS.filter((b) => b.group === group),
190
+ })).filter((entry) => entry.bindings.length > 0);
191
+ /** Curated short hints for the compact footer line, in display order. */
192
+ function getFooterHints() {
193
+ return _footerHints;
194
+ }
195
+ /** Bindings grouped for the `?` help overlay, preserving {@link KEY_GROUPS} order. */
196
+ function getHelpGroups() {
197
+ return _helpGroups;
198
+ }
199
+ /**
200
+ * Render the README "Keyboard Shortcuts" markdown table from the keymap. This is
201
+ * the body placed between the `<!-- KEYS:START -->`/`<!-- KEYS:END -->` markers;
202
+ * a test fails if the README drifts from it (see keymap-readme.test.ts).
203
+ */
204
+ function renderReadmeKeyTable() {
205
+ const rows = exports.KEY_BINDINGS.map((binding) => `| \`${binding.displayKeys}\` | ${binding.help} |`);
206
+ return ['| Key | Action |', '|-----|--------|', ...rows].join('\n');
207
+ }
208
+ //# sourceMappingURL=keymap.js.map