vibecodingmachine-cli 1.0.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 (44) hide show
  1. package/.allnightai/REQUIREMENTS.md +11 -0
  2. package/.allnightai/temp/auto-status.json +6 -0
  3. package/.env +7 -0
  4. package/.eslintrc.js +16 -0
  5. package/README.md +85 -0
  6. package/bin/vibecodingmachine.js +274 -0
  7. package/jest.config.js +8 -0
  8. package/logs/audit/2025-11-07.jsonl +2 -0
  9. package/package.json +64 -0
  10. package/scripts/README.md +128 -0
  11. package/scripts/auto-start-wrapper.sh +92 -0
  12. package/scripts/postinstall.js +81 -0
  13. package/src/commands/auth.js +96 -0
  14. package/src/commands/auto-direct.js +1748 -0
  15. package/src/commands/auto.js +4692 -0
  16. package/src/commands/auto.js.bak +710 -0
  17. package/src/commands/ide.js +70 -0
  18. package/src/commands/repo.js +159 -0
  19. package/src/commands/requirements.js +161 -0
  20. package/src/commands/setup.js +91 -0
  21. package/src/commands/status.js +88 -0
  22. package/src/components/RequirementPage.js +0 -0
  23. package/src/file.js +0 -0
  24. package/src/index.js +5 -0
  25. package/src/main.js +0 -0
  26. package/src/ui/requirements-page.js +0 -0
  27. package/src/utils/auth.js +548 -0
  28. package/src/utils/auto-mode-ansi-ui.js +238 -0
  29. package/src/utils/auto-mode-simple-ui.js +161 -0
  30. package/src/utils/auto-mode-ui.js.bak.blessed +207 -0
  31. package/src/utils/auto-mode.js +65 -0
  32. package/src/utils/config.js +64 -0
  33. package/src/utils/interactive.js +3616 -0
  34. package/src/utils/keyboard-handler.js +152 -0
  35. package/src/utils/logger.js +4 -0
  36. package/src/utils/persistent-header.js +116 -0
  37. package/src/utils/provider-registry.js +128 -0
  38. package/src/utils/requirementUtils.js +0 -0
  39. package/src/utils/status-card.js +120 -0
  40. package/src/utils/status-manager.js +0 -0
  41. package/src/utils/status.js +0 -0
  42. package/src/utils/stdout-interceptor.js +127 -0
  43. package/tests/auto-mode.test.js +37 -0
  44. package/tests/config.test.js +34 -0
@@ -0,0 +1,3616 @@
1
+ const inquirer = require('inquirer');
2
+ const chalk = require('chalk');
3
+ const boxen = require('boxen');
4
+ const path = require('path');
5
+ const os = require('os');
6
+ const fs = require('fs-extra');
7
+ const readline = require('readline');
8
+ const repo = require('../commands/repo');
9
+ const auto = require('../commands/auto');
10
+ const status = require('../commands/status');
11
+ const { getRepoPath, readConfig, writeConfig } = require('./config');
12
+ const { getProviderPreferences, saveProviderPreferences, getProviderDefinitions } = require('../utils/provider-registry');
13
+ const { checkAutoModeStatus } = require('./auto-mode');
14
+ const {
15
+ checkVibeCodingMachineExists,
16
+ getHostname,
17
+ getRequirementsFilename,
18
+ requirementsExists,
19
+ isComputerNameEnabled
20
+ } = require('@vibecodingmachine/core');
21
+
22
+ /**
23
+ * Format IDE name for display
24
+ * @param {string} ide - Internal IDE identifier
25
+ * @returns {string} Display name for IDE
26
+ */
27
+ function formatIDEName(ide) {
28
+ const ideNames = {
29
+ 'claude-code': 'Claude Code CLI',
30
+ 'aider': 'Aider CLI',
31
+ 'continue': 'Continue CLI',
32
+ 'cline': 'Cline CLI',
33
+ 'cursor': 'Cursor',
34
+ 'vscode': 'VS Code',
35
+ 'windsurf': 'Windsurf'
36
+ };
37
+ return ideNames[ide] || ide;
38
+ }
39
+
40
+ /**
41
+ * Get current AI provider name for IDEs that require it (like Cline and Continue)
42
+ * @param {string} ide - Internal IDE identifier
43
+ * @returns {string|null} Provider name or null if not applicable/configured
44
+ */
45
+ /**
46
+ * Get unified agent name (IDE or LLM-based)
47
+ * @param {string} agentType - Agent type (e.g., 'cursor', 'ollama', 'anthropic')
48
+ * @returns {string} - Display name like "Cursor IDE Agent" or "Ollama (qwen2.5-coder:32b)"
49
+ */
50
+ function getAgentDisplayName(agentType) {
51
+ // IDE-based agents
52
+ if (agentType === 'cursor') return 'Cursor IDE Agent';
53
+ if (agentType === 'windsurf') return 'Windsurf IDE Agent';
54
+ if (agentType === 'antigravity') return 'Google Antigravity IDE Agent';
55
+ if (agentType === 'vscode') return 'VS Code IDE Agent';
56
+
57
+ // Claude Code CLI
58
+ if (agentType === 'claude-code') return 'Claude Code CLI';
59
+
60
+ // Direct LLM agents
61
+ if (agentType === 'ollama' || agentType === 'anthropic' || agentType === 'groq' || agentType === 'bedrock') {
62
+ try {
63
+ const fs = require('fs');
64
+ const path = require('path');
65
+ const os = require('os');
66
+ const configPath = path.join(os.homedir(), '.config', 'allnightai', 'config.json');
67
+ if (fs.existsSync(configPath)) {
68
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
69
+ const model = config.auto?.llmModel || config.auto?.aiderModel || config.auto?.groqModel;
70
+
71
+ if (agentType === 'ollama' && model && !model.includes('groq/')) {
72
+ return `Ollama (${model})`;
73
+ } else if (agentType === 'anthropic') {
74
+ return 'Anthropic (Claude Sonnet 4)';
75
+ } else if (agentType === 'groq') {
76
+ // Extract model name from groq/model format
77
+ const groqModel = model && model.includes('groq/') ? model.split('/')[1] : 'llama-3.3-70b-versatile';
78
+ return `Groq (${groqModel})`;
79
+ } else if (agentType === 'bedrock') {
80
+ return 'AWS Bedrock (Claude)';
81
+ }
82
+ }
83
+ } catch (error) {
84
+ // Fallback to generic names
85
+ }
86
+
87
+ // Fallback names
88
+ if (agentType === 'ollama') return 'Ollama (Local)';
89
+ if (agentType === 'anthropic') return 'Anthropic (Claude)';
90
+ if (agentType === 'groq') return 'Groq (llama-3.3-70b-versatile)';
91
+ if (agentType === 'bedrock') return 'AWS Bedrock';
92
+ }
93
+
94
+ // Legacy support for old IDE names
95
+ return formatIDEName(agentType);
96
+ }
97
+
98
+ function getCurrentAIProvider(ide) {
99
+ // Aider, Cline, and Continue require AI provider configuration
100
+ if (ide !== 'aider' && ide !== 'cline' && ide !== 'continue') {
101
+ return null;
102
+ }
103
+
104
+ // Aider uses Ollama by default (or Bedrock if configured)
105
+ if (ide === 'aider') {
106
+ try {
107
+ const { getAutoConfig } = require('./config');
108
+ // Note: getAutoConfig is async, but this function is sync
109
+ // We'll use a sync read for now, or make this function async
110
+ const fs = require('fs');
111
+ const path = require('path');
112
+ const os = require('os');
113
+ const configPath = path.join(os.homedir(), '.config', 'allnightai', 'config.json');
114
+ if (fs.existsSync(configPath)) {
115
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
116
+ const aiderModel = config.auto?.aiderModel;
117
+ if (aiderModel) {
118
+ return `Ollama (${aiderModel})`;
119
+ }
120
+ }
121
+ return 'Ollama';
122
+ } catch (error) {
123
+ return 'Ollama';
124
+ }
125
+ }
126
+
127
+ // Handle Continue CLI - read from Continue config
128
+ if (ide === 'continue') {
129
+ try {
130
+ const fs = require('fs');
131
+ const path = require('path');
132
+ const os = require('os');
133
+ const yaml = require('js-yaml');
134
+ const configPath = path.join(os.homedir(), '.continue', 'config.yaml');
135
+
136
+ if (!fs.existsSync(configPath)) {
137
+ return 'Not configured';
138
+ }
139
+
140
+ const config = yaml.load(fs.readFileSync(configPath, 'utf8'));
141
+ const models = config.models || [];
142
+
143
+ if (models.length === 0) {
144
+ return 'Not configured';
145
+ }
146
+
147
+ const firstModel = models[0];
148
+ if (firstModel.provider === 'ollama') {
149
+ return `Ollama (${firstModel.model || 'model configured'})`;
150
+ }
151
+
152
+ return firstModel.provider || 'Configured';
153
+ } catch (error) {
154
+ return 'Not configured';
155
+ }
156
+ }
157
+
158
+ try {
159
+ const { ClineCLIManager } = require('@vibecodingmachine/core');
160
+ const clineManager = new ClineCLIManager();
161
+
162
+ if (!clineManager.isConfigured()) {
163
+ return 'Not configured';
164
+ }
165
+
166
+ const fs = require('fs');
167
+ const path = require('path');
168
+ const os = require('os');
169
+ const configPath = path.join(os.homedir(), '.cline_cli', 'cline_cli_settings.json');
170
+
171
+ if (!fs.existsSync(configPath)) {
172
+ return 'Not configured';
173
+ }
174
+
175
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
176
+ const apiProvider = config.globalState?.apiProvider;
177
+ const openAiBaseUrl = config.globalState?.openAiBaseUrl;
178
+
179
+ if (!apiProvider) {
180
+ return 'Not configured';
181
+ }
182
+
183
+ // Map provider identifiers to display names
184
+ if (apiProvider === 'anthropic') {
185
+ return 'Anthropic Claude';
186
+ } else if (apiProvider === 'openrouter') {
187
+ return 'OpenRouter';
188
+ } else if (apiProvider === 'openai-native') {
189
+ if (openAiBaseUrl === 'http://localhost:11434/v1') {
190
+ return 'Ollama';
191
+ } else if (openAiBaseUrl?.includes('generativelanguage.googleapis.com')) {
192
+ return 'Google Gemini';
193
+ }
194
+ return 'OpenAI Native';
195
+ }
196
+
197
+ return apiProvider;
198
+ } catch (error) {
199
+ return 'Unknown';
200
+ }
201
+ }
202
+
203
+ function formatPath(fullPath) {
204
+ const homeDir = os.homedir();
205
+ if (fullPath.startsWith(homeDir)) {
206
+ return fullPath.replace(homeDir, '~');
207
+ }
208
+ return fullPath;
209
+ }
210
+
211
+ async function countRequirements() {
212
+ try {
213
+ const { getRequirementsPath, getVibeCodingMachineDir } = require('@vibecodingmachine/core');
214
+ const repoPath = await getRepoPath();
215
+ const reqPath = await getRequirementsPath(repoPath);
216
+
217
+ if (!reqPath || !await fs.pathExists(reqPath)) {
218
+ return null;
219
+ }
220
+
221
+ const content = await fs.readFile(reqPath, 'utf8');
222
+
223
+ // Count requirements in each section
224
+ let todoCount = 0;
225
+ let toVerifyCount = 0;
226
+ let verifiedCount = 0;
227
+
228
+ // Split by sections
229
+ const lines = content.split('\n');
230
+ let currentSection = '';
231
+
232
+ for (const line of lines) {
233
+ const trimmed = line.trim();
234
+
235
+ // Check for requirement headers first (###), then section headers (##)
236
+ // This prevents ### from being treated as section headers
237
+ if (trimmed.startsWith('###')) {
238
+ // Count requirements (### headers in new format)
239
+ // IMPORTANT: Only count if we're in a recognized section
240
+ if (currentSection) {
241
+ const requirementText = trimmed.replace(/^###\s*/, '').trim();
242
+ if (requirementText) { // Only count if requirement text is not empty
243
+ if (currentSection === 'todo') {
244
+ todoCount++;
245
+ } else if (currentSection === 'toverify') {
246
+ toVerifyCount++;
247
+ }
248
+ }
249
+ }
250
+ } else if (trimmed.startsWith('##') && !trimmed.startsWith('###')) {
251
+ // Detect section headers (must start with ## but not ###)
252
+ if (trimmed.includes('⏳ Requirements not yet completed') ||
253
+ trimmed.includes('Requirements not yet completed')) {
254
+ currentSection = 'todo';
255
+ } else if (trimmed.includes('🔍 TO VERIFY BY HUMAN') ||
256
+ trimmed.includes('TO VERIFY BY HUMAN') ||
257
+ trimmed.includes('🔍 TO VERIFY') ||
258
+ trimmed.includes('TO VERIFY') ||
259
+ trimmed.includes('✅ Verified by AI') ||
260
+ trimmed.includes('Verified by AI')) {
261
+ currentSection = 'toverify';
262
+ } else {
263
+ // Any other section header clears the current section
264
+ currentSection = '';
265
+ }
266
+ }
267
+ }
268
+
269
+ // Count verified requirements from CHANGELOG.md (at repository root)
270
+ const allnightDir = await getVibeCodingMachineDir();
271
+ if (allnightDir) {
272
+ // CHANGELOG.md is at the repository root
273
+ // If .vibecodingmachine is inside repo: go up one level
274
+ // If .vibecodingmachine is sibling (../.vibecodingmachine-reponame): go up one level then into repo
275
+ let changelogPath;
276
+ const allnightStatus = await require('@vibecodingmachine/core').checkVibeCodingMachineExists();
277
+
278
+ if (allnightStatus.insideExists) {
279
+ // .vibecodingmachine is inside repo, so go up one level
280
+ changelogPath = path.join(path.dirname(allnightDir), 'CHANGELOG.md');
281
+ } else if (allnightStatus.siblingExists) {
282
+ // .vibecodingmachine is sibling, use current working directory
283
+ changelogPath = path.join(process.cwd(), 'CHANGELOG.md');
284
+ }
285
+
286
+ if (changelogPath && await fs.pathExists(changelogPath)) {
287
+ const changelogContent = await fs.readFile(changelogPath, 'utf8');
288
+ // Count entries that look like completed requirements
289
+ // Each entry typically starts with "- " followed by date/description
290
+ const changelogLines = changelogContent.split('\n');
291
+ for (const line of changelogLines) {
292
+ const trimmed = line.trim();
293
+ // Count lines that start with "- " and have substantial content (not just empty bullets)
294
+ if (trimmed.startsWith('- ') && trimmed.length > 10) {
295
+ verifiedCount++;
296
+ }
297
+ }
298
+ }
299
+ }
300
+
301
+ return { todoCount, toVerifyCount, verifiedCount };
302
+ } catch (error) {
303
+ return null;
304
+ }
305
+ }
306
+
307
+ async function getCurrentProgress() {
308
+ try {
309
+ const { getRequirementsPath } = require('@vibecodingmachine/core');
310
+ const reqPath = await getRequirementsPath();
311
+
312
+ if (!reqPath || !await fs.pathExists(reqPath)) {
313
+ return null;
314
+ }
315
+
316
+ const content = await fs.readFile(reqPath, 'utf8');
317
+ const lines = content.split('\n');
318
+
319
+ let requirement = null;
320
+ let inTodoSection = false;
321
+
322
+ for (let i = 0; i < lines.length; i++) {
323
+ const line = lines[i];
324
+
325
+ // Find TODO section
326
+ if (line.includes('## ⏳ Requirements not yet completed') || line.includes('## Requirements not yet completed')) {
327
+ inTodoSection = true;
328
+ continue;
329
+ }
330
+
331
+ // Exit TODO section when we hit another section
332
+ if (inTodoSection && line.trim().startsWith('##')) {
333
+ break;
334
+ }
335
+
336
+ // Find first TODO requirement
337
+ if (inTodoSection) {
338
+ const trimmed = line.trim();
339
+ if (trimmed.startsWith('- ') || trimmed.startsWith('* ')) {
340
+ requirement = trimmed.substring(2); // Remove "- " or "* " prefix
341
+ break;
342
+ }
343
+ }
344
+ }
345
+
346
+ return requirement ? { status: 'PREPARE', requirement } : null;
347
+ } catch (error) {
348
+ return null;
349
+ }
350
+ }
351
+
352
+ async function showWelcomeScreen() {
353
+ const repoPath = process.cwd(); // Always use current working directory
354
+ const autoStatus = await checkAutoModeStatus();
355
+ const allnightStatus = await checkVibeCodingMachineExists();
356
+ const hostname = getHostname();
357
+ const requirementsFilename = await getRequirementsFilename();
358
+ const useHostname = await isComputerNameEnabled();
359
+
360
+ // Get current IDE from config
361
+ const { getAutoConfig } = require('./config');
362
+ const autoConfig = await getAutoConfig();
363
+ const currentIDE = autoConfig.ide || autoStatus.ide || 'claude-code';
364
+
365
+ // Check for requirements file
366
+ const hasRequirements = await requirementsExists();
367
+ let requirementsLocation = '';
368
+
369
+ if (allnightStatus.insideExists) {
370
+ requirementsLocation = '.vibecodingmachine';
371
+ } else if (allnightStatus.siblingExists) {
372
+ requirementsLocation = path.basename(allnightStatus.siblingDir);
373
+ }
374
+
375
+ // Count requirements if file exists
376
+ const counts = hasRequirements ? await countRequirements() : null;
377
+
378
+ // Clear the screen
379
+ console.clear();
380
+
381
+ // Get version formatted like the UI: "v2025.10.30 4:15 PM MDT"
382
+ const pkg = require('../../package.json');
383
+ const buildDate = new Date(); // Using current date since we don't track install date in CLI
384
+ const year = buildDate.getFullYear();
385
+ const month = String(buildDate.getMonth() + 1).padStart(2, '0');
386
+ const day = String(buildDate.getDate()).padStart(2, '0');
387
+ const dateStr = `${year}.${month}.${day}`;
388
+ const timeStr = buildDate.toLocaleTimeString('en-US', {
389
+ hour: 'numeric',
390
+ minute: '2-digit',
391
+ hour12: true,
392
+ timeZoneName: 'short'
393
+ });
394
+ const version = `v${dateStr} ${timeStr}`;
395
+
396
+ // Display welcome banner with version
397
+ console.log('\n' + boxen(
398
+ chalk.bold.cyan('Vibe Coding Machine') + '\n' +
399
+ chalk.gray(version) + '\n' +
400
+ chalk.gray('Big Dreams + AI + VibeCodingMachine.com = Your money making apps'),
401
+ {
402
+ padding: 1,
403
+ margin: 0,
404
+ borderStyle: 'round',
405
+ borderColor: 'cyan'
406
+ }
407
+ ));
408
+
409
+ // Display repository and system info
410
+ console.log();
411
+ console.log(chalk.gray('Repo: '), formatPath(repoPath));
412
+ console.log(chalk.gray('Computer Name: '), chalk.cyan(hostname));
413
+
414
+ // Display auto mode progress if running
415
+ if (autoStatus.running) {
416
+ console.log(chalk.gray('Chats: '), chalk.cyan(autoStatus.chatCount || 0));
417
+
418
+ // Get current status and requirement from REQUIREMENTS file
419
+ const progress = await getCurrentProgress();
420
+ if (progress) {
421
+ console.log();
422
+ // Display progress in a purple/magenta box similar to UI
423
+ const stageIcons = {
424
+ 'PREPARE': '🔍',
425
+ 'ACT': '⚡',
426
+ 'CLEAN UP': '🧹',
427
+ 'VERIFY': '✅',
428
+ 'DONE': '🎉'
429
+ };
430
+ const icon = stageIcons[progress.status] || '⏳';
431
+ const statusColor = progress.status === 'DONE' ? chalk.green : chalk.magenta;
432
+
433
+ console.log(boxen(
434
+ statusColor.bold(`${icon} ${progress.status}`) + '\n' +
435
+ chalk.gray(progress.requirement ? progress.requirement.substring(0, 60) + (progress.requirement.length > 60 ? '...' : '') : 'No requirement'),
436
+ {
437
+ padding: { left: 1, right: 1, top: 0, bottom: 0 },
438
+ margin: 0,
439
+ borderStyle: 'round',
440
+ borderColor: 'magenta'
441
+ }
442
+ ));
443
+ }
444
+
445
+ // Display recent audit log entries
446
+ const { readAuditLog, getDateStr } = require('@vibecodingmachine/core');
447
+ const todayStr = getDateStr();
448
+ const entries = readAuditLog(todayStr);
449
+ if (entries && entries.length > 0) {
450
+ console.log();
451
+ console.log(chalk.gray.bold('Recent Activity:'));
452
+ // Show last 5 entries
453
+ const recentEntries = entries.slice(-5);
454
+ recentEntries.forEach(entry => {
455
+ const time = new Date(entry.timestamp).toLocaleTimeString('en-US', {
456
+ hour: 'numeric',
457
+ minute: '2-digit',
458
+ second: '2-digit',
459
+ hour12: true
460
+ });
461
+ const icon = entry.type === 'auto-mode-start' ? '▶️' :
462
+ entry.type === 'auto-mode-stop' ? '⏹️' :
463
+ entry.type === 'ide-message' ? '💬' : '•';
464
+ console.log(chalk.gray(` ${time} ${icon}`), entry.message || '');
465
+ });
466
+ }
467
+ }
468
+
469
+ console.log();
470
+ }
471
+
472
+ // Helper to convert index to letter (0->a, 1->b, etc.)
473
+ function indexToLetter(index) {
474
+ return String.fromCharCode(97 + index); // 97 is 'a'
475
+ }
476
+
477
+ // Tree-style requirements navigator
478
+ async function showRequirementsTree() {
479
+ console.log(chalk.bold.cyan('\n📋 Requirements Navigator\n'));
480
+ console.log(chalk.gray('Use ↑/↓ to navigate, →/Enter/Space to expand, ←/ESC to collapse/go back\n'));
481
+
482
+ const tree = {
483
+ expanded: { root: true },
484
+ selected: 0,
485
+ items: []
486
+ };
487
+
488
+ // Build tree structure
489
+ const buildTree = () => {
490
+ tree.items = [];
491
+
492
+ // Root: Requirements
493
+ tree.items.push({ level: 0, type: 'root', label: '📋 Requirements', key: 'root' });
494
+
495
+ if (tree.expanded.root) {
496
+ tree.items.push({ level: 1, type: 'add', label: '➕ Add new requirement', key: 'add-one' });
497
+ tree.items.push({ level: 1, type: 'add', label: '➕ Add multiple requirements', key: 'add-many' });
498
+
499
+ // Calculate counts and percentages
500
+ const verifiedReqs = tree.verifiedReqs || [];
501
+ const verifyReqs = tree.verifyReqs || [];
502
+ const clarificationReqs = tree.clarificationReqs || [];
503
+ const todoReqs = tree.todoReqs || [];
504
+ const recycledReqs = tree.recycledReqs || [];
505
+ const total = verifiedReqs.length + verifyReqs.length + clarificationReqs.length + todoReqs.length + recycledReqs.length;
506
+
507
+ const verifiedPercent = total > 0 ? Math.round((verifiedReqs.length / total) * 100) : 0;
508
+ const verifyPercent = total > 0 ? Math.round((verifyReqs.length / total) * 100) : 0;
509
+ const clarificationPercent = total > 0 ? Math.round((clarificationReqs.length / total) * 100) : 0;
510
+ const todoPercent = total > 0 ? Math.round((todoReqs.length / total) * 100) : 0;
511
+ const recycledPercent = total > 0 ? Math.round((recycledReqs.length / total) * 100) : 0;
512
+
513
+ // VERIFIED section (first)
514
+ tree.items.push({ level: 1, type: 'section', label: `🎉 VERIFIED (${verifiedReqs.length} - ${verifiedPercent}%)`, key: 'verified' });
515
+
516
+ if (tree.expanded.verified) {
517
+ verifiedReqs.forEach((req, idx) => {
518
+ tree.items.push({ level: 2, type: 'verified', label: req, key: `verified-${idx}` });
519
+ });
520
+ }
521
+
522
+ // TO VERIFY section (second)
523
+ tree.items.push({ level: 1, type: 'section', label: `✅ TO VERIFY (${verifyReqs.length} - ${verifyPercent}%)`, key: 'verify', section: '✅ Verified by AI screenshot' });
524
+
525
+ if (tree.expanded.verify) {
526
+ verifyReqs.forEach((req, idx) => {
527
+ tree.items.push({ level: 2, type: 'requirement', label: req.title, key: `verify-${idx}`, req, sectionKey: 'verify' });
528
+ });
529
+ }
530
+
531
+ // NEEDING CLARIFICATION section (third)
532
+ tree.items.push({ level: 1, type: 'section', label: `❓ NEEDING CLARIFICATION (${clarificationReqs.length} - ${clarificationPercent}%)`, key: 'clarification', section: '❓ Requirements needing manual feedback' });
533
+
534
+ if (tree.expanded.clarification) {
535
+ clarificationReqs.forEach((req, idx) => {
536
+ tree.items.push({ level: 2, type: 'clarification', label: req.title, key: `clarification-${idx}`, req, sectionKey: 'clarification' });
537
+ });
538
+ }
539
+
540
+ // TODO section (fourth)
541
+ tree.items.push({ level: 1, type: 'section', label: `⏳ TODO (${todoReqs.length} - ${todoPercent}%)`, key: 'todo', section: '⏳ Requirements not yet completed' });
542
+
543
+ if (tree.expanded.todo) {
544
+ todoReqs.forEach((req, idx) => {
545
+ tree.items.push({ level: 2, type: 'requirement', label: req.title, key: `todo-${idx}`, req, sectionKey: 'todo' });
546
+ });
547
+ }
548
+
549
+ // RECYCLED section (last)
550
+ tree.items.push({ level: 1, type: 'section', label: `♻️ RECYCLED (${recycledReqs.length} - ${recycledPercent}%)`, key: 'recycled', section: '♻️ Recycled' });
551
+
552
+ if (tree.expanded.recycled) {
553
+ recycledReqs.forEach((req, idx) => {
554
+ tree.items.push({ level: 2, type: 'recycled', label: req.title, key: `recycled-${idx}`, req, sectionKey: 'recycled' });
555
+ });
556
+ }
557
+ }
558
+ };
559
+
560
+ // Load requirements for a section
561
+ const loadSection = async (sectionKey, sectionTitle) => {
562
+ const { getRequirementsPath } = require('@vibecodingmachine/core');
563
+ const reqPath = await getRequirementsPath();
564
+
565
+ if (!reqPath || !await fs.pathExists(reqPath)) {
566
+ return [];
567
+ }
568
+
569
+ const content = await fs.readFile(reqPath, 'utf8');
570
+ const lines = content.split('\n');
571
+
572
+ let inSection = false;
573
+ const requirements = [];
574
+
575
+ // For TO VERIFY section, check multiple possible section titles
576
+ const sectionTitles = sectionKey === 'verify'
577
+ ? ['🔍 TO VERIFY BY HUMAN', 'TO VERIFY BY HUMAN', '🔍 TO VERIFY', 'TO VERIFY', '✅ Verified by AI screenshot', 'Verified by AI screenshot']
578
+ : [sectionTitle];
579
+
580
+ for (let i = 0; i < lines.length; i++) {
581
+ const line = lines[i];
582
+
583
+ // Check if this line matches any of the section titles
584
+ if (sectionTitles.some(title => line.includes(title))) {
585
+ inSection = true;
586
+ continue;
587
+ }
588
+
589
+ if (inSection && line.startsWith('## ') && !line.startsWith('###') && !sectionTitles.some(title => line.includes(title))) {
590
+ break;
591
+ }
592
+
593
+ // Read requirements in new format (### header)
594
+ if (inSection && line.trim().startsWith('###')) {
595
+ const title = line.trim().replace(/^###\s*/, '');
596
+ const details = [];
597
+ let package = null;
598
+
599
+ // Read package and description
600
+ for (let j = i + 1; j < lines.length; j++) {
601
+ const nextLine = lines[j].trim();
602
+ // Stop if we hit another requirement or section
603
+ if (nextLine.startsWith('###') || (nextLine.startsWith('##') && !nextLine.startsWith('###'))) {
604
+ break;
605
+ }
606
+ // Check for PACKAGE line
607
+ if (nextLine.startsWith('PACKAGE:')) {
608
+ package = nextLine.replace(/^PACKAGE:\s*/, '').trim();
609
+ } else if (nextLine && !nextLine.startsWith('PACKAGE:')) {
610
+ // Description line
611
+ details.push(nextLine);
612
+ }
613
+ }
614
+
615
+ requirements.push({ title, details, package, lineIndex: i });
616
+ }
617
+ }
618
+
619
+ return requirements;
620
+ };
621
+
622
+ // Load VERIFIED requirements from CHANGELOG
623
+ const loadVerified = async () => {
624
+ const { getVibeCodingMachineDir, checkVibeCodingMachineExists } = require('@vibecodingmachine/core');
625
+ const allnightStatus = await checkVibeCodingMachineExists();
626
+ let changelogPath;
627
+
628
+ if (allnightStatus.insideExists) {
629
+ const allnightDir = await getVibeCodingMachineDir();
630
+ changelogPath = path.join(path.dirname(allnightDir), 'CHANGELOG.md');
631
+ } else if (allnightStatus.siblingExists) {
632
+ changelogPath = path.join(process.cwd(), 'CHANGELOG.md');
633
+ }
634
+
635
+ if (!changelogPath || !await fs.pathExists(changelogPath)) {
636
+ return [];
637
+ }
638
+
639
+ const content = await fs.readFile(changelogPath, 'utf8');
640
+ const lines = content.split('\n');
641
+ const requirements = [];
642
+ let inVerifiedSection = false;
643
+
644
+ for (const line of lines) {
645
+ const trimmed = line.trim();
646
+
647
+ // Check for Verified Requirements section
648
+ if (trimmed.includes('## Verified Requirements')) {
649
+ inVerifiedSection = true;
650
+ continue;
651
+ }
652
+
653
+ // Exit section if we hit another ## header
654
+ if (inVerifiedSection && trimmed.startsWith('##') && !trimmed.includes('Verified Requirements')) {
655
+ break;
656
+ }
657
+
658
+ // Only collect items from within the Verified Requirements section
659
+ if (inVerifiedSection && trimmed.startsWith('- ') && trimmed.length > 10) {
660
+ requirements.push(trimmed.substring(2));
661
+ }
662
+ }
663
+
664
+ return requirements;
665
+ };
666
+
667
+ // Load clarification requirements with questions
668
+ const loadClarification = async () => {
669
+ const { getRequirementsPath } = require('@vibecodingmachine/core');
670
+ const reqPath = await getRequirementsPath();
671
+
672
+ if (!reqPath || !await fs.pathExists(reqPath)) {
673
+ return [];
674
+ }
675
+
676
+ const content = await fs.readFile(reqPath, 'utf8');
677
+ const lines = content.split('\n');
678
+
679
+ let inSection = false;
680
+ const requirements = [];
681
+ let currentReq = null;
682
+
683
+ for (let i = 0; i < lines.length; i++) {
684
+ const line = lines[i];
685
+
686
+ if (line.includes('❓ Requirements needing manual feedback')) {
687
+ inSection = true;
688
+ continue;
689
+ }
690
+
691
+ if (inSection && line.startsWith('##') && !line.includes('❓ Requirements needing manual feedback')) {
692
+ if (currentReq) requirements.push(currentReq);
693
+ break;
694
+ }
695
+
696
+ if (inSection && line.trim().startsWith('- ')) {
697
+ if (currentReq) requirements.push(currentReq);
698
+
699
+ const title = line.trim().substring(2);
700
+ currentReq = { title, questions: [], findings: null, lineIndex: i };
701
+ } else if (inSection && currentReq && line.trim()) {
702
+ // Check for AI findings section
703
+ if (line.trim().startsWith('**AI found in codebase:**')) {
704
+ continue; // Skip the header, capture next line as findings
705
+ } else if (!currentReq.findings && !line.trim().startsWith('**Clarifying questions:**') && !line.trim().match(/^\d+\./)) {
706
+ // This is the findings content (comes after "AI found in codebase:")
707
+ currentReq.findings = line.trim();
708
+ } else if (line.trim().startsWith('**Clarifying questions:**')) {
709
+ continue; // Skip the questions header
710
+ } else if (line.trim().match(/^\d+\./)) {
711
+ // This is a question
712
+ currentReq.questions.push({ question: line.trim(), response: null });
713
+ } else if (currentReq.questions.length > 0) {
714
+ // This might be a response to the last question
715
+ const lastQuestion = currentReq.questions[currentReq.questions.length - 1];
716
+ if (!lastQuestion.response) {
717
+ lastQuestion.response = line.trim();
718
+ }
719
+ }
720
+ }
721
+ }
722
+
723
+ if (currentReq) requirements.push(currentReq);
724
+ return requirements;
725
+ };
726
+
727
+ // Load all sections upfront to show counts immediately
728
+ tree.todoReqs = await loadSection('todo', '⏳ Requirements not yet completed');
729
+ tree.verifyReqs = await loadSection('verify', '✅ Verified by AI screenshot');
730
+ tree.clarificationReqs = await loadClarification();
731
+ tree.verifiedReqs = await loadVerified();
732
+ tree.recycledReqs = await loadSection('recycled', '♻️ Recycled');
733
+
734
+ let inTree = true;
735
+ buildTree();
736
+
737
+ while (inTree) {
738
+ console.clear();
739
+ console.log(chalk.bold.cyan('\n📋 Requirements Navigator\n'));
740
+ console.log(chalk.gray('Use ↑/↓ to navigate, ') + chalk.white('→/Enter') + chalk.gray(' to expand/view questions, ← to go back, ') + chalk.white('R') + chalk.gray(' to Remove, ') + chalk.white('J') + chalk.gray(' to move down, ') + chalk.white('K') + chalk.gray(' to move up, ') + chalk.white('U') + chalk.gray(' Promote (👍), ') + chalk.white('D') + chalk.gray(' Demote/Move to TODO (👎), ') + chalk.white('X/ESC') + chalk.gray(' to Exit.\n'));
741
+
742
+ // Safety check: ensure tree.selected is within bounds
743
+ if (tree.items.length === 0) {
744
+ console.log(chalk.yellow('No items to display.'));
745
+ console.log(chalk.gray('\nPress any key to return to main menu...'));
746
+ await new Promise((resolve) => {
747
+ process.stdin.once('keypress', () => resolve());
748
+ });
749
+ inTree = false;
750
+ continue;
751
+ }
752
+
753
+ if (tree.selected >= tree.items.length) {
754
+ tree.selected = tree.items.length - 1;
755
+ }
756
+
757
+ if (tree.selected < 0) {
758
+ tree.selected = 0;
759
+ }
760
+
761
+ // Calculate window for scrolling (show max 20 items at a time)
762
+ const maxVisible = 20;
763
+ let startIdx = 0;
764
+ let endIdx = tree.items.length;
765
+
766
+ if (tree.items.length > maxVisible) {
767
+ // Center the selected item in the window
768
+ startIdx = Math.max(0, tree.selected - Math.floor(maxVisible / 2));
769
+ endIdx = Math.min(tree.items.length, startIdx + maxVisible);
770
+
771
+ // Adjust if we're near the end
772
+ if (endIdx - startIdx < maxVisible) {
773
+ startIdx = Math.max(0, endIdx - maxVisible);
774
+ }
775
+ }
776
+
777
+ // Show indicator if there are items above
778
+ if (startIdx > 0) {
779
+ console.log(chalk.gray(` ↑ ${startIdx} more above...`));
780
+ }
781
+
782
+ // Display visible tree items
783
+ for (let idx = startIdx; idx < endIdx; idx++) {
784
+ const item = tree.items[idx];
785
+ const indent = ' '.repeat(item.level);
786
+ const arrow = tree.expanded[item.key] ? '▼' : (item.type === 'section' ? '▶' : ' ');
787
+ const prefix = item.type === 'section' || item.type === 'root' ? arrow + ' ' : ' ';
788
+ const selected = idx === tree.selected ? chalk.cyan('❯ ') : ' ';
789
+
790
+ // Truncate long labels to fit terminal width (max 120 chars)
791
+ const maxLabelWidth = 120;
792
+ let label = item.label;
793
+ if (label.length > maxLabelWidth) {
794
+ label = label.substring(0, maxLabelWidth - 3) + '...';
795
+ }
796
+
797
+ console.log(selected + indent + prefix + (idx === tree.selected ? chalk.cyan(label) : chalk.gray(label)));
798
+ }
799
+
800
+ // Show indicator if there are items below
801
+ if (endIdx < tree.items.length) {
802
+ console.log(chalk.gray(` ↓ ${tree.items.length - endIdx} more below...`));
803
+ }
804
+
805
+ console.log();
806
+
807
+ // Handle input
808
+ const key = await new Promise((resolve) => {
809
+ readline.emitKeypressEvents(process.stdin);
810
+ if (process.stdin.isTTY) {
811
+ process.stdin.setRawMode(true);
812
+ }
813
+
814
+ const handler = (str, key) => {
815
+ process.stdin.removeListener('keypress', handler);
816
+ if (process.stdin.isTTY) {
817
+ process.stdin.setRawMode(false);
818
+ }
819
+ resolve(key);
820
+ };
821
+
822
+ process.stdin.on('keypress', handler);
823
+ process.stdin.resume();
824
+ });
825
+
826
+ if (!key) continue;
827
+
828
+ // Handle key presses
829
+ if (key.ctrl && key.name === 'c') {
830
+ // Ctrl+C always exits immediately
831
+ process.exit(0);
832
+ } else if (key.name === 'x' || key.name === 'escape') {
833
+ // X or ESC key - exit CLI with confirmation
834
+ await confirmAndExit();
835
+ } else if (key.name === 'left') {
836
+ const current = tree.items[tree.selected];
837
+ if (!current) continue; // Safety check
838
+
839
+ if (tree.expanded[current.key]) {
840
+ // Collapse expanded section
841
+ tree.expanded[current.key] = false;
842
+ buildTree();
843
+ } else if (current.level > 0) {
844
+ // Go to parent
845
+ for (let i = tree.selected - 1; i >= 0; i--) {
846
+ if (tree.items[i].level < current.level) {
847
+ tree.selected = i;
848
+ break;
849
+ }
850
+ }
851
+ } else {
852
+ // At root level, go back to main menu
853
+ inTree = false;
854
+ }
855
+ } else if (key.name === 'up') {
856
+ tree.selected = Math.max(0, tree.selected - 1);
857
+ } else if (key.name === 'down') {
858
+ tree.selected = Math.min(tree.items.length - 1, tree.selected + 1);
859
+ } else if (key.name === 'right' || key.name === 'return' || key.name === 'space') {
860
+ const current = tree.items[tree.selected];
861
+ if (!current) continue; // Safety check
862
+ if (current.type === 'section') {
863
+ if (!tree.expanded[current.key]) {
864
+ tree.expanded[current.key] = true;
865
+ // Load requirements for this section
866
+ if (current.key === 'todo') {
867
+ tree.todoReqs = await loadSection(current.key, current.section);
868
+ } else if (current.key === 'verify') {
869
+ tree.verifyReqs = await loadSection(current.key, current.section);
870
+ } else if (current.key === 'verified') {
871
+ tree.verifiedReqs = await loadVerified();
872
+ } else if (current.key === 'recycled') {
873
+ tree.recycledReqs = await loadSection(current.key, current.section);
874
+ }
875
+ buildTree();
876
+ } else {
877
+ tree.expanded[current.key] = false;
878
+ buildTree();
879
+ }
880
+ } else if (current.type === 'requirement') {
881
+ // Show requirement actions
882
+ await showRequirementActions(current.req, current.sectionKey, tree);
883
+ buildTree();
884
+ } else if (current.type === 'clarification') {
885
+ // Show clarification requirement with questions
886
+ await showClarificationActions(current.req, tree, loadClarification);
887
+ buildTree();
888
+ } else if (current.type === 'verified') {
889
+ // Show verified item details (read-only)
890
+ console.clear();
891
+ console.log(chalk.bold.green(`\n${current.label}\n`));
892
+ console.log(chalk.gray('(From CHANGELOG.md - read only)'));
893
+ console.log(chalk.gray('\nPress any key to go back...'));
894
+ await new Promise((resolve) => {
895
+ readline.emitKeypressEvents(process.stdin);
896
+ if (process.stdin.isTTY) {
897
+ process.stdin.setRawMode(true);
898
+ }
899
+ const handler = (str, key) => {
900
+ process.stdin.removeListener('keypress', handler);
901
+ if (process.stdin.isTTY) {
902
+ process.stdin.setRawMode(false);
903
+ }
904
+ resolve();
905
+ };
906
+ process.stdin.on('keypress', handler);
907
+ process.stdin.resume();
908
+ });
909
+ } else if (current.type === 'add') {
910
+ // Handle add requirement
911
+ await handleAddRequirement(current.key);
912
+ // Reload TODO section
913
+ tree.todoReqs = await loadSection('todo', '⏳ Requirements not yet completed');
914
+ buildTree();
915
+ }
916
+ } else if (key.name === 'r') {
917
+ const current = tree.items[tree.selected];
918
+ if (!current) continue; // Safety check
919
+
920
+ if (current.type === 'requirement') {
921
+ await deleteRequirement(current.req, current.sectionKey, tree);
922
+ buildTree();
923
+ } else if (current.type === 'clarification') {
924
+ await deleteClarification(current.req, tree);
925
+ tree.clarificationReqs = await loadClarification();
926
+ buildTree();
927
+ } else if (current.type === 'recycled') {
928
+ await deleteRequirement(current.req, current.sectionKey, tree);
929
+ tree.recycledReqs = await loadSection('recycled', '♻️ Recycled');
930
+ buildTree();
931
+ }
932
+ } else if (key.name === 'j') {
933
+ const current = tree.items[tree.selected];
934
+ if (!current) continue; // Safety check
935
+
936
+ if (current.type === 'requirement') {
937
+ await moveRequirementDown(current.req, current.sectionKey, tree);
938
+ buildTree();
939
+ // Move selection down to follow the item
940
+ if (tree.selected < tree.items.length - 1) {
941
+ tree.selected++;
942
+ }
943
+ }
944
+ } else if (key.name === 'k') {
945
+ const current = tree.items[tree.selected];
946
+ if (!current) continue; // Safety check
947
+
948
+ if (current.type === 'requirement') {
949
+ await moveRequirementUp(current.req, current.sectionKey, tree);
950
+ buildTree();
951
+ // Move selection up to follow the item
952
+ if (tree.selected > 0) {
953
+ tree.selected--;
954
+ }
955
+ }
956
+ } else if (key.name === 'u') {
957
+ const current = tree.items[tree.selected];
958
+ if (!current) continue; // Safety check
959
+
960
+ if (current.type === 'requirement') {
961
+ await promoteRequirement(current.req, current.sectionKey, tree, loadSection, loadVerified);
962
+ buildTree();
963
+ }
964
+ } else if (key.name === 'd') {
965
+ const current = tree.items[tree.selected];
966
+ if (!current) continue; // Safety check
967
+
968
+ if (current.type === 'clarification') {
969
+ // D on clarification item = Move to TODO
970
+ await moveClarificationToTodo(current.req, tree);
971
+ tree.clarificationReqs = await loadClarification();
972
+ tree.todoReqs = await loadSection('todo', '⏳ Requirements not yet completed');
973
+ buildTree();
974
+ } else if (current.type === 'requirement' || current.type === 'verified') {
975
+ const sectionKey = current.type === 'verified' ? 'verified' : current.sectionKey;
976
+ const reqTitle = current.type === 'verified' ? current.label : current.req.title;
977
+ await demoteRequirement(reqTitle, sectionKey, tree, loadSection, loadVerified);
978
+ buildTree();
979
+ }
980
+ }
981
+ }
982
+
983
+ process.stdin.pause();
984
+ }
985
+
986
+ // Helper to show goodbye message
987
+ function showGoodbyeMessage() {
988
+ const hour = new Date().getHours();
989
+ const message = hour < 21
990
+ ? '\n👋 Goodbye! Be dreaming about what requirements to add!\n'
991
+ : '\n👋 Goodbye! Go get some sleep!\n';
992
+ console.log(chalk.cyan(message));
993
+ }
994
+
995
+ // Helper to get section title from section key
996
+ function getSectionTitle(sectionKey) {
997
+ if (sectionKey === 'todo') return '⏳ Requirements not yet completed';
998
+ if (sectionKey === 'verify') return '✅ Verified by AI screenshot';
999
+ if (sectionKey === 'recycled') return '♻️ Recycled';
1000
+ return '';
1001
+ }
1002
+
1003
+ // Helper to get requirement list from tree by section key
1004
+ function getRequirementList(tree, sectionKey) {
1005
+ if (sectionKey === 'todo') return tree.todoReqs;
1006
+ if (sectionKey === 'verify') return tree.verifyReqs;
1007
+ if (sectionKey === 'clarification') return tree.clarificationReqs;
1008
+ if (sectionKey === 'recycled') return tree.recycledReqs;
1009
+ return [];
1010
+ }
1011
+
1012
+ // Helper to show confirmation prompt (r/y for yes, N for no, default N)
1013
+ async function confirmAction(message) {
1014
+ console.log();
1015
+ process.stdout.write(chalk.yellow(`${message} `));
1016
+
1017
+ const confirmed = await new Promise((resolve) => {
1018
+ readline.emitKeypressEvents(process.stdin);
1019
+ if (process.stdin.isTTY) {
1020
+ process.stdin.setRawMode(true);
1021
+ }
1022
+
1023
+ const handler = (str, key) => {
1024
+ process.stdin.removeListener('keypress', handler);
1025
+ if (process.stdin.isTTY) {
1026
+ process.stdin.setRawMode(false);
1027
+ }
1028
+
1029
+ if (key && (key.ctrl && key.name === 'c')) {
1030
+ process.exit(0);
1031
+ }
1032
+
1033
+ const keyPressed = key ? key.name : str;
1034
+
1035
+ // Handle Enter as default (No)
1036
+ if (keyPressed === 'return') {
1037
+ console.log('N');
1038
+ resolve(false);
1039
+ } else {
1040
+ console.log(keyPressed || ''); // Echo the key
1041
+ if (keyPressed === 'r' || keyPressed === 'y') {
1042
+ resolve(true);
1043
+ } else {
1044
+ resolve(false);
1045
+ }
1046
+ }
1047
+ };
1048
+
1049
+ process.stdin.on('keypress', handler);
1050
+ process.stdin.resume();
1051
+ });
1052
+
1053
+ return confirmed;
1054
+ }
1055
+
1056
+ // Helper to confirm exit and exit if confirmed (default N)
1057
+ async function confirmAndExit() {
1058
+ console.log(chalk.gray('\n[DEBUG] confirmAndExit called'));
1059
+ console.log();
1060
+ process.stdout.write(chalk.yellow(`Are you sure you want to exit? (${chalk.white('x')}/${chalk.white('y')}/${chalk.white('N')}) `));
1061
+
1062
+ const confirmed = await new Promise((resolve) => {
1063
+ readline.emitKeypressEvents(process.stdin);
1064
+ if (process.stdin.isTTY) {
1065
+ process.stdin.setRawMode(true);
1066
+ }
1067
+
1068
+ const handler = (str, key) => {
1069
+ process.stdin.removeListener('keypress', handler);
1070
+ if (process.stdin.isTTY) {
1071
+ process.stdin.setRawMode(false);
1072
+ }
1073
+
1074
+ if (key && (key.ctrl && key.name === 'c')) {
1075
+ process.exit(0);
1076
+ }
1077
+
1078
+ const keyPressed = key ? key.name : str;
1079
+
1080
+ // Handle Enter as default (No)
1081
+ if (keyPressed === 'return') {
1082
+ console.log('N');
1083
+ resolve(false);
1084
+ } else {
1085
+ console.log(keyPressed || ''); // Echo the key
1086
+ if (keyPressed === 'x' || keyPressed === 'y') {
1087
+ resolve(true);
1088
+ } else {
1089
+ resolve(false);
1090
+ }
1091
+ }
1092
+ };
1093
+
1094
+ process.stdin.on('keypress', handler);
1095
+ process.stdin.resume();
1096
+ });
1097
+
1098
+ if (confirmed) {
1099
+ showGoodbyeMessage();
1100
+ process.exit(0);
1101
+ }
1102
+ }
1103
+
1104
+ // Helper to edit clarification responses
1105
+ async function editClarificationResponses(req, tree) {
1106
+ const { getRequirementsPath } = require('@vibecodingmachine/core');
1107
+ const reqPath = await getRequirementsPath();
1108
+
1109
+ console.clear();
1110
+ console.log(chalk.bold.cyan('\n✍️ Edit Clarification Responses\n'));
1111
+ console.log(chalk.white(`${req.title}\n`));
1112
+
1113
+ const responses = [];
1114
+
1115
+ for (let i = 0; i < req.questions.length; i++) {
1116
+ const q = req.questions[i];
1117
+ console.log(chalk.cyan(`\n${q.question}`));
1118
+
1119
+ if (q.response) {
1120
+ console.log(chalk.gray(`Current response: ${q.response}`));
1121
+ }
1122
+
1123
+ const answer = await inquirer.prompt([{
1124
+ type: 'input',
1125
+ name: 'response',
1126
+ message: 'Your response (press Enter to skip):',
1127
+ default: q.response || ''
1128
+ }]);
1129
+
1130
+ responses.push(answer.response);
1131
+ }
1132
+
1133
+ // Update the file with responses
1134
+ const content = await fs.readFile(reqPath, 'utf8');
1135
+ const lines = content.split('\n');
1136
+
1137
+ let inClarificationSection = false;
1138
+ let inCurrentReq = false;
1139
+ let questionIndex = 0;
1140
+ const newLines = [];
1141
+
1142
+ for (let i = 0; i < lines.length; i++) {
1143
+ const line = lines[i];
1144
+
1145
+ if (line.includes('❓ Requirements needing manual feedback')) {
1146
+ inClarificationSection = true;
1147
+ newLines.push(line);
1148
+ continue;
1149
+ }
1150
+
1151
+ if (inClarificationSection && line.startsWith('##') && !line.includes('❓ Requirements needing manual feedback')) {
1152
+ inClarificationSection = false;
1153
+ newLines.push(line);
1154
+ continue;
1155
+ }
1156
+
1157
+ if (inClarificationSection && line.trim() === `- ${req.title}`) {
1158
+ inCurrentReq = true;
1159
+ questionIndex = 0;
1160
+ newLines.push(line);
1161
+ continue;
1162
+ }
1163
+
1164
+ if (inCurrentReq && (line.trim().startsWith('- ') || line.startsWith('##'))) {
1165
+ inCurrentReq = false;
1166
+ newLines.push(line);
1167
+ continue;
1168
+ }
1169
+
1170
+ if (inCurrentReq && line.trim().match(/^\d+\./)) {
1171
+ // This is a question line
1172
+ newLines.push(line);
1173
+ // Add or update response on next line
1174
+ if (responses[questionIndex]) {
1175
+ newLines.push(` Response: ${responses[questionIndex]}`);
1176
+ }
1177
+ questionIndex++;
1178
+ // Skip existing response line if any
1179
+ if (i + 1 < lines.length && lines[i + 1].trim().startsWith('Response:')) {
1180
+ i++; // Skip the old response line
1181
+ }
1182
+ continue;
1183
+ }
1184
+
1185
+ if (!inCurrentReq || !line.trim().startsWith('Response:')) {
1186
+ newLines.push(line);
1187
+ }
1188
+ }
1189
+
1190
+ await fs.writeFile(reqPath, newLines.join('\n'));
1191
+ console.log(chalk.green('\n✓ Responses saved!'));
1192
+ await new Promise(resolve => setTimeout(resolve, 1000));
1193
+ }
1194
+
1195
+ // Helper to move clarification requirement back to TODO
1196
+ async function moveClarificationToTodo(req, tree) {
1197
+ const { getRequirementsPath } = require('@vibecodingmachine/core');
1198
+ const reqPath = await getRequirementsPath();
1199
+
1200
+ const content = await fs.readFile(reqPath, 'utf8');
1201
+ const lines = content.split('\n');
1202
+
1203
+ let inClarificationSection = false;
1204
+ let inCurrentReq = false;
1205
+ let reqLines = [];
1206
+ let clarificationSectionStart = -1;
1207
+ let todoSectionStart = -1;
1208
+ const newLines = [];
1209
+
1210
+ // First pass: find sections and extract requirement
1211
+ for (let i = 0; i < lines.length; i++) {
1212
+ const line = lines[i];
1213
+
1214
+ if (line.includes('❓ Requirements needing manual feedback')) {
1215
+ inClarificationSection = true;
1216
+ clarificationSectionStart = i;
1217
+ continue;
1218
+ }
1219
+
1220
+ if (line.includes('⏳ Requirements not yet completed')) {
1221
+ todoSectionStart = i;
1222
+ inClarificationSection = false;
1223
+ }
1224
+
1225
+ if (inClarificationSection && line.trim() === `- ${req.title}`) {
1226
+ inCurrentReq = true;
1227
+ reqLines.push(line);
1228
+ continue;
1229
+ }
1230
+
1231
+ if (inCurrentReq && (line.trim().startsWith('- ') || line.startsWith('##'))) {
1232
+ inCurrentReq = false;
1233
+ }
1234
+
1235
+ if (inCurrentReq) {
1236
+ reqLines.push(line);
1237
+ }
1238
+ }
1239
+
1240
+ // Second pass: rebuild file
1241
+ inClarificationSection = false;
1242
+ inCurrentReq = false;
1243
+
1244
+ for (let i = 0; i < lines.length; i++) {
1245
+ const line = lines[i];
1246
+
1247
+ if (line.includes('❓ Requirements needing manual feedback')) {
1248
+ inClarificationSection = true;
1249
+ newLines.push(line);
1250
+ continue;
1251
+ }
1252
+
1253
+ if (line.includes('⏳ Requirements not yet completed')) {
1254
+ inClarificationSection = false;
1255
+ newLines.push(line);
1256
+ // Add the requirement to TODO section (at the top)
1257
+ newLines.push(`- ${req.title}`);
1258
+ continue;
1259
+ }
1260
+
1261
+ if (inClarificationSection && line.trim() === `- ${req.title}`) {
1262
+ inCurrentReq = true;
1263
+ continue; // Skip this line
1264
+ }
1265
+
1266
+ if (inCurrentReq && (line.trim().startsWith('- ') || line.startsWith('##'))) {
1267
+ inCurrentReq = false;
1268
+ }
1269
+
1270
+ if (!inCurrentReq) {
1271
+ newLines.push(line);
1272
+ }
1273
+ }
1274
+
1275
+ await fs.writeFile(reqPath, newLines.join('\n'));
1276
+ console.log(chalk.green('\n✓ Moved to TODO!'));
1277
+ await new Promise(resolve => setTimeout(resolve, 1000));
1278
+ }
1279
+
1280
+ // Helper to move clarification requirement to recycled (used to delete)
1281
+ async function deleteClarification(req, tree) {
1282
+ const { getRequirementsPath } = require('@vibecodingmachine/core');
1283
+ const reqPath = await getRequirementsPath();
1284
+
1285
+ const truncatedTitle = req.title.length > 50 ? req.title.substring(0, 50) + '...' : req.title;
1286
+
1287
+ if (await confirmAction(`Remove? (r/y/N)`)) {
1288
+ // Move to recycled instead of deleting
1289
+ await moveRequirementToRecycled(reqPath, req.title, '❓ Requirements needing manual feedback');
1290
+
1291
+ const content = await fs.readFile(reqPath, 'utf8');
1292
+ const lines = content.split('\n');
1293
+
1294
+ let inClarificationSection = false;
1295
+ let inCurrentReq = false;
1296
+ const newLines = [];
1297
+
1298
+ for (let i = 0; i < lines.length; i++) {
1299
+ const line = lines[i];
1300
+
1301
+ if (line.includes('❓ Requirements needing manual feedback')) {
1302
+ inClarificationSection = true;
1303
+ newLines.push(line);
1304
+ continue;
1305
+ }
1306
+
1307
+ if (inClarificationSection && line.startsWith('##') && !line.includes('❓ Requirements needing manual feedback')) {
1308
+ inClarificationSection = false;
1309
+ }
1310
+
1311
+ if (inClarificationSection && line.trim() === `- ${req.title}`) {
1312
+ inCurrentReq = true;
1313
+ continue; // Skip this line
1314
+ }
1315
+
1316
+ if (inCurrentReq && (line.trim().startsWith('- ') || line.startsWith('##'))) {
1317
+ inCurrentReq = false;
1318
+ }
1319
+
1320
+ if (!inCurrentReq) {
1321
+ newLines.push(line);
1322
+ }
1323
+ }
1324
+
1325
+ await fs.writeFile(reqPath, newLines.join('\n'));
1326
+ }
1327
+ }
1328
+
1329
+ // Helper to show actions for a requirement
1330
+ async function showClarificationActions(req, tree, loadClarification) {
1331
+ const actions = [
1332
+ { label: '✍️ Add/Edit Responses', value: 'edit-responses' },
1333
+ { label: '↩️ Move back to TODO (after clarification)', value: 'move-to-todo' },
1334
+ { label: '🗑️ Delete', value: 'delete' }
1335
+ ];
1336
+
1337
+ let selected = 0;
1338
+
1339
+ while (true) {
1340
+ // Redraw entire screen on each selection change
1341
+ console.clear();
1342
+ console.log(chalk.bold.yellow(`\n❓ Requirement Needing Clarification\n`));
1343
+ console.log(chalk.white(`${req.title}\n`));
1344
+
1345
+ // Display AI findings if available
1346
+ if (req.findings) {
1347
+ console.log(chalk.bold.green('AI Found in Codebase:\n'));
1348
+ console.log(chalk.gray(`${req.findings}\n`));
1349
+ }
1350
+
1351
+ // Display questions
1352
+ console.log(chalk.bold.cyan('Clarifying Questions:\n'));
1353
+ req.questions.forEach((q, idx) => {
1354
+ console.log(chalk.cyan(`${idx + 1}. ${q.question}`));
1355
+ if (q.response) {
1356
+ console.log(chalk.green(` Response: ${q.response}`));
1357
+ } else {
1358
+ console.log(chalk.gray(` (No response yet)`));
1359
+ }
1360
+ console.log();
1361
+ });
1362
+
1363
+ // Display menu
1364
+ console.log(chalk.gray('\nWhat would you like to do? (↑/↓/Enter to select, ESC/← to go back)\n'));
1365
+ actions.forEach((action, idx) => {
1366
+ if (idx === selected) {
1367
+ console.log(chalk.cyan(`❯ ${action.label}`));
1368
+ } else {
1369
+ console.log(` ${action.label}`);
1370
+ }
1371
+ });
1372
+
1373
+ // Handle input
1374
+ const key = await new Promise((resolve) => {
1375
+ readline.emitKeypressEvents(process.stdin);
1376
+ if (process.stdin.isTTY) {
1377
+ process.stdin.setRawMode(true);
1378
+ }
1379
+
1380
+ const handler = (str, key) => {
1381
+ process.stdin.removeListener('keypress', handler);
1382
+ if (process.stdin.isTTY) {
1383
+ process.stdin.setRawMode(false);
1384
+ }
1385
+ resolve(key);
1386
+ };
1387
+
1388
+ process.stdin.once('keypress', handler);
1389
+ });
1390
+
1391
+ if (key.name === 'up') {
1392
+ selected = Math.max(0, selected - 1);
1393
+ } else if (key.name === 'down') {
1394
+ selected = Math.min(actions.length - 1, selected + 1);
1395
+ } else if (key.name === 'return' || key.name === 'space') {
1396
+ const action = actions[selected].value;
1397
+
1398
+ if (action === 'edit-responses') {
1399
+ await editClarificationResponses(req, tree);
1400
+ tree.clarificationReqs = await loadClarification();
1401
+ return;
1402
+ } else if (action === 'move-to-todo') {
1403
+ await moveClarificationToTodo(req, tree);
1404
+ tree.clarificationReqs = await loadClarification();
1405
+ return;
1406
+ } else if (action === 'delete') {
1407
+ await deleteClarification(req, tree);
1408
+ tree.clarificationReqs = await loadClarification();
1409
+ return;
1410
+ }
1411
+ } else if (key.name === 'escape' || key.name === 'left') {
1412
+ return;
1413
+ }
1414
+ }
1415
+ }
1416
+ async function showRequirementActions(req, sectionKey, tree) {
1417
+ const actions = [
1418
+ { label: '👍 Thumbs up (move to top)', value: 'thumbs-up' },
1419
+ { label: '👎 Thumbs down (move to bottom)', value: 'thumbs-down' },
1420
+ { label: '⬆️ Move up', value: 'move-up' },
1421
+ { label: '⬇️ Move down', value: 'move-down' },
1422
+ { label: '🗑️ Delete', value: 'delete' }
1423
+ ];
1424
+
1425
+ let selected = 0;
1426
+ let isFirstRender = true;
1427
+ let lastMenuLines = 0;
1428
+
1429
+ const displayMenu = () => {
1430
+ // Clear previous menu (but not on first render)
1431
+ if (!isFirstRender && lastMenuLines > 0) {
1432
+ // Only move up by menu lines (header stays on screen)
1433
+ readline.moveCursor(process.stdout, 0, -lastMenuLines);
1434
+ readline.clearScreenDown(process.stdout);
1435
+ }
1436
+
1437
+ // Display requirement title and details (only on first render)
1438
+ if (isFirstRender) {
1439
+ console.log(chalk.bold.yellow(`\n${req.title}\n`));
1440
+ if (req.details.length > 0) {
1441
+ console.log(chalk.gray(req.details.join('\n')));
1442
+ console.log();
1443
+ }
1444
+ }
1445
+
1446
+ // Track menu lines printed this render (this is what we clear and reprint)
1447
+ let menuLines = 0;
1448
+
1449
+ // Display menu (always reprinted)
1450
+ console.log();
1451
+ console.log(chalk.gray('What would you like to do? (↑/↓/Enter to select, ESC/← to go back)'));
1452
+ console.log();
1453
+ menuLines += 3; // Blank line + help text + blank line
1454
+ actions.forEach((action, idx) => {
1455
+ if (idx === selected) {
1456
+ console.log(chalk.cyan(`❯ ${action.label}`));
1457
+ } else {
1458
+ console.log(` ${action.label}`);
1459
+ }
1460
+ menuLines++;
1461
+ });
1462
+
1463
+ lastMenuLines = menuLines;
1464
+ isFirstRender = false;
1465
+ };
1466
+
1467
+ while (true) {
1468
+ displayMenu();
1469
+
1470
+ // Handle input
1471
+ const key = await new Promise((resolve) => {
1472
+ readline.emitKeypressEvents(process.stdin);
1473
+ if (process.stdin.isTTY) {
1474
+ process.stdin.setRawMode(true);
1475
+ }
1476
+
1477
+ const handler = (str, key) => {
1478
+ process.stdin.removeListener('keypress', handler);
1479
+ if (process.stdin.isTTY) {
1480
+ process.stdin.setRawMode(false);
1481
+ }
1482
+ resolve(key);
1483
+ };
1484
+
1485
+ process.stdin.on('keypress', handler);
1486
+ process.stdin.resume();
1487
+ });
1488
+
1489
+ if (!key) continue;
1490
+
1491
+ if ((key.ctrl && key.name === 'c') || key.name === 'escape' || key.name === 'left') {
1492
+ return; // Go back
1493
+ } else if (key.name === 'up') {
1494
+ selected = Math.max(0, selected - 1);
1495
+ } else if (key.name === 'down') {
1496
+ selected = Math.min(actions.length - 1, selected + 1);
1497
+ } else if (key.name === 'return' || key.name === 'right') {
1498
+ await performRequirementAction(actions[selected].value, req, sectionKey, tree);
1499
+ return;
1500
+ }
1501
+ }
1502
+ }
1503
+
1504
+ // Helper to perform action on requirement
1505
+ async function performRequirementAction(action, req, sectionKey, tree) {
1506
+ const reqList = getRequirementList(tree, sectionKey);
1507
+ const sectionTitle = getSectionTitle(sectionKey);
1508
+ const reqIndex = reqList.findIndex(r => r.title === req.title);
1509
+
1510
+
1511
+ if (reqIndex === -1) return;
1512
+
1513
+ const { getRequirementsPath } = require('@vibecodingmachine/core');
1514
+ const reqPath = await getRequirementsPath();
1515
+
1516
+ switch (action) {
1517
+ case 'thumbs-up':
1518
+ const thumbsUpReq = reqList.splice(reqIndex, 1)[0];
1519
+ reqList.unshift(thumbsUpReq);
1520
+ await saveRequirementsOrder(reqPath, sectionTitle, reqList);
1521
+ console.log(chalk.green('\n✓ Moved to top\n'));
1522
+ break;
1523
+ case 'thumbs-down':
1524
+ const thumbsDownReq = reqList.splice(reqIndex, 1)[0];
1525
+ reqList.push(thumbsDownReq);
1526
+ await saveRequirementsOrder(reqPath, sectionTitle, reqList);
1527
+ console.log(chalk.yellow('\n✓ Moved to bottom\n'));
1528
+ break;
1529
+ case 'move-up':
1530
+ if (reqIndex > 0) {
1531
+ [reqList[reqIndex], reqList[reqIndex - 1]] = [reqList[reqIndex - 1], reqList[reqIndex]];
1532
+ await saveRequirementsOrder(reqPath, sectionTitle, reqList);
1533
+ console.log(chalk.green('\n✓ Moved up\n'));
1534
+ }
1535
+ break;
1536
+ case 'move-down':
1537
+ if (reqIndex < reqList.length - 1) {
1538
+ [reqList[reqIndex], reqList[reqIndex + 1]] = [reqList[reqIndex + 1], reqList[reqIndex]];
1539
+ await saveRequirementsOrder(reqPath, sectionTitle, reqList);
1540
+ console.log(chalk.green('\n✓ Moved down\n'));
1541
+ }
1542
+ break;
1543
+ case 'delete':
1544
+ if (await confirmAction('Are you sure you want to delete this requirement?')) {
1545
+ reqList.splice(reqIndex, 1);
1546
+ await saveRequirementsOrder(reqPath, sectionTitle, reqList);
1547
+ console.log(chalk.green('\n✓ Deleted\n'));
1548
+ }
1549
+ break;
1550
+ }
1551
+
1552
+ await new Promise(resolve => setTimeout(resolve, 1000));
1553
+ }
1554
+
1555
+ // Helper to move requirement to recycled section (used to delete)
1556
+ async function deleteRequirement(req, sectionKey, tree) {
1557
+ const reqList = getRequirementList(tree, sectionKey);
1558
+ const sectionTitle = getSectionTitle(sectionKey);
1559
+ const reqIndex = reqList.findIndex(r => r.title === req.title);
1560
+
1561
+ if (reqIndex === -1) return;
1562
+
1563
+ const { getRequirementsPath } = require('@vibecodingmachine/core');
1564
+ const reqPath = await getRequirementsPath();
1565
+
1566
+ const truncatedTitle = req.title.substring(0, 50) + (req.title.length > 50 ? '...' : '');
1567
+ if (await confirmAction(`Remove? (r/y/N)`)) {
1568
+ // Move to recycled instead of deleting
1569
+ await moveRequirementToRecycled(reqPath, req.title, sectionTitle);
1570
+ reqList.splice(reqIndex, 1);
1571
+ }
1572
+ }
1573
+
1574
+ // Helper to move requirement to recycled section
1575
+ async function moveRequirementToRecycled(reqPath, requirementTitle, fromSection) {
1576
+ const content = await fs.readFile(reqPath, 'utf8');
1577
+ const lines = content.split('\n');
1578
+
1579
+ // Find the requirement block (### header format)
1580
+ let requirementStartIndex = -1;
1581
+ let requirementEndIndex = -1;
1582
+
1583
+ for (let i = 0; i < lines.length; i++) {
1584
+ const line = lines[i].trim();
1585
+ if (line.startsWith('###')) {
1586
+ const title = line.replace(/^###\s*/, '').trim();
1587
+ if (title && title.includes(requirementTitle)) {
1588
+ requirementStartIndex = i;
1589
+ // Find the end of this requirement (next ### or ## header)
1590
+ for (let j = i + 1; j < lines.length; j++) {
1591
+ const nextLine = lines[j].trim();
1592
+ if (nextLine.startsWith('###') || (nextLine.startsWith('##') && !nextLine.startsWith('###'))) {
1593
+ requirementEndIndex = j;
1594
+ break;
1595
+ }
1596
+ }
1597
+ if (requirementEndIndex === -1) {
1598
+ requirementEndIndex = lines.length;
1599
+ }
1600
+ break;
1601
+ }
1602
+ }
1603
+ }
1604
+
1605
+ if (requirementStartIndex === -1) {
1606
+ console.log(chalk.yellow('⚠️ Could not find requirement to recycle'));
1607
+ return;
1608
+ }
1609
+
1610
+ // Extract the requirement block
1611
+ const requirementBlock = lines.slice(requirementStartIndex, requirementEndIndex);
1612
+
1613
+ // Remove the requirement from its current location
1614
+ lines.splice(requirementStartIndex, requirementEndIndex - requirementStartIndex);
1615
+
1616
+ // Find or create Recycled section
1617
+ let recycledIndex = -1;
1618
+ for (let i = 0; i < lines.length; i++) {
1619
+ if (lines[i].includes('♻️ Recycled') || lines[i].includes('🗑️ Recycled')) {
1620
+ recycledIndex = i;
1621
+ break;
1622
+ }
1623
+ }
1624
+
1625
+ // If Recycled section doesn't exist, create it before the last section
1626
+ if (recycledIndex === -1) {
1627
+ // Find the last section header
1628
+ let lastSectionIndex = -1;
1629
+ for (let i = lines.length - 1; i >= 0; i--) {
1630
+ if (lines[i].startsWith('##') && !lines[i].startsWith('###')) {
1631
+ lastSectionIndex = i;
1632
+ break;
1633
+ }
1634
+ }
1635
+
1636
+ // Insert new Recycled section
1637
+ const insertIndex = lastSectionIndex > 0 ? lastSectionIndex : lines.length;
1638
+ lines.splice(insertIndex, 0, '', '## ♻️ Recycled', '');
1639
+ recycledIndex = insertIndex + 1;
1640
+ }
1641
+
1642
+ // Insert requirement block at TOP of Recycled list
1643
+ let insertIndex = recycledIndex + 1;
1644
+ while (insertIndex < lines.length && lines[insertIndex].trim() === '') {
1645
+ insertIndex++;
1646
+ }
1647
+ lines.splice(insertIndex, 0, ...requirementBlock);
1648
+ // Add blank line after if needed
1649
+ if (insertIndex + requirementBlock.length < lines.length && lines[insertIndex + requirementBlock.length].trim() !== '') {
1650
+ lines.splice(insertIndex + requirementBlock.length, 0, '');
1651
+ }
1652
+
1653
+ // Save
1654
+ await fs.writeFile(reqPath, lines.join('\n'));
1655
+ }
1656
+
1657
+ // Helper to move requirement down with 'j' key
1658
+ async function moveRequirementDown(req, sectionKey, tree) {
1659
+ const reqList = getRequirementList(tree, sectionKey);
1660
+ const sectionTitle = getSectionTitle(sectionKey);
1661
+ const reqIndex = reqList.findIndex(r => r.title === req.title);
1662
+
1663
+ if (reqIndex === -1 || reqIndex >= reqList.length - 1) return;
1664
+
1665
+ const { getRequirementsPath } = require('@vibecodingmachine/core');
1666
+ const reqPath = await getRequirementsPath();
1667
+
1668
+ [reqList[reqIndex], reqList[reqIndex + 1]] = [reqList[reqIndex + 1], reqList[reqIndex]];
1669
+ await saveRequirementsOrder(reqPath, sectionTitle, reqList);
1670
+ }
1671
+
1672
+ // Helper to move requirement up with 'k' key
1673
+ async function moveRequirementUp(req, sectionKey, tree) {
1674
+ const reqList = getRequirementList(tree, sectionKey);
1675
+ const sectionTitle = getSectionTitle(sectionKey);
1676
+ const reqIndex = reqList.findIndex(r => r.title === req.title);
1677
+
1678
+ if (reqIndex === -1 || reqIndex === 0) return;
1679
+
1680
+ const { getRequirementsPath } = require('@vibecodingmachine/core');
1681
+ const reqPath = await getRequirementsPath();
1682
+
1683
+ [reqList[reqIndex], reqList[reqIndex - 1]] = [reqList[reqIndex - 1], reqList[reqIndex]];
1684
+ await saveRequirementsOrder(reqPath, sectionTitle, reqList);
1685
+ }
1686
+
1687
+ // Helper to promote requirement to next list (TODO -> TO VERIFY -> VERIFIED)
1688
+ async function promoteRequirement(req, sectionKey, tree, loadSection, loadVerified) {
1689
+ const { getRequirementsPath, promoteTodoToVerify, promoteToVerified } = require('@vibecodingmachine/core');
1690
+ const reqPath = await getRequirementsPath();
1691
+
1692
+ if (sectionKey === 'todo') {
1693
+ // TODO -> TO VERIFY: Use shared function
1694
+ const success = await promoteTodoToVerify(reqPath, req.title);
1695
+ if (success) {
1696
+ // Reload sections
1697
+ tree.todoReqs = await loadSection('todo', '⏳ Requirements not yet completed');
1698
+ tree.verifyReqs = await loadSection('verify', '✅ Verified by AI screenshot');
1699
+ }
1700
+ } else if (sectionKey === 'verify') {
1701
+ // TO VERIFY -> VERIFIED: Use shared function
1702
+ const success = await promoteToVerified(reqPath, req.title);
1703
+ if (success) {
1704
+ // Reload sections
1705
+ tree.verifyReqs = await loadSection('verify', '✅ Verified by AI screenshot');
1706
+ tree.verifiedReqs = await loadVerified();
1707
+ }
1708
+ }
1709
+ }
1710
+
1711
+ // Helper to demote requirement to previous list (VERIFIED -> TODO, TO VERIFY -> TODO)
1712
+ async function demoteRequirement(reqTitle, sectionKey, tree, loadSection, loadVerified) {
1713
+ const { getRequirementsPath, demoteVerifyToTodo, demoteFromVerifiedToTodo } = require('@vibecodingmachine/core');
1714
+ const reqPath = await getRequirementsPath();
1715
+
1716
+ if (sectionKey === 'verify') {
1717
+ // TO VERIFY -> TODO: Use shared function
1718
+ const success = await demoteVerifyToTodo(reqPath, reqTitle);
1719
+ if (success) {
1720
+ // Reload sections
1721
+ tree.todoReqs = await loadSection('todo', '⏳ Requirements not yet completed');
1722
+ tree.verifyReqs = await loadSection('verify', '✅ Verified by AI screenshot');
1723
+ }
1724
+ } else if (sectionKey === 'verified') {
1725
+ // VERIFIED -> TODO: Use shared function (with TRY AGAIN prefix)
1726
+ const success = await demoteFromVerifiedToTodo(reqPath, reqTitle);
1727
+ if (success) {
1728
+ // Reload sections
1729
+ tree.todoReqs = await loadSection('todo', '⏳ Requirements not yet completed');
1730
+ tree.verifiedReqs = await loadVerified();
1731
+ }
1732
+ }
1733
+ }
1734
+
1735
+ // Helper to handle adding requirements
1736
+ async function handleAddRequirement(type) {
1737
+ const reqCommands = require('../commands/requirements');
1738
+ const packages = ['all', 'cli', 'core', 'electron-app', 'web', 'mobile'];
1739
+
1740
+ if (type === 'add-one') {
1741
+ try {
1742
+ // Get saved package from config
1743
+ const config = await readConfig();
1744
+ let selectedPackage = config.lastPackage || ['all'];
1745
+ // Ensure it's an array
1746
+ if (typeof selectedPackage === 'string') selectedPackage = [selectedPackage];
1747
+
1748
+ let askPackage = !config.lastPackage;
1749
+ let name = '';
1750
+ let pkg = selectedPackage;
1751
+
1752
+ while (true) {
1753
+ if (askPackage) {
1754
+ const answer = await inquirer.prompt([{
1755
+ type: 'checkbox',
1756
+ name: 'pkg',
1757
+ message: 'Select package(s):',
1758
+ choices: packages.map(p => ({
1759
+ name: p,
1760
+ value: p,
1761
+ checked: pkg.includes(p)
1762
+ })),
1763
+ validate: (answer) => {
1764
+ if (answer.length < 1) return 'You must choose at least one package.';
1765
+ return true;
1766
+ }
1767
+ }]);
1768
+ pkg = answer.pkg;
1769
+
1770
+ // Save to config
1771
+ config.lastPackage = pkg;
1772
+ await writeConfig(config);
1773
+ askPackage = false;
1774
+ }
1775
+
1776
+ // Ask for requirement name
1777
+ const pkgDisplay = pkg.join(', ');
1778
+ const nameAnswer = await inquirer.prompt([{
1779
+ type: 'input',
1780
+ name: 'name',
1781
+ message: `Enter requirement name (Package: ${pkgDisplay}) [Type < to change package]:`
1782
+ }]);
1783
+
1784
+ name = nameAnswer.name;
1785
+
1786
+ if (name === '<') {
1787
+ askPackage = true;
1788
+ continue;
1789
+ }
1790
+
1791
+ break;
1792
+ }
1793
+
1794
+ // Then ask for multiline description (press Enter twice to finish)
1795
+ console.log(chalk.gray('\nEnter description (press Enter twice on empty line to finish):\n'));
1796
+ const descriptionLines = [];
1797
+ let emptyLineCount = 0;
1798
+ let isFirstLine = true;
1799
+
1800
+ while (true) {
1801
+ try {
1802
+ const { line } = await inquirer.prompt([{
1803
+ type: 'input',
1804
+ name: 'line',
1805
+ message: isFirstLine ? 'Description:' : ''
1806
+ }]);
1807
+
1808
+ isFirstLine = false;
1809
+
1810
+ if (line.trim() === '') {
1811
+ emptyLineCount++;
1812
+ if (emptyLineCount >= 2) {
1813
+ break; // Two empty lines = done
1814
+ }
1815
+ } else {
1816
+ emptyLineCount = 0;
1817
+ descriptionLines.push(line);
1818
+ }
1819
+ } catch (err) {
1820
+ break; // ESC pressed
1821
+ }
1822
+ }
1823
+
1824
+ const description = descriptionLines.join('\n');
1825
+ await reqCommands.add(name, pkg, description);
1826
+ // Message already printed by reqCommands.add()
1827
+ await new Promise(resolve => setTimeout(resolve, 1000));
1828
+ } catch (err) {
1829
+ // ESC pressed
1830
+ }
1831
+ } else if (type === 'add-many') {
1832
+ try {
1833
+ console.log(chalk.cyan('\nAdding multiple requirements:\n'));
1834
+ const requirements = [];
1835
+ let done = false;
1836
+
1837
+ while (!done) {
1838
+ try {
1839
+ // Ask for package
1840
+ const { package } = await inquirer.prompt([{
1841
+ type: 'list',
1842
+ name: 'package',
1843
+ message: `Package for requirement ${requirements.length + 1}:`,
1844
+ choices: packages.map(p => ({ name: p, value: p })),
1845
+ default: 'all'
1846
+ }]);
1847
+
1848
+ // Ask for name
1849
+ const { name } = await inquirer.prompt([{
1850
+ type: 'input',
1851
+ name: 'name',
1852
+ message: `Name for requirement ${requirements.length + 1}:`
1853
+ }]);
1854
+
1855
+ if (name.trim() === '') {
1856
+ done = true;
1857
+ } else {
1858
+ // Ask for description
1859
+ console.log(chalk.gray('\nEnter description (press Enter twice on empty line to finish):\n'));
1860
+ const descriptionLines = [];
1861
+ let emptyLineCount = 0;
1862
+ let isFirstLine = true;
1863
+
1864
+ while (true) {
1865
+ try {
1866
+ const { line } = await inquirer.prompt([{
1867
+ type: 'input',
1868
+ name: 'line',
1869
+ message: isFirstLine ? 'Description:' : ''
1870
+ }]);
1871
+
1872
+ isFirstLine = false;
1873
+
1874
+ if (line.trim() === '') {
1875
+ emptyLineCount++;
1876
+ if (emptyLineCount >= 2) {
1877
+ break;
1878
+ }
1879
+ } else {
1880
+ emptyLineCount = 0;
1881
+ descriptionLines.push(line);
1882
+ }
1883
+ } catch (err) {
1884
+ break;
1885
+ }
1886
+ }
1887
+
1888
+ const description = descriptionLines.join('\n');
1889
+ requirements.push({ name, package, description });
1890
+ }
1891
+ } catch (err) {
1892
+ done = true;
1893
+ }
1894
+ }
1895
+
1896
+ if (requirements.length > 0) {
1897
+ for (const req of requirements) {
1898
+ await reqCommands.add(req.name, req.package, req.description);
1899
+ }
1900
+ console.log(chalk.green(`\n✓ Added ${requirements.length} requirement(s)\n`));
1901
+ await new Promise(resolve => setTimeout(resolve, 1000));
1902
+ }
1903
+ } catch (err) {
1904
+ // ESC pressed
1905
+ }
1906
+ }
1907
+ }
1908
+
1909
+ // Helper to show requirements from a specific section with actions
1910
+ async function showRequirementsBySection(sectionTitle) {
1911
+ const { getRequirementsPath } = require('@vibecodingmachine/core');
1912
+ const reqPath = await getRequirementsPath();
1913
+
1914
+ if (!reqPath || !await fs.pathExists(reqPath)) {
1915
+ console.log(chalk.yellow('No requirements file found.'));
1916
+ return;
1917
+ }
1918
+
1919
+ const content = await fs.readFile(reqPath, 'utf8');
1920
+ const lines = content.split('\n');
1921
+
1922
+ let inSection = false;
1923
+ const requirements = [];
1924
+
1925
+ for (let i = 0; i < lines.length; i++) {
1926
+ const line = lines[i];
1927
+
1928
+ // Check if we're entering the target section
1929
+ if (line.includes(sectionTitle)) {
1930
+ inSection = true;
1931
+ continue;
1932
+ }
1933
+
1934
+ // Check if we're leaving the section (hit another ## section)
1935
+ if (inSection && line.startsWith('## ') && !line.includes(sectionTitle)) {
1936
+ break;
1937
+ }
1938
+
1939
+ // Collect requirements in the section (formatted as bullet points with -)
1940
+ if (inSection && line.trim().startsWith('- ')) {
1941
+ const title = line.trim().substring(2); // Remove "- " prefix
1942
+ const details = [];
1943
+
1944
+ // Collect details (continuation lines until next bullet point or section)
1945
+ for (let j = i + 1; j < lines.length; j++) {
1946
+ if (lines[j].trim().startsWith('- ') || lines[j].startsWith('##')) {
1947
+ break;
1948
+ }
1949
+ if (lines[j].trim()) {
1950
+ details.push(lines[j].trim());
1951
+ }
1952
+ }
1953
+
1954
+ requirements.push({ title, details, lineIndex: i });
1955
+ }
1956
+ }
1957
+
1958
+ if (requirements.length === 0) {
1959
+ console.log(chalk.gray(' (No requirements in this section)'));
1960
+ return;
1961
+ }
1962
+
1963
+ // Show interactive list
1964
+ let inRequirementsList = true;
1965
+ while (inRequirementsList) {
1966
+ try {
1967
+ console.log(chalk.bold.cyan(`\n${sectionTitle}\n`));
1968
+ console.log(chalk.green(`Total: ${requirements.length} requirement(s)\n`));
1969
+
1970
+ const choices = requirements.map((req, index) => ({
1971
+ name: `${index + 1}. ${req.title}`,
1972
+ value: index
1973
+ }));
1974
+
1975
+ const { selectedIndex } = await inquirer.prompt([{
1976
+ type: 'list',
1977
+ name: 'selectedIndex',
1978
+ message: 'Select a requirement (ESC to go back):',
1979
+ choices: choices,
1980
+ pageSize: 15
1981
+ }]);
1982
+
1983
+ // Show actions for selected requirement
1984
+ const requirement = requirements[selectedIndex];
1985
+ console.log(chalk.bold.yellow(`\n${requirement.title}\n`));
1986
+ if (requirement.details.length > 0) {
1987
+ console.log(chalk.gray(requirement.details.join('\n')));
1988
+ console.log();
1989
+ }
1990
+
1991
+ const { action } = await inquirer.prompt([{
1992
+ type: 'list',
1993
+ name: 'action',
1994
+ message: 'What would you like to do? (ESC to go back)',
1995
+ choices: [
1996
+ { name: '👍 Thumbs up (prioritize)', value: 'thumbs-up' },
1997
+ { name: '👎 Thumbs down (deprioritize)', value: 'thumbs-down' },
1998
+ { name: '⬆️ Move up', value: 'move-up' },
1999
+ { name: '⬇️ Move down', value: 'move-down' },
2000
+ { name: '🗑️ Delete', value: 'delete' }
2001
+ ]
2002
+ }]);
2003
+
2004
+ // Perform action
2005
+ switch (action) {
2006
+ case 'thumbs-up':
2007
+ // Move to top of list
2008
+ const thumbsUpReq = requirements.splice(selectedIndex, 1)[0];
2009
+ requirements.unshift(thumbsUpReq);
2010
+ await saveRequirementsOrder(reqPath, sectionTitle, requirements);
2011
+ console.log(chalk.green('\n✓ Moved to top (prioritized)\n'));
2012
+ break;
2013
+ case 'thumbs-down':
2014
+ // Move to bottom of list
2015
+ const thumbsDownReq = requirements.splice(selectedIndex, 1)[0];
2016
+ requirements.push(thumbsDownReq);
2017
+ await saveRequirementsOrder(reqPath, sectionTitle, requirements);
2018
+ console.log(chalk.yellow('\n✓ Moved to bottom (deprioritized)\n'));
2019
+ break;
2020
+ case 'move-up':
2021
+ if (selectedIndex > 0) {
2022
+ [requirements[selectedIndex], requirements[selectedIndex - 1]] =
2023
+ [requirements[selectedIndex - 1], requirements[selectedIndex]];
2024
+ await saveRequirementsOrder(reqPath, sectionTitle, requirements);
2025
+ console.log(chalk.green('\n✓ Moved up\n'));
2026
+ } else {
2027
+ console.log(chalk.yellow('\n⚠ Already at top\n'));
2028
+ }
2029
+ break;
2030
+ case 'move-down':
2031
+ if (selectedIndex < requirements.length - 1) {
2032
+ [requirements[selectedIndex], requirements[selectedIndex + 1]] =
2033
+ [requirements[selectedIndex + 1], requirements[selectedIndex]];
2034
+ await saveRequirementsOrder(reqPath, sectionTitle, requirements);
2035
+ console.log(chalk.green('\n✓ Moved down\n'));
2036
+ } else {
2037
+ console.log(chalk.yellow('\n⚠ Already at bottom\n'));
2038
+ }
2039
+ break;
2040
+ case 'delete':
2041
+ const { confirmDelete } = await inquirer.prompt([{
2042
+ type: 'confirm',
2043
+ name: 'confirmDelete',
2044
+ message: 'Are you sure you want to delete this requirement?',
2045
+ default: false
2046
+ }]);
2047
+ if (confirmDelete) {
2048
+ requirements.splice(selectedIndex, 1);
2049
+ await saveRequirementsOrder(reqPath, sectionTitle, requirements);
2050
+ console.log(chalk.green('\n✓ Requirement deleted\n'));
2051
+ if (requirements.length === 0) {
2052
+ console.log(chalk.gray('No more requirements in this section.\n'));
2053
+ inRequirementsList = false;
2054
+ }
2055
+ } else {
2056
+ console.log(chalk.yellow('\nCancelled\n'));
2057
+ }
2058
+ break;
2059
+ }
2060
+ } catch (error) {
2061
+ // ESC pressed - exit list
2062
+ inRequirementsList = false;
2063
+ }
2064
+ }
2065
+ }
2066
+
2067
+ // Helper to save reordered requirements back to file
2068
+ async function saveRequirementsOrder(reqPath, sectionTitle, requirements) {
2069
+ const content = await fs.readFile(reqPath, 'utf8');
2070
+ const lines = content.split('\n');
2071
+
2072
+ let inSection = false;
2073
+ let sectionStartIndex = -1;
2074
+ let sectionEndIndex = -1;
2075
+
2076
+ // Find section boundaries
2077
+ for (let i = 0; i < lines.length; i++) {
2078
+ if (lines[i].includes(sectionTitle)) {
2079
+ inSection = true;
2080
+ sectionStartIndex = i;
2081
+ continue;
2082
+ }
2083
+ // Stop at next ## header (but not ### which are requirement headers)
2084
+ if (inSection && lines[i].startsWith('## ') && !lines[i].startsWith('###') && !lines[i].includes(sectionTitle)) {
2085
+ sectionEndIndex = i;
2086
+ break;
2087
+ }
2088
+ }
2089
+
2090
+ if (sectionEndIndex === -1) {
2091
+ sectionEndIndex = lines.length;
2092
+ }
2093
+
2094
+ // Rebuild section with new order (### header format)
2095
+ const newSectionLines = [lines[sectionStartIndex]];
2096
+ for (const req of requirements) {
2097
+ // Add requirement header
2098
+ newSectionLines.push(`### ${req.title}`);
2099
+ // Add package if present
2100
+ if (req.package && req.package !== 'all') {
2101
+ newSectionLines.push(`PACKAGE: ${req.package}`);
2102
+ }
2103
+ // Add description if present
2104
+ if (req.details && req.details.length > 0) {
2105
+ req.details.forEach(line => {
2106
+ if (line.trim()) {
2107
+ newSectionLines.push(line);
2108
+ }
2109
+ });
2110
+ }
2111
+ // Add blank line after requirement
2112
+ newSectionLines.push('');
2113
+ }
2114
+
2115
+ // Replace section in content
2116
+ const newLines = [
2117
+ ...lines.slice(0, sectionStartIndex),
2118
+ ...newSectionLines,
2119
+ ...lines.slice(sectionEndIndex)
2120
+ ];
2121
+
2122
+ await fs.writeFile(reqPath, newLines.join('\n'));
2123
+ }
2124
+
2125
+ // Helper to show verified requirements from CHANGELOG
2126
+ async function showRequirementsFromChangelog() {
2127
+ const { getVibeCodingMachineDir, checkVibeCodingMachineExists } = require('@vibecodingmachine/core');
2128
+
2129
+ const allnightStatus = await checkVibeCodingMachineExists();
2130
+ let changelogPath;
2131
+
2132
+ if (allnightStatus.insideExists) {
2133
+ const allnightDir = await getVibeCodingMachineDir();
2134
+ changelogPath = path.join(path.dirname(allnightDir), 'CHANGELOG.md');
2135
+ } else if (allnightStatus.siblingExists) {
2136
+ changelogPath = path.join(process.cwd(), 'CHANGELOG.md');
2137
+ }
2138
+
2139
+ if (!changelogPath || !await fs.pathExists(changelogPath)) {
2140
+ console.log(chalk.yellow('No CHANGELOG.md found.'));
2141
+ return;
2142
+ }
2143
+
2144
+ const content = await fs.readFile(changelogPath, 'utf8');
2145
+ const lines = content.split('\n');
2146
+ let count = 0;
2147
+
2148
+ console.log(chalk.bold.cyan('Verified Requirements (from CHANGELOG.md):\n'));
2149
+
2150
+ for (const line of lines) {
2151
+ const trimmed = line.trim();
2152
+ if (trimmed.startsWith('- ') && trimmed.length > 10) {
2153
+ count++;
2154
+ console.log(chalk.green(`${count}. ${trimmed.substring(2)}`));
2155
+ }
2156
+ }
2157
+
2158
+ if (count === 0) {
2159
+ console.log(chalk.gray(' (No verified requirements yet)'));
2160
+ } else {
2161
+ console.log(chalk.green(`\nTotal: ${count} requirement(s)`));
2162
+ }
2163
+ }
2164
+
2165
+ // Custom menu with both arrow keys and letter shortcuts
2166
+ async function showQuickMenu(items, initialSelectedIndex = 0) {
2167
+ return new Promise((resolve) => {
2168
+ // Skip blank and info items when setting initial index
2169
+ let selectedIndex = initialSelectedIndex;
2170
+ while (selectedIndex < items.length && (items[selectedIndex].type === 'blank' || items[selectedIndex].type === 'info')) {
2171
+ selectedIndex++;
2172
+ }
2173
+ if (selectedIndex >= items.length) selectedIndex = 0;
2174
+
2175
+ let isFirstRender = true;
2176
+ let lastLinesPrinted = 0;
2177
+
2178
+ // Helper to calculate visual lines occupied by text
2179
+ const getVisualLineCount = (text) => {
2180
+ const columns = process.stdout.columns || 80;
2181
+ let lineCount = 0;
2182
+
2183
+ // Strip ANSI codes for length calculation
2184
+ // Simple regex for stripping ANSI codes
2185
+ const stripAnsi = (str) => str.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '');
2186
+
2187
+ const lines = text.split('\n');
2188
+ for (const line of lines) {
2189
+ const visualLength = stripAnsi(line).length;
2190
+ // If line is empty, it still takes 1 line
2191
+ if (visualLength === 0) {
2192
+ lineCount += 1;
2193
+ } else {
2194
+ // Calculate wrapped lines
2195
+ lineCount += Math.ceil(visualLength / columns);
2196
+ }
2197
+ }
2198
+ return lineCount;
2199
+ };
2200
+
2201
+ const displayMenu = () => {
2202
+ // Clear previous menu (move cursor up and clear lines) - but not on first render
2203
+ if (!isFirstRender && lastLinesPrinted > 0) {
2204
+ // Move cursor up by the number of lines we printed last time
2205
+ readline.moveCursor(process.stdout, 0, -lastLinesPrinted);
2206
+ readline.clearScreenDown(process.stdout);
2207
+ }
2208
+ isFirstRender = false;
2209
+
2210
+ // Track lines printed this render
2211
+ let linesPrinted = 0;
2212
+
2213
+ // Display menu with highlight - all items get letters except info items
2214
+ let letterIndex = 0;
2215
+ items.forEach((item, index) => {
2216
+ const isSelected = index === selectedIndex;
2217
+ let output = '';
2218
+
2219
+ if (item.type === 'blank') {
2220
+ // Blank separator line
2221
+ console.log();
2222
+ linesPrinted++;
2223
+ } else if (item.type === 'info') {
2224
+ // Info items are display-only, no letter, no selection
2225
+ output = chalk.gray(` ${item.name}`);
2226
+ console.log(output);
2227
+ linesPrinted += getVisualLineCount(` ${item.name}`);
2228
+ } else {
2229
+ // All other items have letters (Exit always uses 'x')
2230
+ let letter;
2231
+ if (item.value === 'exit') {
2232
+ letter = 'x';
2233
+ } else {
2234
+ letter = indexToLetter(letterIndex);
2235
+ letterIndex++;
2236
+ }
2237
+
2238
+ if (isSelected) {
2239
+ output = chalk.cyan(`❯ ${letter}) ${item.name}`);
2240
+ console.log(output);
2241
+ } else if (item.type === 'setting') {
2242
+ // Settings in gray
2243
+ output = chalk.gray(` ${letter}) ${item.name}`);
2244
+ console.log(output);
2245
+ } else {
2246
+ // Actions in default color
2247
+ output = ` ${letter}) ${item.name}`;
2248
+ console.log(output);
2249
+ }
2250
+
2251
+ // Calculate lines based on the actual text printed (including indentation/prefix)
2252
+ // We reconstruct the raw string that was logged to calculate wrapping correctly
2253
+ let rawString = '';
2254
+ if (isSelected) {
2255
+ rawString = `❯ ${letter}) ${item.name}`;
2256
+ } else {
2257
+ rawString = ` ${letter}) ${item.name}`;
2258
+ }
2259
+ linesPrinted += getVisualLineCount(rawString);
2260
+ }
2261
+ });
2262
+
2263
+ // Count all items with letters (excluding exit, blank, and info)
2264
+ const letterCount = items.filter(item => item.type !== 'blank' && item.type !== 'info' && item.value !== 'exit').length;
2265
+ const lastLetter = letterCount > 0 ? indexToLetter(letterCount - 1) : 'a';
2266
+ const rangeText = letterCount > 1 ? `a-${lastLetter}` : (letterCount === 1 ? 'a' : '');
2267
+ const helpText = rangeText ? `${rangeText}, x` : 'x';
2268
+ const helpString = `\n ↑/↓ to navigate, →/Enter to select, ←/X to exit, or press a letter (${helpText})`;
2269
+ console.log(chalk.gray(helpString));
2270
+ linesPrinted += getVisualLineCount(helpString);
2271
+
2272
+ // Save for next render
2273
+ lastLinesPrinted = linesPrinted;
2274
+ };
2275
+
2276
+ const cleanup = () => {
2277
+ if (process.stdin.isTTY && process.stdin.setRawMode) {
2278
+ process.stdin.setRawMode(false);
2279
+ }
2280
+ process.stdin.removeListener('keypress', onKeypress);
2281
+ process.stdin.pause();
2282
+ };
2283
+
2284
+ const selectOption = (index) => {
2285
+ // Debug: Log what's being selected
2286
+ console.log(chalk.yellow(`\n[DEBUG] selectOption called with index: ${index}`));
2287
+ console.log(chalk.yellow(`[DEBUG] Item at index ${index}: value="${items[index].value}", name="${items[index].name}"\n`));
2288
+
2289
+ cleanup();
2290
+ // Don't clear screen for exit - we want to keep the status visible
2291
+ if (items[index].value !== 'exit') {
2292
+ // Clear the prompt line for non-exit options
2293
+ readline.moveCursor(process.stdout, 0, -(items.length - index));
2294
+ readline.clearScreenDown(process.stdout);
2295
+ console.log(chalk.cyan(` → ${items[index].name}\n`));
2296
+ }
2297
+ // Always log what value is being resolved
2298
+ console.log(chalk.yellow(`[DEBUG] Resolving value: "${items[index].value}" from index ${index}\n`));
2299
+ resolve({ value: items[index].value, selectedIndex: index });
2300
+ };
2301
+
2302
+ displayMenu();
2303
+
2304
+ // Set up keypress listener
2305
+ readline.emitKeypressEvents(process.stdin);
2306
+ if (process.stdin.isTTY) {
2307
+ process.stdin.setRawMode(true);
2308
+ }
2309
+
2310
+ const onKeypress = (str, key) => {
2311
+ if (!key) return;
2312
+
2313
+ // Ctrl+C to exit
2314
+ if (key.ctrl && key.name === 'c') {
2315
+ cleanup();
2316
+ process.exit(0);
2317
+ return;
2318
+ }
2319
+
2320
+ // ESC or left arrow to exit with confirmation
2321
+ if (key.name === 'escape' || key.name === 'left') {
2322
+ cleanup();
2323
+ // Don't clear screen for exit - keep status visible
2324
+ resolve({ value: 'exit', selectedIndex });
2325
+ return;
2326
+ }
2327
+
2328
+ // Letter keys for instant selection - 'x' always maps to exit
2329
+ if (str && str.length === 1) {
2330
+ if (str === 'x') {
2331
+ // 'x' always maps to exit (will trigger confirmAndExit)
2332
+ cleanup();
2333
+ // Don't clear screen for exit - keep status visible
2334
+ resolve({ value: 'exit', selectedIndex });
2335
+ return;
2336
+ } else if (str >= 'a' && str <= 'z') {
2337
+ // Other letters map to all items (settings + actions, excluding exit, blank, and info)
2338
+ const letterIndex = str.charCodeAt(0) - 97; // Convert letter to index (a=0, b=1, etc.)
2339
+
2340
+ // Find the nth item with a letter (excluding exit, blank, and info)
2341
+ let letterCount = 0;
2342
+ for (let i = 0; i < items.length; i++) {
2343
+ if (items[i].type !== 'blank' && items[i].type !== 'info' && items[i].value !== 'exit') {
2344
+ if (letterCount === letterIndex) {
2345
+ selectOption(i);
2346
+ return;
2347
+ }
2348
+ letterCount++;
2349
+ }
2350
+ }
2351
+ }
2352
+ }
2353
+
2354
+ // Arrow keys for navigation
2355
+ if (key.name === 'up') {
2356
+ // Skip blank and info lines when navigating
2357
+ let newIndex = selectedIndex > 0 ? selectedIndex - 1 : items.length - 1;
2358
+ while ((items[newIndex].type === 'blank' || items[newIndex].type === 'info') && newIndex !== selectedIndex) {
2359
+ newIndex = newIndex > 0 ? newIndex - 1 : items.length - 1;
2360
+ }
2361
+ selectedIndex = newIndex;
2362
+ displayMenu();
2363
+ } else if (key.name === 'down') {
2364
+ // Skip blank and info lines when navigating
2365
+ let newIndex = selectedIndex < items.length - 1 ? selectedIndex + 1 : 0;
2366
+ while ((items[newIndex].type === 'blank' || items[newIndex].type === 'info') && newIndex !== selectedIndex) {
2367
+ newIndex = newIndex < items.length - 1 ? newIndex + 1 : 0;
2368
+ }
2369
+ selectedIndex = newIndex;
2370
+ displayMenu();
2371
+ } else if (key.name === 'return' || key.name === 'right') {
2372
+ // Don't allow selecting blank or info lines
2373
+ if (items[selectedIndex].type !== 'blank' && items[selectedIndex].type !== 'info') {
2374
+ selectOption(selectedIndex);
2375
+ }
2376
+ }
2377
+ };
2378
+
2379
+ process.stdin.on('keypress', onKeypress);
2380
+ process.stdin.resume();
2381
+ });
2382
+ }
2383
+
2384
+ async function showProviderManagerMenu() {
2385
+ const definitions = getProviderDefinitions();
2386
+ const defMap = new Map(definitions.map(def => [def.id, def]));
2387
+ const prefs = await getProviderPreferences();
2388
+ let order = prefs.order.slice();
2389
+ let enabled = { ...prefs.enabled };
2390
+ let selectedIndex = 0;
2391
+ let dirty = false;
2392
+
2393
+ // Initialize ProviderManager to check rate limits
2394
+ const ProviderManager = require('@vibecodingmachine/core/src/ide-integration/provider-manager.cjs');
2395
+ const providerManager = new ProviderManager();
2396
+
2397
+ const render = () => {
2398
+ process.stdout.write('\x1Bc');
2399
+ console.log(chalk.bold.cyan('⚙ Provider Order & Availability\n'));
2400
+ console.log(chalk.gray(' ↑/↓ move selection j/k reorder e enable d disable Space toggle Enter save/select Esc cancel\n'));
2401
+
2402
+ order.forEach((id, idx) => {
2403
+ const def = defMap.get(id);
2404
+ if (!def) return;
2405
+ const isSelected = idx === selectedIndex;
2406
+ const isEnabled = enabled[id] !== false;
2407
+ const statusLabel = isEnabled ? chalk.green('ENABLED') : chalk.red('DISABLED');
2408
+ const typeLabel = def.type === 'ide' ? chalk.cyan('IDE') : chalk.cyan('LLM');
2409
+ const prefix = isSelected ? chalk.cyan('❯') : ' ';
2410
+ let line = `${prefix} ${idx + 1}. ${def.name} ${chalk.gray(`(${def.id})`)} ${typeLabel} ${statusLabel}`;
2411
+
2412
+ // Check for rate limits
2413
+ const timeUntilReset = providerManager.getTimeUntilReset(id, def.model || id);
2414
+ if (timeUntilReset) {
2415
+ const resetTime = Date.now() + timeUntilReset;
2416
+ const resetDate = new Date(resetTime);
2417
+ const timeStr = resetDate.toLocaleString('en-US', {
2418
+ weekday: 'short',
2419
+ month: 'short',
2420
+ day: 'numeric',
2421
+ hour: 'numeric',
2422
+ minute: '2-digit',
2423
+ hour12: true,
2424
+ timeZoneName: 'short'
2425
+ });
2426
+ line += ` ${chalk.red('⏰ Rate limited until ' + timeStr)}`;
2427
+ }
2428
+
2429
+ console.log(line);
2430
+ });
2431
+
2432
+ console.log();
2433
+ if (dirty) {
2434
+ console.log(chalk.yellow('Pending changes will be saved on Enter...'));
2435
+ } else {
2436
+ console.log(chalk.gray('No pending changes.'));
2437
+ }
2438
+ };
2439
+
2440
+ return new Promise((resolve) => {
2441
+ const cleanup = () => {
2442
+ if (process.stdin.isTTY && process.stdin.setRawMode) {
2443
+ process.stdin.setRawMode(false);
2444
+ }
2445
+ process.stdin.removeListener('keypress', onKeypress);
2446
+ process.stdin.pause();
2447
+ };
2448
+
2449
+ const saveAndExit = async (selectedId) => {
2450
+ cleanup();
2451
+ if (dirty) {
2452
+ await saveProviderPreferences(order, enabled);
2453
+ }
2454
+ if (selectedId) {
2455
+ const { setAutoConfig } = require('./config');
2456
+ await setAutoConfig({ agent: selectedId, ide: selectedId });
2457
+ const def = defMap.get(selectedId);
2458
+ console.log(chalk.green(`\n✓ Active provider set to ${def?.name || selectedId}\n`));
2459
+ } else {
2460
+ console.log();
2461
+ }
2462
+ resolve(selectedId || null);
2463
+ };
2464
+
2465
+ const cancel = () => {
2466
+ cleanup();
2467
+ console.log('\n');
2468
+ resolve(null);
2469
+ };
2470
+
2471
+ const moveSelection = (delta) => {
2472
+ const next = selectedIndex + delta;
2473
+ if (next >= 0 && next < order.length) {
2474
+ selectedIndex = next;
2475
+ render();
2476
+ }
2477
+ };
2478
+
2479
+ const reorder = (delta) => {
2480
+ const target = selectedIndex + delta;
2481
+ if (target < 0 || target >= order.length) return;
2482
+ const temp = order[selectedIndex];
2483
+ order[selectedIndex] = order[target];
2484
+ order[target] = temp;
2485
+ selectedIndex = target;
2486
+ dirty = true;
2487
+ render();
2488
+ };
2489
+
2490
+ const toggle = (value) => {
2491
+ const id = order[selectedIndex];
2492
+ enabled[id] = value;
2493
+ dirty = true;
2494
+ render();
2495
+ };
2496
+
2497
+ const onKeypress = (str, key = {}) => {
2498
+ if (key.ctrl && key.name === 'c') {
2499
+ cancel();
2500
+ return;
2501
+ }
2502
+
2503
+ switch (key.name) {
2504
+ case 'up':
2505
+ moveSelection(-1);
2506
+ break;
2507
+ case 'down':
2508
+ moveSelection(1);
2509
+ break;
2510
+ case 'j':
2511
+ reorder(1);
2512
+ break;
2513
+ case 'k':
2514
+ reorder(-1);
2515
+ break;
2516
+ case 'e':
2517
+ toggle(true);
2518
+ break;
2519
+ case 'd':
2520
+ toggle(false);
2521
+ break;
2522
+ case 'space':
2523
+ toggle(!(enabled[order[selectedIndex]] !== false));
2524
+ break;
2525
+ case 'return':
2526
+ saveAndExit(order[selectedIndex]);
2527
+ break;
2528
+ case 'escape':
2529
+ case 'x':
2530
+ cancel();
2531
+ break;
2532
+ default:
2533
+ break;
2534
+ }
2535
+ };
2536
+
2537
+ if (process.stdin.isTTY && process.stdin.setRawMode) {
2538
+ process.stdin.setRawMode(true);
2539
+ }
2540
+ readline.emitKeypressEvents(process.stdin);
2541
+ process.stdin.on('keypress', onKeypress);
2542
+ process.stdin.resume();
2543
+
2544
+ render();
2545
+ });
2546
+ }
2547
+
2548
+ async function showSettings() {
2549
+ console.log(chalk.bold.cyan('\n⚙️ Settings\n'));
2550
+
2551
+ const { setConfigValue } = require('@vibecodingmachine/core');
2552
+ const { getAutoConfig, setAutoConfig } = require('./config');
2553
+ const currentHostnameEnabled = await isComputerNameEnabled();
2554
+ const hostname = getHostname();
2555
+ const autoConfig = await getAutoConfig();
2556
+ const currentIDE = autoConfig.ide || 'claude-code';
2557
+
2558
+ // Show current settings
2559
+ console.log(chalk.gray('Current settings:'));
2560
+ console.log(chalk.gray(' Computer Name: '), chalk.cyan(hostname));
2561
+ console.log(chalk.gray(' Current IDE: '), chalk.cyan(formatIDEName(currentIDE)));
2562
+ console.log(chalk.gray(' Use Hostname in Req File: '), currentHostnameEnabled ? chalk.green('Enabled ✓') : chalk.yellow('Disabled ○'));
2563
+ console.log();
2564
+
2565
+ const { action } = await inquirer.prompt([
2566
+ {
2567
+ type: 'list',
2568
+ name: 'action',
2569
+ message: 'What would you like to do?',
2570
+ choices: [
2571
+ {
2572
+ name: 'Change IDE',
2573
+ value: 'change-ide'
2574
+ },
2575
+ {
2576
+ name: currentHostnameEnabled ? 'Disable hostname in requirements file' : 'Enable hostname in requirements file',
2577
+ value: 'toggle-hostname'
2578
+ },
2579
+ {
2580
+ name: 'Back to main menu',
2581
+ value: 'back'
2582
+ }
2583
+ ]
2584
+ }
2585
+ ]);
2586
+
2587
+ if (action === 'change-ide') {
2588
+ const { ide } = await inquirer.prompt([
2589
+ {
2590
+ type: 'list',
2591
+ name: 'ide',
2592
+ message: 'Select IDE:',
2593
+ choices: [
2594
+ { name: 'Claude Code CLI (recommended - Anthropic Claude)', value: 'claude-code' },
2595
+ { name: 'Aider CLI (best for Ollama & Bedrock)', value: 'aider' },
2596
+ { name: 'Continue CLI (Ollama support, but --auto mode doesn\'t execute code)', value: 'continue' },
2597
+ { name: 'Cline CLI (local AI alternative, but has Ollama connection issues)', value: 'cline' },
2598
+ { name: 'Cursor', value: 'cursor' },
2599
+ { name: 'VS Code', value: 'vscode' },
2600
+ { name: 'Windsurf', value: 'windsurf' }
2601
+ ],
2602
+ default: currentIDE
2603
+ }
2604
+ ]);
2605
+
2606
+ // Save to config
2607
+ const newConfig = { ...autoConfig, ide };
2608
+ await setAutoConfig(newConfig);
2609
+
2610
+ console.log(chalk.green('\n✓'), `IDE changed to ${chalk.cyan(formatIDEName(ide))}`);
2611
+ console.log(chalk.gray(' Note: This will be used for the next Auto Mode session.'));
2612
+ console.log();
2613
+ } else if (action === 'toggle-hostname') {
2614
+ const newValue = !currentHostnameEnabled;
2615
+
2616
+ // Save to shared config (same location as Electron app)
2617
+ await setConfigValue('computerNameEnabled', newValue);
2618
+
2619
+ const statusText = newValue ? chalk.green('enabled') : chalk.yellow('disabled');
2620
+ console.log(chalk.green('\n✓'), `Hostname in requirements file ${statusText}`);
2621
+
2622
+ if (newValue) {
2623
+ console.log(chalk.gray('\n Requirements file will be:'), chalk.cyan(`REQUIREMENTS-${hostname}.md`));
2624
+ } else {
2625
+ console.log(chalk.gray('\n Requirements file will be:'), chalk.cyan('REQUIREMENTS.md'));
2626
+ }
2627
+
2628
+ console.log(chalk.gray('\n Note: You may need to rename your existing requirements file.'));
2629
+ console.log(chalk.gray(' Note: This setting is now synced with the Electron app.'));
2630
+ console.log();
2631
+ }
2632
+ }
2633
+
2634
+ async function startInteractive() {
2635
+ // STRICT AUTH CHECK (only if enabled)
2636
+ const authEnabled = process.env.AUTH_ENABLED === 'true';
2637
+
2638
+ if (authEnabled) {
2639
+ const auth = require('./auth');
2640
+ const isAuth = await auth.isAuthenticated();
2641
+
2642
+ if (!isAuth) {
2643
+ console.clear();
2644
+ console.log(chalk.bold.cyan('\nVibe Coding Machine CLI'));
2645
+ console.log(chalk.cyan('\n🔒 Authentication Required'));
2646
+ console.log(chalk.gray('Opening browser for Google authentication...\n'));
2647
+
2648
+ try {
2649
+ await auth.login();
2650
+ console.log(chalk.green('\n✓ Authentication successful!\n'));
2651
+ // Continue to interactive mode after successful login
2652
+ } catch (error) {
2653
+ console.error(chalk.red('\n✗ Login failed:'), error.message);
2654
+ if (error.message && error.message.includes('redirect_uri_mismatch')) {
2655
+ console.log(chalk.yellow('\n⚠️ Troubleshooting:'));
2656
+ console.log(chalk.gray('This error usually means the redirect URI is not whitelisted in Google Cloud Console.'));
2657
+ console.log(chalk.gray('Ensure "https://<your-cognito-domain>/oauth2/idpresponse" is added to "Authorized redirect URIs".'));
2658
+ }
2659
+ process.exit(1);
2660
+ }
2661
+ }
2662
+ } else {
2663
+ // Auth disabled - show warning
2664
+ console.log(chalk.yellow('\n⚠️ Authentication is currently disabled'));
2665
+ console.log(chalk.gray('Set AUTH_ENABLED=true to enable authentication\n'));
2666
+ }
2667
+
2668
+ // Ensure Auto Mode is stopped when CLI starts
2669
+ const { stopAutoMode } = require('./auto-mode');
2670
+ await stopAutoMode('startup');
2671
+
2672
+ await showWelcomeScreen();
2673
+
2674
+ let exit = false;
2675
+ let lastSelectedIndex = 0; // Track last selected menu item
2676
+ while (!exit) {
2677
+ try {
2678
+ const autoStatus = await checkAutoModeStatus();
2679
+ const repoPath = process.cwd(); // Always use current working directory
2680
+
2681
+ // Check if .vibecodingmachine exists (inside repo or as sibling)
2682
+ const allnightStatus = await checkVibeCodingMachineExists();
2683
+
2684
+ // Get current settings for display
2685
+ const { getAutoConfig } = require('./config');
2686
+ const autoConfig = await getAutoConfig();
2687
+ const currentIDE = autoConfig.ide || 'claude-code';
2688
+ const useHostname = await isComputerNameEnabled();
2689
+
2690
+ // Build dynamic menu items - settings at top (gray, no letters), actions below (with letters)
2691
+ const items = [];
2692
+
2693
+ // Get current agent (unified IDE + LLM)
2694
+ const currentAgent = autoConfig.agent || autoConfig.ide || 'ollama';
2695
+ let agentDisplay = `Current Agent: ${chalk.cyan(getAgentDisplayName(currentAgent))}`;
2696
+
2697
+ // Check for rate limits (for LLM-based agents and Claude Code)
2698
+ if (currentAgent === 'ollama' || currentAgent === 'groq' || currentAgent === 'anthropic' || currentAgent === 'bedrock' || currentAgent === 'claude-code') {
2699
+ try {
2700
+ const ProviderManager = require('@vibecodingmachine/core/src/ide-integration/provider-manager.cjs');
2701
+ const providerManager = new ProviderManager();
2702
+ const fs = require('fs');
2703
+ const path = require('path');
2704
+ const os = require('os');
2705
+
2706
+ const configPath = path.join(os.homedir(), '.config', 'allnightai', 'config.json');
2707
+
2708
+ if (fs.existsSync(configPath)) {
2709
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
2710
+
2711
+ // Get the model based on the current agent type
2712
+ let model;
2713
+ if (currentAgent === 'groq') {
2714
+ model = config.auto?.groqModel || config.auto?.aiderModel || config.auto?.llmModel;
2715
+ // Remove groq/ prefix if present
2716
+ if (model && model.includes('groq/')) {
2717
+ model = model.split('/')[1];
2718
+ }
2719
+ } else if (currentAgent === 'anthropic') {
2720
+ model = config.auto?.anthropicModel || config.auto?.aiderModel || config.auto?.llmModel;
2721
+ } else if (currentAgent === 'ollama') {
2722
+ const rawModel = config.auto?.llmModel || config.auto?.aiderModel;
2723
+ // Only use if it doesn't have groq/ prefix
2724
+ model = rawModel && !rawModel.includes('groq/') ? rawModel : null;
2725
+ } else if (currentAgent === 'bedrock') {
2726
+ model = 'anthropic.claude-sonnet-4-v1';
2727
+ } else if (currentAgent === 'claude-code') {
2728
+ model = 'claude-code-cli';
2729
+ }
2730
+
2731
+ // For Claude Code, use fixed model name
2732
+ const checkModel = currentAgent === 'claude-code' ? 'claude-code-cli' : model;
2733
+ const provider = currentAgent === 'ollama' ? 'ollama' : currentAgent;
2734
+
2735
+ if (checkModel) {
2736
+ const timeUntilReset = providerManager.getTimeUntilReset(provider, checkModel);
2737
+
2738
+ if (timeUntilReset) {
2739
+ const resetTime = Date.now() + timeUntilReset;
2740
+ const resetDate = new Date(resetTime);
2741
+ const timeStr = resetDate.toLocaleString('en-US', {
2742
+ weekday: 'short',
2743
+ month: 'short',
2744
+ day: 'numeric',
2745
+ hour: 'numeric',
2746
+ minute: '2-digit',
2747
+ hour12: true,
2748
+ timeZoneName: 'short'
2749
+ });
2750
+ agentDisplay += ` ${chalk.red('⏰ Rate limited until ' + timeStr)}`;
2751
+ }
2752
+ }
2753
+ }
2754
+ } catch (err) {
2755
+ // Silently ignore rate limit check errors
2756
+ }
2757
+ }
2758
+
2759
+ // Settings at top (gray, left-justified, no letters)
2760
+
2761
+
2762
+
2763
+ // Get stop condition from already loaded autoConfig (line 1704)
2764
+ const stopCondition = autoConfig.neverStop ? 'Never Stop' :
2765
+ autoConfig.maxChats ? `Stop after ${autoConfig.maxChats}` :
2766
+ 'Never Stop';
2767
+
2768
+ // Get restart CLI setting from autoConfig
2769
+ const restartCLI = autoConfig.restartCLI ? chalk.green('Enabled ✓') : chalk.yellow('Disabled ○');
2770
+
2771
+ if (autoStatus.running) {
2772
+ items.push({
2773
+ type: 'setting',
2774
+ name: `Auto Mode: ${chalk.green('Running ✓')}`,
2775
+ value: 'setting:auto-stop'
2776
+ });
2777
+ } else {
2778
+ items.push({
2779
+ type: 'setting',
2780
+ name: `Auto Mode: ${chalk.yellow('Stopped ○')}`,
2781
+ value: 'setting:auto-start'
2782
+ });
2783
+ }
2784
+
2785
+ // Add separate stop condition setting
2786
+ items.push({
2787
+ type: 'setting',
2788
+ name: ` └─ Stop Condition: ${chalk.cyan(stopCondition)}`,
2789
+ value: 'setting:auto-stop-condition'
2790
+ });
2791
+
2792
+ // Add restart CLI setting
2793
+ items.push({
2794
+ type: 'setting',
2795
+ name: ` └─ Restart CLI after each completed requirement: ${restartCLI}`,
2796
+ value: 'setting:restart-cli'
2797
+ });
2798
+
2799
+ // Add setup alias setting
2800
+ items.push({
2801
+ type: 'setting',
2802
+ name: ` └─ Setup 'vcm' alias`,
2803
+ value: 'setting:setup-alias'
2804
+ });
2805
+
2806
+ // Add current agent setting
2807
+ items.push({
2808
+ type: 'setting',
2809
+ name: ` └─ ${agentDisplay}`,
2810
+ value: 'setting:agent'
2811
+ });
2812
+
2813
+
2814
+
2815
+ // Add Requirements as a selectable setting with counts
2816
+ const hasRequirements = await requirementsExists();
2817
+ const counts = hasRequirements ? await countRequirements() : null;
2818
+ let requirementsText = 'Requirements: ';
2819
+ if (counts) {
2820
+ const total = counts.todoCount + counts.toVerifyCount + counts.verifiedCount;
2821
+ if (total > 0) {
2822
+ const todoPercent = Math.round((counts.todoCount / total) * 100);
2823
+ const toVerifyPercent = Math.round((counts.toVerifyCount / total) * 100);
2824
+ const verifiedPercent = Math.round((counts.verifiedCount / total) * 100);
2825
+ requirementsText += `${chalk.yellow(counts.todoCount + ' (' + todoPercent + '%) TODO')}, ${chalk.cyan(counts.toVerifyCount + ' (' + toVerifyPercent + '%) TO VERIFY')}, ${chalk.green(counts.verifiedCount + ' (' + verifiedPercent + '%) VERIFIED')}`;
2826
+
2827
+ // Add warning if no TODO requirements
2828
+ if (counts.todoCount === 0) {
2829
+ requirementsText += ` ${chalk.red('⚠️ No requirements to work on')}`;
2830
+ }
2831
+ } else {
2832
+ requirementsText = '';
2833
+ }
2834
+ } else if (allnightStatus.exists) {
2835
+ requirementsText += chalk.yellow('Not found');
2836
+ } else {
2837
+ requirementsText += chalk.yellow('Not initialized');
2838
+ }
2839
+
2840
+ if (requirementsText !== '') {
2841
+ items.push({
2842
+ type: 'setting',
2843
+ name: requirementsText,
2844
+ value: 'setting:requirements'
2845
+ });
2846
+ }
2847
+
2848
+ items.push({
2849
+ type: 'setting',
2850
+ name: ` └─ Use Hostname in Req File: ${useHostname ? chalk.green('Enabled ✓') : chalk.yellow('Disabled ○')}`,
2851
+ value: 'setting:hostname'
2852
+ });
2853
+
2854
+ // Add "Next TODO Requirement" as a separate menu item if there are TODO items
2855
+ if (counts && counts.todoCount > 0) {
2856
+ // Get the actual next requirement text (new header format)
2857
+ let nextReqText = '...';
2858
+ let count = 0;
2859
+ try {
2860
+ const hostname = await getHostname();
2861
+ const reqFilename = await getRequirementsFilename(hostname);
2862
+ const reqPath = path.join(repoPath, '.vibecodingmachine', reqFilename);
2863
+
2864
+ if (await fs.pathExists(reqPath)) {
2865
+ const reqContent = await fs.readFile(reqPath, 'utf8');
2866
+ const lines = reqContent.split('\n');
2867
+ let inTodoSection = false;
2868
+
2869
+ // Find first non-empty requirement in TODO section
2870
+ for (let i = 0; i < lines.length; i++) {
2871
+ const line = lines[i].trim();
2872
+
2873
+ // Check if we're in the TODO section (same logic as auto-direct.js)
2874
+ if (line.includes('## ⏳ Requirements not yet completed') ||
2875
+ line.includes('Requirements not yet completed')) {
2876
+ inTodoSection = true;
2877
+ continue;
2878
+ }
2879
+
2880
+ // If we hit another section header, stop looking
2881
+ if (inTodoSection && line.startsWith('##') && !line.startsWith('###')) {
2882
+ break;
2883
+ }
2884
+
2885
+ // If we're in TODO section and find a requirement header (###)
2886
+ if (inTodoSection && line.startsWith('###')) {
2887
+ const title = line.replace(/^###\s*/, '').trim();
2888
+ // Skip empty titles
2889
+ if (title && title.length > 0) {
2890
+ count++;
2891
+ let description = '';
2892
+
2893
+ // Read subsequent lines for description
2894
+ for (let j = i + 1; j < lines.length; j++) {
2895
+ const nextLine = lines[j].trim();
2896
+ // Stop if we hit another requirement or section
2897
+ if (nextLine.startsWith('###') || (nextLine.startsWith('##') && !nextLine.startsWith('###'))) {
2898
+ break;
2899
+ }
2900
+ description += nextLine + '\n';
2901
+ }
2902
+ nextReqText = title;
2903
+ break;
2904
+ }
2905
+ }
2906
+ }
2907
+ }
2908
+ } catch (err) {
2909
+ console.error('Error getting next requirement:', err.message);
2910
+ }
2911
+
2912
+ // Add "Next TODO Requirement" to the menu
2913
+ items.push({
2914
+ type: 'info',
2915
+ name: `Next TODO Requirement: ${nextReqText}`,
2916
+ value: 'next-req'
2917
+ });
2918
+ }
2919
+
2920
+ // Add warning message if no TODO requirements and Auto Mode is stopped
2921
+ if (counts && counts.todoCount === 0 && !autoStatus.running) {
2922
+ items.push({
2923
+ type: 'setting',
2924
+ name: chalk.red(' ⚠️ No requirements to work on - cannot start Auto Mode'),
2925
+ value: 'setting:no-requirements'
2926
+ });
2927
+ }
2928
+
2929
+ // Blank separator line
2930
+ items.push({ type: 'blank', name: '', value: 'blank' });
2931
+
2932
+ // Action items (with letters)
2933
+ // Only show Initialize option if neither directory exists
2934
+ if (!allnightStatus.exists) {
2935
+ items.push({ type: 'action', name: 'Initialize repository (.vibecodingmachine)', value: 'repo:init' });
2936
+ }
2937
+
2938
+ items.push({ type: 'action', name: 'Logout', value: 'logout' });
2939
+ items.push({ type: 'action', name: 'Exit', value: 'exit' });
2940
+
2941
+
2942
+ // Use custom quick menu with last selected index
2943
+ const result = await showQuickMenu(items, lastSelectedIndex);
2944
+ const action = result.value;
2945
+ lastSelectedIndex = result.selectedIndex;
2946
+
2947
+ // Handle cancel (ESC key)
2948
+ if (action === '__cancel__') {
2949
+ // Just refresh and continue
2950
+ continue;
2951
+ }
2952
+
2953
+ // Debug: Log the action being processed
2954
+ if (process.env.DEBUG) {
2955
+ console.log(chalk.gray(`[DEBUG] Processing action: ${action}`));
2956
+ }
2957
+
2958
+ // Ensure action is a string (safety check)
2959
+ const actionStr = String(action || '').trim();
2960
+
2961
+ // Log action for debugging (always show, not just in DEBUG mode)
2962
+ console.log(chalk.yellow(`\n[DEBUG] Action received: "${actionStr}" (type: ${typeof action})\n`));
2963
+
2964
+ switch (actionStr) {
2965
+ case 'setting:agent': {
2966
+ await showProviderManagerMenu();
2967
+ await showWelcomeScreen();
2968
+ break;
2969
+ }
2970
+ case 'setting:provider': {
2971
+ // Switch AI provider - run provider setup only, don't start auto mode
2972
+ // Note: Continue CLI doesn't have provider setup - models are configured in ~/.continue/config.yaml
2973
+ if (currentIDE === 'continue') {
2974
+ console.log(chalk.cyan('\n📝 Continue CLI Models\n'));
2975
+
2976
+ // Read Continue CLI config to show current models
2977
+ try {
2978
+ const fs = require('fs');
2979
+ const path = require('path');
2980
+ const os = require('os');
2981
+ const yaml = require('js-yaml');
2982
+ const { spawn } = require('child_process');
2983
+ const configPath = path.join(os.homedir(), '.continue', 'config.yaml');
2984
+
2985
+ if (fs.existsSync(configPath)) {
2986
+ const config = yaml.load(fs.readFileSync(configPath, 'utf8'));
2987
+ const models = config.models || [];
2988
+
2989
+ if (models.length > 0) {
2990
+ // Check which models are downloaded
2991
+ const { execSync } = require('child_process');
2992
+ let installedNames = [];
2993
+ try {
2994
+ const installedModels = execSync('curl -s http://localhost:11434/api/tags', { encoding: 'utf8' });
2995
+ const modelsData = JSON.parse(installedModels);
2996
+ installedNames = modelsData.models.map(m => m.name.replace(':latest', ''));
2997
+ } catch (error) {
2998
+ console.log(chalk.yellow(' ⚠️ Could not check Ollama (is it running?)\n'));
2999
+ }
3000
+
3001
+ // Build choices with status indicators
3002
+ const choices = models.map((model, idx) => {
3003
+ const modelName = model.model;
3004
+ const displayName = model.name || modelName;
3005
+ const isInstalled = installedNames.some(name =>
3006
+ name === modelName || name === modelName.replace(':latest', '')
3007
+ );
3008
+ const statusIcon = isInstalled ? chalk.green('✓') : chalk.yellow('⚠');
3009
+ const statusText = isInstalled ? chalk.gray('(installed)') : chalk.yellow('(needs download)');
3010
+
3011
+ return {
3012
+ name: `${statusIcon} ${displayName} ${statusText}`,
3013
+ value: { model, isInstalled, index: idx }
3014
+ };
3015
+ });
3016
+
3017
+ choices.push({ name: chalk.gray('← Back to menu'), value: null });
3018
+
3019
+ const inquirer = require('inquirer');
3020
+ const { selection } = await inquirer.prompt([
3021
+ {
3022
+ type: 'list',
3023
+ name: 'selection',
3024
+ message: 'Select model to use:',
3025
+ choices: choices
3026
+ }
3027
+ ]);
3028
+
3029
+ if (selection) {
3030
+ const { model, isInstalled, index } = selection;
3031
+ const modelName = model.model;
3032
+
3033
+ // If not installed, download it first
3034
+ if (!isInstalled) {
3035
+ console.log(chalk.cyan(`\n📥 Downloading ${modelName}...\n`));
3036
+ console.log(chalk.gray('This may take a few minutes depending on model size...\n'));
3037
+
3038
+ // Use Ollama HTTP API for model download
3039
+ const https = require('http');
3040
+ const downloadRequest = https.request({
3041
+ hostname: 'localhost',
3042
+ port: 11434,
3043
+ path: '/api/pull',
3044
+ method: 'POST',
3045
+ headers: {
3046
+ 'Content-Type': 'application/json',
3047
+ }
3048
+ }, (res) => {
3049
+ let lastStatus = '';
3050
+ res.on('data', (chunk) => {
3051
+ try {
3052
+ const lines = chunk.toString().split('\n').filter(l => l.trim());
3053
+ lines.forEach(line => {
3054
+ const data = JSON.parse(line);
3055
+ if (data.status) {
3056
+ if (data.total && data.completed) {
3057
+ // Show progress bar with percentage
3058
+ const percent = Math.round((data.completed / data.total) * 100);
3059
+ const completedMB = Math.round(data.completed / 1024 / 1024);
3060
+ const totalMB = Math.round(data.total / 1024 / 1024);
3061
+ const remainingMB = totalMB - completedMB;
3062
+
3063
+ // Create progress bar (50 chars wide)
3064
+ const barWidth = 50;
3065
+ const filledWidth = Math.round((percent / 100) * barWidth);
3066
+ const emptyWidth = barWidth - filledWidth;
3067
+ const bar = chalk.green('█'.repeat(filledWidth)) + chalk.gray('░'.repeat(emptyWidth));
3068
+
3069
+ process.stdout.write(`\r${bar} ${chalk.cyan(percent + '%')} ${chalk.white(completedMB + 'MB')} / ${chalk.gray(totalMB + 'MB')} ${chalk.yellow('(' + remainingMB + 'MB remaining)')}`);
3070
+ } else if (data.status !== lastStatus) {
3071
+ lastStatus = data.status;
3072
+ process.stdout.write(`\r${chalk.gray(data.status)}`.padEnd(120));
3073
+ }
3074
+ }
3075
+ });
3076
+ } catch (e) {
3077
+ // Ignore JSON parse errors for streaming responses
3078
+ }
3079
+ });
3080
+ });
3081
+
3082
+ downloadRequest.on('error', (error) => {
3083
+ console.log(chalk.red(`\n\n✗ Download failed: ${error.message}\n`));
3084
+ });
3085
+
3086
+ downloadRequest.write(JSON.stringify({ name: modelName }));
3087
+ downloadRequest.end();
3088
+
3089
+ await new Promise((resolve) => {
3090
+ downloadRequest.on('close', () => {
3091
+ console.log(chalk.green(`\n\n✓ Successfully downloaded ${modelName}\n`));
3092
+ resolve();
3093
+ });
3094
+ });
3095
+ }
3096
+
3097
+ // Move selected model to the top of the list
3098
+ const updatedModels = [...models];
3099
+ const [selectedModel] = updatedModels.splice(index, 1);
3100
+ updatedModels.unshift(selectedModel);
3101
+
3102
+ // Update config file
3103
+ config.models = updatedModels;
3104
+ fs.writeFileSync(configPath, yaml.dump(config), 'utf8');
3105
+
3106
+ console.log(chalk.green(`✓ Set ${model.name || modelName} as default model\n`));
3107
+ console.log(chalk.gray('Press Enter to continue...'));
3108
+ await inquirer.prompt([
3109
+ {
3110
+ type: 'input',
3111
+ name: 'continue',
3112
+ message: ''
3113
+ }
3114
+ ]);
3115
+ }
3116
+ } else {
3117
+ console.log(chalk.yellow(' ⚠️ No models configured in config.yaml\n'));
3118
+ console.log(chalk.gray(' Config file:'), chalk.cyan(configPath), '\n');
3119
+
3120
+ const inquirer = require('inquirer');
3121
+ await inquirer.prompt([
3122
+ {
3123
+ type: 'input',
3124
+ name: 'continue',
3125
+ message: 'Press Enter to return to menu...',
3126
+ }
3127
+ ]);
3128
+ }
3129
+ } else {
3130
+ console.log(chalk.yellow(' ⚠️ Config file not found\n'));
3131
+ console.log(chalk.gray(' Expected location:'), chalk.cyan(configPath), '\n');
3132
+
3133
+ const inquirer = require('inquirer');
3134
+ await inquirer.prompt([
3135
+ {
3136
+ type: 'input',
3137
+ name: 'continue',
3138
+ message: 'Press Enter to return to menu...',
3139
+ }
3140
+ ]);
3141
+ }
3142
+ } catch (error) {
3143
+ console.log(chalk.red(` ✗ Error: ${error.message}\n`));
3144
+
3145
+ const inquirer = require('inquirer');
3146
+ await inquirer.prompt([
3147
+ {
3148
+ type: 'input',
3149
+ name: 'continue',
3150
+ message: 'Press Enter to return to menu...',
3151
+ }
3152
+ ]);
3153
+ }
3154
+
3155
+ await showWelcomeScreen();
3156
+ } else {
3157
+ console.log(chalk.cyan('\n🔄 Switching AI Provider...\n'));
3158
+ await auto.start({ ide: currentIDE, forceProviderSetup: true, configureOnly: true });
3159
+ await showWelcomeScreen();
3160
+ }
3161
+ break;
3162
+ }
3163
+ case 'setting:hostname': {
3164
+ // Toggle hostname setting
3165
+ const { setConfigValue } = require('@vibecodingmachine/core');
3166
+ const newValue = !useHostname;
3167
+ await setConfigValue('computerNameEnabled', newValue);
3168
+ const statusText = newValue ? chalk.green('enabled') : chalk.yellow('disabled');
3169
+ console.log(chalk.green('\n✓'), `Hostname in requirements file ${statusText}\n`);
3170
+ await showWelcomeScreen();
3171
+ break;
3172
+ }
3173
+ case 'setting:auto-start': {
3174
+ try {
3175
+ console.log(chalk.bold.cyan('\n🚀 Starting Auto Mode...\n'));
3176
+ // Check if there are requirements to work on
3177
+ const hasRequirements = await requirementsExists();
3178
+ const counts = hasRequirements ? await countRequirements() : null;
3179
+
3180
+ if (!counts || counts.todoCount === 0) {
3181
+ console.log(chalk.red('\n⚠️ Cannot start Auto Mode: No requirements to work on'));
3182
+ console.log(chalk.gray(' Add requirements first using "Requirements" menu option\n'));
3183
+ const inquirer = require('inquirer');
3184
+ await inquirer.prompt([{
3185
+ type: 'input',
3186
+ name: 'continue',
3187
+ message: 'Press Enter to return to menu...',
3188
+ }]);
3189
+ await showWelcomeScreen();
3190
+ break;
3191
+ }
3192
+
3193
+ // Start auto mode - use saved config settings
3194
+ try {
3195
+ // Get current config
3196
+ const { getAutoConfig } = require('./config');
3197
+ const currentConfig = await getAutoConfig();
3198
+
3199
+ // Use saved maxChats/neverStop settings
3200
+ const options = { ide: currentIDE };
3201
+ if (currentConfig.neverStop) {
3202
+ options.neverStop = true;
3203
+ } else if (currentConfig.maxChats) {
3204
+ options.maxChats = currentConfig.maxChats;
3205
+ } else {
3206
+ // Default to never stop if not configured
3207
+ options.neverStop = true;
3208
+ }
3209
+ console.log(chalk.gray(`\n[DEBUG] Calling auto.start with options:`, JSON.stringify(options, null, 2)));
3210
+ console.log(chalk.gray('[DEBUG] Step 1: Starting auto mode...'));
3211
+ try {
3212
+ // Simple approach - just call auto.start(), no blessed UI
3213
+ // Ensure stdin is NOT in raw mode before starting auto mode
3214
+ // (otherwise Ctrl+C won't work)
3215
+ if (process.stdin.isTTY && process.stdin.setRawMode) {
3216
+ process.stdin.setRawMode(false);
3217
+ }
3218
+
3219
+ // ALWAYS use auto:direct (supports both LLM and IDE agents with proper looping)
3220
+ const currentAgent = currentConfig.agent || currentConfig.ide || 'ollama';
3221
+ console.log(chalk.gray('[DEBUG] Using auto:direct for agent:', currentAgent));
3222
+ const { handleAutoStart: handleDirectAutoStart } = require('../commands/auto-direct');
3223
+ await handleDirectAutoStart({ maxChats: options.maxChats });
3224
+
3225
+ // Prompt user before returning to menu (so they can read the output)
3226
+ console.log('');
3227
+ const inquirer = require('inquirer');
3228
+ await inquirer.prompt([{
3229
+ type: 'input',
3230
+ name: 'continue',
3231
+ message: 'Press Enter to return to menu...',
3232
+ }]);
3233
+
3234
+ await showWelcomeScreen();
3235
+ break;
3236
+
3237
+ } catch (error) {
3238
+ // Check if it's a cancellation (ESC) or actual error
3239
+ if (error.message && error.message.includes('User force closed')) {
3240
+ console.log(chalk.yellow('\nCancelled\n'));
3241
+ } else {
3242
+ console.log(chalk.red(`\n✗ Error starting Auto Mode: ${error.message}`));
3243
+ // Always show stack trace for debugging
3244
+ if (error.stack) {
3245
+ console.log(chalk.gray('\nStack trace:'));
3246
+ console.log(chalk.gray(error.stack.split('\n').slice(0, 10).join('\n')));
3247
+ }
3248
+ // Give user time to read the error
3249
+ console.log(chalk.yellow('\nReturning to menu in 5 seconds...'));
3250
+ await new Promise(resolve => setTimeout(resolve, 5000));
3251
+ }
3252
+ await showWelcomeScreen();
3253
+ }
3254
+ } catch (error) {
3255
+ // Catch any errors in the inner try block (line 2066)
3256
+ console.log(chalk.red(`\n✗ Error in auto mode setup: ${error.message}`));
3257
+ if (error.stack) {
3258
+ console.log(chalk.gray(error.stack));
3259
+ }
3260
+ const inquirer = require('inquirer');
3261
+ await inquirer.prompt([{
3262
+ type: 'input',
3263
+ name: 'continue',
3264
+ message: 'Press Enter to return to menu...',
3265
+ }]);
3266
+ await showWelcomeScreen();
3267
+ }
3268
+ } catch (error) {
3269
+ // Catch any errors in the outer try block
3270
+ console.log(chalk.red(`\n✗ Unexpected error: ${error.message}`));
3271
+ if (error.stack) {
3272
+ console.log(chalk.gray(error.stack));
3273
+ }
3274
+ const inquirer = require('inquirer');
3275
+ await inquirer.prompt([{
3276
+ type: 'input',
3277
+ name: 'continue',
3278
+ message: 'Press Enter to return to menu...',
3279
+ }]);
3280
+ await showWelcomeScreen();
3281
+ }
3282
+ break;
3283
+ }
3284
+ case 'setting:no-requirements': {
3285
+ // User clicked on the warning message - show helpful info
3286
+ console.log(chalk.red('\n⚠️ No requirements to work on'));
3287
+ console.log(chalk.gray('\nTo start Auto Mode, you need at least one requirement in the "Requirements not yet completed" section.'));
3288
+ console.log(chalk.gray('\nYou can:'));
3289
+ console.log(chalk.cyan(' 1. Add requirements using the "Requirements" menu option'));
3290
+ console.log(chalk.cyan(' 2. Or wait for requirements to be added to your REQUIREMENTS file\n'));
3291
+ const inquirer = require('inquirer');
3292
+ await inquirer.prompt([{
3293
+ type: 'input',
3294
+ name: 'continue',
3295
+ message: 'Press Enter to return to menu...',
3296
+ }]);
3297
+ await showWelcomeScreen();
3298
+ break;
3299
+ }
3300
+ case 'setting:auto-stop':
3301
+ // Stop auto mode
3302
+ await auto.stop();
3303
+ await showWelcomeScreen();
3304
+ break;
3305
+ case 'setting:auto-stop-condition': {
3306
+ // Modify stop condition
3307
+ try {
3308
+ const inquirer = require('inquirer');
3309
+
3310
+ // Get current config
3311
+ const { getAutoConfig, setAutoConfig } = require('./config');
3312
+ const currentConfig = await getAutoConfig();
3313
+
3314
+ // Determine default value for prompt
3315
+ let defaultMaxChats = '';
3316
+ if (currentConfig.neverStop) {
3317
+ defaultMaxChats = '';
3318
+ } else if (currentConfig.maxChats) {
3319
+ defaultMaxChats = String(currentConfig.maxChats);
3320
+ }
3321
+
3322
+ console.log(chalk.bold.cyan('\n⚙️ Configure Stop Condition\n'));
3323
+
3324
+ const { maxChats } = await inquirer.prompt([{
3325
+ type: 'input',
3326
+ name: 'maxChats',
3327
+ message: 'Max chats (leave empty for never stop):',
3328
+ default: defaultMaxChats
3329
+ }]);
3330
+
3331
+ // Update config
3332
+ const newConfig = { ...currentConfig };
3333
+ if (maxChats && maxChats.trim() !== '') {
3334
+ newConfig.maxChats = parseInt(maxChats);
3335
+ newConfig.neverStop = false;
3336
+ console.log(chalk.green('\n✓'), `Stop condition updated: ${chalk.cyan(`Stop after ${newConfig.maxChats}`)}\n`);
3337
+ } else {
3338
+ delete newConfig.maxChats;
3339
+ newConfig.neverStop = true;
3340
+ console.log(chalk.green('\n✓'), `Stop condition updated: ${chalk.cyan('Never Stop')}\n`);
3341
+ }
3342
+
3343
+ await setAutoConfig(newConfig);
3344
+ } catch (error) {
3345
+ console.log(chalk.red('\n✗ Error updating stop condition:', error.message));
3346
+ }
3347
+ await showWelcomeScreen();
3348
+ break;
3349
+ }
3350
+ case 'setting:restart-cli': {
3351
+ // Toggle restart CLI setting
3352
+ try {
3353
+ const { getAutoConfig, setAutoConfig } = require('./config');
3354
+ const currentConfig = await getAutoConfig();
3355
+ const newConfig = { ...currentConfig, restartCLI: !currentConfig.restartCLI };
3356
+ await setAutoConfig(newConfig);
3357
+ const statusText = newConfig.restartCLI ? chalk.green('enabled') : chalk.yellow('disabled');
3358
+ console.log(chalk.green('\n✓'), `Restart CLI after each completed requirement ${statusText}\n`);
3359
+ } catch (error) {
3360
+ console.log(chalk.red('\n✗ Error updating restart CLI setting:', error.message));
3361
+ }
3362
+ await showWelcomeScreen();
3363
+ break;
3364
+ }
3365
+ case 'setting:setup-alias': {
3366
+ const { setupAlias } = require('../commands/setup');
3367
+ await setupAlias();
3368
+ console.log(chalk.gray('\nPress Enter to return to menu...'));
3369
+ const inquirer = require('inquirer');
3370
+ await inquirer.prompt([{
3371
+ type: 'input',
3372
+ name: 'continue',
3373
+ message: ''
3374
+ }]);
3375
+ await showWelcomeScreen();
3376
+ break;
3377
+ }
3378
+ case 'setting:requirements': {
3379
+ // Show tree-style requirements navigator
3380
+ await showRequirementsTree();
3381
+ await showWelcomeScreen();
3382
+ break;
3383
+ }
3384
+ case 'repo:init':
3385
+ await repo.initRepo();
3386
+ break;
3387
+ case 'auto:start': {
3388
+ const { ide, maxChats } = await inquirer.prompt([
3389
+ {
3390
+ type: 'list',
3391
+ name: 'ide',
3392
+ message: 'Select IDE:',
3393
+ choices: [
3394
+ { name: 'Claude Code CLI (recommended - Anthropic Claude)', value: 'claude-code' },
3395
+ { name: 'Aider CLI (best for Ollama & Bedrock)', value: 'aider' },
3396
+ { name: 'Continue CLI (Ollama support, but --auto mode doesn\'t execute code)', value: 'continue' },
3397
+ { name: 'Cline CLI (local AI alternative, but has Ollama connection issues)', value: 'cline' },
3398
+ { name: 'Cursor', value: 'cursor' },
3399
+ { name: 'VS Code', value: 'vscode' },
3400
+ { name: 'Windsurf', value: 'windsurf' }
3401
+ ],
3402
+ default: 'claude-code'
3403
+ },
3404
+ {
3405
+ type: 'input',
3406
+ name: 'maxChats',
3407
+ message: 'Max chats (leave empty for never stop):',
3408
+ default: ''
3409
+ }
3410
+ ]);
3411
+ const options = { ide };
3412
+ if (maxChats) {
3413
+ options.maxChats = parseInt(maxChats);
3414
+ } else {
3415
+ options.neverStop = true;
3416
+ }
3417
+
3418
+ // Use blessed UI for persistent header and status card
3419
+ console.log('[DEBUG] Attempting to load blessed UI...');
3420
+ try {
3421
+ console.log('[DEBUG] Requiring simple UI components...');
3422
+ const { createAutoModeUI } = require('./auto-mode-simple-ui');
3423
+ const { StdoutInterceptor } = require('./stdout-interceptor');
3424
+ const { getRepoPath } = require('./config');
3425
+ const { getRequirementsPath } = require('@vibecodingmachine/core');
3426
+
3427
+ console.log('[DEBUG] Getting repo info...');
3428
+ // Get current repo info for the header
3429
+ const repoPath = await getRepoPath();
3430
+ const hostname = getHostname();
3431
+ console.log('[DEBUG] Repo:', repoPath, 'Hostname:', hostname);
3432
+
3433
+ // Build menu content for header
3434
+ const menuContent = `╭───────────────────────────────────────────────────────╮
3435
+ │ │
3436
+ │ VibeCodingMachine │
3437
+ │ Auto Mode Running - Press Ctrl+C to stop │
3438
+ │ │
3439
+ ╰───────────────────────────────────────────────────────╯
3440
+
3441
+ Repo: ${repoPath || 'Not set'}
3442
+ Computer Name: ${hostname}
3443
+ Current IDE: ${formatIDEName(ide)}
3444
+ AI Provider: ${getCurrentAIProvider(ide) || 'N/A'}
3445
+ Max Chats: ${maxChats || 'Never stop'}`;
3446
+
3447
+ // Create blessed UI
3448
+ const ui = createAutoModeUI({
3449
+ menuContent,
3450
+ onExit: async () => {
3451
+ // Stop auto mode when user presses Ctrl+C or Q
3452
+ interceptor.stop();
3453
+ await auto.stop();
3454
+ process.exit(0);
3455
+ }
3456
+ });
3457
+
3458
+ // Create stdout interceptor to capture console output
3459
+ const interceptor = new StdoutInterceptor();
3460
+ interceptor.addHandler((output) => {
3461
+ // Route console output to the blessed UI log
3462
+ ui.appendOutput(output);
3463
+ });
3464
+ interceptor.start(false); // Don't pass through to original stdout
3465
+
3466
+ // Monitor requirements file for status updates
3467
+ const reqPath = await getRequirementsPath(repoPath);
3468
+ const fs = require('fs-extra');
3469
+ const chokidar = require('chokidar');
3470
+
3471
+ // Initial status - auto mode always starts from first TODO requirement
3472
+ try {
3473
+ ui.updateStatus({
3474
+ requirement: 'Loading first TODO requirement...',
3475
+ step: 'PREPARE',
3476
+ chatCount: 0,
3477
+ maxChats: maxChats ? parseInt(maxChats) : null,
3478
+ progress: 0
3479
+ });
3480
+ } catch (error) {
3481
+ ui.updateStatus({
3482
+ requirement: 'Error loading requirements',
3483
+ step: 'UNKNOWN',
3484
+ chatCount: 0,
3485
+ maxChats: maxChats ? parseInt(maxChats) : null,
3486
+ progress: 0
3487
+ });
3488
+ }
3489
+
3490
+ // Watch requirements file for changes
3491
+ const watcher = chokidar.watch(reqPath, { persistent: true });
3492
+ watcher.on('change', async () => {
3493
+ try {
3494
+ const reqContent = await fs.readFile(reqPath, 'utf-8');
3495
+ const parsed = parseRequirementsFile(reqContent);
3496
+
3497
+ // Calculate progress based on step
3498
+ const stepProgress = {
3499
+ 'PREPARE': 20,
3500
+ 'ACT': 40,
3501
+ 'CLEAN UP': 60,
3502
+ 'VERIFY': 80,
3503
+ 'DONE': 100
3504
+ };
3505
+
3506
+ ui.updateStatus({
3507
+ requirement: parsed.currentRequirement || 'Unknown',
3508
+ step: parsed.currentStatus || 'UNKNOWN',
3509
+ chatCount: 0, // TODO: Track actual chat count
3510
+ maxChats: maxChats ? parseInt(maxChats) : null,
3511
+ progress: stepProgress[parsed.currentStatus] || 0
3512
+ });
3513
+ } catch (error) {
3514
+ // Silently ignore parse errors
3515
+ }
3516
+ });
3517
+
3518
+ // Start auto mode (this will output to our interceptor)
3519
+ try {
3520
+ // Ensure stdin is NOT in raw mode before starting auto mode
3521
+ // (otherwise Ctrl+C won't work)
3522
+ if (process.stdin.isTTY && process.stdin.setRawMode) {
3523
+ process.stdin.setRawMode(false);
3524
+ }
3525
+
3526
+ await auto.start(options);
3527
+ } catch (error) {
3528
+ ui.appendOutput(`\n\nError: ${error.message}\n`);
3529
+ } finally {
3530
+ // Cleanup
3531
+ watcher.close();
3532
+ interceptor.stop();
3533
+ ui.destroy();
3534
+ }
3535
+ } catch (blessedError) {
3536
+ // Fallback to regular auto mode if blessed UI fails
3537
+ console.log(chalk.red('\n✗ Failed to create blessed UI:'), blessedError.message);
3538
+ console.log(chalk.gray(' Falling back to standard output mode...\n'));
3539
+
3540
+ // Ensure stdin is NOT in raw mode before starting auto mode
3541
+ // (otherwise Ctrl+C won't work)
3542
+ if (process.stdin.isTTY && process.stdin.setRawMode) {
3543
+ process.stdin.setRawMode(false);
3544
+ }
3545
+
3546
+ await auto.start(options);
3547
+ }
3548
+
3549
+ break;
3550
+ }
3551
+ case 'auto:stop':
3552
+ await auto.stop();
3553
+ break;
3554
+ case 'auto:status':
3555
+ await auto.status();
3556
+ break;
3557
+ case 'logout': {
3558
+ // Logout
3559
+ const auth = require('./auth');
3560
+ try {
3561
+ await auth.logout();
3562
+ console.log(chalk.green('\n✓ Logged out successfully!\n'));
3563
+ console.log(chalk.gray('Run "vcm auth:login" or "ana" to login again.\n'));
3564
+ process.exit(0);
3565
+ } catch (error) {
3566
+ console.error(chalk.red('\n✗ Logout failed:'), error.message);
3567
+ console.log(chalk.yellow('\nPress any key to continue...'));
3568
+ await new Promise((resolve) => {
3569
+ process.stdin.once('keypress', () => resolve());
3570
+ });
3571
+ }
3572
+ break;
3573
+ }
3574
+ case 'exit': {
3575
+ // Confirm and exit
3576
+ console.log(chalk.gray('[DEBUG] Exit case triggered, calling confirmAndExit'));
3577
+ await confirmAndExit();
3578
+ console.log(chalk.gray('[DEBUG] confirmAndExit returned (user cancelled)'));
3579
+ // If user cancelled (didn't exit), refresh the screen
3580
+ await showWelcomeScreen();
3581
+ break;
3582
+ }
3583
+ default:
3584
+ // Log unhandled actions for debugging
3585
+ console.log(chalk.yellow(`\n⚠️ Unhandled action: "${actionStr}"\n`));
3586
+ console.log(chalk.gray(' This action is not implemented. Please report this issue.\n'));
3587
+ const inquirer = require('inquirer');
3588
+ await inquirer.prompt([{
3589
+ type: 'input',
3590
+ name: 'continue',
3591
+ message: 'Press Enter to return to menu...',
3592
+ }]);
3593
+ await showWelcomeScreen();
3594
+ break;
3595
+ }
3596
+ } catch (error) {
3597
+ // Catch any unexpected errors in the main loop
3598
+ console.log(chalk.red(`\n✗ Unexpected error in menu: ${error.message}`));
3599
+ if (error.stack) {
3600
+ console.log(chalk.gray(error.stack));
3601
+ }
3602
+ const inquirer = require('inquirer');
3603
+ await inquirer.prompt([{
3604
+ type: 'input',
3605
+ name: 'continue',
3606
+ message: 'Press Enter to return to menu...',
3607
+ }]);
3608
+ await showWelcomeScreen();
3609
+ }
3610
+ }
3611
+ }
3612
+
3613
+ module.exports = { startInteractive };
3614
+
3615
+
3616
+