@compilr-dev/cli 0.4.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 (152) hide show
  1. package/README.md +110 -0
  2. package/dist/agent.d.ts +62 -0
  3. package/dist/agent.js +317 -0
  4. package/dist/agents/registry.d.ts +66 -0
  5. package/dist/agents/registry.js +238 -0
  6. package/dist/agents/types.d.ts +40 -0
  7. package/dist/agents/types.js +94 -0
  8. package/dist/commands/custom-registry.d.ts +69 -0
  9. package/dist/commands/custom-registry.js +246 -0
  10. package/dist/commands/index.d.ts +7 -0
  11. package/dist/commands/index.js +7 -0
  12. package/dist/commands/types.d.ts +31 -0
  13. package/dist/commands/types.js +26 -0
  14. package/dist/commands.d.ts +63 -0
  15. package/dist/commands.js +324 -0
  16. package/dist/db/index.d.ts +42 -0
  17. package/dist/db/index.js +146 -0
  18. package/dist/db/repositories/document-repository.d.ts +63 -0
  19. package/dist/db/repositories/document-repository.js +184 -0
  20. package/dist/db/repositories/index.d.ts +9 -0
  21. package/dist/db/repositories/index.js +6 -0
  22. package/dist/db/repositories/project-repository.d.ts +132 -0
  23. package/dist/db/repositories/project-repository.js +337 -0
  24. package/dist/db/repositories/work-item-repository.d.ts +115 -0
  25. package/dist/db/repositories/work-item-repository.js +389 -0
  26. package/dist/db/schema.d.ts +83 -0
  27. package/dist/db/schema.js +143 -0
  28. package/dist/debug.d.ts +8 -0
  29. package/dist/debug.js +48 -0
  30. package/dist/index.d.ts +2 -0
  31. package/dist/index.js +348 -0
  32. package/dist/index.old.d.ts +7 -0
  33. package/dist/index.old.js +1014 -0
  34. package/dist/repl.d.ts +121 -0
  35. package/dist/repl.js +1878 -0
  36. package/dist/settings/index.d.ts +80 -0
  37. package/dist/settings/index.js +195 -0
  38. package/dist/shared-handlers.d.ts +63 -0
  39. package/dist/shared-handlers.js +57 -0
  40. package/dist/slash-autocomplete.d.ts +41 -0
  41. package/dist/slash-autocomplete.js +638 -0
  42. package/dist/state.d.ts +75 -0
  43. package/dist/state.js +130 -0
  44. package/dist/tabbed-menu.d.ts +11 -0
  45. package/dist/tabbed-menu.js +328 -0
  46. package/dist/templates/backlog-md.d.ts +7 -0
  47. package/dist/templates/backlog-md.js +94 -0
  48. package/dist/templates/claude-md.d.ts +7 -0
  49. package/dist/templates/claude-md.js +189 -0
  50. package/dist/templates/coding-standards.d.ts +7 -0
  51. package/dist/templates/coding-standards.js +299 -0
  52. package/dist/templates/compilr-md.d.ts +7 -0
  53. package/dist/templates/compilr-md.js +189 -0
  54. package/dist/templates/config-json.d.ts +38 -0
  55. package/dist/templates/config-json.js +39 -0
  56. package/dist/templates/gitignore.d.ts +7 -0
  57. package/dist/templates/gitignore.js +85 -0
  58. package/dist/templates/index.d.ts +19 -0
  59. package/dist/templates/index.js +302 -0
  60. package/dist/templates/package-json.d.ts +7 -0
  61. package/dist/templates/package-json.js +111 -0
  62. package/dist/templates/readme-md.d.ts +7 -0
  63. package/dist/templates/readme-md.js +161 -0
  64. package/dist/templates/tsconfig.d.ts +7 -0
  65. package/dist/templates/tsconfig.js +61 -0
  66. package/dist/templates/types.d.ts +33 -0
  67. package/dist/templates/types.js +24 -0
  68. package/dist/test-autocomplete.d.ts +7 -0
  69. package/dist/test-autocomplete.js +85 -0
  70. package/dist/test-tabbed-menu.d.ts +7 -0
  71. package/dist/test-tabbed-menu.js +25 -0
  72. package/dist/themes/colors.d.ts +49 -0
  73. package/dist/themes/colors.js +135 -0
  74. package/dist/themes/index.d.ts +23 -0
  75. package/dist/themes/index.js +24 -0
  76. package/dist/themes/registry.d.ts +60 -0
  77. package/dist/themes/registry.js +195 -0
  78. package/dist/themes/types.d.ts +82 -0
  79. package/dist/themes/types.js +7 -0
  80. package/dist/tool-selector.d.ts +71 -0
  81. package/dist/tool-selector.js +184 -0
  82. package/dist/tools/ask-user-simple.d.ts +19 -0
  83. package/dist/tools/ask-user-simple.js +86 -0
  84. package/dist/tools/ask-user.d.ts +32 -0
  85. package/dist/tools/ask-user.js +113 -0
  86. package/dist/tools/backlog.d.ts +53 -0
  87. package/dist/tools/backlog.js +709 -0
  88. package/dist/tools.d.ts +15 -0
  89. package/dist/tools.js +121 -0
  90. package/dist/ui/agents-overlay.d.ts +12 -0
  91. package/dist/ui/agents-overlay.js +501 -0
  92. package/dist/ui/arch-type-overlay.d.ts +20 -0
  93. package/dist/ui/arch-type-overlay.js +229 -0
  94. package/dist/ui/ask-user-overlay.d.ts +26 -0
  95. package/dist/ui/ask-user-overlay.js +647 -0
  96. package/dist/ui/ask-user-simple-overlay.d.ts +25 -0
  97. package/dist/ui/ask-user-simple-overlay.js +242 -0
  98. package/dist/ui/backlog-overlay.d.ts +17 -0
  99. package/dist/ui/backlog-overlay.js +786 -0
  100. package/dist/ui/commands-overlay.d.ts +11 -0
  101. package/dist/ui/commands-overlay.js +410 -0
  102. package/dist/ui/config-overlay.d.ts +34 -0
  103. package/dist/ui/config-overlay.js +977 -0
  104. package/dist/ui/conversation.d.ts +82 -0
  105. package/dist/ui/conversation.js +508 -0
  106. package/dist/ui/diff.d.ts +38 -0
  107. package/dist/ui/diff.js +182 -0
  108. package/dist/ui/ephemeral.d.ts +111 -0
  109. package/dist/ui/ephemeral.js +413 -0
  110. package/dist/ui/file-autocomplete.d.ts +45 -0
  111. package/dist/ui/file-autocomplete.js +237 -0
  112. package/dist/ui/footer.d.ts +153 -0
  113. package/dist/ui/footer.js +422 -0
  114. package/dist/ui/index.d.ts +12 -0
  115. package/dist/ui/index.js +15 -0
  116. package/dist/ui/init-overlay.d.ts +24 -0
  117. package/dist/ui/init-overlay.js +525 -0
  118. package/dist/ui/input-prompt-v2.d.ts +179 -0
  119. package/dist/ui/input-prompt-v2.js +991 -0
  120. package/dist/ui/input-prompt.d.ts +97 -0
  121. package/dist/ui/input-prompt.js +800 -0
  122. package/dist/ui/iteration-limit-overlay.d.ts +21 -0
  123. package/dist/ui/iteration-limit-overlay.js +150 -0
  124. package/dist/ui/keys-overlay.d.ts +14 -0
  125. package/dist/ui/keys-overlay.js +181 -0
  126. package/dist/ui/model-warning-overlay.d.ts +30 -0
  127. package/dist/ui/model-warning-overlay.js +171 -0
  128. package/dist/ui/overlay-controller.d.ts +25 -0
  129. package/dist/ui/overlay-controller.js +35 -0
  130. package/dist/ui/overlays.d.ts +47 -0
  131. package/dist/ui/overlays.js +627 -0
  132. package/dist/ui/permission-overlay.d.ts +16 -0
  133. package/dist/ui/permission-overlay.js +494 -0
  134. package/dist/ui/terminal.d.ts +117 -0
  135. package/dist/ui/terminal.js +237 -0
  136. package/dist/ui/todo-zone.d.ts +112 -0
  137. package/dist/ui/todo-zone.js +353 -0
  138. package/dist/ui/tools-overlay.d.ts +26 -0
  139. package/dist/ui/tools-overlay.js +278 -0
  140. package/dist/ui/tutorial-overlay.d.ts +10 -0
  141. package/dist/ui/tutorial-overlay.js +936 -0
  142. package/dist/ui/types.d.ts +103 -0
  143. package/dist/ui/types.js +33 -0
  144. package/dist/utils/credentials.d.ts +55 -0
  145. package/dist/utils/credentials.js +268 -0
  146. package/dist/utils/model-tiers.d.ts +37 -0
  147. package/dist/utils/model-tiers.js +118 -0
  148. package/dist/utils/project-memory.d.ts +47 -0
  149. package/dist/utils/project-memory.js +117 -0
  150. package/dist/utils/project-status.d.ts +56 -0
  151. package/dist/utils/project-status.js +237 -0
  152. package/package.json +66 -0
@@ -0,0 +1,638 @@
1
+ /**
2
+ * Slash Command Autocomplete
3
+ *
4
+ * Claude Code-style autocomplete for slash commands.
5
+ * Features:
6
+ * - Horizontal separator line above input
7
+ * - Autocomplete dropdown for slash commands
8
+ * - Multiline input with \ continuation
9
+ * - Proper wrapping for long input
10
+ */
11
+ import pc from 'picocolors';
12
+ // ANSI escape codes for terminal control
13
+ const ANSI = {
14
+ HIDE_CURSOR: '\x1B[?25l',
15
+ SHOW_CURSOR: '\x1B[?25h',
16
+ SAVE_CURSOR: '\x1B[s',
17
+ RESTORE_CURSOR: '\x1B[u',
18
+ CLEAR_LINE: '\x1B[2K',
19
+ CLEAR_TO_END_OF_LINE: '\x1B[K',
20
+ CLEAR_TO_END_OF_SCREEN: '\x1B[J',
21
+ MOVE_UP: (n) => `\x1B[${String(n)}A`,
22
+ MOVE_DOWN: (n) => `\x1B[${String(n)}B`,
23
+ MOVE_TO_COLUMN: (n) => `\x1B[${String(n)}G`,
24
+ };
25
+ /**
26
+ * Get terminal width
27
+ */
28
+ function getTerminalWidth() {
29
+ return process.stdout.columns || 80;
30
+ }
31
+ /**
32
+ * Calculate how many physical terminal lines a string occupies
33
+ * (accounting for wrapping at terminal width)
34
+ */
35
+ function calcPhysicalLines(text, startCol, termWidth) {
36
+ if (text.length === 0) {
37
+ return 1; // Empty line still takes 1 physical line
38
+ }
39
+ const totalLen = startCol + text.length;
40
+ return Math.ceil(totalLen / termWidth) || 1;
41
+ }
42
+ /**
43
+ * Available slash commands
44
+ */
45
+ export const SLASH_COMMANDS = [
46
+ { name: '/help', description: 'Show available commands', aliases: ['/?'] },
47
+ { name: '/exit', description: 'Quit the demo', aliases: ['/quit', '/q'] },
48
+ { name: '/clear', description: 'Clear conversation history' },
49
+ { name: '/compact', description: 'Summarize old messages' },
50
+ { name: '/tools', description: 'List available tools' },
51
+ { name: '/tokens', description: 'Show session token usage' },
52
+ { name: '/context', description: 'Show context window usage' },
53
+ ];
54
+ const MAX_VISIBLE = 10;
55
+ /**
56
+ * Strip ANSI codes from string
57
+ */
58
+ export function stripAnsi(str) {
59
+ // eslint-disable-next-line no-control-regex
60
+ return str.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, '');
61
+ }
62
+ /**
63
+ * Filter commands matching input
64
+ */
65
+ function filterCommands(input) {
66
+ const lower = input.toLowerCase();
67
+ return SLASH_COMMANDS.filter(cmd => {
68
+ if (cmd.name.toLowerCase().startsWith(lower))
69
+ return true;
70
+ if (cmd.aliases?.some(a => a.toLowerCase().startsWith(lower)))
71
+ return true;
72
+ return false;
73
+ });
74
+ }
75
+ /**
76
+ * Render autocomplete dropdown below current line
77
+ */
78
+ function renderDropdown(state, promptLen, currentLine, totalLines, hasSeparators) {
79
+ if (!state.active || state.matches.length === 0)
80
+ return 0;
81
+ // Move down past remaining input lines and bottom separator
82
+ let linesToMoveDown = totalLines - 1 - currentLine;
83
+ if (hasSeparators)
84
+ linesToMoveDown += 1;
85
+ if (linesToMoveDown > 0) {
86
+ process.stdout.write(ANSI.MOVE_DOWN(linesToMoveDown));
87
+ }
88
+ process.stdout.write('\n');
89
+ const visible = state.matches.slice(0, MAX_VISIBLE);
90
+ for (let i = 0; i < visible.length; i++) {
91
+ const cmd = visible[i];
92
+ const isSelected = i === state.selectedIndex;
93
+ const prefix = isSelected ? pc.cyan('❯ ') : ' ';
94
+ const name = isSelected ? pc.cyan(pc.bold(cmd.name)) : cmd.name;
95
+ const desc = pc.dim(` - ${cmd.description}`);
96
+ process.stdout.write(`${prefix}${name}${desc}\n`);
97
+ }
98
+ // Move back up to cursor position
99
+ const linesRendered = visible.length;
100
+ const linesToMoveUp = linesRendered + linesToMoveDown + 1;
101
+ process.stdout.write(ANSI.MOVE_UP(linesToMoveUp));
102
+ process.stdout.write(ANSI.MOVE_TO_COLUMN(promptLen + state.cursorPos + 1));
103
+ return linesRendered;
104
+ }
105
+ /**
106
+ * Clear dropdown area
107
+ */
108
+ function clearDropdown(dropdownLines, currentLine, totalLines, hasSeparators) {
109
+ if (dropdownLines === 0)
110
+ return;
111
+ // Move down past remaining lines and separator
112
+ let linesToMoveDown = totalLines - 1 - currentLine;
113
+ if (hasSeparators)
114
+ linesToMoveDown += 1;
115
+ if (linesToMoveDown > 0) {
116
+ process.stdout.write(ANSI.MOVE_DOWN(linesToMoveDown));
117
+ }
118
+ process.stdout.write('\n');
119
+ // Clear dropdown lines
120
+ for (let i = 0; i < dropdownLines; i++) {
121
+ process.stdout.write(ANSI.CLEAR_LINE + '\n');
122
+ }
123
+ // Move back up
124
+ const linesToMoveUp = dropdownLines + linesToMoveDown + 1;
125
+ process.stdout.write(ANSI.MOVE_UP(linesToMoveUp));
126
+ }
127
+ /**
128
+ * Create interactive input with autocomplete
129
+ */
130
+ export function createInteractiveInput(prompt, onSubmit, showSeparator = true, getTodos) {
131
+ let state = {
132
+ active: false,
133
+ lines: [''],
134
+ currentLine: 0,
135
+ cursorPos: 0,
136
+ selectedIndex: 0,
137
+ matches: [],
138
+ };
139
+ let dropdownLines = 0;
140
+ let isRunning = false;
141
+ let hasSeparators = false;
142
+ let linesAboveCursor = 0;
143
+ const promptLen = stripAnsi(prompt).length;
144
+ // Command history
145
+ const history = [];
146
+ let historyIndex = -1;
147
+ let savedInput = '';
148
+ // Track todo lines
149
+ let renderedTodoLines = 0;
150
+ /**
151
+ * Get full input as single string
152
+ */
153
+ function getFullInput() {
154
+ return state.lines.join('\n');
155
+ }
156
+ /**
157
+ * Get separator line
158
+ */
159
+ function getSeparatorLine() {
160
+ return pc.dim('─'.repeat(getTerminalWidth()));
161
+ }
162
+ /**
163
+ * Render todo section
164
+ */
165
+ function renderTodoSection() {
166
+ if (!getTodos)
167
+ return 0;
168
+ const todos = getTodos();
169
+ if (todos.length === 0)
170
+ return 0;
171
+ const lines = ['Todos'];
172
+ for (const todo of todos) {
173
+ let icon;
174
+ let text;
175
+ switch (todo.status) {
176
+ case 'completed':
177
+ icon = pc.dim('☒');
178
+ text = pc.strikethrough(pc.dim(todo.content));
179
+ break;
180
+ case 'in_progress':
181
+ icon = pc.cyan('☐');
182
+ text = pc.bold(pc.cyan(todo.activeForm || todo.content));
183
+ break;
184
+ default:
185
+ icon = pc.dim('☐');
186
+ text = pc.dim(todo.content);
187
+ break;
188
+ }
189
+ lines.push(`${icon} ${text}`);
190
+ }
191
+ process.stdout.write(lines.join('\n') + '\n');
192
+ return lines.length;
193
+ }
194
+ /**
195
+ * Render input with separators
196
+ */
197
+ function renderInput() {
198
+ const termWidth = getTerminalWidth();
199
+ // Clear previous render
200
+ if (linesAboveCursor > 0) {
201
+ process.stdout.write('\r');
202
+ process.stdout.write(ANSI.MOVE_UP(linesAboveCursor));
203
+ }
204
+ else {
205
+ process.stdout.write('\r');
206
+ }
207
+ process.stdout.write(ANSI.CLEAR_TO_END_OF_SCREEN);
208
+ // Render todos
209
+ renderedTodoLines = renderTodoSection();
210
+ // Render top separator
211
+ let physicalLinesRendered = renderedTodoLines;
212
+ if (showSeparator) {
213
+ process.stdout.write(getSeparatorLine() + '\n');
214
+ physicalLinesRendered += 1;
215
+ }
216
+ // Render input lines
217
+ let physicalLinesBeforeCursor = 0;
218
+ for (let i = 0; i < state.lines.length; i++) {
219
+ const linePrompt = i === 0 ? prompt : pc.dim(' \\ ');
220
+ const linePromptLen = i === 0 ? promptLen : 5;
221
+ if (i < state.currentLine) {
222
+ physicalLinesBeforeCursor += calcPhysicalLines(state.lines[i], linePromptLen, termWidth);
223
+ }
224
+ process.stdout.write(linePrompt + state.lines[i]);
225
+ if (i < state.lines.length - 1) {
226
+ process.stdout.write('\n');
227
+ }
228
+ }
229
+ // Render bottom separator
230
+ if (showSeparator) {
231
+ process.stdout.write('\n' + getSeparatorLine());
232
+ }
233
+ // Track state
234
+ renderedTodoLines = state.lines.length;
235
+ hasSeparators = showSeparator;
236
+ // Position cursor
237
+ const currentLinePromptLen = state.currentLine === 0 ? promptLen : 5;
238
+ const cursorAbsPos = currentLinePromptLen + state.cursorPos;
239
+ const currentLinePhysical = calcPhysicalLines(state.lines[state.currentLine], currentLinePromptLen, termWidth);
240
+ const cursorPhysicalRow = Math.floor(cursorAbsPos / termWidth);
241
+ // Move up from bottom to cursor position
242
+ let linesToMoveUp = showSeparator ? 1 : 0;
243
+ for (let i = state.currentLine + 1; i < state.lines.length; i++) {
244
+ const lp = i === 0 ? promptLen : 5;
245
+ linesToMoveUp += calcPhysicalLines(state.lines[i], lp, termWidth);
246
+ }
247
+ linesToMoveUp += (currentLinePhysical - 1 - cursorPhysicalRow);
248
+ if (linesToMoveUp > 0) {
249
+ process.stdout.write(ANSI.MOVE_UP(linesToMoveUp));
250
+ }
251
+ const cursorCol = (cursorAbsPos % termWidth) + 1;
252
+ process.stdout.write(ANSI.MOVE_TO_COLUMN(cursorCol));
253
+ // Track cursor position for next render
254
+ linesAboveCursor = physicalLinesRendered + physicalLinesBeforeCursor + cursorPhysicalRow;
255
+ }
256
+ /**
257
+ * Full render with dropdown
258
+ */
259
+ function render() {
260
+ clearDropdown(dropdownLines, state.currentLine, state.lines.length, hasSeparators);
261
+ renderInput();
262
+ dropdownLines = renderDropdown(state, promptLen, state.currentLine, state.lines.length, hasSeparators);
263
+ }
264
+ /**
265
+ * Update autocomplete state
266
+ */
267
+ function updateAutocomplete() {
268
+ const fullInput = getFullInput();
269
+ if (fullInput.startsWith('/') && state.lines.length === 1) {
270
+ state.active = true;
271
+ state.matches = filterCommands(fullInput);
272
+ if (state.selectedIndex >= state.matches.length) {
273
+ state.selectedIndex = Math.max(0, state.matches.length - 1);
274
+ }
275
+ }
276
+ else {
277
+ state.active = false;
278
+ state.matches = [];
279
+ state.selectedIndex = 0;
280
+ }
281
+ }
282
+ /**
283
+ * Handle keypress
284
+ */
285
+ function handleKey(key, data) {
286
+ const isEscape = data.length === 1 && data[0] === 0x1b;
287
+ const isEnter = key === '\r' || key === '\n';
288
+ const isBackspace = key === '\x7f' || key === '\b';
289
+ const isTab = key === '\t';
290
+ const isUpArrow = data.length === 3 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x41;
291
+ const isDownArrow = data.length === 3 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x42;
292
+ const isLeftArrow = data.length === 3 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x44;
293
+ const isRightArrow = data.length === 3 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x43;
294
+ // Mac Option + Arrow (word navigation)
295
+ const isOptionLeft = (data.length === 6 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x31 &&
296
+ data[3] === 0x3b && data[4] === 0x33 && data[5] === 0x44) ||
297
+ (data.length === 2 && data[0] === 0x1b && data[1] === 0x62);
298
+ const isOptionRight = (data.length === 6 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x31 &&
299
+ data[3] === 0x3b && data[4] === 0x33 && data[5] === 0x43) ||
300
+ (data.length === 2 && data[0] === 0x1b && data[1] === 0x66);
301
+ // Home/End (Cmd+Left/Right on Mac)
302
+ const isHome = key === '\x01' ||
303
+ (data.length === 3 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x48);
304
+ const isEnd = key === '\x05' ||
305
+ (data.length === 3 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x46);
306
+ // Escape - close autocomplete
307
+ if (isEscape) {
308
+ if (state.active) {
309
+ clearDropdown(dropdownLines, state.currentLine, state.lines.length, hasSeparators);
310
+ dropdownLines = 0;
311
+ state.active = false;
312
+ state.matches = [];
313
+ renderInput();
314
+ }
315
+ return;
316
+ }
317
+ // Enter
318
+ if (isEnter) {
319
+ const currentLine = state.lines[state.currentLine];
320
+ // Continuation with backslash
321
+ if (currentLine.endsWith('\\')) {
322
+ state.lines[state.currentLine] = currentLine.slice(0, -1);
323
+ state.lines.push('');
324
+ state.currentLine++;
325
+ state.cursorPos = 0;
326
+ state.active = false;
327
+ clearDropdown(dropdownLines, state.currentLine - 1, state.lines.length - 1, hasSeparators);
328
+ dropdownLines = 0;
329
+ render();
330
+ return;
331
+ }
332
+ // Autocomplete selection
333
+ if (state.active && state.matches.length > 0) {
334
+ state.lines[0] = state.matches[state.selectedIndex].name;
335
+ state.cursorPos = state.lines[0].length;
336
+ }
337
+ // Submit
338
+ clearDropdown(dropdownLines, state.currentLine, state.lines.length, hasSeparators);
339
+ dropdownLines = 0;
340
+ // Clear display
341
+ if (linesAboveCursor > 0) {
342
+ process.stdout.write('\r' + ANSI.MOVE_UP(linesAboveCursor));
343
+ }
344
+ process.stdout.write('\r' + ANSI.CLEAR_TO_END_OF_SCREEN);
345
+ // Print clean input
346
+ for (let i = 0; i < state.lines.length; i++) {
347
+ const linePrompt = i === 0 ? prompt : pc.dim(' \\ ');
348
+ process.stdout.write(linePrompt + state.lines[i]);
349
+ if (i < state.lines.length - 1) {
350
+ process.stdout.write('\n');
351
+ }
352
+ }
353
+ process.stdout.write('\n');
354
+ const input = getFullInput();
355
+ // Save to history
356
+ if (input.trim() && (history.length === 0 || history[history.length - 1] !== input)) {
357
+ history.push(input);
358
+ }
359
+ historyIndex = -1;
360
+ savedInput = '';
361
+ // Reset state
362
+ state = {
363
+ active: false,
364
+ lines: [''],
365
+ currentLine: 0,
366
+ cursorPos: 0,
367
+ selectedIndex: 0,
368
+ matches: [],
369
+ };
370
+ renderedTodoLines = 0;
371
+ linesAboveCursor = 0;
372
+ hasSeparators = false;
373
+ onSubmit(input);
374
+ return;
375
+ }
376
+ // Tab - accept autocomplete or insert spaces
377
+ if (isTab) {
378
+ if (state.active && state.matches.length > 0) {
379
+ state.lines[state.currentLine] = state.matches[state.selectedIndex].name;
380
+ state.cursorPos = state.lines[state.currentLine].length;
381
+ state.active = false;
382
+ state.matches = [];
383
+ clearDropdown(dropdownLines, state.currentLine, state.lines.length, hasSeparators);
384
+ dropdownLines = 0;
385
+ }
386
+ else {
387
+ // Insert 2 spaces
388
+ const line = state.lines[state.currentLine];
389
+ state.lines[state.currentLine] = line.slice(0, state.cursorPos) + ' ' + line.slice(state.cursorPos);
390
+ state.cursorPos += 2;
391
+ historyIndex = -1;
392
+ updateAutocomplete();
393
+ }
394
+ render();
395
+ return;
396
+ }
397
+ // Up arrow
398
+ if (isUpArrow) {
399
+ if (state.active && state.selectedIndex > 0) {
400
+ state.selectedIndex--;
401
+ render();
402
+ return;
403
+ }
404
+ // Visual line navigation
405
+ const termWidth = getTerminalWidth();
406
+ const currentLinePromptLen = state.currentLine === 0 ? promptLen : 5;
407
+ const cursorAbsPos = currentLinePromptLen + state.cursorPos;
408
+ const currentPhysicalRow = Math.floor(cursorAbsPos / termWidth);
409
+ const cursorColInRow = cursorAbsPos % termWidth;
410
+ if (currentPhysicalRow > 0) {
411
+ const newAbsPos = (currentPhysicalRow - 1) * termWidth + cursorColInRow;
412
+ state.cursorPos = Math.max(0, newAbsPos - currentLinePromptLen);
413
+ state.cursorPos = Math.min(state.cursorPos, state.lines[state.currentLine].length);
414
+ render();
415
+ return;
416
+ }
417
+ else if (state.currentLine > 0) {
418
+ state.currentLine--;
419
+ const prevLinePromptLen = state.currentLine === 0 ? promptLen : 5;
420
+ const prevLineLen = state.lines[state.currentLine].length;
421
+ const prevLinePhysical = calcPhysicalLines(state.lines[state.currentLine], prevLinePromptLen, termWidth);
422
+ const lastRowStart = (prevLinePhysical - 1) * termWidth;
423
+ const targetAbsPos = lastRowStart + cursorColInRow;
424
+ state.cursorPos = Math.max(0, targetAbsPos - prevLinePromptLen);
425
+ state.cursorPos = Math.min(state.cursorPos, prevLineLen);
426
+ render();
427
+ return;
428
+ }
429
+ // History navigation
430
+ if (!state.active && state.currentLine === 0 && currentPhysicalRow === 0 && history.length > 0) {
431
+ if (historyIndex === -1) {
432
+ savedInput = getFullInput();
433
+ }
434
+ if (historyIndex < history.length - 1) {
435
+ historyIndex++;
436
+ const historyEntry = history[history.length - 1 - historyIndex];
437
+ state.lines = [historyEntry];
438
+ state.currentLine = 0;
439
+ state.cursorPos = historyEntry.length;
440
+ render();
441
+ }
442
+ }
443
+ return;
444
+ }
445
+ // Down arrow
446
+ if (isDownArrow) {
447
+ if (state.active && state.selectedIndex < state.matches.length - 1 && state.selectedIndex < MAX_VISIBLE - 1) {
448
+ state.selectedIndex++;
449
+ render();
450
+ return;
451
+ }
452
+ const termWidth = getTerminalWidth();
453
+ const currentLinePromptLen = state.currentLine === 0 ? promptLen : 5;
454
+ const cursorAbsPos = currentLinePromptLen + state.cursorPos;
455
+ const currentPhysicalRow = Math.floor(cursorAbsPos / termWidth);
456
+ const cursorColInRow = cursorAbsPos % termWidth;
457
+ const currentLinePhysical = calcPhysicalLines(state.lines[state.currentLine], currentLinePromptLen, termWidth);
458
+ if (currentPhysicalRow < currentLinePhysical - 1) {
459
+ const newAbsPos = (currentPhysicalRow + 1) * termWidth + cursorColInRow;
460
+ state.cursorPos = Math.max(0, newAbsPos - currentLinePromptLen);
461
+ state.cursorPos = Math.min(state.cursorPos, state.lines[state.currentLine].length);
462
+ render();
463
+ return;
464
+ }
465
+ else if (state.currentLine < state.lines.length - 1) {
466
+ state.currentLine++;
467
+ const nextLinePromptLen = state.currentLine === 0 ? promptLen : 5;
468
+ const nextLineLen = state.lines[state.currentLine].length;
469
+ state.cursorPos = Math.max(0, cursorColInRow - nextLinePromptLen);
470
+ state.cursorPos = Math.min(state.cursorPos, nextLineLen);
471
+ render();
472
+ return;
473
+ }
474
+ // History forward
475
+ if (historyIndex >= 0) {
476
+ historyIndex--;
477
+ if (historyIndex === -1) {
478
+ state.lines = savedInput.split('\n');
479
+ state.currentLine = state.lines.length - 1;
480
+ state.cursorPos = state.lines[state.currentLine].length;
481
+ }
482
+ else {
483
+ const historyEntry = history[history.length - 1 - historyIndex];
484
+ state.lines = [historyEntry];
485
+ state.currentLine = 0;
486
+ state.cursorPos = historyEntry.length;
487
+ }
488
+ updateAutocomplete();
489
+ render();
490
+ }
491
+ return;
492
+ }
493
+ // Left arrow
494
+ if (isLeftArrow) {
495
+ if (state.cursorPos > 0) {
496
+ state.cursorPos--;
497
+ process.stdout.write('\x1B[D');
498
+ }
499
+ else if (state.currentLine > 0) {
500
+ state.currentLine--;
501
+ state.cursorPos = state.lines[state.currentLine].length;
502
+ render();
503
+ }
504
+ return;
505
+ }
506
+ // Right arrow
507
+ if (isRightArrow) {
508
+ if (state.cursorPos < state.lines[state.currentLine].length) {
509
+ state.cursorPos++;
510
+ process.stdout.write('\x1B[C');
511
+ }
512
+ else if (state.currentLine < state.lines.length - 1) {
513
+ state.currentLine++;
514
+ state.cursorPos = 0;
515
+ render();
516
+ }
517
+ return;
518
+ }
519
+ // Option + Left (word left)
520
+ if (isOptionLeft) {
521
+ const line = state.lines[state.currentLine];
522
+ if (state.cursorPos > 0) {
523
+ let pos = state.cursorPos;
524
+ while (pos > 0 && line[pos - 1] === ' ')
525
+ pos--;
526
+ while (pos > 0 && line[pos - 1] !== ' ')
527
+ pos--;
528
+ state.cursorPos = pos;
529
+ render();
530
+ }
531
+ else if (state.currentLine > 0) {
532
+ state.currentLine--;
533
+ state.cursorPos = state.lines[state.currentLine].length;
534
+ render();
535
+ }
536
+ return;
537
+ }
538
+ // Option + Right (word right)
539
+ if (isOptionRight) {
540
+ const line = state.lines[state.currentLine];
541
+ if (state.cursorPos < line.length) {
542
+ let pos = state.cursorPos;
543
+ while (pos < line.length && line[pos] !== ' ')
544
+ pos++;
545
+ while (pos < line.length && line[pos] === ' ')
546
+ pos++;
547
+ state.cursorPos = pos;
548
+ render();
549
+ }
550
+ else if (state.currentLine < state.lines.length - 1) {
551
+ state.currentLine++;
552
+ state.cursorPos = 0;
553
+ render();
554
+ }
555
+ return;
556
+ }
557
+ // Home
558
+ if (isHome) {
559
+ state.cursorPos = 0;
560
+ render();
561
+ return;
562
+ }
563
+ // End
564
+ if (isEnd) {
565
+ state.cursorPos = state.lines[state.currentLine].length;
566
+ render();
567
+ return;
568
+ }
569
+ // Backspace
570
+ if (isBackspace) {
571
+ historyIndex = -1;
572
+ if (state.cursorPos > 0) {
573
+ const line = state.lines[state.currentLine];
574
+ state.lines[state.currentLine] = line.slice(0, state.cursorPos - 1) + line.slice(state.cursorPos);
575
+ state.cursorPos--;
576
+ updateAutocomplete();
577
+ render();
578
+ }
579
+ else if (state.currentLine > 0) {
580
+ const currentLine = state.lines[state.currentLine];
581
+ const prevLine = state.lines[state.currentLine - 1];
582
+ state.lines[state.currentLine - 1] = prevLine + currentLine;
583
+ state.lines.splice(state.currentLine, 1);
584
+ state.currentLine--;
585
+ state.cursorPos = prevLine.length;
586
+ updateAutocomplete();
587
+ render();
588
+ }
589
+ return;
590
+ }
591
+ // Regular character(s) - handles typing and paste
592
+ if (key.length >= 1) {
593
+ const printable = key.split('').filter(c => c.charCodeAt(0) >= 32).join('');
594
+ if (printable.length > 0) {
595
+ const line = state.lines[state.currentLine];
596
+ state.lines[state.currentLine] = line.slice(0, state.cursorPos) + printable + line.slice(state.cursorPos);
597
+ state.cursorPos += printable.length;
598
+ historyIndex = -1;
599
+ updateAutocomplete();
600
+ render();
601
+ }
602
+ }
603
+ }
604
+ /**
605
+ * Start input
606
+ */
607
+ function start() {
608
+ if (isRunning)
609
+ return;
610
+ isRunning = true;
611
+ if (process.stdin.isTTY) {
612
+ process.stdin.setRawMode(true);
613
+ }
614
+ process.stdin.resume();
615
+ // Reset state
616
+ renderedTodoLines = 0;
617
+ linesAboveCursor = 0;
618
+ hasSeparators = false;
619
+ renderInput();
620
+ process.stdin.on('data', (data) => {
621
+ if (!isRunning)
622
+ return;
623
+ const key = data.toString();
624
+ handleKey(key, data);
625
+ });
626
+ }
627
+ /**
628
+ * Stop input
629
+ */
630
+ function stop() {
631
+ isRunning = false;
632
+ if (process.stdin.isTTY) {
633
+ process.stdin.setRawMode(false);
634
+ }
635
+ process.stdin.removeAllListeners('data');
636
+ }
637
+ return { start, stop };
638
+ }