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.
- package/bin/vibecodingmachine.js +124 -2
- package/package.json +3 -2
- package/src/commands/agents-check.js +69 -0
- package/src/commands/auto-direct.js +930 -145
- package/src/commands/auto.js +32 -8
- package/src/commands/ide.js +2 -1
- package/src/commands/requirements.js +23 -27
- package/src/utils/auto-mode.js +4 -1
- package/src/utils/cline-js-handler.js +218 -0
- package/src/utils/config.js +22 -0
- package/src/utils/display-formatters-complete.js +229 -0
- package/src/utils/display-formatters-extracted.js +219 -0
- package/src/utils/display-formatters.js +157 -0
- package/src/utils/feedback-handler.js +143 -0
- package/src/utils/first-run.js +5 -8
- package/src/utils/ide-detection-complete.js +126 -0
- package/src/utils/ide-detection-extracted.js +116 -0
- package/src/utils/ide-detection.js +124 -0
- package/src/utils/interactive-backup.js +5664 -0
- package/src/utils/interactive-broken.js +280 -0
- package/src/utils/interactive.js +357 -2367
- package/src/utils/provider-checker.js +410 -0
- package/src/utils/provider-manager.js +254 -0
- package/src/utils/provider-registry.js +18 -9
- package/src/utils/requirement-actions.js +884 -0
- package/src/utils/requirements-navigator.js +587 -0
- package/src/utils/rui-trui-adapter.js +311 -0
- package/src/utils/simple-trui.js +204 -0
- package/src/utils/status-helpers-extracted.js +125 -0
- package/src/utils/status-helpers.js +107 -0
- package/src/utils/trui-debug.js +261 -0
- package/src/utils/trui-feedback.js +133 -0
- package/src/utils/trui-nav-agents.js +119 -0
- package/src/utils/trui-nav-requirements.js +268 -0
- package/src/utils/trui-nav-settings.js +157 -0
- package/src/utils/trui-nav-specifications.js +139 -0
- package/src/utils/trui-navigation.js +305 -0
- package/src/utils/trui-provider-manager.js +182 -0
- package/src/utils/trui-quick-menu.js +370 -0
- package/src/utils/trui-req-actions.js +372 -0
- package/src/utils/trui-req-tree.js +534 -0
- package/src/utils/trui-specifications.js +359 -0
- package/src/utils/trui-text-editor.js +350 -0
- package/src/utils/trui-windsurf.js +350 -0
- package/src/utils/welcome-screen-extracted.js +135 -0
- package/src/utils/welcome-screen.js +134 -0
package/src/utils/interactive.js
CHANGED
|
@@ -1,147 +1,30 @@
|
|
|
1
|
-
const
|
|
2
|
-
const
|
|
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
|
|
7
|
-
const
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
|
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
|
-
|
|
696
|
-
|
|
756
|
+
requirements.push({ title, details, pkg, lineIndex: i });
|
|
757
|
+
}
|
|
758
|
+
}
|
|
697
759
|
|
|
698
|
-
//
|
|
699
|
-
|
|
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
|
|
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
|
-
|
|
878
|
-
await
|
|
879
|
-
|
|
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('\
|
|
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
|
-
//
|
|
2881
|
-
if (
|
|
2882
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
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 = '
|
|
3296
|
-
} else if (!isInstalled) {
|
|
3297
|
-
statusEmoji = '
|
|
2985
|
+
statusEmoji = '🚨'; // Red for disabled
|
|
2986
|
+
} else if (def.type === 'ide' && !isInstalled) {
|
|
2987
|
+
statusEmoji = '🟡'; // Yellow for not installed
|
|
3298
2988
|
} else {
|
|
3299
|
-
|
|
3300
|
-
|
|
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
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
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
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
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
|
-
|
|
3337
|
-
|
|
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
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
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
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
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
|
-
|
|
3025
|
+
line += ` ${chalk.red('[Rate limited]')}`;
|
|
3402
3026
|
}
|
|
3403
3027
|
} else {
|
|
3404
|
-
|
|
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
|
-
|
|
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
|
+
};
|