vibecodingmachine-cli 2026.1.29-713 → 2026.2.20-423
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/bin/vibecodingmachine.js +124 -0
- package/package.json +3 -2
- package/src/commands/agents-check.js +69 -0
- package/src/commands/auto-direct.js +930 -145
- package/src/commands/auto.js +26 -4
- package/src/commands/ide.js +2 -1
- package/src/commands/requirements.js +23 -27
- package/src/utils/auto-mode.js +4 -1
- package/src/utils/cline-js-handler.js +218 -0
- package/src/utils/config.js +22 -0
- package/src/utils/display-formatters-complete.js +229 -0
- package/src/utils/display-formatters-extracted.js +219 -0
- package/src/utils/display-formatters.js +157 -0
- package/src/utils/feedback-handler.js +143 -0
- package/src/utils/ide-detection-complete.js +126 -0
- package/src/utils/ide-detection-extracted.js +116 -0
- package/src/utils/ide-detection.js +124 -0
- package/src/utils/interactive-backup.js +5664 -0
- package/src/utils/interactive-broken.js +280 -0
- package/src/utils/interactive.js +31 -5534
- package/src/utils/provider-checker.js +410 -0
- package/src/utils/provider-manager.js +251 -0
- package/src/utils/provider-registry.js +18 -9
- package/src/utils/requirement-actions.js +884 -0
- package/src/utils/requirements-navigator.js +585 -0
- package/src/utils/rui-trui-adapter.js +311 -0
- package/src/utils/simple-trui.js +204 -0
- package/src/utils/status-helpers-extracted.js +125 -0
- package/src/utils/status-helpers.js +107 -0
- package/src/utils/trui-debug.js +261 -0
- package/src/utils/trui-feedback.js +133 -0
- package/src/utils/trui-nav-agents.js +119 -0
- package/src/utils/trui-nav-requirements.js +268 -0
- package/src/utils/trui-nav-settings.js +157 -0
- package/src/utils/trui-nav-specifications.js +139 -0
- package/src/utils/trui-navigation.js +303 -0
- package/src/utils/trui-provider-manager.js +182 -0
- package/src/utils/trui-quick-menu.js +365 -0
- package/src/utils/trui-req-actions.js +372 -0
- package/src/utils/trui-req-tree.js +534 -0
- package/src/utils/trui-specifications.js +359 -0
- package/src/utils/trui-text-editor.js +350 -0
- package/src/utils/trui-windsurf.js +336 -0
- package/src/utils/welcome-screen-extracted.js +135 -0
- package/src/utils/welcome-screen.js +134 -0
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TRUI Specifications List
|
|
3
|
+
*
|
|
4
|
+
* Shows a list of all specifications from the specs directory.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const chalk = require('chalk');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const { showQuickMenu } = require('./trui-quick-menu');
|
|
11
|
+
const { debugLogger } = require('./trui-debug');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Extract Phase headers from a spec directory's tasks.md.
|
|
15
|
+
* Matches "## Phase N: ..." lines.
|
|
16
|
+
* Falls back to User Story headers from spec.md if tasks.md is absent.
|
|
17
|
+
* Returns an array of title strings.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Extract phases from a spec directory's tasks.md, including per-phase checkbox counts.
|
|
21
|
+
* Falls back to User Story headers from spec.md if tasks.md is absent.
|
|
22
|
+
* Returns an array of { title, done, total, pct } objects.
|
|
23
|
+
*/
|
|
24
|
+
function extractPhases(specPath) {
|
|
25
|
+
try {
|
|
26
|
+
// Primary: tasks.md phases with checkbox roll-up
|
|
27
|
+
const tasksFile = path.join(specPath, 'tasks.md');
|
|
28
|
+
if (fs.existsSync(tasksFile)) {
|
|
29
|
+
const content = fs.readFileSync(tasksFile, 'utf8');
|
|
30
|
+
const phases = [];
|
|
31
|
+
let current = null;
|
|
32
|
+
|
|
33
|
+
for (const line of content.split('\n')) {
|
|
34
|
+
const trimmed = line.trim();
|
|
35
|
+
if (/^## Phase \d+/i.test(trimmed)) {
|
|
36
|
+
if (current) phases.push(current);
|
|
37
|
+
current = { title: trimmed.replace(/^##\s*/, ''), done: 0, total: 0 };
|
|
38
|
+
} else if (current) {
|
|
39
|
+
if (/^- \[x\]/i.test(trimmed)) { current.done++; current.total++; }
|
|
40
|
+
else if (/^- \[ \]/.test(trimmed)) { current.total++; }
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (current) phases.push(current);
|
|
44
|
+
|
|
45
|
+
if (phases.length) {
|
|
46
|
+
return phases.map(p => ({ ...p, pct: p.total > 0 ? Math.round((p.done / p.total) * 100) : 0 }));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// Fallback: User Story headers from spec.md (no task counts available)
|
|
50
|
+
const specFile = path.join(specPath, 'spec.md');
|
|
51
|
+
if (fs.existsSync(specFile)) {
|
|
52
|
+
const content = fs.readFileSync(specFile, 'utf8');
|
|
53
|
+
const stories = [];
|
|
54
|
+
for (const line of content.split('\n')) {
|
|
55
|
+
const trimmed = line.trim();
|
|
56
|
+
if (/^### (?:User Story\b|US-\d+)/i.test(trimmed)) {
|
|
57
|
+
stories.push({ title: trimmed.replace(/^###\s*/, ''), done: 0, total: 0, pct: 0 });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return stories;
|
|
61
|
+
}
|
|
62
|
+
return [];
|
|
63
|
+
} catch (_) {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Legacy alias
|
|
69
|
+
const extractUserStories = extractPhases;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Count checkboxes in a markdown file.
|
|
73
|
+
* Returns { done, total } counts.
|
|
74
|
+
*/
|
|
75
|
+
function _countCheckboxes(filePath) {
|
|
76
|
+
try {
|
|
77
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
78
|
+
const totalMatches = content.match(/^- \[[ x]\]/gmi) || [];
|
|
79
|
+
const doneMatches = content.match(/^- \[x\]/gmi) || [];
|
|
80
|
+
return { done: doneMatches.length, total: totalMatches.length };
|
|
81
|
+
} catch (_) {
|
|
82
|
+
return { done: 0, total: 0 };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get list of all specs from specs directory using core helpers
|
|
88
|
+
*/
|
|
89
|
+
async function getSpecsList() {
|
|
90
|
+
try {
|
|
91
|
+
const { getAllSpecifications } = require('vibecodingmachine-core');
|
|
92
|
+
|
|
93
|
+
// Find the repo root by looking for .git directory
|
|
94
|
+
let currentDir = process.cwd();
|
|
95
|
+
while (currentDir !== path.dirname(currentDir)) {
|
|
96
|
+
if (fs.existsSync(path.join(currentDir, '.git'))) {
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
currentDir = path.dirname(currentDir);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Get specs using core function
|
|
103
|
+
const specs = await getAllSpecifications(currentDir);
|
|
104
|
+
if (!specs || !Array.isArray(specs)) {
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return specs.map(spec => {
|
|
109
|
+
// Count checkboxes across all markdown files in the spec directory
|
|
110
|
+
let totalDone = 0;
|
|
111
|
+
let totalTasks = 0;
|
|
112
|
+
try {
|
|
113
|
+
const specDir = spec.path || path.join(currentDir, 'specs', spec.directory);
|
|
114
|
+
const mdFiles = fs.readdirSync(specDir).filter(f => f.endsWith('.md'));
|
|
115
|
+
for (const mdFile of mdFiles) {
|
|
116
|
+
const counts = _countCheckboxes(path.join(specDir, mdFile));
|
|
117
|
+
totalDone += counts.done;
|
|
118
|
+
totalTasks += counts.total;
|
|
119
|
+
}
|
|
120
|
+
} catch (_) {}
|
|
121
|
+
|
|
122
|
+
const pct = totalTasks > 0 ? Math.round((totalDone / totalTasks) * 100) : 0;
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
id: spec.directory,
|
|
126
|
+
disabled: spec.directory.startsWith('DISABLED-'),
|
|
127
|
+
taskDone: totalDone,
|
|
128
|
+
taskTotal: totalTasks,
|
|
129
|
+
pct,
|
|
130
|
+
path: spec.path
|
|
131
|
+
};
|
|
132
|
+
}).sort((a, b) => {
|
|
133
|
+
// Sort by directory name with DISABLED- prefix stripped so disabled specs
|
|
134
|
+
// stay in their original position rather than moving to the end.
|
|
135
|
+
const nameA = a.id.replace(/^DISABLED-/, '');
|
|
136
|
+
const nameB = b.id.replace(/^DISABLED-/, '');
|
|
137
|
+
return nameA.localeCompare(nameB);
|
|
138
|
+
});
|
|
139
|
+
} catch (error) {
|
|
140
|
+
debugLogger.error('Failed to get specs list', { error: error.message });
|
|
141
|
+
return [];
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Show specifications list menu
|
|
147
|
+
*/
|
|
148
|
+
async function showSpecificationsList() {
|
|
149
|
+
debugLogger.info('Opening specifications list');
|
|
150
|
+
|
|
151
|
+
console.clear();
|
|
152
|
+
console.log(chalk.cyan('\n📋 TODO SPECIFICATIONS\n'));
|
|
153
|
+
console.log(chalk.gray('Available specifications from specs directory\n'));
|
|
154
|
+
|
|
155
|
+
const specs = await getSpecsList();
|
|
156
|
+
|
|
157
|
+
if (specs.length === 0) {
|
|
158
|
+
console.log(chalk.yellow('No specifications found in specs directory.'));
|
|
159
|
+
console.log(chalk.gray('Create a new specification with: vcm create spec <name>'));
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Build menu items
|
|
164
|
+
const items = [];
|
|
165
|
+
|
|
166
|
+
// Add each spec as a menu item
|
|
167
|
+
specs.forEach((spec, index) => {
|
|
168
|
+
let progressStr = '';
|
|
169
|
+
if (spec.taskTotal > 0) {
|
|
170
|
+
progressStr = chalk.gray(` [${spec.pct}%, ${spec.taskDone}/${spec.taskTotal} tasks complete]`);
|
|
171
|
+
}
|
|
172
|
+
items.push({
|
|
173
|
+
type: 'action',
|
|
174
|
+
name: `${spec.id}${progressStr}`,
|
|
175
|
+
value: `spec:${spec.id}`
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Add blank separator
|
|
180
|
+
items.push({ type: 'blank', name: '', value: 'blank' });
|
|
181
|
+
|
|
182
|
+
// Add actions
|
|
183
|
+
items.push({ type: 'action', name: '[+ Create New Specification]', value: 'create-spec' });
|
|
184
|
+
items.push({ type: 'action', name: 'Back to Main Menu', value: '__cancel__' });
|
|
185
|
+
|
|
186
|
+
// Show menu
|
|
187
|
+
const result = await showQuickMenu(items);
|
|
188
|
+
|
|
189
|
+
if (result.value === '__cancel__') {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (result.value === 'create-spec') {
|
|
194
|
+
await createNewSpecification();
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Handle spec selection
|
|
199
|
+
if (result.value.startsWith('spec:')) {
|
|
200
|
+
const specId = result.value.substring(5);
|
|
201
|
+
await showSpecificationDetails(specId);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Get status icon for specification
|
|
208
|
+
*/
|
|
209
|
+
function getStatusIcon(status) {
|
|
210
|
+
switch (status) {
|
|
211
|
+
case 'completed': return '✅';
|
|
212
|
+
case 'in-progress': return '🔄';
|
|
213
|
+
case 'todo': return '📋';
|
|
214
|
+
default: return '❓';
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Show specification details
|
|
220
|
+
*/
|
|
221
|
+
async function showSpecificationDetails(specId) {
|
|
222
|
+
const specDir = path.join(process.cwd(), 'specs', specId);
|
|
223
|
+
const specFile = path.join(specDir, 'spec.md');
|
|
224
|
+
|
|
225
|
+
if (!fs.existsSync(specFile)) {
|
|
226
|
+
console.log(chalk.red(`Specification ${specId} not found`));
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
console.clear();
|
|
231
|
+
console.log(chalk.cyan(`\n📋 Specification: ${specId}\n`));
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
const content = fs.readFileSync(specFile, 'utf8');
|
|
235
|
+
console.log(content);
|
|
236
|
+
} catch (error) {
|
|
237
|
+
console.log(chalk.red('Error reading specification: ' + error.message));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
console.log(chalk.gray('\n─'.repeat(50)));
|
|
241
|
+
console.log(chalk.gray('Press Enter to return to specifications list...'));
|
|
242
|
+
|
|
243
|
+
// Wait for user input - specifically Enter key
|
|
244
|
+
const readline = require('readline');
|
|
245
|
+
readline.emitKeypressEvents(process.stdin);
|
|
246
|
+
if (process.stdin.isTTY && process.stdin.setRawMode) {
|
|
247
|
+
process.stdin.setRawMode(true);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return new Promise(resolve => {
|
|
251
|
+
const onKeypress = (str, key) => {
|
|
252
|
+
// Only accept Enter key or Escape
|
|
253
|
+
if (key && (key.name === 'return' || key.name === 'escape')) {
|
|
254
|
+
cleanup();
|
|
255
|
+
resolve();
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const cleanup = () => {
|
|
261
|
+
if (process.stdin.isTTY && process.stdin.setRawMode) {
|
|
262
|
+
process.stdin.setRawMode(false);
|
|
263
|
+
}
|
|
264
|
+
process.stdin.removeListener('keypress', onKeypress);
|
|
265
|
+
process.stdin.pause();
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
process.stdin.on('keypress', onKeypress);
|
|
269
|
+
process.stdin.resume();
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Create new specification
|
|
275
|
+
*/
|
|
276
|
+
async function createNewSpecification() {
|
|
277
|
+
const inquirer = require('inquirer');
|
|
278
|
+
|
|
279
|
+
console.log(chalk.cyan('\n📝 Create New Specification\n'));
|
|
280
|
+
|
|
281
|
+
const { specName } = await inquirer.prompt([
|
|
282
|
+
{
|
|
283
|
+
type: 'input',
|
|
284
|
+
name: 'specName',
|
|
285
|
+
message: 'Specification name (e.g., 008-new-feature):',
|
|
286
|
+
validate: (input) => {
|
|
287
|
+
if (!input.trim()) return 'Specification name is required';
|
|
288
|
+
if (!/^\d{3}-.+$/.test(input.trim())) {
|
|
289
|
+
return 'Please use format: ###-description (e.g., 008-new-feature)';
|
|
290
|
+
}
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
]);
|
|
295
|
+
|
|
296
|
+
const specDir = path.join(process.cwd(), 'specs', specName.trim());
|
|
297
|
+
|
|
298
|
+
if (fs.existsSync(specDir)) {
|
|
299
|
+
console.log(chalk.yellow(`Specification ${specName} already exists`));
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Create spec directory
|
|
304
|
+
fs.mkdirSync(specDir, { recursive: true });
|
|
305
|
+
|
|
306
|
+
// Create spec.md with template
|
|
307
|
+
const template = `# ${specName}
|
|
308
|
+
|
|
309
|
+
## Background
|
|
310
|
+
|
|
311
|
+
<!-- Describe the context and motivation for this feature -->
|
|
312
|
+
|
|
313
|
+
## User Stories
|
|
314
|
+
|
|
315
|
+
### US-001 — [Story Title] (P1)
|
|
316
|
+
As a [user type], I want [goal] so that [benefit].
|
|
317
|
+
|
|
318
|
+
**Acceptance Criteria:**
|
|
319
|
+
- [ ] Criterion 1
|
|
320
|
+
- [ ] Criterion 2
|
|
321
|
+
- [ ] Criterion 3
|
|
322
|
+
|
|
323
|
+
## Functional Requirements
|
|
324
|
+
|
|
325
|
+
### FR-001
|
|
326
|
+
[Requirement description]
|
|
327
|
+
|
|
328
|
+
### FR-002
|
|
329
|
+
[Requirement description]
|
|
330
|
+
|
|
331
|
+
## Success Criteria
|
|
332
|
+
|
|
333
|
+
### SC-001
|
|
334
|
+
[Measurable success criterion]
|
|
335
|
+
|
|
336
|
+
## Edge Cases
|
|
337
|
+
|
|
338
|
+
### EC-001
|
|
339
|
+
[Edge case description and handling]
|
|
340
|
+
|
|
341
|
+
## Implementation Notes
|
|
342
|
+
|
|
343
|
+
[Technical implementation details and considerations]
|
|
344
|
+
`;
|
|
345
|
+
|
|
346
|
+
fs.writeFileSync(path.join(specDir, 'spec.md'), template);
|
|
347
|
+
|
|
348
|
+
console.log(chalk.green(`\n✓ Specification ${specName} created successfully`));
|
|
349
|
+
console.log(chalk.gray(`Location: ${specDir}/spec.md`));
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
module.exports = {
|
|
353
|
+
showSpecificationsList,
|
|
354
|
+
getSpecsList,
|
|
355
|
+
showSpecificationDetails,
|
|
356
|
+
createNewSpecification,
|
|
357
|
+
extractPhases,
|
|
358
|
+
extractUserStories, // legacy alias
|
|
359
|
+
};
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TRUI Multi-line Text Editor
|
|
3
|
+
*
|
|
4
|
+
* Provides a simple multi-line text editor for requirement descriptions,
|
|
5
|
+
* clarification responses, and feedback text.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const chalk = require('chalk');
|
|
9
|
+
const readline = require('readline');
|
|
10
|
+
const { debugLogger } = require('./trui-debug');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Multi-line text editor with basic navigation
|
|
14
|
+
*/
|
|
15
|
+
class MultiLineEditor {
|
|
16
|
+
constructor(options = {}) {
|
|
17
|
+
this.lines = options.initialText ? options.initialText.split('\n') : [''];
|
|
18
|
+
this.cursorX = 0;
|
|
19
|
+
this.cursorY = 0;
|
|
20
|
+
this.scrollY = 0;
|
|
21
|
+
this.maxLines = options.maxLines || 20;
|
|
22
|
+
this.maxWidth = options.maxWidth || 80;
|
|
23
|
+
this.prompt = options.prompt || '> ';
|
|
24
|
+
this.isEditing = false;
|
|
25
|
+
this.rl = null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Start the editor
|
|
30
|
+
*/
|
|
31
|
+
async start() {
|
|
32
|
+
return new Promise((resolve) => {
|
|
33
|
+
this.isEditing = true;
|
|
34
|
+
this.resolve = resolve;
|
|
35
|
+
|
|
36
|
+
// Set up readline interface
|
|
37
|
+
this.rl = readline.createInterface({
|
|
38
|
+
input: process.stdin,
|
|
39
|
+
output: process.stdout,
|
|
40
|
+
terminal: true
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Hide cursor initially
|
|
44
|
+
process.stdout.write('\x1B[?25l');
|
|
45
|
+
|
|
46
|
+
// Set up raw mode for direct key handling
|
|
47
|
+
if (process.stdin.isTTY && process.stdin.setRawMode) {
|
|
48
|
+
process.stdin.setRawMode(true);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Render initial state
|
|
52
|
+
this.render();
|
|
53
|
+
|
|
54
|
+
// Set up keypress handling
|
|
55
|
+
this.setupKeypressHandling();
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Set up keypress handling for the editor
|
|
61
|
+
*/
|
|
62
|
+
setupKeypressHandling() {
|
|
63
|
+
const onKeypress = (str, key) => {
|
|
64
|
+
if (!this.isEditing) return;
|
|
65
|
+
|
|
66
|
+
debugLogger.info('Editor keypress', { str, key: key ? { name: key.name, ctrl: key.ctrl, shift: key.shift } : null });
|
|
67
|
+
|
|
68
|
+
// Handle special keys
|
|
69
|
+
if (key) {
|
|
70
|
+
if (key.ctrl && key.name === 'c') {
|
|
71
|
+
// Cancel with Ctrl+C
|
|
72
|
+
this.cleanup();
|
|
73
|
+
this.resolve(null);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (key.ctrl && key.name === 'd') {
|
|
78
|
+
// Finish with Ctrl+D
|
|
79
|
+
this.cleanup();
|
|
80
|
+
this.resolve(this.getText());
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (key.name === 'escape') {
|
|
85
|
+
// Cancel with Escape
|
|
86
|
+
this.cleanup();
|
|
87
|
+
this.resolve(null);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (key.name === 'return') {
|
|
92
|
+
// New line
|
|
93
|
+
this.insertNewline();
|
|
94
|
+
this.render();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (key.name === 'backspace') {
|
|
99
|
+
// Backspace
|
|
100
|
+
this.backspace();
|
|
101
|
+
this.render();
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (key.name === 'delete') {
|
|
106
|
+
// Delete
|
|
107
|
+
this.delete();
|
|
108
|
+
this.render();
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (key.name === 'up') {
|
|
113
|
+
// Move cursor up
|
|
114
|
+
this.moveCursorUp();
|
|
115
|
+
this.render();
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (key.name === 'down') {
|
|
120
|
+
// Move cursor down
|
|
121
|
+
this.moveCursorDown();
|
|
122
|
+
this.render();
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (key.name === 'left') {
|
|
127
|
+
// Move cursor left
|
|
128
|
+
this.moveCursorLeft();
|
|
129
|
+
this.render();
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (key.name === 'right') {
|
|
134
|
+
// Move cursor right
|
|
135
|
+
this.moveCursorRight();
|
|
136
|
+
this.render();
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (key.name === 'home') {
|
|
141
|
+
// Move to start of line
|
|
142
|
+
this.cursorX = 0;
|
|
143
|
+
this.render();
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (key.name === 'end') {
|
|
148
|
+
// Move to end of line
|
|
149
|
+
this.cursorX = this.lines[this.cursorY].length;
|
|
150
|
+
this.render();
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Handle regular characters
|
|
156
|
+
if (str && str.length === 1 && str.charCodeAt(0) >= 32 && str.charCodeAt(0) <= 126) {
|
|
157
|
+
this.insertCharacter(str);
|
|
158
|
+
this.render();
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// Set up keypress event handling
|
|
163
|
+
readline.emitKeypressEvents(process.stdin);
|
|
164
|
+
process.stdin.on('keypress', onKeypress);
|
|
165
|
+
this.keypressHandler = onKeypress;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Insert a character at cursor position
|
|
170
|
+
*/
|
|
171
|
+
insertCharacter(char) {
|
|
172
|
+
const line = this.lines[this.cursorY];
|
|
173
|
+
this.lines[this.cursorY] = line.slice(0, this.cursorX) + char + line.slice(this.cursorX);
|
|
174
|
+
this.cursorX++;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Insert a new line
|
|
179
|
+
*/
|
|
180
|
+
insertNewline() {
|
|
181
|
+
const line = this.lines[this.cursorY];
|
|
182
|
+
const beforeCursor = line.slice(0, this.cursorX);
|
|
183
|
+
const afterCursor = line.slice(this.cursorX);
|
|
184
|
+
|
|
185
|
+
this.lines[this.cursorY] = beforeCursor;
|
|
186
|
+
this.lines.splice(this.cursorY + 1, 0, afterCursor);
|
|
187
|
+
|
|
188
|
+
this.cursorX = 0;
|
|
189
|
+
this.cursorY++;
|
|
190
|
+
|
|
191
|
+
// Ensure we don't exceed max lines
|
|
192
|
+
if (this.lines.length > this.maxLines) {
|
|
193
|
+
this.lines.shift();
|
|
194
|
+
this.cursorY--;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Delete character before cursor
|
|
200
|
+
*/
|
|
201
|
+
backspace() {
|
|
202
|
+
if (this.cursorX > 0) {
|
|
203
|
+
const line = this.lines[this.cursorY];
|
|
204
|
+
this.lines[this.cursorY] = line.slice(0, this.cursorX - 1) + line.slice(this.cursorX);
|
|
205
|
+
this.cursorX--;
|
|
206
|
+
} else if (this.cursorY > 0) {
|
|
207
|
+
// Join with previous line
|
|
208
|
+
const prevLine = this.lines[this.cursorY - 1];
|
|
209
|
+
const currentLine = this.lines[this.cursorY];
|
|
210
|
+
this.cursorX = prevLine.length;
|
|
211
|
+
this.lines[this.cursorY - 1] = prevLine + currentLine;
|
|
212
|
+
this.lines.splice(this.cursorY, 1);
|
|
213
|
+
this.cursorY--;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Delete character at cursor
|
|
219
|
+
*/
|
|
220
|
+
delete() {
|
|
221
|
+
const line = this.lines[this.cursorY];
|
|
222
|
+
if (this.cursorX < line.length) {
|
|
223
|
+
this.lines[this.cursorY] = line.slice(0, this.cursorX) + line.slice(this.cursorX + 1);
|
|
224
|
+
} else if (this.cursorY < this.lines.length - 1) {
|
|
225
|
+
// Join with next line
|
|
226
|
+
const currentLine = this.lines[this.cursorY];
|
|
227
|
+
const nextLine = this.lines[this.cursorY + 1];
|
|
228
|
+
this.lines[this.cursorY] = currentLine + nextLine;
|
|
229
|
+
this.lines.splice(this.cursorY + 1, 1);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Move cursor up
|
|
235
|
+
*/
|
|
236
|
+
moveCursorUp() {
|
|
237
|
+
if (this.cursorY > 0) {
|
|
238
|
+
this.cursorY--;
|
|
239
|
+
const lineLength = this.lines[this.cursorY].length;
|
|
240
|
+
this.cursorX = Math.min(this.cursorX, lineLength);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Move cursor down
|
|
246
|
+
*/
|
|
247
|
+
moveCursorDown() {
|
|
248
|
+
if (this.cursorY < this.lines.length - 1) {
|
|
249
|
+
this.cursorY++;
|
|
250
|
+
const lineLength = this.lines[this.cursorY].length;
|
|
251
|
+
this.cursorX = Math.min(this.cursorX, lineLength);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Move cursor left
|
|
257
|
+
*/
|
|
258
|
+
moveCursorLeft() {
|
|
259
|
+
if (this.cursorX > 0) {
|
|
260
|
+
this.cursorX--;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Move cursor right
|
|
266
|
+
*/
|
|
267
|
+
moveCursorRight() {
|
|
268
|
+
const lineLength = this.lines[this.cursorY].length;
|
|
269
|
+
if (this.cursorX < lineLength) {
|
|
270
|
+
this.cursorX++;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Render the editor
|
|
276
|
+
*/
|
|
277
|
+
render() {
|
|
278
|
+
// Clear screen and move to top
|
|
279
|
+
process.stdout.write('\x1B[2J\x1B[H');
|
|
280
|
+
|
|
281
|
+
// Show instructions
|
|
282
|
+
console.log(chalk.cyan('Multi-line Editor'));
|
|
283
|
+
console.log(chalk.gray('Ctrl+D: Save & Exit | Ctrl+C/ESC: Cancel | Arrow Keys: Navigate'));
|
|
284
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
285
|
+
|
|
286
|
+
// Show lines
|
|
287
|
+
const visibleLines = this.lines.slice(this.scrollY, this.scrollY + this.maxLines);
|
|
288
|
+
visibleLines.forEach((line, index) => {
|
|
289
|
+
const actualIndex = this.scrollY + index;
|
|
290
|
+
const isCursorLine = actualIndex === this.cursorY;
|
|
291
|
+
const lineNumber = chalk.gray(`${(actualIndex + 1).toString().padStart(2)}: `);
|
|
292
|
+
|
|
293
|
+
if (isCursorLine) {
|
|
294
|
+
const beforeCursor = line.slice(0, this.cursorX);
|
|
295
|
+
const atCursor = line.slice(this.cursorX, this.cursorX + 1) || ' ';
|
|
296
|
+
const afterCursor = line.slice(this.cursorX + 1);
|
|
297
|
+
|
|
298
|
+
console.log(`${lineNumber}${beforeCursor}${chalk.bgCyan(atCursor)}${afterCursor}`);
|
|
299
|
+
} else {
|
|
300
|
+
console.log(`${lineNumber}${line}`);
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// Show status
|
|
305
|
+
const status = `Line ${this.cursorY + 1}/${this.lines.length} | Col ${this.cursorX + 1}`;
|
|
306
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
307
|
+
console.log(chalk.cyan(status));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Get the edited text
|
|
312
|
+
*/
|
|
313
|
+
getText() {
|
|
314
|
+
return this.lines.join('\n').trim();
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Clean up resources
|
|
319
|
+
*/
|
|
320
|
+
cleanup() {
|
|
321
|
+
this.isEditing = false;
|
|
322
|
+
|
|
323
|
+
if (this.keypressHandler) {
|
|
324
|
+
process.stdin.removeListener('keypress', this.keypressHandler);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (this.rl) {
|
|
328
|
+
this.rl.close();
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (process.stdin.isTTY && process.stdin.setRawMode) {
|
|
332
|
+
process.stdin.setRawMode(false);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Show cursor again
|
|
336
|
+
process.stdout.write('\x1B[?25h');
|
|
337
|
+
|
|
338
|
+
debugLogger.info('Editor cleaned up');
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Show multi-line editor
|
|
344
|
+
*/
|
|
345
|
+
async function showMultiLineEditor(options = {}) {
|
|
346
|
+
const editor = new MultiLineEditor(options);
|
|
347
|
+
return await editor.start();
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
module.exports = { MultiLineEditor, showMultiLineEditor };
|