@a9s/cli 0.0.1 → 1.0.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 (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 +72 -5
  95. package/index.js +0 -1
@@ -0,0 +1,200 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useState } from "react";
3
+ import { Text, useInput } from "ink";
4
+ function clamp(n, min, max) {
5
+ return Math.max(min, Math.min(max, n));
6
+ }
7
+ function isWordChar(char) {
8
+ return /[A-Za-z0-9_]/.test(char);
9
+ }
10
+ export function moveCursorWordLeft(text, cursor) {
11
+ let i = clamp(cursor, 0, text.length);
12
+ while (i > 0 && !isWordChar(text[i - 1] ?? ""))
13
+ i -= 1;
14
+ while (i > 0 && isWordChar(text[i - 1] ?? ""))
15
+ i -= 1;
16
+ return i;
17
+ }
18
+ export function moveCursorWordRight(text, cursor) {
19
+ let i = clamp(cursor, 0, text.length);
20
+ while (i < text.length && !isWordChar(text[i] ?? ""))
21
+ i += 1;
22
+ while (i < text.length && isWordChar(text[i] ?? ""))
23
+ i += 1;
24
+ return i;
25
+ }
26
+ export function decodeAltWordDirection(input, key) {
27
+ if (key.meta && key.leftArrow)
28
+ return "left";
29
+ if (key.meta && key.rightArrow)
30
+ return "right";
31
+ // Fallbacks used by various terminals for Alt/Option+Left/Right.
32
+ if (input === "\u001bb")
33
+ return "left";
34
+ if (input === "\u001bf")
35
+ return "right";
36
+ if (input === "\u001b[1;3D" || input === "\u001b[1;9D")
37
+ return "left";
38
+ if (input === "\u001b[1;3C" || input === "\u001b[1;9C")
39
+ return "right";
40
+ return null;
41
+ }
42
+ function isBackspaceSignal(input, key) {
43
+ // Don't match Alt+Backspace sequences (they start with ESC)
44
+ if (input.startsWith("\u001b"))
45
+ return false;
46
+ // Accept delete key without meta as backspace (some terminals report backspace as delete)
47
+ return (key.backspace ||
48
+ input === "\u0008" ||
49
+ input === "\u007f" ||
50
+ (key.delete && !key.meta && input === ""));
51
+ }
52
+ function isDeleteSignal(input, _key) {
53
+ return input === "\u001b[3~";
54
+ }
55
+ export function isAltBackspaceSignal(input, key) {
56
+ // Ink interprets Alt+Backspace as Alt+Delete
57
+ if (key.meta && key.delete) {
58
+ return true;
59
+ }
60
+ if (key.meta && key.backspace) {
61
+ return true;
62
+ }
63
+ // Fallback for Alt+Backspace sequences in various terminals
64
+ if (input === "\u001b\u007f")
65
+ return true; // ESC+DEL
66
+ if (input === "\u001b\u0008")
67
+ return true; // ESC+BS
68
+ if (input === "\u001b[3;3~")
69
+ return true; // Alt+Del variant
70
+ if (input === "\u001b[127")
71
+ return true; // ESC+127
72
+ if (input === "\u001bDEL")
73
+ return true; // ESC+literal DEL
74
+ return false;
75
+ }
76
+ export function applyAdvancedInputEdit(currentValue, currentCursor, input, key) {
77
+ const cursor = clamp(currentCursor, 0, currentValue.length);
78
+ if (key.return) {
79
+ return { value: currentValue, cursor, submit: true, handled: true };
80
+ }
81
+ const wordDirection = decodeAltWordDirection(input, key);
82
+ if (wordDirection === "left") {
83
+ return {
84
+ value: currentValue,
85
+ cursor: moveCursorWordLeft(currentValue, cursor),
86
+ submit: false,
87
+ handled: true,
88
+ };
89
+ }
90
+ if (wordDirection === "right") {
91
+ return {
92
+ value: currentValue,
93
+ cursor: moveCursorWordRight(currentValue, cursor),
94
+ submit: false,
95
+ handled: true,
96
+ };
97
+ }
98
+ if (key.leftArrow) {
99
+ return { value: currentValue, cursor: Math.max(0, cursor - 1), submit: false, handled: true };
100
+ }
101
+ if (key.rightArrow) {
102
+ return {
103
+ value: currentValue,
104
+ cursor: Math.min(currentValue.length, cursor + 1),
105
+ submit: false,
106
+ handled: true,
107
+ };
108
+ }
109
+ if (key.home) {
110
+ return { value: currentValue, cursor: 0, submit: false, handled: true };
111
+ }
112
+ if (key.end) {
113
+ return { value: currentValue, cursor: currentValue.length, submit: false, handled: true };
114
+ }
115
+ // Check Alt+Backspace first (before regular Backspace) since both use backspace flag
116
+ if (isAltBackspaceSignal(input, key)) {
117
+ if (cursor <= 0)
118
+ return { value: currentValue, cursor, submit: false, handled: true };
119
+ const wordStart = moveCursorWordLeft(currentValue, cursor);
120
+ const next = currentValue.slice(0, wordStart) + currentValue.slice(cursor);
121
+ return { value: next, cursor: wordStart, submit: false, handled: true };
122
+ }
123
+ if (isBackspaceSignal(input, key)) {
124
+ if (cursor <= 0)
125
+ return { value: currentValue, cursor, submit: false, handled: true };
126
+ const next = currentValue.slice(0, cursor - 1) + currentValue.slice(cursor);
127
+ return { value: next, cursor: cursor - 1, submit: false, handled: true };
128
+ }
129
+ if (isDeleteSignal(input, key)) {
130
+ if (cursor >= currentValue.length) {
131
+ return { value: currentValue, cursor, submit: false, handled: true };
132
+ }
133
+ const next = currentValue.slice(0, cursor) + currentValue.slice(cursor + 1);
134
+ return { value: next, cursor, submit: false, handled: true };
135
+ }
136
+ if (key.ctrl || key.meta || key.escape || key.tab) {
137
+ return { value: currentValue, cursor, submit: false, handled: false };
138
+ }
139
+ if (!input) {
140
+ return { value: currentValue, cursor, submit: false, handled: false };
141
+ }
142
+ // Ignore control escape-sequences or other non-printable input.
143
+ // eslint-disable-next-line no-control-regex
144
+ if (input.length > 1 || /[\u0000-\u001f\u007f]/.test(input)) {
145
+ return { value: currentValue, cursor, submit: false, handled: false };
146
+ }
147
+ const next = currentValue.slice(0, cursor) + input + currentValue.slice(cursor);
148
+ return {
149
+ value: next,
150
+ cursor: cursor + input.length,
151
+ submit: false,
152
+ handled: true,
153
+ };
154
+ }
155
+ export function AdvancedTextInput({ value, onChange, onSubmit, placeholder, focus = true, cursorToEndToken, }) {
156
+ const [cursor, setCursor] = useState(value.length);
157
+ useEffect(() => {
158
+ setCursor((prev) => clamp(prev, 0, value.length));
159
+ }, [value]);
160
+ useEffect(() => {
161
+ if (cursorToEndToken === undefined)
162
+ return;
163
+ setCursor(value.length);
164
+ }, [cursorToEndToken, value.length]);
165
+ useInput((input, key) => {
166
+ if (!focus)
167
+ return;
168
+ const result = applyAdvancedInputEdit(value, cursor, input, key);
169
+ if (!result.handled)
170
+ return;
171
+ if (result.value !== value) {
172
+ onChange(result.value);
173
+ }
174
+ if (result.cursor !== cursor) {
175
+ setCursor(result.cursor);
176
+ }
177
+ if (result.submit) {
178
+ onSubmit();
179
+ }
180
+ }, { isActive: focus });
181
+ const rendered = useMemo(() => {
182
+ if (value.length === 0) {
183
+ return { isPlaceholder: true, text: placeholder ?? "" };
184
+ }
185
+ const safeCursor = clamp(cursor, 0, value.length);
186
+ const before = value.slice(0, safeCursor);
187
+ const at = safeCursor < value.length ? value[safeCursor] : " ";
188
+ const after = value.slice(Math.min(value.length, safeCursor + 1));
189
+ return {
190
+ isPlaceholder: false,
191
+ before,
192
+ at,
193
+ after,
194
+ };
195
+ }, [cursor, placeholder, value]);
196
+ if (rendered.isPlaceholder) {
197
+ return _jsx(Text, { color: "gray", children: rendered.text });
198
+ }
199
+ return (_jsxs(Text, { children: [rendered.before, _jsx(Text, { inverse: true, children: rendered.at }), rendered.after] }));
200
+ }
@@ -0,0 +1,190 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { applyAdvancedInputEdit, decodeAltWordDirection, moveCursorWordLeft, moveCursorWordRight, isAltBackspaceSignal, } from "./AdvancedTextInput.js";
3
+ function makeKey(patch = {}) {
4
+ return {
5
+ upArrow: false,
6
+ downArrow: false,
7
+ leftArrow: false,
8
+ rightArrow: false,
9
+ pageDown: false,
10
+ pageUp: false,
11
+ home: false,
12
+ end: false,
13
+ return: false,
14
+ escape: false,
15
+ ctrl: false,
16
+ shift: false,
17
+ tab: false,
18
+ backspace: false,
19
+ delete: false,
20
+ meta: false,
21
+ ...patch,
22
+ };
23
+ }
24
+ describe("AdvancedTextInput helpers", () => {
25
+ it("moves by words left and right across spaces/punctuation", () => {
26
+ const text = "hello, world test";
27
+ expect(moveCursorWordLeft(text, text.length)).toBe(14);
28
+ expect(moveCursorWordLeft(text, 14)).toBe(7);
29
+ expect(moveCursorWordLeft(text, 7)).toBe(0);
30
+ expect(moveCursorWordRight(text, 0)).toBe(5);
31
+ expect(moveCursorWordRight(text, 5)).toBe(12);
32
+ expect(moveCursorWordRight(text, 12)).toBe(18);
33
+ });
34
+ it("decodes alt/meta word directions", () => {
35
+ expect(decodeAltWordDirection("", makeKey({ meta: true, leftArrow: true }))).toBe("left");
36
+ expect(decodeAltWordDirection("", makeKey({ meta: true, rightArrow: true }))).toBe("right");
37
+ expect(decodeAltWordDirection("\u001bb", makeKey())).toBe("left");
38
+ expect(decodeAltWordDirection("\u001bf", makeKey())).toBe("right");
39
+ expect(decodeAltWordDirection("\u001b[1;3D", makeKey())).toBe("left");
40
+ expect(decodeAltWordDirection("\u001b[1;3C", makeKey())).toBe("right");
41
+ expect(decodeAltWordDirection("x", makeKey())).toBeNull();
42
+ });
43
+ it("supports insertion, deletion, and submit", () => {
44
+ const ins = applyAdvancedInputEdit("hello", 5, "!", makeKey());
45
+ expect(ins.value).toBe("hello!");
46
+ expect(ins.cursor).toBe(6);
47
+ const back = applyAdvancedInputEdit("hello", 5, "", makeKey({ backspace: true }));
48
+ expect(back.value).toBe("hell");
49
+ expect(back.cursor).toBe(4);
50
+ // Note: delete key without meta is treated as backspace (deletes before cursor)
51
+ // because some terminals report backspace as delete key
52
+ const del = applyAdvancedInputEdit("hello", 1, "", makeKey({ delete: true }));
53
+ expect(del.value).toBe("ello");
54
+ expect(del.cursor).toBe(0);
55
+ const submit = applyAdvancedInputEdit("hello", 5, "", makeKey({ return: true }));
56
+ expect(submit.submit).toBe(true);
57
+ });
58
+ it("handles backspace with single character", () => {
59
+ // Backspace after single character should delete it
60
+ const singleCharEnd = applyAdvancedInputEdit("a", 1, "", makeKey({ backspace: true }));
61
+ expect(singleCharEnd.value).toBe("");
62
+ expect(singleCharEnd.cursor).toBe(0);
63
+ // Backspace at start should do nothing
64
+ const singleCharStart = applyAdvancedInputEdit("a", 0, "", makeKey({ backspace: true }));
65
+ expect(singleCharStart.value).toBe("a");
66
+ expect(singleCharStart.cursor).toBe(0);
67
+ });
68
+ it("handles backspace when reported as delete key (terminal issue in search mode)", () => {
69
+ // Some terminals report backspace as key.delete with empty input
70
+ // This is critical for search mode where users can't delete characters otherwise
71
+ const deleteAsBackspace = applyAdvancedInputEdit("search", 6, "", makeKey({ delete: true }));
72
+ expect(deleteAsBackspace.value).toBe("searc");
73
+ expect(deleteAsBackspace.cursor).toBe(5);
74
+ // Multiple backspaces in sequence
75
+ const secondBackspace = applyAdvancedInputEdit("searc", 5, "", makeKey({ delete: true }));
76
+ expect(secondBackspace.value).toBe("sear");
77
+ expect(secondBackspace.cursor).toBe(4);
78
+ // Backspace in middle of text (reported as delete)
79
+ const middleBackspace = applyAdvancedInputEdit("hello", 3, "", makeKey({ delete: true }));
80
+ expect(middleBackspace.value).toBe("helo");
81
+ expect(middleBackspace.cursor).toBe(2);
82
+ // Alt+Delete should still delete word, not single char
83
+ const altDelete = applyAdvancedInputEdit("foo bar", 7, "", makeKey({ delete: true, meta: true }));
84
+ expect(altDelete.value).toBe("foo ");
85
+ expect(altDelete.cursor).toBe(4);
86
+ });
87
+ it("treats raw terminal backspace/delete sequences as editing signals", () => {
88
+ const bsAscii = applyAdvancedInputEdit("hello", 5, "\u007f", makeKey());
89
+ expect(bsAscii.value).toBe("hell");
90
+ expect(bsAscii.cursor).toBe(4);
91
+ const bsCtrlH = applyAdvancedInputEdit("hello", 5, "\u0008", makeKey());
92
+ expect(bsCtrlH.value).toBe("hell");
93
+ expect(bsCtrlH.cursor).toBe(4);
94
+ const delSeq = applyAdvancedInputEdit("hello", 1, "\u001b[3~", makeKey());
95
+ expect(delSeq.value).toBe("hllo");
96
+ expect(delSeq.cursor).toBe(1);
97
+ });
98
+ it("does not insert control/escape sequences as text", () => {
99
+ const escSeq = applyAdvancedInputEdit("hello", 5, "\u001b[A", makeKey());
100
+ expect(escSeq.value).toBe("hello");
101
+ expect(escSeq.handled).toBe(false);
102
+ });
103
+ it("supports word jump edit flow via meta and fallback escape sequence", () => {
104
+ const leftMeta = applyAdvancedInputEdit("foo bar baz", 11, "", makeKey({ meta: true, leftArrow: true }));
105
+ expect(leftMeta.cursor).toBe(8);
106
+ const leftSeq = applyAdvancedInputEdit("foo bar baz", 8, "\u001bb", makeKey());
107
+ expect(leftSeq.cursor).toBe(4);
108
+ const rightMeta = applyAdvancedInputEdit("foo bar baz", 4, "", makeKey({ meta: true, rightArrow: true }));
109
+ expect(rightMeta.cursor).toBe(7);
110
+ const rightSeq = applyAdvancedInputEdit("foo bar baz", 7, "\u001bf", makeKey());
111
+ expect(rightSeq.cursor).toBe(11);
112
+ });
113
+ it("detects alt/option+backspace signals", () => {
114
+ // Via key object
115
+ expect(isAltBackspaceSignal("", makeKey({ meta: true, backspace: true }))).toBe(true);
116
+ // Via escape sequences (various terminals)
117
+ expect(isAltBackspaceSignal("\u001b\u007f", makeKey())).toBe(true); // ESC + DEL
118
+ expect(isAltBackspaceSignal("\u001b\u0008", makeKey())).toBe(true); // ESC + Backspace
119
+ expect(isAltBackspaceSignal("\u001b[3;3~", makeKey())).toBe(true); // Alt+Delete variant
120
+ // Non-matches
121
+ expect(isAltBackspaceSignal("", makeKey({ backspace: true }))).toBe(false);
122
+ expect(isAltBackspaceSignal("x", makeKey())).toBe(false);
123
+ });
124
+ it("deletes whole word before cursor with alt+backspace", () => {
125
+ // Delete "baz" when cursor is at position 11 (end of "foo bar baz")
126
+ const deleteEnd = applyAdvancedInputEdit("foo bar baz", 11, "", makeKey({ meta: true, backspace: true }));
127
+ expect(deleteEnd.value).toBe("foo bar ");
128
+ expect(deleteEnd.cursor).toBe(8);
129
+ // Delete "bar" when cursor is at position 7 (the space after "bar")
130
+ // moveCursorWordLeft("foo bar baz", 7) returns 4, so we delete from 4 to 7
131
+ // slice(0, 4) = "foo " + slice(7) = " baz" = "foo baz"
132
+ const deleteMiddle = applyAdvancedInputEdit("foo bar baz", 7, "", makeKey({ meta: true, backspace: true }));
133
+ expect(deleteMiddle.value).toBe("foo baz");
134
+ expect(deleteMiddle.cursor).toBe(4);
135
+ // Delete "foo" when cursor is at position 3 (the space after "foo")
136
+ // moveCursorWordLeft("foo bar baz", 3) returns 0
137
+ const deleteStart = applyAdvancedInputEdit("foo bar baz", 3, "", makeKey({ meta: true, backspace: true }));
138
+ expect(deleteStart.value).toBe(" bar baz");
139
+ expect(deleteStart.cursor).toBe(0);
140
+ });
141
+ it("handles alt+backspace with trailing/leading spaces", () => {
142
+ // Delete "hello" when cursor is at position 5 (after "hello")
143
+ // moveCursorWordLeft("hello world", 5) returns 0
144
+ // Delete from 0 to 5 removes "hello"
145
+ const trailingSpaces = applyAdvancedInputEdit("hello world", 5, "", makeKey({ meta: true, backspace: true }));
146
+ expect(trailingSpaces.value).toBe(" world");
147
+ expect(trailingSpaces.cursor).toBe(0);
148
+ // "hello world" at cursor 8 ('w' position)
149
+ // moveCursorWordLeft returns 0 (goes back through 3 spaces and entire "hello")
150
+ // So we delete from 0 to 8, leaving "world"
151
+ const deleteSpaces = applyAdvancedInputEdit("hello world", 8, "", makeKey({ meta: true, backspace: true }));
152
+ expect(deleteSpaces.value).toBe("world");
153
+ expect(deleteSpaces.cursor).toBe(0);
154
+ // Delete "world" when cursor is at position 13 (end of string)
155
+ // moveCursorWordLeft("hello world", 13) returns 8 (skips back through "world" and 3 spaces to after "hello")
156
+ const deleteWordAfterSpaces = applyAdvancedInputEdit("hello world", 13, "", makeKey({ meta: true, backspace: true }));
157
+ expect(deleteWordAfterSpaces.value).toBe("hello ");
158
+ expect(deleteWordAfterSpaces.cursor).toBe(8);
159
+ });
160
+ it("handles alt+backspace at start of input", () => {
161
+ // At position 0, nothing should be deleted
162
+ const atStart = applyAdvancedInputEdit("hello world", 0, "", makeKey({ meta: true, backspace: true }));
163
+ expect(atStart.value).toBe("hello world");
164
+ expect(atStart.cursor).toBe(0);
165
+ });
166
+ it("supports alt+backspace via escape sequences", () => {
167
+ // Using \u001b\u007f (Alt+Backspace in some terminals)
168
+ const escSeq1 = applyAdvancedInputEdit("foo bar baz", 11, "\u001b\u007f", makeKey());
169
+ expect(escSeq1.value).toBe("foo bar ");
170
+ expect(escSeq1.cursor).toBe(8);
171
+ // Using \u001b\u0008 (Alt+Backspace in other terminals)
172
+ const escSeq2 = applyAdvancedInputEdit("foo bar baz", 11, "\u001b\u0008", makeKey());
173
+ expect(escSeq2.value).toBe("foo bar ");
174
+ expect(escSeq2.cursor).toBe(8);
175
+ });
176
+ it("alt+backspace deletes full word even with numbers/underscores", () => {
177
+ // "foo test_var123 bar", cursor at 14 (the '3' in '123')
178
+ // moveCursorWordLeft at 14 returns 4 (start of "test_var123")
179
+ // Delete from 4 to 14: slice(0,4)="foo " + slice(14)="3 bar" = "foo 3 bar"
180
+ const wordChars = applyAdvancedInputEdit("foo test_var123 bar", 14, "", makeKey({ meta: true, backspace: true }));
181
+ expect(wordChars.value).toBe("foo 3 bar");
182
+ expect(wordChars.cursor).toBe(4);
183
+ // "hello_world", cursor at 8 (the 'r')
184
+ // moveCursorWordLeft at 8 returns 0 (entire "hello_wor" is one word)
185
+ // Delete from 0 to 8, leaving "rld"
186
+ const midWord = applyAdvancedInputEdit("hello_world", 8, "", makeKey({ meta: true, backspace: true }));
187
+ expect(midWord.value).toBe("rld");
188
+ expect(midWord.cursor).toBe(0);
189
+ });
190
+ });
@@ -0,0 +1,29 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React, { useEffect, useImperativeHandle, useMemo, useState } from "react";
3
+ import { Box, Text } from "ink";
4
+ import { AdvancedTextInput } from "./AdvancedTextInput.js";
5
+ export const AutocompleteInput = React.forwardRef(({ value, onChange, onSubmit, placeholder, suggestions = [], focus = true, cursorToEndToken }, ref) => {
6
+ const [inputKey, setInputKey] = useState(0);
7
+ const matchingSuggestions = useMemo(() => {
8
+ if (!value || suggestions.length === 0)
9
+ return [];
10
+ return suggestions.filter((s) => s.toLowerCase().startsWith(value.toLowerCase()));
11
+ }, [value, suggestions]);
12
+ const firstMatch = matchingSuggestions[0];
13
+ const suggestion = firstMatch ? firstMatch.slice(value.length) : "";
14
+ useImperativeHandle(ref, () => ({
15
+ autocomplete: () => {
16
+ if (firstMatch && firstMatch !== value) {
17
+ onChange(firstMatch);
18
+ // Remount input to move cursor to end
19
+ setInputKey((k) => k + 1);
20
+ }
21
+ },
22
+ }));
23
+ useEffect(() => {
24
+ if (cursorToEndToken === undefined)
25
+ return;
26
+ setInputKey((k) => k + 1);
27
+ }, [cursorToEndToken]);
28
+ return (_jsxs(Box, { children: [_jsx(AdvancedTextInput, { value: value, onChange: onChange, onSubmit: onSubmit, placeholder: placeholder, focus: focus, ...(cursorToEndToken !== undefined ? { cursorToEndToken } : {}) }, `autocomplete-input-${inputKey}`), suggestion && (_jsx(Text, { color: "gray", dimColor: true, children: suggestion }))] }));
29
+ });
@@ -0,0 +1,12 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Box, Text } from "ink";
3
+ export function DetailPanel({ title, fields, isLoading, scrollOffset, visibleLines, }) {
4
+ const labelWidth = Math.max(...fields.map((f) => f.label.length), 12);
5
+ // Clamp scrollOffset to valid range
6
+ const clampedOffset = Math.max(0, Math.min(scrollOffset, Math.max(0, fields.length - visibleLines)));
7
+ // Show visible fields only
8
+ const visibleFields = fields.slice(clampedOffset, clampedOffset + visibleLines);
9
+ const hasMoreAbove = clampedOffset > 0;
10
+ const hasMoreBelow = clampedOffset + visibleLines < fields.length;
11
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [_jsx(Text, { bold: true, color: "blue", children: title }), _jsx(Text, { color: "gray", children: "─".repeat(40) }), isLoading ? (_jsx(Text, { color: "gray", children: "Loading..." })) : (_jsxs(_Fragment, { children: [hasMoreAbove && (_jsxs(Text, { color: "gray", dimColor: true, children: ["\u2191 ", clampedOffset, " more above"] })), visibleFields.map((f) => (_jsxs(Box, { children: [_jsx(Text, { color: "gray", children: f.label.padEnd(labelWidth + 2) }), _jsx(Text, { children: f.value })] }, f.label))), hasMoreBelow && (_jsxs(Text, { color: "gray", dimColor: true, children: ["\u2193 ", fields.length - clampedOffset - visibleLines, " more below"] }))] })), _jsx(Text, { color: "gray", children: "─".repeat(40) }), _jsx(Text, { color: "gray", children: "j/k scroll \u2022 Esc close" })] }));
12
+ }
@@ -0,0 +1,17 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from "ink";
3
+ export function DiffViewer({ oldValue, newValue, scrollOffset, visibleLines }) {
4
+ const oldLines = oldValue.split("\n");
5
+ const newLines = newValue.split("\n");
6
+ const maxLines = Math.max(oldLines.length, newLines.length);
7
+ // Clamp scrollOffset to valid range
8
+ const clampedOffset = Math.max(0, Math.min(scrollOffset, Math.max(0, maxLines - visibleLines)));
9
+ // Show visible lines only
10
+ const oldDisplay = oldLines.slice(clampedOffset, clampedOffset + visibleLines).join("\n");
11
+ const newDisplay = newLines.slice(clampedOffset, clampedOffset + visibleLines).join("\n");
12
+ const hasMoreAbove = clampedOffset > 0;
13
+ const hasMoreBelow = clampedOffset + visibleLines < maxLines;
14
+ // Calculate column width
15
+ const colWidth = 35;
16
+ return (_jsxs(Box, { flexDirection: "column", gap: 0, children: [_jsxs(Box, { gap: 2, children: [_jsx(Box, { width: colWidth, children: _jsx(Text, { color: "red", bold: true, children: "Original" }) }), _jsx(Box, { children: _jsx(Text, { color: "green", bold: true, children: "Updated" }) })] }), _jsxs(Box, { gap: 2, children: [_jsx(Box, { width: colWidth, children: _jsx(Text, { color: "gray", children: "-".repeat(30) }) }), _jsx(Text, { color: "gray", children: "-".repeat(30) })] }), _jsxs(Box, { gap: 2, flexDirection: "column", children: [hasMoreAbove && (_jsxs(Box, { gap: 2, children: [_jsx(Box, { width: colWidth, children: _jsxs(Text, { color: "gray", dimColor: true, children: ["\u2191 ", clampedOffset, " lines above"] }) }), _jsxs(Text, { color: "gray", dimColor: true, children: ["\u2191 ", clampedOffset, " lines above"] })] })), _jsxs(Box, { gap: 2, children: [_jsx(Box, { width: colWidth, children: _jsx(Text, { children: oldDisplay }) }), _jsx(Box, { children: _jsx(Text, { children: newDisplay }) })] }), hasMoreBelow && (_jsxs(Box, { gap: 2, children: [_jsx(Box, { width: colWidth, children: _jsxs(Text, { color: "gray", dimColor: true, children: ["\u2193 ", maxLines - clampedOffset - visibleLines, " more lines"] }) }), _jsxs(Text, { color: "gray", dimColor: true, children: ["\u2193 ", maxLines - clampedOffset - visibleLines, " more lines"] })] }))] })] }));
17
+ }
@@ -0,0 +1,5 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from "ink";
3
+ export function ErrorStatePanel({ title, message, hint }) {
4
+ return (_jsx(Box, { width: "100%", borderStyle: "round", borderColor: "red", children: _jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: "red", children: title }), _jsx(Text, { children: message }), hint ? _jsx(Text, { color: "gray", children: hint }) : null] }) }));
5
+ }
@@ -0,0 +1,31 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React from "react";
3
+ import { Box, Text } from "ink";
4
+ export function HUD({ serviceLabel, hudColor, path, accountName, accountId, awsProfile, currentIdentity, region, terminalWidth, loading = false, }) {
5
+ const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
6
+ const [spinnerIndex, setSpinnerIndex] = React.useState(0);
7
+ React.useEffect(() => {
8
+ if (!loading) {
9
+ setSpinnerIndex(0);
10
+ return;
11
+ }
12
+ const timer = setInterval(() => {
13
+ setSpinnerIndex((prev) => (prev + 1) % SPINNER_FRAMES.length);
14
+ }, 120);
15
+ return () => clearInterval(timer);
16
+ }, [loading]);
17
+ const truncate = (value, max) => value.length > max ? `${value.slice(0, Math.max(1, max - 1))}…` : value;
18
+ const nameMaxLen = Math.max(8, terminalWidth - 44);
19
+ const compactName = accountName.length > nameMaxLen ? `${accountName.slice(0, nameMaxLen - 1)}…` : accountName;
20
+ const idPart = `(${accountId})`;
21
+ const profilePart = `[${awsProfile}]`;
22
+ const leftTopRaw = `${compactName}${idPart}·${region}·${profilePart}`;
23
+ const spinnerWidth = loading ? 1 : 0;
24
+ const topPadLen = Math.max(0, terminalWidth - leftTopRaw.length - spinnerWidth);
25
+ const identityLine = truncate(currentIdentity || "-", Math.max(1, terminalWidth));
26
+ const identityPadLen = Math.max(0, terminalWidth - identityLine.length);
27
+ const label = ` ${serviceLabel.toUpperCase()} `;
28
+ const pathDisplay = ` ${path} `;
29
+ const padLen = Math.max(0, terminalWidth - label.length - pathDisplay.length);
30
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: "blue", bold: true, children: compactName }), _jsx(Text, { color: "yellow", bold: true, children: idPart }), _jsx(Text, { color: "gray", bold: true, children: "\u00B7" }), _jsx(Text, { color: "green", bold: true, children: region }), _jsx(Text, { color: "gray", bold: true, children: "\u00B7" }), _jsx(Text, { color: "magenta", bold: true, children: profilePart }), _jsx(Text, { children: " ".repeat(topPadLen) }), loading ? (_jsx(Text, { color: "cyan", bold: true, children: SPINNER_FRAMES[spinnerIndex] })) : null] }), _jsxs(Text, { color: "cyan", wrap: "truncate-end", children: [identityLine, " ".repeat(identityPadLen)] }), _jsxs(Box, { children: [_jsx(Text, { backgroundColor: hudColor.bg, color: hudColor.fg, bold: true, children: label }), _jsxs(Text, { backgroundColor: "gray", color: "white", children: [pathDisplay, " ".repeat(padLen)] })] })] }));
31
+ }
@@ -0,0 +1,33 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from "ink";
3
+ function truncate(text, maxLen) {
4
+ if (text.length <= maxLen)
5
+ return text;
6
+ if (maxLen <= 1)
7
+ return "…";
8
+ return `${text.slice(0, maxLen - 1)}…`;
9
+ }
10
+ export function HelpPanel({ title, scopeLabel, tabs, activeTab, terminalWidth, maxRows, scrollOffset, }) {
11
+ const currentTab = tabs[activeTab] ?? tabs[0];
12
+ const keyColWidth = 12;
13
+ const descColWidth = Math.max(16, terminalWidth - keyColWidth - 8);
14
+ const maxTabRowWidth = Math.max(16, terminalWidth - 2);
15
+ const tabRow = [];
16
+ let rowWidth = 0;
17
+ for (let idx = 0; idx < tabs.length; idx += 1) {
18
+ const shortTitle = truncate(tabs[idx]?.title ?? "Tab", 10);
19
+ const label = ` ${idx + 1}:${shortTitle} `;
20
+ if (rowWidth + label.length > maxTabRowWidth)
21
+ break;
22
+ tabRow.push({ idx, label });
23
+ rowWidth += label.length;
24
+ }
25
+ const listRowsBudget = Math.max(1, maxRows);
26
+ const visibleItems = (currentTab?.items ?? []).slice(scrollOffset, scrollOffset + listRowsBudget);
27
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 1, flexGrow: 1, children: [_jsx(Text, { bold: true, color: "blue", children: title }), _jsx(Text, { color: "gray", children: scopeLabel }), _jsx(Box, { children: tabRow.map((chip) => {
28
+ const isActive = chip.idx === activeTab;
29
+ return (_jsx(Text, { ...(isActive
30
+ ? { backgroundColor: "blue", color: "white" }
31
+ : { color: "cyan" }), bold: isActive, children: chip.label }, `chip-${chip.idx}`));
32
+ }) }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: visibleItems.map((item, idx) => (_jsxs(Box, { children: [_jsx(Text, { color: "yellow", bold: true, children: truncate(item.key, keyColWidth).padEnd(keyColWidth) }), _jsx(Text, { children: truncate(item.description, descColWidth) })] }, `${item.key}-${scrollOffset + idx}`))) })] }));
33
+ }
@@ -0,0 +1,43 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React, { useRef } from "react";
3
+ import { Box, Text } from "ink";
4
+ import { AutocompleteInput } from "./AutocompleteInput.js";
5
+ import { AVAILABLE_COMMANDS } from "../constants/commands.js";
6
+ const MODE_ICONS = {
7
+ navigate: "◉",
8
+ search: "/",
9
+ command: ":",
10
+ };
11
+ const MODE_COLORS = {
12
+ navigate: "blue",
13
+ search: "blue",
14
+ command: "blue",
15
+ };
16
+ export const ModeBar = React.forwardRef(({ mode, filterText, commandText, commandCursorToEndToken, hintOverride, pickerSearchActive, onFilterChange, onCommandChange, onFilterSubmit, onCommandSubmit, }, ref) => {
17
+ const commandInputRef = useRef(null);
18
+ const filterInputRef = useRef(null);
19
+ const renderHint = (hint) => {
20
+ const entries = hint
21
+ .trim()
22
+ .split("•")
23
+ .map((x) => x.trim())
24
+ .filter(Boolean);
25
+ return (_jsx(Text, { color: "gray", wrap: "truncate-end", children: entries.map((entry, idx) => {
26
+ const [rawKey, rawDesc] = entry.split("·").map((x) => x.trim());
27
+ const keyPart = rawKey ?? entry;
28
+ const descPart = rawDesc ?? "";
29
+ return (_jsxs(React.Fragment, { children: [_jsx(Text, { color: "yellow", children: keyPart }), descPart ? _jsxs(Text, { color: "gray", children: [" ", descPart] }) : null, idx < entries.length - 1 ? _jsx(Text, { color: "gray", children: " \u2022 " }) : null] }, `hint-${idx}`));
30
+ }) }));
31
+ };
32
+ React.useImperativeHandle(ref, () => ({
33
+ commandInput: commandInputRef.current ?? {},
34
+ filterInput: filterInputRef.current ?? {},
35
+ }));
36
+ const isPickerSearch = pickerSearchActive === true;
37
+ const icon = isPickerSearch ? "/" : MODE_ICONS[mode];
38
+ const showNavigateHint = mode === "navigate" && !isPickerSearch;
39
+ const showFilterInput = mode === "search" || isPickerSearch;
40
+ return (_jsx(Box, { flexDirection: "column", width: "100%", children: _jsxs(Box, { paddingX: 1, children: [_jsx(Text, { color: MODE_COLORS[mode], bold: true, children: icon }), _jsx(Text, { children: " " }), showNavigateHint && renderHint(hintOverride ?? ""), showFilterInput && (_jsx(AutocompleteInput, { ref: filterInputRef, value: filterText, onChange: onFilterChange, onSubmit: onFilterSubmit, placeholder: "Type to filter", focus: showFilterInput })), mode === "command" && (_jsx(AutocompleteInput, { ref: commandInputRef, value: commandText, onChange: onCommandChange, onSubmit: onCommandSubmit, placeholder: "Type a command", suggestions: [...AVAILABLE_COMMANDS], focus: mode === "command", ...(commandCursorToEndToken !== undefined
41
+ ? { cursorToEndToken: commandCursorToEndToken }
42
+ : {}) }))] }) }));
43
+ });