vibecodingmachine-cli 2025.12.1-534 ā 2025.12.6-1702
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/vibecodingmachine.js +92 -4
- package/package.json +5 -2
- package/repro_open.js +13 -0
- package/scripts/postinstall.js +80 -0
- package/src/commands/auto-direct.js +401 -97
- package/src/commands/auto.js +343 -115
- package/src/commands/computers.js +306 -0
- package/src/commands/requirements-remote.js +304 -0
- package/src/commands/requirements.js +204 -13
- package/src/commands/sync.js +280 -0
- package/src/utils/asset-cleanup.js +61 -0
- package/src/utils/auth.js +84 -7
- package/src/utils/auto-mode-simple-ui.js +2 -22
- package/src/utils/first-run.js +293 -0
- package/src/utils/interactive.js +1027 -217
- package/src/utils/kiro-installer.js +146 -0
- package/src/utils/provider-registry.js +8 -0
- package/src/utils/status-card.js +2 -1
package/src/utils/interactive.js
CHANGED
|
@@ -8,6 +8,7 @@ const readline = require('readline');
|
|
|
8
8
|
const repo = require('../commands/repo');
|
|
9
9
|
const auto = require('../commands/auto');
|
|
10
10
|
const status = require('../commands/status');
|
|
11
|
+
const requirements = require('../commands/requirements');
|
|
11
12
|
const { getRepoPath, readConfig, writeConfig } = require('./config');
|
|
12
13
|
const { getProviderPreferences, saveProviderPreferences, getProviderDefinitions } = require('../utils/provider-registry');
|
|
13
14
|
const { checkAutoModeStatus } = require('./auto-mode');
|
|
@@ -32,7 +33,8 @@ function formatIDEName(ide) {
|
|
|
32
33
|
'cline': 'Cline CLI',
|
|
33
34
|
'cursor': 'Cursor',
|
|
34
35
|
'vscode': 'VS Code',
|
|
35
|
-
'windsurf': 'Windsurf'
|
|
36
|
+
'windsurf': 'Windsurf',
|
|
37
|
+
'kiro': 'AWS Kiro'
|
|
36
38
|
};
|
|
37
39
|
return ideNames[ide] || ide;
|
|
38
40
|
}
|
|
@@ -52,6 +54,7 @@ function getAgentDisplayName(agentType) {
|
|
|
52
54
|
if (agentType === 'cursor') return 'Cursor IDE Agent';
|
|
53
55
|
if (agentType === 'windsurf') return 'Windsurf IDE Agent';
|
|
54
56
|
if (agentType === 'antigravity') return 'Google Antigravity IDE Agent';
|
|
57
|
+
if (agentType === 'kiro') return 'AWS Kiro AI IDE Agent';
|
|
55
58
|
if (agentType === 'vscode') return 'VS Code IDE Agent';
|
|
56
59
|
|
|
57
60
|
// Claude Code CLI
|
|
@@ -350,6 +353,8 @@ async function getCurrentProgress() {
|
|
|
350
353
|
}
|
|
351
354
|
|
|
352
355
|
async function showWelcomeScreen() {
|
|
356
|
+
|
|
357
|
+
|
|
353
358
|
const repoPath = process.cwd(); // Always use current working directory
|
|
354
359
|
const autoStatus = await checkAutoModeStatus();
|
|
355
360
|
const allnightStatus = await checkVibeCodingMachineExists();
|
|
@@ -375,7 +380,8 @@ async function showWelcomeScreen() {
|
|
|
375
380
|
// Count requirements if file exists
|
|
376
381
|
const counts = hasRequirements ? await countRequirements() : null;
|
|
377
382
|
|
|
378
|
-
// Clear the screen
|
|
383
|
+
// Clear the screen using console.clear() for better cross-platform compatibility
|
|
384
|
+
// This ensures proper screen refresh and prevents text overlap
|
|
379
385
|
console.clear();
|
|
380
386
|
|
|
381
387
|
// Get version from package.json
|
|
@@ -419,14 +425,18 @@ async function showWelcomeScreen() {
|
|
|
419
425
|
const icon = stageIcons[progress.status] || 'ā³';
|
|
420
426
|
const statusColor = progress.status === 'DONE' ? chalk.green : chalk.magenta;
|
|
421
427
|
|
|
428
|
+
// Truncate requirement text first, THEN apply color to fix box alignment
|
|
429
|
+
const requirementText = progress.requirement ? progress.requirement.substring(0, 60) + (progress.requirement.length > 60 ? '...' : '') : 'No requirement';
|
|
430
|
+
|
|
422
431
|
console.log(boxen(
|
|
423
432
|
statusColor.bold(`${icon} ${progress.status}`) + '\n' +
|
|
424
|
-
chalk.gray(
|
|
433
|
+
chalk.gray(requirementText),
|
|
425
434
|
{
|
|
426
435
|
padding: { left: 1, right: 1, top: 0, bottom: 0 },
|
|
427
436
|
margin: 0,
|
|
428
437
|
borderStyle: 'round',
|
|
429
|
-
borderColor: 'magenta'
|
|
438
|
+
borderColor: 'magenta',
|
|
439
|
+
width: 70
|
|
430
440
|
}
|
|
431
441
|
));
|
|
432
442
|
}
|
|
@@ -499,49 +509,59 @@ async function showRequirementsTree() {
|
|
|
499
509
|
const todoPercent = total > 0 ? Math.round((todoReqs.length / total) * 100) : 0;
|
|
500
510
|
const recycledPercent = total > 0 ? Math.round((recycledReqs.length / total) * 100) : 0;
|
|
501
511
|
|
|
502
|
-
// VERIFIED section (first)
|
|
503
|
-
|
|
512
|
+
// VERIFIED section (first) - only show if has requirements
|
|
513
|
+
if (verifiedReqs.length > 0) {
|
|
514
|
+
tree.items.push({ level: 1, type: 'section', label: `š VERIFIED (${verifiedReqs.length} - ${verifiedPercent}%)`, key: 'verified' });
|
|
504
515
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
516
|
+
if (tree.expanded.verified) {
|
|
517
|
+
verifiedReqs.forEach((req, idx) => {
|
|
518
|
+
tree.items.push({ level: 2, type: 'verified', label: req, key: `verified-${idx}` });
|
|
519
|
+
});
|
|
520
|
+
}
|
|
509
521
|
}
|
|
510
522
|
|
|
511
|
-
// TO VERIFY section (second)
|
|
512
|
-
|
|
523
|
+
// TO VERIFY section (second) - only show if has requirements
|
|
524
|
+
if (verifyReqs.length > 0) {
|
|
525
|
+
tree.items.push({ level: 1, type: 'section', label: `ā
TO VERIFY (${verifyReqs.length} - ${verifyPercent}%)`, key: 'verify', section: 'ā
Verified by AI screenshot' });
|
|
513
526
|
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
527
|
+
if (tree.expanded.verify) {
|
|
528
|
+
verifyReqs.forEach((req, idx) => {
|
|
529
|
+
tree.items.push({ level: 2, type: 'requirement', label: req.title, key: `verify-${idx}`, req, sectionKey: 'verify' });
|
|
530
|
+
});
|
|
531
|
+
}
|
|
518
532
|
}
|
|
519
533
|
|
|
520
|
-
// NEEDING CLARIFICATION section (third)
|
|
521
|
-
|
|
534
|
+
// NEEDING CLARIFICATION section (third) - only show if has requirements
|
|
535
|
+
if (clarificationReqs.length > 0) {
|
|
536
|
+
tree.items.push({ level: 1, type: 'section', label: `ā NEEDING CLARIFICATION (${clarificationReqs.length} - ${clarificationPercent}%)`, key: 'clarification', section: 'ā Requirements needing manual feedback' });
|
|
522
537
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
538
|
+
if (tree.expanded.clarification) {
|
|
539
|
+
clarificationReqs.forEach((req, idx) => {
|
|
540
|
+
tree.items.push({ level: 2, type: 'clarification', label: req.title, key: `clarification-${idx}`, req, sectionKey: 'clarification' });
|
|
541
|
+
});
|
|
542
|
+
}
|
|
527
543
|
}
|
|
528
544
|
|
|
529
|
-
// TODO section (fourth)
|
|
530
|
-
|
|
545
|
+
// TODO section (fourth) - only show if has requirements
|
|
546
|
+
if (todoReqs.length > 0) {
|
|
547
|
+
tree.items.push({ level: 1, type: 'section', label: `ā³ TODO (${todoReqs.length} - ${todoPercent}%)`, key: 'todo', section: 'ā³ Requirements not yet completed' });
|
|
531
548
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
549
|
+
if (tree.expanded.todo) {
|
|
550
|
+
todoReqs.forEach((req, idx) => {
|
|
551
|
+
tree.items.push({ level: 2, type: 'requirement', label: req.title, key: `todo-${idx}`, req, sectionKey: 'todo' });
|
|
552
|
+
});
|
|
553
|
+
}
|
|
536
554
|
}
|
|
537
555
|
|
|
538
|
-
// RECYCLED section (last)
|
|
539
|
-
|
|
556
|
+
// RECYCLED section (last) - only show if has requirements
|
|
557
|
+
if (recycledReqs.length > 0) {
|
|
558
|
+
tree.items.push({ level: 1, type: 'section', label: `ā»ļø RECYCLED (${recycledReqs.length} - ${recycledPercent}%)`, key: 'recycled', section: 'ā»ļø Recycled' });
|
|
540
559
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
560
|
+
if (tree.expanded.recycled) {
|
|
561
|
+
recycledReqs.forEach((req, idx) => {
|
|
562
|
+
tree.items.push({ level: 2, type: 'recycled', label: req.title, key: `recycled-${idx}`, req, sectionKey: 'recycled' });
|
|
563
|
+
});
|
|
564
|
+
}
|
|
545
565
|
}
|
|
546
566
|
}
|
|
547
567
|
};
|
|
@@ -563,25 +583,110 @@ async function showRequirementsTree() {
|
|
|
563
583
|
|
|
564
584
|
// For TO VERIFY section, check multiple possible section titles
|
|
565
585
|
const sectionTitles = sectionKey === 'verify'
|
|
566
|
-
? ['š TO VERIFY BY HUMAN', 'TO VERIFY BY HUMAN', 'š TO VERIFY', 'TO VERIFY', 'ā
Verified by AI screenshot', 'Verified by AI screenshot']
|
|
586
|
+
? ['š 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']
|
|
567
587
|
: [sectionTitle];
|
|
568
588
|
|
|
589
|
+
// For TO VERIFY, we need to find the exact section header
|
|
590
|
+
// The section header is: ## ā
Verified by AI screenshot. Needs Human to Verify and move to CHANGELOG
|
|
591
|
+
const toVerifySectionHeader = '## ā
Verified by AI screenshot. Needs Human to Verify and move to CHANGELOG';
|
|
592
|
+
|
|
569
593
|
for (let i = 0; i < lines.length; i++) {
|
|
570
594
|
const line = lines[i];
|
|
595
|
+
const trimmed = line.trim();
|
|
571
596
|
|
|
572
597
|
// Check if this line matches any of the section titles
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
598
|
+
// IMPORTANT: Only check section headers (lines starting with ##), not requirement text
|
|
599
|
+
if (trimmed.startsWith('##') && !trimmed.startsWith('###')) {
|
|
600
|
+
// Reset inSection if we hit a section header that's not our target section
|
|
601
|
+
if (sectionKey === 'verify' && inSection) {
|
|
602
|
+
// Check if this is still a TO VERIFY section header
|
|
603
|
+
const isStillToVerify = trimmed === '## š TO VERIFY BY HUMAN' ||
|
|
604
|
+
trimmed.startsWith('## š TO VERIFY BY HUMAN') ||
|
|
605
|
+
trimmed === '## š TO VERIFY' ||
|
|
606
|
+
trimmed.startsWith('## š TO VERIFY') ||
|
|
607
|
+
trimmed === '## TO VERIFY' ||
|
|
608
|
+
trimmed.startsWith('## TO VERIFY') ||
|
|
609
|
+
trimmed === '## ā
TO VERIFY' ||
|
|
610
|
+
trimmed.startsWith('## ā
TO VERIFY') ||
|
|
611
|
+
trimmed === '## ā
Verified by AI screenshot. Needs Human to Verify and move to CHANGELOG' ||
|
|
612
|
+
(trimmed.startsWith('## ā
Verified by AI screenshot. Needs Human to Verify and move to CHANGELOG') &&
|
|
613
|
+
trimmed.includes('Needs Human to Verify'));
|
|
614
|
+
if (!isStillToVerify) {
|
|
615
|
+
// This will be handled by the "leaving section" check below, but ensure we don't process it as entering
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
if (sectionKey === 'verify') {
|
|
619
|
+
// For TO VERIFY, check for the specific section header with exact matching
|
|
620
|
+
// Must match the exact TO VERIFY section header, not just any line containing "TO VERIFY"
|
|
621
|
+
const isToVerifyHeader = trimmed === '## š TO VERIFY BY HUMAN' ||
|
|
622
|
+
trimmed.startsWith('## š TO VERIFY BY HUMAN') ||
|
|
623
|
+
trimmed === '## š TO VERIFY' ||
|
|
624
|
+
trimmed.startsWith('## š TO VERIFY') ||
|
|
625
|
+
trimmed === '## TO VERIFY' ||
|
|
626
|
+
trimmed.startsWith('## TO VERIFY') ||
|
|
627
|
+
trimmed === '## ā
TO VERIFY' ||
|
|
628
|
+
trimmed.startsWith('## ā
TO VERIFY') ||
|
|
629
|
+
trimmed === toVerifySectionHeader ||
|
|
630
|
+
(trimmed.startsWith(toVerifySectionHeader) && trimmed.includes('Needs Human to Verify'));
|
|
631
|
+
|
|
632
|
+
if (isToVerifyHeader) {
|
|
633
|
+
// Make sure it's not a VERIFIED section (without TO VERIFY)
|
|
634
|
+
if (!trimmed.includes('## š VERIFIED') && !trimmed.match(/^##\s+VERIFIED$/i) && !trimmed.includes('š VERIFIED')) {
|
|
635
|
+
inSection = true;
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
638
|
+
} else {
|
|
639
|
+
// If we hit a different section header and we're looking for TO VERIFY, make sure we're not in section
|
|
640
|
+
// This prevents incorrectly reading from TODO or other sections
|
|
641
|
+
if (trimmed.includes('ā³ Requirements not yet completed') ||
|
|
642
|
+
trimmed.includes('Requirements not yet completed') ||
|
|
643
|
+
trimmed === '## š VERIFIED' ||
|
|
644
|
+
trimmed.startsWith('## š VERIFIED')) {
|
|
645
|
+
// We're in TODO or VERIFIED section, not TO VERIFY - reset
|
|
646
|
+
inSection = false;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
} else if (sectionTitles.some(title => trimmed.includes(title))) {
|
|
650
|
+
inSection = true;
|
|
651
|
+
continue;
|
|
652
|
+
}
|
|
576
653
|
}
|
|
577
654
|
|
|
578
|
-
|
|
579
|
-
|
|
655
|
+
// Check if we're leaving the section (new section header that doesn't match)
|
|
656
|
+
if (inSection && trimmed.startsWith('##') && !trimmed.startsWith('###')) {
|
|
657
|
+
// If this is a new section header and it's not one of our section titles, we've left the section
|
|
658
|
+
if (sectionKey === 'verify') {
|
|
659
|
+
// For TO VERIFY, only break if this is clearly a different section
|
|
660
|
+
// Check for specific section headers that indicate we've left TO VERIFY
|
|
661
|
+
const isVerifiedSection = trimmed === '## š VERIFIED' || trimmed.startsWith('## š VERIFIED');
|
|
662
|
+
const isTodoSection = trimmed.includes('ā³ Requirements not yet completed') || trimmed.includes('Requirements not yet completed');
|
|
663
|
+
const isRecycledSection = trimmed === '## ā»ļø RECYCLED' || trimmed.startsWith('## ā»ļø RECYCLED') ||
|
|
664
|
+
trimmed === '## š¦ RECYCLED' || trimmed.startsWith('## š¦ RECYCLED');
|
|
665
|
+
const isClarificationSection = trimmed.includes('## ā Requirements needing') || trimmed.includes('ā Requirements needing');
|
|
666
|
+
|
|
667
|
+
if (isVerifiedSection || isTodoSection || isRecycledSection || isClarificationSection) {
|
|
668
|
+
break; // Different section, we've left TO VERIFY
|
|
669
|
+
}
|
|
670
|
+
// Otherwise, continue - might be REJECTED or CHANGELOG which are not section boundaries for TO VERIFY
|
|
671
|
+
} else {
|
|
672
|
+
// For other sections, break if it's a new section header that doesn't match
|
|
673
|
+
if (!sectionTitles.some(title => trimmed.includes(title))) {
|
|
674
|
+
break;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
580
677
|
}
|
|
581
678
|
|
|
582
679
|
// Read requirements in new format (### header)
|
|
583
680
|
if (inSection && line.trim().startsWith('###')) {
|
|
584
|
-
const title = line.trim().replace(/^###\s*/, '');
|
|
681
|
+
const title = line.trim().replace(/^###\s*/, '').trim();
|
|
682
|
+
|
|
683
|
+
// Skip malformed requirements (title is just a package name, empty, or too short)
|
|
684
|
+
// Common package names that shouldn't be requirement titles
|
|
685
|
+
const packageNames = ['cli', 'core', 'electron-app', 'web', 'mobile', 'vscode-extension', 'sync-server'];
|
|
686
|
+
if (!title || title.length === 0 || packageNames.includes(title.toLowerCase())) {
|
|
687
|
+
continue; // Skip this malformed requirement
|
|
688
|
+
}
|
|
689
|
+
|
|
585
690
|
const details = [];
|
|
586
691
|
let package = null;
|
|
587
692
|
|
|
@@ -605,7 +710,18 @@ async function showRequirementsTree() {
|
|
|
605
710
|
}
|
|
606
711
|
}
|
|
607
712
|
|
|
608
|
-
|
|
713
|
+
// Remove duplicates based on title (keep first occurrence)
|
|
714
|
+
const seenTitles = new Set();
|
|
715
|
+
const uniqueRequirements = [];
|
|
716
|
+
for (const req of requirements) {
|
|
717
|
+
const normalizedTitle = req.title.replace(/^TRY AGAIN \(\d+(st|nd|rd|th) time\):\s*/i, '').trim();
|
|
718
|
+
if (!seenTitles.has(normalizedTitle)) {
|
|
719
|
+
seenTitles.add(normalizedTitle);
|
|
720
|
+
uniqueRequirements.push(req);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
return uniqueRequirements;
|
|
609
725
|
};
|
|
610
726
|
|
|
611
727
|
// Load VERIFIED requirements from CHANGELOG
|
|
@@ -908,13 +1024,19 @@ async function showRequirementsTree() {
|
|
|
908
1024
|
|
|
909
1025
|
if (current.type === 'requirement') {
|
|
910
1026
|
await deleteRequirement(current.req, current.sectionKey, tree);
|
|
1027
|
+
// Reload the section that the requirement was deleted from
|
|
1028
|
+
if (current.sectionKey === 'todo') {
|
|
1029
|
+
tree.todoReqs = await loadSection('todo', 'ā³ Requirements not yet completed');
|
|
1030
|
+
} else if (current.sectionKey === 'verify') {
|
|
1031
|
+
tree.verifyReqs = await loadSection('verify', 'ā
Verified by AI screenshot');
|
|
1032
|
+
}
|
|
911
1033
|
buildTree();
|
|
912
1034
|
} else if (current.type === 'clarification') {
|
|
913
1035
|
await deleteClarification(current.req, tree);
|
|
914
1036
|
tree.clarificationReqs = await loadClarification();
|
|
915
1037
|
buildTree();
|
|
916
1038
|
} else if (current.type === 'recycled') {
|
|
917
|
-
await
|
|
1039
|
+
await permanentlyDeleteRequirement(current.req, current.sectionKey, tree);
|
|
918
1040
|
tree.recycledReqs = await loadSection('recycled', 'ā»ļø Recycled');
|
|
919
1041
|
buildTree();
|
|
920
1042
|
}
|
|
@@ -1404,8 +1526,9 @@ async function showClarificationActions(req, tree, loadClarification) {
|
|
|
1404
1526
|
}
|
|
1405
1527
|
async function showRequirementActions(req, sectionKey, tree) {
|
|
1406
1528
|
const actions = [
|
|
1407
|
-
{ label: '
|
|
1408
|
-
{ label: '
|
|
1529
|
+
{ label: 'āļø Rename/Edit', value: 'rename' },
|
|
1530
|
+
{ label: 'š Thumbs up (promote to Verified)', value: 'thumbs-up' },
|
|
1531
|
+
{ label: 'š Thumbs down (demote to TODO)', value: 'thumbs-down' },
|
|
1409
1532
|
{ label: 'ā¬ļø Move up', value: 'move-up' },
|
|
1410
1533
|
{ label: 'ā¬ļø Move down', value: 'move-down' },
|
|
1411
1534
|
{ label: 'šļø Delete', value: 'delete' }
|
|
@@ -1477,7 +1600,9 @@ async function showRequirementActions(req, sectionKey, tree) {
|
|
|
1477
1600
|
|
|
1478
1601
|
if (!key) continue;
|
|
1479
1602
|
|
|
1480
|
-
if (
|
|
1603
|
+
if (key.ctrl && key.name === 'c') {
|
|
1604
|
+
process.exit(0);
|
|
1605
|
+
} else if (key.name === 'escape' || key.name === 'left') {
|
|
1481
1606
|
return; // Go back
|
|
1482
1607
|
} else if (key.name === 'up') {
|
|
1483
1608
|
selected = Math.max(0, selected - 1);
|
|
@@ -1536,11 +1661,156 @@ async function performRequirementAction(action, req, sectionKey, tree) {
|
|
|
1536
1661
|
console.log(chalk.green('\nā Deleted\n'));
|
|
1537
1662
|
}
|
|
1538
1663
|
break;
|
|
1664
|
+
case 'rename':
|
|
1665
|
+
await renameRequirement(req, sectionKey, tree);
|
|
1666
|
+
break;
|
|
1539
1667
|
}
|
|
1540
1668
|
|
|
1541
1669
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
1542
1670
|
}
|
|
1543
1671
|
|
|
1672
|
+
// Helper to rename requirement (title and description)
|
|
1673
|
+
async function renameRequirement(req, sectionKey, tree) {
|
|
1674
|
+
const { getRequirementsPath } = require('vibecodingmachine-core');
|
|
1675
|
+
const reqPath = await getRequirementsPath();
|
|
1676
|
+
|
|
1677
|
+
console.log(chalk.cyan('\nāļø Rename/Edit Requirement\n'));
|
|
1678
|
+
console.log(chalk.gray('Current title:'), chalk.white(req.title));
|
|
1679
|
+
if (req.details.length > 0) {
|
|
1680
|
+
console.log(chalk.gray('Current description:'));
|
|
1681
|
+
req.details.forEach(line => console.log(chalk.white(' ' + line)));
|
|
1682
|
+
}
|
|
1683
|
+
console.log();
|
|
1684
|
+
|
|
1685
|
+
const answers = await inquirer.prompt([
|
|
1686
|
+
{
|
|
1687
|
+
type: 'input',
|
|
1688
|
+
name: 'title',
|
|
1689
|
+
message: 'New title (leave blank to keep current):',
|
|
1690
|
+
default: ''
|
|
1691
|
+
}
|
|
1692
|
+
]);
|
|
1693
|
+
|
|
1694
|
+
const newTitle = answers.title.trim() || req.title;
|
|
1695
|
+
|
|
1696
|
+
// Ask for description using multi-line input
|
|
1697
|
+
console.log(chalk.gray('\nEnter new description (leave blank to keep current).'));
|
|
1698
|
+
console.log(chalk.gray('Press Enter twice on empty line to finish:\n'));
|
|
1699
|
+
|
|
1700
|
+
const descriptionLines = [];
|
|
1701
|
+
let emptyLineCount = 0;
|
|
1702
|
+
let isFirstLine = true;
|
|
1703
|
+
let newDescription = '';
|
|
1704
|
+
let keptCurrent = false;
|
|
1705
|
+
|
|
1706
|
+
while (true) {
|
|
1707
|
+
try {
|
|
1708
|
+
const { line } = await inquirer.prompt([{
|
|
1709
|
+
type: 'input',
|
|
1710
|
+
name: 'line',
|
|
1711
|
+
message: isFirstLine ? 'Description:' : ''
|
|
1712
|
+
}]);
|
|
1713
|
+
|
|
1714
|
+
if (isFirstLine && line.trim() === '') {
|
|
1715
|
+
// If first line is empty, keep current description
|
|
1716
|
+
keptCurrent = true;
|
|
1717
|
+
break;
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
isFirstLine = false;
|
|
1721
|
+
|
|
1722
|
+
if (line.trim() === '') {
|
|
1723
|
+
emptyLineCount++;
|
|
1724
|
+
if (emptyLineCount >= 2) break;
|
|
1725
|
+
} else {
|
|
1726
|
+
emptyLineCount = 0;
|
|
1727
|
+
descriptionLines.push(line);
|
|
1728
|
+
}
|
|
1729
|
+
} catch (err) {
|
|
1730
|
+
break;
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
if (!keptCurrent) {
|
|
1735
|
+
newDescription = descriptionLines.join('\n');
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
// Read the requirements file
|
|
1739
|
+
const content = await fs.readFile(reqPath, 'utf8');
|
|
1740
|
+
const lines = content.split('\n');
|
|
1741
|
+
|
|
1742
|
+
// Find the requirement block
|
|
1743
|
+
let requirementStartIndex = -1;
|
|
1744
|
+
let requirementEndIndex = -1;
|
|
1745
|
+
|
|
1746
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1747
|
+
const line = lines[i].trim();
|
|
1748
|
+
if (line.startsWith('###')) {
|
|
1749
|
+
const title = line.replace(/^###\s*/, '').trim();
|
|
1750
|
+
if (title === req.title) {
|
|
1751
|
+
requirementStartIndex = i;
|
|
1752
|
+
// Find the end of this requirement (next ### or ## header)
|
|
1753
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
1754
|
+
const nextLine = lines[j].trim();
|
|
1755
|
+
if (nextLine.startsWith('###') || (nextLine.startsWith('##') && !nextLine.startsWith('###'))) {
|
|
1756
|
+
requirementEndIndex = j;
|
|
1757
|
+
break;
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
if (requirementEndIndex === -1) {
|
|
1761
|
+
requirementEndIndex = lines.length;
|
|
1762
|
+
}
|
|
1763
|
+
break;
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
if (requirementStartIndex === -1) {
|
|
1769
|
+
console.log(chalk.yellow('ā ļø Could not find requirement to rename'));
|
|
1770
|
+
return;
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
// Build new requirement block
|
|
1774
|
+
const newBlock = [`### ${newTitle}`];
|
|
1775
|
+
|
|
1776
|
+
// If new description provided, use it; otherwise keep existing details
|
|
1777
|
+
if (newDescription) {
|
|
1778
|
+
newDescription.split('\n').forEach(line => {
|
|
1779
|
+
if (line.trim()) {
|
|
1780
|
+
newBlock.push(line);
|
|
1781
|
+
}
|
|
1782
|
+
});
|
|
1783
|
+
} else {
|
|
1784
|
+
// Keep existing details (skip the ### header line)
|
|
1785
|
+
for (let i = requirementStartIndex + 1; i < requirementEndIndex; i++) {
|
|
1786
|
+
const line = lines[i];
|
|
1787
|
+
// Skip empty lines at the start and PACKAGE lines if we want to preserve them
|
|
1788
|
+
if (line.trim()) {
|
|
1789
|
+
newBlock.push(line);
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
newBlock.push(''); // Blank line after requirement
|
|
1795
|
+
|
|
1796
|
+
// Replace the old block with the new one
|
|
1797
|
+
lines.splice(requirementStartIndex, requirementEndIndex - requirementStartIndex, ...newBlock);
|
|
1798
|
+
|
|
1799
|
+
// Save
|
|
1800
|
+
await fs.writeFile(reqPath, lines.join('\n'));
|
|
1801
|
+
console.log(chalk.green('\nā Requirement renamed/updated\n'));
|
|
1802
|
+
|
|
1803
|
+
// Update the tree data
|
|
1804
|
+
const reqList = getRequirementList(tree, sectionKey);
|
|
1805
|
+
const reqIndex = reqList.findIndex(r => r.title === req.title);
|
|
1806
|
+
if (reqIndex !== -1) {
|
|
1807
|
+
reqList[reqIndex].title = newTitle;
|
|
1808
|
+
if (newDescription) {
|
|
1809
|
+
reqList[reqIndex].details = newDescription.split('\n').filter(line => line.trim());
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1544
1814
|
// Helper to move requirement to recycled section (used to delete)
|
|
1545
1815
|
async function deleteRequirement(req, sectionKey, tree) {
|
|
1546
1816
|
const reqList = getRequirementList(tree, sectionKey);
|
|
@@ -1560,6 +1830,61 @@ async function deleteRequirement(req, sectionKey, tree) {
|
|
|
1560
1830
|
}
|
|
1561
1831
|
}
|
|
1562
1832
|
|
|
1833
|
+
// Helper to permanently delete requirement from file (used for recycled items)
|
|
1834
|
+
async function permanentlyDeleteRequirement(req, sectionKey, tree) {
|
|
1835
|
+
const reqList = getRequirementList(tree, sectionKey);
|
|
1836
|
+
const reqIndex = reqList.findIndex(r => r.title === req.title);
|
|
1837
|
+
|
|
1838
|
+
if (reqIndex === -1) return;
|
|
1839
|
+
|
|
1840
|
+
const { getRequirementsPath } = require('vibecodingmachine-core');
|
|
1841
|
+
const reqPath = await getRequirementsPath();
|
|
1842
|
+
|
|
1843
|
+
const truncatedTitle = req.title.substring(0, 50) + (req.title.length > 50 ? '...' : '');
|
|
1844
|
+
if (await confirmAction(`Permanently delete? (r/y/N)`)) {
|
|
1845
|
+
const content = await fs.readFile(reqPath, 'utf8');
|
|
1846
|
+
const lines = content.split('\n');
|
|
1847
|
+
|
|
1848
|
+
// Find the requirement block (### header format)
|
|
1849
|
+
let requirementStartIndex = -1;
|
|
1850
|
+
let requirementEndIndex = -1;
|
|
1851
|
+
|
|
1852
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1853
|
+
const line = lines[i].trim();
|
|
1854
|
+
if (line.startsWith('###')) {
|
|
1855
|
+
const title = line.replace(/^###\s*/, '').trim();
|
|
1856
|
+
if (title && title.includes(req.title)) {
|
|
1857
|
+
requirementStartIndex = i;
|
|
1858
|
+
// Find the end of this requirement (next ### or ## header)
|
|
1859
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
1860
|
+
const nextLine = lines[j].trim();
|
|
1861
|
+
if (nextLine.startsWith('###') || (nextLine.startsWith('##') && !nextLine.startsWith('###'))) {
|
|
1862
|
+
requirementEndIndex = j;
|
|
1863
|
+
break;
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
if (requirementEndIndex === -1) {
|
|
1867
|
+
requirementEndIndex = lines.length;
|
|
1868
|
+
}
|
|
1869
|
+
break;
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
if (requirementStartIndex === -1) {
|
|
1875
|
+
console.log(chalk.yellow('ā ļø Could not find requirement to delete'));
|
|
1876
|
+
return;
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
// Remove the requirement from the file completely
|
|
1880
|
+
lines.splice(requirementStartIndex, requirementEndIndex - requirementStartIndex);
|
|
1881
|
+
|
|
1882
|
+
// Save
|
|
1883
|
+
await fs.writeFile(reqPath, lines.join('\n'));
|
|
1884
|
+
reqList.splice(reqIndex, 1);
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1563
1888
|
// Helper to move requirement to recycled section
|
|
1564
1889
|
async function moveRequirementToRecycled(reqPath, requirementTitle, fromSection) {
|
|
1565
1890
|
const content = await fs.readFile(reqPath, 'utf8');
|
|
@@ -1600,8 +1925,25 @@ async function moveRequirementToRecycled(reqPath, requirementTitle, fromSection)
|
|
|
1600
1925
|
const requirementBlock = lines.slice(requirementStartIndex, requirementEndIndex);
|
|
1601
1926
|
|
|
1602
1927
|
// Remove the requirement from its current location
|
|
1928
|
+
// Also remove any trailing blank lines that might be left behind
|
|
1603
1929
|
lines.splice(requirementStartIndex, requirementEndIndex - requirementStartIndex);
|
|
1604
1930
|
|
|
1931
|
+
// Clean up: remove any orphaned blank lines or malformed entries left after removal
|
|
1932
|
+
// Check if there's a blank line followed by a malformed requirement (just "cli", "core", etc.)
|
|
1933
|
+
if (requirementStartIndex < lines.length) {
|
|
1934
|
+
const nextLine = lines[requirementStartIndex]?.trim();
|
|
1935
|
+
const packageNames = ['cli', 'core', 'electron-app', 'web', 'mobile', 'vscode-extension', 'sync-server'];
|
|
1936
|
+
// If the next line is just a package name (likely orphaned), remove it
|
|
1937
|
+
if (nextLine && packageNames.includes(nextLine.toLowerCase()) &&
|
|
1938
|
+
!nextLine.startsWith('###') && !nextLine.startsWith('PACKAGE:')) {
|
|
1939
|
+
lines.splice(requirementStartIndex, 1);
|
|
1940
|
+
}
|
|
1941
|
+
// Remove any blank lines left after removal
|
|
1942
|
+
while (requirementStartIndex < lines.length && lines[requirementStartIndex]?.trim() === '') {
|
|
1943
|
+
lines.splice(requirementStartIndex, 1);
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1605
1947
|
// Find or create Recycled section
|
|
1606
1948
|
let recycledIndex = -1;
|
|
1607
1949
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -1703,8 +2045,41 @@ async function demoteRequirement(reqTitle, sectionKey, tree, loadSection, loadVe
|
|
|
1703
2045
|
const reqPath = await getRequirementsPath();
|
|
1704
2046
|
|
|
1705
2047
|
if (sectionKey === 'verify') {
|
|
1706
|
-
//
|
|
1707
|
-
|
|
2048
|
+
// Prompt for explanation of what went wrong
|
|
2049
|
+
// Prompt for explanation of what went wrong
|
|
2050
|
+
console.log(chalk.gray('\nWhat went wrong? (This will be added to help the AI agent)'));
|
|
2051
|
+
console.log(chalk.gray('Press Enter twice on empty line to finish:\n'));
|
|
2052
|
+
|
|
2053
|
+
const explanationLines = [];
|
|
2054
|
+
let emptyLineCount = 0;
|
|
2055
|
+
let isFirstLine = true;
|
|
2056
|
+
|
|
2057
|
+
while (true) {
|
|
2058
|
+
try {
|
|
2059
|
+
const { line } = await inquirer.prompt([{
|
|
2060
|
+
type: 'input',
|
|
2061
|
+
name: 'line',
|
|
2062
|
+
message: isFirstLine ? 'Explanation:' : ''
|
|
2063
|
+
}]);
|
|
2064
|
+
|
|
2065
|
+
isFirstLine = false;
|
|
2066
|
+
|
|
2067
|
+
if (line.trim() === '') {
|
|
2068
|
+
emptyLineCount++;
|
|
2069
|
+
if (emptyLineCount >= 2) break;
|
|
2070
|
+
} else {
|
|
2071
|
+
emptyLineCount = 0;
|
|
2072
|
+
explanationLines.push(line);
|
|
2073
|
+
}
|
|
2074
|
+
} catch (err) {
|
|
2075
|
+
break;
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
const explanation = explanationLines.join('\n');
|
|
2080
|
+
|
|
2081
|
+
// TO VERIFY -> TODO: Use shared function with explanation
|
|
2082
|
+
const success = await demoteVerifyToTodo(reqPath, reqTitle, explanation);
|
|
1708
2083
|
if (success) {
|
|
1709
2084
|
// Reload sections
|
|
1710
2085
|
tree.todoReqs = await loadSection('todo', 'ā³ Requirements not yet completed');
|
|
@@ -1734,86 +2109,104 @@ async function handleAddRequirement(type) {
|
|
|
1734
2109
|
// Ensure it's an array
|
|
1735
2110
|
if (typeof selectedPackage === 'string') selectedPackage = [selectedPackage];
|
|
1736
2111
|
|
|
1737
|
-
let askPackage = !config.lastPackage;
|
|
1738
|
-
let name = '';
|
|
1739
|
-
let pkg = selectedPackage;
|
|
1740
|
-
|
|
1741
2112
|
while (true) {
|
|
1742
|
-
|
|
1743
|
-
|
|
2113
|
+
const pkgDisplay = selectedPackage.join(', ');
|
|
2114
|
+
|
|
2115
|
+
// Custom menu to allow "Up arrow" to select package
|
|
2116
|
+
// Default to "Enter Requirement Name" (index 1) so user can just type
|
|
2117
|
+
const { action } = await inquirer.prompt([{
|
|
2118
|
+
type: 'list',
|
|
2119
|
+
name: 'action',
|
|
2120
|
+
message: 'New Requirement:',
|
|
2121
|
+
choices: [
|
|
2122
|
+
{ name: `š¦ Package: ${pkgDisplay}`, value: 'package' },
|
|
2123
|
+
{ name: 'š Enter Requirement Name', value: 'name' },
|
|
2124
|
+
{ name: 'ā Cancel', value: 'cancel' }
|
|
2125
|
+
],
|
|
2126
|
+
default: 1
|
|
2127
|
+
}]);
|
|
2128
|
+
|
|
2129
|
+
if (action === 'cancel') return;
|
|
2130
|
+
|
|
2131
|
+
if (action === 'package') {
|
|
2132
|
+
// When showing checkboxes, if "all" is currently selected along with specific packages,
|
|
2133
|
+
// unselect "all" first so user sees only specific packages checked
|
|
2134
|
+
let displayPackages = selectedPackage;
|
|
2135
|
+
if (selectedPackage.includes('all') && selectedPackage.length > 1) {
|
|
2136
|
+
displayPackages = selectedPackage.filter(p => p !== 'all');
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
const { pkg } = await inquirer.prompt([{
|
|
1744
2140
|
type: 'checkbox',
|
|
1745
2141
|
name: 'pkg',
|
|
1746
2142
|
message: 'Select package(s):',
|
|
1747
2143
|
choices: packages.map(p => ({
|
|
1748
2144
|
name: p,
|
|
1749
2145
|
value: p,
|
|
1750
|
-
checked:
|
|
2146
|
+
checked: displayPackages.includes(p)
|
|
1751
2147
|
})),
|
|
1752
2148
|
validate: (answer) => {
|
|
1753
2149
|
if (answer.length < 1) return 'You must choose at least one package.';
|
|
1754
2150
|
return true;
|
|
1755
2151
|
}
|
|
1756
2152
|
}]);
|
|
1757
|
-
pkg = answer.pkg;
|
|
1758
2153
|
|
|
1759
|
-
//
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
// Ask for requirement name
|
|
1766
|
-
const pkgDisplay = pkg.join(', ');
|
|
1767
|
-
const nameAnswer = await inquirer.prompt([{
|
|
1768
|
-
type: 'input',
|
|
1769
|
-
name: 'name',
|
|
1770
|
-
message: `Enter requirement name (Package: ${pkgDisplay}) [Type < to change package]:`
|
|
1771
|
-
}]);
|
|
1772
|
-
|
|
1773
|
-
name = nameAnswer.name;
|
|
2154
|
+
// When selecting specific packages, unselect "all"
|
|
2155
|
+
let finalPackage = pkg;
|
|
2156
|
+
// If both "all" and specific packages are selected, keep only specific packages
|
|
2157
|
+
if (pkg.includes('all') && pkg.length > 1) {
|
|
2158
|
+
finalPackage = pkg.filter(p => p !== 'all');
|
|
2159
|
+
}
|
|
1774
2160
|
|
|
1775
|
-
|
|
1776
|
-
|
|
2161
|
+
selectedPackage = finalPackage;
|
|
2162
|
+
config.lastPackage = selectedPackage;
|
|
2163
|
+
await writeConfig(config);
|
|
1777
2164
|
continue;
|
|
1778
2165
|
}
|
|
1779
2166
|
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
// Then ask for multiline description (press Enter twice to finish)
|
|
1784
|
-
console.log(chalk.gray('\nEnter description (press Enter twice on empty line to finish):\n'));
|
|
1785
|
-
const descriptionLines = [];
|
|
1786
|
-
let emptyLineCount = 0;
|
|
1787
|
-
let isFirstLine = true;
|
|
1788
|
-
|
|
1789
|
-
while (true) {
|
|
1790
|
-
try {
|
|
1791
|
-
const { line } = await inquirer.prompt([{
|
|
2167
|
+
if (action === 'name') {
|
|
2168
|
+
const { name } = await inquirer.prompt([{
|
|
1792
2169
|
type: 'input',
|
|
1793
|
-
name: '
|
|
1794
|
-
message:
|
|
2170
|
+
name: 'name',
|
|
2171
|
+
message: `Enter requirement name (Package: ${pkgDisplay}):`
|
|
1795
2172
|
}]);
|
|
1796
2173
|
|
|
1797
|
-
|
|
2174
|
+
if (!name || !name.trim()) continue;
|
|
2175
|
+
|
|
2176
|
+
// Ask for description
|
|
2177
|
+
console.log(chalk.gray('\nEnter description (press Enter twice on empty line to finish):\n'));
|
|
2178
|
+
const descriptionLines = [];
|
|
2179
|
+
let emptyLineCount = 0;
|
|
2180
|
+
let isFirstLine = true;
|
|
2181
|
+
|
|
2182
|
+
while (true) {
|
|
2183
|
+
try {
|
|
2184
|
+
const { line } = await inquirer.prompt([{
|
|
2185
|
+
type: 'input',
|
|
2186
|
+
name: 'line',
|
|
2187
|
+
message: isFirstLine ? 'Description:' : ''
|
|
2188
|
+
}]);
|
|
2189
|
+
|
|
2190
|
+
isFirstLine = false;
|
|
1798
2191
|
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
2192
|
+
if (line.trim() === '') {
|
|
2193
|
+
emptyLineCount++;
|
|
2194
|
+
if (emptyLineCount >= 2) break;
|
|
2195
|
+
} else {
|
|
2196
|
+
emptyLineCount = 0;
|
|
2197
|
+
descriptionLines.push(line);
|
|
2198
|
+
}
|
|
2199
|
+
} catch (err) {
|
|
2200
|
+
break;
|
|
1803
2201
|
}
|
|
1804
|
-
} else {
|
|
1805
|
-
emptyLineCount = 0;
|
|
1806
|
-
descriptionLines.push(line);
|
|
1807
2202
|
}
|
|
1808
|
-
|
|
1809
|
-
|
|
2203
|
+
|
|
2204
|
+
const description = descriptionLines.join('\n');
|
|
2205
|
+
await reqCommands.add(name, selectedPackage, description);
|
|
2206
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
2207
|
+
return;
|
|
1810
2208
|
}
|
|
1811
2209
|
}
|
|
1812
|
-
|
|
1813
|
-
const description = descriptionLines.join('\n');
|
|
1814
|
-
await reqCommands.add(name, pkg, description);
|
|
1815
|
-
// Message already printed by reqCommands.add()
|
|
1816
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
1817
2210
|
} catch (err) {
|
|
1818
2211
|
// ESC pressed
|
|
1819
2212
|
}
|
|
@@ -1986,6 +2379,7 @@ async function showRequirementsBySection(sectionTitle) {
|
|
|
1986
2379
|
{ name: 'š Thumbs down (deprioritize)', value: 'thumbs-down' },
|
|
1987
2380
|
{ name: 'ā¬ļø Move up', value: 'move-up' },
|
|
1988
2381
|
{ name: 'ā¬ļø Move down', value: 'move-down' },
|
|
2382
|
+
{ name: 'ā»ļø Recycle (move to Recycled)', value: 'recycle' },
|
|
1989
2383
|
{ name: 'šļø Delete', value: 'delete' }
|
|
1990
2384
|
]
|
|
1991
2385
|
}]);
|
|
@@ -2026,6 +2420,15 @@ async function showRequirementsBySection(sectionTitle) {
|
|
|
2026
2420
|
console.log(chalk.yellow('\nā Already at bottom\n'));
|
|
2027
2421
|
}
|
|
2028
2422
|
break;
|
|
2423
|
+
case 'recycle':
|
|
2424
|
+
const recycledReq = requirements.splice(selectedIndex, 1)[0];
|
|
2425
|
+
await moveToRecycled(reqPath, recycledReq.title, sectionTitle);
|
|
2426
|
+
console.log(chalk.cyan('\nā Moved to Recycled section\n'));
|
|
2427
|
+
if (requirements.length === 0) {
|
|
2428
|
+
console.log(chalk.gray('No more requirements in this section.\n'));
|
|
2429
|
+
inRequirementsList = false;
|
|
2430
|
+
}
|
|
2431
|
+
break;
|
|
2029
2432
|
case 'delete':
|
|
2030
2433
|
const { confirmDelete } = await inquirer.prompt([{
|
|
2031
2434
|
type: 'confirm',
|
|
@@ -2187,12 +2590,18 @@ async function showQuickMenu(items, initialSelectedIndex = 0) {
|
|
|
2187
2590
|
return lineCount;
|
|
2188
2591
|
};
|
|
2189
2592
|
|
|
2190
|
-
const displayMenu = () => {
|
|
2191
|
-
// Clear
|
|
2192
|
-
if (!isFirstRender
|
|
2193
|
-
//
|
|
2194
|
-
|
|
2195
|
-
|
|
2593
|
+
const displayMenu = async () => {
|
|
2594
|
+
// Clear entire screen on navigation to prevent text overlap with banner
|
|
2595
|
+
if (!isFirstRender) {
|
|
2596
|
+
// No need to console.clear() here as showWelcomeScreen does it,
|
|
2597
|
+
// but ensuring it clears before we start waiting is fine too.
|
|
2598
|
+
console.clear();
|
|
2599
|
+
// Reprint the banner and status info
|
|
2600
|
+
try {
|
|
2601
|
+
await showWelcomeScreen();
|
|
2602
|
+
} catch (err) {
|
|
2603
|
+
console.error('Error displaying banner:', err);
|
|
2604
|
+
}
|
|
2196
2605
|
}
|
|
2197
2606
|
isFirstRender = false;
|
|
2198
2607
|
|
|
@@ -2296,7 +2705,7 @@ async function showQuickMenu(items, initialSelectedIndex = 0) {
|
|
|
2296
2705
|
process.stdin.setRawMode(true);
|
|
2297
2706
|
}
|
|
2298
2707
|
|
|
2299
|
-
const onKeypress = (str, key) => {
|
|
2708
|
+
const onKeypress = async (str, key) => {
|
|
2300
2709
|
if (!key) return;
|
|
2301
2710
|
|
|
2302
2711
|
// Ctrl+C to exit
|
|
@@ -2348,7 +2757,7 @@ async function showQuickMenu(items, initialSelectedIndex = 0) {
|
|
|
2348
2757
|
newIndex = newIndex > 0 ? newIndex - 1 : items.length - 1;
|
|
2349
2758
|
}
|
|
2350
2759
|
selectedIndex = newIndex;
|
|
2351
|
-
displayMenu();
|
|
2760
|
+
await displayMenu();
|
|
2352
2761
|
} else if (key.name === 'down') {
|
|
2353
2762
|
// Skip blank and info lines when navigating
|
|
2354
2763
|
let newIndex = selectedIndex < items.length - 1 ? selectedIndex + 1 : 0;
|
|
@@ -2356,7 +2765,8 @@ async function showQuickMenu(items, initialSelectedIndex = 0) {
|
|
|
2356
2765
|
newIndex = newIndex < items.length - 1 ? newIndex + 1 : 0;
|
|
2357
2766
|
}
|
|
2358
2767
|
selectedIndex = newIndex;
|
|
2359
|
-
displayMenu();
|
|
2768
|
+
await displayMenu();
|
|
2769
|
+
|
|
2360
2770
|
} else if (key.name === 'return' || key.name === 'right') {
|
|
2361
2771
|
// Don't allow selecting blank or info lines
|
|
2362
2772
|
if (items[selectedIndex].type !== 'blank' && items[selectedIndex].type !== 'info') {
|
|
@@ -2393,26 +2803,73 @@ async function showProviderManagerMenu() {
|
|
|
2393
2803
|
if (!def) return;
|
|
2394
2804
|
const isSelected = idx === selectedIndex;
|
|
2395
2805
|
const isEnabled = enabled[id] !== false;
|
|
2396
|
-
const statusLabel = isEnabled ? chalk.green('ENABLED') : chalk.red('DISABLED');
|
|
2397
|
-
const typeLabel = def.type === 'ide' ? chalk.cyan('IDE') : chalk.cyan('LLM');
|
|
2398
|
-
const prefix = isSelected ? chalk.cyan('āÆ') : ' ';
|
|
2399
|
-
let line = `${prefix} ${idx + 1}. ${def.name} ${chalk.gray(`(${def.id})`)} ${typeLabel} ${statusLabel}`;
|
|
2400
2806
|
|
|
2401
2807
|
// Check for rate limits
|
|
2402
2808
|
const timeUntilReset = providerManager.getTimeUntilReset(id, def.model || id);
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2809
|
+
|
|
2810
|
+
// Check for Kiro installation
|
|
2811
|
+
let isInstalled = true;
|
|
2812
|
+
if (id === 'kiro') {
|
|
2813
|
+
try {
|
|
2814
|
+
const { isKiroInstalled } = require('./kiro-installer');
|
|
2815
|
+
isInstalled = isKiroInstalled();
|
|
2816
|
+
} catch (e) {
|
|
2817
|
+
// Ignore error provider checks
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2821
|
+
// Determine status emoji: disabled = red alert, rate limited = green circle, enabled = green circle
|
|
2822
|
+
let statusEmoji;
|
|
2823
|
+
if (id === 'kiro' && !isInstalled) {
|
|
2824
|
+
statusEmoji = 'š”'; // Yellow for not installed
|
|
2825
|
+
} else {
|
|
2826
|
+
statusEmoji = !isEnabled ? 'šØ' : 'š¢';
|
|
2827
|
+
}
|
|
2828
|
+
|
|
2829
|
+
const typeLabel = def.type === 'ide' ? chalk.cyan('IDE') : chalk.cyan('LLM');
|
|
2830
|
+
const prefix = isSelected ? chalk.cyan('āÆ') : ' ';
|
|
2831
|
+
let line = `${prefix} ${statusEmoji} ${idx + 1}. ${def.name} ${chalk.gray(`(${def.id})`)} ${typeLabel}`;
|
|
2832
|
+
|
|
2833
|
+
// Add rate limit time if applicable
|
|
2834
|
+
if (timeUntilReset && isEnabled) {
|
|
2835
|
+
// Calculate reset time
|
|
2836
|
+
const now = new Date();
|
|
2837
|
+
const resetTime = new Date(now.getTime() + timeUntilReset);
|
|
2838
|
+
|
|
2839
|
+
// Format reset time in human-readable format
|
|
2840
|
+
const timeFormatter = new Intl.DateTimeFormat('en-US', {
|
|
2410
2841
|
hour: 'numeric',
|
|
2411
2842
|
minute: '2-digit',
|
|
2412
|
-
hour12: true,
|
|
2413
2843
|
timeZoneName: 'short'
|
|
2414
2844
|
});
|
|
2415
|
-
|
|
2845
|
+
const timeStr = timeFormatter.format(resetTime);
|
|
2846
|
+
|
|
2847
|
+
// Determine if it's today, tomorrow, or a specific date
|
|
2848
|
+
const today = new Date();
|
|
2849
|
+
today.setHours(0, 0, 0, 0);
|
|
2850
|
+
const resetDate = new Date(resetTime);
|
|
2851
|
+
resetDate.setHours(0, 0, 0, 0);
|
|
2852
|
+
const tomorrow = new Date(today);
|
|
2853
|
+
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
2854
|
+
|
|
2855
|
+
let dateStr = '';
|
|
2856
|
+
if (resetDate.getTime() === today.getTime()) {
|
|
2857
|
+
// Today - just show time
|
|
2858
|
+
dateStr = timeStr;
|
|
2859
|
+
} else if (resetDate.getTime() === tomorrow.getTime()) {
|
|
2860
|
+
// Tomorrow
|
|
2861
|
+
dateStr = `tomorrow, ${timeStr}`;
|
|
2862
|
+
} else {
|
|
2863
|
+
// Specific date - show date and time
|
|
2864
|
+
const dateFormatter = new Intl.DateTimeFormat('en-US', {
|
|
2865
|
+
month: 'short',
|
|
2866
|
+
day: 'numeric',
|
|
2867
|
+
year: 'numeric'
|
|
2868
|
+
});
|
|
2869
|
+
dateStr = `${timeStr} ${dateFormatter.format(resetTime)}`;
|
|
2870
|
+
}
|
|
2871
|
+
|
|
2872
|
+
line += ` ${chalk.red('rate limited until ' + dateStr)}`;
|
|
2416
2873
|
}
|
|
2417
2874
|
|
|
2418
2875
|
console.log(line);
|
|
@@ -2441,6 +2898,18 @@ async function showProviderManagerMenu() {
|
|
|
2441
2898
|
await saveProviderPreferences(order, enabled);
|
|
2442
2899
|
}
|
|
2443
2900
|
if (selectedId) {
|
|
2901
|
+
// Check for Kiro installation if selected
|
|
2902
|
+
if (selectedId === 'kiro') {
|
|
2903
|
+
try {
|
|
2904
|
+
const { isKiroInstalled, installKiro } = require('./kiro-installer');
|
|
2905
|
+
if (!isKiroInstalled()) {
|
|
2906
|
+
await installKiro();
|
|
2907
|
+
}
|
|
2908
|
+
} catch (e) {
|
|
2909
|
+
console.log(chalk.red('Error checking/installing Kiro: ' + e.message));
|
|
2910
|
+
}
|
|
2911
|
+
}
|
|
2912
|
+
|
|
2444
2913
|
const { setAutoConfig } = require('./config');
|
|
2445
2914
|
await setAutoConfig({ agent: selectedId, ide: selectedId });
|
|
2446
2915
|
const def = defMap.get(selectedId);
|
|
@@ -2515,6 +2984,7 @@ async function showProviderManagerMenu() {
|
|
|
2515
2984
|
saveAndExit(order[selectedIndex]);
|
|
2516
2985
|
break;
|
|
2517
2986
|
case 'escape':
|
|
2987
|
+
case 'left':
|
|
2518
2988
|
case 'x':
|
|
2519
2989
|
cancel();
|
|
2520
2990
|
break;
|
|
@@ -2620,6 +3090,276 @@ async function showSettings() {
|
|
|
2620
3090
|
}
|
|
2621
3091
|
}
|
|
2622
3092
|
|
|
3093
|
+
/**
|
|
3094
|
+
* Show cloud sync management menu
|
|
3095
|
+
*/
|
|
3096
|
+
async function showCloudSyncMenu() {
|
|
3097
|
+
console.clear();
|
|
3098
|
+
console.log(chalk.bold.cyan('\nāļø Cloud Sync Management\n'));
|
|
3099
|
+
|
|
3100
|
+
// Check if cloud sync is configured
|
|
3101
|
+
try {
|
|
3102
|
+
const SyncEngine = require('vibecodingmachine-core/src/sync/sync-engine');
|
|
3103
|
+
const testEngine = new SyncEngine();
|
|
3104
|
+
await testEngine.initialize();
|
|
3105
|
+
testEngine.stop();
|
|
3106
|
+
} catch (error) {
|
|
3107
|
+
console.log(chalk.yellow('ā ļø Cloud sync is not configured.\n'));
|
|
3108
|
+
console.log(chalk.white('To set up cloud sync:\n'));
|
|
3109
|
+
console.log(chalk.gray('1. Run: ') + chalk.cyan('./scripts/setup-cloud-sync.sh'));
|
|
3110
|
+
console.log(chalk.gray('2. Add AWS configuration to your .env file'));
|
|
3111
|
+
console.log(chalk.gray('3. Restart vcm\n'));
|
|
3112
|
+
console.log(chalk.gray('For more info, see: ') + chalk.cyan('docs/CLOUD_SYNC.md\n'));
|
|
3113
|
+
|
|
3114
|
+
console.log(chalk.gray('Press Enter to return to main menu...'));
|
|
3115
|
+
await new Promise(resolve => {
|
|
3116
|
+
const rl = readline.createInterface({
|
|
3117
|
+
input: process.stdin,
|
|
3118
|
+
output: process.stdout
|
|
3119
|
+
});
|
|
3120
|
+
rl.question('', () => {
|
|
3121
|
+
rl.close();
|
|
3122
|
+
resolve();
|
|
3123
|
+
});
|
|
3124
|
+
});
|
|
3125
|
+
return;
|
|
3126
|
+
}
|
|
3127
|
+
|
|
3128
|
+
const computerCommands = require('../commands/computers');
|
|
3129
|
+
const syncCommands = require('../commands/sync');
|
|
3130
|
+
|
|
3131
|
+
const choices = [
|
|
3132
|
+
{ name: 'š View All Computers', value: 'computers' },
|
|
3133
|
+
{ name: 'š„ļø Manage Another Computer\'s Requirements', value: 'manage-remote' },
|
|
3134
|
+
{ name: 'š Sync Now', value: 'sync-now' },
|
|
3135
|
+
{ name: 'š Sync Status', value: 'sync-status' },
|
|
3136
|
+
{ name: 'š Sync History', value: 'sync-history' },
|
|
3137
|
+
{ name: 'š View Offline Queue', value: 'sync-queue' },
|
|
3138
|
+
{ name: 'š„ļø Register This Computer', value: 'register' },
|
|
3139
|
+
{ name: 'šÆ Update Focus Area', value: 'update-focus' },
|
|
3140
|
+
{ name: chalk.gray('ā Back to Main Menu'), value: 'back' }
|
|
3141
|
+
];
|
|
3142
|
+
|
|
3143
|
+
const { action } = await inquirer.prompt([
|
|
3144
|
+
{
|
|
3145
|
+
type: 'list',
|
|
3146
|
+
name: 'action',
|
|
3147
|
+
message: 'Select an option:',
|
|
3148
|
+
choices: choices
|
|
3149
|
+
}
|
|
3150
|
+
]);
|
|
3151
|
+
|
|
3152
|
+
switch (action) {
|
|
3153
|
+
case 'computers':
|
|
3154
|
+
try {
|
|
3155
|
+
await computerCommands.listComputers();
|
|
3156
|
+
} catch (error) {
|
|
3157
|
+
console.log(chalk.red('\nā Error: ') + error.message);
|
|
3158
|
+
console.log(chalk.gray('\nTip: Make sure AWS credentials are configured and DynamoDB tables exist.'));
|
|
3159
|
+
}
|
|
3160
|
+
console.log(chalk.gray('\nPress Enter to continue...'));
|
|
3161
|
+
await new Promise(resolve => {
|
|
3162
|
+
const rl = readline.createInterface({
|
|
3163
|
+
input: process.stdin,
|
|
3164
|
+
output: process.stdout
|
|
3165
|
+
});
|
|
3166
|
+
rl.question('', () => {
|
|
3167
|
+
rl.close();
|
|
3168
|
+
resolve();
|
|
3169
|
+
});
|
|
3170
|
+
});
|
|
3171
|
+
await showCloudSyncMenu();
|
|
3172
|
+
break;
|
|
3173
|
+
|
|
3174
|
+
case 'manage-remote':
|
|
3175
|
+
try {
|
|
3176
|
+
// First, get list of computers
|
|
3177
|
+
const SyncEngine = require('vibecodingmachine-core/src/sync/sync-engine');
|
|
3178
|
+
const { ScanCommand } = require('@aws-sdk/lib-dynamodb');
|
|
3179
|
+
|
|
3180
|
+
const syncEngine = new SyncEngine();
|
|
3181
|
+
await syncEngine.initialize();
|
|
3182
|
+
|
|
3183
|
+
const tableName = 'vibecodingmachine-computers';
|
|
3184
|
+
const command = new ScanCommand({ TableName: tableName });
|
|
3185
|
+
const response = await syncEngine.dynamoClient.send(command);
|
|
3186
|
+
const computers = response.Items || [];
|
|
3187
|
+
|
|
3188
|
+
syncEngine.stop();
|
|
3189
|
+
|
|
3190
|
+
if (computers.length === 0) {
|
|
3191
|
+
console.log(chalk.yellow('\nā No computers registered yet.\n'));
|
|
3192
|
+
} else {
|
|
3193
|
+
// Let user select a computer
|
|
3194
|
+
const computerChoices = computers.map(c => ({
|
|
3195
|
+
name: `${c.hostname || c.computerId} - ${c.focusArea || 'No focus'}`,
|
|
3196
|
+
value: c.computerId
|
|
3197
|
+
}));
|
|
3198
|
+
computerChoices.push({ name: chalk.gray('ā Cancel'), value: null });
|
|
3199
|
+
|
|
3200
|
+
const { selectedComputer } = await inquirer.prompt([
|
|
3201
|
+
{
|
|
3202
|
+
type: 'list',
|
|
3203
|
+
name: 'selectedComputer',
|
|
3204
|
+
message: 'Select computer to manage:',
|
|
3205
|
+
choices: computerChoices
|
|
3206
|
+
}
|
|
3207
|
+
]);
|
|
3208
|
+
|
|
3209
|
+
if (selectedComputer) {
|
|
3210
|
+
const remoteReqCommands = require('../commands/requirements-remote');
|
|
3211
|
+
await remoteReqCommands.manageRemoteRequirements(selectedComputer);
|
|
3212
|
+
}
|
|
3213
|
+
}
|
|
3214
|
+
} catch (error) {
|
|
3215
|
+
console.log(chalk.red('\nā Error: ') + error.message);
|
|
3216
|
+
}
|
|
3217
|
+
await showCloudSyncMenu();
|
|
3218
|
+
break;
|
|
3219
|
+
|
|
3220
|
+
case 'sync-now':
|
|
3221
|
+
try {
|
|
3222
|
+
await syncCommands.syncNow();
|
|
3223
|
+
} catch (error) {
|
|
3224
|
+
console.log(chalk.red('\nā Error: ') + error.message);
|
|
3225
|
+
}
|
|
3226
|
+
console.log(chalk.gray('\nPress Enter to continue...'));
|
|
3227
|
+
await new Promise(resolve => {
|
|
3228
|
+
const rl = readline.createInterface({
|
|
3229
|
+
input: process.stdin,
|
|
3230
|
+
output: process.stdout
|
|
3231
|
+
});
|
|
3232
|
+
rl.question('', () => {
|
|
3233
|
+
rl.close();
|
|
3234
|
+
resolve();
|
|
3235
|
+
});
|
|
3236
|
+
});
|
|
3237
|
+
await showCloudSyncMenu();
|
|
3238
|
+
break;
|
|
3239
|
+
|
|
3240
|
+
case 'sync-status':
|
|
3241
|
+
try {
|
|
3242
|
+
await syncCommands.syncStatus();
|
|
3243
|
+
} catch (error) {
|
|
3244
|
+
console.log(chalk.red('\nā Error: ') + error.message);
|
|
3245
|
+
}
|
|
3246
|
+
console.log(chalk.gray('\nPress Enter to continue...'));
|
|
3247
|
+
await new Promise(resolve => {
|
|
3248
|
+
const rl = readline.createInterface({
|
|
3249
|
+
input: process.stdin,
|
|
3250
|
+
output: process.stdout
|
|
3251
|
+
});
|
|
3252
|
+
rl.question('', () => {
|
|
3253
|
+
rl.close();
|
|
3254
|
+
resolve();
|
|
3255
|
+
});
|
|
3256
|
+
});
|
|
3257
|
+
await showCloudSyncMenu();
|
|
3258
|
+
break;
|
|
3259
|
+
|
|
3260
|
+
case 'sync-history':
|
|
3261
|
+
try {
|
|
3262
|
+
await syncCommands.viewHistory({ limit: 50 });
|
|
3263
|
+
} catch (error) {
|
|
3264
|
+
console.log(chalk.red('\nā Error: ') + error.message);
|
|
3265
|
+
}
|
|
3266
|
+
console.log(chalk.gray('\nPress Enter to continue...'));
|
|
3267
|
+
await new Promise(resolve => {
|
|
3268
|
+
const rl = readline.createInterface({
|
|
3269
|
+
input: process.stdin,
|
|
3270
|
+
output: process.stdout
|
|
3271
|
+
});
|
|
3272
|
+
rl.question('', () => {
|
|
3273
|
+
rl.close();
|
|
3274
|
+
resolve();
|
|
3275
|
+
});
|
|
3276
|
+
});
|
|
3277
|
+
await showCloudSyncMenu();
|
|
3278
|
+
break;
|
|
3279
|
+
|
|
3280
|
+
case 'sync-queue':
|
|
3281
|
+
try {
|
|
3282
|
+
await syncCommands.viewQueue();
|
|
3283
|
+
} catch (error) {
|
|
3284
|
+
console.log(chalk.red('\nā Error: ') + error.message);
|
|
3285
|
+
}
|
|
3286
|
+
console.log(chalk.gray('\nPress Enter to continue...'));
|
|
3287
|
+
await new Promise(resolve => {
|
|
3288
|
+
const rl = readline.createInterface({
|
|
3289
|
+
input: process.stdin,
|
|
3290
|
+
output: process.stdout
|
|
3291
|
+
});
|
|
3292
|
+
rl.question('', () => {
|
|
3293
|
+
rl.close();
|
|
3294
|
+
resolve();
|
|
3295
|
+
});
|
|
3296
|
+
});
|
|
3297
|
+
await showCloudSyncMenu();
|
|
3298
|
+
break;
|
|
3299
|
+
|
|
3300
|
+
case 'register':
|
|
3301
|
+
try {
|
|
3302
|
+
const { focusArea } = await inquirer.prompt([
|
|
3303
|
+
{
|
|
3304
|
+
type: 'input',
|
|
3305
|
+
name: 'focusArea',
|
|
3306
|
+
message: 'Enter focus area for this computer:',
|
|
3307
|
+
default: 'General Development'
|
|
3308
|
+
}
|
|
3309
|
+
]);
|
|
3310
|
+
await computerCommands.registerComputer(focusArea);
|
|
3311
|
+
} catch (error) {
|
|
3312
|
+
console.log(chalk.red('\nā Error: ') + error.message);
|
|
3313
|
+
}
|
|
3314
|
+
console.log(chalk.gray('\nPress Enter to continue...'));
|
|
3315
|
+
await new Promise(resolve => {
|
|
3316
|
+
const rl = readline.createInterface({
|
|
3317
|
+
input: process.stdin,
|
|
3318
|
+
output: process.stdout
|
|
3319
|
+
});
|
|
3320
|
+
rl.question('', () => {
|
|
3321
|
+
rl.close();
|
|
3322
|
+
resolve();
|
|
3323
|
+
});
|
|
3324
|
+
});
|
|
3325
|
+
await showCloudSyncMenu();
|
|
3326
|
+
break;
|
|
3327
|
+
|
|
3328
|
+
case 'update-focus':
|
|
3329
|
+
try {
|
|
3330
|
+
const { newFocus } = await inquirer.prompt([
|
|
3331
|
+
{
|
|
3332
|
+
type: 'input',
|
|
3333
|
+
name: 'newFocus',
|
|
3334
|
+
message: 'Enter new focus area:'
|
|
3335
|
+
}
|
|
3336
|
+
]);
|
|
3337
|
+
if (newFocus) {
|
|
3338
|
+
await computerCommands.updateFocus(newFocus);
|
|
3339
|
+
}
|
|
3340
|
+
} catch (error) {
|
|
3341
|
+
console.log(chalk.red('\nā Error: ') + error.message);
|
|
3342
|
+
}
|
|
3343
|
+
console.log(chalk.gray('\nPress Enter to continue...'));
|
|
3344
|
+
await new Promise(resolve => {
|
|
3345
|
+
const rl = readline.createInterface({
|
|
3346
|
+
input: process.stdin,
|
|
3347
|
+
output: process.stdout
|
|
3348
|
+
});
|
|
3349
|
+
rl.question('', () => {
|
|
3350
|
+
rl.close();
|
|
3351
|
+
resolve();
|
|
3352
|
+
});
|
|
3353
|
+
});
|
|
3354
|
+
await showCloudSyncMenu();
|
|
3355
|
+
break;
|
|
3356
|
+
|
|
3357
|
+
case 'back':
|
|
3358
|
+
// Return to main menu
|
|
3359
|
+
break;
|
|
3360
|
+
}
|
|
3361
|
+
}
|
|
3362
|
+
|
|
2623
3363
|
async function startInteractive() {
|
|
2624
3364
|
// STRICT AUTH CHECK (only if enabled)
|
|
2625
3365
|
const authEnabled = process.env.AUTH_ENABLED === 'true';
|
|
@@ -2679,12 +3419,22 @@ async function startInteractive() {
|
|
|
2679
3419
|
// Build dynamic menu items - settings at top (gray, no letters), actions below (with letters)
|
|
2680
3420
|
const items = [];
|
|
2681
3421
|
|
|
2682
|
-
// Get
|
|
2683
|
-
const
|
|
2684
|
-
|
|
3422
|
+
// Get first ENABLED agent from provider preferences
|
|
3423
|
+
const { getProviderPreferences } = require('../utils/provider-registry');
|
|
3424
|
+
const prefs = await getProviderPreferences();
|
|
3425
|
+
let firstEnabledAgent = null;
|
|
3426
|
+
for (const agentId of prefs.order) {
|
|
3427
|
+
if (prefs.enabled[agentId] !== false) {
|
|
3428
|
+
firstEnabledAgent = agentId;
|
|
3429
|
+
break;
|
|
3430
|
+
}
|
|
3431
|
+
}
|
|
3432
|
+
// Fallback to current agent if no enabled agents found
|
|
3433
|
+
const displayAgent = firstEnabledAgent || autoConfig.agent || autoConfig.ide || 'ollama';
|
|
3434
|
+
let agentDisplay = `First Agent: ${chalk.cyan(getAgentDisplayName(displayAgent))}`;
|
|
2685
3435
|
|
|
2686
3436
|
// Check for rate limits (for LLM-based agents and Claude Code)
|
|
2687
|
-
if (
|
|
3437
|
+
if (displayAgent === 'ollama' || displayAgent === 'groq' || displayAgent === 'anthropic' || displayAgent === 'bedrock' || displayAgent === 'claude-code') {
|
|
2688
3438
|
try {
|
|
2689
3439
|
const ProviderManager = require('vibecodingmachine-core/src/ide-integration/provider-manager.cjs');
|
|
2690
3440
|
const providerManager = new ProviderManager();
|
|
@@ -2699,44 +3449,47 @@ async function startInteractive() {
|
|
|
2699
3449
|
|
|
2700
3450
|
// Get the model based on the current agent type
|
|
2701
3451
|
let model;
|
|
2702
|
-
if (
|
|
3452
|
+
if (displayAgent === 'groq') {
|
|
2703
3453
|
model = config.auto?.groqModel || config.auto?.aiderModel || config.auto?.llmModel;
|
|
2704
3454
|
// Remove groq/ prefix if present
|
|
2705
3455
|
if (model && model.includes('groq/')) {
|
|
2706
3456
|
model = model.split('/')[1];
|
|
2707
3457
|
}
|
|
2708
|
-
} else if (
|
|
3458
|
+
} else if (displayAgent === 'anthropic') {
|
|
2709
3459
|
model = config.auto?.anthropicModel || config.auto?.aiderModel || config.auto?.llmModel;
|
|
2710
|
-
} else if (
|
|
3460
|
+
} else if (displayAgent === 'ollama') {
|
|
2711
3461
|
const rawModel = config.auto?.llmModel || config.auto?.aiderModel;
|
|
2712
3462
|
// Only use if it doesn't have groq/ prefix
|
|
2713
3463
|
model = rawModel && !rawModel.includes('groq/') ? rawModel : null;
|
|
2714
|
-
} else if (
|
|
3464
|
+
} else if (displayAgent === 'bedrock') {
|
|
2715
3465
|
model = 'anthropic.claude-sonnet-4-v1';
|
|
2716
|
-
} else if (
|
|
3466
|
+
} else if (displayAgent === 'claude-code') {
|
|
2717
3467
|
model = 'claude-code-cli';
|
|
2718
3468
|
}
|
|
2719
3469
|
|
|
2720
3470
|
// For Claude Code, use fixed model name
|
|
2721
|
-
const checkModel =
|
|
2722
|
-
const provider =
|
|
3471
|
+
const checkModel = displayAgent === 'claude-code' ? 'claude-code-cli' : model;
|
|
3472
|
+
const provider = displayAgent === 'ollama' ? 'ollama' : displayAgent;
|
|
2723
3473
|
|
|
2724
3474
|
if (checkModel) {
|
|
2725
3475
|
const timeUntilReset = providerManager.getTimeUntilReset(provider, checkModel);
|
|
2726
3476
|
|
|
2727
3477
|
if (timeUntilReset) {
|
|
2728
|
-
|
|
2729
|
-
const
|
|
2730
|
-
const
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
}
|
|
2739
|
-
|
|
3478
|
+
// Format time remaining in human-readable format
|
|
3479
|
+
const hours = Math.floor(timeUntilReset / (1000 * 60 * 60));
|
|
3480
|
+
const minutes = Math.floor((timeUntilReset % (1000 * 60 * 60)) / (1000 * 60));
|
|
3481
|
+
const seconds = Math.floor((timeUntilReset % (1000 * 60)) / 1000);
|
|
3482
|
+
|
|
3483
|
+
let timeStr = '';
|
|
3484
|
+
if (hours > 0) {
|
|
3485
|
+
timeStr = `${hours}h ${minutes}m`;
|
|
3486
|
+
} else if (minutes > 0) {
|
|
3487
|
+
timeStr = `${minutes}m ${seconds}s`;
|
|
3488
|
+
} else {
|
|
3489
|
+
timeStr = `${seconds}s`;
|
|
3490
|
+
}
|
|
3491
|
+
|
|
3492
|
+
agentDisplay += ` ${chalk.red('ā° Rate limit resets in ' + timeStr)}`;
|
|
2740
3493
|
}
|
|
2741
3494
|
}
|
|
2742
3495
|
}
|
|
@@ -2754,9 +3507,6 @@ async function startInteractive() {
|
|
|
2754
3507
|
autoConfig.maxChats ? `Stop after ${autoConfig.maxChats}` :
|
|
2755
3508
|
'Never Stop';
|
|
2756
3509
|
|
|
2757
|
-
// Get restart CLI setting from autoConfig
|
|
2758
|
-
const restartCLI = autoConfig.restartCLI ? chalk.green('Enabled ā') : chalk.yellow('Disabled ā');
|
|
2759
|
-
|
|
2760
3510
|
if (autoStatus.running) {
|
|
2761
3511
|
items.push({
|
|
2762
3512
|
type: 'setting',
|
|
@@ -2778,45 +3528,27 @@ async function startInteractive() {
|
|
|
2778
3528
|
value: 'setting:auto-stop-condition'
|
|
2779
3529
|
});
|
|
2780
3530
|
|
|
2781
|
-
// Add
|
|
2782
|
-
items.push({
|
|
2783
|
-
type: 'setting',
|
|
2784
|
-
name: ` āā Restart CLI after each completed requirement: ${restartCLI}`,
|
|
2785
|
-
value: 'setting:restart-cli'
|
|
2786
|
-
});
|
|
2787
|
-
|
|
2788
|
-
// Add setup alias setting
|
|
2789
|
-
items.push({
|
|
2790
|
-
type: 'setting',
|
|
2791
|
-
name: ` āā Setup 'vcm' alias`,
|
|
2792
|
-
value: 'setting:setup-alias'
|
|
2793
|
-
});
|
|
2794
|
-
|
|
2795
|
-
// Add current agent setting
|
|
3531
|
+
// Add current agent setting (rate limit info already included in agentDisplay if applicable)
|
|
2796
3532
|
items.push({
|
|
2797
3533
|
type: 'setting',
|
|
2798
3534
|
name: ` āā ${agentDisplay}`,
|
|
2799
3535
|
value: 'setting:agent'
|
|
2800
3536
|
});
|
|
2801
3537
|
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
3538
|
// Add Requirements as a selectable setting with counts
|
|
2805
3539
|
const hasRequirements = await requirementsExists();
|
|
2806
3540
|
const counts = hasRequirements ? await countRequirements() : null;
|
|
2807
3541
|
let requirementsText = 'Requirements: ';
|
|
2808
3542
|
if (counts) {
|
|
3543
|
+
// Calculate actual iterations: lesser of stop after number and TODO requirements
|
|
3544
|
+
const actualIterations = autoConfig.neverStop ? counts.todoCount :
|
|
3545
|
+
Math.min(autoConfig.maxChats || counts.todoCount, counts.todoCount);
|
|
2809
3546
|
const total = counts.todoCount + counts.toVerifyCount + counts.verifiedCount;
|
|
2810
3547
|
if (total > 0) {
|
|
2811
|
-
const todoPercent = Math.round((
|
|
3548
|
+
const todoPercent = Math.round((actualIterations / total) * 100);
|
|
2812
3549
|
const toVerifyPercent = Math.round((counts.toVerifyCount / total) * 100);
|
|
2813
3550
|
const verifiedPercent = Math.round((counts.verifiedCount / total) * 100);
|
|
2814
|
-
requirementsText += `${chalk.yellow(
|
|
2815
|
-
|
|
2816
|
-
// Add warning if no TODO requirements
|
|
2817
|
-
if (counts.todoCount === 0) {
|
|
2818
|
-
requirementsText += ` ${chalk.red('ā ļø No requirements to work on')}`;
|
|
2819
|
-
}
|
|
3551
|
+
requirementsText += `${chalk.yellow(actualIterations + ' (' + todoPercent + '%) TODO')}, ${chalk.cyan(counts.toVerifyCount + ' (' + toVerifyPercent + '%) TO VERIFY')}, ${chalk.green(counts.verifiedCount + ' (' + verifiedPercent + '%) VERIFIED')}`;
|
|
2820
3552
|
} else {
|
|
2821
3553
|
requirementsText = '';
|
|
2822
3554
|
}
|
|
@@ -2836,10 +3568,47 @@ async function startInteractive() {
|
|
|
2836
3568
|
|
|
2837
3569
|
items.push({
|
|
2838
3570
|
type: 'setting',
|
|
2839
|
-
name: ` āā Use Hostname in Req File: ${useHostname ? chalk.green('
|
|
3571
|
+
name: ` āā Use Hostname in Req File: ${useHostname ? chalk.green('ā') : chalk.red('š')} ${useHostname ? 'ā ENABLED' : 'š DISABLED'}`,
|
|
2840
3572
|
value: 'setting:hostname'
|
|
2841
3573
|
});
|
|
2842
3574
|
|
|
3575
|
+
// Cloud Sync Status
|
|
3576
|
+
try {
|
|
3577
|
+
const SyncEngine = require('vibecodingmachine-core/src/sync/sync-engine');
|
|
3578
|
+
const syncEngine = new SyncEngine();
|
|
3579
|
+
|
|
3580
|
+
// Try to initialize, but don't fail if AWS not configured
|
|
3581
|
+
try {
|
|
3582
|
+
await syncEngine.initialize();
|
|
3583
|
+
const syncStatus = syncEngine.getStatus();
|
|
3584
|
+
syncEngine.stop();
|
|
3585
|
+
|
|
3586
|
+
const onlineIcon = syncStatus.isOnline ? chalk.green('ā') : chalk.red('ā');
|
|
3587
|
+
const onlineText = syncStatus.isOnline ? 'Online' : 'Offline';
|
|
3588
|
+
const queueText = syncStatus.queuedChanges > 0 ? chalk.yellow(` (${syncStatus.queuedChanges} queued)`) : '';
|
|
3589
|
+
|
|
3590
|
+
items.push({
|
|
3591
|
+
type: 'setting',
|
|
3592
|
+
name: `Cloud Sync: ${onlineIcon} ${onlineText}${queueText}`,
|
|
3593
|
+
value: 'setting:cloud-sync'
|
|
3594
|
+
});
|
|
3595
|
+
} catch (initError) {
|
|
3596
|
+
// Initialization failed - AWS not configured or credentials missing
|
|
3597
|
+
items.push({
|
|
3598
|
+
type: 'setting',
|
|
3599
|
+
name: `Cloud Sync: ${chalk.gray('ā Not configured')}`,
|
|
3600
|
+
value: 'setting:cloud-sync-setup'
|
|
3601
|
+
});
|
|
3602
|
+
}
|
|
3603
|
+
} catch (error) {
|
|
3604
|
+
// Module not found or other error - show disabled
|
|
3605
|
+
items.push({
|
|
3606
|
+
type: 'setting',
|
|
3607
|
+
name: `Cloud Sync: ${chalk.gray('ā Disabled')}`,
|
|
3608
|
+
value: 'setting:cloud-sync-setup'
|
|
3609
|
+
});
|
|
3610
|
+
}
|
|
3611
|
+
|
|
2843
3612
|
// Add "Next TODO Requirement" as a separate menu item if there are TODO items
|
|
2844
3613
|
if (counts && counts.todoCount > 0) {
|
|
2845
3614
|
// Get the actual next requirement text (new header format)
|
|
@@ -2886,32 +3655,32 @@ async function startInteractive() {
|
|
|
2886
3655
|
if (nextLine.startsWith('###') || (nextLine.startsWith('##') && !nextLine.startsWith('###'))) {
|
|
2887
3656
|
break;
|
|
2888
3657
|
}
|
|
3658
|
+
|
|
2889
3659
|
description += nextLine + '\n';
|
|
2890
3660
|
}
|
|
2891
|
-
nextReqText = title;
|
|
3661
|
+
nextReqText = title + '\n' + description;
|
|
2892
3662
|
break;
|
|
2893
3663
|
}
|
|
2894
3664
|
}
|
|
2895
3665
|
}
|
|
3666
|
+
items.push({
|
|
3667
|
+
type: 'info',
|
|
3668
|
+
name: ` āā Next TODO Requirement: ${nextReqText}`,
|
|
3669
|
+
value: 'info:next-requirement'
|
|
3670
|
+
});
|
|
2896
3671
|
}
|
|
2897
3672
|
} catch (err) {
|
|
2898
|
-
console.error('Error
|
|
3673
|
+
console.error('Error reading requirements file:', err.message);
|
|
2899
3674
|
}
|
|
2900
|
-
|
|
2901
|
-
// Add "Next TODO Requirement" to the menu
|
|
2902
|
-
items.push({
|
|
2903
|
-
type: 'info',
|
|
2904
|
-
name: `Next TODO Requirement: ${nextReqText}`,
|
|
2905
|
-
value: 'next-req'
|
|
2906
|
-
});
|
|
2907
3675
|
}
|
|
2908
3676
|
|
|
3677
|
+
|
|
2909
3678
|
// Add warning message if no TODO requirements and Auto Mode is stopped
|
|
2910
3679
|
if (counts && counts.todoCount === 0 && !autoStatus.running) {
|
|
2911
3680
|
items.push({
|
|
2912
|
-
type: '
|
|
3681
|
+
type: 'info',
|
|
2913
3682
|
name: chalk.red(' ā ļø No requirements to work on - cannot start Auto Mode'),
|
|
2914
|
-
value: '
|
|
3683
|
+
value: 'info:no-requirements'
|
|
2915
3684
|
});
|
|
2916
3685
|
}
|
|
2917
3686
|
|
|
@@ -2924,6 +3693,8 @@ async function startInteractive() {
|
|
|
2924
3693
|
items.push({ type: 'action', name: 'Initialize repository (.vibecodingmachine)', value: 'repo:init' });
|
|
2925
3694
|
}
|
|
2926
3695
|
|
|
3696
|
+
items.push({ type: 'action', name: 'View All Computers', value: 'computers:list' });
|
|
3697
|
+
items.push({ type: 'action', name: 'Sync Now', value: 'sync:now' });
|
|
2927
3698
|
items.push({ type: 'action', name: 'Logout', value: 'logout' });
|
|
2928
3699
|
items.push({ type: 'action', name: 'Exit', value: 'exit' });
|
|
2929
3700
|
|
|
@@ -3313,13 +4084,13 @@ async function startInteractive() {
|
|
|
3313
4084
|
const { maxChats } = await inquirer.prompt([{
|
|
3314
4085
|
type: 'input',
|
|
3315
4086
|
name: 'maxChats',
|
|
3316
|
-
message: 'Max chats (
|
|
4087
|
+
message: 'Max chats (0 for never stop):',
|
|
3317
4088
|
default: defaultMaxChats
|
|
3318
4089
|
}]);
|
|
3319
4090
|
|
|
3320
4091
|
// Update config
|
|
3321
4092
|
const newConfig = { ...currentConfig };
|
|
3322
|
-
if (maxChats && maxChats.trim() !== '') {
|
|
4093
|
+
if (maxChats && maxChats.trim() !== '' && maxChats.trim() !== '0') {
|
|
3323
4094
|
newConfig.maxChats = parseInt(maxChats);
|
|
3324
4095
|
newConfig.neverStop = false;
|
|
3325
4096
|
console.log(chalk.green('\nā'), `Stop condition updated: ${chalk.cyan(`Stop after ${newConfig.maxChats}`)}\n`);
|
|
@@ -3336,43 +4107,82 @@ async function startInteractive() {
|
|
|
3336
4107
|
await showWelcomeScreen();
|
|
3337
4108
|
break;
|
|
3338
4109
|
}
|
|
3339
|
-
case 'setting:
|
|
3340
|
-
//
|
|
3341
|
-
|
|
3342
|
-
const { getAutoConfig, setAutoConfig } = require('./config');
|
|
3343
|
-
const currentConfig = await getAutoConfig();
|
|
3344
|
-
const newConfig = { ...currentConfig, restartCLI: !currentConfig.restartCLI };
|
|
3345
|
-
await setAutoConfig(newConfig);
|
|
3346
|
-
const statusText = newConfig.restartCLI ? chalk.green('enabled') : chalk.yellow('disabled');
|
|
3347
|
-
console.log(chalk.green('\nā'), `Restart CLI after each completed requirement ${statusText}\n`);
|
|
3348
|
-
} catch (error) {
|
|
3349
|
-
console.log(chalk.red('\nā Error updating restart CLI setting:', error.message));
|
|
3350
|
-
}
|
|
4110
|
+
case 'setting:requirements': {
|
|
4111
|
+
// Show tree-style requirements navigator
|
|
4112
|
+
await showRequirementsTree();
|
|
3351
4113
|
await showWelcomeScreen();
|
|
3352
4114
|
break;
|
|
3353
4115
|
}
|
|
3354
|
-
case '
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
4116
|
+
case 'repo:init':
|
|
4117
|
+
await repo.initRepo();
|
|
4118
|
+
break;
|
|
4119
|
+
|
|
4120
|
+
case 'computers:list': {
|
|
4121
|
+
const computerCommands = require('../commands/computers');
|
|
4122
|
+
await computerCommands.listComputers();
|
|
4123
|
+
console.log(chalk.gray('\nPress Enter to continue...'));
|
|
4124
|
+
await new Promise(resolve => {
|
|
4125
|
+
const rl = readline.createInterface({
|
|
4126
|
+
input: process.stdin,
|
|
4127
|
+
output: process.stdout
|
|
4128
|
+
});
|
|
4129
|
+
rl.question('', () => {
|
|
4130
|
+
rl.close();
|
|
4131
|
+
resolve();
|
|
4132
|
+
});
|
|
4133
|
+
});
|
|
3364
4134
|
await showWelcomeScreen();
|
|
3365
4135
|
break;
|
|
3366
4136
|
}
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
4137
|
+
|
|
4138
|
+
case 'sync:now': {
|
|
4139
|
+
const syncCommands = require('../commands/sync');
|
|
4140
|
+
await syncCommands.syncNow();
|
|
4141
|
+
console.log(chalk.gray('\nPress Enter to continue...'));
|
|
4142
|
+
await new Promise(resolve => {
|
|
4143
|
+
const rl = readline.createInterface({
|
|
4144
|
+
input: process.stdin,
|
|
4145
|
+
output: process.stdout
|
|
4146
|
+
});
|
|
4147
|
+
rl.question('', () => {
|
|
4148
|
+
rl.close();
|
|
4149
|
+
resolve();
|
|
4150
|
+
});
|
|
4151
|
+
});
|
|
3370
4152
|
await showWelcomeScreen();
|
|
3371
4153
|
break;
|
|
3372
4154
|
}
|
|
3373
|
-
|
|
3374
|
-
|
|
4155
|
+
|
|
4156
|
+
case 'setting:cloud-sync': {
|
|
4157
|
+
await showCloudSyncMenu();
|
|
4158
|
+
await showWelcomeScreen();
|
|
3375
4159
|
break;
|
|
4160
|
+
}
|
|
4161
|
+
|
|
4162
|
+
case 'setting:cloud-sync-setup': {
|
|
4163
|
+
console.clear();
|
|
4164
|
+
console.log(chalk.bold.cyan('\nāļø Cloud Sync Setup\n'));
|
|
4165
|
+
console.log(chalk.yellow('Cloud sync is not configured yet.\n'));
|
|
4166
|
+
console.log(chalk.white('To set up cloud sync:\n'));
|
|
4167
|
+
console.log(chalk.gray('1. Run: ') + chalk.cyan('./scripts/setup-cloud-sync.sh'));
|
|
4168
|
+
console.log(chalk.gray('2. Add AWS configuration to your .env file'));
|
|
4169
|
+
console.log(chalk.gray('3. Register this computer with: ') + chalk.cyan('vcm computer:register "<focus>"'));
|
|
4170
|
+
console.log(chalk.gray('\nFor more info, see: ') + chalk.cyan('docs/CLOUD_SYNC.md\n'));
|
|
4171
|
+
|
|
4172
|
+
console.log(chalk.gray('Press Enter to continue...'));
|
|
4173
|
+
await new Promise(resolve => {
|
|
4174
|
+
const rl = readline.createInterface({
|
|
4175
|
+
input: process.stdin,
|
|
4176
|
+
output: process.stdout
|
|
4177
|
+
});
|
|
4178
|
+
rl.question('', () => {
|
|
4179
|
+
rl.close();
|
|
4180
|
+
resolve();
|
|
4181
|
+
});
|
|
4182
|
+
});
|
|
4183
|
+
await showWelcomeScreen();
|
|
4184
|
+
break;
|
|
4185
|
+
}
|
|
3376
4186
|
case 'auto:start': {
|
|
3377
4187
|
const { ide, maxChats } = await inquirer.prompt([
|
|
3378
4188
|
{
|
|
@@ -3393,12 +4203,12 @@ async function startInteractive() {
|
|
|
3393
4203
|
{
|
|
3394
4204
|
type: 'input',
|
|
3395
4205
|
name: 'maxChats',
|
|
3396
|
-
message: 'Max chats (
|
|
4206
|
+
message: 'Max chats (0 for never stop):',
|
|
3397
4207
|
default: ''
|
|
3398
4208
|
}
|
|
3399
4209
|
]);
|
|
3400
4210
|
const options = { ide };
|
|
3401
|
-
if (maxChats) {
|
|
4211
|
+
if (maxChats && maxChats.trim() !== '0') {
|
|
3402
4212
|
options.maxChats = parseInt(maxChats);
|
|
3403
4213
|
} else {
|
|
3404
4214
|
options.neverStop = true;
|