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.
Files changed (45) hide show
  1. package/bin/vibecodingmachine.js +124 -0
  2. package/package.json +3 -2
  3. package/src/commands/agents-check.js +69 -0
  4. package/src/commands/auto-direct.js +930 -145
  5. package/src/commands/auto.js +26 -4
  6. package/src/commands/ide.js +2 -1
  7. package/src/commands/requirements.js +23 -27
  8. package/src/utils/auto-mode.js +4 -1
  9. package/src/utils/cline-js-handler.js +218 -0
  10. package/src/utils/config.js +22 -0
  11. package/src/utils/display-formatters-complete.js +229 -0
  12. package/src/utils/display-formatters-extracted.js +219 -0
  13. package/src/utils/display-formatters.js +157 -0
  14. package/src/utils/feedback-handler.js +143 -0
  15. package/src/utils/ide-detection-complete.js +126 -0
  16. package/src/utils/ide-detection-extracted.js +116 -0
  17. package/src/utils/ide-detection.js +124 -0
  18. package/src/utils/interactive-backup.js +5664 -0
  19. package/src/utils/interactive-broken.js +280 -0
  20. package/src/utils/interactive.js +31 -5534
  21. package/src/utils/provider-checker.js +410 -0
  22. package/src/utils/provider-manager.js +251 -0
  23. package/src/utils/provider-registry.js +18 -9
  24. package/src/utils/requirement-actions.js +884 -0
  25. package/src/utils/requirements-navigator.js +585 -0
  26. package/src/utils/rui-trui-adapter.js +311 -0
  27. package/src/utils/simple-trui.js +204 -0
  28. package/src/utils/status-helpers-extracted.js +125 -0
  29. package/src/utils/status-helpers.js +107 -0
  30. package/src/utils/trui-debug.js +261 -0
  31. package/src/utils/trui-feedback.js +133 -0
  32. package/src/utils/trui-nav-agents.js +119 -0
  33. package/src/utils/trui-nav-requirements.js +268 -0
  34. package/src/utils/trui-nav-settings.js +157 -0
  35. package/src/utils/trui-nav-specifications.js +139 -0
  36. package/src/utils/trui-navigation.js +303 -0
  37. package/src/utils/trui-provider-manager.js +182 -0
  38. package/src/utils/trui-quick-menu.js +365 -0
  39. package/src/utils/trui-req-actions.js +372 -0
  40. package/src/utils/trui-req-tree.js +534 -0
  41. package/src/utils/trui-specifications.js +359 -0
  42. package/src/utils/trui-text-editor.js +350 -0
  43. package/src/utils/trui-windsurf.js +336 -0
  44. package/src/utils/welcome-screen-extracted.js +135 -0
  45. 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 };