vibecodingmachine-cli 2026.1.29-713 → 2026.2.20-426

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 (46) hide show
  1. package/bin/vibecodingmachine.js +124 -2
  2. package/package.json +3 -2
  3. package/src/commands/agents-check.js +69 -0
  4. package/src/commands/auto-direct.js +930 -145
  5. package/src/commands/auto.js +32 -8
  6. package/src/commands/ide.js +2 -1
  7. package/src/commands/requirements.js +23 -27
  8. package/src/utils/auto-mode.js +4 -1
  9. package/src/utils/cline-js-handler.js +218 -0
  10. package/src/utils/config.js +22 -0
  11. package/src/utils/display-formatters-complete.js +229 -0
  12. package/src/utils/display-formatters-extracted.js +219 -0
  13. package/src/utils/display-formatters.js +157 -0
  14. package/src/utils/feedback-handler.js +143 -0
  15. package/src/utils/first-run.js +5 -8
  16. package/src/utils/ide-detection-complete.js +126 -0
  17. package/src/utils/ide-detection-extracted.js +116 -0
  18. package/src/utils/ide-detection.js +124 -0
  19. package/src/utils/interactive-backup.js +5664 -0
  20. package/src/utils/interactive-broken.js +280 -0
  21. package/src/utils/interactive.js +357 -2367
  22. package/src/utils/provider-checker.js +410 -0
  23. package/src/utils/provider-manager.js +254 -0
  24. package/src/utils/provider-registry.js +18 -9
  25. package/src/utils/requirement-actions.js +884 -0
  26. package/src/utils/requirements-navigator.js +587 -0
  27. package/src/utils/rui-trui-adapter.js +311 -0
  28. package/src/utils/simple-trui.js +204 -0
  29. package/src/utils/status-helpers-extracted.js +125 -0
  30. package/src/utils/status-helpers.js +107 -0
  31. package/src/utils/trui-debug.js +261 -0
  32. package/src/utils/trui-feedback.js +133 -0
  33. package/src/utils/trui-nav-agents.js +119 -0
  34. package/src/utils/trui-nav-requirements.js +268 -0
  35. package/src/utils/trui-nav-settings.js +157 -0
  36. package/src/utils/trui-nav-specifications.js +139 -0
  37. package/src/utils/trui-navigation.js +305 -0
  38. package/src/utils/trui-provider-manager.js +182 -0
  39. package/src/utils/trui-quick-menu.js +370 -0
  40. package/src/utils/trui-req-actions.js +372 -0
  41. package/src/utils/trui-req-tree.js +534 -0
  42. package/src/utils/trui-specifications.js +359 -0
  43. package/src/utils/trui-text-editor.js +350 -0
  44. package/src/utils/trui-windsurf.js +350 -0
  45. package/src/utils/welcome-screen-extracted.js +135 -0
  46. package/src/utils/welcome-screen.js +134 -0
@@ -1,147 +1,30 @@
1
- const inquirer = require('inquirer');
2
- const chalk = require('chalk');
3
- const boxen = require('boxen');
4
- const path = require('path');
1
+ const { TRUINavigation } = require('./trui-navigation');
2
+ const { checkVibeCodingMachineExists, getHostname, requirementsExists, t, isComputerNameEnabled } = require('vibecodingmachine-core');
5
3
  const os = require('os');
6
- const fs = require('fs-extra');
7
- const readline = require('readline');
8
- const { execSync } = require('child_process');
9
- const repo = require('../commands/repo');
10
- const auto = require('../commands/auto');
11
- const status = require('../commands/status');
12
- const requirements = require('../commands/requirements');
13
- const { getRepoPath, readConfig, writeConfig, getAutoConfig, getProviderCache, setProviderCache } = require('./config');
14
- const { getProviderPreferences, saveProviderPreferences, getProviderDefinitions } = require('../utils/provider-registry');
4
+ const path = require('path');
5
+ const chalk = require('chalk');
15
6
  const { checkAutoModeStatus } = require('./auto-mode');
16
- const {
17
- checkVibeCodingMachineExists,
18
- getHostname,
19
- getRequirementsFilename,
20
- requirementsExists,
21
- isComputerNameEnabled,
22
- t,
23
- detectLocale,
24
- setLocale,
25
- AppleScriptManager,
26
- IDEHealthTracker,
27
- HealthReporter
28
- } = require('vibecodingmachine-core');
29
- const { promptWithDefaultsOnce } = require('./prompt-helper');
30
- const { formatResetsAtLabel } = require('./date-formatter');
31
-
32
- // Initialize locale detection for interactive mode
33
- const detectedLocale = detectLocale();
34
- setLocale(detectedLocale);
35
7
  const pkg = require('../../package.json');
8
+ const boxen = require('boxen');
9
+ const inquirer = require('inquirer');
36
10
 
37
- const { exec } = require('child_process');
38
- const util = require('util');
39
- const execAsync = util.promisify(exec);
40
-
41
- async function checkVSCodeExtension(extensionId) {
42
- // Check if a VS Code extension is installed
43
- try {
44
- const { stdout } = await execAsync('code --list-extensions', { timeout: 5000 });
45
- const extensions = stdout.toLowerCase().split('\n').map(e => e.trim());
46
-
47
- // Map common extension IDs to their actual extension IDs
48
- const extensionMap = {
49
- 'github-copilot': 'github.copilot',
50
- 'amazon-q': 'amazonwebservices.amazon-q-vscode'
51
- };
52
-
53
- const actualExtensionId = extensionMap[extensionId] || extensionId;
54
- return extensions.includes(actualExtensionId.toLowerCase());
55
- } catch (error) {
56
- // If code CLI is not available or command fails, return false
57
- return false;
58
- }
11
+ /**
12
+ * Start TRUI interface
13
+ */
14
+ async function startInteractive() {
15
+ const nav = new TRUINavigation();
16
+ await nav.start();
59
17
  }
60
18
 
61
- async function checkAppOrBinary(names = [], binaries = []) {
62
- // names: app bundle base names (e.g., 'Cursor' -> /Applications/Cursor.app)
63
- // binaries: CLI binary names to check on PATH (e.g., 'code')
64
- const platform = os.platform();
65
- // Check common application directories
66
- if (platform === 'darwin') {
67
- const appDirs = ['/Applications', path.join(os.homedir(), 'Applications')];
68
- for (const appName of names) {
69
- for (const dir of appDirs) {
70
- try {
71
- const p = path.join(dir, `${appName}.app`);
72
- if (await fs.pathExists(p)) {
73
- // Ensure this is a real application bundle (has Contents/MacOS executable)
74
- try {
75
- const macosDir = path.join(p, 'Contents', 'MacOS');
76
- const exists = await fs.pathExists(macosDir);
77
- if (exists) {
78
- const files = await fs.readdir(macosDir);
79
- if (files && files.length > 0) {
80
- // Prefer to ensure the app is usable: use spctl to assess, fallback to quarantine xattr
81
- try {
82
- // spctl returns non-zero on rejected/invalid apps
83
- // Use async exec to avoid blocking the event loop
84
- await execAsync(`spctl --assess -v "${p}"`, { timeout: 5000 });
85
-
86
- // additionally validate codesign quickly (timeout to avoid hangs)
87
- try {
88
- await execAsync(`codesign -v --deep --strict "${p}"`, { timeout: 5000 });
89
- return true;
90
- } catch (csErr) {
91
- // codesign failed or timed out — treat as not usable/damaged
92
- return false;
93
- }
94
- } catch (e) {
95
- // spctl failed or timed out — check if app has quarantine attribute
96
- try {
97
- const { stdout } = await execAsync(`xattr -p com.apple.quarantine "${p}" 2>/dev/null || true`, { encoding: 'utf8' });
98
- const out = stdout.trim();
99
- if (!out) {
100
- // no quarantine attribute but spctl failed — be conservative and treat as not installed
101
- return false;
102
- }
103
- // If quarantine attribute exists, treat as not installed (damaged/not allowed)
104
- return false;
105
- } catch (e2) {
106
- return false;
107
- }
108
- }
109
- }
110
- }
111
- } catch (e) {
112
- // if we can't stat inside, be conservative and continue searching
113
- }
114
- }
115
- } catch (e) {
116
- /* ignore */
117
- }
118
- }
119
- }
120
- }
121
-
122
- // Check PATH for known binaries
123
- for (const bin of binaries) {
124
- try {
125
- await execAsync(`which ${bin}`, { timeout: 2000 });
126
- return true;
127
- } catch (e) {
128
- /* not found */
129
- }
130
- }
131
-
132
- // Check common Homebrew bin locations
133
- const brewPaths = ['/opt/homebrew/bin', '/usr/local/bin'];
134
- for (const bin of binaries) {
135
- for (const brew of brewPaths) {
136
- try {
137
- if (await fs.pathExists(path.join(brew, bin))) return true;
138
- } catch (e) { /* ignore */ }
139
- }
140
- }
141
-
142
- return false;
19
+ /**
20
+ * Expose showProviderManagerMenu for external callers (Electron, tests)
21
+ */
22
+ async function showProviderManagerMenu() {
23
+ const { showProviderManagerMenu: show } = require('./trui-provider-manager');
24
+ return show();
143
25
  }
144
26
 
27
+
145
28
  /**
146
29
  * Translate workflow stage names
147
30
  */
@@ -161,6 +44,78 @@ function translateStage(stage) {
161
44
  return key ? t(key) : stage;
162
45
  }
163
46
 
47
+ function normalizeProjectDirName(input) {
48
+ const s = String(input || '').trim().toLowerCase();
49
+ const replaced = s.replace(/\s+/g, '-');
50
+ const cleaned = replaced.replace(/[^a-z0-9._-]/g, '-');
51
+ const collapsed = cleaned.replace(/-+/g, '-').replace(/^[-._]+|[-._]+$/g, '');
52
+ return collapsed || 'my-project';
53
+ }
54
+
55
+ async function bootstrapProjectIfInHomeDir() {
56
+ let cwdResolved = path.resolve(process.cwd());
57
+ let homeResolved = path.resolve(os.homedir());
58
+
59
+ try {
60
+ cwdResolved = await fs.realpath(cwdResolved);
61
+ } catch (err) {
62
+ // Ignore
63
+ }
64
+
65
+ try {
66
+ homeResolved = await fs.realpath(homeResolved);
67
+ } catch (err) {
68
+ // Ignore
69
+ }
70
+
71
+ if (cwdResolved !== homeResolved) {
72
+ return;
73
+ }
74
+
75
+ const codeDir = path.join(homeResolved, 'code');
76
+ const codeDirExists = await fs.pathExists(codeDir);
77
+
78
+ if (!codeDirExists) {
79
+ const { shouldCreateCodeDir } = await inquirer.prompt([
80
+ {
81
+ type: 'confirm',
82
+ name: 'shouldCreateCodeDir',
83
+ message: `No code directory found at ${codeDir}. Would you like to create it?`,
84
+ default: true
85
+ }
86
+ ]);
87
+
88
+ if (!shouldCreateCodeDir) {
89
+ return;
90
+ }
91
+
92
+ await fs.ensureDir(codeDir);
93
+ }
94
+
95
+ const { projectName } = await inquirer.prompt([
96
+ {
97
+ type: 'input',
98
+ name: 'projectName',
99
+ message: 'What is the project name you want to create?',
100
+ validate: (value) => {
101
+ const trimmed = String(value || '').trim();
102
+ return trimmed.length > 0 || 'Please enter a project name.';
103
+ }
104
+ }
105
+ ]);
106
+
107
+ const dirName = normalizeProjectDirName(projectName);
108
+ const projectDir = path.join(codeDir, dirName);
109
+ await fs.ensureDir(projectDir);
110
+
111
+ process.chdir(projectDir);
112
+ try {
113
+ await writeConfig({ ...(await readConfig()), repoPath: projectDir });
114
+ } catch (err) {
115
+ // Best-effort: interactive mode will still use process.cwd()
116
+ }
117
+ }
118
+
164
119
  /**
165
120
  * Format IDE name for display
166
121
  * @param {string} ide - Internal IDE identifier
@@ -466,7 +421,7 @@ async function showWelcomeScreen() {
466
421
 
467
422
  // Display welcome banner with version
468
423
  console.log('\n' + boxen(
469
- chalk.bold.cyan('Vibe Coding Machine!') + '\n' +
424
+ chalk.bold.cyan('Vibe Coding Machine') + '\n' +
470
425
  chalk.gray(version) + '\n' +
471
426
  chalk.gray(t('banner.tagline')),
472
427
  {
@@ -477,9 +432,6 @@ async function showWelcomeScreen() {
477
432
  }
478
433
  ));
479
434
 
480
- // Display feedback hint at the top of every screen
481
- console.log(chalk.gray('💡 Press F for feedback - Share your thoughts anytime'));
482
-
483
435
  // Display repository and system info
484
436
  console.log();
485
437
  console.log(chalk.gray(t('system.repo').padEnd(25)), formatPath(repoPath));
@@ -568,14 +520,10 @@ function indexToLetter(index) {
568
520
  return String.fromCharCode(97 + index); // 97 is 'a'
569
521
  }
570
522
 
571
- // Parse requirements from file content for a given section (supports ###, PACKAGE:, and '- ' bullets)
572
- const { parseRequirementsFromContent } = require('./requirements-parser');
573
-
574
523
  // Tree-style requirements navigator
575
524
  async function showRequirementsTree() {
576
525
  console.log(chalk.bold.cyan('\n📋 ' + t('requirements.navigator.title') + '\n'));
577
526
  console.log(chalk.gray(t('requirements.navigator.basic.instructions') + '\n'));
578
- console.log(chalk.gray('💡 Press F for feedback - Share your thoughts anytime\n'));
579
527
 
580
528
  const tree = {
581
529
  expanded: { root: true },
@@ -691,14 +639,136 @@ async function showRequirementsTree() {
691
639
  }
692
640
 
693
641
  const content = await fs.readFile(reqPath, 'utf8');
642
+ const lines = content.split('\n');
643
+
644
+ let inSection = false;
645
+ const requirements = [];
646
+
647
+ // For TO VERIFY section, check multiple possible section titles
648
+ const sectionTitles = sectionKey === 'verify'
649
+ ? ['🔍 TO VERIFY BY HUMAN', 'TO VERIFY BY HUMAN', '🔍 TO VERIFY', 'TO VERIFY', '✅ Verified by AI screenshot', 'Verified by AI screenshot', 'Verified by AI screenshot. Needs Human to Verify and move to CHANGELOG']
650
+ : [sectionTitle];
651
+
652
+ // For TO VERIFY, we need to find the exact section header
653
+ // The section header is: ## ✅ Verified by AI screenshot. Needs Human to Verify and move to CHANGELOG
654
+ const toVerifySectionHeader = '## ✅ Verified by AI screenshot. Needs Human to Verify and move to CHANGELOG';
655
+
656
+ for (let i = 0; i < lines.length; i++) {
657
+ const line = lines[i];
658
+ const trimmed = line.trim();
659
+
660
+ // Check if this line matches any of the section titles
661
+ // IMPORTANT: Only check section headers (lines starting with ##), not requirement text
662
+ if (trimmed.startsWith('##') && !trimmed.startsWith('###')) {
663
+ // Reset inSection if we hit a section header that's not our target section
664
+ if (sectionKey === 'verify' && inSection) {
665
+ // Check if this is still a TO VERIFY section header
666
+ if (!isStillToVerify) {
667
+ // This will be handled by the "leaving section" check below, but ensure we don't process it as entering
668
+ }
669
+ }
670
+ if (sectionKey === 'verify') {
671
+ // For TO VERIFY, check for the specific section header with exact matching
672
+ // Must match the exact TO VERIFY section header, not just any line containing "TO VERIFY"
673
+ const isToVerifyHeader = trimmed === '## 🔍 TO VERIFY BY HUMAN' ||
674
+ trimmed.startsWith('## 🔍 TO VERIFY BY HUMAN') ||
675
+ trimmed === '## 🔍 TO VERIFY' ||
676
+ trimmed.startsWith('## 🔍 TO VERIFY') ||
677
+ trimmed === '## TO VERIFY' ||
678
+ trimmed.startsWith('## TO VERIFY') ||
679
+ trimmed === '## ✅ TO VERIFY' ||
680
+ trimmed.startsWith('## ✅ TO VERIFY') ||
681
+ trimmed === toVerifySectionHeader ||
682
+ (trimmed.startsWith(toVerifySectionHeader) && trimmed.includes('Needs Human to Verify'));
683
+
684
+ if (isToVerifyHeader) {
685
+ // Make sure it's not a VERIFIED section (without TO VERIFY)
686
+ if (!trimmed.includes('## 📝 VERIFIED') && !trimmed.match(/^##\s+VERIFIED$/i) && !trimmed.includes('📝 VERIFIED')) {
687
+ inSection = true;
688
+ continue;
689
+ }
690
+ } else {
691
+ // If we hit a different section header and we're looking for TO VERIFY, make sure we're not in section
692
+ // This prevents incorrectly reading from TODO or other sections
693
+ if (trimmed.includes('⏳ Requirements not yet completed') ||
694
+ trimmed.includes('Requirements not yet completed') ||
695
+ trimmed === '## 📝 VERIFIED' ||
696
+ trimmed.startsWith('## 📝 VERIFIED')) {
697
+ // We're in TODO or VERIFIED section, not TO VERIFY - reset
698
+ inSection = false;
699
+ }
700
+ }
701
+ } else if (sectionTitles.some(title => trimmed.includes(title))) {
702
+ inSection = true;
703
+ continue;
704
+ }
705
+ }
706
+
707
+ // Check if we're leaving the section (new section header that doesn't match)
708
+ if (inSection && trimmed.startsWith('##') && !trimmed.startsWith('###')) {
709
+ // If this is a new section header and it's not one of our section titles, we've left the section
710
+ if (sectionKey === 'verify') {
711
+ // For TO VERIFY, only break if this is clearly a different section
712
+ // Check for specific section headers that indicate we've left TO VERIFY
713
+
714
+ if (isVerifiedSection || isTodoSection || isRecycledSection || isClarificationSection) {
715
+ break; // Different section, we've left TO VERIFY
716
+ }
717
+ // Otherwise, continue - might be REJECTED or CHANGELOG which are not section boundaries for TO VERIFY
718
+ } else {
719
+ // For other sections, break if it's a new section header that doesn't match
720
+ if (!sectionTitles.some(title => trimmed.includes(title))) {
721
+ break;
722
+ }
723
+ }
724
+ }
725
+
726
+ // Read requirements in new format (### header)
727
+ if (inSection && line.trim().startsWith('###')) {
728
+ const title = line.trim().replace(/^###\s*/, '').trim();
729
+
730
+ // Skip malformed requirements (title is just a package name, empty, or too short)
731
+ // Common package names that shouldn't be requirement titles
732
+ const packageNames = ['cli', 'core', 'electron-app', 'web', 'mobile', 'vscode-extension', 'sync-server'];
733
+ if (!title || title.length === 0 || packageNames.includes(title.toLowerCase())) {
734
+ continue; // Skip this malformed requirement
735
+ }
736
+
737
+ const details = [];
738
+ let pkg = null;
739
+
740
+ // Read package and description
741
+ for (let j = i + 1; j < lines.length; j++) {
742
+ const nextLine = lines[j].trim();
743
+ // Stop if we hit another requirement or section
744
+ if (nextLine.startsWith('###') || (nextLine.startsWith('##') && !nextLine.startsWith('###'))) {
745
+ break;
746
+ }
747
+ // Check for PACKAGE line
748
+ if (nextLine.startsWith('PACKAGE:')) {
749
+ pkg = nextLine.replace(/^PACKAGE:\s*/, '').trim();
750
+ } else if (nextLine && !nextLine.startsWith('PACKAGE:')) {
751
+ // Description line
752
+ details.push(nextLine);
753
+ }
754
+ }
694
755
 
695
- // Delegate to reusable parser
696
- const allReqs = parseRequirementsFromContent(content, sectionKey, sectionTitle);
756
+ requirements.push({ title, details, pkg, lineIndex: i });
757
+ }
758
+ }
697
759
 
698
- // For TODO section, only show primary heading requirements (those marked from '###' titles)
699
- if (sectionKey === 'todo') return allReqs.filter(r => r.source === 'heading');
760
+ // Remove duplicates based on title (keep first occurrence)
761
+ const seenTitles = new Set();
762
+ const uniqueRequirements = [];
763
+ for (const req of requirements) {
764
+ const normalizedTitle = req.title.replace(/^TRY AGAIN \(\d+(st|nd|rd|th) time\):\s*/i, '').trim();
765
+ if (!seenTitles.has(normalizedTitle)) {
766
+ seenTitles.add(normalizedTitle);
767
+ uniqueRequirements.push(req);
768
+ }
769
+ }
700
770
 
701
- return allReqs;
771
+ return uniqueRequirements;
702
772
  };
703
773
 
704
774
  // Load VERIFIED requirements from CHANGELOG
@@ -874,10 +944,12 @@ async function showRequirementsTree() {
874
944
  // Safety check: ensure tree.selected is within bounds
875
945
  if (tree.items.length === 0) {
876
946
  console.log(chalk.yellow('No items to display.'));
877
- console.log(chalk.gray(`\n${t('interactive.press.any.key.return')}`));
878
- await new Promise((resolve) => {
879
- process.stdin.once('keypress', () => resolve());
880
- });
947
+ const inquirer = require('inquirer');
948
+ await inquirer.prompt([{
949
+ type: 'input',
950
+ name: 'continue',
951
+ message: `${t('interactive.press.any.key.return')}`
952
+ }]);
881
953
  inTree = false;
882
954
  continue;
883
955
  }
@@ -1047,10 +1119,6 @@ async function showRequirementsTree() {
1047
1119
  tree.todoReqs = await loadSection('todo', '⏳ Requirements not yet completed');
1048
1120
  await buildTree();
1049
1121
  }
1050
- } else if (key.name === 'f') {
1051
- // Feedback button ( megaphone 📣 )
1052
- await handleFeedbackSubmission();
1053
- await buildTree();
1054
1122
  } else if (key.name === 'r') {
1055
1123
  const current = tree.items[tree.selected];
1056
1124
  if (!current) continue; // Safety check
@@ -1127,140 +1195,6 @@ async function showRequirementsTree() {
1127
1195
  process.stdin.pause();
1128
1196
  }
1129
1197
 
1130
- /**
1131
- * Handle feedback submission
1132
- */
1133
- async function handleFeedbackSubmission() {
1134
- console.log(chalk.bold.cyan('\n📣 ' + t('interactive.feedback.title')));
1135
- console.log(chalk.gray(t('interactive.feedback.instructions') + '\n'));
1136
-
1137
- const { getUserProfile } = require('./auth');
1138
- const userProfile = await getUserProfile();
1139
- const userEmail = userProfile ? userProfile.email : 'anonymous';
1140
-
1141
- // Ask if user wants to include a screenshot
1142
- let includeScreenshot = false;
1143
- let screenshotData = null;
1144
-
1145
- try {
1146
- const { screenshot } = await inquirer.prompt([{
1147
- type: 'confirm',
1148
- name: 'screenshot',
1149
- message: 'Include a screenshot with your feedback?',
1150
- default: false
1151
- }]);
1152
-
1153
- includeScreenshot = screenshot;
1154
-
1155
- if (includeScreenshot) {
1156
- console.log(chalk.gray('📸 Capturing screenshot...'));
1157
- try {
1158
- const screenshot = require('screenshot-desktop');
1159
- const imgBuffer = await screenshot({ format: 'png' });
1160
-
1161
- // Convert to base64 and check size
1162
- const base64 = imgBuffer.toString('base64');
1163
- const dataUrl = `data:image/png;base64,${base64}`;
1164
- const size = Buffer.byteLength(dataUrl, 'utf8');
1165
-
1166
- // Much stricter size limit - 100KB max
1167
- if (size > 100 * 1024) {
1168
- console.log(chalk.yellow('⚠️ Screenshot is too large, submitting feedback without screenshot'));
1169
- includeScreenshot = false;
1170
- } else {
1171
- screenshotData = dataUrl;
1172
- console.log(chalk.green(`✅ Screenshot captured (${(size / 1024).toFixed(2)} KB)`));
1173
- }
1174
- } catch (screenshotError) {
1175
- console.log(chalk.yellow('⚠️ Failed to capture screenshot, continuing without it'));
1176
- includeScreenshot = false;
1177
- }
1178
- }
1179
- } catch (err) {
1180
- // User cancelled or error occurred
1181
- includeScreenshot = false;
1182
- }
1183
-
1184
- console.log(chalk.gray('\n' + t('interactive.feedback.comment.instructions') + '\n'));
1185
-
1186
- const commentLines = [];
1187
- let emptyLineCount = 0;
1188
- let isFirstLine = true;
1189
-
1190
- while (true) {
1191
- try {
1192
- const { line } = await inquirer.prompt([{
1193
- type: 'input',
1194
- name: 'line',
1195
- message: isFirstLine ? t('interactive.feedback.comment') : ''
1196
- }]);
1197
-
1198
- isFirstLine = false;
1199
-
1200
- if (line.trim() === '') {
1201
- emptyLineCount++;
1202
- if (emptyLineCount >= 2) break;
1203
- } else {
1204
- emptyLineCount = 0;
1205
- commentLines.push(line);
1206
- }
1207
- } catch (err) {
1208
- break;
1209
- }
1210
- }
1211
-
1212
- const comment = commentLines.join('\n');
1213
-
1214
- if (!comment || comment.trim().length === 0) {
1215
- console.log(chalk.yellow('\n⚠️ ' + t('interactive.feedback.cancelled')));
1216
- return;
1217
- }
1218
-
1219
- try {
1220
- console.log(chalk.gray('\n' + t('interactive.feedback.submitting') + '...'));
1221
-
1222
- const auth = require('./auth');
1223
- const UserDatabase = require('vibecodingmachine-core/src/database/user-schema');
1224
- const userDb = new UserDatabase();
1225
-
1226
- // Set auth token for API requests
1227
- const token = await auth.getAuthToken();
1228
- if (token) {
1229
- userDb.setAuthToken(token);
1230
- }
1231
-
1232
- const feedbackData = {
1233
- email: userEmail,
1234
- comment: comment,
1235
- interface: 'cli',
1236
- version: pkg.version,
1237
- type: 'cli_feedback'
1238
- };
1239
-
1240
- if (includeScreenshot && screenshotData) {
1241
- feedbackData.screenshot = screenshotData;
1242
- }
1243
-
1244
- await userDb.submitFeedback(feedbackData);
1245
-
1246
- console.log(chalk.green('\n✓ ' + t('interactive.feedback.success')));
1247
- } catch (error) {
1248
- console.log(chalk.red('\n✗ ' + t('interactive.feedback.error') + ': ') + error.message);
1249
- }
1250
-
1251
- console.log(chalk.gray(`\n${t('interactive.press.enter.continue')}`));
1252
- await new Promise(resolve => {
1253
- const rl = readline.createInterface({
1254
- input: process.stdin,
1255
- output: process.stdout
1256
- });
1257
- rl.question('', () => {
1258
- rl.close();
1259
- resolve();
1260
- });
1261
- });
1262
- }
1263
-
1264
1198
  // Helper to show goodbye message
1265
1199
  function showGoodbyeMessage() {
1266
1200
  const hour = new Date().getHours();
@@ -1609,7 +1543,6 @@ async function showClarificationActions(req, tree, loadClarification) {
1609
1543
  const actions = [
1610
1544
  { label: '✍️ Add/Edit Responses', value: 'edit-responses' },
1611
1545
  { label: '↩️ Move back to TODO (after clarification)', value: 'move-to-todo' },
1612
- { label: '📣 Feedback', value: 'feedback' },
1613
1546
  { label: '🗑️ Delete', value: 'delete' }
1614
1547
  ];
1615
1548
 
@@ -1640,8 +1573,7 @@ async function showClarificationActions(req, tree, loadClarification) {
1640
1573
  });
1641
1574
 
1642
1575
  // Display menu
1643
- console.log(chalk.gray('\n💡 Press F for feedback - Share your thoughts anytime'));
1644
- console.log(chalk.gray('What would you like to do? (↑/↓/Enter to select, ESC/← to go back)\n'));
1576
+ console.log(chalk.gray('\nWhat would you like to do? (↑/↓/Enter to select, ESC/← to go back)\n'));
1645
1577
  actions.forEach((action, idx) => {
1646
1578
  if (idx === selected) {
1647
1579
  console.log(chalk.cyan(`❯ ${action.label}`));
@@ -1672,10 +1604,6 @@ async function showClarificationActions(req, tree, loadClarification) {
1672
1604
  selected = Math.max(0, selected - 1);
1673
1605
  } else if (key.name === 'down') {
1674
1606
  selected = Math.min(actions.length - 1, selected + 1);
1675
- } else if (key.name === 'f') {
1676
- // Feedback button ( megaphone 📣 )
1677
- await handleFeedbackSubmission();
1678
- return;
1679
1607
  } else if (key.name === 'return' || key.name === 'space') {
1680
1608
  const action = actions[selected].value;
1681
1609
 
@@ -1687,9 +1615,6 @@ async function showClarificationActions(req, tree, loadClarification) {
1687
1615
  await moveClarificationToTodo(req, tree);
1688
1616
  tree.clarificationReqs = await loadClarification();
1689
1617
  return;
1690
- } else if (action === 'feedback') {
1691
- await handleFeedbackSubmission();
1692
- return;
1693
1618
  } else if (action === 'delete') {
1694
1619
  await deleteClarification(req, tree);
1695
1620
  tree.clarificationReqs = await loadClarification();
@@ -1707,7 +1632,6 @@ async function showRequirementActions(req, sectionKey, tree) {
1707
1632
  { label: '👎 Thumbs down (demote to TODO)', value: 'thumbs-down' },
1708
1633
  { label: '⬆️ Move up', value: 'move-up' },
1709
1634
  { label: '⬇️ Move down', value: 'move-down' },
1710
- { label: '📣 Feedback', value: 'feedback' },
1711
1635
  { label: '🗑️ Delete', value: 'delete' }
1712
1636
  ];
1713
1637
 
@@ -1737,7 +1661,6 @@ async function showRequirementActions(req, sectionKey, tree) {
1737
1661
 
1738
1662
  // Display menu (always reprinted)
1739
1663
  console.log();
1740
- console.log(chalk.gray('💡 Press F for feedback - Share your thoughts anytime'));
1741
1664
  console.log(chalk.gray('What would you like to do? (↑/↓/Enter to select, ESC/← to go back)'));
1742
1665
  console.log();
1743
1666
  menuLines += 3; // Blank line + help text + blank line
@@ -1782,10 +1705,6 @@ async function showRequirementActions(req, sectionKey, tree) {
1782
1705
  process.exit(0);
1783
1706
  } else if (key.name === 'escape' || key.name === 'left') {
1784
1707
  return; // Go back
1785
- } else if (key.name === 'f') {
1786
- // Feedback button ( megaphone 📣 )
1787
- await handleFeedbackSubmission();
1788
- return;
1789
1708
  } else if (key.name === 'up') {
1790
1709
  selected = Math.max(0, selected - 1);
1791
1710
  } else if (key.name === 'down') {
@@ -1846,9 +1765,6 @@ async function performRequirementAction(action, req, sectionKey, tree) {
1846
1765
  case 'rename':
1847
1766
  await renameRequirement(req, sectionKey, tree);
1848
1767
  break;
1849
- case 'feedback':
1850
- await handleFeedbackSubmission();
1851
- break;
1852
1768
  }
1853
1769
 
1854
1770
  await new Promise(resolve => setTimeout(resolve, 1000));
@@ -2641,104 +2557,6 @@ async function showRequirementsBySection(sectionTitle) {
2641
2557
  }
2642
2558
  }
2643
2559
 
2644
- // Helper to move a requirement to the Recycled section
2645
- async function moveToRecycled(reqPath, requirementTitle, fromSectionTitle) {
2646
- const content = await fs.readFile(reqPath, 'utf8');
2647
- const lines = content.split('\n');
2648
-
2649
- let inFromSection = false;
2650
- let inRecycledSection = false;
2651
- let reqStartIndex = -1;
2652
- let reqEndIndex = -1;
2653
- let recycledSectionIndex = -1;
2654
- let requirement = null;
2655
-
2656
- // Find the requirement in the source section
2657
- for (let i = 0; i < lines.length; i++) {
2658
- const line = lines[i];
2659
-
2660
- // Track if we're in the source section
2661
- if (line.includes(fromSectionTitle)) {
2662
- inFromSection = true;
2663
- continue;
2664
- }
2665
-
2666
- // Exit source section at next ## header
2667
- if (inFromSection && line.startsWith('## ') && !line.startsWith('###') && !line.includes(fromSectionTitle)) {
2668
- inFromSection = false;
2669
- }
2670
-
2671
- // Find the requirement
2672
- if (inFromSection && line.trim() === `### ${requirementTitle}`) {
2673
- reqStartIndex = i;
2674
- requirement = { title: requirementTitle, details: [], package: null };
2675
-
2676
- // Read requirement details
2677
- for (let j = i + 1; j < lines.length; j++) {
2678
- const nextLine = lines[j];
2679
- // Stop at next requirement or section
2680
- if (nextLine.startsWith('###') || (nextLine.startsWith('## ') && !nextLine.startsWith('###'))) {
2681
- reqEndIndex = j;
2682
- break;
2683
- }
2684
- // Check for package
2685
- if (nextLine.startsWith('PACKAGE:')) {
2686
- requirement.package = nextLine.replace('PACKAGE:', '').trim();
2687
- } else if (nextLine.trim()) {
2688
- requirement.details.push(nextLine);
2689
- }
2690
- }
2691
-
2692
- if (reqEndIndex === -1) {
2693
- reqEndIndex = lines.length;
2694
- }
2695
- break;
2696
- }
2697
-
2698
- // Find Recycled section
2699
- if (line.includes('♻️ Recycled') || line.includes('## Recycled')) {
2700
- recycledSectionIndex = i;
2701
- inRecycledSection = true;
2702
- }
2703
- }
2704
-
2705
- if (!requirement || reqStartIndex === -1) {
2706
- throw new Error(`Requirement "${requirementTitle}" not found in ${fromSectionTitle}`);
2707
- }
2708
-
2709
- // If Recycled section doesn't exist, create it at the end
2710
- if (recycledSectionIndex === -1) {
2711
- recycledSectionIndex = lines.length;
2712
- lines.push('', '## ♻️ Recycled', '');
2713
- }
2714
-
2715
- // Remove requirement from source section
2716
- const removedLines = lines.splice(reqStartIndex, reqEndIndex - reqStartIndex);
2717
-
2718
- // Find where to insert in Recycled section (after the section header)
2719
- let insertIndex = recycledSectionIndex + 1;
2720
- while (insertIndex < lines.length && lines[insertIndex].trim() === '') {
2721
- insertIndex++;
2722
- }
2723
-
2724
- // Build requirement lines
2725
- const reqLines = [`### ${requirement.title}`];
2726
- if (requirement.package && requirement.package !== 'all') {
2727
- reqLines.push(`PACKAGE: ${requirement.package}`);
2728
- }
2729
- requirement.details.forEach(line => {
2730
- if (line.trim()) {
2731
- reqLines.push(line);
2732
- }
2733
- });
2734
- reqLines.push('');
2735
-
2736
- // Insert into Recycled section
2737
- lines.splice(insertIndex, 0, ...reqLines);
2738
-
2739
- await fs.writeFile(reqPath, lines.join('\n'));
2740
- }
2741
-
2742
2560
  // Helper to save reordered requirements back to file
2743
2561
  async function saveRequirementsOrder(reqPath, sectionTitle, requirements) {
2744
2562
  const content = await fs.readFile(reqPath, 'utf8');
@@ -2837,9 +2655,6 @@ async function showRequirementsFromChangelog() {
2837
2655
  }
2838
2656
  }
2839
2657
 
2840
- // Input suppression timestamp to avoid immediate re-entry into menus after cancel
2841
- let menuSuppressUntil = 0;
2842
-
2843
2658
  // Custom menu with both arrow keys and letter shortcuts
2844
2659
  async function showQuickMenu(items, initialSelectedIndex = 0) {
2845
2660
  return new Promise((resolve) => {
@@ -2851,7 +2666,6 @@ async function showQuickMenu(items, initialSelectedIndex = 0) {
2851
2666
  if (selectedIndex >= items.length) selectedIndex = 0;
2852
2667
 
2853
2668
  let isFirstRender = true;
2854
- let lastLinesPrinted = 0;
2855
2669
 
2856
2670
  // Helper to calculate visual lines occupied by text
2857
2671
  const getVisualLineCount = (text) => {
@@ -2877,10 +2691,9 @@ async function showQuickMenu(items, initialSelectedIndex = 0) {
2877
2691
  };
2878
2692
 
2879
2693
  const displayMenu = async () => {
2880
- // Clear entire screen on navigation to prevent text overlap with banner
2881
- if (!isFirstRender) {
2882
- // No need to console.clear() here as showWelcomeScreen does it,
2883
- // but ensuring it clears before we start waiting is fine too.
2694
+ // Only clear and redraw on first render, not on arrow navigation
2695
+ if (isFirstRender) {
2696
+ // Clear entire screen only on first render
2884
2697
  console.clear();
2885
2698
  // Reprint the banner and status info
2886
2699
  try {
@@ -2888,7 +2701,15 @@ async function showQuickMenu(items, initialSelectedIndex = 0) {
2888
2701
  } catch (err) {
2889
2702
  console.error('Error displaying banner:', err);
2890
2703
  }
2891
- }
2704
+ } else {
2705
+ // For arrow navigation, just move cursor to menu start position
2706
+ // and clear only the menu area, not the entire screen
2707
+ if (lastLinesPrinted > 0) {
2708
+ // Move cursor up to the start of the menu and clear menu area
2709
+ readline.moveCursor(process.stdout, 0, -lastLinesPrinted);
2710
+ readline.clearScreenDown(process.stdout);
2711
+ }
2712
+ }
2892
2713
  isFirstRender = false;
2893
2714
 
2894
2715
  // Track lines printed this render
@@ -2991,30 +2812,7 @@ async function showQuickMenu(items, initialSelectedIndex = 0) {
2991
2812
  process.stdin.setRawMode(true);
2992
2813
  }
2993
2814
 
2994
- // Ignore spurious keypresses for a short debounce window after the menu opens
2995
- // This prevents accidental immediate re-entry when returning from sub-menus.
2996
- const IGNORE_INPUT_MS = 300; // milliseconds
2997
- // If a recent menu cancel occurred, extend the ignore window to avoid key repeat re-opening
2998
- let ignoreInputUntil = Math.max(Date.now() + IGNORE_INPUT_MS, menuSuppressUntil || 0);
2999
-
3000
2815
  const onKeypress = async (str, key) => {
3001
- if (process.env.VCM_DEBUG_MENU === '1') {
3002
- const msg = `[DEBUG-MENU ${new Date().toISOString()}] onKeypress: str=${JSON.stringify(str)} key=${JSON.stringify(key)} now=${Date.now()}\n`;
3003
- console.log(msg);
3004
- try { require('fs').appendFileSync('/tmp/vcm-menu-debug.log', msg); } catch (e) { }
3005
- }
3006
- // Always allow Ctrl+C to pass through immediately
3007
- if (key && key.ctrl && key.name === 'c') {
3008
- cleanup();
3009
- process.exit(0);
3010
- return;
3011
- }
3012
-
3013
- // Debounce: ignore inputs until the short window passes
3014
- if (Date.now() < ignoreInputUntil) {
3015
- return;
3016
- }
3017
-
3018
2816
  if (!key) return;
3019
2817
 
3020
2818
  // Ctrl+C to exit
@@ -3036,7 +2834,6 @@ async function showQuickMenu(items, initialSelectedIndex = 0) {
3036
2834
  if (str && str.length === 1) {
3037
2835
  if (str === 'x') {
3038
2836
  // 'x' always maps to exit (will trigger confirmAndExit)
3039
- if (process.env.VCM_DEBUG_MENU === '1') console.log(`[DEBUG-MENU ${new Date().toISOString()}] quickMenu: 'x' pressed (exit)`);
3040
2837
  cleanup();
3041
2838
  // Don't clear screen for exit - keep status visible
3042
2839
  resolve({ value: 'exit', selectedIndex });
@@ -3096,11 +2893,6 @@ async function showQuickMenu(items, initialSelectedIndex = 0) {
3096
2893
  }
3097
2894
 
3098
2895
  async function showProviderManagerMenu() {
3099
- if (process.env.VCM_DEBUG_MENU === '1') {
3100
- const msg = `[DEBUG-MENU ${new Date().toISOString()}] showProviderManagerMenu() called\n` + new Error().stack.split('\n').slice(1, 6).join('\n') + '\n';
3101
- console.log(msg);
3102
- try { require('fs').appendFileSync('/tmp/vcm-menu-debug.log', msg); } catch (e) { }
3103
- }
3104
2896
  const definitions = getProviderDefinitions();
3105
2897
  const defMap = new Map(definitions.map(def => [def.id, def]));
3106
2898
  const prefs = await getProviderPreferences();
@@ -3110,8 +2902,6 @@ async function showProviderManagerMenu() {
3110
2902
  let dirty = false;
3111
2903
 
3112
2904
  const { fetchQuotaForAgent } = require('vibecodingmachine-core/src/quota-management');
3113
- const ProviderManager = require('vibecodingmachine-core/src/ide-integration/provider-manager.cjs');
3114
- const providerManager = new ProviderManager();
3115
2905
 
3116
2906
  const debugQuota = process.env.VCM_DEBUG_QUOTA === '1' || process.env.VCM_DEBUG_QUOTA === 'true';
3117
2907
 
@@ -3127,76 +2917,14 @@ async function showProviderManagerMenu() {
3127
2917
  return `${seconds}s`;
3128
2918
  };
3129
2919
 
3130
- const formatTimeAmPm = (date) => {
3131
- const hours24 = date.getHours();
3132
- const minutes = String(date.getMinutes()).padStart(2, '0');
3133
- const ampm = hours24 >= 12 ? 'pm' : 'am';
3134
- const hours12 = (hours24 % 12) || 12;
3135
- return `${hours12}:${minutes} ${ampm}`;
3136
- };
3137
-
3138
-
3139
-
3140
- // Pre-fetch data to avoid lag in render loop
3141
- const sharedAuth = require('vibecodingmachine-core/src/auth/shared-auth-storage');
3142
-
3143
- // Initialize caches from persistent storage
3144
- const savedCache = await getProviderCache();
3145
-
3146
- // Hydrate local maps from saved cache
3147
- const installationStatus = new Map();
3148
- const agentQuotas = new Map();
3149
-
3150
- Object.keys(savedCache).forEach(id => {
3151
- if (savedCache[id].installed !== undefined) {
3152
- installationStatus.set(id, savedCache[id].installed);
3153
- }
3154
- if (savedCache[id].quota) {
3155
- agentQuotas.set(id, savedCache[id].quota);
3156
- }
3157
- });
3158
-
3159
- // Also prefill agent quotas from the ProviderManager rate-limit file so the UI can
3160
- // display `[Resets at ...]` instantly (best-effort, read-only).
3161
- try {
3162
- const { getProviderRateLimitedQuotas } = require('./provider-rate-cache');
3163
- const prefetched = getProviderRateLimitedQuotas(definitions);
3164
- for (const [id, q] of prefetched) {
3165
- // Only set if we don't already have a richer quota cached
3166
- const existing = agentQuotas.get(id);
3167
- if (!existing || existing.type !== 'rate-limit') {
3168
- agentQuotas.set(id, q);
3169
- }
3170
- }
3171
- } catch (e) {
3172
- // Ignore — this is a non-critical optimization
3173
- }
3174
-
3175
- // Default values while loading
3176
- let quotaInfo = { maxIterations: 10, todayUsage: 0 };
3177
- let autoConfig = {};
3178
- // Active flag to prevent background render after menu is closed
3179
- let isMenuActive = true;
3180
-
3181
- // Load health metrics for all IDEs (load once at startup, not on every render)
3182
- let healthMetricsMap = new Map();
3183
- try {
3184
- const healthTracker = new IDEHealthTracker();
3185
- await healthTracker.load();
3186
- healthMetricsMap = await healthTracker.getAllHealthMetrics();
3187
- } catch (error) {
3188
- // Silently ignore health loading errors - health display is optional
3189
- }
3190
-
3191
- // Clear loading message
3192
- // process.stdout.write('\r\x1b[K'); // Removed as per diff
3193
-
3194
2920
  const render = async () => {
3195
- if (!isMenuActive) return; // Prevent rendering after menu closed
3196
2921
  process.stdout.write('\x1Bc');
3197
2922
  console.log(chalk.bold.cyan('⚙ ' + t('provider.title') + '\n'));
3198
2923
 
3199
- // Use cached quota info
2924
+ // Fetch quota info
2925
+ const sharedAuth = require('vibecodingmachine-core/src/auth/shared-auth-storage');
2926
+ const autoConfig = await getAutoConfig();
2927
+ const quotaInfo = await sharedAuth.canRunAutoMode();
3200
2928
  const remaining = Math.max(0, (quotaInfo.maxIterations || 10) - (quotaInfo.todayUsage || 0));
3201
2929
 
3202
2930
  // Calculate time until reset (midnight)
@@ -3207,65 +2935,15 @@ async function showProviderManagerMenu() {
3207
2935
  const hoursUntilReset = Math.floor(msUntilReset / (1000 * 60 * 60));
3208
2936
  const minsUntilReset = Math.floor((msUntilReset % (1000 * 60 * 60)) / (1000 * 60));
3209
2937
 
3210
- // Display quota, accounting for per-agent rate limits to avoid showing "10/10" when some
3211
- // providers are rate-limited. We avoid surfacing agent-specific "[Resets at ...]" labels in
3212
- // the top-level overall quota row and instead show a compact partial-availability message.
2938
+ // Display quota as time-based instead of numeric (0/1 or 1/1 format)
3213
2939
  let quotaDisplay;
3214
-
3215
- // Detect how many agents are currently rate-limited
3216
- const rateLimitedAgents = Array.from(agentQuotas.values()).filter(q => {
3217
- if (!q) return false;
3218
- if (q.type !== 'rate-limit') return false;
3219
- if (typeof q.isExceeded === 'function') return q.isExceeded();
3220
- if (q.remaining !== undefined) return q.remaining <= 0;
3221
- return false;
3222
- });
3223
- const rateLimitedCount = rateLimitedAgents.length;
3224
-
3225
2940
  if (remaining === 0) {
3226
- // Overall rate limit active - show when it resets (in red)
2941
+ // Rate limit active - show when it resets (in red)
3227
2942
  quotaDisplay = chalk.gray(' ' + t('provider.overall.quota') + ': ') + chalk.red(`⏰ ${t('provider.rate.limit.resets')} ${hoursUntilReset}h ${minsUntilReset}m`);
3228
- } else if (rateLimitedCount > 0) {
3229
- // Some providers are rate-limited — show partial availability instead of full 10/10
3230
- const effectiveRemaining = Math.max(0, remaining - rateLimitedCount);
3231
-
3232
- // If one or more rate-limited agents provide a specific reset time, show that instead
3233
- // of a generic "Resets in Xh Ym" message to match per-agent rows (e.g., "[Resets at 5:00 pm]").
3234
- const earliestReset = rateLimitedAgents.reduce((min, q) => {
3235
- if (!q || !q.resetsAt) return min;
3236
- try {
3237
- const d = new Date(q.resetsAt);
3238
- if (Number.isNaN(d.getTime())) return min;
3239
- if (!min) return q.resetsAt;
3240
- return d.getTime() < new Date(min).getTime() ? q.resetsAt : min;
3241
- } catch (e) {
3242
- return min;
3243
- }
3244
- }, null);
3245
-
3246
- const resetLabel = earliestReset ? formatResetsAtLabel(earliestReset) : null;
3247
-
3248
- quotaDisplay = chalk.gray(' ' + t('provider.overall.quota') + ': ') + chalk.yellow(`⚠️ ${t('provider.available')} (${effectiveRemaining}/${quotaInfo.maxIterations})`) + chalk.gray(` • ${rateLimitedCount} provider${rateLimitedCount > 1 ? 's' : ''} limited`);
3249
-
3250
- if (resetLabel) {
3251
- quotaDisplay += ' ' + chalk.red(`[${resetLabel}]`);
3252
- } else {
3253
- quotaDisplay += chalk.gray(' • ' + t('provider.resets.in') + ' ') + chalk.cyan(`${hoursUntilReset}h ${minsUntilReset}m`);
3254
- }
3255
2943
  } else {
3256
2944
  // Quota available - show when it resets (in green)
3257
- // If the quota is full (no usage), don't show the reset time it's unnecessary and
3258
- // can be confusing when nothing is rate-limited.
3259
- const fullQuota = remaining === (quotaInfo.maxIterations || 10);
3260
- quotaDisplay = chalk.gray(' ' + t('provider.overall.quota') + ': ') + chalk.green(`✓ ${t('provider.available')} (${remaining}/${quotaInfo.maxIterations})`);
3261
- if (!fullQuota) {
3262
- quotaDisplay += chalk.gray(' • ' + t('provider.resets.in') + ' ') + chalk.cyan(`${hoursUntilReset}h ${minsUntilReset}m`);
3263
- }
2945
+ quotaDisplay = chalk.gray(' ' + t('provider.overall.quota') + ': ') + chalk.green(`✓ ${t('provider.available')} (${remaining}/${quotaInfo.maxIterations})`) + chalk.gray(' ' + t('provider.resets.in') + ' ') + chalk.cyan(`${hoursUntilReset}h ${minsUntilReset}m`);
3264
2946
  }
3265
-
3266
- // Display Legend
3267
- console.log(chalk.gray(' Interface types: 🖥️ GUI Automation ⚡ Direct Automation'));
3268
- console.log(chalk.gray(' Provider type: ☁️ Cloud 🏠 Local (Slow, Requires lots of memory and CPU, Private)'));
3269
2947
  console.log(quotaDisplay);
3270
2948
  console.log(chalk.gray(' ' + t('provider.instructions') + '\n'));
3271
2949
 
@@ -3276,137 +2954,91 @@ async function showProviderManagerMenu() {
3276
2954
  const isSelected = idx === selectedIndex;
3277
2955
  const isEnabled = enabled[id] !== false;
3278
2956
 
3279
- // Use cached installation status (default to false if not found in cache)
3280
- // For LLMs, we consider them always "installed" (cloud)
3281
- let isInstalled = def.type === 'ide' ? (installationStatus.get(id) === true) : true;
3282
- let quota = agentQuotas.get(id);
3283
-
3284
- // Determine status emoji based on user requirements:
3285
- // (Grey Square): Not installed
3286
- // ⚠️ (Yellow Triangle): Installed but (Quota limit OR Not verified)
3287
- // 🚫 (Red Circle): Disabled
3288
- // 🛑 (Red Stop Sign): Not working/Error
3289
- // 🟢 (Green Circle): Available/Working
2957
+ // Check installation status for all IDEs
2958
+ let isInstalled = true;
2959
+ if (def.type === 'ide') {
2960
+ try {
2961
+ if (id === 'kiro') {
2962
+ const { isKiroInstalled } = require('./kiro-installer');
2963
+ isInstalled = isKiroInstalled();
2964
+ } else if (id === 'cursor') {
2965
+ isInstalled = await checkAppOrBinary(['Cursor'], ['cursor']);
2966
+ } else if (id === 'windsurf') {
2967
+ isInstalled = await checkAppOrBinary(['Windsurf'], ['windsurf']);
2968
+ } else if (id === 'vscode') {
2969
+ isInstalled = await checkAppOrBinary(['Visual Studio Code', 'Visual Studio Code - Insiders'], ['code', 'code-insiders']);
2970
+ } else if (id === 'antigravity') {
2971
+ isInstalled = await checkAppOrBinary(['Antigravity'], ['antigravity']);
2972
+ } else {
2973
+ // For other IDEs, try binary name same as id
2974
+ isInstalled = await checkAppOrBinary([], [id]);
2975
+ }
2976
+ } catch (e) {
2977
+ // If detection fails, assume not installed
2978
+ isInstalled = false;
2979
+ }
2980
+ }
3290
2981
 
2982
+ // Determine status emoji: disabled = red alert, not installed = yellow, enabled = green
3291
2983
  let statusEmoji;
3292
- let statusText = '';
3293
-
3294
2984
  if (!isEnabled) {
3295
- statusEmoji = '🚫'; // Disabled
3296
- } else if (!isInstalled) {
3297
- statusEmoji = ''; // Not installed
2985
+ statusEmoji = '🚨'; // Red for disabled
2986
+ } else if (def.type === 'ide' && !isInstalled) {
2987
+ statusEmoji = '🟡'; // Yellow for not installed
3298
2988
  } else {
3299
- // Installed and Enabled, assume Green unless quota or error
3300
- statusEmoji = '🟢';
3301
-
3302
- if (def.type === 'ide') {
3303
- // TODO: Add actual "verified" check if available. For now assuming installed = verified.
3304
- // If we had a verification check:
3305
- // if (!isVerified) statusEmoji = '⚠️';
3306
- }
2989
+ statusEmoji = '🟢'; // Green for enabled and installed (or LLM)
2990
+ }
3307
2991
 
3308
- // Check Quota
3309
- if (quota) {
3310
- // Handle both Quota class instances and plain JSON objects from cache
3311
- let isExceeded = false;
3312
- if (typeof quota.isExceeded === 'function') {
3313
- isExceeded = quota.isExceeded();
3314
- } else if (quota.remaining !== undefined) {
3315
- isExceeded = quota.remaining <= 0;
3316
- }
2992
+ const typeLabel = def.type === 'ide' ? chalk.cyan('IDE') : chalk.cyan('LLM');
2993
+ const prefix = isSelected ? chalk.cyan('❯') : ' ';
2994
+ let line = `${prefix} ${statusEmoji} ${idx + 1}. ${def.name} ${chalk.gray(`(${def.id})`)} ${typeLabel}`;
3317
2995
 
3318
- if (quota.type === 'rate-limit' && isExceeded) {
3319
- statusEmoji = '⚠️ ';
3320
- if (quota.resetsAt) {
3321
- const label = formatResetsAtLabel(quota.resetsAt);
3322
- statusText = label ? ` ${chalk.red(`⏰ ${label}`)}` : ` ${chalk.red('[Rate limited]')}`;
3323
- } else {
3324
- statusText = ` ${chalk.red('[Rate limited]')}`;
3325
- }
3326
- }
2996
+ // Fetch and display specific quota for this agent
2997
+ try {
2998
+ // Find the active model for this provider if possible
2999
+ let model = def.defaultModel || id;
3000
+ if (id === 'groq') model = autoConfig.groqModel || model;
3001
+ else if (id === 'anthropic') model = autoConfig.anthropicModel || model;
3002
+ else if (id === 'ollama') {
3003
+ const preferredModel = autoConfig.llmModel && autoConfig.llmModel.includes('ollama/')
3004
+ ? autoConfig.llmModel.split('/')[1]
3005
+ : autoConfig.llmModel || autoConfig.aiderModel;
3006
+ model = (preferredModel && preferredModel !== id) ? preferredModel : model;
3327
3007
  }
3328
- }
3329
-
3330
- // Determine Interface Type Icon (GUI vs Direct Automation)
3331
- let interfaceIcon = '⚡'; // Default to Direct Automation
3332
- if (def.type === 'ide') {
3333
- interfaceIcon = '🖥️'; // GUI Automation for IDEs
3334
- }
3335
3008
 
3336
- // Determine Provider Type Icon (Cloud vs Local)
3337
- let providerTypeIcon = '☁️'; // Default to Cloud (IDEs and most LLMs)
3338
- if (def.type === 'direct' && def.category === 'llm' && def.id === 'ollama') {
3339
- providerTypeIcon = '🏠'; // Local (only for Ollama)
3340
- }
3009
+ const agentId = `${id}:${model}`;
3010
+ const quota = await fetchQuotaForAgent(agentId);
3341
3011
 
3342
- // Trim emojis to remove any hidden characters and ensure clean spacing
3343
- interfaceIcon = interfaceIcon.trim();
3344
- providerTypeIcon = providerTypeIcon.trim();
3345
- const isWarningEmoji = statusEmoji.includes('⚠️');
3346
- statusEmoji = statusEmoji.trim();
3347
-
3348
- // Determine spacing based on interface icon type and provider type
3349
- // Multi-char icons (🖥️) need double spaces, single-char icons (⚡) use single space after interface icon
3350
- const isMultiCharInterface = interfaceIcon.length > 1;
3351
- const spaceAfterInterface = isMultiCharInterface ? ' ' : ' ';
3352
- // Local provider (🏠) uses single space, others use double space after provider icon
3353
- const isLocalProvider = providerTypeIcon === '🏠';
3354
- const spaceAfterProvider = isLocalProvider ? ' ' : ' ';
3355
-
3356
- // Adjust spacing after status emoji - warning emoji needs extra space
3357
- const spaceAfterStatus = isWarningEmoji ? ' ' : ' ';
3358
-
3359
- const prefix = isSelected ? chalk.cyan('❯') + ' ' : ' ';
3360
-
3361
- // Get health metrics for this IDE (if available)
3362
- let healthCounters = '';
3363
- const ideMetrics = healthMetricsMap.get(def.id);
3364
- if (ideMetrics) {
3365
- const counters = HealthReporter.formatInlineCounters(ideMetrics);
3366
- if (counters) {
3367
- // Color code the counters: green for +N, red for -M
3368
- const coloredCounters = counters
3369
- .split(' ')
3370
- .map(part => part.startsWith('+') ? chalk.green(part) : chalk.red(part))
3371
- .join(' ');
3372
- healthCounters = ` ${coloredCounters}`;
3012
+ if (debugQuota) {
3013
+ const resetMs = quota?.resetsAt ? (new Date(quota.resetsAt).getTime() - Date.now()) : null;
3014
+ console.error(`[VCM_DEBUG_QUOTA] provider=${id} model=${model} type=${quota?.type} remaining=${quota?.remaining} limit=${quota?.limit} resetsAt=${quota?.resetsAt ? new Date(quota.resetsAt).toISOString() : 'null'} resetIn=${resetMs !== null ? formatDuration(resetMs) : 'null'}`);
3373
3015
  }
3374
- }
3375
-
3376
- // Status emoji is now first, followed by interface icon and provider type icon
3377
- let line = `${prefix}${statusEmoji}${spaceAfterStatus}${interfaceIcon}${spaceAfterInterface}${providerTypeIcon}${spaceAfterProvider} ${idx + 1}. ${def.name} ${chalk.gray(`(${def.id})`)}${healthCounters}${statusText}`;
3378
- if (debugQuota && quota) {
3379
- // Append extra debug info if needed
3380
- const resetMs = quota?.resetsAt ? (new Date(quota.resetsAt).getTime() - Date.now()) : null;
3381
- line += chalk.gray(` [D: type=${quota?.type} rem=${quota?.remaining} lim=${quota?.limit} resetIn=${resetMs !== null ? formatDuration(resetMs) : 'null'}]`);
3382
- }
3383
- console.log(line);
3384
3016
 
3385
- if (Array.isArray(def.subAgents) && def.subAgents.length > 0) {
3386
- for (const sub of def.subAgents) {
3387
- const info = providerManager.getRateLimitInfo(def.id, sub.model);
3388
- let subStatusEmoji;
3389
- let subStatusText = '';
3390
-
3391
- if (!isEnabled) {
3392
- subStatusEmoji = '🚫';
3393
- } else if (!isInstalled) {
3394
- subStatusEmoji = '⬜';
3395
- } else if (info && info.isRateLimited) {
3396
- subStatusEmoji = '⚠️';
3397
- if (info.resetTime) {
3398
- const label = formatResetsAtLabel(info.resetTime);
3399
- subStatusText = label ? ` ${chalk.red(`⏰ ${label}`)}` : ` ${chalk.red('[Rate limited]')}`;
3017
+ if (quota.type === 'infinite') {
3018
+ line += ` ${chalk.gray('[' + t('provider.status.quota.infinite') + ']')}`;
3019
+ } else if (quota.type === 'rate-limit') {
3020
+ if (quota.isExceeded()) {
3021
+ if (quota.resetsAt) {
3022
+ const msUntilReset = new Date(quota.resetsAt).getTime() - Date.now();
3023
+ line += ` ${chalk.red(`[⏳ resets in ${formatDuration(msUntilReset)}]`)}`;
3400
3024
  } else {
3401
- subStatusText = ` ${chalk.red('[Rate limited]')}`;
3025
+ line += ` ${chalk.red('[Rate limited]')}`;
3402
3026
  }
3403
3027
  } else {
3404
- subStatusEmoji = '🟢';
3028
+ // Show time until rate limit starts (when it resets)
3029
+ if (quota.resetsAt) {
3030
+ const msUntilReset = new Date(quota.resetsAt).getTime() - Date.now();
3031
+ line += ` ${chalk.green(`[✓ ${t('provider.status.available.resets')} ${formatDuration(msUntilReset)}]`)}`;
3032
+ } else {
3033
+ line += ` ${chalk.green('[' + t('provider.status.available') + ']')}`;
3034
+ }
3405
3035
  }
3406
-
3407
- console.log(` ${subStatusEmoji} ${chalk.gray('↳')} ${sub.name} ${chalk.gray(`(${sub.model})`)}${subStatusText}`);
3408
3036
  }
3037
+ } catch (e) {
3038
+ // Silently skip if quota fetch fails
3409
3039
  }
3040
+
3041
+ console.log(line);
3410
3042
  }
3411
3043
 
3412
3044
  console.log();
@@ -3417,143 +3049,6 @@ async function showProviderManagerMenu() {
3417
3049
  }
3418
3050
  };
3419
3051
 
3420
- // Background loading function
3421
- const loadDataInBackground = async () => {
3422
- try {
3423
- // 1. Load config and auth info
3424
- const [config, quota] = await Promise.all([
3425
- getAutoConfig(),
3426
- sharedAuth.canRunAutoMode()
3427
- ]);
3428
- autoConfig = config;
3429
- quotaInfo = quota;
3430
- if (typeof render === 'function') await render();
3431
-
3432
- // 2. Check installation status in parallel
3433
- const installPromises = definitions.map(async (def) => {
3434
- let isInstalled = true;
3435
- if (def.type === 'ide') {
3436
- try {
3437
- if (def.id === 'kiro') {
3438
- const { isKiroInstalled } = require('./kiro-installer');
3439
- isInstalled = isKiroInstalled();
3440
- } else if (def.id === 'cursor') {
3441
- isInstalled = await checkAppOrBinary(['Cursor'], ['cursor']);
3442
- } else if (def.id === 'windsurf') {
3443
- isInstalled = await checkAppOrBinary(['Windsurf'], ['windsurf']);
3444
- } else if (def.id === 'vscode') {
3445
- isInstalled = await checkAppOrBinary(['Visual Studio Code', 'Visual Studio Code - Insiders'], ['code', 'code-insiders']);
3446
- } else if (def.id === 'github-copilot' || def.id === 'amazon-q') {
3447
- // Check if VS Code is installed AND the extension is installed
3448
- const vscodeInstalled = await checkAppOrBinary(['Visual Studio Code', 'Visual Studio Code - Insiders'], ['code', 'code-insiders']);
3449
- if (vscodeInstalled) {
3450
- isInstalled = await checkVSCodeExtension(def.id);
3451
- } else {
3452
- isInstalled = false;
3453
- }
3454
- } else if (def.id === 'replit') {
3455
- // Replit is web-based, always considered "installed"
3456
- isInstalled = true;
3457
- } else if (def.id === 'antigravity') {
3458
- isInstalled = await checkAppOrBinary(['Antigravity'], ['antigravity']);
3459
- } else {
3460
- isInstalled = await checkAppOrBinary([], [def.id]);
3461
- }
3462
- } catch (e) {
3463
- isInstalled = false;
3464
- }
3465
- }
3466
- installationStatus.set(def.id, isInstalled);
3467
- return { id: def.id, installed: isInstalled };
3468
- });
3469
-
3470
- const installResults = await Promise.all(installPromises);
3471
-
3472
- // Update cache
3473
- let newCache = {};
3474
- installResults.forEach(r => {
3475
- newCache[r.id] = { ...(savedCache[r.id] || {}), installed: r.installed };
3476
- });
3477
- await setProviderCache(newCache);
3478
-
3479
- if (typeof render === 'function') await render();
3480
-
3481
- // 3. Check quotas in parallel
3482
- const quotaPromises = definitions.map(async (def) => {
3483
- try {
3484
- let model = def.defaultModel || def.id;
3485
- if (def.id === 'groq') model = autoConfig.groqModel || model;
3486
- else if (def.id === 'anthropic') model = autoConfig.anthropicModel || model;
3487
- else if (def.id === 'ollama') {
3488
- const preferredModel = autoConfig.llmModel && autoConfig.llmModel.includes('ollama/')
3489
- ? autoConfig.llmModel.split('/')[1]
3490
- : autoConfig.llmModel || autoConfig.aiderModel;
3491
- model = (preferredModel && preferredModel !== def.id) ? preferredModel : model;
3492
- }
3493
-
3494
- const agentId = `${def.id}:${model}`;
3495
- const quota = await fetchQuotaForAgent(agentId);
3496
-
3497
- // Merge cautiously: if we already have an active rate-limit prefetched from
3498
- // ProviderManager, avoid overwriting it with a non-rate-limit result which
3499
- // could make the UI lose the reset information briefly. If both are rate-limits,
3500
- // prefer the earliest reset time.
3501
- try {
3502
- const existing = agentQuotas.get(def.id);
3503
- const existingIsActiveRateLimit = existing && existing.type === 'rate-limit' && new Date(existing.resetsAt).getTime() > Date.now();
3504
- const newIsRateLimit = quota && quota.type === 'rate-limit';
3505
-
3506
- if (existingIsActiveRateLimit && !newIsRateLimit) {
3507
- // Keep the existing active rate-limit and don't overwrite
3508
- } else if (existingIsActiveRateLimit && newIsRateLimit) {
3509
- // Both rate-limited: keep the one with the earliest reset
3510
- const existingReset = new Date(existing.resetsAt).getTime();
3511
- const newReset = quota && quota.resetsAt ? new Date(quota.resetsAt).getTime() : null;
3512
- if (!newReset || existingReset <= newReset) {
3513
- // keep existing
3514
- } else {
3515
- agentQuotas.set(def.id, quota);
3516
- }
3517
- } else {
3518
- // No active existing rate limit — set/overwrite normally
3519
- agentQuotas.set(def.id, quota);
3520
- }
3521
- } catch (e) {
3522
- // On any error, set the quota to avoid leaving stale state
3523
- agentQuotas.set(def.id, quota);
3524
- }
3525
-
3526
- return { id: def.id, quota };
3527
- } catch (e) {
3528
- return null;
3529
- }
3530
- });
3531
-
3532
- const quotaResults = await Promise.all(quotaPromises);
3533
-
3534
- // Update cache with quotas
3535
- newCache = {};
3536
- quotaResults.forEach(r => {
3537
- if (r) {
3538
- newCache[r.id] = { ...(savedCache[r.id] || {}), quota: r.quota };
3539
- // Preserve installed status if we just set it
3540
- if (installationStatus.has(r.id)) {
3541
- newCache[r.id].installed = installationStatus.get(r.id);
3542
- }
3543
- }
3544
- });
3545
- await setProviderCache(newCache);
3546
-
3547
- if (typeof render === 'function') await render();
3548
-
3549
- } catch (error) {
3550
- // Silently fail in background
3551
- }
3552
- };
3553
-
3554
- // Start background loading
3555
- loadDataInBackground();
3556
-
3557
3052
  if (process.env.VCM_RENDER_ONCE === '1' || process.env.VCM_RENDER_ONCE === 'true') {
3558
3053
  await render();
3559
3054
  return;
@@ -3561,8 +3056,6 @@ async function showProviderManagerMenu() {
3561
3056
 
3562
3057
  return new Promise(async (resolve) => {
3563
3058
  const cleanup = () => {
3564
- // Mark menu as inactive so background loaders stop rendering
3565
- isMenuActive = false;
3566
3059
  if (process.stdin.isTTY && process.stdin.setRawMode) {
3567
3060
  process.stdin.setRawMode(false);
3568
3061
  }
@@ -3599,13 +3092,6 @@ async function showProviderManagerMenu() {
3599
3092
  };
3600
3093
 
3601
3094
  const cancel = () => {
3602
- // Suppress input for a short period to avoid buffered key repeats reopening menus
3603
- menuSuppressUntil = Date.now() + 1000; // 1 second
3604
- if (process.env.VCM_DEBUG_MENU === '1') {
3605
- const msg = `[DEBUG-MENU ${new Date().toISOString()}] menu cancelled; suppress until ${new Date(menuSuppressUntil).toISOString()}\n`;
3606
- console.log(msg);
3607
- try { require('fs').appendFileSync('/tmp/vcm-menu-debug.log', msg); } catch (e) { }
3608
- }
3609
3095
  cleanup();
3610
3096
  console.log('\n');
3611
3097
  resolve(null);
@@ -3637,104 +3123,12 @@ async function showProviderManagerMenu() {
3637
3123
  await render();
3638
3124
  };
3639
3125
 
3640
- const installSelected = async () => {
3641
- const id = order[selectedIndex];
3642
- const def = defMap.get(id);
3643
-
3644
- if (!def) return;
3645
-
3646
- // Only IDE providers can be installed
3647
- if (def.type !== 'ide') {
3648
- console.log(chalk.yellow('\n⚠️ Installation is only available for IDE providers.\n'));
3649
- await render();
3650
- return;
3651
- }
3652
-
3653
- // Check if already installed
3654
- const isInstalled = installationStatus.get(id);
3655
- if (isInstalled) {
3656
- console.log(chalk.yellow(`\n⚠️ ${def.name} is already installed.\n`));
3657
- await render();
3658
- return;
3659
- }
3660
-
3661
- console.log(chalk.cyan(`\n🔧 Installing ${def.name}...\n`));
3662
-
3663
- try {
3664
- const appleScriptManager = new AppleScriptManager();
3665
- const repoPath = await getRepoPath();
3666
-
3667
- // Use openIDE method which handles installation
3668
- await appleScriptManager.openIDE(id, repoPath);
3669
-
3670
- // Re-check installation status
3671
- let newStatus = true;
3672
- try {
3673
- if (id === 'kiro') {
3674
- const { isKiroInstalled } = require('./kiro-installer');
3675
- newStatus = isKiroInstalled();
3676
- } else if (id === 'cursor') {
3677
- newStatus = await checkAppOrBinary(['Cursor'], ['cursor']);
3678
- } else if (id === 'windsurf') {
3679
- newStatus = await checkAppOrBinary(['Windsurf'], ['windsurf']);
3680
- } else if (id === 'vscode') {
3681
- newStatus = await checkAppOrBinary(['Visual Studio Code', 'Visual Studio Code - Insiders'], ['code', 'code-insiders']);
3682
- } else if (id === 'github-copilot' || id === 'amazon-q') {
3683
- // Check if VS Code is installed AND the extension is installed
3684
- const vscodeInstalled = await checkAppOrBinary(['Visual Studio Code', 'Visual Studio Code - Insiders'], ['code', 'code-insiders']);
3685
- if (vscodeInstalled) {
3686
- newStatus = await checkVSCodeExtension(id);
3687
- } else {
3688
- newStatus = false;
3689
- }
3690
- } else if (id === 'antigravity') {
3691
- newStatus = await checkAppOrBinary(['Antigravity'], ['antigravity']);
3692
- } else if (id === 'replit') {
3693
- // Replit is web-based, always considered "installed"
3694
- newStatus = true;
3695
- } else {
3696
- newStatus = await checkAppOrBinary([], [id]);
3697
- }
3698
- } catch (e) {
3699
- newStatus = false;
3700
- }
3701
-
3702
- installationStatus.set(id, newStatus);
3703
-
3704
- // Update cache
3705
- const savedCache = await getProviderCache();
3706
- const newCache = {
3707
- ...savedCache,
3708
- [id]: { ...(savedCache[id] || {}), installed: newStatus }
3709
- };
3710
- await setProviderCache(newCache);
3711
-
3712
- if (newStatus) {
3713
- console.log(chalk.green(`\n✓ ${def.name} installed successfully!\n`));
3714
- } else {
3715
- console.log(chalk.yellow(`\n⚠️ Installation may have completed, but ${def.name} was not detected. Please check manually.\n`));
3716
- }
3717
- } catch (error) {
3718
- console.log(chalk.red(`\n✗ Installation failed: ${error.message}\n`));
3719
- }
3720
-
3721
- await render();
3722
- };
3723
-
3724
- // Ignore spurious keypresses for a short debounce window after opening the menu
3725
- const IGNORE_INPUT_MS = 300; // milliseconds
3726
- let ignoreInputUntil = Date.now() + IGNORE_INPUT_MS;
3727
-
3728
3126
  const onKeypress = async (str, key = {}) => {
3729
- // Allow immediate Ctrl+C regardless of debounce
3730
3127
  if (key.ctrl && key.name === 'c') {
3731
3128
  cancel();
3732
3129
  return;
3733
3130
  }
3734
3131
 
3735
- // Debounce: ignore inputs that happen too soon after the menu opens
3736
- if (Date.now() < ignoreInputUntil) return;
3737
-
3738
3132
  switch (key.name) {
3739
3133
  case 'up':
3740
3134
  await moveSelection(-1);
@@ -3754,22 +3148,15 @@ async function showProviderManagerMenu() {
3754
3148
  case 'd':
3755
3149
  await toggle(false);
3756
3150
  break;
3757
- case 'i':
3758
- await installSelected();
3759
- break;
3760
3151
  case 'space':
3761
3152
  await toggle(!(enabled[order[selectedIndex]] !== false));
3762
3153
  break;
3763
3154
  case 'return':
3764
3155
  saveAndExit(order[selectedIndex]);
3765
3156
  break;
3766
- case 'left':
3767
- // Left arrow: save any pending changes and exit (match Enter's save behavior for order)
3768
- saveAndExit();
3769
- break;
3770
3157
  case 'escape':
3158
+ case 'left':
3771
3159
  case 'x':
3772
- // Escape or 'x' should cancel without persisting pending changes
3773
3160
  cancel();
3774
3161
  break;
3775
3162
  default:
@@ -3788,54 +3175,23 @@ async function showProviderManagerMenu() {
3788
3175
  });
3789
3176
  }
3790
3177
 
3791
- /**
3792
- * Get health metrics for IDE choices
3793
- */
3794
- async function getIDEHealthMetrics() {
3795
- try {
3796
- const healthTracker = new IDEHealthTracker();
3797
- const allMetrics = await healthTracker.getAllHealthMetrics();
3798
- return allMetrics;
3799
- } catch (error) {
3800
- // Health tracking not available, return empty metrics
3801
- return new Map();
3802
- }
3803
- }
3804
-
3805
- /**
3806
- * Format IDE name with health metrics
3807
- */
3808
- async function formatIDEChoiceWithHealth(ideName, ideValue, allMetrics) {
3809
- const metrics = allMetrics.get(ideValue);
3810
-
3811
- if (!metrics || metrics.totalInteractions === 0) {
3812
- return ideName; // No health data available
3813
- }
3814
-
3815
- const successCount = metrics.successCount || 0;
3816
- const failureCount = metrics.failureCount || 0;
3817
- const healthIndicator = metrics.consecutiveFailures === 0 ? '✅' : '⚠️';
3818
-
3819
- return `${ideName} ${healthIndicator} +${successCount} -${failureCount}`;
3820
- }
3821
-
3822
- async function showSettings() {
3178
+ /* async function showSettings() {
3823
3179
  console.log(chalk.bold.cyan('\n⚙️ Settings\n'));
3824
-
3180
+
3825
3181
  const { setConfigValue } = require('vibecodingmachine-core');
3826
3182
  const { getAutoConfig, setAutoConfig } = require('./config');
3827
3183
  const currentHostnameEnabled = await isComputerNameEnabled();
3828
3184
  const hostname = getHostname();
3829
3185
  const autoConfig = await getAutoConfig();
3830
3186
  const currentIDE = autoConfig.ide || 'claude-code';
3831
-
3187
+
3832
3188
  // Show current settings
3833
3189
  console.log(chalk.gray('Current settings:'));
3834
3190
  console.log(chalk.gray(' Computer Name: '), chalk.cyan(hostname));
3835
3191
  console.log(chalk.gray(' Current IDE: '), chalk.cyan(formatIDEName(currentIDE)));
3836
3192
  console.log(chalk.gray(' Use Hostname in Req File: '), currentHostnameEnabled ? chalk.green('Enabled ✓') : chalk.yellow('Disabled ○'));
3837
3193
  console.log();
3838
-
3194
+
3839
3195
  const { action } = await inquirer.prompt([
3840
3196
  {
3841
3197
  type: 'list',
@@ -3857,59 +3213,54 @@ async function showSettings() {
3857
3213
  ]
3858
3214
  }
3859
3215
  ]);
3860
-
3861
- /**
3862
- * Get health metrics for IDE choices
3863
- */
3864
- async function getIDEHealthMetrics() {
3865
- try {
3866
- const healthTracker = new IDEHealthTracker();
3867
- const allMetrics = await healthTracker.getAllHealthMetrics();
3868
- return allMetrics;
3869
- } catch (error) {
3870
- // Health tracking not available, return empty metrics
3871
- return new Map();
3872
- }
3873
- }
3874
-
3875
- /**
3876
- * Format IDE name with health metrics
3877
- */
3878
- async function formatIDEChoiceWithHealth(ideName, ideValue, allMetrics) {
3879
- const metrics = allMetrics.get(ideValue);
3880
-
3881
- if (!metrics || metrics.totalInteractions === 0) {
3882
- return ideName; // No health data available
3883
- }
3884
-
3885
- const successCount = metrics.successCount || 0;
3886
- const failureCount = metrics.failureCount || 0;
3887
- const healthIndicator = metrics.consecutiveFailures === 0 ? '✅' : '⚠️';
3888
-
3889
- return `${ideName} ${healthIndicator} +${successCount} -${failureCount}`;
3890
- }
3891
3216
 
3892
- async function showSettings() {
3217
+ if (action === 'change-ide') {
3218
+ const { ide } = await inquirer.prompt([
3219
+ {
3220
+ type: 'list',
3221
+ name: 'ide',
3222
+ message: 'Select IDE:',
3223
+ choices: [
3224
+ { name: 'Claude Code CLI (recommended - Anthropic Claude)', value: 'claude-code' },
3225
+ { name: 'Aider CLI (best for Ollama & Bedrock)', value: 'aider' },
3226
+ { name: 'Continue CLI (Ollama support, but --auto mode doesn\'t execute code)', value: 'continue' },
3227
+ { name: 'Cline CLI (local AI alternative, but has Ollama connection issues)', value: 'cline' },
3228
+ { name: 'Cursor', value: 'cursor' },
3229
+ { name: 'VS Code', value: 'vscode' },
3230
+ { name: 'Windsurf', value: 'windsurf' }
3231
+ ],
3232
+ default: currentIDE
3233
+ }
3234
+ ]);
3235
+
3236
+ // Save to config
3237
+ const newConfig = { ...autoConfig, ide };
3238
+ await setAutoConfig(newConfig);
3239
+
3240
+ console.log(chalk.green('\n✓'), `IDE changed to ${chalk.cyan(formatIDEName(ide))}`);
3241
+ console.log(chalk.gray(' Note: This will be used for the next Auto Mode session.'));
3242
+ console.log();
3243
+ } else if (action === 'toggle-hostname') {
3893
3244
  const newValue = !currentHostnameEnabled;
3894
-
3245
+
3895
3246
  // Save to shared config (same location as Electron app)
3896
3247
  await setConfigValue('computerNameEnabled', newValue);
3897
-
3248
+
3898
3249
  const statusText = newValue ? chalk.green('enabled') : chalk.yellow('disabled');
3899
3250
  console.log(chalk.green('\n✓'), `Hostname in requirements file ${statusText}`);
3900
-
3251
+
3901
3252
  if (newValue) {
3902
3253
  console.log(chalk.gray('\n Requirements file will be:'), chalk.cyan(`REQUIREMENTS-${hostname}.md`));
3903
3254
  } else {
3904
3255
  console.log(chalk.gray('\n Requirements file will be:'), chalk.cyan('REQUIREMENTS.md'));
3905
3256
  }
3906
-
3257
+
3907
3258
  console.log(chalk.gray('\n Note: You may need to rename your existing requirements file.'));
3908
3259
  console.log(chalk.gray(' Note: This setting is now synced with the Electron app.'));
3909
3260
  console.log();
3910
3261
  }
3911
3262
  }
3912
-
3263
+
3913
3264
  /**
3914
3265
  * Show cloud sync management menu
3915
3266
  */
@@ -4180,1372 +3531,11 @@ async function showCloudSyncMenu() {
4180
3531
  }
4181
3532
  }
4182
3533
 
4183
- async function startInteractive() {
4184
- // STRICT AUTH CHECK (only if enabled)
4185
- const authEnabled = process.env.AUTH_ENABLED === 'true';
4186
-
4187
- if (authEnabled) {
4188
- const auth = require('./auth');
4189
- const isAuth = await auth.isAuthenticated();
4190
-
4191
- if (!isAuth) {
4192
- console.clear();
4193
- console.log(chalk.bold.cyan('\nVibe Coding Machine CLI'));
4194
- console.log(chalk.cyan('\n🔒 Authentication Required'));
4195
- console.log(chalk.gray('Opening browser for Google authentication...\n'));
4196
-
4197
- try {
4198
- await auth.login();
4199
- console.log(chalk.green('\n✓ Authentication successful!\n'));
4200
- // Continue to interactive mode after successful login
4201
- } catch (error) {
4202
- console.error(chalk.red('\n✗ Login failed:'), error.message);
4203
- if (error.message && error.message.includes('redirect_uri_mismatch')) {
4204
- console.log(chalk.yellow('\n⚠️ Troubleshooting:'));
4205
- console.log(chalk.gray('This error usually means the redirect URI is not whitelisted in Google Cloud Console.'));
4206
- console.log(chalk.gray('Ensure "https://<your-cognito-domain>/oauth2/idpresponse" is added to "Authorized redirect URIs".'));
4207
- }
4208
- process.exit(1);
4209
- }
4210
- }
4211
- } else {
4212
- // Auth disabled - show warning
4213
- console.log(chalk.yellow('\n⚠️ Authentication is currently disabled'));
4214
- console.log(chalk.gray('Set AUTH_ENABLED=true to enable authentication\n'));
4215
- }
4216
-
4217
- // Ensure Auto Mode is stopped when CLI starts
4218
- const { stopAutoMode } = require('./auto-mode');
4219
- await stopAutoMode('startup');
4220
-
4221
- await showWelcomeScreen();
4222
-
4223
- if (process.env.VCM_OPEN_PROVIDER_MENU === '1' || process.env.VCM_OPEN_PROVIDER_MENU === 'true') {
4224
- await showProviderManagerMenu();
4225
- return;
4226
- }
4227
-
4228
- let exit = false;
4229
- let lastSelectedIndex = 0; // Track last selected menu item
4230
- while (!exit) {
4231
- try {
4232
- const autoStatus = await checkAutoModeStatus();
4233
- const repoPath = process.cwd(); // Always use current working directory
4234
-
4235
- // Check if .vibecodingmachine exists (inside repo or as sibling)
4236
- const allnightStatus = await checkVibeCodingMachineExists();
4237
-
4238
- // Get current settings for display
4239
- const { getAutoConfig } = require('./config');
4240
- const autoConfig = await getAutoConfig();
4241
- const currentIDE = autoConfig.ide || 'claude-code';
4242
- const useHostname = await isComputerNameEnabled();
4243
-
4244
- // Build dynamic menu items - settings at top (gray, no letters), actions below (with letters)
4245
- const items = [];
4246
-
4247
- // Get first ENABLED agent from provider preferences
4248
- const { getProviderPreferences } = require('../utils/provider-registry');
4249
- const prefs = await getProviderPreferences();
4250
- let firstEnabledAgent = null;
4251
- for (const agentId of prefs.order) {
4252
- if (prefs.enabled[agentId] !== false) {
4253
- firstEnabledAgent = agentId;
4254
- break;
4255
- }
4256
- }
4257
- // Fallback to current agent if no enabled agents found
4258
- const displayAgent = firstEnabledAgent || autoConfig.agent || autoConfig.ide || 'ollama';
4259
- let agentDisplay = `${t('interactive.first.agent')}: ${chalk.cyan(getAgentDisplayName(displayAgent))}`;
4260
-
4261
- // Check for rate limits (for LLM-based agents and Claude Code)
4262
- if (displayAgent === 'ollama' || displayAgent === 'groq' || displayAgent === 'anthropic' || displayAgent === 'bedrock' || displayAgent === 'claude-code') {
4263
- try {
4264
- const ProviderManager = require('vibecodingmachine-core/src/ide-integration/provider-manager.cjs');
4265
- const providerManager = new ProviderManager();
4266
- const fs = require('fs');
4267
- const path = require('path');
4268
- const os = require('os');
4269
-
4270
- const configPath = path.join(os.homedir(), '.config', 'vibecodingmachine', 'config.json');
4271
-
4272
- if (fs.existsSync(configPath)) {
4273
- const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
4274
-
4275
- // Get the model based on the current agent type
4276
- let model;
4277
- if (displayAgent === 'groq') {
4278
- model = config.auto?.groqModel || config.auto?.aiderModel || config.auto?.llmModel;
4279
- // Remove groq/ prefix if present
4280
- if (model && model.includes('groq/')) {
4281
- model = model.split('/')[1];
4282
- }
4283
- } else if (displayAgent === 'anthropic') {
4284
- model = config.auto?.anthropicModel || config.auto?.aiderModel || config.auto?.llmModel;
4285
- } else if (displayAgent === 'ollama') {
4286
- const rawModel = config.auto?.llmModel || config.auto?.aiderModel;
4287
- // Only use if it doesn't have groq/ prefix
4288
- model = rawModel && !rawModel.includes('groq/') ? rawModel : null;
4289
- } else if (displayAgent === 'bedrock') {
4290
- model = 'anthropic.claude-sonnet-4-v1';
4291
- } else if (displayAgent === 'claude-code') {
4292
- model = 'claude-code-cli';
4293
- }
4294
-
4295
- // For Claude Code, use fixed model name
4296
- const checkModel = displayAgent === 'claude-code' ? 'claude-code-cli' : model;
4297
- const provider = displayAgent === 'ollama' ? 'ollama' : displayAgent;
4298
-
4299
- if (checkModel) {
4300
- const info = providerManager.getRateLimitInfo(provider, checkModel);
4301
-
4302
- if (info && info.isRateLimited && info.resetTime) {
4303
- const label = formatResetsAtLabel(info.resetTime);
4304
- if (label) {
4305
- agentDisplay += ` ${chalk.red('⏰ ' + label)}`;
4306
- }
4307
- }
4308
- }
4309
- }
4310
- } catch (err) {
4311
- // Silently ignore rate limit check errors
4312
- }
4313
- }
4314
-
4315
- // Settings at top (gray, left-justified, no letters)
4316
-
4317
-
4318
-
4319
- // Get stop condition from already loaded autoConfig (line 1704)
4320
- const stopCondition = autoConfig.neverStop ? t('interactive.never.stop') :
4321
- autoConfig.maxChats ? t('interactive.stop.after', { count: autoConfig.maxChats }) :
4322
- t('interactive.never.stop');
4323
-
4324
- if (autoStatus.running) {
4325
- items.push({
4326
- type: 'setting',
4327
- name: `Auto Mode: ${chalk.green('Running ✓')}`,
4328
- value: 'setting:auto-stop'
4329
- });
4330
- } else {
4331
- items.push({
4332
- type: 'setting',
4333
- name: `${t('interactive.auto.mode')}: ${chalk.yellow(t('interactive.auto.stopped') + ' ○')}`,
4334
- value: 'setting:auto-start'
4335
- });
4336
- }
4337
-
4338
- // Add separate stop condition setting
4339
- items.push({
4340
- type: 'setting',
4341
- name: ` └─ ${t('interactive.stop.condition')}: ${chalk.cyan(stopCondition)}`,
4342
- value: 'setting:auto-stop-condition'
4343
- });
4344
-
4345
- // Add current agent setting (rate limit info already included in agentDisplay if applicable)
4346
- items.push({
4347
- type: 'setting',
4348
- name: ` └─ ${agentDisplay}`,
4349
- value: 'setting:agent'
4350
- });
4351
-
4352
- // Add Requirements as a selectable setting with counts
4353
- const hasRequirements = await requirementsExists();
4354
- const { getComputerFilter } = require('./config');
4355
- const computerFilter = await getComputerFilter();
4356
- const counts = hasRequirements ? await countRequirements(computerFilter) : null;
4357
- let requirementsText = t('interactive.requirements') + ': ';
4358
- if (counts) {
4359
- // Use actual counts for display (not capped by maxChats)
4360
- const total = counts.todoCount + counts.toVerifyCount + counts.verifiedCount;
4361
- if (total > 0) {
4362
- const todoPercent = Math.round((counts.todoCount / total) * 100);
4363
- const toVerifyPercent = Math.round((counts.toVerifyCount / total) * 100);
4364
- const verifiedPercent = Math.round((counts.verifiedCount / total) * 100);
4365
- requirementsText += `${chalk.yellow(counts.todoCount + ' (' + todoPercent + '%) ' + t('interactive.todo'))}, ${chalk.cyan(counts.toVerifyCount + ' (' + toVerifyPercent + '%) ' + t('interactive.to.verify'))}, ${chalk.green(counts.verifiedCount + ' (' + verifiedPercent + '%) ' + t('interactive.verified'))}`;
4366
- } else {
4367
- requirementsText = '';
4368
- }
4369
- } else if (allnightStatus.exists) {
4370
- requirementsText += chalk.yellow('Not found');
4371
- } else {
4372
- requirementsText += chalk.yellow('Not initialized');
4373
- }
4374
-
4375
- if (requirementsText !== '') {
4376
- items.push({
4377
- type: 'setting',
4378
- name: requirementsText + ` ${chalk.blue('Sync Status: ' + (await getSyncStatus()))}`,
4379
- value: 'setting:requirements'
4380
- });
4381
- }
4382
-
4383
- // Add requirement filtering by computer
4384
- const filterText = computerFilter ? chalk.cyan(computerFilter) : chalk.gray('All Computers');
4385
- items.push({
4386
- type: 'setting',
4387
- name: ` └─ Filter Requirements by Computer: ${filterText}`,
4388
- value: 'setting:filter-requirements'
4389
- });
4390
-
4391
- items.push({
4392
- type: 'setting',
4393
- name: ` └─ ${t('interactive.hostname.enabled')}: ${useHostname ? chalk.green('✓') : chalk.red('🛑')} ${useHostname ? '✓ ENABLED' : '🛑 ' + t('interactive.hostname.disabled')}`,
4394
- value: 'setting:hostname'
4395
- });
4396
-
4397
- // Add Stages configuration
4398
- const { getStages } = require('./config');
4399
- const configuredStages = await getStages();
4400
- const stagesCount = configuredStages.length;
4401
- items.push({
4402
- type: 'setting',
4403
- name: ` └─ ${t('interactive.configure.stages')}: ${chalk.cyan(stagesCount + ' ' + t('interactive.stages'))}`,
4404
- value: 'setting:stages'
4405
- });
4406
-
4407
- items.push({
4408
- type: 'setting',
4409
- name: `Start Auto: Switch to Console tab and save updated agent list on exit (Left arrow to exit agent list will also save)`,
4410
- value: 'setting:start-auto'
4411
- });
4412
-
4413
- items.push({
4414
- type: 'setting',
4415
- name: ` └─ ${t('interactive.feedback')}: ${chalk.cyan('📣 Press Ctrl+F for feedback')}`,
4416
- value: 'setting:feedback'
4417
- });
4418
-
4419
- // TODO: Implement getNextTodoRequirement function
4420
- // if (counts && counts.todoCount > 0) {
4421
- // const { getNextTodoRequirement } = require('./auto-mode');
4422
- // const nextRequirement = await getNextTodoRequirement();
4423
- // if (nextRequirement) {
4424
- // const requirementPreview = nextRequirement.length > 80
4425
- // ? nextRequirement.substring(0, 77) + '...'
4426
- // : nextRequirement;
4427
- // items.push({
4428
- // type: 'info',
4429
- // name: ` └─ ${t('interactive.next.requirement')}: ${chalk.cyan(requirementPreview)}`,
4430
- // value: 'info:next-requirement'
4431
- // });
4432
- // }
4433
- // }
4434
- if (counts && counts.todoCount > 0) {
4435
- // Get the actual next requirement text (new header format)
4436
- let nextReqText = '...';
4437
- let count = 0;
4438
- try {
4439
- const hostname = await getHostname();
4440
- const reqFilename = await getRequirementsFilename(hostname);
4441
- const reqPath = path.join(repoPath, '.vibecodingmachine', reqFilename);
4442
- // Increase the delay to prevent flashing and repaint
4443
- await new Promise(resolve => setTimeout(resolve, 1000));
4444
-
4445
-
4446
- if (await fs.pathExists(reqPath)) {
4447
- const reqContent = await fs.readFile(reqPath, 'utf8');
4448
- const lines = reqContent.split('\n');
4449
- let inTodoSection = false;
4450
-
4451
- // Find first non-empty requirement in TODO section
4452
- for (let i = 0; i < lines.length; i++) {
4453
- const line = lines[i].trim();
4454
-
4455
- // Check if we're in the TODO section (same logic as auto-direct.js)
4456
- if (line.includes('## ⏳ Requirements not yet completed') ||
4457
- line.includes('Requirements not yet completed')) {
4458
- inTodoSection = true;
4459
- continue;
4460
- }
4461
-
4462
- // If we hit another section header, stop looking
4463
- if (inTodoSection && line.startsWith('##') && !line.startsWith('###')) {
4464
- break;
4465
- }
4466
-
4467
- // If we're in TODO section and find a requirement header (###)
4468
- if (inTodoSection && line.startsWith('###')) {
4469
- const title = line.replace(/^###\s*/, '').trim();
4470
- // Skip empty titles
4471
- if (title && title.length > 0) {
4472
- count++;
4473
- let description = '';
4474
-
4475
- // Read subsequent lines for description
4476
- for (let j = i + 1; j < lines.length; j++) {
4477
- const nextLine = lines[j].trim();
4478
- // Stop if we hit another requirement or section
4479
- if (nextLine.startsWith('###') || (nextLine.startsWith('##') && !nextLine.startsWith('###'))) {
4480
- break;
4481
- }
4482
-
4483
- description += nextLine + '\n';
4484
- }
4485
- // Translate requirement labels
4486
- let displayText = title + '\n' + description;
4487
- displayText = displayText.replace(/PACKAGE:/g, t('requirement.label.package'));
4488
- displayText = displayText.replace(/STATUS:/g, t('requirement.label.status'));
4489
- nextReqText = displayText;
4490
- break;
4491
- }
4492
- }
4493
- }
4494
- items.push({
4495
- type: 'info',
4496
- name: ` └─ ${t('interactive.next.todo')}: ${nextReqText}`,
4497
- value: 'info:next-requirement'
4498
- });
4499
- }
4500
- } catch (err) {
4501
- console.error('Error reading requirements file:', err.message);
4502
- }
4503
- }
4504
-
4505
-
4506
- // Add warning message if no TODO requirements and Auto Mode is stopped
4507
- if (counts && counts.todoCount === 0 && !autoStatus.running) {
4508
- items.push({
4509
- type: 'info',
4510
- name: chalk.red(' ⚠️ No requirements to work on - cannot start Auto Mode'),
4511
- value: 'info:no-requirements'
4512
- });
4513
- }
4514
-
4515
- // Blank separator line
4516
- items.push({ type: 'blank', name: '', value: 'blank' });
4517
-
4518
- // Action items (with letters)
4519
- // Only show Initialize option if neither directory exists
4520
- if (!allnightStatus.exists) {
4521
- items.push({ type: 'action', name: t('interactive.initialize'), value: 'repo:init' });
4522
- }
4523
-
4524
- items.push({ type: 'action', name: t('interactive.view.computers'), value: 'computers:list' });
4525
- items.push({ type: 'action', name: t('interactive.sync.now'), value: 'sync:now' });
4526
- items.push({ type: 'action', name: t('interactive.logout'), value: 'logout' });
4527
- items.push({ type: 'action', name: t('interactive.exit'), value: 'exit' });
4528
-
4529
-
4530
- // Use custom quick menu with last selected index
4531
- const result = await showQuickMenu(items, lastSelectedIndex);
4532
- const action = result.value;
4533
- lastSelectedIndex = result.selectedIndex;
4534
-
4535
- // Handle cancel (ESC key)
4536
- if (action === '__cancel__') {
4537
- // Just refresh and continue
4538
- continue;
4539
- }
4540
-
4541
- // Debug: Log the action being processed
4542
- if (process.env.DEBUG) {
4543
- console.log(chalk.gray(`[DEBUG] Processing action: ${action}`));
4544
- }
4545
-
4546
- // Ensure action is a string (safety check)
4547
- const actionStr = String(action || '').trim();
4548
-
4549
- // Log action for debugging (always show, not just in DEBUG mode)
4550
- console.log(chalk.yellow(`\n[DEBUG] Action received: "${actionStr}" (type: ${typeof action})\n`));
4551
-
4552
- switch (actionStr) {
4553
- case 'setting:agent': {
4554
- if (process.env.VCM_DEBUG_MENU === '1') {
4555
- const msg = `[DEBUG-MENU ${new Date().toISOString()}] Main menu handling 'setting:agent' - invoking showProviderManagerMenu()\n` + new Error().stack.split('\n').slice(1, 6).join('\n') + '\n';
4556
- console.log(msg);
4557
- try { require('fs').appendFileSync('/tmp/vcm-menu-debug.log', msg); } catch (e) { }
4558
- }
4559
- await showProviderManagerMenu();
4560
- await showWelcomeScreen();
4561
- break;
4562
- }
4563
- case 'setting:feedback': {
4564
- await handleFeedbackSubmission();
4565
- await showWelcomeScreen();
4566
- break;
4567
- }
4568
- case 'setting:provider': {
4569
- // Switch AI provider - run provider setup only, don't start auto mode
4570
- // Note: Continue CLI doesn't have provider setup - models are configured in ~/.continue/config.yaml
4571
- if (currentIDE === 'continue') {
4572
- console.log(chalk.cyan('\n📝 Continue CLI Models\n'));
4573
-
4574
- // Read Continue CLI config to show current models
4575
- try {
4576
- const fs = require('fs');
4577
- const path = require('path');
4578
- const os = require('os');
4579
- const yaml = require('js-yaml');
4580
- const configPath = path.join(os.homedir(), '.continue', 'config.yaml');
4581
-
4582
- if (fs.existsSync(configPath)) {
4583
- const config = yaml.load(fs.readFileSync(configPath, 'utf8'));
4584
- const models = config.models || [];
4585
-
4586
- if (models.length > 0) {
4587
- // Check which models are downloaded
4588
- const { execSync } = require('child_process');
4589
- let installedNames = [];
4590
- try {
4591
- const installedModels = execSync('curl -s http://localhost:11434/api/tags', { encoding: 'utf8' });
4592
- const modelsData = JSON.parse(installedModels);
4593
- installedNames = modelsData.models.map(m => m.name.replace(':latest', ''));
4594
- } catch (error) {
4595
- console.log(chalk.yellow(' ⚠️ Could not check Ollama (is it running?)\n'));
4596
- }
4597
-
4598
- // Build choices with status indicators
4599
- const choices = models.map((model, idx) => {
4600
- const modelName = model.model;
4601
- const displayName = model.name || modelName;
4602
- const isInstalled = installedNames.some(name =>
4603
- name === modelName || name === modelName.replace(':latest', '')
4604
- );
4605
- const statusIcon = isInstalled ? chalk.green('✓') : chalk.yellow('⚠');
4606
- const statusText = isInstalled ? chalk.gray('(installed)') : chalk.yellow('(needs download)');
4607
-
4608
- return {
4609
- name: `${statusIcon} ${displayName} ${statusText}`,
4610
- value: { model, isInstalled, index: idx }
4611
- };
4612
- });
4613
-
4614
- choices.push({ name: chalk.gray('← Back to menu'), value: null });
4615
-
4616
- const inquirer = require('inquirer');
4617
- const { selection } = await inquirer.prompt([
4618
- {
4619
- type: 'list',
4620
- name: 'selection',
4621
- message: 'Select model to use:',
4622
- choices: choices
4623
- }
4624
- ]);
4625
-
4626
- if (selection) {
4627
- const { model, isInstalled, index } = selection;
4628
- const modelName = model.model;
4629
-
4630
- // If not installed, download it first
4631
- if (!isInstalled) {
4632
- console.log(chalk.cyan(`\n📥 Downloading ${modelName}...\n`));
4633
- console.log(chalk.gray('This may take a few minutes depending on model size...\n'));
4634
-
4635
- // Use Ollama HTTP API for model download
4636
- const https = require('http');
4637
- const downloadRequest = https.request({
4638
- hostname: 'localhost',
4639
- port: 11434,
4640
- path: '/api/pull',
4641
- method: 'POST',
4642
- headers: {
4643
- 'Content-Type': 'application/json',
4644
- }
4645
- }, (res) => {
4646
- let lastStatus = '';
4647
- res.on('data', (chunk) => {
4648
- try {
4649
- const lines = chunk.toString().split('\n').filter(l => l.trim());
4650
- lines.forEach(line => {
4651
- const data = JSON.parse(line);
4652
- if (data.status) {
4653
- if (data.total && data.completed) {
4654
- // Show progress bar with percentage
4655
- const percent = Math.round((data.completed / data.total) * 100);
4656
- const completedMB = Math.round(data.completed / 1024 / 1024);
4657
- const totalMB = Math.round(data.total / 1024 / 1024);
4658
- const remainingMB = totalMB - completedMB;
4659
-
4660
- // Create progress bar (50 chars wide)
4661
- const barWidth = 50;
4662
- const filledWidth = Math.round((percent / 100) * barWidth);
4663
- const emptyWidth = barWidth - filledWidth;
4664
- const bar = chalk.green('█'.repeat(filledWidth)) + chalk.gray('░'.repeat(emptyWidth));
4665
-
4666
- process.stdout.write(`\r${bar} ${chalk.cyan(percent + '%')} ${chalk.white(completedMB + 'MB')} / ${chalk.gray(totalMB + 'MB')} ${chalk.yellow('(' + remainingMB + 'MB remaining)')}`);
4667
- } else if (data.status !== lastStatus) {
4668
- lastStatus = data.status;
4669
- process.stdout.write(`\r${chalk.gray(data.status)}`.padEnd(120));
4670
- }
4671
- }
4672
- });
4673
- } catch (e) {
4674
- // Ignore JSON parse errors for streaming responses
4675
- }
4676
- });
4677
- });
4678
-
4679
- downloadRequest.on('error', (error) => {
4680
- console.log(chalk.red(`\n\n✗ Download failed: ${error.message}\n`));
4681
- });
4682
-
4683
- downloadRequest.write(JSON.stringify({ name: modelName }));
4684
- downloadRequest.end();
4685
-
4686
- await new Promise((resolve) => {
4687
- downloadRequest.on('close', () => {
4688
- console.log(chalk.green(`\n\n✓ Successfully downloaded ${modelName}\n`));
4689
- resolve();
4690
- });
4691
- });
4692
- }
4693
-
4694
- // Move selected model to the top of the list
4695
- const updatedModels = [...models];
4696
- const [selectedModel] = updatedModels.splice(index, 1);
4697
- updatedModels.unshift(selectedModel);
4698
-
4699
- // Update config file
4700
- config.models = updatedModels;
4701
- fs.writeFileSync(configPath, yaml.dump(config), 'utf8');
4702
-
4703
- console.log(chalk.green(`✓ Set ${model.name || modelName} as default model\n`));
4704
- console.log(chalk.gray(t('interactive.press.enter.continue')));
4705
- await inquirer.prompt([
4706
- {
4707
- type: 'input',
4708
- name: 'continue',
4709
- message: ''
4710
- }
4711
- ]);
4712
- }
4713
- } else {
4714
- console.log(chalk.yellow(' ⚠️ No models configured in config.yaml\n'));
4715
- console.log(chalk.gray(' Config file:'), chalk.cyan(configPath), '\n');
4716
-
4717
- const inquirer = require('inquirer');
4718
- await inquirer.prompt([
4719
- {
4720
- type: 'input',
4721
- name: 'continue',
4722
- message: t('interactive.press.enter.return'),
4723
- }
4724
- ]);
4725
- }
4726
- } else {
4727
- console.log(chalk.yellow(' ⚠️ Config file not found\n'));
4728
- console.log(chalk.gray(' Expected location:'), chalk.cyan(configPath), '\n');
4729
-
4730
- const inquirer = require('inquirer');
4731
- await inquirer.prompt([
4732
- {
4733
- type: 'input',
4734
- name: 'continue',
4735
- message: t('interactive.press.enter.return'),
4736
- }
4737
- ]);
4738
- }
4739
- } catch (error) {
4740
- console.log(chalk.red(` ✗ Error: ${error.message}\n`));
4741
-
4742
- const inquirer = require('inquirer');
4743
- await inquirer.prompt([
4744
- {
4745
- type: 'input',
4746
- name: 'continue',
4747
- message: t('interactive.press.enter.return'),
4748
- }
4749
- ]);
4750
- }
4751
-
4752
- await showWelcomeScreen();
4753
- } else {
4754
- console.log(chalk.cyan('\n🔄 Switching AI Provider...\n'));
4755
- await auto.start({ ide: currentIDE, forceProviderSetup: true, configureOnly: true });
4756
- await showWelcomeScreen();
4757
- }
4758
- break;
4759
- }
4760
- case 'setting:hostname': {
4761
- // Toggle hostname setting
4762
- const { setConfigValue } = require('vibecodingmachine-core');
4763
- const newValue = !useHostname;
4764
- await setConfigValue('computerNameEnabled', newValue);
4765
- const statusText = newValue ? chalk.green('enabled') : chalk.yellow('disabled');
4766
- console.log(chalk.green('\n✓'), `Hostname in requirements file ${statusText}\n`);
4767
- await showWelcomeScreen();
4768
- break;
4769
- }
4770
- case 'setting:filter-requirements': {
4771
- // Configure requirement filtering by computer
4772
- const { getComputerFilter, setComputerFilter } = require('./config');
4773
- const inquirer = require('inquirer');
4774
- const repoPath = await getRepoPath();
4775
-
4776
- if (!repoPath) {
4777
- console.log(chalk.yellow('No repository configured. Use repo:init or repo:set first.'));
4778
- await showWelcomeScreen();
4779
- break;
4780
- }
4781
-
4782
- // Find all REQUIREMENTS-*.md files to get available computers
4783
- const vibeDir = path.join(repoPath, '.vibecodingmachine');
4784
- const files = await fs.pathExists(vibeDir) ? await fs.readdir(vibeDir) : [];
4785
- const reqFiles = files.filter(f => f.startsWith('REQUIREMENTS') && f.endsWith('.md'));
4786
-
4787
- const computers = [];
4788
- for (const file of reqFiles) {
4789
- const filePath = path.join(vibeDir, file);
4790
- const content = await fs.readFile(filePath, 'utf8');
4791
- const hostnameMatch = content.match(/- \*\*Hostname\*\*:\s*(.+)/);
4792
- if (hostnameMatch) {
4793
- computers.push(hostnameMatch[1].trim());
4794
- }
4795
- }
4796
-
4797
- if (computers.length === 0) {
4798
- console.log(chalk.yellow('No computers found in requirements files.'));
4799
- await showWelcomeScreen();
4800
- break;
4801
- }
4802
-
4803
- const currentFilter = await getComputerFilter();
4804
-
4805
- console.log(chalk.cyan(`\n🖥️ ${t('interactive.filter.requirements.by.computer')}\n`));
4806
- console.log(chalk.gray('Select which computer\'s requirements to show, or "All Computers" to show all.\n'));
4807
-
4808
- const { selectedComputer } = await inquirer.prompt([
4809
- {
4810
- type: 'list',
4811
- name: 'selectedComputer',
4812
- message: t('interactive.select.computer'),
4813
- choices: [
4814
- { name: chalk.gray('All Computers'), value: null },
4815
- ...computers.map(c => ({
4816
- name: c === currentFilter ? chalk.cyan(`${c} (current)`) : c,
4817
- value: c
4818
- }))
4819
- ],
4820
- default: currentFilter || null,
4821
- loop: false,
4822
- pageSize: 15
4823
- }
4824
- ]);
4825
-
4826
- await setComputerFilter(selectedComputer);
4827
-
4828
- const filterText = selectedComputer ? chalk.cyan(selectedComputer) : chalk.gray('All Computers');
4829
- console.log(chalk.green('\n✓'), `Requirements filter set to: ${filterText}\n`);
4830
- await showWelcomeScreen();
4831
- break;
4832
- }
4833
- case 'setting:stages': {
4834
- // Configure stages
4835
- const { getStages, setStages, DEFAULT_STAGES } = require('./config');
4836
- const inquirer = require('inquirer');
4837
-
4838
- // Override inquirer's checkbox help text for current locale
4839
- const CheckboxPrompt = require('inquirer/lib/prompts/checkbox');
4840
- const originalGetHelpText = CheckboxPrompt.prototype.getHelp || function () {
4841
- return '(Press <space> to select, <a> to toggle all, <i> to invert selection, and <enter> to proceed)';
4842
- };
4843
-
4844
- CheckboxPrompt.prototype.getHelp = function () {
4845
- const locale = detectLocale();
4846
- if (locale === 'es') {
4847
- return '(Presiona <espacio> para seleccionar, <a> para alternar todo, <i> para invertir selección, y <enter> para proceder)';
4848
- }
4849
- return originalGetHelpText.call(this);
4850
- };
4851
-
4852
- const currentStages = await getStages();
4853
-
4854
- console.log(chalk.cyan(`\n🔨 ${t('workflow.config.title')}\n`));
4855
- console.log(chalk.gray(t('workflow.config.description')));
4856
- console.log(chalk.gray(t('workflow.config.order.note')));
4857
- console.log(chalk.gray(`${t('workflow.config.instructions')}\n`));
4858
-
4859
- const { selectedStages } = await inquirer.prompt([
4860
- {
4861
- type: 'checkbox',
4862
- name: 'selectedStages',
4863
- message: t('workflow.config.select.prompt'),
4864
- choices: DEFAULT_STAGES.map(stage => ({
4865
- name: translateStage(stage),
4866
- value: stage, // Keep original English value for internal use
4867
- checked: currentStages.includes(stage)
4868
- })),
4869
- validate: (answer) => {
4870
- if (answer.length < 1) {
4871
- return t('workflow.config.validation.error');
4872
- }
4873
- return true;
4874
- },
4875
- loop: false,
4876
- pageSize: 15
4877
- }
4878
- ]);
4879
-
4880
- // Restore original getHelp method
4881
- CheckboxPrompt.prototype.getHelp = originalGetHelpText;
4882
-
4883
- // Preserve order from DEFAULT_STAGES for selected items
4884
- // This ensures stages always run in the correct logical order
4885
- const newStages = DEFAULT_STAGES.filter(stage => selectedStages.includes(stage));
4886
-
4887
- await setStages(newStages);
4888
- const translatedStages = newStages.map(stage => translateStage(stage));
4889
- console.log(chalk.green('\n✓'), `${t('workflow.config.updated')} ${translatedStages.join(' → ')}\n`);
4890
-
4891
- const { continue: _ } = await inquirer.prompt([{
4892
- type: 'input',
4893
- name: 'continue',
4894
- message: t('interactive.press.enter.return')
4895
- }]);
4896
-
4897
- await showWelcomeScreen();
4898
- break;
4899
- }
4900
- case 'setting:auto-start': {
4901
- try {
4902
- console.log(chalk.bold.cyan('\n' + t('auto.starting') + '\n'));
4903
- // Check if there are requirements to work on
4904
- const hasRequirements = await requirementsExists();
4905
- const counts = hasRequirements ? await countRequirements() : null;
4906
-
4907
- if (!counts || counts.todoCount === 0) {
4908
- console.log(chalk.red('\n⚠️ Cannot start Auto Mode: No requirements to work on'));
4909
- console.log(chalk.gray(' Add requirements first using "Requirements" menu option\n'));
4910
- const inquirer = require('inquirer');
4911
- await inquirer.prompt([{
4912
- type: 'input',
4913
- name: 'continue',
4914
- message: t('interactive.press.enter.return'),
4915
- }]);
4916
- await showWelcomeScreen();
4917
- break;
4918
- }
4919
-
4920
- // Start auto mode - use saved config settings
4921
- try {
4922
- // Get current config
4923
- const { getAutoConfig } = require('./config');
4924
- const currentConfig = await getAutoConfig();
4925
-
4926
- // Get first enabled agent from provider preferences (this is what user expects)
4927
- const { getProviderPreferences } = require('../utils/provider-registry');
4928
- const prefs = await getProviderPreferences();
4929
- console.log(chalk.gray('[DEBUG] Provider preferences:'), JSON.stringify(prefs, null, 2));
4930
- let firstEnabledAgent = null;
4931
- for (const agentId of prefs.order) {
4932
- if (prefs.enabled[agentId] !== false) {
4933
- firstEnabledAgent = agentId;
4934
- console.log(chalk.gray('[DEBUG] Found first enabled agent:'), firstEnabledAgent);
4935
- break;
4936
- }
4937
- }
4938
- const agentToUse = firstEnabledAgent || currentIDE;
4939
- console.log(chalk.gray('[DEBUG] Agent to use:'), agentToUse, '(firstEnabled:', firstEnabledAgent, ', fallback:', currentIDE, ')');
4940
-
4941
- // Use saved maxChats/neverStop settings
4942
- const options = { ide: agentToUse };
4943
-
4944
- // Set extension for VS Code extensions from saved config
4945
- if (agentToUse === 'amazon-q' || agentToUse === 'github-copilot') {
4946
- options.extension = agentToUse;
4947
- }
4948
-
4949
- if (currentConfig.neverStop) {
4950
- options.neverStop = true;
4951
- } else if (currentConfig.maxChats) {
4952
- options.maxChats = currentConfig.maxChats;
4953
- } else {
4954
- // Default to never stop if not configured
4955
- options.neverStop = true;
4956
- }
4957
- console.log(chalk.gray(`\n[DEBUG] Calling auto.start with options:`, JSON.stringify(options, null, 2)));
4958
- console.log(chalk.gray('[DEBUG] Step 1: Starting auto mode...'));
4959
- try {
4960
- // Simple approach - just call auto.start(), no blessed UI
4961
- // Ensure stdin is NOT in raw mode before starting auto mode
4962
- // (otherwise Ctrl+C won't work)
4963
- if (process.stdin.isTTY && process.stdin.setRawMode) {
4964
- process.stdin.setRawMode(false);
4965
- }
4966
-
4967
- // ALWAYS use auto:direct (supports both LLM and IDE agents with proper looping)
4968
- console.log(chalk.gray('[DEBUG] Using auto:direct for agent:', agentToUse));
4969
- const { handleAutoStart: handleDirectAutoStart } = require('../commands/auto-direct');
4970
- await handleDirectAutoStart(options);
4971
-
4972
- // Prompt user before returning to menu (so they can read the output)
4973
- console.log('');
4974
- const inquirer = require('inquirer');
4975
- await inquirer.prompt([{
4976
- type: 'input',
4977
- name: 'continue',
4978
- message: t('interactive.press.enter.return'),
4979
- }]);
4980
-
4981
- await showWelcomeScreen();
4982
- break;
4983
-
4984
- } catch (error) {
4985
- // Check if it's a cancellation (ESC) or actual error
4986
- if (error.message && error.message.includes('User force closed')) {
4987
- console.log(chalk.yellow('\nCancelled\n'));
4988
- } else {
4989
- console.log(chalk.red(`\n✗ Error starting Auto Mode: ${error.message}`));
4990
- // Always show stack trace for debugging
4991
- if (error.stack) {
4992
- console.log(chalk.gray('\nStack trace:'));
4993
- console.log(chalk.gray(error.stack.split('\n').slice(0, 10).join('\n')));
4994
- }
4995
- // Give user time to read the error
4996
- console.log(chalk.yellow('\nReturning to menu in 5 seconds...'));
4997
- await new Promise(resolve => setTimeout(resolve, 5000));
4998
- }
4999
- await showWelcomeScreen();
5000
- }
5001
- } catch (error) {
5002
- // Catch any errors in the inner try block (line 2066)
5003
- console.log(chalk.red(`\n✗ Error in auto mode setup: ${error.message}`));
5004
- if (error.stack) {
5005
- console.log(chalk.gray(error.stack));
5006
- }
5007
- const inquirer = require('inquirer');
5008
- await inquirer.prompt([{
5009
- type: 'input',
5010
- name: 'continue',
5011
- message: t('interactive.press.enter.return'),
5012
- }]);
5013
- await showWelcomeScreen();
5014
- }
5015
- } catch (error) {
5016
- // Catch any errors in the outer try block
5017
- console.log(chalk.red(`\n✗ Unexpected error: ${error.message}`));
5018
- if (error.stack) {
5019
- console.log(chalk.gray(error.stack));
5020
- }
5021
- const inquirer = require('inquirer');
5022
- await inquirer.prompt([{
5023
- type: 'input',
5024
- name: 'continue',
5025
- message: t('interactive.press.enter.return'),
5026
- }]);
5027
- await showWelcomeScreen();
5028
- }
5029
- break;
5030
- }
5031
- case 'setting:no-requirements': {
5032
- // User clicked on the warning message - show helpful info
5033
- console.log(chalk.red('\n⚠️ No requirements to work on'));
5034
- console.log(chalk.gray('\nTo start Auto Mode, you need at least one requirement in the "Requirements not yet completed" section.'));
5035
- console.log(chalk.gray('\nYou can:'));
5036
- console.log(chalk.cyan(' 1. Add requirements using the "Requirements" menu option'));
5037
- console.log(chalk.cyan(' 2. Or wait for requirements to be added to your REQUIREMENTS file\n'));
5038
- const inquirer = require('inquirer');
5039
- await inquirer.prompt([{
5040
- type: 'input',
5041
- name: 'continue',
5042
- message: t('interactive.press.enter.return'),
5043
- }]);
5044
- await showWelcomeScreen();
5045
- break;
5046
- }
5047
- case 'setting:auto-stop':
5048
- // Stop auto mode
5049
- await auto.stop();
5050
- await showWelcomeScreen();
5051
- break;
5052
- case 'setting:auto-stop-condition': {
5053
- // Modify stop condition
5054
- try {
5055
- const inquirer = require('inquirer');
5056
-
5057
- // Get current config
5058
- const { getAutoConfig, setAutoConfig } = require('./config');
5059
- const currentConfig = await getAutoConfig();
5060
-
5061
- // Determine default value for prompt
5062
- let defaultMaxChats = '';
5063
- if (currentConfig.neverStop) {
5064
- defaultMaxChats = '';
5065
- } else if (currentConfig.maxChats) {
5066
- defaultMaxChats = String(currentConfig.maxChats);
5067
- }
5068
-
5069
- console.log(chalk.bold.cyan('\n' + t('config.stop.condition.title') + '\n'));
5070
-
5071
- const { maxChats } = await inquirer.prompt([{
5072
- type: 'input',
5073
- name: 'maxChats',
5074
- message: t('config.max.chats.prompt'),
5075
- default: defaultMaxChats
5076
- }]);
5077
-
5078
- // Update config
5079
- const newConfig = { ...currentConfig };
5080
- if (maxChats && maxChats.trim() !== '' && maxChats.trim() !== '0') {
5081
- newConfig.maxChats = parseInt(maxChats);
5082
- newConfig.neverStop = false;
5083
- console.log(chalk.green('\n✓'), `${t('config.stop.condition.updated')} ${chalk.cyan(t('config.stop.after', { count: newConfig.maxChats }))}\n`);
5084
- } else {
5085
- delete newConfig.maxChats;
5086
- newConfig.neverStop = true;
5087
- console.log(chalk.green('\n✓'), `${t('config.stop.condition.updated')} ${chalk.cyan(t('config.never.stop'))}\n`);
5088
- }
5089
-
5090
- await setAutoConfig(newConfig);
5091
- } catch (error) {
5092
- console.log(chalk.red('\n✗ Error updating stop condition:', error.message));
5093
- }
5094
- await showWelcomeScreen();
5095
- break;
5096
- }
5097
- case 'setting:requirements': {
5098
- // Show tree-style requirements navigator
5099
- await showRequirementsTree();
5100
- await showWelcomeScreen();
5101
- break;
5102
- }
5103
- case 'repo:init':
5104
- await repo.initRepo();
5105
- break;
5106
-
5107
- case 'computers:list': {
5108
- const computerCommands = require('../commands/computers');
5109
- await computerCommands.listComputers();
5110
- console.log(chalk.gray(`\n${t('interactive.press.enter.continue')}`));
5111
- await new Promise(resolve => {
5112
- const rl = readline.createInterface({
5113
- input: process.stdin,
5114
- output: process.stdout
5115
- });
5116
- rl.question('', () => {
5117
- rl.close();
5118
- resolve();
5119
- });
5120
- });
5121
- await showWelcomeScreen();
5122
- break;
5123
- }
5124
-
5125
- case 'sync:now': {
5126
- const syncCommands = require('../commands/sync');
5127
- await syncCommands.syncNow();
5128
- console.log(chalk.gray(`\n${t('interactive.press.enter.continue')}`));
5129
- await new Promise(resolve => {
5130
- const rl = readline.createInterface({
5131
- input: process.stdin,
5132
- output: process.stdout
5133
- });
5134
- rl.question('', () => {
5135
- rl.close();
5136
- resolve();
5137
- });
5138
- });
5139
- await showWelcomeScreen();
5140
- break;
5141
- }
5142
-
5143
- case 'setting:cloud-sync': {
5144
- await showCloudSyncMenu();
5145
- await showWelcomeScreen();
5146
- break;
5147
- }
5148
-
5149
- case 'setting:cloud-sync-setup': {
5150
- console.clear();
5151
- console.log(chalk.bold.cyan('\n☁️ Cloud Sync Setup\n'));
5152
- console.log(chalk.yellow('Cloud sync is not configured yet.\n'));
5153
- console.log(chalk.white('To set up cloud sync:\n'));
5154
- console.log(chalk.gray('1. Run: ') + chalk.cyan('./scripts/setup-cloud-sync.sh'));
5155
- console.log(chalk.gray('2. Add AWS configuration to your .env file'));
5156
- console.log(chalk.gray('3. Register this computer with: ') + chalk.cyan('vcm computer:register "<focus>"'));
5157
- console.log(chalk.gray('\nFor more info, see: ') + chalk.cyan('docs/CLOUD_SYNC.md\n'));
5158
-
5159
- console.log(chalk.gray(t('interactive.press.enter.continue')));
5160
- await new Promise(resolve => {
5161
- const rl = readline.createInterface({
5162
- input: process.stdin,
5163
- output: process.stdout
5164
- });
5165
- rl.question('', () => {
5166
- rl.close();
5167
- resolve();
5168
- });
5169
- });
5170
- await showWelcomeScreen();
5171
- break;
5172
- }
5173
- case 'auto:start': {
5174
- const { ide, maxChats } = await inquirer.prompt([
5175
- {
5176
- type: 'list',
5177
- name: 'ide',
5178
- message: 'Select IDE:',
5179
- choices: [
5180
- { name: 'Claude Code CLI (recommended - Anthropic Claude)', value: 'claude-code' },
5181
- { name: 'Aider CLI (best for Ollama & Bedrock)', value: 'aider' },
5182
- { name: 'Continue CLI (Ollama support, but --auto mode doesn\'t execute code)', value: 'continue' },
5183
- { name: 'Cline CLI (local AI alternative, but has Ollama connection issues)', value: 'cline' },
5184
- { name: 'Cursor', value: 'cursor' },
5185
- { name: 'VS Code (with Amazon Q Developer)', value: 'amazon-q' },
5186
- { name: 'VS Code (with GitHub Copilot)', value: 'github-copilot' },
5187
- { name: 'VS Code (plain)', value: 'vscode' },
5188
- { name: 'Windsurf', value: 'windsurf' },
5189
- { name: 'Google Antigravity', value: 'antigravity' }
5190
- ],
5191
- default: 'claude-code'
5192
- },
5193
- {
5194
- type: 'input',
5195
- name: 'maxChats',
5196
- message: t('config.max.chats.prompt'),
5197
- default: ''
5198
- }
5199
- ]);
5200
-
5201
- let ideModel = null;
5202
- if (ide === 'windsurf' || ide === 'antigravity') {
5203
- const { selectedModel } = await inquirer.prompt([
5204
- {
5205
- type: 'list',
5206
- name: 'selectedModel',
5207
- message: `Select ${ide === 'windsurf' ? 'Windsurf' : 'Antigravity'} agent/model:`,
5208
- choices: (ide === 'windsurf' ? [
5209
- { name: 'Default', value: 'default' },
5210
- { name: 'SWE-1-lite', value: 'swe-1-lite' }
5211
- ] : [
5212
- { name: 'Default', value: 'antigravity' },
5213
- { name: 'Gemini 3 Pro (Low)', value: 'Gemini 3 Pro (Low)' },
5214
- { name: 'Claude Sonnet 4.5', value: 'Claude Sonnet 4.5' },
5215
- { name: 'Claude Sonnet 4.5 (Thinking)', value: 'Claude Sonnet 4.5 (Thinking)' },
5216
- { name: 'GPT-OSS 120B (Medium)', value: 'GPT-OSS 120B (Medium)' }
5217
- ]),
5218
- default: 'default'
5219
- }
5220
- ]);
5221
- ideModel = selectedModel;
5222
- }
5223
-
5224
- const options = { ide };
5225
- if (ideModel) {
5226
- options.ideModel = ideModel;
5227
- }
5228
-
5229
- // Set extension for VS Code extensions
5230
- if (ide === 'amazon-q') {
5231
- options.extension = 'amazon-q';
5232
- } else if (ide === 'github-copilot') {
5233
- options.extension = 'github-copilot';
5234
- }
5235
-
5236
- if (maxChats && maxChats.trim() !== '0') {
5237
- options.maxChats = parseInt(maxChats);
5238
- } else {
5239
- options.neverStop = true;
5240
- }
5241
-
5242
- // Use blessed UI for persistent header and status card
5243
- console.log('[DEBUG] Attempting to load blessed UI...');
5244
- try {
5245
- console.log('[DEBUG] Requiring simple UI components...');
5246
- const { createAutoModeUI } = require('./auto-mode-simple-ui');
5247
- const { StdoutInterceptor } = require('./stdout-interceptor');
5248
- const { getRepoPath } = require('./config');
5249
- const { getRequirementsPath } = require('vibecodingmachine-core');
5250
-
5251
- console.log('[DEBUG] Getting repo info...');
5252
- // Get current repo info for the header
5253
- const repoPath = await getRepoPath();
5254
- const hostname = getHostname();
5255
- console.log('[DEBUG] Repo:', repoPath, 'Hostname:', hostname);
5256
-
5257
- // Get current git branch if in a git repo
5258
- let currentBranch = '';
5259
- if (repoPath) {
5260
- try {
5261
- const { execSync } = require('child_process');
5262
- currentBranch = execSync('git branch --show-current', {
5263
- cwd: repoPath,
5264
- encoding: 'utf8'
5265
- }).trim();
5266
- } catch (err) {
5267
- // Not a git repo or git not available
5268
- currentBranch = '';
5269
- }
5270
- }
5271
-
5272
- // Build menu content for header
5273
- const menuContent = `╭───────────────────────────────────────────────────────╮
5274
- │ │
5275
- │ VibeCodingMachine │
5276
- │ Auto Mode Running - Press Ctrl+C to stop │
5277
- │ │
5278
- ╰───────────────────────────────────────────────────────╯
5279
-
5280
- ${t('system.repo').padEnd(25)} ${repoPath || 'Not set'}
5281
- ${currentBranch ? t('system.git.branch').padEnd(25) + ' ' + currentBranch + '\n' : ''}${t('system.computer.name').padEnd(25)} ${hostname}
5282
- Current IDE: ${formatIDEName(ide)}
5283
- AI Provider: ${getCurrentAIProvider(ide) || 'N/A'}
5284
- Max Chats: ${maxChats || 'Never stop'}`;
5285
-
5286
- // Create blessed UI
5287
- const ui = createAutoModeUI({
5288
- menuContent,
5289
- onExit: async () => {
5290
- // Stop auto mode when user presses Ctrl+C or Q
5291
- interceptor.stop();
5292
- await auto.stop();
5293
- process.exit(0);
5294
- }
5295
- });
5296
-
5297
- // Create stdout interceptor to capture console output
5298
- const interceptor = new StdoutInterceptor();
5299
- interceptor.addHandler((output) => {
5300
- // Route console output to the blessed UI log
5301
- ui.appendOutput(output);
5302
- });
5303
- interceptor.start(false); // Don't pass through to original stdout
5304
-
5305
- // Monitor requirements file for status updates
5306
- const reqPath = await getRequirementsPath(repoPath);
5307
- const fs = require('fs-extra');
5308
- const chokidar = require('chokidar');
5309
-
5310
- // Initial status - auto mode always starts from first TODO requirement
5311
- try {
5312
- ui.updateStatus({
5313
- requirement: 'Loading first TODO requirement...',
5314
- step: 'PREPARE',
5315
- chatCount: 0,
5316
- maxChats: maxChats ? parseInt(maxChats) : null,
5317
- progress: 0
5318
- });
5319
- } catch (error) {
5320
- ui.updateStatus({
5321
- requirement: 'Error loading requirements',
5322
- step: 'UNKNOWN',
5323
- chatCount: 0,
5324
- maxChats: maxChats ? parseInt(maxChats) : null,
5325
- progress: 0
5326
- });
5327
- }
5328
-
5329
- // Helper to parse requirements file content
5330
- const parseRequirementsFile = (content) => {
5331
- const lines = content.split('\n');
5332
- let currentRequirement = null;
5333
- let currentStatus = null;
5334
-
5335
- // Find current requirement (first one under TODO section)
5336
- let inTodoSection = false;
5337
- for (let i = 0; i < lines.length; i++) {
5338
- const line = lines[i];
5339
- const trimmed = line.trim();
5340
-
5341
- // Check if we're in TODO section
5342
- if (trimmed.includes('⏳ Requirements not yet completed') || trimmed.includes('Requirements not yet completed')) {
5343
- inTodoSection = true;
5344
- continue;
5345
- }
5346
-
5347
- // Exit TODO section at next ## header
5348
- if (inTodoSection && trimmed.startsWith('## ') && !trimmed.startsWith('###')) {
5349
- break;
5350
- }
5351
-
5352
- // Get first requirement in TODO section
5353
- if (inTodoSection && trimmed.startsWith('### ')) {
5354
- currentRequirement = trimmed.replace(/^###\s*/, '').trim();
5355
- break;
5356
- }
5357
- }
5358
-
5359
- // Find current status
5360
- for (let i = 0; i < lines.length; i++) {
5361
- const line = lines[i];
5362
- const trimmed = line.trim();
5363
-
5364
- if (trimmed.includes('🚦 Current Status') || trimmed.includes('Current Status')) {
5365
- // Look for status in next few lines
5366
- for (let j = i + 1; j < Math.min(i + 5, lines.length); j++) {
5367
- const statusLine = lines[j].trim();
5368
- if (statusLine.match(/^(PREPARE|CREATE|ACT|CLEAN UP|VERIFY|DONE)$/i)) {
5369
- currentStatus = statusLine.toUpperCase();
5370
- break;
5371
- }
5372
- }
5373
- break;
5374
- }
5375
- }
5376
-
5377
- return {
5378
- currentRequirement: currentRequirement || 'No active requirement',
5379
- currentStatus: currentStatus || 'PREPARE'
5380
- };
5381
- };
5382
-
5383
- // Watch requirements file for changes
5384
- const watcher = chokidar.watch(reqPath, { persistent: true });
5385
- watcher.on('change', async () => {
5386
- try {
5387
- const reqContent = await fs.readFile(reqPath, 'utf-8');
5388
- const parsed = parseRequirementsFile(reqContent);
5389
-
5390
- // Calculate progress based on step
5391
- const stepProgress = {
5392
- 'PREPARE': 20,
5393
- 'ACT': 40,
5394
- 'CLEAN UP': 60,
5395
- 'VERIFY': 80,
5396
- 'DONE': 100
5397
- };
5398
-
5399
- ui.updateStatus({
5400
- requirement: parsed.currentRequirement || 'Unknown',
5401
- step: parsed.currentStatus || 'UNKNOWN',
5402
- chatCount: 0, // TODO: Track actual chat count
5403
- maxChats: maxChats ? parseInt(maxChats) : null,
5404
- progress: stepProgress[parsed.currentStatus] || 0
5405
- });
5406
- } catch (error) {
5407
- // Silently ignore parse errors
5408
- }
5409
- });
5410
-
5411
- // Start auto mode (this will output to our interceptor)
5412
- try {
5413
- // Ensure stdin is NOT in raw mode before starting auto mode
5414
- // (otherwise Ctrl+C won't work)
5415
- if (process.stdin.isTTY && process.stdin.setRawMode) {
5416
- process.stdin.setRawMode(false);
5417
- }
5418
-
5419
- await auto.start(options);
5420
- } catch (error) {
5421
- ui.appendOutput(`\n\nError: ${error.message}\n`);
5422
- } finally {
5423
- // Cleanup
5424
- watcher.close();
5425
- interceptor.stop();
5426
- ui.destroy();
5427
- }
5428
- } catch (blessedError) {
5429
- // Fallback to regular auto mode if blessed UI fails
5430
- console.log(chalk.red('\n✗ Failed to create blessed UI:'), blessedError.message);
5431
- console.log(chalk.gray(' Falling back to standard output mode...\n'));
5432
-
5433
- // Ensure stdin is NOT in raw mode before starting auto mode
5434
- // (otherwise Ctrl+C won't work)
5435
- if (process.stdin.isTTY && process.stdin.setRawMode) {
5436
- process.stdin.setRawMode(false);
5437
- }
5438
-
5439
- await auto.start(options);
5440
- }
5441
-
5442
- break;
5443
- }
5444
- case 'auto:stop':
5445
- await auto.stop();
5446
- break;
5447
- case 'auto:status':
5448
- await auto.status();
5449
- break;
5450
- case 'logout': {
5451
- // Logout
5452
- const auth = require('./auth');
5453
- try {
5454
- await auth.logout();
5455
- console.log(chalk.green(`\n✓ ${t('interactive.logout.success')}\n`));
5456
- console.log(chalk.gray(`${t('interactive.logout.login.again')}\n`));
5457
- process.exit(0);
5458
- } catch (error) {
5459
- console.error(chalk.red(`\n✗ ${t('interactive.logout.failed')}`), error.message);
5460
- console.log(chalk.yellow(`\n${t('interactive.press.any.key')}...`));
5461
- await new Promise((resolve) => {
5462
- process.stdin.once('keypress', () => resolve());
5463
- });
5464
- }
5465
- break;
5466
- }
5467
- case 'exit': {
5468
- // Confirm and exit
5469
- console.log(chalk.gray('[DEBUG] Exit case triggered, calling confirmAndExit'));
5470
- await confirmAndExit();
5471
- console.log(chalk.gray('[DEBUG] confirmAndExit returned (user cancelled)'));
5472
- // If user cancelled (didn't exit), refresh the screen
5473
- await showWelcomeScreen();
5474
- break;
5475
- }
5476
- default:
5477
- // Log unhandled actions for debugging
5478
- console.log(chalk.yellow(`\n⚠️ Unhandled action: "${actionStr}"\n`));
5479
- console.log(chalk.gray(' This action is not implemented. Please report this issue.\n'));
5480
- const inquirer = require('inquirer');
5481
- await inquirer.prompt([{
5482
- type: 'input',
5483
- name: 'continue',
5484
- message: t('interactive.press.enter.return'),
5485
- }]);
5486
- await showWelcomeScreen();
5487
- break;
5488
- }
5489
- } catch (error) {
5490
- // Catch any unexpected errors in the main loop
5491
- console.log(chalk.red(`\n✗ Unexpected error in menu: ${error.message}`));
5492
- if (error.stack) {
5493
- console.log(chalk.gray(error.stack));
5494
- }
5495
- const inquirer = require('inquirer');
5496
- await inquirer.prompt([{
5497
- type: 'input',
5498
- name: 'continue',
5499
- message: t('interactive.press.enter.return'),
5500
- }]);
5501
- await showWelcomeScreen();
5502
- }
5503
- }
5504
- }
5505
-
5506
- async function bootstrapProjectIfInHomeDir() {
5507
- const { checkVibeCodingMachineExists, getRequirementsFilename } = require('vibecodingmachine-core');
5508
- const exists = await checkVibeCodingMachineExists();
5509
- const home = os.homedir();
5510
-
5511
- // If a VibeCodingMachine project already exists nearby, do nothing
5512
- if (exists && (exists.insideExists || exists.siblingExists)) return;
5513
-
5514
- // Only bootstrap when we're sitting at the user's home directory (resolve symlinks)
5515
- try {
5516
- const cwdResolved = await fs.realpath(process.cwd());
5517
- const homeResolved = await fs.realpath(home);
5518
- if (cwdResolved !== homeResolved) return;
5519
- } catch (e) {
5520
- // If we can't resolve, fallback to path.resolve comparison
5521
- if (path.resolve(process.cwd()) !== path.resolve(home)) return;
5522
- }
5523
-
5524
- const inquirer = require('inquirer');
5525
- const { shouldCreateCodeDir } = await inquirer.prompt({ type: 'confirm', name: 'shouldCreateCodeDir', message: t('interactive.should.create.code.dir') });
5526
- if (!shouldCreateCodeDir) return;
5527
-
5528
- const { projectName } = await inquirer.prompt({ type: 'input', name: 'projectName', message: t('interactive.project.name') });
5529
- const slug = (projectName || 'my-project-name').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '') || 'my-project-name';
5530
- const codeDir = path.join(home, 'code');
5531
- await fs.ensureDir(codeDir);
5532
-
5533
- const projectDir = path.join(codeDir, slug);
5534
- await fs.ensureDir(projectDir);
5535
-
5536
- process.chdir(projectDir);
5537
-
5538
- // Create .vibecodingmachine and REQUIREMENTS file if missing
5539
- const vcmDir = path.join(projectDir, '.vibecodingmachine');
5540
- await fs.ensureDir(vcmDir);
5541
- const reqFilename = await getRequirementsFilename();
5542
- const reqPath = path.join(vcmDir, reqFilename);
5543
- if (!await fs.pathExists(reqPath)) {
5544
- await fs.writeFile(reqPath, `# ${projectName}\n\n## ⏳ Requirements not yet completed\n\n`, 'utf8');
5545
- }
5546
- }
5547
-
5548
- module.exports = { startInteractive, showProviderManagerMenu, /* exported for tests */ parseRequirementsFromContent, bootstrapProjectIfInHomeDir };
5549
-
5550
-
5551
3534
 
3535
+ module.exports = {
3536
+ startInteractive,
3537
+ bootstrapProjectIfInHomeDir,
3538
+ showProviderManagerMenu,
3539
+ translateStage,
3540
+ normalizeProjectDirName
3541
+ };