vibecodingmachine-cli 2025.11.2-9.855 → 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.
@@ -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(progress.requirement ? progress.requirement.substring(0, 60) + (progress.requirement.length > 60 ? '...' : '') : 'No requirement'),
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
- tree.items.push({ level: 1, type: 'section', label: `šŸŽ‰ VERIFIED (${verifiedReqs.length} - ${verifiedPercent}%)`, key: 'verified' });
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
- if (tree.expanded.verified) {
506
- verifiedReqs.forEach((req, idx) => {
507
- tree.items.push({ level: 2, type: 'verified', label: req, key: `verified-${idx}` });
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
- tree.items.push({ level: 1, type: 'section', label: `āœ… TO VERIFY (${verifyReqs.length} - ${verifyPercent}%)`, key: 'verify', section: 'āœ… Verified by AI screenshot' });
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
- if (tree.expanded.verify) {
515
- verifyReqs.forEach((req, idx) => {
516
- tree.items.push({ level: 2, type: 'requirement', label: req.title, key: `verify-${idx}`, req, sectionKey: 'verify' });
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
- tree.items.push({ level: 1, type: 'section', label: `ā“ NEEDING CLARIFICATION (${clarificationReqs.length} - ${clarificationPercent}%)`, key: 'clarification', section: 'ā“ Requirements needing manual feedback' });
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
- if (tree.expanded.clarification) {
524
- clarificationReqs.forEach((req, idx) => {
525
- tree.items.push({ level: 2, type: 'clarification', label: req.title, key: `clarification-${idx}`, req, sectionKey: 'clarification' });
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
- tree.items.push({ level: 1, type: 'section', label: `ā³ TODO (${todoReqs.length} - ${todoPercent}%)`, key: 'todo', section: 'ā³ Requirements not yet completed' });
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
- if (tree.expanded.todo) {
533
- todoReqs.forEach((req, idx) => {
534
- tree.items.push({ level: 2, type: 'requirement', label: req.title, key: `todo-${idx}`, req, sectionKey: 'todo' });
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
- tree.items.push({ level: 1, type: 'section', label: `ā™»ļø RECYCLED (${recycledReqs.length} - ${recycledPercent}%)`, key: 'recycled', section: 'ā™»ļø Recycled' });
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
- if (tree.expanded.recycled) {
542
- recycledReqs.forEach((req, idx) => {
543
- tree.items.push({ level: 2, type: 'recycled', label: req.title, key: `recycled-${idx}`, req, sectionKey: 'recycled' });
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
- if (sectionTitles.some(title => line.includes(title))) {
574
- inSection = true;
575
- continue;
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
- if (inSection && line.startsWith('## ') && !line.startsWith('###') && !sectionTitles.some(title => line.includes(title))) {
579
- break;
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
- return requirements;
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 deleteRequirement(current.req, current.sectionKey, tree);
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: 'šŸ‘ Thumbs up (move to top)', value: 'thumbs-up' },
1408
- { label: 'šŸ‘Ž Thumbs down (move to bottom)', value: 'thumbs-down' },
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 ((key.ctrl && key.name === 'c') || key.name === 'escape' || key.name === 'left') {
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
- // TO VERIFY -> TODO: Use shared function
1707
- const success = await demoteVerifyToTodo(reqPath, reqTitle);
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
- if (askPackage) {
1743
- const answer = await inquirer.prompt([{
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: pkg.includes(p)
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
- // Save to config
1760
- config.lastPackage = pkg;
1761
- await writeConfig(config);
1762
- askPackage = false;
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
- if (name === '<') {
1776
- askPackage = true;
2161
+ selectedPackage = finalPackage;
2162
+ config.lastPackage = selectedPackage;
2163
+ await writeConfig(config);
1777
2164
  continue;
1778
2165
  }
1779
2166
 
1780
- break;
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: 'line',
1794
- message: isFirstLine ? 'Description:' : ''
2170
+ name: 'name',
2171
+ message: `Enter requirement name (Package: ${pkgDisplay}):`
1795
2172
  }]);
1796
2173
 
1797
- isFirstLine = false;
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
- if (line.trim() === '') {
1800
- emptyLineCount++;
1801
- if (emptyLineCount >= 2) {
1802
- break; // Two empty lines = done
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
- } catch (err) {
1809
- break; // ESC pressed
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 previous menu (move cursor up and clear lines) - but not on first render
2192
- if (!isFirstRender && lastLinesPrinted > 0) {
2193
- // Move cursor up by the number of lines we printed last time
2194
- readline.moveCursor(process.stdout, 0, -lastLinesPrinted);
2195
- readline.clearScreenDown(process.stdout);
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
- if (timeUntilReset) {
2404
- const resetTime = Date.now() + timeUntilReset;
2405
- const resetDate = new Date(resetTime);
2406
- const timeStr = resetDate.toLocaleString('en-US', {
2407
- weekday: 'short',
2408
- month: 'short',
2409
- day: 'numeric',
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
- line += ` ${chalk.red('ā° Rate limited until ' + timeStr)}`;
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 current agent (unified IDE + LLM)
2683
- const currentAgent = autoConfig.agent || autoConfig.ide || 'ollama';
2684
- let agentDisplay = `Current Agent: ${chalk.cyan(getAgentDisplayName(currentAgent))}`;
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 (currentAgent === 'ollama' || currentAgent === 'groq' || currentAgent === 'anthropic' || currentAgent === 'bedrock' || currentAgent === 'claude-code') {
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 (currentAgent === 'groq') {
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 (currentAgent === 'anthropic') {
3458
+ } else if (displayAgent === 'anthropic') {
2709
3459
  model = config.auto?.anthropicModel || config.auto?.aiderModel || config.auto?.llmModel;
2710
- } else if (currentAgent === 'ollama') {
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 (currentAgent === 'bedrock') {
3464
+ } else if (displayAgent === 'bedrock') {
2715
3465
  model = 'anthropic.claude-sonnet-4-v1';
2716
- } else if (currentAgent === 'claude-code') {
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 = currentAgent === 'claude-code' ? 'claude-code-cli' : model;
2722
- const provider = currentAgent === 'ollama' ? 'ollama' : currentAgent;
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
- const resetTime = Date.now() + timeUntilReset;
2729
- const resetDate = new Date(resetTime);
2730
- const timeStr = resetDate.toLocaleString('en-US', {
2731
- weekday: 'short',
2732
- month: 'short',
2733
- day: 'numeric',
2734
- hour: 'numeric',
2735
- minute: '2-digit',
2736
- hour12: true,
2737
- timeZoneName: 'short'
2738
- });
2739
- agentDisplay += ` ${chalk.red('ā° Rate limited until ' + timeStr)}`;
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 restart CLI setting
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((counts.todoCount / total) * 100);
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(counts.todoCount + ' (' + todoPercent + '%) TODO')}, ${chalk.cyan(counts.toVerifyCount + ' (' + toVerifyPercent + '%) TO VERIFY')}, ${chalk.green(counts.verifiedCount + ' (' + verifiedPercent + '%) VERIFIED')}`;
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('Enabled āœ“') : chalk.yellow('Disabled ā—‹')}`,
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 getting next requirement:', err.message);
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: 'setting',
3681
+ type: 'info',
2913
3682
  name: chalk.red(' āš ļø No requirements to work on - cannot start Auto Mode'),
2914
- value: 'setting:no-requirements'
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 (leave empty for never stop):',
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:restart-cli': {
3340
- // Toggle restart CLI setting
3341
- try {
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 'setting:setup-alias': {
3355
- const { setupAlias } = require('../commands/setup');
3356
- await setupAlias();
3357
- console.log(chalk.gray('\nPress Enter to return to menu...'));
3358
- const inquirer = require('inquirer');
3359
- await inquirer.prompt([{
3360
- type: 'input',
3361
- name: 'continue',
3362
- message: ''
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
- case 'setting:requirements': {
3368
- // Show tree-style requirements navigator
3369
- await showRequirementsTree();
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
- case 'repo:init':
3374
- await repo.initRepo();
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 (leave empty for never stop):',
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;