@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,800 @@
1
+ /**
2
+ * Input Prompt
3
+ *
4
+ * @deprecated This module is superseded by input-prompt-v2.ts which uses
5
+ * event-driven architecture for non-blocking input with queue mode support.
6
+ * This file is kept for reference and may be removed in a future cleanup.
7
+ *
8
+ * Self-contained input handling with multiline support, autocomplete,
9
+ * history navigation, and proper visual line navigation.
10
+ */
11
+ import pc from 'picocolors';
12
+ import * as terminal from './terminal.js';
13
+ // =============================================================================
14
+ // Constants
15
+ // =============================================================================
16
+ const MAX_VISIBLE_COMMANDS = 10;
17
+ // =============================================================================
18
+ // Default Commands
19
+ // =============================================================================
20
+ export const DEFAULT_COMMANDS = [
21
+ { command: '/help', description: 'Show available commands' },
22
+ { command: '/exit', description: 'Quit the demo' },
23
+ { command: '/clear', description: 'Clear conversation history' },
24
+ { command: '/compact', description: 'Summarize old messages' },
25
+ { command: '/tools', description: 'List available tools' },
26
+ { command: '/tokens', description: 'Show session token usage' },
27
+ { command: '/context', description: 'Show context window usage' },
28
+ ];
29
+ // =============================================================================
30
+ // Helper Functions
31
+ // =============================================================================
32
+ /**
33
+ * Strip ANSI codes from string
34
+ */
35
+ export function stripAnsi(str) {
36
+ // eslint-disable-next-line no-control-regex
37
+ return str.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, '');
38
+ }
39
+ /**
40
+ * Calculate physical lines for wrapped text
41
+ */
42
+ function calcPhysicalLines(text, startCol, termWidth) {
43
+ if (text.length === 0)
44
+ return 1;
45
+ const totalLen = startCol + text.length;
46
+ return Math.ceil(totalLen / termWidth) || 1;
47
+ }
48
+ /**
49
+ * Filter commands matching input
50
+ */
51
+ function filterCommands(input, commands) {
52
+ const lower = input.toLowerCase();
53
+ return commands.filter((cmd) => cmd.command.toLowerCase().startsWith(lower));
54
+ }
55
+ // =============================================================================
56
+ // Input Prompt Class
57
+ // =============================================================================
58
+ export class InputPrompt {
59
+ // Configuration
60
+ prompt;
61
+ promptLen;
62
+ showSeparators;
63
+ commands;
64
+ getTodos;
65
+ // Input state
66
+ state = {
67
+ lines: [''],
68
+ currentLine: 0,
69
+ cursorPos: 0,
70
+ };
71
+ // Autocomplete
72
+ autocomplete = {
73
+ active: false,
74
+ matches: [],
75
+ selectedIndex: 0,
76
+ };
77
+ // History
78
+ history = [];
79
+ historyIndex = -1;
80
+ savedInput = '';
81
+ // Rendering tracking
82
+ renderedLines = 0;
83
+ dropdownLines = 0;
84
+ linesAboveCursor = 0;
85
+ hasSeparators = false;
86
+ // Control
87
+ isRunning = false;
88
+ resolveInput = null;
89
+ constructor(options = {}) {
90
+ this.prompt = options.prompt ?? pc.cyan('❯ ');
91
+ this.promptLen = stripAnsi(this.prompt).length;
92
+ this.showSeparators = options.showSeparators ?? true;
93
+ this.commands = options.commands ?? DEFAULT_COMMANDS;
94
+ this.getTodos = options.getTodos;
95
+ }
96
+ // ===========================================================================
97
+ // Public API
98
+ // ===========================================================================
99
+ /**
100
+ * Get input from user (async)
101
+ * Returns when user submits or cancels
102
+ */
103
+ async getInput() {
104
+ return new Promise((resolve) => {
105
+ this.resolveInput = resolve;
106
+ this.start();
107
+ });
108
+ }
109
+ /**
110
+ * Start input mode
111
+ */
112
+ start() {
113
+ if (this.isRunning)
114
+ return;
115
+ this.isRunning = true;
116
+ terminal.enableRawMode();
117
+ this.resetState();
118
+ this.render();
119
+ process.stdin.on('data', this.handleData);
120
+ }
121
+ /**
122
+ * Stop input mode
123
+ */
124
+ stop() {
125
+ this.isRunning = false;
126
+ terminal.disableRawMode();
127
+ process.stdin.removeListener('data', this.handleData);
128
+ }
129
+ /**
130
+ * Get current buffer value
131
+ */
132
+ getValue() {
133
+ return this.state.lines.join('\n');
134
+ }
135
+ /**
136
+ * Set buffer value
137
+ */
138
+ setValue(value) {
139
+ this.state.lines = value.split('\n');
140
+ this.state.currentLine = this.state.lines.length - 1;
141
+ this.state.cursorPos = this.state.lines[this.state.currentLine].length;
142
+ if (this.isRunning) {
143
+ this.updateAutocomplete();
144
+ this.render();
145
+ }
146
+ }
147
+ /**
148
+ * Clear the rendered area
149
+ */
150
+ clear() {
151
+ this.clearDropdown();
152
+ if (this.linesAboveCursor > 0) {
153
+ terminal.moveCursorToLineStart();
154
+ terminal.moveCursorUp(this.linesAboveCursor);
155
+ }
156
+ terminal.clearToEndOfScreen();
157
+ this.renderedLines = 0;
158
+ this.linesAboveCursor = 0;
159
+ }
160
+ /**
161
+ * Get number of rendered lines
162
+ */
163
+ getRenderedLines() {
164
+ return this.renderedLines + this.dropdownLines + (this.showSeparators ? 2 : 0);
165
+ }
166
+ /**
167
+ * Add command to history
168
+ */
169
+ addToHistory(input) {
170
+ if (input.trim() && (this.history.length === 0 || this.history[this.history.length - 1] !== input)) {
171
+ this.history.push(input);
172
+ }
173
+ }
174
+ // ===========================================================================
175
+ // Private: State Management
176
+ // ===========================================================================
177
+ resetState() {
178
+ this.state = {
179
+ lines: [''],
180
+ currentLine: 0,
181
+ cursorPos: 0,
182
+ };
183
+ this.autocomplete = {
184
+ active: false,
185
+ matches: [],
186
+ selectedIndex: 0,
187
+ };
188
+ this.historyIndex = -1;
189
+ this.savedInput = '';
190
+ this.renderedLines = 0;
191
+ this.dropdownLines = 0;
192
+ this.linesAboveCursor = 0;
193
+ this.hasSeparators = false;
194
+ }
195
+ updateAutocomplete() {
196
+ const fullInput = this.getValue();
197
+ if (fullInput.startsWith('/') && this.state.lines.length === 1) {
198
+ this.autocomplete.active = true;
199
+ this.autocomplete.matches = filterCommands(fullInput, this.commands);
200
+ if (this.autocomplete.selectedIndex >= this.autocomplete.matches.length) {
201
+ this.autocomplete.selectedIndex = Math.max(0, this.autocomplete.matches.length - 1);
202
+ }
203
+ }
204
+ else {
205
+ this.autocomplete.active = false;
206
+ this.autocomplete.matches = [];
207
+ this.autocomplete.selectedIndex = 0;
208
+ }
209
+ }
210
+ // ===========================================================================
211
+ // Private: Layout Calculation
212
+ // ===========================================================================
213
+ /**
214
+ * Get physical layout of current buffer
215
+ */
216
+ getPhysicalLayout() {
217
+ const termWidth = terminal.getTerminalWidth();
218
+ const lines = [];
219
+ let cursorRow = 0;
220
+ let cursorCol = 0;
221
+ let totalRows = 0;
222
+ for (let i = 0; i < this.state.lines.length; i++) {
223
+ const linePromptLen = i === 0 ? this.promptLen : 5; // " \ "
224
+ const lineText = this.state.lines[i];
225
+ const physicalLines = calcPhysicalLines(lineText, linePromptLen, termWidth);
226
+ for (let p = 0; p < physicalLines; p++) {
227
+ const start = p * termWidth - (p === 0 ? 0 : linePromptLen);
228
+ const end = start + termWidth;
229
+ lines.push(lineText.slice(Math.max(0, start), end));
230
+ }
231
+ if (i === this.state.currentLine) {
232
+ const cursorAbsPos = linePromptLen + this.state.cursorPos;
233
+ cursorRow = totalRows + Math.floor(cursorAbsPos / termWidth);
234
+ cursorCol = cursorAbsPos % termWidth;
235
+ }
236
+ totalRows += physicalLines;
237
+ }
238
+ return { lines, cursorRow, cursorCol, totalRows };
239
+ }
240
+ // ===========================================================================
241
+ // Private: Rendering
242
+ // ===========================================================================
243
+ getSeparatorLine() {
244
+ return pc.dim('─'.repeat(terminal.getTerminalWidth()));
245
+ }
246
+ renderTodos() {
247
+ if (!this.getTodos)
248
+ return 0;
249
+ const todos = this.getTodos();
250
+ if (todos.length === 0)
251
+ return 0;
252
+ let linesRendered = 0;
253
+ // Group todos by status for display
254
+ const inProgress = todos.filter(t => t.status === 'in_progress');
255
+ const pending = todos.filter(t => t.status === 'pending');
256
+ const completed = todos.filter(t => t.status === 'completed');
257
+ // Show in-progress task prominently
258
+ for (const todo of inProgress) {
259
+ const label = todo.activeForm || todo.content;
260
+ terminal.write(pc.cyan('⟳ ') + pc.bold(label) + '\n');
261
+ linesRendered++;
262
+ }
263
+ // Show pending tasks
264
+ for (const todo of pending) {
265
+ terminal.write(pc.dim('○ ') + pc.dim(todo.content) + '\n');
266
+ linesRendered++;
267
+ }
268
+ // Show completed tasks (dimmed)
269
+ for (const todo of completed) {
270
+ terminal.write(pc.green('✓ ') + pc.dim(pc.strikethrough(todo.content)) + '\n');
271
+ linesRendered++;
272
+ }
273
+ if (linesRendered > 0) {
274
+ terminal.write('\n');
275
+ linesRendered++;
276
+ }
277
+ return linesRendered;
278
+ }
279
+ render() {
280
+ const termWidth = terminal.getTerminalWidth();
281
+ // Clear previous render
282
+ this.clearDropdown();
283
+ if (this.linesAboveCursor > 0) {
284
+ terminal.moveCursorToLineStart();
285
+ terminal.moveCursorUp(this.linesAboveCursor);
286
+ }
287
+ else {
288
+ terminal.moveCursorToLineStart();
289
+ }
290
+ terminal.clearToEndOfScreen();
291
+ // Render todos (above input area)
292
+ let physicalLinesRendered = 0;
293
+ physicalLinesRendered += this.renderTodos();
294
+ // Render top separator
295
+ if (this.showSeparators) {
296
+ terminal.write(this.getSeparatorLine() + '\n');
297
+ physicalLinesRendered += 1;
298
+ }
299
+ // Render input lines
300
+ let physicalLinesBeforeCursor = 0;
301
+ for (let i = 0; i < this.state.lines.length; i++) {
302
+ const linePrompt = i === 0 ? this.prompt : pc.dim(' \\ ');
303
+ const linePromptLen = i === 0 ? this.promptLen : 5;
304
+ if (i < this.state.currentLine) {
305
+ physicalLinesBeforeCursor += calcPhysicalLines(this.state.lines[i], linePromptLen, termWidth);
306
+ }
307
+ terminal.write(linePrompt + this.state.lines[i]);
308
+ if (i < this.state.lines.length - 1) {
309
+ terminal.write('\n');
310
+ }
311
+ }
312
+ // Render bottom separator
313
+ if (this.showSeparators) {
314
+ terminal.write('\n' + this.getSeparatorLine());
315
+ }
316
+ // Track state
317
+ this.renderedLines = this.state.lines.length;
318
+ this.hasSeparators = this.showSeparators;
319
+ // Position cursor
320
+ const currentLinePromptLen = this.state.currentLine === 0 ? this.promptLen : 5;
321
+ const cursorAbsPos = currentLinePromptLen + this.state.cursorPos;
322
+ const currentLinePhysical = calcPhysicalLines(this.state.lines[this.state.currentLine], currentLinePromptLen, termWidth);
323
+ const cursorPhysicalRow = Math.floor(cursorAbsPos / termWidth);
324
+ // Move up from bottom to cursor position
325
+ let linesToMoveUp = this.showSeparators ? 1 : 0;
326
+ for (let i = this.state.currentLine + 1; i < this.state.lines.length; i++) {
327
+ const lp = i === 0 ? this.promptLen : 5;
328
+ linesToMoveUp += calcPhysicalLines(this.state.lines[i], lp, termWidth);
329
+ }
330
+ linesToMoveUp += currentLinePhysical - 1 - cursorPhysicalRow;
331
+ if (linesToMoveUp > 0) {
332
+ terminal.moveCursorUp(linesToMoveUp);
333
+ }
334
+ const cursorCol = (cursorAbsPos % termWidth) + 1;
335
+ terminal.moveCursorToColumn(cursorCol);
336
+ // Track cursor position for next render
337
+ this.linesAboveCursor = physicalLinesRendered + physicalLinesBeforeCursor + cursorPhysicalRow;
338
+ // Render dropdown
339
+ this.renderDropdown();
340
+ }
341
+ renderDropdown() {
342
+ if (!this.autocomplete.active || this.autocomplete.matches.length === 0) {
343
+ this.dropdownLines = 0;
344
+ return;
345
+ }
346
+ const termWidth = terminal.getTerminalWidth();
347
+ const currentLinePromptLen = this.state.currentLine === 0 ? this.promptLen : 5;
348
+ // Move down past remaining input lines and bottom separator
349
+ let linesToMoveDown = this.state.lines.length - 1 - this.state.currentLine;
350
+ if (this.hasSeparators)
351
+ linesToMoveDown += 1;
352
+ // Account for wrapped lines
353
+ for (let i = this.state.currentLine; i < this.state.lines.length; i++) {
354
+ const lp = i === 0 ? this.promptLen : 5;
355
+ const physical = calcPhysicalLines(this.state.lines[i], lp, termWidth);
356
+ if (i === this.state.currentLine) {
357
+ const cursorAbsPos = currentLinePromptLen + this.state.cursorPos;
358
+ const cursorRow = Math.floor(cursorAbsPos / termWidth);
359
+ linesToMoveDown += physical - 1 - cursorRow;
360
+ }
361
+ else {
362
+ linesToMoveDown += physical - 1;
363
+ }
364
+ }
365
+ if (linesToMoveDown > 0) {
366
+ terminal.moveCursorDown(linesToMoveDown);
367
+ }
368
+ terminal.write('\n');
369
+ const visible = this.autocomplete.matches.slice(0, MAX_VISIBLE_COMMANDS);
370
+ for (let i = 0; i < visible.length; i++) {
371
+ const cmd = visible[i];
372
+ const isSelected = i === this.autocomplete.selectedIndex;
373
+ const prefix = isSelected ? pc.cyan('❯ ') : ' ';
374
+ const name = isSelected ? pc.cyan(pc.bold(cmd.command)) : cmd.command;
375
+ const desc = pc.dim(` - ${cmd.description}`);
376
+ terminal.write(`${prefix}${name}${desc}\n`);
377
+ }
378
+ // Move back up to cursor position
379
+ this.dropdownLines = visible.length;
380
+ const linesToMoveUp = this.dropdownLines + linesToMoveDown + 1;
381
+ terminal.moveCursorUp(linesToMoveUp);
382
+ const cursorAbsPos = currentLinePromptLen + this.state.cursorPos;
383
+ const cursorCol = (cursorAbsPos % termWidth) + 1;
384
+ terminal.moveCursorToColumn(cursorCol);
385
+ }
386
+ clearDropdown() {
387
+ if (this.dropdownLines === 0)
388
+ return;
389
+ const termWidth = terminal.getTerminalWidth();
390
+ const currentLinePromptLen = this.state.currentLine === 0 ? this.promptLen : 5;
391
+ // Move down past remaining lines and separator
392
+ let linesToMoveDown = this.state.lines.length - 1 - this.state.currentLine;
393
+ if (this.hasSeparators)
394
+ linesToMoveDown += 1;
395
+ for (let i = this.state.currentLine; i < this.state.lines.length; i++) {
396
+ const lp = i === 0 ? this.promptLen : 5;
397
+ const physical = calcPhysicalLines(this.state.lines[i], lp, termWidth);
398
+ if (i === this.state.currentLine) {
399
+ const cursorAbsPos = currentLinePromptLen + this.state.cursorPos;
400
+ const cursorRow = Math.floor(cursorAbsPos / termWidth);
401
+ linesToMoveDown += physical - 1 - cursorRow;
402
+ }
403
+ else {
404
+ linesToMoveDown += physical - 1;
405
+ }
406
+ }
407
+ if (linesToMoveDown > 0) {
408
+ terminal.moveCursorDown(linesToMoveDown);
409
+ }
410
+ terminal.write('\n');
411
+ // Clear dropdown lines
412
+ for (let i = 0; i < this.dropdownLines; i++) {
413
+ terminal.clearLine();
414
+ terminal.write('\n');
415
+ }
416
+ // Move back up
417
+ const linesToMoveUp = this.dropdownLines + linesToMoveDown + 1;
418
+ terminal.moveCursorUp(linesToMoveUp);
419
+ this.dropdownLines = 0;
420
+ }
421
+ // ===========================================================================
422
+ // Private: Input Handling
423
+ // ===========================================================================
424
+ handleData = (data) => {
425
+ if (!this.isRunning)
426
+ return;
427
+ const key = data.toString();
428
+ this.handleKey(key, data);
429
+ };
430
+ handleKey(key, data) {
431
+ // Detect special keys
432
+ const isEscape = data.length === 1 && data[0] === 0x1b;
433
+ const isEnter = key === '\r' || key === '\n';
434
+ const isBackspace = key === '\x7f' || key === '\b';
435
+ const isTab = key === '\t';
436
+ const isCtrlC = key === '\x03';
437
+ const isUpArrow = data.length === 3 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x41;
438
+ const isDownArrow = data.length === 3 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x42;
439
+ const isLeftArrow = data.length === 3 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x44;
440
+ const isRightArrow = data.length === 3 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x43;
441
+ // Mac Option + Arrow (word navigation)
442
+ const isOptionLeft = (data.length === 6 &&
443
+ data[0] === 0x1b &&
444
+ data[1] === 0x5b &&
445
+ data[2] === 0x31 &&
446
+ data[3] === 0x3b &&
447
+ data[4] === 0x33 &&
448
+ data[5] === 0x44) ||
449
+ (data.length === 2 && data[0] === 0x1b && data[1] === 0x62);
450
+ const isOptionRight = (data.length === 6 &&
451
+ data[0] === 0x1b &&
452
+ data[1] === 0x5b &&
453
+ data[2] === 0x31 &&
454
+ data[3] === 0x3b &&
455
+ data[4] === 0x33 &&
456
+ data[5] === 0x43) ||
457
+ (data.length === 2 && data[0] === 0x1b && data[1] === 0x66);
458
+ // Home/End (Cmd+Left/Right on Mac, or Home/End keys)
459
+ const isHome = key === '\x01' || (data.length === 3 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x48);
460
+ const isEnd = key === '\x05' || (data.length === 3 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x46);
461
+ // Ctrl+C - cancel
462
+ if (isCtrlC) {
463
+ this.finishInput({ action: 'cancel' });
464
+ return;
465
+ }
466
+ // Escape - close autocomplete
467
+ if (isEscape) {
468
+ if (this.autocomplete.active) {
469
+ this.clearDropdown();
470
+ this.autocomplete.active = false;
471
+ this.autocomplete.matches = [];
472
+ this.render();
473
+ }
474
+ return;
475
+ }
476
+ // Enter
477
+ if (isEnter) {
478
+ this.handleEnter();
479
+ return;
480
+ }
481
+ // Tab - accept autocomplete or insert spaces
482
+ if (isTab) {
483
+ this.handleTab();
484
+ return;
485
+ }
486
+ // Arrow keys
487
+ if (isUpArrow) {
488
+ this.handleArrowUp();
489
+ return;
490
+ }
491
+ if (isDownArrow) {
492
+ this.handleArrowDown();
493
+ return;
494
+ }
495
+ if (isLeftArrow) {
496
+ this.handleArrowLeft();
497
+ return;
498
+ }
499
+ if (isRightArrow) {
500
+ this.handleArrowRight();
501
+ return;
502
+ }
503
+ // Word navigation
504
+ if (isOptionLeft) {
505
+ this.handleWordLeft();
506
+ return;
507
+ }
508
+ if (isOptionRight) {
509
+ this.handleWordRight();
510
+ return;
511
+ }
512
+ // Home/End
513
+ if (isHome) {
514
+ this.state.cursorPos = 0;
515
+ this.render();
516
+ return;
517
+ }
518
+ if (isEnd) {
519
+ this.state.cursorPos = this.state.lines[this.state.currentLine].length;
520
+ this.render();
521
+ return;
522
+ }
523
+ // Backspace
524
+ if (isBackspace) {
525
+ this.handleBackspace();
526
+ return;
527
+ }
528
+ // Regular character(s) - handles typing and paste
529
+ if (key.length >= 1) {
530
+ const printable = key
531
+ .split('')
532
+ .filter((c) => c.charCodeAt(0) >= 32)
533
+ .join('');
534
+ if (printable.length > 0) {
535
+ const line = this.state.lines[this.state.currentLine];
536
+ this.state.lines[this.state.currentLine] =
537
+ line.slice(0, this.state.cursorPos) + printable + line.slice(this.state.cursorPos);
538
+ this.state.cursorPos += printable.length;
539
+ this.historyIndex = -1;
540
+ this.updateAutocomplete();
541
+ this.render();
542
+ }
543
+ }
544
+ }
545
+ handleEnter() {
546
+ const currentLine = this.state.lines[this.state.currentLine];
547
+ // Continuation with backslash
548
+ if (currentLine.endsWith('\\')) {
549
+ this.state.lines[this.state.currentLine] = currentLine.slice(0, -1);
550
+ this.state.lines.push('');
551
+ this.state.currentLine++;
552
+ this.state.cursorPos = 0;
553
+ this.autocomplete.active = false;
554
+ this.clearDropdown();
555
+ this.render();
556
+ return;
557
+ }
558
+ // Autocomplete selection
559
+ if (this.autocomplete.active && this.autocomplete.matches.length > 0) {
560
+ this.state.lines[0] = this.autocomplete.matches[this.autocomplete.selectedIndex].command;
561
+ this.state.cursorPos = this.state.lines[0].length;
562
+ }
563
+ const input = this.getValue();
564
+ // Check if it's a command
565
+ if (input.startsWith('/')) {
566
+ const parts = input.slice(1).split(/\s+/);
567
+ const command = parts[0];
568
+ const args = parts.slice(1).join(' ');
569
+ this.finishInput({ action: 'command', command, args });
570
+ }
571
+ else {
572
+ this.finishInput({ action: 'submit', value: input });
573
+ }
574
+ }
575
+ handleTab() {
576
+ if (this.autocomplete.active && this.autocomplete.matches.length > 0) {
577
+ this.state.lines[this.state.currentLine] =
578
+ this.autocomplete.matches[this.autocomplete.selectedIndex].command;
579
+ this.state.cursorPos = this.state.lines[this.state.currentLine].length;
580
+ this.autocomplete.active = false;
581
+ this.autocomplete.matches = [];
582
+ this.clearDropdown();
583
+ }
584
+ else {
585
+ // Insert 2 spaces
586
+ const line = this.state.lines[this.state.currentLine];
587
+ this.state.lines[this.state.currentLine] =
588
+ line.slice(0, this.state.cursorPos) + ' ' + line.slice(this.state.cursorPos);
589
+ this.state.cursorPos += 2;
590
+ this.historyIndex = -1;
591
+ this.updateAutocomplete();
592
+ }
593
+ this.render();
594
+ }
595
+ handleArrowUp() {
596
+ // Autocomplete navigation
597
+ if (this.autocomplete.active && this.autocomplete.selectedIndex > 0) {
598
+ this.autocomplete.selectedIndex--;
599
+ this.render();
600
+ return;
601
+ }
602
+ const termWidth = terminal.getTerminalWidth();
603
+ const currentLinePromptLen = this.state.currentLine === 0 ? this.promptLen : 5;
604
+ const cursorAbsPos = currentLinePromptLen + this.state.cursorPos;
605
+ const currentPhysicalRow = Math.floor(cursorAbsPos / termWidth);
606
+ const cursorColInRow = cursorAbsPos % termWidth;
607
+ // Navigate within wrapped line
608
+ if (currentPhysicalRow > 0) {
609
+ const newAbsPos = (currentPhysicalRow - 1) * termWidth + cursorColInRow;
610
+ this.state.cursorPos = Math.max(0, newAbsPos - currentLinePromptLen);
611
+ this.state.cursorPos = Math.min(this.state.cursorPos, this.state.lines[this.state.currentLine].length);
612
+ this.render();
613
+ return;
614
+ }
615
+ // Navigate to previous logical line
616
+ if (this.state.currentLine > 0) {
617
+ this.state.currentLine--;
618
+ const prevLinePromptLen = this.state.currentLine === 0 ? this.promptLen : 5;
619
+ const prevLineLen = this.state.lines[this.state.currentLine].length;
620
+ const prevLinePhysical = calcPhysicalLines(this.state.lines[this.state.currentLine], prevLinePromptLen, termWidth);
621
+ const lastRowStart = (prevLinePhysical - 1) * termWidth;
622
+ const targetAbsPos = lastRowStart + cursorColInRow;
623
+ this.state.cursorPos = Math.max(0, targetAbsPos - prevLinePromptLen);
624
+ this.state.cursorPos = Math.min(this.state.cursorPos, prevLineLen);
625
+ this.render();
626
+ return;
627
+ }
628
+ // History navigation (at top of input)
629
+ if (!this.autocomplete.active && this.history.length > 0) {
630
+ if (this.historyIndex === -1) {
631
+ this.savedInput = this.getValue();
632
+ }
633
+ if (this.historyIndex < this.history.length - 1) {
634
+ this.historyIndex++;
635
+ const historyEntry = this.history[this.history.length - 1 - this.historyIndex];
636
+ this.state.lines = [historyEntry];
637
+ this.state.currentLine = 0;
638
+ this.state.cursorPos = historyEntry.length;
639
+ this.render();
640
+ }
641
+ }
642
+ }
643
+ handleArrowDown() {
644
+ // Autocomplete navigation
645
+ if (this.autocomplete.active &&
646
+ this.autocomplete.selectedIndex < this.autocomplete.matches.length - 1 &&
647
+ this.autocomplete.selectedIndex < MAX_VISIBLE_COMMANDS - 1) {
648
+ this.autocomplete.selectedIndex++;
649
+ this.render();
650
+ return;
651
+ }
652
+ const termWidth = terminal.getTerminalWidth();
653
+ const currentLinePromptLen = this.state.currentLine === 0 ? this.promptLen : 5;
654
+ const cursorAbsPos = currentLinePromptLen + this.state.cursorPos;
655
+ const currentPhysicalRow = Math.floor(cursorAbsPos / termWidth);
656
+ const cursorColInRow = cursorAbsPos % termWidth;
657
+ const currentLinePhysical = calcPhysicalLines(this.state.lines[this.state.currentLine], currentLinePromptLen, termWidth);
658
+ // Navigate within wrapped line
659
+ if (currentPhysicalRow < currentLinePhysical - 1) {
660
+ const newAbsPos = (currentPhysicalRow + 1) * termWidth + cursorColInRow;
661
+ this.state.cursorPos = Math.max(0, newAbsPos - currentLinePromptLen);
662
+ this.state.cursorPos = Math.min(this.state.cursorPos, this.state.lines[this.state.currentLine].length);
663
+ this.render();
664
+ return;
665
+ }
666
+ // Navigate to next logical line
667
+ if (this.state.currentLine < this.state.lines.length - 1) {
668
+ this.state.currentLine++;
669
+ const nextLinePromptLen = this.state.currentLine === 0 ? this.promptLen : 5;
670
+ const nextLineLen = this.state.lines[this.state.currentLine].length;
671
+ this.state.cursorPos = Math.max(0, cursorColInRow - nextLinePromptLen);
672
+ this.state.cursorPos = Math.min(this.state.cursorPos, nextLineLen);
673
+ this.render();
674
+ return;
675
+ }
676
+ // History forward
677
+ if (this.historyIndex >= 0) {
678
+ this.historyIndex--;
679
+ if (this.historyIndex === -1) {
680
+ this.state.lines = this.savedInput.split('\n');
681
+ this.state.currentLine = this.state.lines.length - 1;
682
+ this.state.cursorPos = this.state.lines[this.state.currentLine].length;
683
+ }
684
+ else {
685
+ const historyEntry = this.history[this.history.length - 1 - this.historyIndex];
686
+ this.state.lines = [historyEntry];
687
+ this.state.currentLine = 0;
688
+ this.state.cursorPos = historyEntry.length;
689
+ }
690
+ this.updateAutocomplete();
691
+ this.render();
692
+ }
693
+ }
694
+ handleArrowLeft() {
695
+ if (this.state.cursorPos > 0) {
696
+ this.state.cursorPos--;
697
+ terminal.write('\x1B[D');
698
+ }
699
+ else if (this.state.currentLine > 0) {
700
+ this.state.currentLine--;
701
+ this.state.cursorPos = this.state.lines[this.state.currentLine].length;
702
+ this.render();
703
+ }
704
+ }
705
+ handleArrowRight() {
706
+ if (this.state.cursorPos < this.state.lines[this.state.currentLine].length) {
707
+ this.state.cursorPos++;
708
+ terminal.write('\x1B[C');
709
+ }
710
+ else if (this.state.currentLine < this.state.lines.length - 1) {
711
+ this.state.currentLine++;
712
+ this.state.cursorPos = 0;
713
+ this.render();
714
+ }
715
+ }
716
+ handleWordLeft() {
717
+ const line = this.state.lines[this.state.currentLine];
718
+ if (this.state.cursorPos > 0) {
719
+ let pos = this.state.cursorPos;
720
+ while (pos > 0 && line[pos - 1] === ' ')
721
+ pos--;
722
+ while (pos > 0 && line[pos - 1] !== ' ')
723
+ pos--;
724
+ this.state.cursorPos = pos;
725
+ this.render();
726
+ }
727
+ else if (this.state.currentLine > 0) {
728
+ this.state.currentLine--;
729
+ this.state.cursorPos = this.state.lines[this.state.currentLine].length;
730
+ this.render();
731
+ }
732
+ }
733
+ handleWordRight() {
734
+ const line = this.state.lines[this.state.currentLine];
735
+ if (this.state.cursorPos < line.length) {
736
+ let pos = this.state.cursorPos;
737
+ while (pos < line.length && line[pos] !== ' ')
738
+ pos++;
739
+ while (pos < line.length && line[pos] === ' ')
740
+ pos++;
741
+ this.state.cursorPos = pos;
742
+ this.render();
743
+ }
744
+ else if (this.state.currentLine < this.state.lines.length - 1) {
745
+ this.state.currentLine++;
746
+ this.state.cursorPos = 0;
747
+ this.render();
748
+ }
749
+ }
750
+ handleBackspace() {
751
+ this.historyIndex = -1;
752
+ if (this.state.cursorPos > 0) {
753
+ const line = this.state.lines[this.state.currentLine];
754
+ this.state.lines[this.state.currentLine] =
755
+ line.slice(0, this.state.cursorPos - 1) + line.slice(this.state.cursorPos);
756
+ this.state.cursorPos--;
757
+ this.updateAutocomplete();
758
+ this.render();
759
+ }
760
+ else if (this.state.currentLine > 0) {
761
+ const currentLine = this.state.lines[this.state.currentLine];
762
+ const prevLine = this.state.lines[this.state.currentLine - 1];
763
+ this.state.lines[this.state.currentLine - 1] = prevLine + currentLine;
764
+ this.state.lines.splice(this.state.currentLine, 1);
765
+ this.state.currentLine--;
766
+ this.state.cursorPos = prevLine.length;
767
+ this.updateAutocomplete();
768
+ this.render();
769
+ }
770
+ }
771
+ finishInput(result) {
772
+ // Clear display
773
+ this.clearDropdown();
774
+ if (this.linesAboveCursor > 0) {
775
+ terminal.moveCursorToLineStart();
776
+ terminal.moveCursorUp(this.linesAboveCursor);
777
+ }
778
+ terminal.clearToEndOfScreen();
779
+ // Print clean input (without separators)
780
+ for (let i = 0; i < this.state.lines.length; i++) {
781
+ const linePrompt = i === 0 ? this.prompt : pc.dim(' \\ ');
782
+ terminal.write(linePrompt + this.state.lines[i]);
783
+ if (i < this.state.lines.length - 1) {
784
+ terminal.write('\n');
785
+ }
786
+ }
787
+ terminal.writeLine('');
788
+ // Save to history if submitting
789
+ if (result.action === 'submit' || result.action === 'command') {
790
+ const input = this.getValue();
791
+ this.addToHistory(input);
792
+ }
793
+ // Stop and resolve
794
+ this.stop();
795
+ if (this.resolveInput) {
796
+ this.resolveInput(result);
797
+ this.resolveInput = null;
798
+ }
799
+ }
800
+ }