@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.
- package/README.md +110 -0
- package/dist/agent.d.ts +62 -0
- package/dist/agent.js +317 -0
- package/dist/agents/registry.d.ts +66 -0
- package/dist/agents/registry.js +238 -0
- package/dist/agents/types.d.ts +40 -0
- package/dist/agents/types.js +94 -0
- package/dist/commands/custom-registry.d.ts +69 -0
- package/dist/commands/custom-registry.js +246 -0
- package/dist/commands/index.d.ts +7 -0
- package/dist/commands/index.js +7 -0
- package/dist/commands/types.d.ts +31 -0
- package/dist/commands/types.js +26 -0
- package/dist/commands.d.ts +63 -0
- package/dist/commands.js +324 -0
- package/dist/db/index.d.ts +42 -0
- package/dist/db/index.js +146 -0
- package/dist/db/repositories/document-repository.d.ts +63 -0
- package/dist/db/repositories/document-repository.js +184 -0
- package/dist/db/repositories/index.d.ts +9 -0
- package/dist/db/repositories/index.js +6 -0
- package/dist/db/repositories/project-repository.d.ts +132 -0
- package/dist/db/repositories/project-repository.js +337 -0
- package/dist/db/repositories/work-item-repository.d.ts +115 -0
- package/dist/db/repositories/work-item-repository.js +389 -0
- package/dist/db/schema.d.ts +83 -0
- package/dist/db/schema.js +143 -0
- package/dist/debug.d.ts +8 -0
- package/dist/debug.js +48 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +348 -0
- package/dist/index.old.d.ts +7 -0
- package/dist/index.old.js +1014 -0
- package/dist/repl.d.ts +121 -0
- package/dist/repl.js +1878 -0
- package/dist/settings/index.d.ts +80 -0
- package/dist/settings/index.js +195 -0
- package/dist/shared-handlers.d.ts +63 -0
- package/dist/shared-handlers.js +57 -0
- package/dist/slash-autocomplete.d.ts +41 -0
- package/dist/slash-autocomplete.js +638 -0
- package/dist/state.d.ts +75 -0
- package/dist/state.js +130 -0
- package/dist/tabbed-menu.d.ts +11 -0
- package/dist/tabbed-menu.js +328 -0
- package/dist/templates/backlog-md.d.ts +7 -0
- package/dist/templates/backlog-md.js +94 -0
- package/dist/templates/claude-md.d.ts +7 -0
- package/dist/templates/claude-md.js +189 -0
- package/dist/templates/coding-standards.d.ts +7 -0
- package/dist/templates/coding-standards.js +299 -0
- package/dist/templates/compilr-md.d.ts +7 -0
- package/dist/templates/compilr-md.js +189 -0
- package/dist/templates/config-json.d.ts +38 -0
- package/dist/templates/config-json.js +39 -0
- package/dist/templates/gitignore.d.ts +7 -0
- package/dist/templates/gitignore.js +85 -0
- package/dist/templates/index.d.ts +19 -0
- package/dist/templates/index.js +302 -0
- package/dist/templates/package-json.d.ts +7 -0
- package/dist/templates/package-json.js +111 -0
- package/dist/templates/readme-md.d.ts +7 -0
- package/dist/templates/readme-md.js +161 -0
- package/dist/templates/tsconfig.d.ts +7 -0
- package/dist/templates/tsconfig.js +61 -0
- package/dist/templates/types.d.ts +33 -0
- package/dist/templates/types.js +24 -0
- package/dist/test-autocomplete.d.ts +7 -0
- package/dist/test-autocomplete.js +85 -0
- package/dist/test-tabbed-menu.d.ts +7 -0
- package/dist/test-tabbed-menu.js +25 -0
- package/dist/themes/colors.d.ts +49 -0
- package/dist/themes/colors.js +135 -0
- package/dist/themes/index.d.ts +23 -0
- package/dist/themes/index.js +24 -0
- package/dist/themes/registry.d.ts +60 -0
- package/dist/themes/registry.js +195 -0
- package/dist/themes/types.d.ts +82 -0
- package/dist/themes/types.js +7 -0
- package/dist/tool-selector.d.ts +71 -0
- package/dist/tool-selector.js +184 -0
- package/dist/tools/ask-user-simple.d.ts +19 -0
- package/dist/tools/ask-user-simple.js +86 -0
- package/dist/tools/ask-user.d.ts +32 -0
- package/dist/tools/ask-user.js +113 -0
- package/dist/tools/backlog.d.ts +53 -0
- package/dist/tools/backlog.js +709 -0
- package/dist/tools.d.ts +15 -0
- package/dist/tools.js +121 -0
- package/dist/ui/agents-overlay.d.ts +12 -0
- package/dist/ui/agents-overlay.js +501 -0
- package/dist/ui/arch-type-overlay.d.ts +20 -0
- package/dist/ui/arch-type-overlay.js +229 -0
- package/dist/ui/ask-user-overlay.d.ts +26 -0
- package/dist/ui/ask-user-overlay.js +647 -0
- package/dist/ui/ask-user-simple-overlay.d.ts +25 -0
- package/dist/ui/ask-user-simple-overlay.js +242 -0
- package/dist/ui/backlog-overlay.d.ts +17 -0
- package/dist/ui/backlog-overlay.js +786 -0
- package/dist/ui/commands-overlay.d.ts +11 -0
- package/dist/ui/commands-overlay.js +410 -0
- package/dist/ui/config-overlay.d.ts +34 -0
- package/dist/ui/config-overlay.js +977 -0
- package/dist/ui/conversation.d.ts +82 -0
- package/dist/ui/conversation.js +508 -0
- package/dist/ui/diff.d.ts +38 -0
- package/dist/ui/diff.js +182 -0
- package/dist/ui/ephemeral.d.ts +111 -0
- package/dist/ui/ephemeral.js +413 -0
- package/dist/ui/file-autocomplete.d.ts +45 -0
- package/dist/ui/file-autocomplete.js +237 -0
- package/dist/ui/footer.d.ts +153 -0
- package/dist/ui/footer.js +422 -0
- package/dist/ui/index.d.ts +12 -0
- package/dist/ui/index.js +15 -0
- package/dist/ui/init-overlay.d.ts +24 -0
- package/dist/ui/init-overlay.js +525 -0
- package/dist/ui/input-prompt-v2.d.ts +179 -0
- package/dist/ui/input-prompt-v2.js +991 -0
- package/dist/ui/input-prompt.d.ts +97 -0
- package/dist/ui/input-prompt.js +800 -0
- package/dist/ui/iteration-limit-overlay.d.ts +21 -0
- package/dist/ui/iteration-limit-overlay.js +150 -0
- package/dist/ui/keys-overlay.d.ts +14 -0
- package/dist/ui/keys-overlay.js +181 -0
- package/dist/ui/model-warning-overlay.d.ts +30 -0
- package/dist/ui/model-warning-overlay.js +171 -0
- package/dist/ui/overlay-controller.d.ts +25 -0
- package/dist/ui/overlay-controller.js +35 -0
- package/dist/ui/overlays.d.ts +47 -0
- package/dist/ui/overlays.js +627 -0
- package/dist/ui/permission-overlay.d.ts +16 -0
- package/dist/ui/permission-overlay.js +494 -0
- package/dist/ui/terminal.d.ts +117 -0
- package/dist/ui/terminal.js +237 -0
- package/dist/ui/todo-zone.d.ts +112 -0
- package/dist/ui/todo-zone.js +353 -0
- package/dist/ui/tools-overlay.d.ts +26 -0
- package/dist/ui/tools-overlay.js +278 -0
- package/dist/ui/tutorial-overlay.d.ts +10 -0
- package/dist/ui/tutorial-overlay.js +936 -0
- package/dist/ui/types.d.ts +103 -0
- package/dist/ui/types.js +33 -0
- package/dist/utils/credentials.d.ts +55 -0
- package/dist/utils/credentials.js +268 -0
- package/dist/utils/model-tiers.d.ts +37 -0
- package/dist/utils/model-tiers.js +118 -0
- package/dist/utils/project-memory.d.ts +47 -0
- package/dist/utils/project-memory.js +117 -0
- package/dist/utils/project-status.d.ts +56 -0
- package/dist/utils/project-status.js +237 -0
- 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
|
+
}
|