@a9s/cli 0.0.1 → 0.1.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 (95) hide show
  1. package/README.md +167 -2
  2. package/dist/scripts/seed.js +310 -0
  3. package/dist/src/App.js +476 -0
  4. package/dist/src/adapters/ServiceAdapter.js +1 -0
  5. package/dist/src/adapters/capabilities/ActionCapability.js +1 -0
  6. package/dist/src/adapters/capabilities/DetailCapability.js +1 -0
  7. package/dist/src/adapters/capabilities/EditCapability.js +1 -0
  8. package/dist/src/adapters/capabilities/YankCapability.js +42 -0
  9. package/dist/src/adapters/capabilities/YankCapability.test.js +29 -0
  10. package/dist/src/components/AdvancedTextInput.js +200 -0
  11. package/dist/src/components/AdvancedTextInput.test.js +190 -0
  12. package/dist/src/components/AutocompleteInput.js +29 -0
  13. package/dist/src/components/DetailPanel.js +12 -0
  14. package/dist/src/components/DiffViewer.js +17 -0
  15. package/dist/src/components/ErrorStatePanel.js +5 -0
  16. package/dist/src/components/HUD.js +31 -0
  17. package/dist/src/components/HelpPanel.js +33 -0
  18. package/dist/src/components/ModeBar.js +43 -0
  19. package/dist/src/components/Table/index.js +109 -0
  20. package/dist/src/components/Table/widths.js +19 -0
  21. package/dist/src/components/TableSkeleton.js +25 -0
  22. package/dist/src/components/YankHelpPanel.js +43 -0
  23. package/dist/src/constants/commands.js +15 -0
  24. package/dist/src/constants/keybindings.js +530 -0
  25. package/dist/src/constants/keys.js +37 -0
  26. package/dist/src/features/AppMainView.integration.test.js +133 -0
  27. package/dist/src/features/AppMainView.js +95 -0
  28. package/dist/src/hooks/inputEvents.js +1 -0
  29. package/dist/src/hooks/mainInputScopes.js +68 -0
  30. package/dist/src/hooks/mainInputScopes.test.js +24 -0
  31. package/dist/src/hooks/useActionController.js +78 -0
  32. package/dist/src/hooks/useAppController.js +102 -0
  33. package/dist/src/hooks/useAppController.test.js +54 -0
  34. package/dist/src/hooks/useAppData.js +48 -0
  35. package/dist/src/hooks/useAwsContext.js +77 -0
  36. package/dist/src/hooks/useAwsProfiles.js +53 -0
  37. package/dist/src/hooks/useAwsRegions.js +105 -0
  38. package/dist/src/hooks/useCommandRouter.js +56 -0
  39. package/dist/src/hooks/useCommandRouter.test.js +27 -0
  40. package/dist/src/hooks/useDetailController.js +57 -0
  41. package/dist/src/hooks/useDetailController.test.js +32 -0
  42. package/dist/src/hooks/useHelpPanel.js +65 -0
  43. package/dist/src/hooks/useHierarchyState.js +39 -0
  44. package/dist/src/hooks/useInputEventProcessor.js +450 -0
  45. package/dist/src/hooks/useInputEventProcessor.test.js +174 -0
  46. package/dist/src/hooks/useKeyChord.js +83 -0
  47. package/dist/src/hooks/useMainInput.js +18 -0
  48. package/dist/src/hooks/useNavigation.js +47 -0
  49. package/dist/src/hooks/usePendingAction.js +8 -0
  50. package/dist/src/hooks/usePickerManager.js +130 -0
  51. package/dist/src/hooks/usePickerState.js +47 -0
  52. package/dist/src/hooks/usePickerTable.js +20 -0
  53. package/dist/src/hooks/useServiceView.js +226 -0
  54. package/dist/src/hooks/useUiHints.js +60 -0
  55. package/dist/src/hooks/useYankMode.js +24 -0
  56. package/dist/src/hooks/yankHeaderMarkers.js +23 -0
  57. package/dist/src/hooks/yankHeaderMarkers.test.js +49 -0
  58. package/dist/src/index.js +30 -0
  59. package/dist/src/services.js +12 -0
  60. package/dist/src/state/atoms.js +27 -0
  61. package/dist/src/types.js +12 -0
  62. package/dist/src/utils/aws.js +39 -0
  63. package/dist/src/utils/debugLogger.js +34 -0
  64. package/dist/src/utils/secretDisplay.js +45 -0
  65. package/dist/src/utils/withFullscreen.js +38 -0
  66. package/dist/src/views/dynamodb/adapter.js +22 -0
  67. package/dist/src/views/iam/adapter.js +258 -0
  68. package/dist/src/views/iam/capabilities/detailCapability.js +93 -0
  69. package/dist/src/views/iam/capabilities/editCapability.js +59 -0
  70. package/dist/src/views/iam/capabilities/yankCapability.js +6 -0
  71. package/dist/src/views/iam/capabilities/yankOptions.js +15 -0
  72. package/dist/src/views/iam/schema.js +7 -0
  73. package/dist/src/views/iam/types.js +1 -0
  74. package/dist/src/views/iam/utils.js +21 -0
  75. package/dist/src/views/route53/adapter.js +22 -0
  76. package/dist/src/views/s3/adapter.js +154 -0
  77. package/dist/src/views/s3/capabilities/actionCapability.js +172 -0
  78. package/dist/src/views/s3/capabilities/detailCapability.js +115 -0
  79. package/dist/src/views/s3/capabilities/editCapability.js +35 -0
  80. package/dist/src/views/s3/capabilities/yankCapability.js +6 -0
  81. package/dist/src/views/s3/capabilities/yankOptions.js +55 -0
  82. package/dist/src/views/s3/client.js +12 -0
  83. package/dist/src/views/s3/fetcher.js +86 -0
  84. package/dist/src/views/s3/schema.js +6 -0
  85. package/dist/src/views/s3/utils.js +19 -0
  86. package/dist/src/views/secretsmanager/adapter.js +188 -0
  87. package/dist/src/views/secretsmanager/capabilities/actionCapability.js +193 -0
  88. package/dist/src/views/secretsmanager/capabilities/detailCapability.js +46 -0
  89. package/dist/src/views/secretsmanager/capabilities/editCapability.js +116 -0
  90. package/dist/src/views/secretsmanager/capabilities/yankCapability.js +7 -0
  91. package/dist/src/views/secretsmanager/capabilities/yankOptions.js +68 -0
  92. package/dist/src/views/secretsmanager/schema.js +28 -0
  93. package/dist/src/views/secretsmanager/types.js +1 -0
  94. package/package.json +68 -5
  95. package/index.js +0 -1
@@ -0,0 +1,530 @@
1
+ import { KB } from "./keys.js";
2
+ /** Convert a trigger to a human-readable display string */
3
+ export function triggerToString(t) {
4
+ switch (t.type) {
5
+ case "key":
6
+ return t.char;
7
+ case "special":
8
+ return SPECIAL_DISPLAY[t.name];
9
+ case "chord":
10
+ return t.keys.join(" ");
11
+ case "any":
12
+ return t.of.map(triggerToString).join(" / ");
13
+ }
14
+ }
15
+ const SPECIAL_DISPLAY = {
16
+ return: "Enter",
17
+ escape: "Esc",
18
+ tab: "Tab",
19
+ upArrow: "↑",
20
+ downArrow: "↓",
21
+ leftArrow: "←",
22
+ rightArrow: "→",
23
+ };
24
+ const SCOPE_LABELS = {
25
+ navigate: "Navigate",
26
+ search: "Search",
27
+ command: "Command",
28
+ yank: "Yank",
29
+ details: "Details",
30
+ upload: "Upload",
31
+ picker: "Pickers",
32
+ help: "Help Panel",
33
+ };
34
+ // ---------------------------------------------------------------------------
35
+ // Registry — THE single source of truth for every keybinding
36
+ // ---------------------------------------------------------------------------
37
+ const j_down = {
38
+ type: "any",
39
+ of: [
40
+ { type: "key", char: "j" },
41
+ { type: "special", name: "downArrow" },
42
+ ],
43
+ };
44
+ const k_up = {
45
+ type: "any",
46
+ of: [
47
+ { type: "key", char: "k" },
48
+ { type: "special", name: "upArrow" },
49
+ ],
50
+ };
51
+ export const KEYBINDINGS = [
52
+ // --- Navigate ---
53
+ {
54
+ action: KB.MOVE_DOWN,
55
+ trigger: j_down,
56
+ scope: "navigate",
57
+ label: "Move selection down",
58
+ shortLabel: "down",
59
+ },
60
+ {
61
+ action: KB.MOVE_UP,
62
+ trigger: k_up,
63
+ scope: "navigate",
64
+ label: "Move selection up",
65
+ shortLabel: "up",
66
+ },
67
+ {
68
+ action: KB.GO_TOP,
69
+ trigger: { type: "chord", keys: ["g", "g"] },
70
+ scope: "navigate",
71
+ label: "Jump to top",
72
+ shortLabel: "top",
73
+ },
74
+ {
75
+ action: KB.GO_BOTTOM,
76
+ trigger: { type: "key", char: "G" },
77
+ scope: "navigate",
78
+ label: "Jump to bottom",
79
+ shortLabel: "bottom",
80
+ },
81
+ {
82
+ action: KB.NAVIGATE_INTO,
83
+ trigger: { type: "special", name: "return" },
84
+ scope: "navigate",
85
+ label: "Navigate into / select",
86
+ shortLabel: "navigate",
87
+ },
88
+ {
89
+ action: KB.EDIT,
90
+ trigger: { type: "key", char: "e" },
91
+ scope: "navigate",
92
+ label: "Edit selected item",
93
+ shortLabel: "edit",
94
+ },
95
+ {
96
+ action: KB.DETAILS,
97
+ trigger: { type: "key", char: "d" },
98
+ scope: "navigate",
99
+ label: "Open details panel",
100
+ shortLabel: "details",
101
+ },
102
+ {
103
+ action: KB.YANK_MODE,
104
+ trigger: { type: "key", char: "y" },
105
+ scope: "navigate",
106
+ label: "Open yank mode",
107
+ shortLabel: "yank",
108
+ },
109
+ {
110
+ action: KB.SEARCH_MODE,
111
+ trigger: { type: "key", char: "/" },
112
+ scope: "navigate",
113
+ label: "Search mode",
114
+ shortLabel: "search",
115
+ },
116
+ {
117
+ action: KB.COMMAND_MODE,
118
+ trigger: { type: "key", char: ":" },
119
+ scope: "navigate",
120
+ label: "Command mode",
121
+ shortLabel: "command",
122
+ },
123
+ {
124
+ action: KB.REFRESH,
125
+ trigger: { type: "key", char: "r" },
126
+ scope: "navigate",
127
+ label: "Refresh",
128
+ shortLabel: "refresh",
129
+ },
130
+ {
131
+ action: KB.REVEAL_TOGGLE,
132
+ trigger: { type: "key", char: "v" },
133
+ scope: "navigate",
134
+ label: "Toggle reveal secrets",
135
+ shortLabel: "reveal",
136
+ priority: 90,
137
+ showIf: (ctx) => ctx.hasHiddenSecrets,
138
+ },
139
+ {
140
+ action: KB.QUIT,
141
+ trigger: { type: "key", char: "q" },
142
+ scope: "navigate",
143
+ label: "Quit",
144
+ shortLabel: "quit",
145
+ },
146
+ {
147
+ action: KB.HELP,
148
+ trigger: { type: "key", char: "?" },
149
+ scope: "navigate",
150
+ label: "Open help (navigate mode only)",
151
+ shortLabel: "help",
152
+ },
153
+ // --- Search (informational — text input handles actual typing) ---
154
+ {
155
+ action: KB.SEARCH_MODE,
156
+ trigger: { type: "key", char: "/" },
157
+ scope: "search",
158
+ label: "Open: press / in navigate mode",
159
+ shortLabel: "open search",
160
+ },
161
+ {
162
+ action: KB.NAVIGATE_INTO,
163
+ trigger: { type: "special", name: "return" },
164
+ scope: "search",
165
+ label: "Apply filter and return to navigate",
166
+ shortLabel: "apply filter",
167
+ },
168
+ {
169
+ action: KB.QUIT,
170
+ trigger: { type: "special", name: "escape" },
171
+ scope: "search",
172
+ label: "Cancel and restore previous filter",
173
+ shortLabel: "cancel search",
174
+ },
175
+ // --- Command (informational) ---
176
+ {
177
+ action: KB.COMMAND_MODE,
178
+ trigger: { type: "key", char: ":" },
179
+ scope: "command",
180
+ label: "Open: press : in navigate mode",
181
+ shortLabel: "open command",
182
+ },
183
+ {
184
+ action: KB.NAVIGATE_INTO,
185
+ trigger: { type: "special", name: "return" },
186
+ scope: "command",
187
+ label: "Run command",
188
+ shortLabel: "run",
189
+ },
190
+ {
191
+ action: KB.QUIT,
192
+ trigger: { type: "special", name: "escape" },
193
+ scope: "command",
194
+ label: "Cancel command mode",
195
+ shortLabel: "cancel",
196
+ },
197
+ // --- Yank (informational — actual options come from adapter.capabilities?.yank?.getYankOptions) ---
198
+ {
199
+ action: KB.YANK_MODE,
200
+ trigger: { type: "key", char: "y" },
201
+ scope: "yank",
202
+ label: "Open: press y in navigate mode",
203
+ shortLabel: "open yank",
204
+ },
205
+ {
206
+ action: KB.YANK_MODE,
207
+ trigger: { type: "key", char: "n" },
208
+ scope: "yank",
209
+ label: "Copy selected name",
210
+ shortLabel: "copy name",
211
+ },
212
+ {
213
+ action: KB.YANK_MODE,
214
+ trigger: { type: "key", char: "a" },
215
+ scope: "yank",
216
+ label: "Copy ARN (when available)",
217
+ shortLabel: "copy arn",
218
+ },
219
+ {
220
+ action: KB.QUIT,
221
+ trigger: { type: "special", name: "escape" },
222
+ scope: "yank",
223
+ label: "Cancel yank mode",
224
+ shortLabel: "cancel",
225
+ },
226
+ // --- Upload (informational) ---
227
+ {
228
+ action: KB.MOVE_DOWN,
229
+ trigger: j_down,
230
+ scope: "upload",
231
+ label: "Scroll diff down",
232
+ shortLabel: "scroll down",
233
+ },
234
+ {
235
+ action: KB.MOVE_UP,
236
+ trigger: k_up,
237
+ scope: "upload",
238
+ label: "Scroll diff up",
239
+ shortLabel: "scroll up",
240
+ },
241
+ {
242
+ action: KB.YANK_MODE,
243
+ trigger: { type: "key", char: "y" },
244
+ scope: "upload",
245
+ label: "Upload edited file",
246
+ shortLabel: "upload",
247
+ },
248
+ {
249
+ action: KB.QUIT,
250
+ trigger: {
251
+ type: "any",
252
+ of: [
253
+ { type: "key", char: "n" },
254
+ { type: "special", name: "escape" },
255
+ ],
256
+ },
257
+ scope: "upload",
258
+ label: "Cancel upload",
259
+ shortLabel: "cancel",
260
+ },
261
+ // --- Details ---
262
+ {
263
+ action: KB.DETAILS,
264
+ trigger: { type: "key", char: "d" },
265
+ scope: "details",
266
+ label: "Open: press d in navigate mode",
267
+ shortLabel: "open details",
268
+ },
269
+ {
270
+ action: KB.MOVE_DOWN,
271
+ trigger: j_down,
272
+ scope: "details",
273
+ label: "Scroll down",
274
+ shortLabel: "scroll down",
275
+ },
276
+ {
277
+ action: KB.MOVE_UP,
278
+ trigger: k_up,
279
+ scope: "details",
280
+ label: "Scroll up",
281
+ shortLabel: "scroll up",
282
+ },
283
+ {
284
+ action: KB.QUIT,
285
+ trigger: { type: "special", name: "escape" },
286
+ scope: "details",
287
+ label: "Close details panel",
288
+ shortLabel: "close",
289
+ },
290
+ // --- Picker ---
291
+ {
292
+ action: KB.PICKER_DOWN,
293
+ trigger: j_down,
294
+ scope: "picker",
295
+ label: "Move down",
296
+ shortLabel: "down",
297
+ },
298
+ { action: KB.PICKER_UP, trigger: k_up, scope: "picker", label: "Move up", shortLabel: "up" },
299
+ {
300
+ action: KB.PICKER_TOP,
301
+ trigger: { type: "chord", keys: ["g", "g"] },
302
+ scope: "picker",
303
+ label: "Jump to top",
304
+ shortLabel: "top",
305
+ },
306
+ {
307
+ action: KB.PICKER_BOTTOM,
308
+ trigger: { type: "key", char: "G" },
309
+ scope: "picker",
310
+ label: "Jump to bottom",
311
+ shortLabel: "bottom",
312
+ },
313
+ {
314
+ action: KB.PICKER_FILTER,
315
+ trigger: { type: "key", char: "/" },
316
+ scope: "picker",
317
+ label: "Filter",
318
+ shortLabel: "filter",
319
+ },
320
+ {
321
+ action: KB.PICKER_CONFIRM,
322
+ trigger: { type: "special", name: "return" },
323
+ scope: "picker",
324
+ label: "Confirm selection",
325
+ shortLabel: "confirm",
326
+ },
327
+ {
328
+ action: KB.PICKER_CLOSE,
329
+ trigger: { type: "special", name: "escape" },
330
+ scope: "picker",
331
+ label: "Close",
332
+ shortLabel: "close",
333
+ },
334
+ // --- Help panel ---
335
+ {
336
+ action: KB.HELP_PREV_TAB,
337
+ trigger: {
338
+ type: "any",
339
+ of: [
340
+ { type: "key", char: "h" },
341
+ { type: "special", name: "leftArrow" },
342
+ ],
343
+ },
344
+ scope: "help",
345
+ label: "Previous tab",
346
+ shortLabel: "prev",
347
+ },
348
+ {
349
+ action: KB.HELP_NEXT_TAB,
350
+ trigger: {
351
+ type: "any",
352
+ of: [
353
+ { type: "key", char: "l" },
354
+ { type: "special", name: "rightArrow" },
355
+ ],
356
+ },
357
+ scope: "help",
358
+ label: "Next tab",
359
+ shortLabel: "next",
360
+ },
361
+ {
362
+ action: KB.HELP_SCROLL_UP,
363
+ trigger: k_up,
364
+ scope: "help",
365
+ label: "Scroll up",
366
+ shortLabel: "scroll up",
367
+ },
368
+ {
369
+ action: KB.HELP_SCROLL_DOWN,
370
+ trigger: j_down,
371
+ scope: "help",
372
+ label: "Scroll down",
373
+ shortLabel: "scroll down",
374
+ },
375
+ {
376
+ action: KB.HELP_CLOSE,
377
+ trigger: {
378
+ type: "any",
379
+ of: [
380
+ { type: "key", char: "?" },
381
+ { type: "special", name: "escape" },
382
+ ],
383
+ },
384
+ scope: "help",
385
+ label: "Close help",
386
+ shortLabel: "close",
387
+ },
388
+ ];
389
+ // ---------------------------------------------------------------------------
390
+ // Help panel derivation
391
+ // ---------------------------------------------------------------------------
392
+ const SCOPE_ORDER = [
393
+ "navigate",
394
+ "search",
395
+ "command",
396
+ "yank",
397
+ "details",
398
+ "upload",
399
+ "picker",
400
+ "help",
401
+ ];
402
+ /**
403
+ * Build HelpPanel tabs from KEYBINDINGS and adapter-specific bindings.
404
+ * Display key is automatically derived from each binding's trigger.
405
+ */
406
+ export function buildHelpTabs(adapterId, adapterBindings) {
407
+ const groups = new Map();
408
+ for (const kb of KEYBINDINGS) {
409
+ if (!groups.has(kb.scope))
410
+ groups.set(kb.scope, []);
411
+ groups.get(kb.scope).push({
412
+ key: triggerToString(kb.trigger),
413
+ description: kb.label,
414
+ });
415
+ }
416
+ // Add adapter-specific bindings
417
+ if (adapterBindings) {
418
+ for (const ab of adapterBindings) {
419
+ const scope = ab.scope || "navigate";
420
+ if (!groups.has(scope))
421
+ groups.set(scope, []);
422
+ groups.get(scope).push({
423
+ key: triggerToString(ab.trigger),
424
+ description: ab.label,
425
+ });
426
+ }
427
+ }
428
+ // De-duplicate entries with identical key+description within the same scope
429
+ // (e.g. search/command/yank open entries share action with navigate entries)
430
+ for (const [scope, items] of groups) {
431
+ const seen = new Set();
432
+ groups.set(scope, items.filter((item) => {
433
+ const key = `${item.key}|${item.description}`;
434
+ if (seen.has(key))
435
+ return false;
436
+ seen.add(key);
437
+ return true;
438
+ }));
439
+ }
440
+ return SCOPE_ORDER.filter((s) => groups.has(s)).map((s) => ({
441
+ title: SCOPE_LABELS[s],
442
+ items: groups.get(s),
443
+ }));
444
+ }
445
+ /** Derive the navigate mode hint string from KEYBINDINGS */
446
+ export function buildNavigateHint() {
447
+ const navigateKeys = KEYBINDINGS.filter((kb) => kb.scope === "navigate");
448
+ return (" " +
449
+ navigateKeys
450
+ .slice(0, 6)
451
+ .map((kb) => `${triggerToString(kb.trigger)} ${kb.shortLabel}`)
452
+ .join(" • "));
453
+ }
454
+ /** Generic bottom-hint builder for a given scope, derived from KEYBINDINGS and adapter bindings. */
455
+ export function buildScopeHint(scope, adapterBindings, maxItems = 8, context = { hasHiddenSecrets: false }) {
456
+ const filtered = KEYBINDINGS.filter((kb) => kb.scope === scope);
457
+ const seen = new Set();
458
+ const compact = filtered.filter((kb) => {
459
+ const key = `${kb.action}|${triggerToString(kb.trigger)}`;
460
+ if (seen.has(key))
461
+ return false;
462
+ seen.add(key);
463
+ return true;
464
+ });
465
+ // Process adapter-specific bindings with filtering and priority support
466
+ const adapterBindingsByKey = new Map();
467
+ if (adapterBindings) {
468
+ for (const ab of adapterBindings) {
469
+ if ((ab.scope || "navigate") === scope) {
470
+ const key = `${ab.actionId}|${triggerToString(ab.trigger)}`;
471
+ if (!seen.has(key)) {
472
+ const passesFilter = !ab.showIf || ab.showIf(context);
473
+ if (passesFilter) {
474
+ seen.add(key);
475
+ const shortLabel = ab.shortLabel || ab.label;
476
+ const item = {
477
+ trigger: ab.trigger,
478
+ shortLabel: `${ab.adapterId}: ${shortLabel}`,
479
+ };
480
+ if (ab.priority !== undefined) {
481
+ item.priority = ab.priority;
482
+ }
483
+ adapterBindingsByKey.set(key, item);
484
+ }
485
+ }
486
+ }
487
+ }
488
+ }
489
+ let ordered;
490
+ if (scope === "navigate") {
491
+ // Filter core bindings
492
+ const filteredCore = compact.filter((kb) => !kb.showIf || kb.showIf(context));
493
+ const allItems = [
494
+ ...filteredCore.map((kb) => ({
495
+ trigger: kb.trigger,
496
+ shortLabel: kb.shortLabel,
497
+ priority: kb.priority ?? 100,
498
+ })),
499
+ ...Array.from(adapterBindingsByKey.values()).map((item) => ({
500
+ trigger: item.trigger,
501
+ shortLabel: item.shortLabel,
502
+ priority: item.priority ?? 100,
503
+ })),
504
+ ];
505
+ // Sort by priority (lower number = higher prominence) and drop the priority field
506
+ ordered = allItems
507
+ .sort((a, b) => a.priority - b.priority)
508
+ .map(({ priority: _priority, ...item }) => item);
509
+ }
510
+ else {
511
+ // For non-navigate scopes, filter but maintain source order (core first, then adapters)
512
+ const filteredCore = compact
513
+ .filter((kb) => !kb.showIf || kb.showIf(context))
514
+ .map((kb) => ({
515
+ trigger: kb.trigger,
516
+ shortLabel: kb.shortLabel,
517
+ }));
518
+ ordered = [
519
+ ...filteredCore,
520
+ ...Array.from(adapterBindingsByKey.values()).map((item) => ({
521
+ trigger: item.trigger,
522
+ shortLabel: item.shortLabel,
523
+ })),
524
+ ];
525
+ }
526
+ const parts = ordered.slice(0, maxItems).map((item) => {
527
+ return `${triggerToString(item.trigger)} · ${item.shortLabel}`;
528
+ });
529
+ return parts.length > 0 ? ` ${parts.join(" • ")}` : "";
530
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Closed action-enum: every user-triggerable action has a named constant here.
3
+ * Reference KB.XXX everywhere — never use raw strings like "move_down".
4
+ */
5
+ export const KB = {
6
+ // Navigation
7
+ MOVE_DOWN: "move_down",
8
+ MOVE_UP: "move_up",
9
+ GO_TOP: "go_top", // chord: g g
10
+ GO_BOTTOM: "go_bottom", // G
11
+ NAVIGATE_INTO: "navigate_into", // Enter
12
+ EDIT: "edit", // e
13
+ DETAILS: "details", // d
14
+ FETCH: "fetch", // f (S3 only)
15
+ YANK_MODE: "yank_mode", // y
16
+ SEARCH_MODE: "search_mode", // /
17
+ COMMAND_MODE: "command_mode", // :
18
+ REFRESH: "refresh", // r
19
+ REVEAL_TOGGLE: "reveal_toggle", // v (secrets/fields only)
20
+ QUIT: "quit", // q
21
+ HELP: "help", // ?
22
+ JUMP_TO_PATH: "jump_to_path", // chord: g p (S3 only)
23
+ // Picker
24
+ PICKER_UP: "picker_up",
25
+ PICKER_DOWN: "picker_down",
26
+ PICKER_FILTER: "picker_filter",
27
+ PICKER_CONFIRM: "picker_confirm",
28
+ PICKER_CLOSE: "picker_close",
29
+ PICKER_TOP: "picker_top", // chord: g g
30
+ PICKER_BOTTOM: "picker_bottom", // G
31
+ // Help panel
32
+ HELP_PREV_TAB: "help_prev_tab",
33
+ HELP_NEXT_TAB: "help_next_tab",
34
+ HELP_SCROLL_UP: "help_scroll_up",
35
+ HELP_SCROLL_DOWN: "help_scroll_down",
36
+ HELP_CLOSE: "help_close",
37
+ };
@@ -0,0 +1,133 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { describe, expect, it } from "vitest";
3
+ import { render } from "ink-testing-library";
4
+ import { AppMainView } from "./AppMainView.js";
5
+ import { textCell } from "../types.js";
6
+ function createPickerManager(active) {
7
+ const noop = () => { };
8
+ const mkEntry = (id) => ({
9
+ id,
10
+ columns: [{ key: id, label: id }],
11
+ contextLabel: id,
12
+ open: false,
13
+ filter: "",
14
+ searchEntry: null,
15
+ pickerMode: "navigate",
16
+ setFilter: noop,
17
+ openPicker: noop,
18
+ closePicker: noop,
19
+ startSearch: noop,
20
+ cancelSearch: noop,
21
+ confirmSearch: noop,
22
+ filteredRows: [],
23
+ selectedRow: null,
24
+ selectedIndex: 0,
25
+ scrollOffset: 0,
26
+ moveUp: noop,
27
+ moveDown: noop,
28
+ reset: noop,
29
+ toTop: noop,
30
+ toBottom: noop,
31
+ });
32
+ return {
33
+ region: mkEntry("region"),
34
+ profile: mkEntry("profile"),
35
+ resource: mkEntry("resource"),
36
+ activePicker: active,
37
+ openPicker: (_id) => { },
38
+ closeActivePicker: noop,
39
+ resetPicker: (_id) => { },
40
+ confirmActivePickerSelection: (_handlers) => { },
41
+ };
42
+ }
43
+ const baseProps = {
44
+ helpPanel: {
45
+ helpOpen: false,
46
+ helpTabIndex: 0,
47
+ helpScrollOffset: 0,
48
+ helpVisibleRows: 10,
49
+ open: () => { },
50
+ openAtTab: () => { },
51
+ close: () => { },
52
+ scrollUp: () => { },
53
+ scrollDown: () => { },
54
+ goToPrevTab: () => { },
55
+ goToNextTab: () => { },
56
+ goToTab: () => true,
57
+ },
58
+ helpTabs: [{ title: "General", items: [{ key: "q", description: "quit" }] }],
59
+ pickers: createPickerManager(null),
60
+ error: null,
61
+ describeState: null,
62
+ isLoading: false,
63
+ filteredRows: [{ id: "r1", cells: { name: textCell("row1") } }],
64
+ columns: [{ key: "name", label: "Name" }],
65
+ selectedIndex: 0,
66
+ scrollOffset: 0,
67
+ filterText: "",
68
+ adapter: {
69
+ id: "s3",
70
+ label: "S3",
71
+ hudColor: { bg: "blue", fg: "white" },
72
+ getColumns: () => [{ key: "name", label: "Name" }],
73
+ getRows: async () => [],
74
+ onSelect: async () => ({ action: "none" }),
75
+ canGoBack: () => false,
76
+ goBack: () => { },
77
+ getPath: () => "s3://",
78
+ getContextLabel: () => "Buckets",
79
+ },
80
+ termCols: 120,
81
+ tableHeight: 20,
82
+ yankHelpOpen: false,
83
+ yankOptions: [],
84
+ yankHelpRow: null,
85
+ panelScrollOffset: 0,
86
+ };
87
+ describe("AppMainView integration", () => {
88
+ it("prioritizes help panel when open", () => {
89
+ const { lastFrame } = render(_jsx(AppMainView, { ...baseProps, helpPanel: { ...baseProps.helpPanel, helpOpen: true } }));
90
+ expect(lastFrame()).toContain("Keyboard Help");
91
+ });
92
+ it("renders picker table when picker is active", () => {
93
+ const activePicker = {
94
+ ...createPickerManager(null).resource,
95
+ open: true,
96
+ contextLabel: "Select AWS Resource",
97
+ filteredRows: [
98
+ { id: "s3", cells: { resource: textCell("s3"), description: textCell("S3") } },
99
+ ],
100
+ columns: [
101
+ { key: "resource", label: "Resource" },
102
+ { key: "description", label: "Description" },
103
+ ],
104
+ };
105
+ const { lastFrame } = render(_jsx(AppMainView, { ...baseProps, pickers: createPickerManager(activePicker) }));
106
+ expect(lastFrame()).toContain("Select AWS Resource");
107
+ });
108
+ it("renders details panel when details are present", () => {
109
+ const { lastFrame } = render(_jsx(AppMainView, { ...baseProps, describeState: {
110
+ row: { id: "obj-1", cells: { name: textCell("object-1") } },
111
+ fields: [{ label: "Name", value: "object-1" }],
112
+ loading: false,
113
+ } }));
114
+ expect(lastFrame()).toContain("object-1");
115
+ });
116
+ it("renders yank header markers when provided", () => {
117
+ const { lastFrame } = render(_jsx(AppMainView, { ...baseProps, headerMarkers: { name: ["n", "k"] } }));
118
+ expect(lastFrame()).toContain("[n,k]");
119
+ });
120
+ it("renders lightweight yank help panel", () => {
121
+ const { lastFrame } = render(_jsx(AppMainView, { ...baseProps, yankHelpOpen: true, yankOptions: [
122
+ {
123
+ trigger: { type: "key", char: "n" },
124
+ label: "copy name",
125
+ feedback: "Copied",
126
+ isRelevant: () => true,
127
+ resolve: async () => "x",
128
+ },
129
+ ], yankHelpRow: { id: "row", cells: { name: textCell("item") } } }));
130
+ expect(lastFrame()).toContain("Yank Options");
131
+ expect(lastFrame()).toContain("copy name");
132
+ });
133
+ });