@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,476 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useMemo, useState } from "react";
3
+ import { Box, Text, useApp } from "ink";
4
+ import { useAtom } from "jotai";
5
+ import clipboardy from "clipboardy";
6
+ import { HUD } from "./components/HUD.js";
7
+ import { ModeBar } from "./components/ModeBar.js";
8
+ import { AdvancedTextInput } from "./components/AdvancedTextInput.js";
9
+ import { FullscreenBox, useScreenSize } from "./utils/withFullscreen.js";
10
+ import { useHelpPanel } from "./hooks/useHelpPanel.js";
11
+ import { useAwsContext } from "./hooks/useAwsContext.js";
12
+ import { useAwsRegions } from "./hooks/useAwsRegions.js";
13
+ import { useAwsProfiles } from "./hooks/useAwsProfiles.js";
14
+ import { useMainInput } from "./hooks/useMainInput.js";
15
+ import { useInputEventProcessor } from "./hooks/useInputEventProcessor.js";
16
+ import { useHierarchyState } from "./hooks/useHierarchyState.js";
17
+ import { useAppController } from "./hooks/useAppController.js";
18
+ import { useCommandRouter } from "./hooks/useCommandRouter.js";
19
+ import { useDetailController } from "./hooks/useDetailController.js";
20
+ import { useActionController } from "./hooks/useActionController.js";
21
+ import { useUiHints } from "./hooks/useUiHints.js";
22
+ import { useAppData } from "./hooks/useAppData.js";
23
+ import { deriveYankHeaderMarkers } from "./hooks/yankHeaderMarkers.js";
24
+ import { AppMainView } from "./features/AppMainView.js";
25
+ import { AVAILABLE_COMMANDS } from "./constants/commands.js";
26
+ import { buildHelpTabs, triggerToString } from "./constants/keybindings.js";
27
+ import { currentlySelectedServiceAtom, selectedRegionAtom, selectedProfileAtom, revealSecretsAtom, } from "./state/atoms.js";
28
+ const INITIAL_AWS_PROFILE = process.env.AWS_PROFILE;
29
+ export function App({ initialService, endpointUrl }) {
30
+ const { exit } = useApp();
31
+ const { columns: termCols, rows: termRows } = useScreenSize();
32
+ const [selectedRegion, setSelectedRegion] = useAtom(selectedRegionAtom);
33
+ const [selectedProfile, setSelectedProfile] = useAtom(selectedProfileAtom);
34
+ const [currentService, setCurrentService] = useAtom(currentlySelectedServiceAtom);
35
+ const [revealSecrets, setRevealSecrets] = useAtom(revealSecretsAtom);
36
+ const { accountName, accountId, awsProfile, currentIdentity, region } = useAwsContext(endpointUrl, selectedRegion, selectedProfile);
37
+ const availableRegions = useAwsRegions(selectedRegion, selectedProfile);
38
+ const availableProfiles = useAwsProfiles();
39
+ const { reset: resetHierarchy, updateCurrentFilter, pushLevel, popLevel } = useHierarchyState();
40
+ const { state, actions } = useAppController();
41
+ useEffect(() => {
42
+ if (selectedProfile === "$default") {
43
+ if (INITIAL_AWS_PROFILE === undefined) {
44
+ delete process.env.AWS_PROFILE;
45
+ }
46
+ else {
47
+ process.env.AWS_PROFILE = INITIAL_AWS_PROFILE;
48
+ }
49
+ return;
50
+ }
51
+ process.env.AWS_PROFILE = selectedProfile;
52
+ }, [selectedProfile]);
53
+ useEffect(() => {
54
+ setCurrentService(initialService);
55
+ }, [initialService, setCurrentService]);
56
+ const HUD_LINES = 3;
57
+ const MODEBAR_LINES = 1;
58
+ const HEADER_LINES = 2;
59
+ const tableHeight = Math.max(1, termRows - HUD_LINES - MODEBAR_LINES - HEADER_LINES - 4);
60
+ const { adapter, columns, isLoading, error, select, edit, goBack, refresh, path, filteredRows, selectedRow, navigation, pickers, } = useAppData({
61
+ currentService,
62
+ endpointUrl,
63
+ selectedRegion,
64
+ tableHeight,
65
+ filterText: state.filterText,
66
+ availableRegions,
67
+ availableProfiles,
68
+ });
69
+ const [didOpenInitialResources, setDidOpenInitialResources] = useState(false);
70
+ useEffect(() => {
71
+ if (didOpenInitialResources)
72
+ return;
73
+ pickers.openPicker("resource");
74
+ setDidOpenInitialResources(true);
75
+ }, [didOpenInitialResources, pickers]);
76
+ const switchAdapter = useCallback((serviceId) => {
77
+ setCurrentService(serviceId);
78
+ actions.setFilterText("");
79
+ actions.setDescribeState(null);
80
+ actions.setSearchEntryFilter(null);
81
+ actions.setMode("navigate");
82
+ actions.setYankMode(false);
83
+ actions.setUploadPending(null);
84
+ actions.setPendingAction(null);
85
+ resetHierarchy();
86
+ navigation.reset();
87
+ }, [actions, navigation, resetHierarchy, setCurrentService]);
88
+ const navigateBack = useCallback(() => {
89
+ if (!adapter.canGoBack())
90
+ return;
91
+ void goBack().then(() => {
92
+ actions.setDescribeState(null);
93
+ actions.setSearchEntryFilter(null);
94
+ const { restoredFilter, restoredIndex } = popLevel();
95
+ actions.setFilterText(restoredFilter);
96
+ navigation.setIndex(restoredIndex);
97
+ });
98
+ }, [actions, adapter, goBack, navigation, popLevel]);
99
+ const navigateIntoSelection = useCallback(() => {
100
+ if (!selectedRow)
101
+ return;
102
+ if (selectedRow.meta?.type === "object")
103
+ return;
104
+ void select(selectedRow).then((result) => {
105
+ if (result?.action === "navigate") {
106
+ actions.setSearchEntryFilter(null);
107
+ pushLevel(navigation.selectedIndex, "");
108
+ actions.setFilterText("");
109
+ actions.setDescribeState(null);
110
+ navigation.reset();
111
+ return;
112
+ }
113
+ if (result.action === "edit" && "needsUpload" in result && result.needsUpload) {
114
+ actions.setUploadPending({
115
+ filePath: result.filePath,
116
+ metadata: result.metadata,
117
+ });
118
+ }
119
+ });
120
+ }, [actions, navigation, pushLevel, select, selectedRow]);
121
+ const editSelection = useCallback(() => {
122
+ if (!selectedRow)
123
+ return;
124
+ void edit(selectedRow).then((result) => {
125
+ if (result.action === "edit" && "needsUpload" in result && result.needsUpload) {
126
+ actions.setUploadPending({
127
+ filePath: result.filePath,
128
+ metadata: result.metadata,
129
+ });
130
+ }
131
+ });
132
+ }, [actions, edit, selectedRow]);
133
+ const { showDetails: showDetailsBase, closeDetails } = useDetailController({
134
+ adapter,
135
+ setDescribeState: actions.setDescribeState,
136
+ });
137
+ const showDetails = useCallback((row) => {
138
+ setPanelScrollOffset(0);
139
+ showDetailsBase(row);
140
+ }, [showDetailsBase]);
141
+ const { handleActionEffect, submitPendingAction } = useActionController({
142
+ adapter,
143
+ refresh,
144
+ setPendingAction: actions.setPendingAction,
145
+ pushFeedback: actions.pushFeedback,
146
+ });
147
+ const runAdapterAction = useCallback((actionId, row) => {
148
+ if (!adapter.capabilities?.actions)
149
+ return;
150
+ void adapter.capabilities.actions
151
+ .executeAction(actionId, { row })
152
+ .then((effect) => {
153
+ handleActionEffect(effect, row);
154
+ })
155
+ .catch((err) => {
156
+ actions.pushFeedback(`Action failed: ${err.message}`, 3000);
157
+ });
158
+ }, [actions, adapter.capabilities?.actions, handleActionEffect]);
159
+ const adapterBindings = useMemo(() => adapter.capabilities?.actions?.getKeybindings() ?? [], [adapter]);
160
+ const helpTabs = useMemo(() => buildHelpTabs(adapter.id, adapterBindings), [adapter.id, adapterBindings]);
161
+ const helpContainerHeight = Math.max(1, termRows - HUD_LINES - MODEBAR_LINES);
162
+ const helpPanel = useHelpPanel(helpTabs, helpContainerHeight);
163
+ const nameOption = {
164
+ trigger: { type: "key", char: "n" },
165
+ label: "copy name",
166
+ feedback: "Copied Name",
167
+ headerKey: "name",
168
+ isRelevant: () => true,
169
+ resolve: async (row) => {
170
+ const nameCell = row.cells.name;
171
+ if (!nameCell)
172
+ return null;
173
+ return typeof nameCell === "string" ? nameCell : nameCell.displayName;
174
+ },
175
+ };
176
+ const yankOptions = useMemo(() => {
177
+ const adapterOptions = selectedRow
178
+ ? (adapter.capabilities?.yank?.getYankOptions(selectedRow) ?? [])
179
+ : [];
180
+ return [nameOption, ...adapterOptions];
181
+ }, [adapter, selectedRow]);
182
+ const yankHint = useMemo(() => [...yankOptions.map((o) => `${triggerToString(o.trigger)} · ${o.label}`), "Esc cancel"].join(" • "), [yankOptions]);
183
+ const yankHeaderMarkers = useMemo(() => deriveYankHeaderMarkers(state.yankMode, yankOptions), [state.yankMode, yankOptions]);
184
+ // Compute context for keybinding hints
185
+ const uiHintsContext = useMemo(() => {
186
+ // Check if any visible row contains secret cells
187
+ const hasSecretData = filteredRows.some((row) => Object.values(row.cells).some((cell) => typeof cell === "object" && cell?.type === "secret"));
188
+ // Only show reveal toggle if there are secrets AND they're currently hidden
189
+ return { hasHiddenSecrets: hasSecretData && !revealSecrets };
190
+ }, [filteredRows, revealSecrets]);
191
+ const { bottomHint } = useUiHints({
192
+ mode: state.mode,
193
+ helpOpen: helpPanel.helpOpen,
194
+ pickers,
195
+ pendingAction: state.pendingAction,
196
+ uploadPending: state.uploadPending,
197
+ describeState: state.describeState,
198
+ yankMode: state.yankMode,
199
+ adapterBindings,
200
+ yankHint,
201
+ context: uiHintsContext,
202
+ });
203
+ const commandRouter = useCommandRouter({
204
+ setSelectedRegion,
205
+ setSelectedProfile,
206
+ switchAdapter,
207
+ openProfilePicker: () => pickers.openPicker("profile"),
208
+ openRegionPicker: () => pickers.openPicker("region"),
209
+ openResourcePicker: () => pickers.openPicker("resource"),
210
+ exit,
211
+ });
212
+ const handleFilterChange = useCallback((value) => {
213
+ if (pickers.activePicker) {
214
+ pickers.activePicker.setFilter(value);
215
+ return;
216
+ }
217
+ actions.setFilterText(value);
218
+ updateCurrentFilter(value);
219
+ }, [actions, pickers, updateCurrentFilter]);
220
+ const handleFilterSubmit = useCallback(() => {
221
+ if (pickers.activePicker) {
222
+ pickers.activePicker.confirmSearch();
223
+ return;
224
+ }
225
+ actions.setSearchEntryFilter(null);
226
+ actions.setMode("navigate");
227
+ }, [actions, pickers]);
228
+ const handleCommandSubmit = useCallback(() => {
229
+ const command = state.commandText.trim();
230
+ actions.setCommandText("");
231
+ actions.setMode("navigate");
232
+ commandRouter(command);
233
+ }, [actions, commandRouter, state.commandText]);
234
+ const [uploadPreview, setUploadPreview] = useState({ old: "", new: "" });
235
+ const [panelScrollOffset, setPanelScrollOffset] = useState(0);
236
+ const handleUploadDecision = useCallback((confirmed) => {
237
+ if (!state.uploadPending)
238
+ return;
239
+ if (!confirmed) {
240
+ actions.pushFeedback("Upload cancelled", 2000);
241
+ actions.setUploadPending(null);
242
+ setUploadPreview({ old: "", new: "" });
243
+ return;
244
+ }
245
+ void (async () => {
246
+ try {
247
+ await adapter.capabilities?.edit?.uploadFile(state.uploadPending.filePath, state.uploadPending.metadata);
248
+ actions.pushFeedback("✓ Uploaded successfully", 2000);
249
+ // Refresh to show updated data
250
+ await refresh();
251
+ }
252
+ catch (err) {
253
+ actions.pushFeedback(`✗ Upload failed: ${err.message}`, 3000);
254
+ }
255
+ finally {
256
+ actions.setUploadPending(null);
257
+ setUploadPreview({ old: "", new: "" });
258
+ }
259
+ })();
260
+ }, [actions, adapter.capabilities?.edit, state.uploadPending, refresh]);
261
+ // Load preview when uploadPending changes
262
+ useEffect(() => {
263
+ if (state.uploadPending) {
264
+ setPanelScrollOffset(0);
265
+ void (async () => {
266
+ try {
267
+ const { readFile } = await import("fs/promises");
268
+ const newContent = await readFile(state.uploadPending.filePath, "utf-8");
269
+ // Try to get old value from adapter (current value from AWS)
270
+ let oldContent = "";
271
+ const meta = state.uploadPending.metadata;
272
+ // For Secrets Manager fields: fetch the current field value
273
+ if (meta.fieldKey && meta.secretArn) {
274
+ try {
275
+ const { runAwsJsonAsync } = await import("./utils/aws.js");
276
+ const regionArgs = selectedRegion ? ["--region", selectedRegion] : [];
277
+ const secretData = await runAwsJsonAsync([
278
+ "secretsmanager",
279
+ "get-secret-value",
280
+ "--secret-id",
281
+ meta.secretArn,
282
+ ...regionArgs,
283
+ ]);
284
+ const secretString = secretData.SecretString || "";
285
+ // $RAW field is the whole secret value, not a JSON field
286
+ if (meta.fieldKey === "$RAW") {
287
+ oldContent = secretString;
288
+ }
289
+ else {
290
+ // Regular JSON field - parse and extract
291
+ try {
292
+ const parsed = JSON.parse(secretString);
293
+ oldContent = parsed[meta.fieldKey] || "";
294
+ }
295
+ catch {
296
+ // Not JSON - oldContent stays empty
297
+ }
298
+ }
299
+ }
300
+ catch {
301
+ // Failed to fetch
302
+ }
303
+ }
304
+ // For whole secrets: fetch current secret value
305
+ else if (meta.secretArn && !meta.fieldKey) {
306
+ try {
307
+ const { runAwsJsonAsync } = await import("./utils/aws.js");
308
+ const regionArgs = selectedRegion ? ["--region", selectedRegion] : [];
309
+ const secretData = await runAwsJsonAsync([
310
+ "secretsmanager",
311
+ "get-secret-value",
312
+ "--secret-id",
313
+ meta.secretArn,
314
+ ...regionArgs,
315
+ ]);
316
+ oldContent = secretData.SecretString || "";
317
+ }
318
+ catch {
319
+ // Failed to fetch
320
+ }
321
+ }
322
+ setUploadPreview({ old: oldContent, new: newContent });
323
+ }
324
+ catch {
325
+ setUploadPreview({ old: "", new: "[Unable to load preview]" });
326
+ }
327
+ })();
328
+ }
329
+ }, [state.uploadPending, selectedRegion]);
330
+ const commandAutocomplete = useCallback(() => {
331
+ const match = AVAILABLE_COMMANDS.find((cmd) => cmd.toLowerCase().startsWith(state.commandText.toLowerCase()));
332
+ if (!match)
333
+ return;
334
+ actions.setCommandText(match);
335
+ actions.bumpCommandCursorToEnd();
336
+ }, [actions, state.commandText]);
337
+ const inputRuntime = useMemo(() => ({
338
+ mode: state.mode,
339
+ filterText: state.filterText,
340
+ commandText: state.commandText,
341
+ searchEntryFilter: state.searchEntryFilter,
342
+ yankMode: state.yankMode,
343
+ yankHelpOpen: state.yankHelpOpen,
344
+ selectedRow,
345
+ helpOpen: helpPanel.helpOpen,
346
+ pickerMode: pickers.activePicker?.pickerMode ?? null,
347
+ describeOpen: Boolean(state.describeState),
348
+ uploadPending: Boolean(state.uploadPending),
349
+ pendingActionType: state.pendingAction?.effect.type ?? null,
350
+ }), [helpPanel.helpOpen, pickers.activePicker?.pickerMode, selectedRow, state]);
351
+ const inputDispatch = useInputEventProcessor({
352
+ runtime: inputRuntime,
353
+ actions: {
354
+ app: { exit },
355
+ help: {
356
+ open: helpPanel.open,
357
+ close: helpPanel.close,
358
+ prevTab: helpPanel.goToPrevTab,
359
+ nextTab: helpPanel.goToNextTab,
360
+ scrollUp: helpPanel.scrollUp,
361
+ scrollDown: helpPanel.scrollDown,
362
+ goToTab: helpPanel.goToTab,
363
+ },
364
+ picker: {
365
+ close: pickers.closeActivePicker,
366
+ cancelSearch: () => pickers.activePicker?.cancelSearch(),
367
+ startSearch: () => pickers.activePicker?.startSearch(),
368
+ moveDown: () => pickers.activePicker?.moveDown(),
369
+ moveUp: () => pickers.activePicker?.moveUp(),
370
+ top: () => pickers.activePicker?.toTop(),
371
+ bottom: () => pickers.activePicker?.toBottom(),
372
+ confirm: () => pickers.confirmActivePickerSelection({
373
+ onSelectResource: switchAdapter,
374
+ onSelectRegion: setSelectedRegion,
375
+ onSelectProfile: setSelectedProfile,
376
+ }),
377
+ },
378
+ mode: {
379
+ cancelSearchOrCommand: () => {
380
+ if (state.mode === "search") {
381
+ if (state.searchEntryFilter !== null && state.filterText !== "") {
382
+ handleFilterChange(state.searchEntryFilter);
383
+ }
384
+ actions.setSearchEntryFilter(null);
385
+ }
386
+ actions.setMode("navigate");
387
+ },
388
+ clearFilterOrNavigateBack: () => {
389
+ if (state.filterText !== "") {
390
+ handleFilterChange("");
391
+ }
392
+ else {
393
+ navigateBack();
394
+ }
395
+ },
396
+ startSearch: () => {
397
+ actions.setSearchEntryFilter(state.filterText);
398
+ actions.setMode("search");
399
+ },
400
+ startCommand: () => {
401
+ actions.setCommandText("");
402
+ actions.setMode("command");
403
+ },
404
+ commandAutocomplete,
405
+ },
406
+ navigation: {
407
+ refresh: () => {
408
+ void refresh();
409
+ },
410
+ revealToggle: () => {
411
+ setRevealSecrets(!revealSecrets);
412
+ },
413
+ showDetails: () => showDetails(selectedRow),
414
+ editSelection,
415
+ top: navigation.toTop,
416
+ bottom: navigation.toBottom,
417
+ enter: navigateIntoSelection,
418
+ },
419
+ scroll: {
420
+ up: () => {
421
+ if (state.uploadPending || state.describeState) {
422
+ setPanelScrollOffset((p) => Math.max(0, p - 1));
423
+ }
424
+ else {
425
+ navigation.moveUp();
426
+ }
427
+ },
428
+ down: () => {
429
+ if (state.uploadPending || state.describeState) {
430
+ setPanelScrollOffset((p) => p + 1);
431
+ }
432
+ else {
433
+ navigation.moveDown();
434
+ }
435
+ },
436
+ },
437
+ yank: {
438
+ enter: () => {
439
+ actions.setYankMode(true);
440
+ actions.setYankHelpOpen(false);
441
+ },
442
+ cancel: () => {
443
+ actions.setYankMode(false);
444
+ actions.setYankHelpOpen(false);
445
+ },
446
+ openHelp: () => {
447
+ actions.setYankHelpOpen(true);
448
+ },
449
+ closeHelp: () => {
450
+ actions.setYankHelpOpen(false);
451
+ },
452
+ },
453
+ details: {
454
+ close: closeDetails,
455
+ },
456
+ pending: {
457
+ cancelPrompt: () => actions.setPendingAction(null),
458
+ submit: (confirmed) => submitPendingAction(state.pendingAction, confirmed),
459
+ },
460
+ upload: {
461
+ decide: handleUploadDecision,
462
+ },
463
+ adapterAction: {
464
+ run: runAdapterAction,
465
+ bindings: adapterBindings,
466
+ },
467
+ },
468
+ yankOptions,
469
+ pushYankFeedback: actions.pushFeedback,
470
+ writeClipboard: clipboardy.write,
471
+ hasCommandAutocomplete: (text) => AVAILABLE_COMMANDS.some((cmd) => cmd.toLowerCase().startsWith(text.toLowerCase())),
472
+ });
473
+ useMainInput(inputDispatch);
474
+ const activePickerFilter = pickers.activePicker?.filter ?? state.filterText;
475
+ return (_jsx(FullscreenBox, { children: _jsxs(Box, { flexDirection: "column", width: termCols, height: termRows, children: [_jsx(HUD, { serviceLabel: adapter.label, hudColor: adapter.hudColor, path: path, accountName: accountName, accountId: accountId, awsProfile: awsProfile, currentIdentity: currentIdentity, region: region, terminalWidth: termCols, loading: isLoading || Boolean(state.describeState?.loading) }), _jsx(Box, { flexDirection: "row", width: "100%", flexGrow: 1, children: _jsx(AppMainView, { helpPanel: helpPanel, helpTabs: helpTabs, pickers: pickers, error: error, describeState: state.describeState, isLoading: isLoading, filteredRows: filteredRows, columns: columns, selectedIndex: navigation.selectedIndex, scrollOffset: navigation.scrollOffset, filterText: state.filterText, adapter: adapter, termCols: termCols, tableHeight: tableHeight, yankHelpOpen: state.yankHelpOpen, yankOptions: yankOptions, yankHelpRow: selectedRow, uploadPending: state.uploadPending, uploadPreview: uploadPreview, panelScrollOffset: panelScrollOffset, ...(yankHeaderMarkers ? { headerMarkers: yankHeaderMarkers } : {}) }) }), !helpPanel.helpOpen && state.yankFeedbackMessage && (_jsx(Box, { paddingX: 1, children: _jsx(Text, { color: "green", children: state.yankFeedbackMessage }) })), state.pendingAction && state.pendingAction.effect.type === "prompt" && (_jsxs(Box, { paddingX: 1, children: [_jsxs(Text, { color: "cyan", children: [state.pendingAction.effect.label, " "] }), _jsx(AdvancedTextInput, { value: state.pendingAction.inputValue, onChange: (value) => actions.setPendingInputValue(value), onSubmit: () => submitPendingAction(state.pendingAction, true), focus: true })] })), state.pendingAction && state.pendingAction.effect.type === "confirm" && (_jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: "yellow", children: [state.pendingAction.effect.message, " (y/n)"] }) })), _jsx(ModeBar, { mode: state.mode, filterText: activePickerFilter, commandText: state.commandText, commandCursorToEndToken: state.commandCursorToEndToken, hintOverride: bottomHint, pickerSearchActive: pickers.activePicker?.pickerMode === "search", onFilterChange: handleFilterChange, onCommandChange: actions.setCommandText, onFilterSubmit: handleFilterSubmit, onCommandSubmit: handleCommandSubmit })] }) }));
476
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Generic factory — the single implementation of getYankOptions.
3
+ * Parses row.meta via schema, filters by isRelevant, wraps with typed resolve.
4
+ */
5
+ export function createYankCapability(options, schema, ctx) {
6
+ const parse = (row) => {
7
+ const result = schema.safeParse(row.meta);
8
+ if (!result.success)
9
+ return null;
10
+ return { id: row.id, cells: row.cells, meta: result.data };
11
+ };
12
+ return {
13
+ getYankOptions(row) {
14
+ const typedRow = parse(row);
15
+ if (!typedRow)
16
+ return [];
17
+ return options
18
+ .filter((def) => def.isRelevant(typedRow))
19
+ .map((def) => {
20
+ const option = {
21
+ trigger: def.trigger,
22
+ label: def.label,
23
+ feedback: def.feedback,
24
+ isRelevant: (r) => {
25
+ const tr = parse(r);
26
+ return tr !== null && def.isRelevant(tr);
27
+ },
28
+ resolve: (r) => {
29
+ const tr = parse(r);
30
+ if (!tr)
31
+ return Promise.resolve(null);
32
+ return def.resolve(tr, ctx);
33
+ },
34
+ };
35
+ if (def.headerKey !== undefined) {
36
+ option.headerKey = def.headerKey;
37
+ }
38
+ return option;
39
+ });
40
+ },
41
+ };
42
+ }
@@ -0,0 +1,29 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { z } from "zod";
3
+ import { createYankCapability } from "./YankCapability.js";
4
+ import { textCell } from "../../types.js";
5
+ describe("createYankCapability", () => {
6
+ it("propagates headerKey to public yank options", async () => {
7
+ const capability = createYankCapability([
8
+ {
9
+ trigger: { type: "key", char: "n" },
10
+ label: "copy name",
11
+ feedback: "Copied Name",
12
+ headerKey: "name",
13
+ isRelevant: () => true,
14
+ resolve: async () => "hello",
15
+ },
16
+ ], z.object({ type: z.literal("bucket") }), {});
17
+ const options = capability.getYankOptions({
18
+ id: "row",
19
+ cells: { name: textCell("test") },
20
+ meta: { type: "bucket" },
21
+ });
22
+ expect(options[0]?.headerKey).toBe("name");
23
+ await expect(options[0]?.resolve({
24
+ id: "row",
25
+ cells: { name: textCell("test") },
26
+ meta: { type: "bucket" },
27
+ })).resolves.toBe("hello");
28
+ });
29
+ });