vibecodingmachine-cli 2025.12.25-25 → 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 +92 -118
- package/logs/audit/2025-12-27.jsonl +1 -0
- package/logs/audit/2026-01-03.jsonl +2 -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/auth.js +5 -1
- package/src/commands/auto-direct.js +747 -182
- package/src/commands/auto.js +206 -48
- package/src/commands/computers.js +9 -0
- package/src/commands/feature.js +123 -0
- package/src/commands/ide.js +108 -3
- package/src/commands/repo.js +27 -22
- package/src/commands/requirements-remote.js +34 -2
- package/src/commands/requirements.js +129 -9
- package/src/commands/setup.js +2 -1
- package/src/commands/status.js +39 -1
- package/src/commands/sync.js +7 -1
- package/src/utils/antigravity-js-handler.js +13 -4
- package/src/utils/auth.js +56 -25
- package/src/utils/compliance-check.js +10 -0
- package/src/utils/config.js +42 -1
- package/src/utils/date-formatter.js +44 -0
- package/src/utils/first-run.js +8 -6
- package/src/utils/interactive.js +1363 -334
- package/src/utils/kiro-js-handler.js +188 -0
- package/src/utils/prompt-helper.js +64 -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/home-bootstrap.test.js +76 -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
|
@@ -5,11 +5,12 @@ const path = require('path');
|
|
|
5
5
|
const os = require('os');
|
|
6
6
|
const fs = require('fs-extra');
|
|
7
7
|
const readline = require('readline');
|
|
8
|
+
const { execSync } = require('child_process');
|
|
8
9
|
const repo = require('../commands/repo');
|
|
9
10
|
const auto = require('../commands/auto');
|
|
10
11
|
const status = require('../commands/status');
|
|
11
12
|
const requirements = require('../commands/requirements');
|
|
12
|
-
const { getRepoPath, readConfig, writeConfig, getAutoConfig } = require('./config');
|
|
13
|
+
const { getRepoPath, readConfig, writeConfig, getAutoConfig, getProviderCache, setProviderCache } = require('./config');
|
|
13
14
|
const { getProviderPreferences, saveProviderPreferences, getProviderDefinitions } = require('../utils/provider-registry');
|
|
14
15
|
const { checkAutoModeStatus } = require('./auto-mode');
|
|
15
16
|
const {
|
|
@@ -20,14 +21,127 @@ const {
|
|
|
20
21
|
isComputerNameEnabled,
|
|
21
22
|
t,
|
|
22
23
|
detectLocale,
|
|
23
|
-
setLocale
|
|
24
|
+
setLocale,
|
|
25
|
+
AppleScriptManager,
|
|
26
|
+
IDEHealthTracker,
|
|
27
|
+
HealthReporter
|
|
24
28
|
} = require('vibecodingmachine-core');
|
|
29
|
+
const { promptWithDefaultsOnce } = require('./prompt-helper');
|
|
30
|
+
const { formatResetsAtLabel } = require('./date-formatter');
|
|
25
31
|
|
|
26
32
|
// Initialize locale detection for interactive mode
|
|
27
33
|
const detectedLocale = detectLocale();
|
|
28
34
|
setLocale(detectedLocale);
|
|
29
35
|
const pkg = require('../../package.json');
|
|
30
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
|
+
|
|
61
|
+
async function checkAppOrBinary(names = [], binaries = []) {
|
|
62
|
+
// names: app bundle base names (e.g., 'Cursor' -> /Applications/Cursor.app)
|
|
63
|
+
// binaries: CLI binary names to check on PATH (e.g., 'code')
|
|
64
|
+
const platform = os.platform();
|
|
65
|
+
// Check common application directories
|
|
66
|
+
if (platform === 'darwin') {
|
|
67
|
+
const appDirs = ['/Applications', path.join(os.homedir(), 'Applications')];
|
|
68
|
+
for (const appName of names) {
|
|
69
|
+
for (const dir of appDirs) {
|
|
70
|
+
try {
|
|
71
|
+
const p = path.join(dir, `${appName}.app`);
|
|
72
|
+
if (await fs.pathExists(p)) {
|
|
73
|
+
// Ensure this is a real application bundle (has Contents/MacOS executable)
|
|
74
|
+
try {
|
|
75
|
+
const macosDir = path.join(p, 'Contents', 'MacOS');
|
|
76
|
+
const exists = await fs.pathExists(macosDir);
|
|
77
|
+
if (exists) {
|
|
78
|
+
const files = await fs.readdir(macosDir);
|
|
79
|
+
if (files && files.length > 0) {
|
|
80
|
+
// Prefer to ensure the app is usable: use spctl to assess, fallback to quarantine xattr
|
|
81
|
+
try {
|
|
82
|
+
// spctl returns non-zero on rejected/invalid apps
|
|
83
|
+
// Use async exec to avoid blocking the event loop
|
|
84
|
+
await execAsync(`spctl --assess -v "${p}"`, { timeout: 5000 });
|
|
85
|
+
|
|
86
|
+
// additionally validate codesign quickly (timeout to avoid hangs)
|
|
87
|
+
try {
|
|
88
|
+
await execAsync(`codesign -v --deep --strict "${p}"`, { timeout: 5000 });
|
|
89
|
+
return true;
|
|
90
|
+
} catch (csErr) {
|
|
91
|
+
// codesign failed or timed out — treat as not usable/damaged
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
} catch (e) {
|
|
95
|
+
// spctl failed or timed out — check if app has quarantine attribute
|
|
96
|
+
try {
|
|
97
|
+
const { stdout } = await execAsync(`xattr -p com.apple.quarantine "${p}" 2>/dev/null || true`, { encoding: 'utf8' });
|
|
98
|
+
const out = stdout.trim();
|
|
99
|
+
if (!out) {
|
|
100
|
+
// no quarantine attribute but spctl failed — be conservative and treat as not installed
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
// If quarantine attribute exists, treat as not installed (damaged/not allowed)
|
|
104
|
+
return false;
|
|
105
|
+
} catch (e2) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
} catch (e) {
|
|
112
|
+
// if we can't stat inside, be conservative and continue searching
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
} catch (e) {
|
|
116
|
+
/* ignore */
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Check PATH for known binaries
|
|
123
|
+
for (const bin of binaries) {
|
|
124
|
+
try {
|
|
125
|
+
await execAsync(`which ${bin}`, { timeout: 2000 });
|
|
126
|
+
return true;
|
|
127
|
+
} catch (e) {
|
|
128
|
+
/* not found */
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Check common Homebrew bin locations
|
|
133
|
+
const brewPaths = ['/opt/homebrew/bin', '/usr/local/bin'];
|
|
134
|
+
for (const bin of binaries) {
|
|
135
|
+
for (const brew of brewPaths) {
|
|
136
|
+
try {
|
|
137
|
+
if (await fs.pathExists(path.join(brew, bin))) return true;
|
|
138
|
+
} catch (e) { /* ignore */ }
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
|
|
31
145
|
/**
|
|
32
146
|
* Translate workflow stage names
|
|
33
147
|
*/
|
|
@@ -248,6 +362,39 @@ async function countRequirements() {
|
|
|
248
362
|
}
|
|
249
363
|
}
|
|
250
364
|
|
|
365
|
+
async function getSyncStatus() {
|
|
366
|
+
try {
|
|
367
|
+
const SyncEngine = require('vibecodingmachine-core/src/sync/sync-engine');
|
|
368
|
+
const syncEngine = new SyncEngine();
|
|
369
|
+
await syncEngine.initialize();
|
|
370
|
+
const status = syncEngine.getStatus();
|
|
371
|
+
syncEngine.stop();
|
|
372
|
+
|
|
373
|
+
if (!status.isOnline && status.queuedChanges > 0) {
|
|
374
|
+
return `[Offline: ${status.queuedChanges} queued]`;
|
|
375
|
+
} else if (!status.isOnline) {
|
|
376
|
+
return '[Offline]';
|
|
377
|
+
} else if (status.isSyncing) {
|
|
378
|
+
return '[Syncing...]';
|
|
379
|
+
} else if (status.lastSyncTime) {
|
|
380
|
+
const timeSinceSync = Date.now() - status.lastSyncTime;
|
|
381
|
+
const minutesAgo = Math.floor(timeSinceSync / (1000 * 60));
|
|
382
|
+
if (minutesAgo < 1) {
|
|
383
|
+
return '[Sync: ✓ just now]';
|
|
384
|
+
} else if (minutesAgo < 60) {
|
|
385
|
+
return `[Sync: ✓ ${minutesAgo}m ago]`;
|
|
386
|
+
} else {
|
|
387
|
+
const hoursAgo = Math.floor(minutesAgo / 60);
|
|
388
|
+
return `[Sync: ✓ ${hoursAgo}h ago]`;
|
|
389
|
+
}
|
|
390
|
+
} else {
|
|
391
|
+
return '[Never synced]';
|
|
392
|
+
}
|
|
393
|
+
} catch (error) {
|
|
394
|
+
return '[Sync unavailable]';
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
251
398
|
async function getCurrentProgress() {
|
|
252
399
|
try {
|
|
253
400
|
const { getRequirementsPath } = require('vibecodingmachine-core');
|
|
@@ -319,7 +466,7 @@ async function showWelcomeScreen() {
|
|
|
319
466
|
|
|
320
467
|
// Display welcome banner with version
|
|
321
468
|
console.log('\n' + boxen(
|
|
322
|
-
chalk.bold.cyan('Vibe Coding Machine') + '\n' +
|
|
469
|
+
chalk.bold.cyan('Vibe Coding Machine!') + '\n' +
|
|
323
470
|
chalk.gray(version) + '\n' +
|
|
324
471
|
chalk.gray(t('banner.tagline')),
|
|
325
472
|
{
|
|
@@ -330,9 +477,28 @@ async function showWelcomeScreen() {
|
|
|
330
477
|
}
|
|
331
478
|
));
|
|
332
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
|
+
|
|
333
483
|
// Display repository and system info
|
|
334
484
|
console.log();
|
|
335
485
|
console.log(chalk.gray(t('system.repo').padEnd(25)), formatPath(repoPath));
|
|
486
|
+
|
|
487
|
+
// Display git branch if in a git repo
|
|
488
|
+
try {
|
|
489
|
+
const { isGitRepo, getCurrentBranch, hasUncommittedChanges } = require('vibecodingmachine-core');
|
|
490
|
+
if (isGitRepo(repoPath)) {
|
|
491
|
+
const branch = getCurrentBranch(repoPath);
|
|
492
|
+
if (branch) {
|
|
493
|
+
const isDirty = hasUncommittedChanges(repoPath);
|
|
494
|
+
const branchDisplay = isDirty ? `${chalk.cyan(branch)} ${chalk.yellow(t('system.git.status.dirty'))}` : chalk.cyan(branch);
|
|
495
|
+
console.log(chalk.gray(t('system.git.branch').padEnd(25)), branchDisplay);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
} catch (error) {
|
|
499
|
+
// Ignore git display errors
|
|
500
|
+
}
|
|
501
|
+
|
|
336
502
|
console.log(chalk.gray(t('system.computer.name').padEnd(25)), chalk.cyan(hostname));
|
|
337
503
|
|
|
338
504
|
// Display auto mode progress if running
|
|
@@ -402,10 +568,14 @@ function indexToLetter(index) {
|
|
|
402
568
|
return String.fromCharCode(97 + index); // 97 is 'a'
|
|
403
569
|
}
|
|
404
570
|
|
|
571
|
+
// Parse requirements from file content for a given section (supports ###, PACKAGE:, and '- ' bullets)
|
|
572
|
+
const { parseRequirementsFromContent } = require('./requirements-parser');
|
|
573
|
+
|
|
405
574
|
// Tree-style requirements navigator
|
|
406
575
|
async function showRequirementsTree() {
|
|
407
576
|
console.log(chalk.bold.cyan('\n📋 ' + t('requirements.navigator.title') + '\n'));
|
|
408
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'));
|
|
409
579
|
|
|
410
580
|
const tree = {
|
|
411
581
|
expanded: { root: true },
|
|
@@ -432,16 +602,16 @@ async function showRequirementsTree() {
|
|
|
432
602
|
};
|
|
433
603
|
|
|
434
604
|
// Create localized labels
|
|
435
|
-
const localizedTodoLabel = todoCount > 0 ?
|
|
436
|
-
`⏳ ${t('requirements.section.todo')} (${todoCount} - ${Math.round((todoCount / total) * 100)}%)` :
|
|
605
|
+
const localizedTodoLabel = todoCount > 0 ?
|
|
606
|
+
`⏳ ${t('requirements.section.todo')} (${todoCount} - ${Math.round((todoCount / total) * 100)}%)` :
|
|
437
607
|
`⏳ ${t('requirements.section.todo')} (0 - 0%)`;
|
|
438
|
-
|
|
439
|
-
const localizedToVerifyLabel = toVerifyCount > 0 ?
|
|
440
|
-
`✅ ${t('requirements.section.to.verify')} (${toVerifyCount} - ${Math.round((toVerifyCount / total) * 100)}%)` :
|
|
608
|
+
|
|
609
|
+
const localizedToVerifyLabel = toVerifyCount > 0 ?
|
|
610
|
+
`✅ ${t('requirements.section.to.verify')} (${toVerifyCount} - ${Math.round((toVerifyCount / total) * 100)}%)` :
|
|
441
611
|
`✅ ${t('requirements.section.to.verify')} (0 - 0%)`;
|
|
442
|
-
|
|
443
|
-
const localizedVerifiedLabel = verifiedCount > 0 ?
|
|
444
|
-
`🎉 ${t('requirements.section.verified')} (${verifiedCount} - ${Math.round((verifiedCount / total) * 100)}%)` :
|
|
612
|
+
|
|
613
|
+
const localizedVerifiedLabel = verifiedCount > 0 ?
|
|
614
|
+
`🎉 ${t('requirements.section.verified')} (${verifiedCount} - ${Math.round((verifiedCount / total) * 100)}%)` :
|
|
445
615
|
`🎉 ${t('requirements.section.verified')} (0 - 0%)`;
|
|
446
616
|
|
|
447
617
|
const verifiedReqs = tree.verifiedReqs || [];
|
|
@@ -450,6 +620,10 @@ async function showRequirementsTree() {
|
|
|
450
620
|
const todoReqs = tree.todoReqs || [];
|
|
451
621
|
const recycledReqs = tree.recycledReqs || [];
|
|
452
622
|
|
|
623
|
+
// Calculate percentages for clarification and recycled sections
|
|
624
|
+
const clarificationPercent = total > 0 ? Math.round((clarificationReqs.length / total) * 100) : 0;
|
|
625
|
+
const recycledPercent = total > 0 ? Math.round((recycledReqs.length / total) * 100) : 0;
|
|
626
|
+
|
|
453
627
|
// VERIFIED section (first) - only show if has requirements
|
|
454
628
|
if (verifiedReqs.length > 0 || verifiedCount > 0) {
|
|
455
629
|
tree.items.push({ level: 1, type: 'section', label: localizedVerifiedLabel, key: 'verified' });
|
|
@@ -517,136 +691,14 @@ async function showRequirementsTree() {
|
|
|
517
691
|
}
|
|
518
692
|
|
|
519
693
|
const content = await fs.readFile(reqPath, 'utf8');
|
|
520
|
-
const lines = content.split('\n');
|
|
521
|
-
|
|
522
|
-
let inSection = false;
|
|
523
|
-
const requirements = [];
|
|
524
|
-
|
|
525
|
-
// For TO VERIFY section, check multiple possible section titles
|
|
526
|
-
const sectionTitles = sectionKey === 'verify'
|
|
527
|
-
? ['🔍 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']
|
|
528
|
-
: [sectionTitle];
|
|
529
|
-
|
|
530
|
-
// For TO VERIFY, we need to find the exact section header
|
|
531
|
-
// The section header is: ## ✅ Verified by AI screenshot. Needs Human to Verify and move to CHANGELOG
|
|
532
|
-
const toVerifySectionHeader = '## ✅ Verified by AI screenshot. Needs Human to Verify and move to CHANGELOG';
|
|
533
694
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
const trimmed = line.trim();
|
|
537
|
-
|
|
538
|
-
// Check if this line matches any of the section titles
|
|
539
|
-
// IMPORTANT: Only check section headers (lines starting with ##), not requirement text
|
|
540
|
-
if (trimmed.startsWith('##') && !trimmed.startsWith('###')) {
|
|
541
|
-
// Reset inSection if we hit a section header that's not our target section
|
|
542
|
-
if (sectionKey === 'verify' && inSection) {
|
|
543
|
-
// Check if this is still a TO VERIFY section header
|
|
544
|
-
if (!isStillToVerify) {
|
|
545
|
-
// This will be handled by the "leaving section" check below, but ensure we don't process it as entering
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
if (sectionKey === 'verify') {
|
|
549
|
-
// For TO VERIFY, check for the specific section header with exact matching
|
|
550
|
-
// Must match the exact TO VERIFY section header, not just any line containing "TO VERIFY"
|
|
551
|
-
const isToVerifyHeader = trimmed === '## 🔍 TO VERIFY BY HUMAN' ||
|
|
552
|
-
trimmed.startsWith('## 🔍 TO VERIFY BY HUMAN') ||
|
|
553
|
-
trimmed === '## 🔍 TO VERIFY' ||
|
|
554
|
-
trimmed.startsWith('## 🔍 TO VERIFY') ||
|
|
555
|
-
trimmed === '## TO VERIFY' ||
|
|
556
|
-
trimmed.startsWith('## TO VERIFY') ||
|
|
557
|
-
trimmed === '## ✅ TO VERIFY' ||
|
|
558
|
-
trimmed.startsWith('## ✅ TO VERIFY') ||
|
|
559
|
-
trimmed === toVerifySectionHeader ||
|
|
560
|
-
(trimmed.startsWith(toVerifySectionHeader) && trimmed.includes('Needs Human to Verify'));
|
|
561
|
-
|
|
562
|
-
if (isToVerifyHeader) {
|
|
563
|
-
// Make sure it's not a VERIFIED section (without TO VERIFY)
|
|
564
|
-
if (!trimmed.includes('## 📝 VERIFIED') && !trimmed.match(/^##\s+VERIFIED$/i) && !trimmed.includes('📝 VERIFIED')) {
|
|
565
|
-
inSection = true;
|
|
566
|
-
continue;
|
|
567
|
-
}
|
|
568
|
-
} else {
|
|
569
|
-
// If we hit a different section header and we're looking for TO VERIFY, make sure we're not in section
|
|
570
|
-
// This prevents incorrectly reading from TODO or other sections
|
|
571
|
-
if (trimmed.includes('⏳ Requirements not yet completed') ||
|
|
572
|
-
trimmed.includes('Requirements not yet completed') ||
|
|
573
|
-
trimmed === '## 📝 VERIFIED' ||
|
|
574
|
-
trimmed.startsWith('## 📝 VERIFIED')) {
|
|
575
|
-
// We're in TODO or VERIFIED section, not TO VERIFY - reset
|
|
576
|
-
inSection = false;
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
} else if (sectionTitles.some(title => trimmed.includes(title))) {
|
|
580
|
-
inSection = true;
|
|
581
|
-
continue;
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
// Check if we're leaving the section (new section header that doesn't match)
|
|
586
|
-
if (inSection && trimmed.startsWith('##') && !trimmed.startsWith('###')) {
|
|
587
|
-
// If this is a new section header and it's not one of our section titles, we've left the section
|
|
588
|
-
if (sectionKey === 'verify') {
|
|
589
|
-
// For TO VERIFY, only break if this is clearly a different section
|
|
590
|
-
// Check for specific section headers that indicate we've left TO VERIFY
|
|
591
|
-
|
|
592
|
-
if (isVerifiedSection || isTodoSection || isRecycledSection || isClarificationSection) {
|
|
593
|
-
break; // Different section, we've left TO VERIFY
|
|
594
|
-
}
|
|
595
|
-
// Otherwise, continue - might be REJECTED or CHANGELOG which are not section boundaries for TO VERIFY
|
|
596
|
-
} else {
|
|
597
|
-
// For other sections, break if it's a new section header that doesn't match
|
|
598
|
-
if (!sectionTitles.some(title => trimmed.includes(title))) {
|
|
599
|
-
break;
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
// Read requirements in new format (### header)
|
|
605
|
-
if (inSection && line.trim().startsWith('###')) {
|
|
606
|
-
const title = line.trim().replace(/^###\s*/, '').trim();
|
|
607
|
-
|
|
608
|
-
// Skip malformed requirements (title is just a package name, empty, or too short)
|
|
609
|
-
// Common package names that shouldn't be requirement titles
|
|
610
|
-
const packageNames = ['cli', 'core', 'electron-app', 'web', 'mobile', 'vscode-extension', 'sync-server'];
|
|
611
|
-
if (!title || title.length === 0 || packageNames.includes(title.toLowerCase())) {
|
|
612
|
-
continue; // Skip this malformed requirement
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
const details = [];
|
|
616
|
-
let pkg = null;
|
|
617
|
-
|
|
618
|
-
// Read package and description
|
|
619
|
-
for (let j = i + 1; j < lines.length; j++) {
|
|
620
|
-
const nextLine = lines[j].trim();
|
|
621
|
-
// Stop if we hit another requirement or section
|
|
622
|
-
if (nextLine.startsWith('###') || (nextLine.startsWith('##') && !nextLine.startsWith('###'))) {
|
|
623
|
-
break;
|
|
624
|
-
}
|
|
625
|
-
// Check for PACKAGE line
|
|
626
|
-
if (nextLine.startsWith('PACKAGE:')) {
|
|
627
|
-
pkg = nextLine.replace(/^PACKAGE:\s*/, '').trim();
|
|
628
|
-
} else if (nextLine && !nextLine.startsWith('PACKAGE:')) {
|
|
629
|
-
// Description line
|
|
630
|
-
details.push(nextLine);
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
requirements.push({ title, details, pkg, lineIndex: i });
|
|
635
|
-
}
|
|
636
|
-
}
|
|
695
|
+
// Delegate to reusable parser
|
|
696
|
+
const allReqs = parseRequirementsFromContent(content, sectionKey, sectionTitle);
|
|
637
697
|
|
|
638
|
-
//
|
|
639
|
-
|
|
640
|
-
const uniqueRequirements = [];
|
|
641
|
-
for (const req of requirements) {
|
|
642
|
-
const normalizedTitle = req.title.replace(/^TRY AGAIN \(\d+(st|nd|rd|th) time\):\s*/i, '').trim();
|
|
643
|
-
if (!seenTitles.has(normalizedTitle)) {
|
|
644
|
-
seenTitles.add(normalizedTitle);
|
|
645
|
-
uniqueRequirements.push(req);
|
|
646
|
-
}
|
|
647
|
-
}
|
|
698
|
+
// For TODO section, only show primary heading requirements (those marked from '###' titles)
|
|
699
|
+
if (sectionKey === 'todo') return allReqs.filter(r => r.source === 'heading');
|
|
648
700
|
|
|
649
|
-
return
|
|
701
|
+
return allReqs;
|
|
650
702
|
};
|
|
651
703
|
|
|
652
704
|
// Load VERIFIED requirements from CHANGELOG
|
|
@@ -708,49 +760,99 @@ async function showRequirementsTree() {
|
|
|
708
760
|
|
|
709
761
|
let inSection = false;
|
|
710
762
|
const requirements = [];
|
|
711
|
-
let currentReq = null;
|
|
712
763
|
|
|
713
764
|
for (let i = 0; i < lines.length; i++) {
|
|
714
765
|
const line = lines[i];
|
|
766
|
+
const trimmed = line.trim();
|
|
715
767
|
|
|
716
|
-
if
|
|
768
|
+
// Check if we're entering the clarification section
|
|
769
|
+
if (trimmed.includes('❓ Requirements needing manual feedback')) {
|
|
717
770
|
inSection = true;
|
|
718
771
|
continue;
|
|
719
772
|
}
|
|
720
773
|
|
|
721
|
-
|
|
722
|
-
|
|
774
|
+
// Check if we're leaving the section (hit another ## section)
|
|
775
|
+
if (inSection && trimmed.startsWith('##') && !trimmed.startsWith('###')) {
|
|
723
776
|
break;
|
|
724
777
|
}
|
|
725
778
|
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
if (!
|
|
747
|
-
|
|
779
|
+
// Read requirements in new format (### header)
|
|
780
|
+
if (inSection && trimmed.startsWith('###')) {
|
|
781
|
+
const title = trimmed.replace(/^###\s*/, '').trim();
|
|
782
|
+
|
|
783
|
+
// Skip empty titles
|
|
784
|
+
if (!title || title.length === 0) {
|
|
785
|
+
continue;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
const details = [];
|
|
789
|
+
const questions = [];
|
|
790
|
+
let pkg = null;
|
|
791
|
+
let findings = null;
|
|
792
|
+
let currentQuestion = null;
|
|
793
|
+
|
|
794
|
+
// Read package, description, and clarifying questions
|
|
795
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
796
|
+
const nextLine = lines[j].trim();
|
|
797
|
+
|
|
798
|
+
// Stop if we hit another requirement or section
|
|
799
|
+
if (nextLine.startsWith('###') || (nextLine.startsWith('##') && !nextLine.startsWith('###'))) {
|
|
800
|
+
break;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// Check for PACKAGE line
|
|
804
|
+
if (nextLine.startsWith('PACKAGE:')) {
|
|
805
|
+
pkg = nextLine.replace(/^PACKAGE:\s*/, '').trim();
|
|
806
|
+
}
|
|
807
|
+
// Check for AI findings
|
|
808
|
+
else if (nextLine.startsWith('**AI found in codebase:**')) {
|
|
809
|
+
// Next line will be the findings
|
|
810
|
+
continue;
|
|
811
|
+
}
|
|
812
|
+
else if (nextLine.startsWith('**What went wrong')) {
|
|
813
|
+
// Description line
|
|
814
|
+
details.push(nextLine);
|
|
815
|
+
}
|
|
816
|
+
else if (nextLine.startsWith('**Clarifying questions:**')) {
|
|
817
|
+
// Start of questions section
|
|
818
|
+
continue;
|
|
819
|
+
}
|
|
820
|
+
else if (nextLine.match(/^\d+\./)) {
|
|
821
|
+
// Save previous question if exists
|
|
822
|
+
if (currentQuestion) {
|
|
823
|
+
questions.push(currentQuestion);
|
|
824
|
+
}
|
|
825
|
+
// This is a new question
|
|
826
|
+
currentQuestion = { question: nextLine, response: null };
|
|
827
|
+
}
|
|
828
|
+
else if (currentQuestion && nextLine && !nextLine.startsWith('PACKAGE:') && !nextLine.startsWith('**')) {
|
|
829
|
+
// This might be a response to the current question or description/findings
|
|
830
|
+
if (!findings && !currentQuestion.response && questions.length === 0 && !nextLine.match(/^\d+\./)) {
|
|
831
|
+
// This is findings content
|
|
832
|
+
findings = nextLine;
|
|
833
|
+
} else if (currentQuestion && !currentQuestion.response) {
|
|
834
|
+
// This is a response to the current question
|
|
835
|
+
currentQuestion.response = nextLine;
|
|
836
|
+
} else {
|
|
837
|
+
// Description line
|
|
838
|
+
details.push(nextLine);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
else if (nextLine && !nextLine.startsWith('PACKAGE:') && !nextLine.startsWith('**')) {
|
|
842
|
+
// Description line
|
|
843
|
+
details.push(nextLine);
|
|
748
844
|
}
|
|
749
845
|
}
|
|
846
|
+
|
|
847
|
+
// Save last question if exists
|
|
848
|
+
if (currentQuestion) {
|
|
849
|
+
questions.push(currentQuestion);
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
requirements.push({ title, details, pkg, questions, findings, lineIndex: i });
|
|
750
853
|
}
|
|
751
854
|
}
|
|
752
855
|
|
|
753
|
-
if (currentReq) requirements.push(currentReq);
|
|
754
856
|
return requirements;
|
|
755
857
|
};
|
|
756
858
|
|
|
@@ -945,6 +1047,10 @@ async function showRequirementsTree() {
|
|
|
945
1047
|
tree.todoReqs = await loadSection('todo', '⏳ Requirements not yet completed');
|
|
946
1048
|
await buildTree();
|
|
947
1049
|
}
|
|
1050
|
+
} else if (key.name === 'f') {
|
|
1051
|
+
// Feedback button ( megaphone 📣 )
|
|
1052
|
+
await handleFeedbackSubmission();
|
|
1053
|
+
await buildTree();
|
|
948
1054
|
} else if (key.name === 'r') {
|
|
949
1055
|
const current = tree.items[tree.selected];
|
|
950
1056
|
if (!current) continue; // Safety check
|
|
@@ -1021,6 +1127,90 @@ async function showRequirementsTree() {
|
|
|
1021
1127
|
process.stdin.pause();
|
|
1022
1128
|
}
|
|
1023
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
|
+
|
|
1024
1214
|
// Helper to show goodbye message
|
|
1025
1215
|
function showGoodbyeMessage() {
|
|
1026
1216
|
const hour = new Date().getHours();
|
|
@@ -1369,6 +1559,7 @@ async function showClarificationActions(req, tree, loadClarification) {
|
|
|
1369
1559
|
const actions = [
|
|
1370
1560
|
{ label: '✍️ Add/Edit Responses', value: 'edit-responses' },
|
|
1371
1561
|
{ label: '↩️ Move back to TODO (after clarification)', value: 'move-to-todo' },
|
|
1562
|
+
{ label: '📣 Feedback', value: 'feedback' },
|
|
1372
1563
|
{ label: '🗑️ Delete', value: 'delete' }
|
|
1373
1564
|
];
|
|
1374
1565
|
|
|
@@ -1399,7 +1590,8 @@ async function showClarificationActions(req, tree, loadClarification) {
|
|
|
1399
1590
|
});
|
|
1400
1591
|
|
|
1401
1592
|
// Display menu
|
|
1402
|
-
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'));
|
|
1403
1595
|
actions.forEach((action, idx) => {
|
|
1404
1596
|
if (idx === selected) {
|
|
1405
1597
|
console.log(chalk.cyan(`❯ ${action.label}`));
|
|
@@ -1430,6 +1622,10 @@ async function showClarificationActions(req, tree, loadClarification) {
|
|
|
1430
1622
|
selected = Math.max(0, selected - 1);
|
|
1431
1623
|
} else if (key.name === 'down') {
|
|
1432
1624
|
selected = Math.min(actions.length - 1, selected + 1);
|
|
1625
|
+
} else if (key.name === 'f') {
|
|
1626
|
+
// Feedback button ( megaphone 📣 )
|
|
1627
|
+
await handleFeedbackSubmission();
|
|
1628
|
+
return;
|
|
1433
1629
|
} else if (key.name === 'return' || key.name === 'space') {
|
|
1434
1630
|
const action = actions[selected].value;
|
|
1435
1631
|
|
|
@@ -1441,6 +1637,9 @@ async function showClarificationActions(req, tree, loadClarification) {
|
|
|
1441
1637
|
await moveClarificationToTodo(req, tree);
|
|
1442
1638
|
tree.clarificationReqs = await loadClarification();
|
|
1443
1639
|
return;
|
|
1640
|
+
} else if (action === 'feedback') {
|
|
1641
|
+
await handleFeedbackSubmission();
|
|
1642
|
+
return;
|
|
1444
1643
|
} else if (action === 'delete') {
|
|
1445
1644
|
await deleteClarification(req, tree);
|
|
1446
1645
|
tree.clarificationReqs = await loadClarification();
|
|
@@ -1458,6 +1657,7 @@ async function showRequirementActions(req, sectionKey, tree) {
|
|
|
1458
1657
|
{ label: '👎 Thumbs down (demote to TODO)', value: 'thumbs-down' },
|
|
1459
1658
|
{ label: '⬆️ Move up', value: 'move-up' },
|
|
1460
1659
|
{ label: '⬇️ Move down', value: 'move-down' },
|
|
1660
|
+
{ label: '📣 Feedback', value: 'feedback' },
|
|
1461
1661
|
{ label: '🗑️ Delete', value: 'delete' }
|
|
1462
1662
|
];
|
|
1463
1663
|
|
|
@@ -1487,6 +1687,7 @@ async function showRequirementActions(req, sectionKey, tree) {
|
|
|
1487
1687
|
|
|
1488
1688
|
// Display menu (always reprinted)
|
|
1489
1689
|
console.log();
|
|
1690
|
+
console.log(chalk.gray('💡 Press F for feedback - Share your thoughts anytime'));
|
|
1490
1691
|
console.log(chalk.gray('What would you like to do? (↑/↓/Enter to select, ESC/← to go back)'));
|
|
1491
1692
|
console.log();
|
|
1492
1693
|
menuLines += 3; // Blank line + help text + blank line
|
|
@@ -1531,6 +1732,10 @@ async function showRequirementActions(req, sectionKey, tree) {
|
|
|
1531
1732
|
process.exit(0);
|
|
1532
1733
|
} else if (key.name === 'escape' || key.name === 'left') {
|
|
1533
1734
|
return; // Go back
|
|
1735
|
+
} else if (key.name === 'f') {
|
|
1736
|
+
// Feedback button ( megaphone 📣 )
|
|
1737
|
+
await handleFeedbackSubmission();
|
|
1738
|
+
return;
|
|
1534
1739
|
} else if (key.name === 'up') {
|
|
1535
1740
|
selected = Math.max(0, selected - 1);
|
|
1536
1741
|
} else if (key.name === 'down') {
|
|
@@ -1591,6 +1796,9 @@ async function performRequirementAction(action, req, sectionKey, tree) {
|
|
|
1591
1796
|
case 'rename':
|
|
1592
1797
|
await renameRequirement(req, sectionKey, tree);
|
|
1593
1798
|
break;
|
|
1799
|
+
case 'feedback':
|
|
1800
|
+
await handleFeedbackSubmission();
|
|
1801
|
+
break;
|
|
1594
1802
|
}
|
|
1595
1803
|
|
|
1596
1804
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
@@ -2357,7 +2565,7 @@ async function showRequirementsBySection(sectionTitle) {
|
|
|
2357
2565
|
}
|
|
2358
2566
|
break;
|
|
2359
2567
|
case 'delete':
|
|
2360
|
-
const { confirmDelete } = await
|
|
2568
|
+
const { confirmDelete } = await promptWithDefaultsOnce([{
|
|
2361
2569
|
type: 'confirm',
|
|
2362
2570
|
name: 'confirmDelete',
|
|
2363
2571
|
message: 'Are you sure you want to delete this requirement?',
|
|
@@ -2383,6 +2591,104 @@ async function showRequirementsBySection(sectionTitle) {
|
|
|
2383
2591
|
}
|
|
2384
2592
|
}
|
|
2385
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
|
+
|
|
2386
2692
|
// Helper to save reordered requirements back to file
|
|
2387
2693
|
async function saveRequirementsOrder(reqPath, sectionTitle, requirements) {
|
|
2388
2694
|
const content = await fs.readFile(reqPath, 'utf8');
|
|
@@ -2481,6 +2787,9 @@ async function showRequirementsFromChangelog() {
|
|
|
2481
2787
|
}
|
|
2482
2788
|
}
|
|
2483
2789
|
|
|
2790
|
+
// Input suppression timestamp to avoid immediate re-entry into menus after cancel
|
|
2791
|
+
let menuSuppressUntil = 0;
|
|
2792
|
+
|
|
2484
2793
|
// Custom menu with both arrow keys and letter shortcuts
|
|
2485
2794
|
async function showQuickMenu(items, initialSelectedIndex = 0) {
|
|
2486
2795
|
return new Promise((resolve) => {
|
|
@@ -2492,6 +2801,7 @@ async function showQuickMenu(items, initialSelectedIndex = 0) {
|
|
|
2492
2801
|
if (selectedIndex >= items.length) selectedIndex = 0;
|
|
2493
2802
|
|
|
2494
2803
|
let isFirstRender = true;
|
|
2804
|
+
let lastLinesPrinted = 0;
|
|
2495
2805
|
|
|
2496
2806
|
// Helper to calculate visual lines occupied by text
|
|
2497
2807
|
const getVisualLineCount = (text) => {
|
|
@@ -2631,19 +2941,42 @@ async function showQuickMenu(items, initialSelectedIndex = 0) {
|
|
|
2631
2941
|
process.stdin.setRawMode(true);
|
|
2632
2942
|
}
|
|
2633
2943
|
|
|
2634
|
-
|
|
2635
|
-
|
|
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);
|
|
2636
2949
|
|
|
2637
|
-
|
|
2638
|
-
if (
|
|
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') {
|
|
2639
2958
|
cleanup();
|
|
2640
2959
|
process.exit(0);
|
|
2641
2960
|
return;
|
|
2642
2961
|
}
|
|
2643
2962
|
|
|
2644
|
-
//
|
|
2645
|
-
if (
|
|
2646
|
-
|
|
2963
|
+
// Debounce: ignore inputs until the short window passes
|
|
2964
|
+
if (Date.now() < ignoreInputUntil) {
|
|
2965
|
+
return;
|
|
2966
|
+
}
|
|
2967
|
+
|
|
2968
|
+
if (!key) return;
|
|
2969
|
+
|
|
2970
|
+
// Ctrl+C to exit
|
|
2971
|
+
if (key.ctrl && key.name === 'c') {
|
|
2972
|
+
cleanup();
|
|
2973
|
+
process.exit(0);
|
|
2974
|
+
return;
|
|
2975
|
+
}
|
|
2976
|
+
|
|
2977
|
+
// ESC or left arrow to exit with confirmation
|
|
2978
|
+
if (key.name === 'escape' || key.name === 'left') {
|
|
2979
|
+
cleanup();
|
|
2647
2980
|
// Don't clear screen for exit - keep status visible
|
|
2648
2981
|
resolve({ value: 'exit', selectedIndex });
|
|
2649
2982
|
return;
|
|
@@ -2653,6 +2986,7 @@ async function showQuickMenu(items, initialSelectedIndex = 0) {
|
|
|
2653
2986
|
if (str && str.length === 1) {
|
|
2654
2987
|
if (str === 'x') {
|
|
2655
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)`);
|
|
2656
2990
|
cleanup();
|
|
2657
2991
|
// Don't clear screen for exit - keep status visible
|
|
2658
2992
|
resolve({ value: 'exit', selectedIndex });
|
|
@@ -2712,6 +3046,11 @@ async function showQuickMenu(items, initialSelectedIndex = 0) {
|
|
|
2712
3046
|
}
|
|
2713
3047
|
|
|
2714
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
|
+
}
|
|
2715
3054
|
const definitions = getProviderDefinitions();
|
|
2716
3055
|
const defMap = new Map(definitions.map(def => [def.id, def]));
|
|
2717
3056
|
const prefs = await getProviderPreferences();
|
|
@@ -2721,6 +3060,8 @@ async function showProviderManagerMenu() {
|
|
|
2721
3060
|
let dirty = false;
|
|
2722
3061
|
|
|
2723
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();
|
|
2724
3065
|
|
|
2725
3066
|
const debugQuota = process.env.VCM_DEBUG_QUOTA === '1' || process.env.VCM_DEBUG_QUOTA === 'true';
|
|
2726
3067
|
|
|
@@ -2736,14 +3077,76 @@ async function showProviderManagerMenu() {
|
|
|
2736
3077
|
return `${seconds}s`;
|
|
2737
3078
|
};
|
|
2738
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
|
+
|
|
2739
3144
|
const render = async () => {
|
|
3145
|
+
if (!isMenuActive) return; // Prevent rendering after menu closed
|
|
2740
3146
|
process.stdout.write('\x1Bc');
|
|
2741
3147
|
console.log(chalk.bold.cyan('⚙ ' + t('provider.title') + '\n'));
|
|
2742
3148
|
|
|
2743
|
-
//
|
|
2744
|
-
const sharedAuth = require('vibecodingmachine-core/src/auth/shared-auth-storage');
|
|
2745
|
-
const autoConfig = await getAutoConfig();
|
|
2746
|
-
const quotaInfo = await sharedAuth.canRunAutoMode();
|
|
3149
|
+
// Use cached quota info
|
|
2747
3150
|
const remaining = Math.max(0, (quotaInfo.maxIterations || 10) - (quotaInfo.todayUsage || 0));
|
|
2748
3151
|
|
|
2749
3152
|
// Calculate time until reset (midnight)
|
|
@@ -2754,15 +3157,65 @@ async function showProviderManagerMenu() {
|
|
|
2754
3157
|
const hoursUntilReset = Math.floor(msUntilReset / (1000 * 60 * 60));
|
|
2755
3158
|
const minsUntilReset = Math.floor((msUntilReset % (1000 * 60 * 60)) / (1000 * 60));
|
|
2756
3159
|
|
|
2757
|
-
// 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.
|
|
2758
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
|
+
|
|
2759
3175
|
if (remaining === 0) {
|
|
2760
|
-
//
|
|
3176
|
+
// Overall rate limit active - show when it resets (in red)
|
|
2761
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
|
+
}
|
|
2762
3205
|
} else {
|
|
2763
3206
|
// Quota available - show when it resets (in green)
|
|
2764
|
-
|
|
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
|
+
}
|
|
2765
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)'));
|
|
2766
3219
|
console.log(quotaDisplay);
|
|
2767
3220
|
console.log(chalk.gray(' ' + t('provider.instructions') + '\n'));
|
|
2768
3221
|
|
|
@@ -2773,75 +3226,137 @@ async function showProviderManagerMenu() {
|
|
|
2773
3226
|
const isSelected = idx === selectedIndex;
|
|
2774
3227
|
const isEnabled = enabled[id] !== false;
|
|
2775
3228
|
|
|
2776
|
-
//
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
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
|
|
2786
3240
|
|
|
2787
|
-
// Determine status emoji: disabled = red alert, rate limited = green circle, enabled = green circle
|
|
2788
3241
|
let statusEmoji;
|
|
2789
|
-
|
|
2790
|
-
|
|
3242
|
+
let statusText = '';
|
|
3243
|
+
|
|
3244
|
+
if (!isEnabled) {
|
|
3245
|
+
statusEmoji = '🚫'; // Disabled
|
|
3246
|
+
} else if (!isInstalled) {
|
|
3247
|
+
statusEmoji = '⬜'; // Not installed
|
|
2791
3248
|
} else {
|
|
2792
|
-
|
|
2793
|
-
|
|
3249
|
+
// Installed and Enabled, assume Green unless quota or error
|
|
3250
|
+
statusEmoji = '🟢';
|
|
2794
3251
|
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
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
|
+
}
|
|
2798
3257
|
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
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
|
+
}
|
|
2810
3277
|
}
|
|
3278
|
+
}
|
|
2811
3279
|
|
|
2812
|
-
|
|
2813
|
-
|
|
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
|
+
}
|
|
3285
|
+
|
|
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
|
+
}
|
|
2814
3291
|
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
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}`;
|
|
2818
3323
|
}
|
|
3324
|
+
}
|
|
2819
3325
|
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
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]')}`;
|
|
2827
3350
|
} else {
|
|
2828
|
-
|
|
3351
|
+
subStatusText = ` ${chalk.red('[Rate limited]')}`;
|
|
2829
3352
|
}
|
|
2830
3353
|
} else {
|
|
2831
|
-
|
|
2832
|
-
if (quota.resetsAt) {
|
|
2833
|
-
const msUntilReset = new Date(quota.resetsAt).getTime() - Date.now();
|
|
2834
|
-
line += ` ${chalk.green(`[✓ ${t('provider.status.available.resets')} ${formatDuration(msUntilReset)}]`)}`;
|
|
2835
|
-
} else {
|
|
2836
|
-
line += ` ${chalk.green('[' + t('provider.status.available') + ']')}`;
|
|
2837
|
-
}
|
|
3354
|
+
subStatusEmoji = '🟢';
|
|
2838
3355
|
}
|
|
3356
|
+
|
|
3357
|
+
console.log(` ${subStatusEmoji} ${chalk.gray('↳')} ${sub.name} ${chalk.gray(`(${sub.model})`)}${subStatusText}`);
|
|
2839
3358
|
}
|
|
2840
|
-
} catch (e) {
|
|
2841
|
-
// Silently skip if quota fetch fails
|
|
2842
3359
|
}
|
|
2843
|
-
|
|
2844
|
-
console.log(line);
|
|
2845
3360
|
}
|
|
2846
3361
|
|
|
2847
3362
|
console.log();
|
|
@@ -2852,6 +3367,143 @@ async function showProviderManagerMenu() {
|
|
|
2852
3367
|
}
|
|
2853
3368
|
};
|
|
2854
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
|
+
|
|
2855
3507
|
if (process.env.VCM_RENDER_ONCE === '1' || process.env.VCM_RENDER_ONCE === 'true') {
|
|
2856
3508
|
await render();
|
|
2857
3509
|
return;
|
|
@@ -2859,6 +3511,8 @@ async function showProviderManagerMenu() {
|
|
|
2859
3511
|
|
|
2860
3512
|
return new Promise(async (resolve) => {
|
|
2861
3513
|
const cleanup = () => {
|
|
3514
|
+
// Mark menu as inactive so background loaders stop rendering
|
|
3515
|
+
isMenuActive = false;
|
|
2862
3516
|
if (process.stdin.isTTY && process.stdin.setRawMode) {
|
|
2863
3517
|
process.stdin.setRawMode(false);
|
|
2864
3518
|
}
|
|
@@ -2895,6 +3549,13 @@ async function showProviderManagerMenu() {
|
|
|
2895
3549
|
};
|
|
2896
3550
|
|
|
2897
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
|
+
}
|
|
2898
3559
|
cleanup();
|
|
2899
3560
|
console.log('\n');
|
|
2900
3561
|
resolve(null);
|
|
@@ -2926,12 +3587,104 @@ async function showProviderManagerMenu() {
|
|
|
2926
3587
|
await render();
|
|
2927
3588
|
};
|
|
2928
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
|
+
|
|
2929
3678
|
const onKeypress = async (str, key = {}) => {
|
|
3679
|
+
// Allow immediate Ctrl+C regardless of debounce
|
|
2930
3680
|
if (key.ctrl && key.name === 'c') {
|
|
2931
3681
|
cancel();
|
|
2932
3682
|
return;
|
|
2933
3683
|
}
|
|
2934
3684
|
|
|
3685
|
+
// Debounce: ignore inputs that happen too soon after the menu opens
|
|
3686
|
+
if (Date.now() < ignoreInputUntil) return;
|
|
3687
|
+
|
|
2935
3688
|
switch (key.name) {
|
|
2936
3689
|
case 'up':
|
|
2937
3690
|
await moveSelection(-1);
|
|
@@ -2951,15 +3704,22 @@ async function showProviderManagerMenu() {
|
|
|
2951
3704
|
case 'd':
|
|
2952
3705
|
await toggle(false);
|
|
2953
3706
|
break;
|
|
3707
|
+
case 'i':
|
|
3708
|
+
await installSelected();
|
|
3709
|
+
break;
|
|
2954
3710
|
case 'space':
|
|
2955
3711
|
await toggle(!(enabled[order[selectedIndex]] !== false));
|
|
2956
3712
|
break;
|
|
2957
3713
|
case 'return':
|
|
2958
3714
|
saveAndExit(order[selectedIndex]);
|
|
2959
3715
|
break;
|
|
2960
|
-
case 'escape':
|
|
2961
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':
|
|
2962
3721
|
case 'x':
|
|
3722
|
+
// Escape or 'x' should cancel without persisting pending changes
|
|
2963
3723
|
cancel();
|
|
2964
3724
|
break;
|
|
2965
3725
|
default:
|
|
@@ -2978,23 +3738,54 @@ async function showProviderManagerMenu() {
|
|
|
2978
3738
|
});
|
|
2979
3739
|
}
|
|
2980
3740
|
|
|
2981
|
-
|
|
2982
|
-
|
|
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
|
+
}
|
|
2983
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
|
+
}
|
|
3771
|
+
|
|
3772
|
+
async function showSettings() {
|
|
3773
|
+
console.log(chalk.bold.cyan('\n⚙️ Settings\n'));
|
|
3774
|
+
|
|
2984
3775
|
const { setConfigValue } = require('vibecodingmachine-core');
|
|
2985
3776
|
const { getAutoConfig, setAutoConfig } = require('./config');
|
|
2986
3777
|
const currentHostnameEnabled = await isComputerNameEnabled();
|
|
2987
3778
|
const hostname = getHostname();
|
|
2988
3779
|
const autoConfig = await getAutoConfig();
|
|
2989
3780
|
const currentIDE = autoConfig.ide || 'claude-code';
|
|
2990
|
-
|
|
3781
|
+
|
|
2991
3782
|
// Show current settings
|
|
2992
3783
|
console.log(chalk.gray('Current settings:'));
|
|
2993
3784
|
console.log(chalk.gray(' Computer Name: '), chalk.cyan(hostname));
|
|
2994
3785
|
console.log(chalk.gray(' Current IDE: '), chalk.cyan(formatIDEName(currentIDE)));
|
|
2995
3786
|
console.log(chalk.gray(' Use Hostname in Req File: '), currentHostnameEnabled ? chalk.green('Enabled ✓') : chalk.yellow('Disabled ○'));
|
|
2996
3787
|
console.log();
|
|
2997
|
-
|
|
3788
|
+
|
|
2998
3789
|
const { action } = await inquirer.prompt([
|
|
2999
3790
|
{
|
|
3000
3791
|
type: 'list',
|
|
@@ -3016,54 +3807,59 @@ async function showProviderManagerMenu() {
|
|
|
3016
3807
|
]
|
|
3017
3808
|
}
|
|
3018
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
|
+
}
|
|
3019
3824
|
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
{ name: 'Continue CLI (Ollama support, but --auto mode doesn\'t execute code)', value: 'continue' },
|
|
3030
|
-
{ name: 'Cline CLI (local AI alternative, but has Ollama connection issues)', value: 'cline' },
|
|
3031
|
-
{ name: 'Cursor', value: 'cursor' },
|
|
3032
|
-
{ name: 'VS Code', value: 'vscode' },
|
|
3033
|
-
{ name: 'Windsurf', value: 'windsurf' }
|
|
3034
|
-
],
|
|
3035
|
-
default: currentIDE
|
|
3036
|
-
}
|
|
3037
|
-
]);
|
|
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
|
+
}
|
|
3038
3834
|
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
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
|
+
}
|
|
3042
3841
|
|
|
3043
|
-
|
|
3044
|
-
console.log(chalk.gray(' Note: This will be used for the next Auto Mode session.'));
|
|
3045
|
-
console.log();
|
|
3046
|
-
} else if (action === 'toggle-hostname') {
|
|
3842
|
+
async function showSettings() {
|
|
3047
3843
|
const newValue = !currentHostnameEnabled;
|
|
3048
|
-
|
|
3844
|
+
|
|
3049
3845
|
// Save to shared config (same location as Electron app)
|
|
3050
3846
|
await setConfigValue('computerNameEnabled', newValue);
|
|
3051
|
-
|
|
3847
|
+
|
|
3052
3848
|
const statusText = newValue ? chalk.green('enabled') : chalk.yellow('disabled');
|
|
3053
3849
|
console.log(chalk.green('\n✓'), `Hostname in requirements file ${statusText}`);
|
|
3054
|
-
|
|
3850
|
+
|
|
3055
3851
|
if (newValue) {
|
|
3056
3852
|
console.log(chalk.gray('\n Requirements file will be:'), chalk.cyan(`REQUIREMENTS-${hostname}.md`));
|
|
3057
3853
|
} else {
|
|
3058
3854
|
console.log(chalk.gray('\n Requirements file will be:'), chalk.cyan('REQUIREMENTS.md'));
|
|
3059
3855
|
}
|
|
3060
|
-
|
|
3856
|
+
|
|
3061
3857
|
console.log(chalk.gray('\n Note: You may need to rename your existing requirements file.'));
|
|
3062
3858
|
console.log(chalk.gray(' Note: This setting is now synced with the Electron app.'));
|
|
3063
3859
|
console.log();
|
|
3064
3860
|
}
|
|
3065
3861
|
}
|
|
3066
|
-
|
|
3862
|
+
|
|
3067
3863
|
/**
|
|
3068
3864
|
* Show cloud sync management menu
|
|
3069
3865
|
*/
|
|
@@ -3451,24 +4247,13 @@ async function startInteractive() {
|
|
|
3451
4247
|
const provider = displayAgent === 'ollama' ? 'ollama' : displayAgent;
|
|
3452
4248
|
|
|
3453
4249
|
if (checkModel) {
|
|
3454
|
-
const
|
|
3455
|
-
|
|
3456
|
-
if (timeUntilReset) {
|
|
3457
|
-
// Format time remaining in human-readable format
|
|
3458
|
-
const hours = Math.floor(timeUntilReset / (1000 * 60 * 60));
|
|
3459
|
-
const minutes = Math.floor((timeUntilReset % (1000 * 60 * 60)) / (1000 * 60));
|
|
3460
|
-
const seconds = Math.floor((timeUntilReset % (1000 * 60)) / 1000);
|
|
3461
|
-
|
|
3462
|
-
let timeStr = '';
|
|
3463
|
-
if (hours > 0) {
|
|
3464
|
-
timeStr = `${hours}h ${minutes}m`;
|
|
3465
|
-
} else if (minutes > 0) {
|
|
3466
|
-
timeStr = `${minutes}m ${seconds}s`;
|
|
3467
|
-
} else {
|
|
3468
|
-
timeStr = `${seconds}s`;
|
|
3469
|
-
}
|
|
4250
|
+
const info = providerManager.getRateLimitInfo(provider, checkModel);
|
|
3470
4251
|
|
|
3471
|
-
|
|
4252
|
+
if (info && info.isRateLimited && info.resetTime) {
|
|
4253
|
+
const label = formatResetsAtLabel(info.resetTime);
|
|
4254
|
+
if (label) {
|
|
4255
|
+
agentDisplay += ` ${chalk.red('⏰ ' + label)}`;
|
|
4256
|
+
}
|
|
3472
4257
|
}
|
|
3473
4258
|
}
|
|
3474
4259
|
}
|
|
@@ -3516,7 +4301,9 @@ async function startInteractive() {
|
|
|
3516
4301
|
|
|
3517
4302
|
// Add Requirements as a selectable setting with counts
|
|
3518
4303
|
const hasRequirements = await requirementsExists();
|
|
3519
|
-
const
|
|
4304
|
+
const { getComputerFilter } = require('./config');
|
|
4305
|
+
const computerFilter = await getComputerFilter();
|
|
4306
|
+
const counts = hasRequirements ? await countRequirements(computerFilter) : null;
|
|
3520
4307
|
let requirementsText = t('interactive.requirements') + ': ';
|
|
3521
4308
|
if (counts) {
|
|
3522
4309
|
// Use actual counts for display (not capped by maxChats)
|
|
@@ -3538,11 +4325,19 @@ async function startInteractive() {
|
|
|
3538
4325
|
if (requirementsText !== '') {
|
|
3539
4326
|
items.push({
|
|
3540
4327
|
type: 'setting',
|
|
3541
|
-
name: requirementsText
|
|
4328
|
+
name: requirementsText + ` ${chalk.blue('Sync Status: ' + (await getSyncStatus()))}`,
|
|
3542
4329
|
value: 'setting:requirements'
|
|
3543
4330
|
});
|
|
3544
4331
|
}
|
|
3545
4332
|
|
|
4333
|
+
// Add requirement filtering by computer
|
|
4334
|
+
const filterText = computerFilter ? chalk.cyan(computerFilter) : chalk.gray('All Computers');
|
|
4335
|
+
items.push({
|
|
4336
|
+
type: 'setting',
|
|
4337
|
+
name: ` └─ Filter Requirements by Computer: ${filterText}`,
|
|
4338
|
+
value: 'setting:filter-requirements'
|
|
4339
|
+
});
|
|
4340
|
+
|
|
3546
4341
|
items.push({
|
|
3547
4342
|
type: 'setting',
|
|
3548
4343
|
name: ` └─ ${t('interactive.hostname.enabled')}: ${useHostname ? chalk.green('✓') : chalk.red('🛑')} ${useHostname ? '✓ ENABLED' : '🛑 ' + t('interactive.hostname.disabled')}`,
|
|
@@ -3559,44 +4354,33 @@ async function startInteractive() {
|
|
|
3559
4354
|
value: 'setting:stages'
|
|
3560
4355
|
});
|
|
3561
4356
|
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
4357
|
+
items.push({
|
|
4358
|
+
type: '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'
|
|
4361
|
+
});
|
|
3566
4362
|
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
const onlineIcon = syncStatus.isOnline ? chalk.green('●') : chalk.red('●');
|
|
3574
|
-
const onlineText = syncStatus.isOnline ? t('interactive.online') : t('interactive.offline');
|
|
3575
|
-
const queueText = syncStatus.queuedChanges > 0 ? chalk.yellow(` (${syncStatus.queuedChanges} queued)`) : '';
|
|
3576
|
-
|
|
3577
|
-
items.push({
|
|
3578
|
-
type: 'setting',
|
|
3579
|
-
name: `${t('interactive.cloud.sync')}: ${onlineIcon} ${onlineText}${queueText}`,
|
|
3580
|
-
value: 'setting:cloud-sync'
|
|
3581
|
-
});
|
|
3582
|
-
} catch (initError) {
|
|
3583
|
-
// Initialization failed - AWS not configured or credentials missing
|
|
3584
|
-
items.push({
|
|
3585
|
-
type: 'setting',
|
|
3586
|
-
name: `Cloud Sync: ${chalk.gray('● Not configured')}`,
|
|
3587
|
-
value: 'setting:cloud-sync-setup'
|
|
3588
|
-
});
|
|
3589
|
-
}
|
|
3590
|
-
} catch (error) {
|
|
3591
|
-
// Module not found or other error - show disabled
|
|
3592
|
-
items.push({
|
|
3593
|
-
type: 'setting',
|
|
3594
|
-
name: `Cloud Sync: ${chalk.gray('● Disabled')}`,
|
|
3595
|
-
value: 'setting:cloud-sync-setup'
|
|
3596
|
-
});
|
|
3597
|
-
}
|
|
4363
|
+
items.push({
|
|
4364
|
+
type: 'setting',
|
|
4365
|
+
name: ` └─ ${t('interactive.feedback')}: ${chalk.cyan('📣 Press Ctrl+F for feedback')}`,
|
|
4366
|
+
value: 'setting:feedback'
|
|
4367
|
+
});
|
|
3598
4368
|
|
|
3599
|
-
//
|
|
4369
|
+
// TODO: Implement getNextTodoRequirement function
|
|
4370
|
+
// if (counts && counts.todoCount > 0) {
|
|
4371
|
+
// const { getNextTodoRequirement } = require('./auto-mode');
|
|
4372
|
+
// const nextRequirement = await getNextTodoRequirement();
|
|
4373
|
+
// if (nextRequirement) {
|
|
4374
|
+
// const requirementPreview = nextRequirement.length > 80
|
|
4375
|
+
// ? nextRequirement.substring(0, 77) + '...'
|
|
4376
|
+
// : nextRequirement;
|
|
4377
|
+
// items.push({
|
|
4378
|
+
// type: 'info',
|
|
4379
|
+
// name: ` └─ ${t('interactive.next.requirement')}: ${chalk.cyan(requirementPreview)}`,
|
|
4380
|
+
// value: 'info:next-requirement'
|
|
4381
|
+
// });
|
|
4382
|
+
// }
|
|
4383
|
+
// }
|
|
3600
4384
|
if (counts && counts.todoCount > 0) {
|
|
3601
4385
|
// Get the actual next requirement text (new header format)
|
|
3602
4386
|
let nextReqText = '...';
|
|
@@ -3605,6 +4389,9 @@ async function startInteractive() {
|
|
|
3605
4389
|
const hostname = await getHostname();
|
|
3606
4390
|
const reqFilename = await getRequirementsFilename(hostname);
|
|
3607
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
|
+
|
|
3608
4395
|
|
|
3609
4396
|
if (await fs.pathExists(reqPath)) {
|
|
3610
4397
|
const reqContent = await fs.readFile(reqPath, 'utf8');
|
|
@@ -3714,10 +4501,20 @@ async function startInteractive() {
|
|
|
3714
4501
|
|
|
3715
4502
|
switch (actionStr) {
|
|
3716
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
|
+
}
|
|
3717
4509
|
await showProviderManagerMenu();
|
|
3718
4510
|
await showWelcomeScreen();
|
|
3719
4511
|
break;
|
|
3720
4512
|
}
|
|
4513
|
+
case 'setting:feedback': {
|
|
4514
|
+
await handleFeedbackSubmission();
|
|
4515
|
+
await showWelcomeScreen();
|
|
4516
|
+
break;
|
|
4517
|
+
}
|
|
3721
4518
|
case 'setting:provider': {
|
|
3722
4519
|
// Switch AI provider - run provider setup only, don't start auto mode
|
|
3723
4520
|
// Note: Continue CLI doesn't have provider setup - models are configured in ~/.continue/config.yaml
|
|
@@ -3920,6 +4717,69 @@ async function startInteractive() {
|
|
|
3920
4717
|
await showWelcomeScreen();
|
|
3921
4718
|
break;
|
|
3922
4719
|
}
|
|
4720
|
+
case 'setting:filter-requirements': {
|
|
4721
|
+
// Configure requirement filtering by computer
|
|
4722
|
+
const { getComputerFilter, setComputerFilter } = require('./config');
|
|
4723
|
+
const inquirer = require('inquirer');
|
|
4724
|
+
const repoPath = await getRepoPath();
|
|
4725
|
+
|
|
4726
|
+
if (!repoPath) {
|
|
4727
|
+
console.log(chalk.yellow('No repository configured. Use repo:init or repo:set first.'));
|
|
4728
|
+
await showWelcomeScreen();
|
|
4729
|
+
break;
|
|
4730
|
+
}
|
|
4731
|
+
|
|
4732
|
+
// Find all REQUIREMENTS-*.md files to get available computers
|
|
4733
|
+
const vibeDir = path.join(repoPath, '.vibecodingmachine');
|
|
4734
|
+
const files = await fs.pathExists(vibeDir) ? await fs.readdir(vibeDir) : [];
|
|
4735
|
+
const reqFiles = files.filter(f => f.startsWith('REQUIREMENTS') && f.endsWith('.md'));
|
|
4736
|
+
|
|
4737
|
+
const computers = [];
|
|
4738
|
+
for (const file of reqFiles) {
|
|
4739
|
+
const filePath = path.join(vibeDir, file);
|
|
4740
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
4741
|
+
const hostnameMatch = content.match(/- \*\*Hostname\*\*:\s*(.+)/);
|
|
4742
|
+
if (hostnameMatch) {
|
|
4743
|
+
computers.push(hostnameMatch[1].trim());
|
|
4744
|
+
}
|
|
4745
|
+
}
|
|
4746
|
+
|
|
4747
|
+
if (computers.length === 0) {
|
|
4748
|
+
console.log(chalk.yellow('No computers found in requirements files.'));
|
|
4749
|
+
await showWelcomeScreen();
|
|
4750
|
+
break;
|
|
4751
|
+
}
|
|
4752
|
+
|
|
4753
|
+
const currentFilter = await getComputerFilter();
|
|
4754
|
+
|
|
4755
|
+
console.log(chalk.cyan(`\n🖥️ ${t('interactive.filter.requirements.by.computer')}\n`));
|
|
4756
|
+
console.log(chalk.gray('Select which computer\'s requirements to show, or "All Computers" to show all.\n'));
|
|
4757
|
+
|
|
4758
|
+
const { selectedComputer } = await inquirer.prompt([
|
|
4759
|
+
{
|
|
4760
|
+
type: 'list',
|
|
4761
|
+
name: 'selectedComputer',
|
|
4762
|
+
message: t('interactive.select.computer'),
|
|
4763
|
+
choices: [
|
|
4764
|
+
{ name: chalk.gray('All Computers'), value: null },
|
|
4765
|
+
...computers.map(c => ({
|
|
4766
|
+
name: c === currentFilter ? chalk.cyan(`${c} (current)`) : c,
|
|
4767
|
+
value: c
|
|
4768
|
+
}))
|
|
4769
|
+
],
|
|
4770
|
+
default: currentFilter || null,
|
|
4771
|
+
loop: false,
|
|
4772
|
+
pageSize: 15
|
|
4773
|
+
}
|
|
4774
|
+
]);
|
|
4775
|
+
|
|
4776
|
+
await setComputerFilter(selectedComputer);
|
|
4777
|
+
|
|
4778
|
+
const filterText = selectedComputer ? chalk.cyan(selectedComputer) : chalk.gray('All Computers');
|
|
4779
|
+
console.log(chalk.green('\n✓'), `Requirements filter set to: ${filterText}\n`);
|
|
4780
|
+
await showWelcomeScreen();
|
|
4781
|
+
break;
|
|
4782
|
+
}
|
|
3923
4783
|
case 'setting:stages': {
|
|
3924
4784
|
// Configure stages
|
|
3925
4785
|
const { getStages, setStages, DEFAULT_STAGES } = require('./config');
|
|
@@ -3927,11 +4787,11 @@ async function startInteractive() {
|
|
|
3927
4787
|
|
|
3928
4788
|
// Override inquirer's checkbox help text for current locale
|
|
3929
4789
|
const CheckboxPrompt = require('inquirer/lib/prompts/checkbox');
|
|
3930
|
-
const originalGetHelpText = CheckboxPrompt.prototype.getHelp || function() {
|
|
4790
|
+
const originalGetHelpText = CheckboxPrompt.prototype.getHelp || function () {
|
|
3931
4791
|
return '(Press <space> to select, <a> to toggle all, <i> to invert selection, and <enter> to proceed)';
|
|
3932
4792
|
};
|
|
3933
4793
|
|
|
3934
|
-
CheckboxPrompt.prototype.getHelp = function() {
|
|
4794
|
+
CheckboxPrompt.prototype.getHelp = function () {
|
|
3935
4795
|
const locale = detectLocale();
|
|
3936
4796
|
if (locale === 'es') {
|
|
3937
4797
|
return '(Presiona <espacio> para seleccionar, <a> para alternar todo, <i> para invertir selección, y <enter> para proceder)';
|
|
@@ -4013,8 +4873,29 @@ async function startInteractive() {
|
|
|
4013
4873
|
const { getAutoConfig } = require('./config');
|
|
4014
4874
|
const currentConfig = await getAutoConfig();
|
|
4015
4875
|
|
|
4876
|
+
// Get first enabled agent from provider preferences (this is what user expects)
|
|
4877
|
+
const { getProviderPreferences } = require('../utils/provider-registry');
|
|
4878
|
+
const prefs = await getProviderPreferences();
|
|
4879
|
+
console.log(chalk.gray('[DEBUG] Provider preferences:'), JSON.stringify(prefs, null, 2));
|
|
4880
|
+
let firstEnabledAgent = null;
|
|
4881
|
+
for (const agentId of prefs.order) {
|
|
4882
|
+
if (prefs.enabled[agentId] !== false) {
|
|
4883
|
+
firstEnabledAgent = agentId;
|
|
4884
|
+
console.log(chalk.gray('[DEBUG] Found first enabled agent:'), firstEnabledAgent);
|
|
4885
|
+
break;
|
|
4886
|
+
}
|
|
4887
|
+
}
|
|
4888
|
+
const agentToUse = firstEnabledAgent || currentIDE;
|
|
4889
|
+
console.log(chalk.gray('[DEBUG] Agent to use:'), agentToUse, '(firstEnabled:', firstEnabledAgent, ', fallback:', currentIDE, ')');
|
|
4890
|
+
|
|
4016
4891
|
// Use saved maxChats/neverStop settings
|
|
4017
|
-
const options = { ide:
|
|
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
|
+
|
|
4018
4899
|
if (currentConfig.neverStop) {
|
|
4019
4900
|
options.neverStop = true;
|
|
4020
4901
|
} else if (currentConfig.maxChats) {
|
|
@@ -4034,10 +4915,9 @@ async function startInteractive() {
|
|
|
4034
4915
|
}
|
|
4035
4916
|
|
|
4036
4917
|
// ALWAYS use auto:direct (supports both LLM and IDE agents with proper looping)
|
|
4037
|
-
|
|
4038
|
-
console.log(chalk.gray('[DEBUG] Using auto:direct for agent:', currentAgent));
|
|
4918
|
+
console.log(chalk.gray('[DEBUG] Using auto:direct for agent:', agentToUse));
|
|
4039
4919
|
const { handleAutoStart: handleDirectAutoStart } = require('../commands/auto-direct');
|
|
4040
|
-
await handleDirectAutoStart(
|
|
4920
|
+
await handleDirectAutoStart(options);
|
|
4041
4921
|
|
|
4042
4922
|
// Prompt user before returning to menu (so they can read the output)
|
|
4043
4923
|
console.log('');
|
|
@@ -4252,8 +5132,11 @@ async function startInteractive() {
|
|
|
4252
5132
|
{ name: 'Continue CLI (Ollama support, but --auto mode doesn\'t execute code)', value: 'continue' },
|
|
4253
5133
|
{ name: 'Cline CLI (local AI alternative, but has Ollama connection issues)', value: 'cline' },
|
|
4254
5134
|
{ name: 'Cursor', value: 'cursor' },
|
|
4255
|
-
{ name: 'VS Code', value: '
|
|
4256
|
-
{ 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' }
|
|
4257
5140
|
],
|
|
4258
5141
|
default: 'claude-code'
|
|
4259
5142
|
},
|
|
@@ -4264,7 +5147,42 @@ async function startInteractive() {
|
|
|
4264
5147
|
default: ''
|
|
4265
5148
|
}
|
|
4266
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
|
+
|
|
4267
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
|
+
|
|
4268
5186
|
if (maxChats && maxChats.trim() !== '0') {
|
|
4269
5187
|
options.maxChats = parseInt(maxChats);
|
|
4270
5188
|
} else {
|
|
@@ -4286,6 +5204,21 @@ async function startInteractive() {
|
|
|
4286
5204
|
const hostname = getHostname();
|
|
4287
5205
|
console.log('[DEBUG] Repo:', repoPath, 'Hostname:', hostname);
|
|
4288
5206
|
|
|
5207
|
+
// Get current git branch if in a git repo
|
|
5208
|
+
let currentBranch = '';
|
|
5209
|
+
if (repoPath) {
|
|
5210
|
+
try {
|
|
5211
|
+
const { execSync } = require('child_process');
|
|
5212
|
+
currentBranch = execSync('git branch --show-current', {
|
|
5213
|
+
cwd: repoPath,
|
|
5214
|
+
encoding: 'utf8'
|
|
5215
|
+
}).trim();
|
|
5216
|
+
} catch (err) {
|
|
5217
|
+
// Not a git repo or git not available
|
|
5218
|
+
currentBranch = '';
|
|
5219
|
+
}
|
|
5220
|
+
}
|
|
5221
|
+
|
|
4289
5222
|
// Build menu content for header
|
|
4290
5223
|
const menuContent = `╭───────────────────────────────────────────────────────╮
|
|
4291
5224
|
│ │
|
|
@@ -4295,7 +5228,7 @@ async function startInteractive() {
|
|
|
4295
5228
|
╰───────────────────────────────────────────────────────╯
|
|
4296
5229
|
|
|
4297
5230
|
${t('system.repo').padEnd(25)} ${repoPath || 'Not set'}
|
|
4298
|
-
${t('system.computer.name').padEnd(25)} ${hostname}
|
|
5231
|
+
${currentBranch ? t('system.git.branch').padEnd(25) + ' ' + currentBranch + '\n' : ''}${t('system.computer.name').padEnd(25)} ${hostname}
|
|
4299
5232
|
Current IDE: ${formatIDEName(ide)}
|
|
4300
5233
|
AI Provider: ${getCurrentAIProvider(ide) || 'N/A'}
|
|
4301
5234
|
Max Chats: ${maxChats || 'Never stop'}`;
|
|
@@ -4343,6 +5276,60 @@ Max Chats: ${maxChats || 'Never stop'}`;
|
|
|
4343
5276
|
});
|
|
4344
5277
|
}
|
|
4345
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
|
+
|
|
4346
5333
|
// Watch requirements file for changes
|
|
4347
5334
|
const watcher = chokidar.watch(reqPath, { persistent: true });
|
|
4348
5335
|
watcher.on('change', async () => {
|
|
@@ -4466,7 +5453,49 @@ Max Chats: ${maxChats || 'Never stop'}`;
|
|
|
4466
5453
|
}
|
|
4467
5454
|
}
|
|
4468
5455
|
|
|
4469
|
-
|
|
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 };
|
|
4470
5499
|
|
|
4471
5500
|
|
|
4472
5501
|
|