vibecodingmachine-cli 2026.1.3-2209 → 2026.1.22-1441
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/__tests__/antigravity-js-handler.test.js +23 -0
- package/__tests__/provider-manager.test.js +84 -0
- package/__tests__/provider-rate-cache.test.js +27 -0
- package/bin/vibecodingmachine.js +8 -0
- package/package.json +2 -2
- package/reset_provider_order.js +21 -0
- package/scripts/convert-requirements.js +35 -0
- package/scripts/debug-parse.js +24 -0
- package/src/commands/auto-direct.js +679 -120
- package/src/commands/auto.js +200 -45
- package/src/commands/ide.js +108 -3
- package/src/commands/requirements-remote.js +10 -1
- package/src/commands/status.js +39 -1
- package/src/utils/antigravity-js-handler.js +13 -4
- package/src/utils/auth.js +37 -13
- package/src/utils/compliance-check.js +10 -0
- package/src/utils/config.js +29 -1
- package/src/utils/date-formatter.js +44 -0
- package/src/utils/interactive.js +1006 -537
- package/src/utils/kiro-js-handler.js +188 -0
- package/src/utils/provider-rate-cache.js +31 -0
- package/src/utils/provider-registry.js +42 -1
- package/src/utils/requirements-converter.js +107 -0
- package/src/utils/requirements-parser.js +144 -0
- package/tests/antigravity-js-handler.test.js +23 -0
- package/tests/integration/health-tracking.integration.test.js +284 -0
- package/tests/provider-manager.test.js +92 -0
- package/tests/rate-limit-display.test.js +44 -0
- package/tests/requirements-bullet-parsing.test.js +15 -0
- package/tests/requirements-converter.test.js +42 -0
- package/tests/requirements-heading-count.test.js +27 -0
- package/tests/requirements-legacy-parsing.test.js +15 -0
- package/tests/requirements-parse-integration.test.js +44 -0
- package/tests/wait-for-ide-completion.test.js +56 -0
- package/tests/wait-for-ide-quota-detection-cursor-screenshot.test.js +61 -0
- package/tests/wait-for-ide-quota-detection-cursor.test.js +60 -0
- package/tests/wait-for-ide-quota-detection-negative.test.js +45 -0
- package/tests/wait-for-ide-quota-detection.test.js +59 -0
- package/verify_fix.js +36 -0
- package/verify_ui.js +38 -0
package/src/utils/interactive.js
CHANGED
|
@@ -10,7 +10,7 @@ const repo = require('../commands/repo');
|
|
|
10
10
|
const auto = require('../commands/auto');
|
|
11
11
|
const status = require('../commands/status');
|
|
12
12
|
const requirements = require('../commands/requirements');
|
|
13
|
-
const { getRepoPath, readConfig, writeConfig, getAutoConfig } = require('./config');
|
|
13
|
+
const { getRepoPath, readConfig, writeConfig, getAutoConfig, getProviderCache, setProviderCache } = require('./config');
|
|
14
14
|
const { getProviderPreferences, saveProviderPreferences, getProviderDefinitions } = require('../utils/provider-registry');
|
|
15
15
|
const { checkAutoModeStatus } = require('./auto-mode');
|
|
16
16
|
const {
|
|
@@ -21,15 +21,43 @@ const {
|
|
|
21
21
|
isComputerNameEnabled,
|
|
22
22
|
t,
|
|
23
23
|
detectLocale,
|
|
24
|
-
setLocale
|
|
24
|
+
setLocale,
|
|
25
|
+
AppleScriptManager,
|
|
26
|
+
IDEHealthTracker,
|
|
27
|
+
HealthReporter
|
|
25
28
|
} = require('vibecodingmachine-core');
|
|
26
29
|
const { promptWithDefaultsOnce } = require('./prompt-helper');
|
|
30
|
+
const { formatResetsAtLabel } = require('./date-formatter');
|
|
27
31
|
|
|
28
32
|
// Initialize locale detection for interactive mode
|
|
29
33
|
const detectedLocale = detectLocale();
|
|
30
34
|
setLocale(detectedLocale);
|
|
31
35
|
const pkg = require('../../package.json');
|
|
32
36
|
|
|
37
|
+
const { exec } = require('child_process');
|
|
38
|
+
const util = require('util');
|
|
39
|
+
const execAsync = util.promisify(exec);
|
|
40
|
+
|
|
41
|
+
async function checkVSCodeExtension(extensionId) {
|
|
42
|
+
// Check if a VS Code extension is installed
|
|
43
|
+
try {
|
|
44
|
+
const { stdout } = await execAsync('code --list-extensions', { timeout: 5000 });
|
|
45
|
+
const extensions = stdout.toLowerCase().split('\n').map(e => e.trim());
|
|
46
|
+
|
|
47
|
+
// Map common extension IDs to their actual extension IDs
|
|
48
|
+
const extensionMap = {
|
|
49
|
+
'github-copilot': 'github.copilot',
|
|
50
|
+
'amazon-q': 'amazonwebservices.amazon-q-vscode'
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const actualExtensionId = extensionMap[extensionId] || extensionId;
|
|
54
|
+
return extensions.includes(actualExtensionId.toLowerCase());
|
|
55
|
+
} catch (error) {
|
|
56
|
+
// If code CLI is not available or command fails, return false
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
33
61
|
async function checkAppOrBinary(names = [], binaries = []) {
|
|
34
62
|
// names: app bundle base names (e.g., 'Cursor' -> /Applications/Cursor.app)
|
|
35
63
|
// binaries: CLI binary names to check on PATH (e.g., 'code')
|
|
@@ -52,10 +80,12 @@ async function checkAppOrBinary(names = [], binaries = []) {
|
|
|
52
80
|
// Prefer to ensure the app is usable: use spctl to assess, fallback to quarantine xattr
|
|
53
81
|
try {
|
|
54
82
|
// spctl returns non-zero on rejected/invalid apps
|
|
55
|
-
|
|
83
|
+
// Use async exec to avoid blocking the event loop
|
|
84
|
+
await execAsync(`spctl --assess -v "${p}"`, { timeout: 5000 });
|
|
85
|
+
|
|
56
86
|
// additionally validate codesign quickly (timeout to avoid hangs)
|
|
57
87
|
try {
|
|
58
|
-
|
|
88
|
+
await execAsync(`codesign -v --deep --strict "${p}"`, { timeout: 5000 });
|
|
59
89
|
return true;
|
|
60
90
|
} catch (csErr) {
|
|
61
91
|
// codesign failed or timed out — treat as not usable/damaged
|
|
@@ -64,7 +94,8 @@ async function checkAppOrBinary(names = [], binaries = []) {
|
|
|
64
94
|
} catch (e) {
|
|
65
95
|
// spctl failed or timed out — check if app has quarantine attribute
|
|
66
96
|
try {
|
|
67
|
-
const
|
|
97
|
+
const { stdout } = await execAsync(`xattr -p com.apple.quarantine "${p}" 2>/dev/null || true`, { encoding: 'utf8' });
|
|
98
|
+
const out = stdout.trim();
|
|
68
99
|
if (!out) {
|
|
69
100
|
// no quarantine attribute but spctl failed — be conservative and treat as not installed
|
|
70
101
|
return false;
|
|
@@ -91,7 +122,7 @@ async function checkAppOrBinary(names = [], binaries = []) {
|
|
|
91
122
|
// Check PATH for known binaries
|
|
92
123
|
for (const bin of binaries) {
|
|
93
124
|
try {
|
|
94
|
-
|
|
125
|
+
await execAsync(`which ${bin}`, { timeout: 2000 });
|
|
95
126
|
return true;
|
|
96
127
|
} catch (e) {
|
|
97
128
|
/* not found */
|
|
@@ -130,78 +161,6 @@ function translateStage(stage) {
|
|
|
130
161
|
return key ? t(key) : stage;
|
|
131
162
|
}
|
|
132
163
|
|
|
133
|
-
function normalizeProjectDirName(input) {
|
|
134
|
-
const s = String(input || '').trim().toLowerCase();
|
|
135
|
-
const replaced = s.replace(/\s+/g, '-');
|
|
136
|
-
const cleaned = replaced.replace(/[^a-z0-9._-]/g, '-');
|
|
137
|
-
const collapsed = cleaned.replace(/-+/g, '-').replace(/^[-._]+|[-._]+$/g, '');
|
|
138
|
-
return collapsed || 'my-project';
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
async function bootstrapProjectIfInHomeDir() {
|
|
142
|
-
let cwdResolved = path.resolve(process.cwd());
|
|
143
|
-
let homeResolved = path.resolve(os.homedir());
|
|
144
|
-
|
|
145
|
-
try {
|
|
146
|
-
cwdResolved = await fs.realpath(cwdResolved);
|
|
147
|
-
} catch (err) {
|
|
148
|
-
// Ignore
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
try {
|
|
152
|
-
homeResolved = await fs.realpath(homeResolved);
|
|
153
|
-
} catch (err) {
|
|
154
|
-
// Ignore
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (cwdResolved !== homeResolved) {
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const codeDir = path.join(homeResolved, 'code');
|
|
162
|
-
const codeDirExists = await fs.pathExists(codeDir);
|
|
163
|
-
|
|
164
|
-
if (!codeDirExists) {
|
|
165
|
-
const { shouldCreateCodeDir } = await inquirer.prompt([
|
|
166
|
-
{
|
|
167
|
-
type: 'confirm',
|
|
168
|
-
name: 'shouldCreateCodeDir',
|
|
169
|
-
message: `No code directory found at ${codeDir}. Would you like to create it?`,
|
|
170
|
-
default: true
|
|
171
|
-
}
|
|
172
|
-
]);
|
|
173
|
-
|
|
174
|
-
if (!shouldCreateCodeDir) {
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
await fs.ensureDir(codeDir);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
const { projectName } = await inquirer.prompt([
|
|
182
|
-
{
|
|
183
|
-
type: 'input',
|
|
184
|
-
name: 'projectName',
|
|
185
|
-
message: 'What is the project name you want to create?',
|
|
186
|
-
validate: (value) => {
|
|
187
|
-
const trimmed = String(value || '').trim();
|
|
188
|
-
return trimmed.length > 0 || 'Please enter a project name.';
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
]);
|
|
192
|
-
|
|
193
|
-
const dirName = normalizeProjectDirName(projectName);
|
|
194
|
-
const projectDir = path.join(codeDir, dirName);
|
|
195
|
-
await fs.ensureDir(projectDir);
|
|
196
|
-
|
|
197
|
-
process.chdir(projectDir);
|
|
198
|
-
try {
|
|
199
|
-
await writeConfig({ ...(await readConfig()), repoPath: projectDir });
|
|
200
|
-
} catch (err) {
|
|
201
|
-
// Best-effort: interactive mode will still use process.cwd()
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
164
|
/**
|
|
206
165
|
* Format IDE name for display
|
|
207
166
|
* @param {string} ide - Internal IDE identifier
|
|
@@ -507,7 +466,7 @@ async function showWelcomeScreen() {
|
|
|
507
466
|
|
|
508
467
|
// Display welcome banner with version
|
|
509
468
|
console.log('\n' + boxen(
|
|
510
|
-
chalk.bold.cyan('Vibe Coding Machine') + '\n' +
|
|
469
|
+
chalk.bold.cyan('Vibe Coding Machine!') + '\n' +
|
|
511
470
|
chalk.gray(version) + '\n' +
|
|
512
471
|
chalk.gray(t('banner.tagline')),
|
|
513
472
|
{
|
|
@@ -518,6 +477,9 @@ async function showWelcomeScreen() {
|
|
|
518
477
|
}
|
|
519
478
|
));
|
|
520
479
|
|
|
480
|
+
// Display feedback hint at the top of every screen
|
|
481
|
+
console.log(chalk.gray('💡 Press F for feedback - Share your thoughts anytime'));
|
|
482
|
+
|
|
521
483
|
// Display repository and system info
|
|
522
484
|
console.log();
|
|
523
485
|
console.log(chalk.gray(t('system.repo').padEnd(25)), formatPath(repoPath));
|
|
@@ -606,10 +568,14 @@ function indexToLetter(index) {
|
|
|
606
568
|
return String.fromCharCode(97 + index); // 97 is 'a'
|
|
607
569
|
}
|
|
608
570
|
|
|
571
|
+
// Parse requirements from file content for a given section (supports ###, PACKAGE:, and '- ' bullets)
|
|
572
|
+
const { parseRequirementsFromContent } = require('./requirements-parser');
|
|
573
|
+
|
|
609
574
|
// Tree-style requirements navigator
|
|
610
575
|
async function showRequirementsTree() {
|
|
611
576
|
console.log(chalk.bold.cyan('\n📋 ' + t('requirements.navigator.title') + '\n'));
|
|
612
577
|
console.log(chalk.gray(t('requirements.navigator.basic.instructions') + '\n'));
|
|
578
|
+
console.log(chalk.gray('💡 Press F for feedback - Share your thoughts anytime\n'));
|
|
613
579
|
|
|
614
580
|
const tree = {
|
|
615
581
|
expanded: { root: true },
|
|
@@ -725,136 +691,14 @@ async function showRequirementsTree() {
|
|
|
725
691
|
}
|
|
726
692
|
|
|
727
693
|
const content = await fs.readFile(reqPath, 'utf8');
|
|
728
|
-
const lines = content.split('\n');
|
|
729
|
-
|
|
730
|
-
let inSection = false;
|
|
731
|
-
const requirements = [];
|
|
732
|
-
|
|
733
|
-
// For TO VERIFY section, check multiple possible section titles
|
|
734
|
-
const sectionTitles = sectionKey === 'verify'
|
|
735
|
-
? ['🔍 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']
|
|
736
|
-
: [sectionTitle];
|
|
737
|
-
|
|
738
|
-
// For TO VERIFY, we need to find the exact section header
|
|
739
|
-
// The section header is: ## ✅ Verified by AI screenshot. Needs Human to Verify and move to CHANGELOG
|
|
740
|
-
const toVerifySectionHeader = '## ✅ Verified by AI screenshot. Needs Human to Verify and move to CHANGELOG';
|
|
741
|
-
|
|
742
|
-
for (let i = 0; i < lines.length; i++) {
|
|
743
|
-
const line = lines[i];
|
|
744
|
-
const trimmed = line.trim();
|
|
745
|
-
|
|
746
|
-
// Check if this line matches any of the section titles
|
|
747
|
-
// IMPORTANT: Only check section headers (lines starting with ##), not requirement text
|
|
748
|
-
if (trimmed.startsWith('##') && !trimmed.startsWith('###')) {
|
|
749
|
-
// Reset inSection if we hit a section header that's not our target section
|
|
750
|
-
if (sectionKey === 'verify' && inSection) {
|
|
751
|
-
// Check if this is still a TO VERIFY section header
|
|
752
|
-
if (!isStillToVerify) {
|
|
753
|
-
// This will be handled by the "leaving section" check below, but ensure we don't process it as entering
|
|
754
|
-
}
|
|
755
|
-
}
|
|
756
|
-
if (sectionKey === 'verify') {
|
|
757
|
-
// For TO VERIFY, check for the specific section header with exact matching
|
|
758
|
-
// Must match the exact TO VERIFY section header, not just any line containing "TO VERIFY"
|
|
759
|
-
const isToVerifyHeader = trimmed === '## 🔍 TO VERIFY BY HUMAN' ||
|
|
760
|
-
trimmed.startsWith('## 🔍 TO VERIFY BY HUMAN') ||
|
|
761
|
-
trimmed === '## 🔍 TO VERIFY' ||
|
|
762
|
-
trimmed.startsWith('## 🔍 TO VERIFY') ||
|
|
763
|
-
trimmed === '## TO VERIFY' ||
|
|
764
|
-
trimmed.startsWith('## TO VERIFY') ||
|
|
765
|
-
trimmed === '## ✅ TO VERIFY' ||
|
|
766
|
-
trimmed.startsWith('## ✅ TO VERIFY') ||
|
|
767
|
-
trimmed === toVerifySectionHeader ||
|
|
768
|
-
(trimmed.startsWith(toVerifySectionHeader) && trimmed.includes('Needs Human to Verify'));
|
|
769
|
-
|
|
770
|
-
if (isToVerifyHeader) {
|
|
771
|
-
// Make sure it's not a VERIFIED section (without TO VERIFY)
|
|
772
|
-
if (!trimmed.includes('## 📝 VERIFIED') && !trimmed.match(/^##\s+VERIFIED$/i) && !trimmed.includes('📝 VERIFIED')) {
|
|
773
|
-
inSection = true;
|
|
774
|
-
continue;
|
|
775
|
-
}
|
|
776
|
-
} else {
|
|
777
|
-
// If we hit a different section header and we're looking for TO VERIFY, make sure we're not in section
|
|
778
|
-
// This prevents incorrectly reading from TODO or other sections
|
|
779
|
-
if (trimmed.includes('⏳ Requirements not yet completed') ||
|
|
780
|
-
trimmed.includes('Requirements not yet completed') ||
|
|
781
|
-
trimmed === '## 📝 VERIFIED' ||
|
|
782
|
-
trimmed.startsWith('## 📝 VERIFIED')) {
|
|
783
|
-
// We're in TODO or VERIFIED section, not TO VERIFY - reset
|
|
784
|
-
inSection = false;
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
} else if (sectionTitles.some(title => trimmed.includes(title))) {
|
|
788
|
-
inSection = true;
|
|
789
|
-
continue;
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
// Check if we're leaving the section (new section header that doesn't match)
|
|
794
|
-
if (inSection && trimmed.startsWith('##') && !trimmed.startsWith('###')) {
|
|
795
|
-
// If this is a new section header and it's not one of our section titles, we've left the section
|
|
796
|
-
if (sectionKey === 'verify') {
|
|
797
|
-
// For TO VERIFY, only break if this is clearly a different section
|
|
798
|
-
// Check for specific section headers that indicate we've left TO VERIFY
|
|
799
|
-
|
|
800
|
-
if (isVerifiedSection || isTodoSection || isRecycledSection || isClarificationSection) {
|
|
801
|
-
break; // Different section, we've left TO VERIFY
|
|
802
|
-
}
|
|
803
|
-
// Otherwise, continue - might be REJECTED or CHANGELOG which are not section boundaries for TO VERIFY
|
|
804
|
-
} else {
|
|
805
|
-
// For other sections, break if it's a new section header that doesn't match
|
|
806
|
-
if (!sectionTitles.some(title => trimmed.includes(title))) {
|
|
807
|
-
break;
|
|
808
|
-
}
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
// Read requirements in new format (### header)
|
|
813
|
-
if (inSection && line.trim().startsWith('###')) {
|
|
814
|
-
const title = line.trim().replace(/^###\s*/, '').trim();
|
|
815
|
-
|
|
816
|
-
// Skip malformed requirements (title is just a package name, empty, or too short)
|
|
817
|
-
// Common package names that shouldn't be requirement titles
|
|
818
|
-
const packageNames = ['cli', 'core', 'electron-app', 'web', 'mobile', 'vscode-extension', 'sync-server'];
|
|
819
|
-
if (!title || title.length === 0 || packageNames.includes(title.toLowerCase())) {
|
|
820
|
-
continue; // Skip this malformed requirement
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
const details = [];
|
|
824
|
-
let pkg = null;
|
|
825
|
-
|
|
826
|
-
// Read package and description
|
|
827
|
-
for (let j = i + 1; j < lines.length; j++) {
|
|
828
|
-
const nextLine = lines[j].trim();
|
|
829
|
-
// Stop if we hit another requirement or section
|
|
830
|
-
if (nextLine.startsWith('###') || (nextLine.startsWith('##') && !nextLine.startsWith('###'))) {
|
|
831
|
-
break;
|
|
832
|
-
}
|
|
833
|
-
// Check for PACKAGE line
|
|
834
|
-
if (nextLine.startsWith('PACKAGE:')) {
|
|
835
|
-
pkg = nextLine.replace(/^PACKAGE:\s*/, '').trim();
|
|
836
|
-
} else if (nextLine && !nextLine.startsWith('PACKAGE:')) {
|
|
837
|
-
// Description line
|
|
838
|
-
details.push(nextLine);
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
694
|
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
}
|
|
695
|
+
// Delegate to reusable parser
|
|
696
|
+
const allReqs = parseRequirementsFromContent(content, sectionKey, sectionTitle);
|
|
845
697
|
|
|
846
|
-
//
|
|
847
|
-
|
|
848
|
-
const uniqueRequirements = [];
|
|
849
|
-
for (const req of requirements) {
|
|
850
|
-
const normalizedTitle = req.title.replace(/^TRY AGAIN \(\d+(st|nd|rd|th) time\):\s*/i, '').trim();
|
|
851
|
-
if (!seenTitles.has(normalizedTitle)) {
|
|
852
|
-
seenTitles.add(normalizedTitle);
|
|
853
|
-
uniqueRequirements.push(req);
|
|
854
|
-
}
|
|
855
|
-
}
|
|
698
|
+
// For TODO section, only show primary heading requirements (those marked from '###' titles)
|
|
699
|
+
if (sectionKey === 'todo') return allReqs.filter(r => r.source === 'heading');
|
|
856
700
|
|
|
857
|
-
return
|
|
701
|
+
return allReqs;
|
|
858
702
|
};
|
|
859
703
|
|
|
860
704
|
// Load VERIFIED requirements from CHANGELOG
|
|
@@ -1203,6 +1047,10 @@ async function showRequirementsTree() {
|
|
|
1203
1047
|
tree.todoReqs = await loadSection('todo', '⏳ Requirements not yet completed');
|
|
1204
1048
|
await buildTree();
|
|
1205
1049
|
}
|
|
1050
|
+
} else if (key.name === 'f') {
|
|
1051
|
+
// Feedback button ( megaphone 📣 )
|
|
1052
|
+
await handleFeedbackSubmission();
|
|
1053
|
+
await buildTree();
|
|
1206
1054
|
} else if (key.name === 'r') {
|
|
1207
1055
|
const current = tree.items[tree.selected];
|
|
1208
1056
|
if (!current) continue; // Safety check
|
|
@@ -1279,6 +1127,90 @@ async function showRequirementsTree() {
|
|
|
1279
1127
|
process.stdin.pause();
|
|
1280
1128
|
}
|
|
1281
1129
|
|
|
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
|
+
console.log(chalk.gray('\n' + t('interactive.feedback.comment.instructions') + '\n'));
|
|
1142
|
+
|
|
1143
|
+
const commentLines = [];
|
|
1144
|
+
let emptyLineCount = 0;
|
|
1145
|
+
let isFirstLine = true;
|
|
1146
|
+
|
|
1147
|
+
while (true) {
|
|
1148
|
+
try {
|
|
1149
|
+
const { line } = await inquirer.prompt([{
|
|
1150
|
+
type: 'input',
|
|
1151
|
+
name: 'line',
|
|
1152
|
+
message: isFirstLine ? t('interactive.feedback.comment') : ''
|
|
1153
|
+
}]);
|
|
1154
|
+
|
|
1155
|
+
isFirstLine = false;
|
|
1156
|
+
|
|
1157
|
+
if (line.trim() === '') {
|
|
1158
|
+
emptyLineCount++;
|
|
1159
|
+
if (emptyLineCount >= 2) break;
|
|
1160
|
+
} else {
|
|
1161
|
+
emptyLineCount = 0;
|
|
1162
|
+
commentLines.push(line);
|
|
1163
|
+
}
|
|
1164
|
+
} catch (err) {
|
|
1165
|
+
break;
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
const comment = commentLines.join('\n');
|
|
1170
|
+
|
|
1171
|
+
if (!comment || comment.trim().length === 0) {
|
|
1172
|
+
console.log(chalk.yellow('\n⚠️ ' + t('interactive.feedback.cancelled')));
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
try {
|
|
1177
|
+
console.log(chalk.gray('\n' + t('interactive.feedback.submitting') + '...'));
|
|
1178
|
+
|
|
1179
|
+
const auth = require('./auth');
|
|
1180
|
+
const UserDatabase = require('vibecodingmachine-core/src/database/user-schema');
|
|
1181
|
+
const userDb = new UserDatabase();
|
|
1182
|
+
|
|
1183
|
+
// Set auth token for API requests
|
|
1184
|
+
const token = await auth.getAuthToken();
|
|
1185
|
+
if (token) {
|
|
1186
|
+
userDb.setAuthToken(token);
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
await userDb.submitFeedback({
|
|
1190
|
+
email: userEmail,
|
|
1191
|
+
comment: comment,
|
|
1192
|
+
interface: 'cli',
|
|
1193
|
+
version: pkg.version
|
|
1194
|
+
});
|
|
1195
|
+
|
|
1196
|
+
console.log(chalk.green('\n✓ ' + t('interactive.feedback.success')));
|
|
1197
|
+
} catch (error) {
|
|
1198
|
+
console.log(chalk.red('\n✗ ' + t('interactive.feedback.error') + ': ') + error.message);
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
console.log(chalk.gray(`\n${t('interactive.press.enter.continue')}`));
|
|
1202
|
+
await new Promise(resolve => {
|
|
1203
|
+
const rl = readline.createInterface({
|
|
1204
|
+
input: process.stdin,
|
|
1205
|
+
output: process.stdout
|
|
1206
|
+
});
|
|
1207
|
+
rl.question('', () => {
|
|
1208
|
+
rl.close();
|
|
1209
|
+
resolve();
|
|
1210
|
+
});
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1282
1214
|
// Helper to show goodbye message
|
|
1283
1215
|
function showGoodbyeMessage() {
|
|
1284
1216
|
const hour = new Date().getHours();
|
|
@@ -1627,6 +1559,7 @@ async function showClarificationActions(req, tree, loadClarification) {
|
|
|
1627
1559
|
const actions = [
|
|
1628
1560
|
{ label: '✍️ Add/Edit Responses', value: 'edit-responses' },
|
|
1629
1561
|
{ label: '↩️ Move back to TODO (after clarification)', value: 'move-to-todo' },
|
|
1562
|
+
{ label: '📣 Feedback', value: 'feedback' },
|
|
1630
1563
|
{ label: '🗑️ Delete', value: 'delete' }
|
|
1631
1564
|
];
|
|
1632
1565
|
|
|
@@ -1657,7 +1590,8 @@ async function showClarificationActions(req, tree, loadClarification) {
|
|
|
1657
1590
|
});
|
|
1658
1591
|
|
|
1659
1592
|
// Display menu
|
|
1660
|
-
console.log(chalk.gray('\
|
|
1593
|
+
console.log(chalk.gray('\n💡 Press F for feedback - Share your thoughts anytime'));
|
|
1594
|
+
console.log(chalk.gray('What would you like to do? (↑/↓/Enter to select, ESC/← to go back)\n'));
|
|
1661
1595
|
actions.forEach((action, idx) => {
|
|
1662
1596
|
if (idx === selected) {
|
|
1663
1597
|
console.log(chalk.cyan(`❯ ${action.label}`));
|
|
@@ -1688,6 +1622,10 @@ async function showClarificationActions(req, tree, loadClarification) {
|
|
|
1688
1622
|
selected = Math.max(0, selected - 1);
|
|
1689
1623
|
} else if (key.name === 'down') {
|
|
1690
1624
|
selected = Math.min(actions.length - 1, selected + 1);
|
|
1625
|
+
} else if (key.name === 'f') {
|
|
1626
|
+
// Feedback button ( megaphone 📣 )
|
|
1627
|
+
await handleFeedbackSubmission();
|
|
1628
|
+
return;
|
|
1691
1629
|
} else if (key.name === 'return' || key.name === 'space') {
|
|
1692
1630
|
const action = actions[selected].value;
|
|
1693
1631
|
|
|
@@ -1699,6 +1637,9 @@ async function showClarificationActions(req, tree, loadClarification) {
|
|
|
1699
1637
|
await moveClarificationToTodo(req, tree);
|
|
1700
1638
|
tree.clarificationReqs = await loadClarification();
|
|
1701
1639
|
return;
|
|
1640
|
+
} else if (action === 'feedback') {
|
|
1641
|
+
await handleFeedbackSubmission();
|
|
1642
|
+
return;
|
|
1702
1643
|
} else if (action === 'delete') {
|
|
1703
1644
|
await deleteClarification(req, tree);
|
|
1704
1645
|
tree.clarificationReqs = await loadClarification();
|
|
@@ -1716,6 +1657,7 @@ async function showRequirementActions(req, sectionKey, tree) {
|
|
|
1716
1657
|
{ label: '👎 Thumbs down (demote to TODO)', value: 'thumbs-down' },
|
|
1717
1658
|
{ label: '⬆️ Move up', value: 'move-up' },
|
|
1718
1659
|
{ label: '⬇️ Move down', value: 'move-down' },
|
|
1660
|
+
{ label: '📣 Feedback', value: 'feedback' },
|
|
1719
1661
|
{ label: '🗑️ Delete', value: 'delete' }
|
|
1720
1662
|
];
|
|
1721
1663
|
|
|
@@ -1745,6 +1687,7 @@ async function showRequirementActions(req, sectionKey, tree) {
|
|
|
1745
1687
|
|
|
1746
1688
|
// Display menu (always reprinted)
|
|
1747
1689
|
console.log();
|
|
1690
|
+
console.log(chalk.gray('💡 Press F for feedback - Share your thoughts anytime'));
|
|
1748
1691
|
console.log(chalk.gray('What would you like to do? (↑/↓/Enter to select, ESC/← to go back)'));
|
|
1749
1692
|
console.log();
|
|
1750
1693
|
menuLines += 3; // Blank line + help text + blank line
|
|
@@ -1789,6 +1732,10 @@ async function showRequirementActions(req, sectionKey, tree) {
|
|
|
1789
1732
|
process.exit(0);
|
|
1790
1733
|
} else if (key.name === 'escape' || key.name === 'left') {
|
|
1791
1734
|
return; // Go back
|
|
1735
|
+
} else if (key.name === 'f') {
|
|
1736
|
+
// Feedback button ( megaphone 📣 )
|
|
1737
|
+
await handleFeedbackSubmission();
|
|
1738
|
+
return;
|
|
1792
1739
|
} else if (key.name === 'up') {
|
|
1793
1740
|
selected = Math.max(0, selected - 1);
|
|
1794
1741
|
} else if (key.name === 'down') {
|
|
@@ -1849,6 +1796,9 @@ async function performRequirementAction(action, req, sectionKey, tree) {
|
|
|
1849
1796
|
case 'rename':
|
|
1850
1797
|
await renameRequirement(req, sectionKey, tree);
|
|
1851
1798
|
break;
|
|
1799
|
+
case 'feedback':
|
|
1800
|
+
await handleFeedbackSubmission();
|
|
1801
|
+
break;
|
|
1852
1802
|
}
|
|
1853
1803
|
|
|
1854
1804
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
@@ -2641,6 +2591,104 @@ async function showRequirementsBySection(sectionTitle) {
|
|
|
2641
2591
|
}
|
|
2642
2592
|
}
|
|
2643
2593
|
|
|
2594
|
+
// Helper to move a requirement to the Recycled section
|
|
2595
|
+
async function moveToRecycled(reqPath, requirementTitle, fromSectionTitle) {
|
|
2596
|
+
const content = await fs.readFile(reqPath, 'utf8');
|
|
2597
|
+
const lines = content.split('\n');
|
|
2598
|
+
|
|
2599
|
+
let inFromSection = false;
|
|
2600
|
+
let inRecycledSection = false;
|
|
2601
|
+
let reqStartIndex = -1;
|
|
2602
|
+
let reqEndIndex = -1;
|
|
2603
|
+
let recycledSectionIndex = -1;
|
|
2604
|
+
let requirement = null;
|
|
2605
|
+
|
|
2606
|
+
// Find the requirement in the source section
|
|
2607
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2608
|
+
const line = lines[i];
|
|
2609
|
+
|
|
2610
|
+
// Track if we're in the source section
|
|
2611
|
+
if (line.includes(fromSectionTitle)) {
|
|
2612
|
+
inFromSection = true;
|
|
2613
|
+
continue;
|
|
2614
|
+
}
|
|
2615
|
+
|
|
2616
|
+
// Exit source section at next ## header
|
|
2617
|
+
if (inFromSection && line.startsWith('## ') && !line.startsWith('###') && !line.includes(fromSectionTitle)) {
|
|
2618
|
+
inFromSection = false;
|
|
2619
|
+
}
|
|
2620
|
+
|
|
2621
|
+
// Find the requirement
|
|
2622
|
+
if (inFromSection && line.trim() === `### ${requirementTitle}`) {
|
|
2623
|
+
reqStartIndex = i;
|
|
2624
|
+
requirement = { title: requirementTitle, details: [], package: null };
|
|
2625
|
+
|
|
2626
|
+
// Read requirement details
|
|
2627
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
2628
|
+
const nextLine = lines[j];
|
|
2629
|
+
// Stop at next requirement or section
|
|
2630
|
+
if (nextLine.startsWith('###') || (nextLine.startsWith('## ') && !nextLine.startsWith('###'))) {
|
|
2631
|
+
reqEndIndex = j;
|
|
2632
|
+
break;
|
|
2633
|
+
}
|
|
2634
|
+
// Check for package
|
|
2635
|
+
if (nextLine.startsWith('PACKAGE:')) {
|
|
2636
|
+
requirement.package = nextLine.replace('PACKAGE:', '').trim();
|
|
2637
|
+
} else if (nextLine.trim()) {
|
|
2638
|
+
requirement.details.push(nextLine);
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2641
|
+
|
|
2642
|
+
if (reqEndIndex === -1) {
|
|
2643
|
+
reqEndIndex = lines.length;
|
|
2644
|
+
}
|
|
2645
|
+
break;
|
|
2646
|
+
}
|
|
2647
|
+
|
|
2648
|
+
// Find Recycled section
|
|
2649
|
+
if (line.includes('♻️ Recycled') || line.includes('## Recycled')) {
|
|
2650
|
+
recycledSectionIndex = i;
|
|
2651
|
+
inRecycledSection = true;
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2654
|
+
|
|
2655
|
+
if (!requirement || reqStartIndex === -1) {
|
|
2656
|
+
throw new Error(`Requirement "${requirementTitle}" not found in ${fromSectionTitle}`);
|
|
2657
|
+
}
|
|
2658
|
+
|
|
2659
|
+
// If Recycled section doesn't exist, create it at the end
|
|
2660
|
+
if (recycledSectionIndex === -1) {
|
|
2661
|
+
recycledSectionIndex = lines.length;
|
|
2662
|
+
lines.push('', '## ♻️ Recycled', '');
|
|
2663
|
+
}
|
|
2664
|
+
|
|
2665
|
+
// Remove requirement from source section
|
|
2666
|
+
const removedLines = lines.splice(reqStartIndex, reqEndIndex - reqStartIndex);
|
|
2667
|
+
|
|
2668
|
+
// Find where to insert in Recycled section (after the section header)
|
|
2669
|
+
let insertIndex = recycledSectionIndex + 1;
|
|
2670
|
+
while (insertIndex < lines.length && lines[insertIndex].trim() === '') {
|
|
2671
|
+
insertIndex++;
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2674
|
+
// Build requirement lines
|
|
2675
|
+
const reqLines = [`### ${requirement.title}`];
|
|
2676
|
+
if (requirement.package && requirement.package !== 'all') {
|
|
2677
|
+
reqLines.push(`PACKAGE: ${requirement.package}`);
|
|
2678
|
+
}
|
|
2679
|
+
requirement.details.forEach(line => {
|
|
2680
|
+
if (line.trim()) {
|
|
2681
|
+
reqLines.push(line);
|
|
2682
|
+
}
|
|
2683
|
+
});
|
|
2684
|
+
reqLines.push('');
|
|
2685
|
+
|
|
2686
|
+
// Insert into Recycled section
|
|
2687
|
+
lines.splice(insertIndex, 0, ...reqLines);
|
|
2688
|
+
|
|
2689
|
+
await fs.writeFile(reqPath, lines.join('\n'));
|
|
2690
|
+
}
|
|
2691
|
+
|
|
2644
2692
|
// Helper to save reordered requirements back to file
|
|
2645
2693
|
async function saveRequirementsOrder(reqPath, sectionTitle, requirements) {
|
|
2646
2694
|
const content = await fs.readFile(reqPath, 'utf8');
|
|
@@ -2739,6 +2787,9 @@ async function showRequirementsFromChangelog() {
|
|
|
2739
2787
|
}
|
|
2740
2788
|
}
|
|
2741
2789
|
|
|
2790
|
+
// Input suppression timestamp to avoid immediate re-entry into menus after cancel
|
|
2791
|
+
let menuSuppressUntil = 0;
|
|
2792
|
+
|
|
2742
2793
|
// Custom menu with both arrow keys and letter shortcuts
|
|
2743
2794
|
async function showQuickMenu(items, initialSelectedIndex = 0) {
|
|
2744
2795
|
return new Promise((resolve) => {
|
|
@@ -2750,6 +2801,7 @@ async function showQuickMenu(items, initialSelectedIndex = 0) {
|
|
|
2750
2801
|
if (selectedIndex >= items.length) selectedIndex = 0;
|
|
2751
2802
|
|
|
2752
2803
|
let isFirstRender = true;
|
|
2804
|
+
let lastLinesPrinted = 0;
|
|
2753
2805
|
|
|
2754
2806
|
// Helper to calculate visual lines occupied by text
|
|
2755
2807
|
const getVisualLineCount = (text) => {
|
|
@@ -2889,7 +2941,30 @@ async function showQuickMenu(items, initialSelectedIndex = 0) {
|
|
|
2889
2941
|
process.stdin.setRawMode(true);
|
|
2890
2942
|
}
|
|
2891
2943
|
|
|
2944
|
+
// Ignore spurious keypresses for a short debounce window after the menu opens
|
|
2945
|
+
// This prevents accidental immediate re-entry when returning from sub-menus.
|
|
2946
|
+
const IGNORE_INPUT_MS = 300; // milliseconds
|
|
2947
|
+
// If a recent menu cancel occurred, extend the ignore window to avoid key repeat re-opening
|
|
2948
|
+
let ignoreInputUntil = Math.max(Date.now() + IGNORE_INPUT_MS, menuSuppressUntil || 0);
|
|
2949
|
+
|
|
2892
2950
|
const onKeypress = async (str, key) => {
|
|
2951
|
+
if (process.env.VCM_DEBUG_MENU === '1') {
|
|
2952
|
+
const msg = `[DEBUG-MENU ${new Date().toISOString()}] onKeypress: str=${JSON.stringify(str)} key=${JSON.stringify(key)} now=${Date.now()}\n`;
|
|
2953
|
+
console.log(msg);
|
|
2954
|
+
try { require('fs').appendFileSync('/tmp/vcm-menu-debug.log', msg); } catch (e) { }
|
|
2955
|
+
}
|
|
2956
|
+
// Always allow Ctrl+C to pass through immediately
|
|
2957
|
+
if (key && key.ctrl && key.name === 'c') {
|
|
2958
|
+
cleanup();
|
|
2959
|
+
process.exit(0);
|
|
2960
|
+
return;
|
|
2961
|
+
}
|
|
2962
|
+
|
|
2963
|
+
// Debounce: ignore inputs until the short window passes
|
|
2964
|
+
if (Date.now() < ignoreInputUntil) {
|
|
2965
|
+
return;
|
|
2966
|
+
}
|
|
2967
|
+
|
|
2893
2968
|
if (!key) return;
|
|
2894
2969
|
|
|
2895
2970
|
// Ctrl+C to exit
|
|
@@ -2911,6 +2986,7 @@ async function showQuickMenu(items, initialSelectedIndex = 0) {
|
|
|
2911
2986
|
if (str && str.length === 1) {
|
|
2912
2987
|
if (str === 'x') {
|
|
2913
2988
|
// 'x' always maps to exit (will trigger confirmAndExit)
|
|
2989
|
+
if (process.env.VCM_DEBUG_MENU === '1') console.log(`[DEBUG-MENU ${new Date().toISOString()}] quickMenu: 'x' pressed (exit)`);
|
|
2914
2990
|
cleanup();
|
|
2915
2991
|
// Don't clear screen for exit - keep status visible
|
|
2916
2992
|
resolve({ value: 'exit', selectedIndex });
|
|
@@ -2970,6 +3046,11 @@ async function showQuickMenu(items, initialSelectedIndex = 0) {
|
|
|
2970
3046
|
}
|
|
2971
3047
|
|
|
2972
3048
|
async function showProviderManagerMenu() {
|
|
3049
|
+
if (process.env.VCM_DEBUG_MENU === '1') {
|
|
3050
|
+
const msg = `[DEBUG-MENU ${new Date().toISOString()}] showProviderManagerMenu() called\n` + new Error().stack.split('\n').slice(1, 6).join('\n') + '\n';
|
|
3051
|
+
console.log(msg);
|
|
3052
|
+
try { require('fs').appendFileSync('/tmp/vcm-menu-debug.log', msg); } catch (e) { }
|
|
3053
|
+
}
|
|
2973
3054
|
const definitions = getProviderDefinitions();
|
|
2974
3055
|
const defMap = new Map(definitions.map(def => [def.id, def]));
|
|
2975
3056
|
const prefs = await getProviderPreferences();
|
|
@@ -2979,6 +3060,8 @@ async function showProviderManagerMenu() {
|
|
|
2979
3060
|
let dirty = false;
|
|
2980
3061
|
|
|
2981
3062
|
const { fetchQuotaForAgent } = require('vibecodingmachine-core/src/quota-management');
|
|
3063
|
+
const ProviderManager = require('vibecodingmachine-core/src/ide-integration/provider-manager.cjs');
|
|
3064
|
+
const providerManager = new ProviderManager();
|
|
2982
3065
|
|
|
2983
3066
|
const debugQuota = process.env.VCM_DEBUG_QUOTA === '1' || process.env.VCM_DEBUG_QUOTA === 'true';
|
|
2984
3067
|
|
|
@@ -2994,14 +3077,76 @@ async function showProviderManagerMenu() {
|
|
|
2994
3077
|
return `${seconds}s`;
|
|
2995
3078
|
};
|
|
2996
3079
|
|
|
3080
|
+
const formatTimeAmPm = (date) => {
|
|
3081
|
+
const hours24 = date.getHours();
|
|
3082
|
+
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
3083
|
+
const ampm = hours24 >= 12 ? 'pm' : 'am';
|
|
3084
|
+
const hours12 = (hours24 % 12) || 12;
|
|
3085
|
+
return `${hours12}:${minutes} ${ampm}`;
|
|
3086
|
+
};
|
|
3087
|
+
|
|
3088
|
+
|
|
3089
|
+
|
|
3090
|
+
// Pre-fetch data to avoid lag in render loop
|
|
3091
|
+
const sharedAuth = require('vibecodingmachine-core/src/auth/shared-auth-storage');
|
|
3092
|
+
|
|
3093
|
+
// Initialize caches from persistent storage
|
|
3094
|
+
const savedCache = await getProviderCache();
|
|
3095
|
+
|
|
3096
|
+
// Hydrate local maps from saved cache
|
|
3097
|
+
const installationStatus = new Map();
|
|
3098
|
+
const agentQuotas = new Map();
|
|
3099
|
+
|
|
3100
|
+
Object.keys(savedCache).forEach(id => {
|
|
3101
|
+
if (savedCache[id].installed !== undefined) {
|
|
3102
|
+
installationStatus.set(id, savedCache[id].installed);
|
|
3103
|
+
}
|
|
3104
|
+
if (savedCache[id].quota) {
|
|
3105
|
+
agentQuotas.set(id, savedCache[id].quota);
|
|
3106
|
+
}
|
|
3107
|
+
});
|
|
3108
|
+
|
|
3109
|
+
// Also prefill agent quotas from the ProviderManager rate-limit file so the UI can
|
|
3110
|
+
// display `[Resets at ...]` instantly (best-effort, read-only).
|
|
3111
|
+
try {
|
|
3112
|
+
const { getProviderRateLimitedQuotas } = require('./provider-rate-cache');
|
|
3113
|
+
const prefetched = getProviderRateLimitedQuotas(definitions);
|
|
3114
|
+
for (const [id, q] of prefetched) {
|
|
3115
|
+
// Only set if we don't already have a richer quota cached
|
|
3116
|
+
const existing = agentQuotas.get(id);
|
|
3117
|
+
if (!existing || existing.type !== 'rate-limit') {
|
|
3118
|
+
agentQuotas.set(id, q);
|
|
3119
|
+
}
|
|
3120
|
+
}
|
|
3121
|
+
} catch (e) {
|
|
3122
|
+
// Ignore — this is a non-critical optimization
|
|
3123
|
+
}
|
|
3124
|
+
|
|
3125
|
+
// Default values while loading
|
|
3126
|
+
let quotaInfo = { maxIterations: 10, todayUsage: 0 };
|
|
3127
|
+
let autoConfig = {};
|
|
3128
|
+
// Active flag to prevent background render after menu is closed
|
|
3129
|
+
let isMenuActive = true;
|
|
3130
|
+
|
|
3131
|
+
// Load health metrics for all IDEs (load once at startup, not on every render)
|
|
3132
|
+
let healthMetricsMap = new Map();
|
|
3133
|
+
try {
|
|
3134
|
+
const healthTracker = new IDEHealthTracker();
|
|
3135
|
+
await healthTracker.load();
|
|
3136
|
+
healthMetricsMap = await healthTracker.getAllHealthMetrics();
|
|
3137
|
+
} catch (error) {
|
|
3138
|
+
// Silently ignore health loading errors - health display is optional
|
|
3139
|
+
}
|
|
3140
|
+
|
|
3141
|
+
// Clear loading message
|
|
3142
|
+
// process.stdout.write('\r\x1b[K'); // Removed as per diff
|
|
3143
|
+
|
|
2997
3144
|
const render = async () => {
|
|
3145
|
+
if (!isMenuActive) return; // Prevent rendering after menu closed
|
|
2998
3146
|
process.stdout.write('\x1Bc');
|
|
2999
3147
|
console.log(chalk.bold.cyan('⚙ ' + t('provider.title') + '\n'));
|
|
3000
3148
|
|
|
3001
|
-
//
|
|
3002
|
-
const sharedAuth = require('vibecodingmachine-core/src/auth/shared-auth-storage');
|
|
3003
|
-
const autoConfig = await getAutoConfig();
|
|
3004
|
-
const quotaInfo = await sharedAuth.canRunAutoMode();
|
|
3149
|
+
// Use cached quota info
|
|
3005
3150
|
const remaining = Math.max(0, (quotaInfo.maxIterations || 10) - (quotaInfo.todayUsage || 0));
|
|
3006
3151
|
|
|
3007
3152
|
// Calculate time until reset (midnight)
|
|
@@ -3012,15 +3157,65 @@ async function showProviderManagerMenu() {
|
|
|
3012
3157
|
const hoursUntilReset = Math.floor(msUntilReset / (1000 * 60 * 60));
|
|
3013
3158
|
const minsUntilReset = Math.floor((msUntilReset % (1000 * 60 * 60)) / (1000 * 60));
|
|
3014
3159
|
|
|
3015
|
-
// Display quota
|
|
3160
|
+
// Display quota, accounting for per-agent rate limits to avoid showing "10/10" when some
|
|
3161
|
+
// providers are rate-limited. We avoid surfacing agent-specific "[Resets at ...]" labels in
|
|
3162
|
+
// the top-level overall quota row and instead show a compact partial-availability message.
|
|
3016
3163
|
let quotaDisplay;
|
|
3164
|
+
|
|
3165
|
+
// Detect how many agents are currently rate-limited
|
|
3166
|
+
const rateLimitedAgents = Array.from(agentQuotas.values()).filter(q => {
|
|
3167
|
+
if (!q) return false;
|
|
3168
|
+
if (q.type !== 'rate-limit') return false;
|
|
3169
|
+
if (typeof q.isExceeded === 'function') return q.isExceeded();
|
|
3170
|
+
if (q.remaining !== undefined) return q.remaining <= 0;
|
|
3171
|
+
return false;
|
|
3172
|
+
});
|
|
3173
|
+
const rateLimitedCount = rateLimitedAgents.length;
|
|
3174
|
+
|
|
3017
3175
|
if (remaining === 0) {
|
|
3018
|
-
//
|
|
3176
|
+
// Overall rate limit active - show when it resets (in red)
|
|
3019
3177
|
quotaDisplay = chalk.gray(' ' + t('provider.overall.quota') + ': ') + chalk.red(`⏰ ${t('provider.rate.limit.resets')} ${hoursUntilReset}h ${minsUntilReset}m`);
|
|
3178
|
+
} else if (rateLimitedCount > 0) {
|
|
3179
|
+
// Some providers are rate-limited — show partial availability instead of full 10/10
|
|
3180
|
+
const effectiveRemaining = Math.max(0, remaining - rateLimitedCount);
|
|
3181
|
+
|
|
3182
|
+
// If one or more rate-limited agents provide a specific reset time, show that instead
|
|
3183
|
+
// of a generic "Resets in Xh Ym" message to match per-agent rows (e.g., "[Resets at 5:00 pm]").
|
|
3184
|
+
const earliestReset = rateLimitedAgents.reduce((min, q) => {
|
|
3185
|
+
if (!q || !q.resetsAt) return min;
|
|
3186
|
+
try {
|
|
3187
|
+
const d = new Date(q.resetsAt);
|
|
3188
|
+
if (Number.isNaN(d.getTime())) return min;
|
|
3189
|
+
if (!min) return q.resetsAt;
|
|
3190
|
+
return d.getTime() < new Date(min).getTime() ? q.resetsAt : min;
|
|
3191
|
+
} catch (e) {
|
|
3192
|
+
return min;
|
|
3193
|
+
}
|
|
3194
|
+
}, null);
|
|
3195
|
+
|
|
3196
|
+
const resetLabel = earliestReset ? formatResetsAtLabel(earliestReset) : null;
|
|
3197
|
+
|
|
3198
|
+
quotaDisplay = chalk.gray(' ' + t('provider.overall.quota') + ': ') + chalk.yellow(`⚠️ ${t('provider.available')} (${effectiveRemaining}/${quotaInfo.maxIterations})`) + chalk.gray(` • ${rateLimitedCount} provider${rateLimitedCount > 1 ? 's' : ''} limited`);
|
|
3199
|
+
|
|
3200
|
+
if (resetLabel) {
|
|
3201
|
+
quotaDisplay += ' ' + chalk.red(`[${resetLabel}]`);
|
|
3202
|
+
} else {
|
|
3203
|
+
quotaDisplay += chalk.gray(' • ' + t('provider.resets.in') + ' ') + chalk.cyan(`${hoursUntilReset}h ${minsUntilReset}m`);
|
|
3204
|
+
}
|
|
3020
3205
|
} else {
|
|
3021
3206
|
// Quota available - show when it resets (in green)
|
|
3022
|
-
|
|
3207
|
+
// If the quota is full (no usage), don't show the reset time — it's unnecessary and
|
|
3208
|
+
// can be confusing when nothing is rate-limited.
|
|
3209
|
+
const fullQuota = remaining === (quotaInfo.maxIterations || 10);
|
|
3210
|
+
quotaDisplay = chalk.gray(' ' + t('provider.overall.quota') + ': ') + chalk.green(`✓ ${t('provider.available')} (${remaining}/${quotaInfo.maxIterations})`);
|
|
3211
|
+
if (!fullQuota) {
|
|
3212
|
+
quotaDisplay += chalk.gray(' • ' + t('provider.resets.in') + ' ') + chalk.cyan(`${hoursUntilReset}h ${minsUntilReset}m`);
|
|
3213
|
+
}
|
|
3023
3214
|
}
|
|
3215
|
+
|
|
3216
|
+
// Display Legend
|
|
3217
|
+
console.log(chalk.gray(' Interface types: 🖥️ GUI Automation ⚡ Direct Automation'));
|
|
3218
|
+
console.log(chalk.gray(' Provider type: ☁️ Cloud 🏠 Local (Slow, Requires lots of memory and CPU, Private)'));
|
|
3024
3219
|
console.log(quotaDisplay);
|
|
3025
3220
|
console.log(chalk.gray(' ' + t('provider.instructions') + '\n'));
|
|
3026
3221
|
|
|
@@ -3031,91 +3226,137 @@ async function showProviderManagerMenu() {
|
|
|
3031
3226
|
const isSelected = idx === selectedIndex;
|
|
3032
3227
|
const isEnabled = enabled[id] !== false;
|
|
3033
3228
|
|
|
3034
|
-
//
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
} else if (id === 'vscode') {
|
|
3046
|
-
isInstalled = await checkAppOrBinary(['Visual Studio Code', 'Visual Studio Code - Insiders'], ['code', 'code-insiders']);
|
|
3047
|
-
} else if (id === 'antigravity') {
|
|
3048
|
-
isInstalled = await checkAppOrBinary(['Antigravity'], ['antigravity']);
|
|
3049
|
-
} else {
|
|
3050
|
-
// For other IDEs, try binary name same as id
|
|
3051
|
-
isInstalled = await checkAppOrBinary([], [id]);
|
|
3052
|
-
}
|
|
3053
|
-
} catch (e) {
|
|
3054
|
-
// If detection fails, assume not installed
|
|
3055
|
-
isInstalled = false;
|
|
3056
|
-
}
|
|
3057
|
-
}
|
|
3229
|
+
// Use cached installation status (default to false if not found in cache)
|
|
3230
|
+
// For LLMs, we consider them always "installed" (cloud)
|
|
3231
|
+
let isInstalled = def.type === 'ide' ? (installationStatus.get(id) === true) : true;
|
|
3232
|
+
let quota = agentQuotas.get(id);
|
|
3233
|
+
|
|
3234
|
+
// Determine status emoji based on user requirements:
|
|
3235
|
+
// ⬜ (Grey Square): Not installed
|
|
3236
|
+
// ⚠️ (Yellow Triangle): Installed but (Quota limit OR Not verified)
|
|
3237
|
+
// 🚫 (Red Circle): Disabled
|
|
3238
|
+
// 🛑 (Red Stop Sign): Not working/Error
|
|
3239
|
+
// 🟢 (Green Circle): Available/Working
|
|
3058
3240
|
|
|
3059
|
-
// Determine status emoji: disabled = red alert, not installed = yellow, enabled = green
|
|
3060
3241
|
let statusEmoji;
|
|
3242
|
+
let statusText = '';
|
|
3243
|
+
|
|
3061
3244
|
if (!isEnabled) {
|
|
3062
|
-
statusEmoji = '
|
|
3063
|
-
} else if (
|
|
3064
|
-
statusEmoji = '
|
|
3245
|
+
statusEmoji = '🚫'; // Disabled
|
|
3246
|
+
} else if (!isInstalled) {
|
|
3247
|
+
statusEmoji = '⬜'; // Not installed
|
|
3065
3248
|
} else {
|
|
3066
|
-
|
|
3067
|
-
|
|
3249
|
+
// Installed and Enabled, assume Green unless quota or error
|
|
3250
|
+
statusEmoji = '🟢';
|
|
3068
3251
|
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3252
|
+
if (def.type === 'ide') {
|
|
3253
|
+
// TODO: Add actual "verified" check if available. For now assuming installed = verified.
|
|
3254
|
+
// If we had a verification check:
|
|
3255
|
+
// if (!isVerified) statusEmoji = '⚠️';
|
|
3256
|
+
}
|
|
3072
3257
|
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3258
|
+
// Check Quota
|
|
3259
|
+
if (quota) {
|
|
3260
|
+
// Handle both Quota class instances and plain JSON objects from cache
|
|
3261
|
+
let isExceeded = false;
|
|
3262
|
+
if (typeof quota.isExceeded === 'function') {
|
|
3263
|
+
isExceeded = quota.isExceeded();
|
|
3264
|
+
} else if (quota.remaining !== undefined) {
|
|
3265
|
+
isExceeded = quota.remaining <= 0;
|
|
3266
|
+
}
|
|
3267
|
+
|
|
3268
|
+
if (quota.type === 'rate-limit' && isExceeded) {
|
|
3269
|
+
statusEmoji = '⚠️ ';
|
|
3270
|
+
if (quota.resetsAt) {
|
|
3271
|
+
const label = formatResetsAtLabel(quota.resetsAt);
|
|
3272
|
+
statusText = label ? ` ${chalk.red(`⏰ ${label}`)}` : ` ${chalk.red('[Rate limited]')}`;
|
|
3273
|
+
} else {
|
|
3274
|
+
statusText = ` ${chalk.red('[Rate limited]')}`;
|
|
3275
|
+
}
|
|
3276
|
+
}
|
|
3084
3277
|
}
|
|
3278
|
+
}
|
|
3085
3279
|
|
|
3086
|
-
|
|
3087
|
-
|
|
3280
|
+
// Determine Interface Type Icon (GUI vs Direct Automation)
|
|
3281
|
+
let interfaceIcon = '⚡'; // Default to Direct Automation
|
|
3282
|
+
if (def.type === 'ide') {
|
|
3283
|
+
interfaceIcon = '🖥️'; // GUI Automation for IDEs
|
|
3284
|
+
}
|
|
3088
3285
|
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3286
|
+
// Determine Provider Type Icon (Cloud vs Local)
|
|
3287
|
+
let providerTypeIcon = '☁️'; // Default to Cloud (IDEs and most LLMs)
|
|
3288
|
+
if (def.type === 'direct' && def.category === 'llm' && def.id === 'ollama') {
|
|
3289
|
+
providerTypeIcon = '🏠'; // Local (only for Ollama)
|
|
3290
|
+
}
|
|
3291
|
+
|
|
3292
|
+
// Trim emojis to remove any hidden characters and ensure clean spacing
|
|
3293
|
+
interfaceIcon = interfaceIcon.trim();
|
|
3294
|
+
providerTypeIcon = providerTypeIcon.trim();
|
|
3295
|
+
const isWarningEmoji = statusEmoji.includes('⚠️');
|
|
3296
|
+
statusEmoji = statusEmoji.trim();
|
|
3297
|
+
|
|
3298
|
+
// Determine spacing based on interface icon type and provider type
|
|
3299
|
+
// Multi-char icons (🖥️) need double spaces, single-char icons (⚡) use single space after interface icon
|
|
3300
|
+
const isMultiCharInterface = interfaceIcon.length > 1;
|
|
3301
|
+
const spaceAfterInterface = isMultiCharInterface ? ' ' : ' ';
|
|
3302
|
+
// Local provider (🏠) uses single space, others use double space after provider icon
|
|
3303
|
+
const isLocalProvider = providerTypeIcon === '🏠';
|
|
3304
|
+
const spaceAfterProvider = isLocalProvider ? ' ' : ' ';
|
|
3305
|
+
|
|
3306
|
+
// Adjust spacing after status emoji - warning emoji needs extra space
|
|
3307
|
+
const spaceAfterStatus = isWarningEmoji ? ' ' : ' ';
|
|
3308
|
+
|
|
3309
|
+
const prefix = isSelected ? chalk.cyan('❯') + ' ' : ' ';
|
|
3310
|
+
|
|
3311
|
+
// Get health metrics for this IDE (if available)
|
|
3312
|
+
let healthCounters = '';
|
|
3313
|
+
const ideMetrics = healthMetricsMap.get(def.id);
|
|
3314
|
+
if (ideMetrics) {
|
|
3315
|
+
const counters = HealthReporter.formatInlineCounters(ideMetrics);
|
|
3316
|
+
if (counters) {
|
|
3317
|
+
// Color code the counters: green for +N, red for -M
|
|
3318
|
+
const coloredCounters = counters
|
|
3319
|
+
.split(' ')
|
|
3320
|
+
.map(part => part.startsWith('+') ? chalk.green(part) : chalk.red(part))
|
|
3321
|
+
.join(' ');
|
|
3322
|
+
healthCounters = ` ${coloredCounters}`;
|
|
3092
3323
|
}
|
|
3324
|
+
}
|
|
3093
3325
|
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3326
|
+
// Status emoji is now first, followed by interface icon and provider type icon
|
|
3327
|
+
let line = `${prefix}${statusEmoji}${spaceAfterStatus}${interfaceIcon}${spaceAfterInterface}${providerTypeIcon}${spaceAfterProvider} ${idx + 1}. ${def.name} ${chalk.gray(`(${def.id})`)}${healthCounters}${statusText}`;
|
|
3328
|
+
if (debugQuota && quota) {
|
|
3329
|
+
// Append extra debug info if needed
|
|
3330
|
+
const resetMs = quota?.resetsAt ? (new Date(quota.resetsAt).getTime() - Date.now()) : null;
|
|
3331
|
+
line += chalk.gray(` [D: type=${quota?.type} rem=${quota?.remaining} lim=${quota?.limit} resetIn=${resetMs !== null ? formatDuration(resetMs) : 'null'}]`);
|
|
3332
|
+
}
|
|
3333
|
+
console.log(line);
|
|
3334
|
+
|
|
3335
|
+
if (Array.isArray(def.subAgents) && def.subAgents.length > 0) {
|
|
3336
|
+
for (const sub of def.subAgents) {
|
|
3337
|
+
const info = providerManager.getRateLimitInfo(def.id, sub.model);
|
|
3338
|
+
let subStatusEmoji;
|
|
3339
|
+
let subStatusText = '';
|
|
3340
|
+
|
|
3341
|
+
if (!isEnabled) {
|
|
3342
|
+
subStatusEmoji = '🚫';
|
|
3343
|
+
} else if (!isInstalled) {
|
|
3344
|
+
subStatusEmoji = '⬜';
|
|
3345
|
+
} else if (info && info.isRateLimited) {
|
|
3346
|
+
subStatusEmoji = '⚠️';
|
|
3347
|
+
if (info.resetTime) {
|
|
3348
|
+
const label = formatResetsAtLabel(info.resetTime);
|
|
3349
|
+
subStatusText = label ? ` ${chalk.red(`⏰ ${label}`)}` : ` ${chalk.red('[Rate limited]')}`;
|
|
3101
3350
|
} else {
|
|
3102
|
-
|
|
3351
|
+
subStatusText = ` ${chalk.red('[Rate limited]')}`;
|
|
3103
3352
|
}
|
|
3104
3353
|
} else {
|
|
3105
|
-
|
|
3106
|
-
if (quota.resetsAt) {
|
|
3107
|
-
const msUntilReset = new Date(quota.resetsAt).getTime() - Date.now();
|
|
3108
|
-
line += ` ${chalk.green(`[✓ ${t('provider.status.available.resets')} ${formatDuration(msUntilReset)}]`)}`;
|
|
3109
|
-
} else {
|
|
3110
|
-
line += ` ${chalk.green('[' + t('provider.status.available') + ']')}`;
|
|
3111
|
-
}
|
|
3354
|
+
subStatusEmoji = '🟢';
|
|
3112
3355
|
}
|
|
3356
|
+
|
|
3357
|
+
console.log(` ${subStatusEmoji} ${chalk.gray('↳')} ${sub.name} ${chalk.gray(`(${sub.model})`)}${subStatusText}`);
|
|
3113
3358
|
}
|
|
3114
|
-
} catch (e) {
|
|
3115
|
-
// Silently skip if quota fetch fails
|
|
3116
3359
|
}
|
|
3117
|
-
|
|
3118
|
-
console.log(line);
|
|
3119
3360
|
}
|
|
3120
3361
|
|
|
3121
3362
|
console.log();
|
|
@@ -3126,6 +3367,143 @@ async function showProviderManagerMenu() {
|
|
|
3126
3367
|
}
|
|
3127
3368
|
};
|
|
3128
3369
|
|
|
3370
|
+
// Background loading function
|
|
3371
|
+
const loadDataInBackground = async () => {
|
|
3372
|
+
try {
|
|
3373
|
+
// 1. Load config and auth info
|
|
3374
|
+
const [config, quota] = await Promise.all([
|
|
3375
|
+
getAutoConfig(),
|
|
3376
|
+
sharedAuth.canRunAutoMode()
|
|
3377
|
+
]);
|
|
3378
|
+
autoConfig = config;
|
|
3379
|
+
quotaInfo = quota;
|
|
3380
|
+
if (typeof render === 'function') await render();
|
|
3381
|
+
|
|
3382
|
+
// 2. Check installation status in parallel
|
|
3383
|
+
const installPromises = definitions.map(async (def) => {
|
|
3384
|
+
let isInstalled = true;
|
|
3385
|
+
if (def.type === 'ide') {
|
|
3386
|
+
try {
|
|
3387
|
+
if (def.id === 'kiro') {
|
|
3388
|
+
const { isKiroInstalled } = require('./kiro-installer');
|
|
3389
|
+
isInstalled = isKiroInstalled();
|
|
3390
|
+
} else if (def.id === 'cursor') {
|
|
3391
|
+
isInstalled = await checkAppOrBinary(['Cursor'], ['cursor']);
|
|
3392
|
+
} else if (def.id === 'windsurf') {
|
|
3393
|
+
isInstalled = await checkAppOrBinary(['Windsurf'], ['windsurf']);
|
|
3394
|
+
} else if (def.id === 'vscode') {
|
|
3395
|
+
isInstalled = await checkAppOrBinary(['Visual Studio Code', 'Visual Studio Code - Insiders'], ['code', 'code-insiders']);
|
|
3396
|
+
} else if (def.id === 'github-copilot' || def.id === 'amazon-q') {
|
|
3397
|
+
// Check if VS Code is installed AND the extension is installed
|
|
3398
|
+
const vscodeInstalled = await checkAppOrBinary(['Visual Studio Code', 'Visual Studio Code - Insiders'], ['code', 'code-insiders']);
|
|
3399
|
+
if (vscodeInstalled) {
|
|
3400
|
+
isInstalled = await checkVSCodeExtension(def.id);
|
|
3401
|
+
} else {
|
|
3402
|
+
isInstalled = false;
|
|
3403
|
+
}
|
|
3404
|
+
} else if (def.id === 'replit') {
|
|
3405
|
+
// Replit is web-based, always considered "installed"
|
|
3406
|
+
isInstalled = true;
|
|
3407
|
+
} else if (def.id === 'antigravity') {
|
|
3408
|
+
isInstalled = await checkAppOrBinary(['Antigravity'], ['antigravity']);
|
|
3409
|
+
} else {
|
|
3410
|
+
isInstalled = await checkAppOrBinary([], [def.id]);
|
|
3411
|
+
}
|
|
3412
|
+
} catch (e) {
|
|
3413
|
+
isInstalled = false;
|
|
3414
|
+
}
|
|
3415
|
+
}
|
|
3416
|
+
installationStatus.set(def.id, isInstalled);
|
|
3417
|
+
return { id: def.id, installed: isInstalled };
|
|
3418
|
+
});
|
|
3419
|
+
|
|
3420
|
+
const installResults = await Promise.all(installPromises);
|
|
3421
|
+
|
|
3422
|
+
// Update cache
|
|
3423
|
+
let newCache = {};
|
|
3424
|
+
installResults.forEach(r => {
|
|
3425
|
+
newCache[r.id] = { ...(savedCache[r.id] || {}), installed: r.installed };
|
|
3426
|
+
});
|
|
3427
|
+
await setProviderCache(newCache);
|
|
3428
|
+
|
|
3429
|
+
if (typeof render === 'function') await render();
|
|
3430
|
+
|
|
3431
|
+
// 3. Check quotas in parallel
|
|
3432
|
+
const quotaPromises = definitions.map(async (def) => {
|
|
3433
|
+
try {
|
|
3434
|
+
let model = def.defaultModel || def.id;
|
|
3435
|
+
if (def.id === 'groq') model = autoConfig.groqModel || model;
|
|
3436
|
+
else if (def.id === 'anthropic') model = autoConfig.anthropicModel || model;
|
|
3437
|
+
else if (def.id === 'ollama') {
|
|
3438
|
+
const preferredModel = autoConfig.llmModel && autoConfig.llmModel.includes('ollama/')
|
|
3439
|
+
? autoConfig.llmModel.split('/')[1]
|
|
3440
|
+
: autoConfig.llmModel || autoConfig.aiderModel;
|
|
3441
|
+
model = (preferredModel && preferredModel !== def.id) ? preferredModel : model;
|
|
3442
|
+
}
|
|
3443
|
+
|
|
3444
|
+
const agentId = `${def.id}:${model}`;
|
|
3445
|
+
const quota = await fetchQuotaForAgent(agentId);
|
|
3446
|
+
|
|
3447
|
+
// Merge cautiously: if we already have an active rate-limit prefetched from
|
|
3448
|
+
// ProviderManager, avoid overwriting it with a non-rate-limit result which
|
|
3449
|
+
// could make the UI lose the reset information briefly. If both are rate-limits,
|
|
3450
|
+
// prefer the earliest reset time.
|
|
3451
|
+
try {
|
|
3452
|
+
const existing = agentQuotas.get(def.id);
|
|
3453
|
+
const existingIsActiveRateLimit = existing && existing.type === 'rate-limit' && new Date(existing.resetsAt).getTime() > Date.now();
|
|
3454
|
+
const newIsRateLimit = quota && quota.type === 'rate-limit';
|
|
3455
|
+
|
|
3456
|
+
if (existingIsActiveRateLimit && !newIsRateLimit) {
|
|
3457
|
+
// Keep the existing active rate-limit and don't overwrite
|
|
3458
|
+
} else if (existingIsActiveRateLimit && newIsRateLimit) {
|
|
3459
|
+
// Both rate-limited: keep the one with the earliest reset
|
|
3460
|
+
const existingReset = new Date(existing.resetsAt).getTime();
|
|
3461
|
+
const newReset = quota && quota.resetsAt ? new Date(quota.resetsAt).getTime() : null;
|
|
3462
|
+
if (!newReset || existingReset <= newReset) {
|
|
3463
|
+
// keep existing
|
|
3464
|
+
} else {
|
|
3465
|
+
agentQuotas.set(def.id, quota);
|
|
3466
|
+
}
|
|
3467
|
+
} else {
|
|
3468
|
+
// No active existing rate limit — set/overwrite normally
|
|
3469
|
+
agentQuotas.set(def.id, quota);
|
|
3470
|
+
}
|
|
3471
|
+
} catch (e) {
|
|
3472
|
+
// On any error, set the quota to avoid leaving stale state
|
|
3473
|
+
agentQuotas.set(def.id, quota);
|
|
3474
|
+
}
|
|
3475
|
+
|
|
3476
|
+
return { id: def.id, quota };
|
|
3477
|
+
} catch (e) {
|
|
3478
|
+
return null;
|
|
3479
|
+
}
|
|
3480
|
+
});
|
|
3481
|
+
|
|
3482
|
+
const quotaResults = await Promise.all(quotaPromises);
|
|
3483
|
+
|
|
3484
|
+
// Update cache with quotas
|
|
3485
|
+
newCache = {};
|
|
3486
|
+
quotaResults.forEach(r => {
|
|
3487
|
+
if (r) {
|
|
3488
|
+
newCache[r.id] = { ...(savedCache[r.id] || {}), quota: r.quota };
|
|
3489
|
+
// Preserve installed status if we just set it
|
|
3490
|
+
if (installationStatus.has(r.id)) {
|
|
3491
|
+
newCache[r.id].installed = installationStatus.get(r.id);
|
|
3492
|
+
}
|
|
3493
|
+
}
|
|
3494
|
+
});
|
|
3495
|
+
await setProviderCache(newCache);
|
|
3496
|
+
|
|
3497
|
+
if (typeof render === 'function') await render();
|
|
3498
|
+
|
|
3499
|
+
} catch (error) {
|
|
3500
|
+
// Silently fail in background
|
|
3501
|
+
}
|
|
3502
|
+
};
|
|
3503
|
+
|
|
3504
|
+
// Start background loading
|
|
3505
|
+
loadDataInBackground();
|
|
3506
|
+
|
|
3129
3507
|
if (process.env.VCM_RENDER_ONCE === '1' || process.env.VCM_RENDER_ONCE === 'true') {
|
|
3130
3508
|
await render();
|
|
3131
3509
|
return;
|
|
@@ -3133,6 +3511,8 @@ async function showProviderManagerMenu() {
|
|
|
3133
3511
|
|
|
3134
3512
|
return new Promise(async (resolve) => {
|
|
3135
3513
|
const cleanup = () => {
|
|
3514
|
+
// Mark menu as inactive so background loaders stop rendering
|
|
3515
|
+
isMenuActive = false;
|
|
3136
3516
|
if (process.stdin.isTTY && process.stdin.setRawMode) {
|
|
3137
3517
|
process.stdin.setRawMode(false);
|
|
3138
3518
|
}
|
|
@@ -3169,6 +3549,13 @@ async function showProviderManagerMenu() {
|
|
|
3169
3549
|
};
|
|
3170
3550
|
|
|
3171
3551
|
const cancel = () => {
|
|
3552
|
+
// Suppress input for a short period to avoid buffered key repeats reopening menus
|
|
3553
|
+
menuSuppressUntil = Date.now() + 1000; // 1 second
|
|
3554
|
+
if (process.env.VCM_DEBUG_MENU === '1') {
|
|
3555
|
+
const msg = `[DEBUG-MENU ${new Date().toISOString()}] menu cancelled; suppress until ${new Date(menuSuppressUntil).toISOString()}\n`;
|
|
3556
|
+
console.log(msg);
|
|
3557
|
+
try { require('fs').appendFileSync('/tmp/vcm-menu-debug.log', msg); } catch (e) { }
|
|
3558
|
+
}
|
|
3172
3559
|
cleanup();
|
|
3173
3560
|
console.log('\n');
|
|
3174
3561
|
resolve(null);
|
|
@@ -3200,12 +3587,104 @@ async function showProviderManagerMenu() {
|
|
|
3200
3587
|
await render();
|
|
3201
3588
|
};
|
|
3202
3589
|
|
|
3590
|
+
const installSelected = async () => {
|
|
3591
|
+
const id = order[selectedIndex];
|
|
3592
|
+
const def = defMap.get(id);
|
|
3593
|
+
|
|
3594
|
+
if (!def) return;
|
|
3595
|
+
|
|
3596
|
+
// Only IDE providers can be installed
|
|
3597
|
+
if (def.type !== 'ide') {
|
|
3598
|
+
console.log(chalk.yellow('\n⚠️ Installation is only available for IDE providers.\n'));
|
|
3599
|
+
await render();
|
|
3600
|
+
return;
|
|
3601
|
+
}
|
|
3602
|
+
|
|
3603
|
+
// Check if already installed
|
|
3604
|
+
const isInstalled = installationStatus.get(id);
|
|
3605
|
+
if (isInstalled) {
|
|
3606
|
+
console.log(chalk.yellow(`\n⚠️ ${def.name} is already installed.\n`));
|
|
3607
|
+
await render();
|
|
3608
|
+
return;
|
|
3609
|
+
}
|
|
3610
|
+
|
|
3611
|
+
console.log(chalk.cyan(`\n🔧 Installing ${def.name}...\n`));
|
|
3612
|
+
|
|
3613
|
+
try {
|
|
3614
|
+
const appleScriptManager = new AppleScriptManager();
|
|
3615
|
+
const repoPath = await getRepoPath();
|
|
3616
|
+
|
|
3617
|
+
// Use openIDE method which handles installation
|
|
3618
|
+
await appleScriptManager.openIDE(id, repoPath);
|
|
3619
|
+
|
|
3620
|
+
// Re-check installation status
|
|
3621
|
+
let newStatus = true;
|
|
3622
|
+
try {
|
|
3623
|
+
if (id === 'kiro') {
|
|
3624
|
+
const { isKiroInstalled } = require('./kiro-installer');
|
|
3625
|
+
newStatus = isKiroInstalled();
|
|
3626
|
+
} else if (id === 'cursor') {
|
|
3627
|
+
newStatus = await checkAppOrBinary(['Cursor'], ['cursor']);
|
|
3628
|
+
} else if (id === 'windsurf') {
|
|
3629
|
+
newStatus = await checkAppOrBinary(['Windsurf'], ['windsurf']);
|
|
3630
|
+
} else if (id === 'vscode') {
|
|
3631
|
+
newStatus = await checkAppOrBinary(['Visual Studio Code', 'Visual Studio Code - Insiders'], ['code', 'code-insiders']);
|
|
3632
|
+
} else if (id === 'github-copilot' || id === 'amazon-q') {
|
|
3633
|
+
// Check if VS Code is installed AND the extension is installed
|
|
3634
|
+
const vscodeInstalled = await checkAppOrBinary(['Visual Studio Code', 'Visual Studio Code - Insiders'], ['code', 'code-insiders']);
|
|
3635
|
+
if (vscodeInstalled) {
|
|
3636
|
+
newStatus = await checkVSCodeExtension(id);
|
|
3637
|
+
} else {
|
|
3638
|
+
newStatus = false;
|
|
3639
|
+
}
|
|
3640
|
+
} else if (id === 'antigravity') {
|
|
3641
|
+
newStatus = await checkAppOrBinary(['Antigravity'], ['antigravity']);
|
|
3642
|
+
} else if (id === 'replit') {
|
|
3643
|
+
// Replit is web-based, always considered "installed"
|
|
3644
|
+
newStatus = true;
|
|
3645
|
+
} else {
|
|
3646
|
+
newStatus = await checkAppOrBinary([], [id]);
|
|
3647
|
+
}
|
|
3648
|
+
} catch (e) {
|
|
3649
|
+
newStatus = false;
|
|
3650
|
+
}
|
|
3651
|
+
|
|
3652
|
+
installationStatus.set(id, newStatus);
|
|
3653
|
+
|
|
3654
|
+
// Update cache
|
|
3655
|
+
const savedCache = await getProviderCache();
|
|
3656
|
+
const newCache = {
|
|
3657
|
+
...savedCache,
|
|
3658
|
+
[id]: { ...(savedCache[id] || {}), installed: newStatus }
|
|
3659
|
+
};
|
|
3660
|
+
await setProviderCache(newCache);
|
|
3661
|
+
|
|
3662
|
+
if (newStatus) {
|
|
3663
|
+
console.log(chalk.green(`\n✓ ${def.name} installed successfully!\n`));
|
|
3664
|
+
} else {
|
|
3665
|
+
console.log(chalk.yellow(`\n⚠️ Installation may have completed, but ${def.name} was not detected. Please check manually.\n`));
|
|
3666
|
+
}
|
|
3667
|
+
} catch (error) {
|
|
3668
|
+
console.log(chalk.red(`\n✗ Installation failed: ${error.message}\n`));
|
|
3669
|
+
}
|
|
3670
|
+
|
|
3671
|
+
await render();
|
|
3672
|
+
};
|
|
3673
|
+
|
|
3674
|
+
// Ignore spurious keypresses for a short debounce window after opening the menu
|
|
3675
|
+
const IGNORE_INPUT_MS = 300; // milliseconds
|
|
3676
|
+
let ignoreInputUntil = Date.now() + IGNORE_INPUT_MS;
|
|
3677
|
+
|
|
3203
3678
|
const onKeypress = async (str, key = {}) => {
|
|
3679
|
+
// Allow immediate Ctrl+C regardless of debounce
|
|
3204
3680
|
if (key.ctrl && key.name === 'c') {
|
|
3205
3681
|
cancel();
|
|
3206
3682
|
return;
|
|
3207
3683
|
}
|
|
3208
3684
|
|
|
3685
|
+
// Debounce: ignore inputs that happen too soon after the menu opens
|
|
3686
|
+
if (Date.now() < ignoreInputUntil) return;
|
|
3687
|
+
|
|
3209
3688
|
switch (key.name) {
|
|
3210
3689
|
case 'up':
|
|
3211
3690
|
await moveSelection(-1);
|
|
@@ -3225,15 +3704,22 @@ async function showProviderManagerMenu() {
|
|
|
3225
3704
|
case 'd':
|
|
3226
3705
|
await toggle(false);
|
|
3227
3706
|
break;
|
|
3707
|
+
case 'i':
|
|
3708
|
+
await installSelected();
|
|
3709
|
+
break;
|
|
3228
3710
|
case 'space':
|
|
3229
3711
|
await toggle(!(enabled[order[selectedIndex]] !== false));
|
|
3230
3712
|
break;
|
|
3231
3713
|
case 'return':
|
|
3232
3714
|
saveAndExit(order[selectedIndex]);
|
|
3233
3715
|
break;
|
|
3234
|
-
case 'escape':
|
|
3235
3716
|
case 'left':
|
|
3717
|
+
// Left arrow: save any pending changes and exit (match Enter's save behavior for order)
|
|
3718
|
+
saveAndExit();
|
|
3719
|
+
break;
|
|
3720
|
+
case 'escape':
|
|
3236
3721
|
case 'x':
|
|
3722
|
+
// Escape or 'x' should cancel without persisting pending changes
|
|
3237
3723
|
cancel();
|
|
3238
3724
|
break;
|
|
3239
3725
|
default:
|
|
@@ -3252,23 +3738,54 @@ async function showProviderManagerMenu() {
|
|
|
3252
3738
|
});
|
|
3253
3739
|
}
|
|
3254
3740
|
|
|
3255
|
-
|
|
3256
|
-
|
|
3741
|
+
/**
|
|
3742
|
+
* Get health metrics for IDE choices
|
|
3743
|
+
*/
|
|
3744
|
+
async function getIDEHealthMetrics() {
|
|
3745
|
+
try {
|
|
3746
|
+
const healthTracker = new IDEHealthTracker();
|
|
3747
|
+
const allMetrics = await healthTracker.getAllHealthMetrics();
|
|
3748
|
+
return allMetrics;
|
|
3749
|
+
} catch (error) {
|
|
3750
|
+
// Health tracking not available, return empty metrics
|
|
3751
|
+
return new Map();
|
|
3752
|
+
}
|
|
3753
|
+
}
|
|
3754
|
+
|
|
3755
|
+
/**
|
|
3756
|
+
* Format IDE name with health metrics
|
|
3757
|
+
*/
|
|
3758
|
+
async function formatIDEChoiceWithHealth(ideName, ideValue, allMetrics) {
|
|
3759
|
+
const metrics = allMetrics.get(ideValue);
|
|
3760
|
+
|
|
3761
|
+
if (!metrics || metrics.totalInteractions === 0) {
|
|
3762
|
+
return ideName; // No health data available
|
|
3763
|
+
}
|
|
3764
|
+
|
|
3765
|
+
const successCount = metrics.successCount || 0;
|
|
3766
|
+
const failureCount = metrics.failureCount || 0;
|
|
3767
|
+
const healthIndicator = metrics.consecutiveFailures === 0 ? '✅' : '⚠️';
|
|
3768
|
+
|
|
3769
|
+
return `${ideName} ${healthIndicator} +${successCount} -${failureCount}`;
|
|
3770
|
+
}
|
|
3257
3771
|
|
|
3772
|
+
async function showSettings() {
|
|
3773
|
+
console.log(chalk.bold.cyan('\n⚙️ Settings\n'));
|
|
3774
|
+
|
|
3258
3775
|
const { setConfigValue } = require('vibecodingmachine-core');
|
|
3259
3776
|
const { getAutoConfig, setAutoConfig } = require('./config');
|
|
3260
3777
|
const currentHostnameEnabled = await isComputerNameEnabled();
|
|
3261
3778
|
const hostname = getHostname();
|
|
3262
3779
|
const autoConfig = await getAutoConfig();
|
|
3263
3780
|
const currentIDE = autoConfig.ide || 'claude-code';
|
|
3264
|
-
|
|
3781
|
+
|
|
3265
3782
|
// Show current settings
|
|
3266
3783
|
console.log(chalk.gray('Current settings:'));
|
|
3267
3784
|
console.log(chalk.gray(' Computer Name: '), chalk.cyan(hostname));
|
|
3268
3785
|
console.log(chalk.gray(' Current IDE: '), chalk.cyan(formatIDEName(currentIDE)));
|
|
3269
3786
|
console.log(chalk.gray(' Use Hostname in Req File: '), currentHostnameEnabled ? chalk.green('Enabled ✓') : chalk.yellow('Disabled ○'));
|
|
3270
3787
|
console.log();
|
|
3271
|
-
|
|
3788
|
+
|
|
3272
3789
|
const { action } = await inquirer.prompt([
|
|
3273
3790
|
{
|
|
3274
3791
|
type: 'list',
|
|
@@ -3290,54 +3807,59 @@ async function showProviderManagerMenu() {
|
|
|
3290
3807
|
]
|
|
3291
3808
|
}
|
|
3292
3809
|
]);
|
|
3810
|
+
|
|
3811
|
+
/**
|
|
3812
|
+
* Get health metrics for IDE choices
|
|
3813
|
+
*/
|
|
3814
|
+
async function getIDEHealthMetrics() {
|
|
3815
|
+
try {
|
|
3816
|
+
const healthTracker = new IDEHealthTracker();
|
|
3817
|
+
const allMetrics = await healthTracker.getAllHealthMetrics();
|
|
3818
|
+
return allMetrics;
|
|
3819
|
+
} catch (error) {
|
|
3820
|
+
// Health tracking not available, return empty metrics
|
|
3821
|
+
return new Map();
|
|
3822
|
+
}
|
|
3823
|
+
}
|
|
3293
3824
|
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
{ name: 'Continue CLI (Ollama support, but --auto mode doesn\'t execute code)', value: 'continue' },
|
|
3304
|
-
{ name: 'Cline CLI (local AI alternative, but has Ollama connection issues)', value: 'cline' },
|
|
3305
|
-
{ name: 'Cursor', value: 'cursor' },
|
|
3306
|
-
{ name: 'VS Code', value: 'vscode' },
|
|
3307
|
-
{ name: 'Windsurf', value: 'windsurf' }
|
|
3308
|
-
],
|
|
3309
|
-
default: currentIDE
|
|
3310
|
-
}
|
|
3311
|
-
]);
|
|
3825
|
+
/**
|
|
3826
|
+
* Format IDE name with health metrics
|
|
3827
|
+
*/
|
|
3828
|
+
async function formatIDEChoiceWithHealth(ideName, ideValue, allMetrics) {
|
|
3829
|
+
const metrics = allMetrics.get(ideValue);
|
|
3830
|
+
|
|
3831
|
+
if (!metrics || metrics.totalInteractions === 0) {
|
|
3832
|
+
return ideName; // No health data available
|
|
3833
|
+
}
|
|
3312
3834
|
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3835
|
+
const successCount = metrics.successCount || 0;
|
|
3836
|
+
const failureCount = metrics.failureCount || 0;
|
|
3837
|
+
const healthIndicator = metrics.consecutiveFailures === 0 ? '✅' : '⚠️';
|
|
3838
|
+
|
|
3839
|
+
return `${ideName} ${healthIndicator} +${successCount} -${failureCount}`;
|
|
3840
|
+
}
|
|
3316
3841
|
|
|
3317
|
-
|
|
3318
|
-
console.log(chalk.gray(' Note: This will be used for the next Auto Mode session.'));
|
|
3319
|
-
console.log();
|
|
3320
|
-
} else if (action === 'toggle-hostname') {
|
|
3842
|
+
async function showSettings() {
|
|
3321
3843
|
const newValue = !currentHostnameEnabled;
|
|
3322
|
-
|
|
3844
|
+
|
|
3323
3845
|
// Save to shared config (same location as Electron app)
|
|
3324
3846
|
await setConfigValue('computerNameEnabled', newValue);
|
|
3325
|
-
|
|
3847
|
+
|
|
3326
3848
|
const statusText = newValue ? chalk.green('enabled') : chalk.yellow('disabled');
|
|
3327
3849
|
console.log(chalk.green('\n✓'), `Hostname in requirements file ${statusText}`);
|
|
3328
|
-
|
|
3850
|
+
|
|
3329
3851
|
if (newValue) {
|
|
3330
3852
|
console.log(chalk.gray('\n Requirements file will be:'), chalk.cyan(`REQUIREMENTS-${hostname}.md`));
|
|
3331
3853
|
} else {
|
|
3332
3854
|
console.log(chalk.gray('\n Requirements file will be:'), chalk.cyan('REQUIREMENTS.md'));
|
|
3333
3855
|
}
|
|
3334
|
-
|
|
3856
|
+
|
|
3335
3857
|
console.log(chalk.gray('\n Note: You may need to rename your existing requirements file.'));
|
|
3336
3858
|
console.log(chalk.gray(' Note: This setting is now synced with the Electron app.'));
|
|
3337
3859
|
console.log();
|
|
3338
3860
|
}
|
|
3339
3861
|
}
|
|
3340
|
-
|
|
3862
|
+
|
|
3341
3863
|
/**
|
|
3342
3864
|
* Show cloud sync management menu
|
|
3343
3865
|
*/
|
|
@@ -3646,36 +4168,8 @@ async function startInteractive() {
|
|
|
3646
4168
|
const { stopAutoMode } = require('./auto-mode');
|
|
3647
4169
|
await stopAutoMode('startup');
|
|
3648
4170
|
|
|
3649
|
-
await bootstrapProjectIfInHomeDir();
|
|
3650
|
-
|
|
3651
4171
|
await showWelcomeScreen();
|
|
3652
4172
|
|
|
3653
|
-
// Check if .vibecodingmachine directory exists, offer to create if not
|
|
3654
|
-
const allnightStatusCheck = await checkVibeCodingMachineExists();
|
|
3655
|
-
if (!allnightStatusCheck.insideExists && !allnightStatusCheck.siblingExists) {
|
|
3656
|
-
console.log(chalk.yellow('\n⚠️ No .vibecodingmachine directory found'));
|
|
3657
|
-
console.log(chalk.gray('This directory is needed to save requirements and track progress.\n'));
|
|
3658
|
-
|
|
3659
|
-
const { shouldInit } = await inquirer.prompt([
|
|
3660
|
-
{
|
|
3661
|
-
type: 'confirm',
|
|
3662
|
-
name: 'shouldInit',
|
|
3663
|
-
message: 'Would you like to create the .vibecodingmachine directory now?',
|
|
3664
|
-
default: true
|
|
3665
|
-
}
|
|
3666
|
-
]);
|
|
3667
|
-
|
|
3668
|
-
if (shouldInit) {
|
|
3669
|
-
// Use the initRepo function to create the directory
|
|
3670
|
-
const { initRepo } = require('../commands/repo');
|
|
3671
|
-
await initRepo({ location: 'inside' });
|
|
3672
|
-
console.log('');
|
|
3673
|
-
} else {
|
|
3674
|
-
console.log(chalk.yellow('\n⚠️ Warning: Without the .vibecodingmachine directory, requirements cannot be saved.'));
|
|
3675
|
-
console.log(chalk.gray('You can create it later by running: ') + chalk.cyan('vcm repo:init\n'));
|
|
3676
|
-
}
|
|
3677
|
-
}
|
|
3678
|
-
|
|
3679
4173
|
if (process.env.VCM_OPEN_PROVIDER_MENU === '1' || process.env.VCM_OPEN_PROVIDER_MENU === 'true') {
|
|
3680
4174
|
await showProviderManagerMenu();
|
|
3681
4175
|
return;
|
|
@@ -3753,24 +4247,13 @@ async function startInteractive() {
|
|
|
3753
4247
|
const provider = displayAgent === 'ollama' ? 'ollama' : displayAgent;
|
|
3754
4248
|
|
|
3755
4249
|
if (checkModel) {
|
|
3756
|
-
const
|
|
3757
|
-
|
|
3758
|
-
if (timeUntilReset) {
|
|
3759
|
-
// Format time remaining in human-readable format
|
|
3760
|
-
const hours = Math.floor(timeUntilReset / (1000 * 60 * 60));
|
|
3761
|
-
const minutes = Math.floor((timeUntilReset % (1000 * 60 * 60)) / (1000 * 60));
|
|
3762
|
-
const seconds = Math.floor((timeUntilReset % (1000 * 60)) / 1000);
|
|
3763
|
-
|
|
3764
|
-
let timeStr = '';
|
|
3765
|
-
if (hours > 0) {
|
|
3766
|
-
timeStr = `${hours}h ${minutes}m`;
|
|
3767
|
-
} else if (minutes > 0) {
|
|
3768
|
-
timeStr = `${minutes}m ${seconds}s`;
|
|
3769
|
-
} else {
|
|
3770
|
-
timeStr = `${seconds}s`;
|
|
3771
|
-
}
|
|
4250
|
+
const info = providerManager.getRateLimitInfo(provider, checkModel);
|
|
3772
4251
|
|
|
3773
|
-
|
|
4252
|
+
if (info && info.isRateLimited && info.resetTime) {
|
|
4253
|
+
const label = formatResetsAtLabel(info.resetTime);
|
|
4254
|
+
if (label) {
|
|
4255
|
+
agentDisplay += ` ${chalk.red('⏰ ' + label)}`;
|
|
4256
|
+
}
|
|
3774
4257
|
}
|
|
3775
4258
|
}
|
|
3776
4259
|
}
|
|
@@ -3871,181 +4354,18 @@ async function startInteractive() {
|
|
|
3871
4354
|
value: 'setting:stages'
|
|
3872
4355
|
});
|
|
3873
4356
|
|
|
3874
|
-
// Add Admin Dashboard - Multi-Computer Dashboard
|
|
3875
|
-
items.push({
|
|
3876
|
-
type: 'setting',
|
|
3877
|
-
name: ` └─ Admin Dashboard: ${chalk.cyan('Multi-Computer Dashboard')}`,
|
|
3878
|
-
value: 'setting:admin-dashboard-multi-computer'
|
|
3879
|
-
});
|
|
3880
|
-
|
|
3881
|
-
// Add Admin Dashboard - Compliance and Terms Tracking
|
|
3882
|
-
items.push({
|
|
3883
|
-
type: 'setting',
|
|
3884
|
-
name: ` └─ Admin Dashboard: ${chalk.cyan('Compliance and Terms Tracking')}`,
|
|
3885
|
-
value: 'setting:admin-dashboard-compliance'
|
|
3886
|
-
});
|
|
3887
|
-
|
|
3888
|
-
// Add Admin Dashboard - Authentication and Security
|
|
3889
|
-
items.push({
|
|
3890
|
-
type: 'setting',
|
|
3891
|
-
name: ` └─ Admin Dashboard: ${chalk.cyan('Authentication and Security')}`,
|
|
3892
|
-
value: 'setting:admin-dashboard-auth'
|
|
3893
|
-
});
|
|
3894
|
-
|
|
3895
|
-
// Add Admin Dashboard - Terms and Privacy Policy
|
|
3896
|
-
items.push({
|
|
3897
|
-
type: 'setting',
|
|
3898
|
-
name: ` └─ Admin Dashboard: ${chalk.cyan('Terms and Privacy Policy')}`,
|
|
3899
|
-
value: 'setting:admin-dashboard-terms'
|
|
3900
|
-
});
|
|
3901
|
-
|
|
3902
|
-
// Add Admin Dashboard - Deployment and Infrastructure
|
|
3903
|
-
items.push({
|
|
3904
|
-
type: 'setting',
|
|
3905
|
-
name: ` └─ Admin Dashboard: ${chalk.cyan('Deployment and Infrastructure')}`,
|
|
3906
|
-
value: 'setting:admin-dashboard-deployment'
|
|
3907
|
-
});
|
|
3908
|
-
|
|
3909
|
-
// Add Cloud Sync Infrastructure - AWS DynamoDB Setup
|
|
3910
|
-
items.push({
|
|
3911
|
-
type: 'setting',
|
|
3912
|
-
name: ` └─ Cloud Sync Infrastructure: ${chalk.cyan('AWS DynamoDB Setup')}`,
|
|
3913
|
-
value: 'setting:cloud-sync-dynamodb'
|
|
3914
|
-
});
|
|
3915
|
-
|
|
3916
|
-
// Add Cloud Sync Infrastructure - Lambda Functions
|
|
3917
|
-
items.push({
|
|
3918
|
-
type: 'setting',
|
|
3919
|
-
name: ` └─ Cloud Sync Infrastructure: ${chalk.cyan('Lambda Functions')}`,
|
|
3920
|
-
value: 'setting:cloud-sync-lambda'
|
|
3921
|
-
});
|
|
3922
|
-
|
|
3923
|
-
// Add Cloud Sync Infrastructure - API Gateway Setup
|
|
3924
|
-
items.push({
|
|
3925
|
-
type: 'setting',
|
|
3926
|
-
name: ` └─ Cloud Sync: ${chalk.cyan('AWS DynamoDB Setup')}`,
|
|
3927
|
-
value: 'setting:cloud-sync-dynamodb'
|
|
3928
|
-
});
|
|
3929
|
-
|
|
3930
|
-
// Add Cloud Sync Infrastructure - AWS API Gateway
|
|
3931
|
-
items.push({
|
|
3932
|
-
type: 'setting',
|
|
3933
|
-
name: ` └─ Cloud Sync: ${chalk.cyan('AWS API Gateway')}`,
|
|
3934
|
-
value: 'setting:cloud-sync-api-gateway'
|
|
3935
|
-
});
|
|
3936
|
-
|
|
3937
|
-
// Add Cloud Sync Infrastructure - AWS Cognito Authentication
|
|
3938
|
-
items.push({
|
|
3939
|
-
type: 'setting',
|
|
3940
|
-
name: ` └─ Cloud Sync Infrastructure: ${chalk.cyan('AWS Cognito Authentication')}`,
|
|
3941
|
-
value: 'setting:cloud-sync-cognito'
|
|
3942
|
-
});
|
|
3943
|
-
|
|
3944
|
-
// Add Cloud Sync Infrastructure - AWS DynamoDB Setup
|
|
3945
4357
|
items.push({
|
|
3946
4358
|
type: 'setting',
|
|
3947
|
-
name: `
|
|
3948
|
-
value: 'setting:
|
|
4359
|
+
name: `Start Auto: Switch to Console tab and save updated agent list on exit (Left arrow to exit agent list will also save)`,
|
|
4360
|
+
value: 'setting:start-auto'
|
|
3949
4361
|
});
|
|
3950
4362
|
|
|
3951
|
-
// Cloud Sync Status
|
|
3952
|
-
try {
|
|
3953
|
-
const SyncEngine = require('vibecodingmachine-core/src/sync/sync-engine');
|
|
3954
|
-
const syncEngine = new SyncEngine();
|
|
3955
|
-
|
|
3956
|
-
// Try to initialize, but don't fail if AWS not configured
|
|
3957
|
-
try {
|
|
3958
|
-
await syncEngine.initialize();
|
|
3959
|
-
const syncStatus = syncEngine.getStatus();
|
|
3960
|
-
syncEngine.stop();
|
|
3961
|
-
|
|
3962
|
-
const onlineIcon = syncStatus.isOnline ? chalk.green('●') : chalk.red('●');
|
|
3963
|
-
const onlineText = syncStatus.isOnline ? t('interactive.online') : t('interactive.offline');
|
|
3964
|
-
const queueText = syncStatus.queuedChanges > 0 ? chalk.yellow(` (${syncStatus.queuedChanges} queued)`) : '';
|
|
3965
|
-
|
|
3966
|
-
items.push({
|
|
3967
|
-
type: 'setting',
|
|
3968
|
-
name: `${t('interactive.cloud.sync')}: ${onlineIcon} ${onlineText}${queueText}`,
|
|
3969
|
-
value: 'setting:cloud-sync'
|
|
3970
|
-
});
|
|
3971
|
-
} catch (initError) {
|
|
3972
|
-
// Initialization failed - AWS not configured or credentials missing
|
|
3973
|
-
items.push({
|
|
3974
|
-
type: 'setting',
|
|
3975
|
-
name: `Cloud Sync: ${chalk.gray('● Not configured')}`,
|
|
3976
|
-
value: 'setting:cloud-sync-setup'
|
|
3977
|
-
});
|
|
3978
|
-
}
|
|
3979
|
-
} catch (error) {
|
|
3980
|
-
// Module not found or other error - show disabled
|
|
3981
|
-
items.push({
|
|
3982
|
-
type: 'setting',
|
|
3983
|
-
name: `Cloud Sync: ${chalk.gray('● Disabled')}`,
|
|
3984
|
-
value: 'setting:cloud-sync-setup'
|
|
3985
|
-
});
|
|
3986
|
-
}
|
|
3987
|
-
|
|
3988
|
-
// Computer and Usage Tracking Status
|
|
3989
|
-
try {
|
|
3990
|
-
const os = require('os');
|
|
3991
|
-
const hostname = os.hostname();
|
|
3992
|
-
const platform = os.platform();
|
|
3993
|
-
const arch = os.arch();
|
|
3994
|
-
const usageIcon = chalk.blue('●');
|
|
3995
|
-
const trackingText = `${hostname} (${platform}/${arch})`;
|
|
3996
|
-
|
|
3997
|
-
items.push({
|
|
3998
|
-
type: 'setting',
|
|
3999
|
-
name: `${t('interactive.usage.tracking') || 'Usage Tracking'}: ${usageIcon} ${chalk.cyan(trackingText)}`,
|
|
4000
|
-
value: 'setting:usage-tracking'
|
|
4001
|
-
});
|
|
4002
|
-
} catch (error) {
|
|
4003
|
-
// Tracking info unavailable
|
|
4004
|
-
items.push({
|
|
4005
|
-
type: 'setting',
|
|
4006
|
-
name: `Usage Tracking: ${chalk.gray('● Unavailable')}`,
|
|
4007
|
-
value: 'setting:usage-tracking'
|
|
4008
|
-
});
|
|
4009
|
-
}
|
|
4010
|
-
|
|
4011
|
-
// Add requirement number tracking status
|
|
4012
4363
|
items.push({
|
|
4013
4364
|
type: 'setting',
|
|
4014
|
-
name: ` └─ ${t('interactive.
|
|
4015
|
-
value: 'setting:
|
|
4365
|
+
name: ` └─ ${t('interactive.feedback')}: ${chalk.cyan('📣 Press Ctrl+F for feedback')}`,
|
|
4366
|
+
value: 'setting:feedback'
|
|
4016
4367
|
});
|
|
4017
|
-
const { readConfig } = require('./config');
|
|
4018
|
-
const config = await readConfig();
|
|
4019
|
-
const lastReqNumber = config.lastRequirementNumber || 0;
|
|
4020
|
-
items.push({
|
|
4021
|
-
type: 'setting',
|
|
4022
|
-
name: ` └─ ${t('interactive.last.requirement.number')}: ${chalk.cyan('R' + lastReqNumber)}`,
|
|
4023
|
-
value: 'setting:req-number'
|
|
4024
|
-
});
|
|
4025
|
-
|
|
4026
|
-
// Add "Next TODO Requirement" as a separate menu item if there are TODO items
|
|
4027
|
-
if (counts && counts.todoCount > 0) {
|
|
4028
|
-
items.push({
|
|
4029
|
-
type: 'setting',
|
|
4030
|
-
name: ` └─ ${t('interactive.next.todo.requirement')}`,
|
|
4031
|
-
value: 'setting:next-todo-requirement'
|
|
4032
|
-
});
|
|
4033
|
-
}
|
|
4034
4368
|
|
|
4035
|
-
// Save last tab opened
|
|
4036
|
-
const lastTabOpened = config.lastTabOpened;
|
|
4037
|
-
if (lastTabOpened) {
|
|
4038
|
-
items.push({
|
|
4039
|
-
type: 'setting',
|
|
4040
|
-
name: ` └─ ${t('interactive.last.tab.opened')}: ${lastTabOpened}`,
|
|
4041
|
-
value: 'setting:last-tab-opened'
|
|
4042
|
-
});
|
|
4043
|
-
}
|
|
4044
|
-
items.push({
|
|
4045
|
-
type: 'setting',
|
|
4046
|
-
name: `Start Auto: Switch to Console tab`,
|
|
4047
|
-
value: 'setting:start-auto'
|
|
4048
|
-
});
|
|
4049
4369
|
// TODO: Implement getNextTodoRequirement function
|
|
4050
4370
|
// if (counts && counts.todoCount > 0) {
|
|
4051
4371
|
// const { getNextTodoRequirement } = require('./auto-mode');
|
|
@@ -4069,6 +4389,9 @@ async function startInteractive() {
|
|
|
4069
4389
|
const hostname = await getHostname();
|
|
4070
4390
|
const reqFilename = await getRequirementsFilename(hostname);
|
|
4071
4391
|
const reqPath = path.join(repoPath, '.vibecodingmachine', reqFilename);
|
|
4392
|
+
// Increase the delay to prevent flashing and repaint
|
|
4393
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
4394
|
+
|
|
4072
4395
|
|
|
4073
4396
|
if (await fs.pathExists(reqPath)) {
|
|
4074
4397
|
const reqContent = await fs.readFile(reqPath, 'utf8');
|
|
@@ -4178,10 +4501,20 @@ async function startInteractive() {
|
|
|
4178
4501
|
|
|
4179
4502
|
switch (actionStr) {
|
|
4180
4503
|
case 'setting:agent': {
|
|
4504
|
+
if (process.env.VCM_DEBUG_MENU === '1') {
|
|
4505
|
+
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';
|
|
4506
|
+
console.log(msg);
|
|
4507
|
+
try { require('fs').appendFileSync('/tmp/vcm-menu-debug.log', msg); } catch (e) { }
|
|
4508
|
+
}
|
|
4181
4509
|
await showProviderManagerMenu();
|
|
4182
4510
|
await showWelcomeScreen();
|
|
4183
4511
|
break;
|
|
4184
4512
|
}
|
|
4513
|
+
case 'setting:feedback': {
|
|
4514
|
+
await handleFeedbackSubmission();
|
|
4515
|
+
await showWelcomeScreen();
|
|
4516
|
+
break;
|
|
4517
|
+
}
|
|
4185
4518
|
case 'setting:provider': {
|
|
4186
4519
|
// Switch AI provider - run provider setup only, don't start auto mode
|
|
4187
4520
|
// Note: Continue CLI doesn't have provider setup - models are configured in ~/.continue/config.yaml
|
|
@@ -4557,6 +4890,12 @@ async function startInteractive() {
|
|
|
4557
4890
|
|
|
4558
4891
|
// Use saved maxChats/neverStop settings
|
|
4559
4892
|
const options = { ide: agentToUse };
|
|
4893
|
+
|
|
4894
|
+
// Set extension for VS Code extensions from saved config
|
|
4895
|
+
if (agentToUse === 'amazon-q' || agentToUse === 'github-copilot') {
|
|
4896
|
+
options.extension = agentToUse;
|
|
4897
|
+
}
|
|
4898
|
+
|
|
4560
4899
|
if (currentConfig.neverStop) {
|
|
4561
4900
|
options.neverStop = true;
|
|
4562
4901
|
} else if (currentConfig.maxChats) {
|
|
@@ -4578,7 +4917,7 @@ async function startInteractive() {
|
|
|
4578
4917
|
// ALWAYS use auto:direct (supports both LLM and IDE agents with proper looping)
|
|
4579
4918
|
console.log(chalk.gray('[DEBUG] Using auto:direct for agent:', agentToUse));
|
|
4580
4919
|
const { handleAutoStart: handleDirectAutoStart } = require('../commands/auto-direct');
|
|
4581
|
-
await handleDirectAutoStart(
|
|
4920
|
+
await handleDirectAutoStart(options);
|
|
4582
4921
|
|
|
4583
4922
|
// Prompt user before returning to menu (so they can read the output)
|
|
4584
4923
|
console.log('');
|
|
@@ -4793,8 +5132,11 @@ async function startInteractive() {
|
|
|
4793
5132
|
{ name: 'Continue CLI (Ollama support, but --auto mode doesn\'t execute code)', value: 'continue' },
|
|
4794
5133
|
{ name: 'Cline CLI (local AI alternative, but has Ollama connection issues)', value: 'cline' },
|
|
4795
5134
|
{ name: 'Cursor', value: 'cursor' },
|
|
4796
|
-
{ name: 'VS Code', value: '
|
|
4797
|
-
{ name: '
|
|
5135
|
+
{ name: 'VS Code (with Amazon Q Developer)', value: 'amazon-q' },
|
|
5136
|
+
{ name: 'VS Code (with GitHub Copilot)', value: 'github-copilot' },
|
|
5137
|
+
{ name: 'VS Code (plain)', value: 'vscode' },
|
|
5138
|
+
{ name: 'Windsurf', value: 'windsurf' },
|
|
5139
|
+
{ name: 'Google Antigravity', value: 'antigravity' }
|
|
4798
5140
|
],
|
|
4799
5141
|
default: 'claude-code'
|
|
4800
5142
|
},
|
|
@@ -4805,7 +5147,42 @@ async function startInteractive() {
|
|
|
4805
5147
|
default: ''
|
|
4806
5148
|
}
|
|
4807
5149
|
]);
|
|
5150
|
+
|
|
5151
|
+
let ideModel = null;
|
|
5152
|
+
if (ide === 'windsurf' || ide === 'antigravity') {
|
|
5153
|
+
const { selectedModel } = await inquirer.prompt([
|
|
5154
|
+
{
|
|
5155
|
+
type: 'list',
|
|
5156
|
+
name: 'selectedModel',
|
|
5157
|
+
message: `Select ${ide === 'windsurf' ? 'Windsurf' : 'Antigravity'} agent/model:`,
|
|
5158
|
+
choices: (ide === 'windsurf' ? [
|
|
5159
|
+
{ name: 'Default', value: 'default' },
|
|
5160
|
+
{ name: 'SWE-1-lite', value: 'swe-1-lite' }
|
|
5161
|
+
] : [
|
|
5162
|
+
{ name: 'Default', value: 'antigravity' },
|
|
5163
|
+
{ name: 'Gemini 3 Pro (Low)', value: 'Gemini 3 Pro (Low)' },
|
|
5164
|
+
{ name: 'Claude Sonnet 4.5', value: 'Claude Sonnet 4.5' },
|
|
5165
|
+
{ name: 'Claude Sonnet 4.5 (Thinking)', value: 'Claude Sonnet 4.5 (Thinking)' },
|
|
5166
|
+
{ name: 'GPT-OSS 120B (Medium)', value: 'GPT-OSS 120B (Medium)' }
|
|
5167
|
+
]),
|
|
5168
|
+
default: 'default'
|
|
5169
|
+
}
|
|
5170
|
+
]);
|
|
5171
|
+
ideModel = selectedModel;
|
|
5172
|
+
}
|
|
5173
|
+
|
|
4808
5174
|
const options = { ide };
|
|
5175
|
+
if (ideModel) {
|
|
5176
|
+
options.ideModel = ideModel;
|
|
5177
|
+
}
|
|
5178
|
+
|
|
5179
|
+
// Set extension for VS Code extensions
|
|
5180
|
+
if (ide === 'amazon-q') {
|
|
5181
|
+
options.extension = 'amazon-q';
|
|
5182
|
+
} else if (ide === 'github-copilot') {
|
|
5183
|
+
options.extension = 'github-copilot';
|
|
5184
|
+
}
|
|
5185
|
+
|
|
4809
5186
|
if (maxChats && maxChats.trim() !== '0') {
|
|
4810
5187
|
options.maxChats = parseInt(maxChats);
|
|
4811
5188
|
} else {
|
|
@@ -4899,6 +5276,60 @@ Max Chats: ${maxChats || 'Never stop'}`;
|
|
|
4899
5276
|
});
|
|
4900
5277
|
}
|
|
4901
5278
|
|
|
5279
|
+
// Helper to parse requirements file content
|
|
5280
|
+
const parseRequirementsFile = (content) => {
|
|
5281
|
+
const lines = content.split('\n');
|
|
5282
|
+
let currentRequirement = null;
|
|
5283
|
+
let currentStatus = null;
|
|
5284
|
+
|
|
5285
|
+
// Find current requirement (first one under TODO section)
|
|
5286
|
+
let inTodoSection = false;
|
|
5287
|
+
for (let i = 0; i < lines.length; i++) {
|
|
5288
|
+
const line = lines[i];
|
|
5289
|
+
const trimmed = line.trim();
|
|
5290
|
+
|
|
5291
|
+
// Check if we're in TODO section
|
|
5292
|
+
if (trimmed.includes('⏳ Requirements not yet completed') || trimmed.includes('Requirements not yet completed')) {
|
|
5293
|
+
inTodoSection = true;
|
|
5294
|
+
continue;
|
|
5295
|
+
}
|
|
5296
|
+
|
|
5297
|
+
// Exit TODO section at next ## header
|
|
5298
|
+
if (inTodoSection && trimmed.startsWith('## ') && !trimmed.startsWith('###')) {
|
|
5299
|
+
break;
|
|
5300
|
+
}
|
|
5301
|
+
|
|
5302
|
+
// Get first requirement in TODO section
|
|
5303
|
+
if (inTodoSection && trimmed.startsWith('### ')) {
|
|
5304
|
+
currentRequirement = trimmed.replace(/^###\s*/, '').trim();
|
|
5305
|
+
break;
|
|
5306
|
+
}
|
|
5307
|
+
}
|
|
5308
|
+
|
|
5309
|
+
// Find current status
|
|
5310
|
+
for (let i = 0; i < lines.length; i++) {
|
|
5311
|
+
const line = lines[i];
|
|
5312
|
+
const trimmed = line.trim();
|
|
5313
|
+
|
|
5314
|
+
if (trimmed.includes('🚦 Current Status') || trimmed.includes('Current Status')) {
|
|
5315
|
+
// Look for status in next few lines
|
|
5316
|
+
for (let j = i + 1; j < Math.min(i + 5, lines.length); j++) {
|
|
5317
|
+
const statusLine = lines[j].trim();
|
|
5318
|
+
if (statusLine.match(/^(PREPARE|CREATE|ACT|CLEAN UP|VERIFY|DONE)$/i)) {
|
|
5319
|
+
currentStatus = statusLine.toUpperCase();
|
|
5320
|
+
break;
|
|
5321
|
+
}
|
|
5322
|
+
}
|
|
5323
|
+
break;
|
|
5324
|
+
}
|
|
5325
|
+
}
|
|
5326
|
+
|
|
5327
|
+
return {
|
|
5328
|
+
currentRequirement: currentRequirement || 'No active requirement',
|
|
5329
|
+
currentStatus: currentStatus || 'PREPARE'
|
|
5330
|
+
};
|
|
5331
|
+
};
|
|
5332
|
+
|
|
4902
5333
|
// Watch requirements file for changes
|
|
4903
5334
|
const watcher = chokidar.watch(reqPath, { persistent: true });
|
|
4904
5335
|
watcher.on('change', async () => {
|
|
@@ -5022,11 +5453,49 @@ Max Chats: ${maxChats || 'Never stop'}`;
|
|
|
5022
5453
|
}
|
|
5023
5454
|
}
|
|
5024
5455
|
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
5456
|
+
async function bootstrapProjectIfInHomeDir() {
|
|
5457
|
+
const { checkVibeCodingMachineExists, getRequirementsFilename } = require('vibecodingmachine-core');
|
|
5458
|
+
const exists = await checkVibeCodingMachineExists();
|
|
5459
|
+
const home = os.homedir();
|
|
5460
|
+
|
|
5461
|
+
// If a VibeCodingMachine project already exists nearby, do nothing
|
|
5462
|
+
if (exists && (exists.insideExists || exists.siblingExists)) return;
|
|
5463
|
+
|
|
5464
|
+
// Only bootstrap when we're sitting at the user's home directory (resolve symlinks)
|
|
5465
|
+
try {
|
|
5466
|
+
const cwdResolved = await fs.realpath(process.cwd());
|
|
5467
|
+
const homeResolved = await fs.realpath(home);
|
|
5468
|
+
if (cwdResolved !== homeResolved) return;
|
|
5469
|
+
} catch (e) {
|
|
5470
|
+
// If we can't resolve, fallback to path.resolve comparison
|
|
5471
|
+
if (path.resolve(process.cwd()) !== path.resolve(home)) return;
|
|
5472
|
+
}
|
|
5473
|
+
|
|
5474
|
+
const inquirer = require('inquirer');
|
|
5475
|
+
const { shouldCreateCodeDir } = await inquirer.prompt({ type: 'confirm', name: 'shouldCreateCodeDir', message: t('interactive.should.create.code.dir') });
|
|
5476
|
+
if (!shouldCreateCodeDir) return;
|
|
5477
|
+
|
|
5478
|
+
const { projectName } = await inquirer.prompt({ type: 'input', name: 'projectName', message: t('interactive.project.name') });
|
|
5479
|
+
const slug = (projectName || 'my-project-name').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '') || 'my-project-name';
|
|
5480
|
+
const codeDir = path.join(home, 'code');
|
|
5481
|
+
await fs.ensureDir(codeDir);
|
|
5482
|
+
|
|
5483
|
+
const projectDir = path.join(codeDir, slug);
|
|
5484
|
+
await fs.ensureDir(projectDir);
|
|
5485
|
+
|
|
5486
|
+
process.chdir(projectDir);
|
|
5487
|
+
|
|
5488
|
+
// Create .vibecodingmachine and REQUIREMENTS file if missing
|
|
5489
|
+
const vcmDir = path.join(projectDir, '.vibecodingmachine');
|
|
5490
|
+
await fs.ensureDir(vcmDir);
|
|
5491
|
+
const reqFilename = await getRequirementsFilename();
|
|
5492
|
+
const reqPath = path.join(vcmDir, reqFilename);
|
|
5493
|
+
if (!await fs.pathExists(reqPath)) {
|
|
5494
|
+
await fs.writeFile(reqPath, `# ${projectName}\n\n## ⏳ Requirements not yet completed\n\n`, 'utf8');
|
|
5495
|
+
}
|
|
5496
|
+
}
|
|
5497
|
+
|
|
5498
|
+
module.exports = { startInteractive, showProviderManagerMenu, /* exported for tests */ parseRequirementsFromContent, bootstrapProjectIfInHomeDir };
|
|
5030
5499
|
|
|
5031
5500
|
|
|
5032
5501
|
|