vibecodingmachine-cli 2026.2.20-438 → 2026.2.26-1739

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.
Files changed (101) hide show
  1. package/bin/auth/auth-compliance.js +126 -0
  2. package/bin/cli-program.js +104 -0
  3. package/bin/cli-setup.js +52 -0
  4. package/bin/commands/agent-commands.js +310 -0
  5. package/bin/commands/auto-commands.js +70 -0
  6. package/bin/commands/command-aliases.js +118 -0
  7. package/bin/commands/repo-commands.js +39 -0
  8. package/bin/commands/rui-commands.js +152 -0
  9. package/bin/config/cli-config.js +394 -0
  10. package/bin/init/environment-setup.js +84 -0
  11. package/bin/update/update-checker.js +126 -0
  12. package/bin/vibecodingmachine-new.js +50 -0
  13. package/bin/vibecodingmachine.js +29 -663
  14. package/package.json +8 -2
  15. package/src/commands/agents/add.js +277 -0
  16. package/src/commands/agents/check.js +380 -0
  17. package/src/commands/agents/list.js +471 -0
  18. package/src/commands/agents/remove.js +351 -0
  19. package/src/commands/analyze-file-sizes.js +428 -0
  20. package/src/commands/auto-direct/code-processor.js +282 -0
  21. package/src/commands/auto-direct/file-scanner.js +266 -0
  22. package/src/commands/auto-direct/provider-config.js +178 -0
  23. package/src/commands/auto-direct/provider-manager.js +219 -0
  24. package/src/commands/auto-direct/requirement-manager.js +172 -0
  25. package/src/commands/auto-direct/status-display.js +91 -0
  26. package/src/commands/auto-direct/utils.js +106 -0
  27. package/src/commands/auto-direct.js +875 -488
  28. package/src/commands/auto-execution.js +342 -0
  29. package/src/commands/auto-provider-management.js +102 -0
  30. package/src/commands/auto-requirement-management.js +161 -0
  31. package/src/commands/auto-status-helpers.js +141 -0
  32. package/src/commands/auto.js +105 -5155
  33. package/src/commands/check-compliance.js +536 -0
  34. package/src/commands/continuous-scan.js +119 -0
  35. package/src/commands/ide.js +16 -4
  36. package/src/commands/refactor-file.js +486 -0
  37. package/src/commands/requirements.js +301 -2
  38. package/src/commands/timeout.js +290 -0
  39. package/src/trui/TruiInterface.js +108 -0
  40. package/src/trui/agents/AgentInterface.js +580 -0
  41. package/src/utils/antigravity-installer.js +60 -6
  42. package/src/utils/clarification-actions.js +290 -0
  43. package/src/utils/config.js +123 -2
  44. package/src/utils/first-run.js +5 -5
  45. package/src/utils/ide-handlers.js +212 -0
  46. package/src/utils/interactive/clarification-actions.js +348 -0
  47. package/src/utils/interactive/core-ui.js +265 -0
  48. package/src/utils/interactive/file-backup.js +237 -0
  49. package/src/utils/interactive/file-import-export.js +305 -0
  50. package/src/utils/interactive/file-operations.js +49 -0
  51. package/src/utils/interactive/file-validation.js +276 -0
  52. package/src/utils/interactive/interactive-prompts.js +480 -0
  53. package/src/utils/interactive/requirement-actions.js +127 -0
  54. package/src/utils/interactive/requirement-crud.js +356 -0
  55. package/src/utils/interactive/requirements-navigation.js +286 -0
  56. package/src/utils/interactive.js +390 -3459
  57. package/src/utils/provider-checker/agent-checker.js +250 -0
  58. package/src/utils/provider-checker/agent-runner.js +450 -0
  59. package/src/utils/provider-checker/cli-installer.js +123 -0
  60. package/src/utils/provider-checker/cli-utils.js +15 -0
  61. package/src/utils/provider-checker/format-utils.js +32 -0
  62. package/src/utils/provider-checker/ide-manager.js +72 -0
  63. package/src/utils/provider-checker/ide-utils.js +71 -0
  64. package/src/utils/provider-checker/node-detector.js +56 -0
  65. package/src/utils/provider-checker/node-utils.js +61 -0
  66. package/src/utils/provider-checker/process-spawn.js +22 -0
  67. package/src/utils/provider-checker/process-utils.js +37 -0
  68. package/src/utils/provider-checker/provider-validator.js +160 -0
  69. package/src/utils/provider-checker/quota-checker.js +54 -0
  70. package/src/utils/provider-checker/quota-detector.js +44 -0
  71. package/src/utils/provider-checker/requirements-manager.js +94 -0
  72. package/src/utils/provider-checker/test-requirements.js +95 -0
  73. package/src/utils/provider-checker/time-formatter.js +18 -0
  74. package/src/utils/provider-checker-new.js +14 -0
  75. package/src/utils/provider-checker.js +12 -407
  76. package/src/utils/provider-checkers/ide-manager.js +128 -0
  77. package/src/utils/provider-checkers/node-executable-finder.js +51 -0
  78. package/src/utils/provider-checkers/provider-checker-core.js +172 -0
  79. package/src/utils/provider-checkers/provider-checker-main.js +107 -0
  80. package/src/utils/provider-manager.js +60 -4
  81. package/src/utils/provider-registry.js +26 -3
  82. package/src/utils/provider-utils.js +173 -0
  83. package/src/utils/quota-detectors.js +212 -0
  84. package/src/utils/requirement-action-handlers.js +288 -0
  85. package/src/utils/requirement-actions/clarification-actions.js +229 -0
  86. package/src/utils/requirement-actions/confirmation-prompts.js +93 -0
  87. package/src/utils/requirement-actions/file-operations.js +92 -0
  88. package/src/utils/requirement-actions/helpers.js +40 -0
  89. package/src/utils/requirement-actions/requirement-operations.js +335 -0
  90. package/src/utils/requirement-actions.js +46 -856
  91. package/src/utils/requirement-file-operations.js +259 -0
  92. package/src/utils/requirement-helpers.js +128 -0
  93. package/src/utils/requirement-management.js +279 -0
  94. package/src/utils/requirement-navigation.js +146 -0
  95. package/src/utils/requirement-organization.js +271 -0
  96. package/src/utils/simple-trui.js +75 -1
  97. package/src/utils/trui-navigation.js +28 -2
  98. package/src/utils/trui-req-tree.js +196 -11
  99. package/src/utils/trui-specifications.js +31 -1
  100. package/src/utils/interactive-backup.js +0 -5664
  101. package/src/utils/trui-provider-manager.js +0 -182
@@ -1,12 +1,68 @@
1
1
  const { TRUINavigation } = require('./trui-navigation');
2
2
  const { checkVibeCodingMachineExists, getHostname, requirementsExists, t, isComputerNameEnabled } = require('vibecodingmachine-core');
3
- const os = require('os');
4
- const path = require('path');
5
3
  const chalk = require('chalk');
6
4
  const { checkAutoModeStatus } = require('./auto-mode');
7
5
  const pkg = require('../../package.json');
8
- const boxen = require('boxen');
9
- const inquirer = require('inquirer');
6
+
7
+ // Import refactored modules
8
+ const {
9
+ translateStage,
10
+ normalizeProjectDirName,
11
+ bootstrapProjectIfInHomeDir,
12
+ formatIDEName,
13
+ getCurrentAIProvider,
14
+ getAgentDisplayName,
15
+ formatPath,
16
+ countRequirements,
17
+ getRepoPath,
18
+ getSyncStatus,
19
+ getCurrentProgress,
20
+ showGoodbyeMessage,
21
+ indexToLetter
22
+ } = require('./interactive/core-ui');
23
+
24
+ const {
25
+ showRequirementsTree,
26
+ getSectionTitle,
27
+ getRequirementList,
28
+ handleAddRequirement
29
+ } = require('./interactive/requirements-navigation');
30
+
31
+ const {
32
+ confirmAction,
33
+ confirmAndExit,
34
+ performRequirementAction
35
+ } = require('./interactive/requirement-actions');
36
+
37
+ const {
38
+ showWelcomeScreen,
39
+ showMainMenu,
40
+ showSettingsMenu,
41
+ showComputerNameSettings,
42
+ showStatistics,
43
+ showSyncStatus,
44
+ searchRequirements,
45
+ showConfirmDialog,
46
+ showInputDialog,
47
+ showSelectDialog,
48
+ showMultiSelectDialog,
49
+ showError,
50
+ showSuccess,
51
+ showWarning,
52
+ showInfo
53
+ } = require('./interactive/interactive-prompts');
54
+
55
+ const {
56
+ showRequirementsBySection,
57
+ showRequirementsFromChangelog,
58
+ createBackup,
59
+ restoreFromBackup,
60
+ listBackups,
61
+ validateRequirementsFile,
62
+ getRequirementsStats,
63
+ exportRequirements,
64
+ importRequirements
65
+ } = require('./interactive/file-operations');
10
66
 
11
67
  /**
12
68
  * Start TRUI interface
@@ -20,3522 +76,397 @@ async function startInteractive() {
20
76
  * Expose showProviderManagerMenu for external callers (Electron, tests)
21
77
  */
22
78
  async function showProviderManagerMenu() {
23
- const { showProviderManagerMenu: show } = require('./trui-provider-manager');
79
+ const { showProviderManagerMenu: show } = require('./provider-manager');
24
80
  return show();
25
81
  }
26
82
 
27
-
28
83
  /**
29
- * Translate workflow stage names
84
+ * Main interactive loop
30
85
  */
31
- function translateStage(stage) {
32
- const stageMap = {
33
- 'PREPARE': 'workflow.stage.prepare',
34
- 'REPRODUCE': 'workflow.stage.reproduce',
35
- 'CREATE UNIT TEST': 'workflow.stage.create.unit.test',
36
- 'ACT': 'workflow.stage.act',
37
- 'CLEAN UP': 'workflow.stage.clean.up',
38
- 'VERIFY': 'workflow.stage.verify',
39
- 'RUN UNIT TESTS': 'workflow.stage.run.unit.tests',
40
- 'DONE': 'workflow.stage.done'
41
- };
42
-
43
- const key = stageMap[stage];
44
- return key ? t(key) : stage;
45
- }
46
-
47
- function normalizeProjectDirName(input) {
48
- const s = String(input || '').trim().toLowerCase();
49
- const replaced = s.replace(/\s+/g, '-');
50
- const cleaned = replaced.replace(/[^a-z0-9._-]/g, '-');
51
- const collapsed = cleaned.replace(/-+/g, '-').replace(/^[-._]+|[-._]+$/g, '');
52
- return collapsed || 'my-project';
53
- }
54
-
55
- async function bootstrapProjectIfInHomeDir() {
56
- let cwdResolved = path.resolve(process.cwd());
57
- let homeResolved = path.resolve(os.homedir());
58
-
59
- try {
60
- cwdResolved = await fs.realpath(cwdResolved);
61
- } catch (err) {
62
- // Ignore
63
- }
64
-
86
+ async function main() {
65
87
  try {
66
- homeResolved = await fs.realpath(homeResolved);
67
- } catch (err) {
68
- // Ignore
69
- }
70
-
71
- if (cwdResolved !== homeResolved) {
72
- return;
73
- }
74
-
75
- const codeDir = path.join(homeResolved, 'code');
76
- const codeDirExists = await fs.pathExists(codeDir);
77
-
78
- if (!codeDirExists) {
79
- const { shouldCreateCodeDir } = await inquirer.prompt([
80
- {
81
- type: 'confirm',
82
- name: 'shouldCreateCodeDir',
83
- message: `No code directory found at ${codeDir}. Would you like to create it?`,
84
- default: true
88
+ // Bootstrap project if in home directory
89
+ await bootstrapProjectIfInHomeDir();
90
+
91
+ // Show welcome screen
92
+ await showWelcomeScreen();
93
+
94
+ // Main menu loop
95
+ while (true) {
96
+ const choice = await showMainMenu();
97
+
98
+ switch (choice) {
99
+ case 'requirements':
100
+ await showRequirementsTree();
101
+ break;
102
+
103
+ case 'search':
104
+ await searchRequirements();
105
+ break;
106
+
107
+ case 'add':
108
+ await handleAddRequirement();
109
+ break;
110
+
111
+ case 'stats':
112
+ await showStatistics();
113
+ break;
114
+
115
+ case 'settings':
116
+ await handleSettings();
117
+ break;
118
+
119
+ case 'sync':
120
+ await showSyncStatus();
121
+ break;
122
+
123
+ case 'auto-mode':
124
+ await handleAutoMode();
125
+ break;
126
+
127
+ case 'exit':
128
+ showGoodbyeMessage();
129
+ return;
85
130
  }
86
- ]);
87
-
88
- if (!shouldCreateCodeDir) {
89
- return;
90
- }
91
-
92
- await fs.ensureDir(codeDir);
93
- }
94
-
95
- const { projectName } = await inquirer.prompt([
96
- {
97
- type: 'input',
98
- name: 'projectName',
99
- message: 'What is the project name you want to create?',
100
- validate: (value) => {
101
- const trimmed = String(value || '').trim();
102
- return trimmed.length > 0 || 'Please enter a project name.';
131
+
132
+ // Pause before showing menu again
133
+ if (choice !== 'exit') {
134
+ console.log(chalk.gray('\nPress Enter to continue...'));
135
+ await new Promise(resolve => {
136
+ process.stdin.once('data', resolve);
137
+ });
138
+ console.clear();
103
139
  }
104
140
  }
105
- ]);
106
-
107
- const dirName = normalizeProjectDirName(projectName);
108
- const projectDir = path.join(codeDir, dirName);
109
- await fs.ensureDir(projectDir);
110
-
111
- process.chdir(projectDir);
112
- try {
113
- await writeConfig({ ...(await readConfig()), repoPath: projectDir });
114
- } catch (err) {
115
- // Best-effort: interactive mode will still use process.cwd()
141
+
142
+ } catch (error) {
143
+ showError('An error occurred in the interactive interface', error);
144
+ process.exit(1);
116
145
  }
117
146
  }
118
147
 
119
148
  /**
120
- * Format IDE name for display
121
- * @param {string} ide - Internal IDE identifier
122
- * @returns {string} Display name for IDE
149
+ * Handle settings menu
123
150
  */
124
- function formatIDEName(ide) {
125
- const ideNames = {
126
- 'claude-code': 'Claude Code CLI',
127
- 'aider': 'Aider CLI',
128
- 'continue': 'Continue CLI',
129
- 'cline': 'Cline CLI',
130
- 'cursor': 'Cursor',
131
- 'vscode': 'VS Code',
132
- 'windsurf': 'Windsurf',
133
- 'kiro': 'AWS Kiro'
134
- };
135
- return ideNames[ide] || ide;
151
+ async function handleSettings() {
152
+ while (true) {
153
+ const choice = await showSettingsMenu();
154
+
155
+ switch (choice) {
156
+ case 'computer-name':
157
+ await showComputerNameSettings();
158
+ break;
159
+
160
+ case 'language':
161
+ await handleLanguageSettings();
162
+ break;
163
+
164
+ case 'statistics':
165
+ await handleStatisticsSettings();
166
+ break;
167
+
168
+ case 'sync':
169
+ await handleSyncSettings();
170
+ break;
171
+
172
+ case 'back':
173
+ return;
174
+ }
175
+ }
136
176
  }
137
177
 
138
178
  /**
139
- * Get current AI provider name for IDEs that require it (like Cline and Continue)
140
- * @param {string} ide - Internal IDE identifier
141
- * @returns {string|null} Provider name or null if not applicable/configured
179
+ * Handle language settings
142
180
  */
181
+ async function handleLanguageSettings() {
182
+ console.log(chalk.bold('\n🌐 Language Settings\n'));
183
+ console.log(chalk.gray('Language settings are controlled by environment variables\n'));
184
+ console.log(chalk.gray('Set LANG environment variable to change language\n'));
185
+
186
+ const currentLang = process.env.LANG || 'en_US.UTF-8';
187
+ console.log(chalk.yellow(`Current language: ${currentLang}\n`));
188
+ }
189
+
143
190
  /**
144
- * Get unified agent name (IDE or LLM-based)
145
- * @param {string} agentType - Agent type (e.g., 'cursor', 'ollama', 'anthropic')
146
- * @returns {string} - Display name like "Cursor IDE Agent" or "Ollama (qwen2.5-coder:32b)"
191
+ * Handle statistics settings
147
192
  */
148
- function getAgentDisplayName(agentType) {
149
- // IDE-based agents
150
- if (agentType === 'cursor') return 'Cursor IDE Agent';
151
- if (agentType === 'windsurf') return 'Windsurf IDE Agent';
152
- if (agentType === 'antigravity') return 'Google Antigravity IDE Agent';
153
- if (agentType === 'kiro') return 'AWS Kiro AI IDE Agent';
154
- if (agentType === 'vscode') return 'VS Code IDE Agent';
155
-
156
- // Claude Code CLI
157
- if (agentType === 'claude-code') return 'Claude Code CLI';
158
-
159
- // Direct LLM agents
160
- if (agentType === 'ollama' || agentType === 'anthropic' || agentType === 'groq' || agentType === 'bedrock') {
161
- try {
162
- const fs = require('fs');
163
- const path = require('path');
164
- const os = require('os');
165
- const configPath = path.join(os.homedir(), '.config', 'vibecodingmachine', 'config.json');
166
- if (fs.existsSync(configPath)) {
167
- const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
168
- const model = config.auto?.llmModel || config.auto?.aiderModel || config.auto?.groqModel;
169
-
170
- if (agentType === 'ollama' && model && !model.includes('groq/')) {
171
- return `Ollama (${model})`;
172
- } else if (agentType === 'anthropic') {
173
- return 'Anthropic (Claude Sonnet 4)';
174
- } else if (agentType === 'groq') {
175
- // Extract model name from groq/model format
176
- const groqModel = model && model.includes('groq/') ? model.split('/')[1] : 'llama-3.3-70b-versatile';
177
- return `Groq (${groqModel})`;
178
- } else if (agentType === 'bedrock') {
179
- return 'AWS Bedrock (Claude)';
180
- }
181
- }
182
- } catch (error) {
183
- // Fallback to generic names
184
- }
185
-
186
- // Fallback names
187
- if (agentType === 'ollama') return 'Ollama (Local)';
188
- if (agentType === 'anthropic') return 'Anthropic (Claude)';
189
- if (agentType === 'groq') return 'Groq (llama-3.3-70b-versatile)';
190
- if (agentType === 'bedrock') return 'AWS Bedrock';
193
+ async function handleStatisticsSettings() {
194
+ console.log(chalk.bold('\n📊 Statistics Settings\n'));
195
+
196
+ const choices = [
197
+ { name: '📈 Show detailed statistics', value: 'detailed' },
198
+ { name: '📋 Export statistics', value: 'export' },
199
+ { name: '🔄 Refresh statistics cache', value: 'refresh' },
200
+ { name: '⬅️ Back', value: 'back' }
201
+ ];
202
+
203
+ const choice = await showSelectDialog('Statistics options:', choices);
204
+
205
+ switch (choice) {
206
+ case 'detailed':
207
+ await showDetailedStatistics();
208
+ break;
209
+ case 'export':
210
+ await exportStatistics();
211
+ break;
212
+ case 'refresh':
213
+ await refreshStatistics();
214
+ break;
191
215
  }
192
-
193
- // Legacy support for old IDE names
194
- return formatIDEName(agentType);
195
216
  }
196
217
 
197
- function getCurrentAIProvider(ide) {
198
- // Aider, Cline, and Continue require AI provider configuration
199
- if (ide !== 'aider' && ide !== 'cline' && ide !== 'continue') {
200
- return null;
201
- }
202
-
203
- // Aider uses Ollama by default (or Bedrock if configured)
204
- if (ide === 'aider') {
205
- try {
206
- const { getAutoConfig } = require('./config');
207
- // Note: getAutoConfig is async, but this function is sync
208
- // We'll use a sync read for now, or make this function async
209
- const fs = require('fs');
210
- const path = require('path');
211
- const os = require('os');
212
- const configPath = path.join(os.homedir(), '.config', 'vibecodingmachine', 'config.json');
213
- if (fs.existsSync(configPath)) {
214
- const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
215
- const aiderModel = config.auto?.aiderModel;
216
- if (aiderModel) {
217
- return `Ollama (${aiderModel})`;
218
- }
219
- }
220
- return 'Ollama';
221
- } catch (error) {
222
- return 'Ollama';
223
- }
224
- }
225
-
226
- // Handle Continue CLI - read from Continue config
227
- if (ide === 'continue') {
228
- try {
229
- const fs = require('fs');
230
- const path = require('path');
231
- const os = require('os');
232
- const yaml = require('js-yaml');
233
- const configPath = path.join(os.homedir(), '.continue', 'config.yaml');
234
-
235
- if (!fs.existsSync(configPath)) {
236
- return 'Not configured';
237
- }
238
-
239
- const config = yaml.load(fs.readFileSync(configPath, 'utf8'));
240
- const models = config.models || [];
241
-
242
- if (models.length === 0) {
243
- return 'Not configured';
244
- }
245
-
246
- const firstModel = models[0];
247
- if (firstModel.provider === 'ollama') {
248
- return `Ollama (${firstModel.model || 'model configured'})`;
249
- }
250
-
251
- return firstModel.provider || 'Configured';
252
- } catch (error) {
253
- return 'Not configured';
254
- }
218
+ /**
219
+ * Handle sync settings
220
+ */
221
+ async function handleSyncSettings() {
222
+ console.log(chalk.bold('\n🔄 Sync Settings\n'));
223
+
224
+ const choices = [
225
+ { name: '🔄 Force sync now', value: 'force-sync' },
226
+ { name: '⚙️ Sync configuration', value: 'config' },
227
+ { name: '📊 Sync history', value: 'history' },
228
+ { name: '⬅️ Back', value: 'back' }
229
+ ];
230
+
231
+ const choice = await showSelectDialog('Sync options:', choices);
232
+
233
+ switch (choice) {
234
+ case 'force-sync':
235
+ await forceSync();
236
+ break;
237
+ case 'config':
238
+ await showSyncConfig();
239
+ break;
240
+ case 'history':
241
+ await showSyncHistory();
242
+ break;
255
243
  }
244
+ }
256
245
 
246
+ /**
247
+ * Handle auto mode
248
+ */
249
+ async function handleAutoMode() {
257
250
  try {
258
- const { ClineCLIManager } = require('vibecodingmachine-core');
259
- const clineManager = new ClineCLIManager();
260
-
261
- if (!clineManager.isConfigured()) {
262
- return 'Not configured';
263
- }
264
-
265
- const fs = require('fs');
266
- const path = require('path');
267
- const os = require('os');
268
- const configPath = path.join(os.homedir(), '.cline_cli', 'cline_cli_settings.json');
269
-
270
- if (!fs.existsSync(configPath)) {
271
- return 'Not configured';
272
- }
273
-
274
- const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
275
- const apiProvider = config.globalState?.apiProvider;
276
- const openAiBaseUrl = config.globalState?.openAiBaseUrl;
277
-
278
- if (!apiProvider) {
279
- return 'Not configured';
280
- }
281
-
282
- // Map provider identifiers to display names
283
- if (apiProvider === 'anthropic') {
284
- return 'Anthropic Claude';
285
- } else if (apiProvider === 'openrouter') {
286
- return 'OpenRouter';
287
- } else if (apiProvider === 'openai-native') {
288
- if (openAiBaseUrl === 'http://localhost:11434/v1') {
289
- return 'Ollama';
290
- } else if (openAiBaseUrl?.includes('generativelanguage.googleapis.com')) {
291
- return 'Google Gemini';
251
+ const autoModeStatus = await checkAutoModeStatus();
252
+
253
+ if (autoModeStatus.running) {
254
+ const confirmed = await showConfirmDialog('Stop auto mode?');
255
+ if (confirmed) {
256
+ // TODO: Implement stop auto mode
257
+ showSuccess('Auto mode stopped');
258
+ }
259
+ } else {
260
+ const confirmed = await showConfirmDialog('Start auto mode?');
261
+ if (confirmed) {
262
+ // TODO: Implement start auto mode
263
+ showSuccess('Auto mode started');
292
264
  }
293
- return 'OpenAI Native';
294
265
  }
295
-
296
- return apiProvider;
297
266
  } catch (error) {
298
- return 'Unknown';
267
+ showError('Error handling auto mode', error);
299
268
  }
300
269
  }
301
270
 
302
- function formatPath(fullPath) {
303
- const homeDir = os.homedir();
304
- if (fullPath.startsWith(homeDir)) {
305
- return fullPath.replace(homeDir, '~');
306
- }
307
- return fullPath;
308
- }
309
-
310
- async function countRequirements() {
271
+ /**
272
+ * Show detailed statistics
273
+ */
274
+ async function showDetailedStatistics() {
275
+ console.log(chalk.bold('\n📈 Detailed Statistics\n'));
276
+
311
277
  try {
312
- const { getProjectRequirementStats } = require('vibecodingmachine-core');
313
- const repoPath = await getRepoPath();
314
- return await getProjectRequirementStats(repoPath);
278
+ const stats = await getRequirementsStats();
279
+ if (stats) {
280
+ console.log(chalk.yellow('File Statistics:'));
281
+ console.log(chalk.gray(` • Total lines: ${stats.totalLines}`));
282
+ console.log(chalk.gray(` • Last modified: ${stats.lastModified.toLocaleString()}\n`));
283
+
284
+ console.log(chalk.yellow('Requirement Distribution:'));
285
+ console.log(chalk.gray(` • Todo: ${stats.todoCount}`));
286
+ console.log(chalk.gray(` • Verified: ${stats.verifyCount}`));
287
+ console.log(chalk.gray(` • Needs clarification: ${stats.clarificationCount}`));
288
+ console.log(chalk.gray(` • Recycled: ${stats.recycledCount}`));
289
+ console.log(chalk.gray(` • Total: ${stats.totalRequirements}\n`));
290
+ }
315
291
  } catch (error) {
316
- return null;
292
+ showError('Error getting detailed statistics', error);
317
293
  }
318
294
  }
319
295
 
320
- async function getSyncStatus() {
296
+ /**
297
+ * Export statistics
298
+ */
299
+ async function exportStatistics() {
300
+ const formats = ['json', 'csv', 'txt'];
301
+ const format = await showSelectDialog('Export format:', formats);
302
+
321
303
  try {
322
- const SyncEngine = require('vibecodingmachine-core/src/sync/sync-engine');
323
- const syncEngine = new SyncEngine();
324
- await syncEngine.initialize();
325
- const status = syncEngine.getStatus();
326
- syncEngine.stop();
327
-
328
- if (!status.isOnline && status.queuedChanges > 0) {
329
- return `[Offline: ${status.queuedChanges} queued]`;
330
- } else if (!status.isOnline) {
331
- return '[Offline]';
332
- } else if (status.isSyncing) {
333
- return '[Syncing...]';
334
- } else if (status.lastSyncTime) {
335
- const timeSinceSync = Date.now() - status.lastSyncTime;
336
- const minutesAgo = Math.floor(timeSinceSync / (1000 * 60));
337
- if (minutesAgo < 1) {
338
- return '[Sync: ✓ just now]';
339
- } else if (minutesAgo < 60) {
340
- return `[Sync: ✓ ${minutesAgo}m ago]`;
341
- } else {
342
- const hoursAgo = Math.floor(minutesAgo / 60);
343
- return `[Sync: ✓ ${hoursAgo}h ago]`;
344
- }
345
- } else {
346
- return '[Never synced]';
347
- }
304
+ await exportRequirements(format);
348
305
  } catch (error) {
349
- return '[Sync unavailable]';
306
+ showError('Error exporting statistics', error);
350
307
  }
351
308
  }
352
309
 
353
- async function getCurrentProgress() {
310
+ /**
311
+ * Refresh statistics cache
312
+ */
313
+ async function refreshStatistics() {
354
314
  try {
355
- const { getRequirementsPath } = require('vibecodingmachine-core');
356
- const reqPath = await getRequirementsPath();
357
-
358
- if (!reqPath || !await fs.pathExists(reqPath)) {
359
- return null;
360
- }
361
-
362
- const content = await fs.readFile(reqPath, 'utf8');
363
- const lines = content.split('\n');
364
-
365
- let requirement = null;
366
- let inTodoSection = false;
367
-
368
- for (let i = 0; i < lines.length; i++) {
369
- const line = lines[i];
370
-
371
- // Find TODO section
372
- if (line.includes('## ⏳ Requirements not yet completed') || line.includes('## Requirements not yet completed')) {
373
- inTodoSection = true;
374
- continue;
375
- }
376
-
377
- // Exit TODO section when we hit another section
378
- if (inTodoSection && line.trim().startsWith('##')) {
379
- break;
380
- }
381
-
382
- // Find first TODO requirement
383
- if (inTodoSection) {
384
- const trimmed = line.trim();
385
- if (trimmed.startsWith('- ') || trimmed.startsWith('* ')) {
386
- requirement = trimmed.substring(2); // Remove "- " or "* " prefix
387
- break;
388
- }
389
- }
390
- }
391
-
392
- return requirement ? { status: 'PREPARE', requirement } : null;
315
+ // TODO: Implement cache refresh
316
+ showSuccess('Statistics cache refreshed');
393
317
  } catch (error) {
394
- return null;
318
+ showError('Error refreshing statistics', error);
395
319
  }
396
320
  }
397
321
 
398
- async function showWelcomeScreen() {
399
-
400
-
401
- const repoPath = process.cwd(); // Always use current working directory
402
- const autoStatus = await checkAutoModeStatus();
403
- const hostname = getHostname();
404
-
405
- // Get current IDE from config
406
- const { getAutoConfig } = require('./config');
407
- const autoConfig = await getAutoConfig();
408
-
409
- // Check for requirements file
410
- const hasRequirements = await requirementsExists();
411
-
412
- // Count requirements if file exists
413
- const counts = hasRequirements ? await countRequirements() : null;
414
-
415
- // Clear the screen using console.clear() for better cross-platform compatibility
416
- // This ensures proper screen refresh and prevents text overlap
417
- console.clear();
418
-
419
- // Get version from package.json
420
- const version = `v${pkg.version}`;
421
-
422
- // Display welcome banner with version
423
- console.log('\n' + boxen(
424
- chalk.bold.cyan('Vibe Coding Machine') + '\n' +
425
- chalk.gray(version) + '\n' +
426
- chalk.gray(t('banner.tagline')),
427
- {
428
- padding: 1,
429
- margin: 0,
430
- borderStyle: 'round',
431
- borderColor: 'cyan'
432
- }
433
- ));
434
-
435
- // Display repository and system info
436
- console.log();
437
- console.log(chalk.gray(t('system.repo').padEnd(25)), formatPath(repoPath));
438
-
439
- // Display git branch if in a git repo
322
+ /**
323
+ * Force sync
324
+ */
325
+ async function forceSync() {
440
326
  try {
441
- const { isGitRepo, getCurrentBranch, hasUncommittedChanges } = require('vibecodingmachine-core');
442
- if (isGitRepo(repoPath)) {
443
- const branch = getCurrentBranch(repoPath);
444
- if (branch) {
445
- const isDirty = hasUncommittedChanges(repoPath);
446
- const branchDisplay = isDirty ? `${chalk.cyan(branch)} ${chalk.yellow(t('system.git.status.dirty'))}` : chalk.cyan(branch);
447
- console.log(chalk.gray(t('system.git.branch').padEnd(25)), branchDisplay);
448
- }
449
- }
327
+ // TODO: Implement force sync
328
+ showSuccess('Sync completed');
450
329
  } catch (error) {
451
- // Ignore git display errors
452
- }
453
-
454
- console.log(chalk.gray(t('system.computer.name').padEnd(25)), chalk.cyan(hostname));
455
-
456
- // Display auto mode progress if running
457
- if (autoStatus.running) {
458
- console.log(chalk.gray('Chats: '), chalk.cyan(autoStatus.chatCount || 0));
459
-
460
- // Get current status and requirement from REQUIREMENTS file
461
- const progress = await getCurrentProgress();
462
- if (progress) {
463
- console.log();
464
- // Display progress in a purple/magenta box similar to UI
465
- const stageIcons = {
466
- 'PREPARE': '🔍',
467
- 'ACT': '⚡',
468
- 'CLEAN UP': '🧹',
469
- 'VERIFY': '✅',
470
- 'DONE': '🎉'
471
- };
472
- const icon = stageIcons[progress.status] || '⏳';
473
- const statusColor = progress.status === 'DONE' ? chalk.green : chalk.magenta;
474
-
475
- // Truncate requirement text first, THEN apply color to fix box alignment
476
- const requirementText = progress.requirement ? progress.requirement.substring(0, 60) + (progress.requirement.length > 60 ? '...' : '') : 'No requirement';
477
-
478
- console.log(boxen(
479
- statusColor.bold(`${icon} ${progress.status}`) + '\n' +
480
- chalk.gray(requirementText),
481
- {
482
- padding: { left: 1, right: 1, top: 0, bottom: 0 },
483
- margin: 0,
484
- borderStyle: 'round',
485
- borderColor: 'magenta',
486
- width: 70
487
- }
488
- ));
489
- }
490
-
491
- // Display recent audit log entries
492
- const { readAuditLog, getDateStr } = require('vibecodingmachine-core');
493
- const todayStr = getDateStr();
494
- const entries = readAuditLog(todayStr);
495
- if (entries && entries.length > 0) {
496
- console.log();
497
- console.log(chalk.gray.bold('Recent Activity:'));
498
- // Show last 5 entries
499
- const recentEntries = entries.slice(-5);
500
- recentEntries.forEach(entry => {
501
- const time = new Date(entry.timestamp).toLocaleTimeString('en-US', {
502
- hour: 'numeric',
503
- minute: '2-digit',
504
- second: '2-digit',
505
- hour12: true
506
- });
507
- const icon = entry.type === 'auto-mode-start' ? '▶️' :
508
- entry.type === 'auto-mode-stop' ? '⏹️' :
509
- entry.type === 'ide-message' ? '💬' : '•';
510
- console.log(chalk.gray(` ${time} ${icon}`), entry.message || '');
511
- });
512
- }
330
+ showError('Error forcing sync', error);
513
331
  }
514
-
515
- console.log();
516
332
  }
517
333
 
518
- // Helper to convert index to letter (0->a, 1->b, etc.)
519
- function indexToLetter(index) {
520
- return String.fromCharCode(97 + index); // 97 is 'a'
334
+ /**
335
+ * Show sync configuration
336
+ */
337
+ async function showSyncConfig() {
338
+ console.log(chalk.bold('\n⚙️ Sync Configuration\n'));
339
+ console.log(chalk.gray('Sync configuration is stored in .vibecodingmachine/config.json\n'));
340
+ // TODO: Show actual config
521
341
  }
522
342
 
523
- // Tree-style requirements navigator
524
- async function showRequirementsTree() {
525
- console.log(chalk.bold.cyan('\n📋 ' + t('requirements.navigator.title') + '\n'));
526
- console.log(chalk.gray(t('requirements.navigator.basic.instructions') + '\n'));
527
-
528
- const tree = {
529
- expanded: { root: true },
530
- selected: 0,
531
- items: []
532
- };
533
-
534
- // Build tree structure
535
- const buildTree = async () => {
536
- tree.items = [];
537
-
538
- // Root: Requirements
539
- tree.items.push({ level: 0, type: 'root', label: `📋 ${t('requirements.menu.label')}`, key: 'root' });
540
-
541
- if (tree.expanded.root) {
542
- tree.items.push({ level: 1, type: 'add', label: '➕ ' + t('requirements.add.new'), key: 'add-one' });
543
- tree.items.push({ level: 1, type: 'add', label: '➕ ' + t('requirements.add.multiple'), key: 'add-many' });
544
-
545
- // Use pre-calculated stats and labels from shared logic
546
- const stats = await countRequirements();
547
- const { todoCount, toVerifyCount, verifiedCount, total, todoLabel, toVerifyLabel, verifiedLabel } = stats || {
548
- todoCount: 0, toVerifyCount: 0, verifiedCount: 0, total: 0,
549
- todoLabel: '⏳ TODO (0 - 0%)', toVerifyLabel: '✅ TO VERIFY (0 - 0%)', verifiedLabel: '🎉 VERIFIED (0 - 0%)'
550
- };
551
-
552
- // Create localized labels
553
- const localizedTodoLabel = todoCount > 0 ?
554
- `⏳ ${t('requirements.section.todo')} (${todoCount} - ${Math.round((todoCount / total) * 100)}%)` :
555
- `⏳ ${t('requirements.section.todo')} (0 - 0%)`;
556
-
557
- const localizedToVerifyLabel = toVerifyCount > 0 ?
558
- `✅ ${t('requirements.section.to.verify')} (${toVerifyCount} - ${Math.round((toVerifyCount / total) * 100)}%)` :
559
- `✅ ${t('requirements.section.to.verify')} (0 - 0%)`;
560
-
561
- const localizedVerifiedLabel = verifiedCount > 0 ?
562
- `🎉 ${t('requirements.section.verified')} (${verifiedCount} - ${Math.round((verifiedCount / total) * 100)}%)` :
563
- `🎉 ${t('requirements.section.verified')} (0 - 0%)`;
564
-
565
- const verifiedReqs = tree.verifiedReqs || [];
566
- const verifyReqs = tree.verifyReqs || [];
567
- const clarificationReqs = tree.clarificationReqs || [];
568
- const todoReqs = tree.todoReqs || [];
569
- const recycledReqs = tree.recycledReqs || [];
570
-
571
- // Calculate percentages for clarification and recycled sections
572
- const clarificationPercent = total > 0 ? Math.round((clarificationReqs.length / total) * 100) : 0;
573
- const recycledPercent = total > 0 ? Math.round((recycledReqs.length / total) * 100) : 0;
574
-
575
- // VERIFIED section (first) - only show if has requirements
576
- if (verifiedReqs.length > 0 || verifiedCount > 0) {
577
- tree.items.push({ level: 1, type: 'section', label: localizedVerifiedLabel, key: 'verified' });
578
-
579
- if (tree.expanded.verified) {
580
- verifiedReqs.forEach((req, idx) => {
581
- tree.items.push({ level: 2, type: 'verified', label: req, key: `verified-${idx}` });
582
- });
583
- }
584
- }
585
-
586
- // TO VERIFY section (second) - only show if has requirements
587
- if (verifyReqs.length > 0 || toVerifyCount > 0) {
588
- tree.items.push({ level: 1, type: 'section', label: localizedToVerifyLabel, key: 'verify', section: '✅ Verified by AI screenshot' });
589
-
590
- if (tree.expanded.verify) {
591
- verifyReqs.forEach((req, idx) => {
592
- tree.items.push({ level: 2, type: 'requirement', label: req.title, key: `verify-${idx}`, req, sectionKey: 'verify' });
593
- });
594
- }
595
- }
596
-
597
- // NEEDING CLARIFICATION section (third) - only show if has requirements
598
- if (clarificationReqs.length > 0) {
599
- tree.items.push({ level: 1, type: 'section', label: `❓ NEEDING CLARIFICATION (${clarificationReqs.length} - ${clarificationPercent}%)`, key: 'clarification', section: '❓ Requirements needing manual feedback' });
600
-
601
- if (tree.expanded.clarification) {
602
- clarificationReqs.forEach((req, idx) => {
603
- tree.items.push({ level: 2, type: 'clarification', label: req.title, key: `clarification-${idx}`, req, sectionKey: 'clarification' });
604
- });
605
- }
606
- }
607
-
608
- // TODO section (fourth) - only show if has requirements
609
- if (todoReqs.length > 0 || todoCount > 0) {
610
- tree.items.push({ level: 1, type: 'section', label: localizedTodoLabel, key: 'todo', section: '⏳ Requirements not yet completed' });
611
-
612
- if (tree.expanded.todo) {
613
- todoReqs.forEach((req, idx) => {
614
- tree.items.push({ level: 2, type: 'requirement', label: req.title, key: `todo-${idx}`, req, sectionKey: 'todo' });
615
- });
616
- }
617
- }
618
-
619
- // RECYCLED section (last) - only show if has requirements
620
- if (recycledReqs.length > 0) {
621
- tree.items.push({ level: 1, type: 'section', label: `♻️ RECYCLED (${recycledReqs.length} - ${recycledPercent}%)`, key: 'recycled', section: '♻️ Recycled' });
622
-
623
- if (tree.expanded.recycled) {
624
- recycledReqs.forEach((req, idx) => {
625
- tree.items.push({ level: 2, type: 'recycled', label: req.title, key: `recycled-${idx}`, req, sectionKey: 'recycled' });
626
- });
627
- }
628
- }
629
- }
630
- };
631
-
632
- // Load requirements for a section
633
- const loadSection = async (sectionKey, sectionTitle) => {
634
- const { getRequirementsPath } = require('vibecodingmachine-core');
635
- const reqPath = await getRequirementsPath();
636
-
637
- if (!reqPath || !await fs.pathExists(reqPath)) {
638
- return [];
639
- }
640
-
641
- const content = await fs.readFile(reqPath, 'utf8');
642
- const lines = content.split('\n');
643
-
644
- let inSection = false;
645
- const requirements = [];
646
-
647
- // For TO VERIFY section, check multiple possible section titles
648
- const sectionTitles = sectionKey === 'verify'
649
- ? ['🔍 TO VERIFY BY HUMAN', 'TO VERIFY BY HUMAN', '🔍 TO VERIFY', 'TO VERIFY', '✅ Verified by AI screenshot', 'Verified by AI screenshot', 'Verified by AI screenshot. Needs Human to Verify and move to CHANGELOG']
650
- : [sectionTitle];
651
-
652
- // For TO VERIFY, we need to find the exact section header
653
- // The section header is: ## ✅ Verified by AI screenshot. Needs Human to Verify and move to CHANGELOG
654
- const toVerifySectionHeader = '## ✅ Verified by AI screenshot. Needs Human to Verify and move to CHANGELOG';
655
-
656
- for (let i = 0; i < lines.length; i++) {
657
- const line = lines[i];
658
- const trimmed = line.trim();
659
-
660
- // Check if this line matches any of the section titles
661
- // IMPORTANT: Only check section headers (lines starting with ##), not requirement text
662
- if (trimmed.startsWith('##') && !trimmed.startsWith('###')) {
663
- // Reset inSection if we hit a section header that's not our target section
664
- if (sectionKey === 'verify' && inSection) {
665
- // Check if this is still a TO VERIFY section header
666
- if (!isStillToVerify) {
667
- // This will be handled by the "leaving section" check below, but ensure we don't process it as entering
668
- }
669
- }
670
- if (sectionKey === 'verify') {
671
- // For TO VERIFY, check for the specific section header with exact matching
672
- // Must match the exact TO VERIFY section header, not just any line containing "TO VERIFY"
673
- const isToVerifyHeader = trimmed === '## 🔍 TO VERIFY BY HUMAN' ||
674
- trimmed.startsWith('## 🔍 TO VERIFY BY HUMAN') ||
675
- trimmed === '## 🔍 TO VERIFY' ||
676
- trimmed.startsWith('## 🔍 TO VERIFY') ||
677
- trimmed === '## TO VERIFY' ||
678
- trimmed.startsWith('## TO VERIFY') ||
679
- trimmed === '## ✅ TO VERIFY' ||
680
- trimmed.startsWith('## ✅ TO VERIFY') ||
681
- trimmed === toVerifySectionHeader ||
682
- (trimmed.startsWith(toVerifySectionHeader) && trimmed.includes('Needs Human to Verify'));
683
-
684
- if (isToVerifyHeader) {
685
- // Make sure it's not a VERIFIED section (without TO VERIFY)
686
- if (!trimmed.includes('## 📝 VERIFIED') && !trimmed.match(/^##\s+VERIFIED$/i) && !trimmed.includes('📝 VERIFIED')) {
687
- inSection = true;
688
- continue;
689
- }
690
- } else {
691
- // If we hit a different section header and we're looking for TO VERIFY, make sure we're not in section
692
- // This prevents incorrectly reading from TODO or other sections
693
- if (trimmed.includes('⏳ Requirements not yet completed') ||
694
- trimmed.includes('Requirements not yet completed') ||
695
- trimmed === '## 📝 VERIFIED' ||
696
- trimmed.startsWith('## 📝 VERIFIED')) {
697
- // We're in TODO or VERIFIED section, not TO VERIFY - reset
698
- inSection = false;
699
- }
700
- }
701
- } else if (sectionTitles.some(title => trimmed.includes(title))) {
702
- inSection = true;
703
- continue;
704
- }
705
- }
706
-
707
- // Check if we're leaving the section (new section header that doesn't match)
708
- if (inSection && trimmed.startsWith('##') && !trimmed.startsWith('###')) {
709
- // If this is a new section header and it's not one of our section titles, we've left the section
710
- if (sectionKey === 'verify') {
711
- // For TO VERIFY, only break if this is clearly a different section
712
- // Check for specific section headers that indicate we've left TO VERIFY
713
-
714
- if (isVerifiedSection || isTodoSection || isRecycledSection || isClarificationSection) {
715
- break; // Different section, we've left TO VERIFY
716
- }
717
- // Otherwise, continue - might be REJECTED or CHANGELOG which are not section boundaries for TO VERIFY
718
- } else {
719
- // For other sections, break if it's a new section header that doesn't match
720
- if (!sectionTitles.some(title => trimmed.includes(title))) {
721
- break;
722
- }
723
- }
724
- }
725
-
726
- // Read requirements in new format (### header)
727
- if (inSection && line.trim().startsWith('###')) {
728
- const title = line.trim().replace(/^###\s*/, '').trim();
729
-
730
- // Skip malformed requirements (title is just a package name, empty, or too short)
731
- // Common package names that shouldn't be requirement titles
732
- const packageNames = ['cli', 'core', 'electron-app', 'web', 'mobile', 'vscode-extension', 'sync-server'];
733
- if (!title || title.length === 0 || packageNames.includes(title.toLowerCase())) {
734
- continue; // Skip this malformed requirement
735
- }
736
-
737
- const details = [];
738
- let pkg = null;
739
-
740
- // Read package and description
741
- for (let j = i + 1; j < lines.length; j++) {
742
- const nextLine = lines[j].trim();
743
- // Stop if we hit another requirement or section
744
- if (nextLine.startsWith('###') || (nextLine.startsWith('##') && !nextLine.startsWith('###'))) {
745
- break;
746
- }
747
- // Check for PACKAGE line
748
- if (nextLine.startsWith('PACKAGE:')) {
749
- pkg = nextLine.replace(/^PACKAGE:\s*/, '').trim();
750
- } else if (nextLine && !nextLine.startsWith('PACKAGE:')) {
751
- // Description line
752
- details.push(nextLine);
753
- }
754
- }
755
-
756
- requirements.push({ title, details, pkg, lineIndex: i });
757
- }
758
- }
759
-
760
- // Remove duplicates based on title (keep first occurrence)
761
- const seenTitles = new Set();
762
- const uniqueRequirements = [];
763
- for (const req of requirements) {
764
- const normalizedTitle = req.title.replace(/^TRY AGAIN \(\d+(st|nd|rd|th) time\):\s*/i, '').trim();
765
- if (!seenTitles.has(normalizedTitle)) {
766
- seenTitles.add(normalizedTitle);
767
- uniqueRequirements.push(req);
768
- }
769
- }
770
-
771
- return uniqueRequirements;
772
- };
773
-
774
- // Load VERIFIED requirements from CHANGELOG
775
- const loadVerified = async () => {
776
- const { getVibeCodingMachineDir, checkVibeCodingMachineExists } = require('vibecodingmachine-core');
777
- const allnightStatus = await checkVibeCodingMachineExists();
778
- let changelogPath;
779
-
780
- if (allnightStatus.insideExists) {
781
- const allnightDir = await getVibeCodingMachineDir();
782
- changelogPath = path.join(path.dirname(allnightDir), 'CHANGELOG.md');
783
- } else if (allnightStatus.siblingExists) {
784
- changelogPath = path.join(process.cwd(), 'CHANGELOG.md');
785
- }
786
-
787
- if (!changelogPath || !await fs.pathExists(changelogPath)) {
788
- return [];
789
- }
790
-
791
- const content = await fs.readFile(changelogPath, 'utf8');
792
- const lines = content.split('\n');
793
- const requirements = [];
794
- let inVerifiedSection = false;
795
-
796
- for (const line of lines) {
797
- const trimmed = line.trim();
798
-
799
- // Check for Verified Requirements section
800
- if (trimmed.includes('## Verified Requirements')) {
801
- inVerifiedSection = true;
802
- continue;
803
- }
804
-
805
- // Exit section if we hit another ## header
806
- if (inVerifiedSection && trimmed.startsWith('##') && !trimmed.includes('Verified Requirements')) {
807
- break;
808
- }
809
-
810
- // Only collect items from within the Verified Requirements section
811
- if (inVerifiedSection && trimmed.startsWith('- ') && trimmed.length > 10) {
812
- requirements.push(trimmed.substring(2));
813
- }
814
- }
815
-
816
- return requirements;
817
- };
818
-
819
- // Load clarification requirements with questions
820
- const loadClarification = async () => {
821
- const { getRequirementsPath } = require('vibecodingmachine-core');
822
- const reqPath = await getRequirementsPath();
343
+ /**
344
+ * Show sync history
345
+ */
346
+ async function showSyncHistory() {
347
+ console.log(chalk.bold('\n📊 Sync History\n'));
348
+ console.log(chalk.gray('Sync history is stored in .vibecodingmachine/logs/sync.log\n'));
349
+ // TODO: Show actual history
350
+ }
823
351
 
824
- if (!reqPath || !await fs.pathExists(reqPath)) {
825
- return [];
352
+ /**
353
+ * Handle keyboard shortcuts
354
+ */
355
+ function setupKeyboardShortcuts() {
356
+ process.stdin.setRawMode(true);
357
+ process.stdin.resume();
358
+ process.stdin.setEncoding('utf8');
359
+
360
+ process.stdin.on('data', (key) => {
361
+ // Handle Ctrl+C
362
+ if (key === '\u0003') {
363
+ confirmAndExit();
364
+ }
365
+
366
+ // Handle 'q' for quit
367
+ if (key === 'q') {
368
+ confirmAndExit();
369
+ }
370
+
371
+ // Handle '?' for help
372
+ if (key === '?') {
373
+ showHelp();
826
374
  }
375
+ });
376
+ }
827
377
 
828
- const content = await fs.readFile(reqPath, 'utf8');
829
- const lines = content.split('\n');
830
-
831
- let inSection = false;
832
- const requirements = [];
833
-
834
- for (let i = 0; i < lines.length; i++) {
835
- const line = lines[i];
836
- const trimmed = line.trim();
837
-
838
- // Check if we're entering the clarification section
839
- if (trimmed.includes('❓ Requirements needing manual feedback')) {
840
- inSection = true;
841
- continue;
842
- }
843
-
844
- // Check if we're leaving the section (hit another ## section)
845
- if (inSection && trimmed.startsWith('##') && !trimmed.startsWith('###')) {
846
- break;
847
- }
848
-
849
- // Read requirements in new format (### header)
850
- if (inSection && trimmed.startsWith('###')) {
851
- const title = trimmed.replace(/^###\s*/, '').trim();
852
-
853
- // Skip empty titles
854
- if (!title || title.length === 0) {
855
- continue;
856
- }
857
-
858
- const details = [];
859
- const questions = [];
860
- let pkg = null;
861
- let findings = null;
862
- let currentQuestion = null;
863
-
864
- // Read package, description, and clarifying questions
865
- for (let j = i + 1; j < lines.length; j++) {
866
- const nextLine = lines[j].trim();
867
-
868
- // Stop if we hit another requirement or section
869
- if (nextLine.startsWith('###') || (nextLine.startsWith('##') && !nextLine.startsWith('###'))) {
870
- break;
871
- }
872
-
873
- // Check for PACKAGE line
874
- if (nextLine.startsWith('PACKAGE:')) {
875
- pkg = nextLine.replace(/^PACKAGE:\s*/, '').trim();
876
- }
877
- // Check for AI findings
878
- else if (nextLine.startsWith('**AI found in codebase:**')) {
879
- // Next line will be the findings
880
- continue;
881
- }
882
- else if (nextLine.startsWith('**What went wrong')) {
883
- // Description line
884
- details.push(nextLine);
885
- }
886
- else if (nextLine.startsWith('**Clarifying questions:**')) {
887
- // Start of questions section
888
- continue;
889
- }
890
- else if (nextLine.match(/^\d+\./)) {
891
- // Save previous question if exists
892
- if (currentQuestion) {
893
- questions.push(currentQuestion);
894
- }
895
- // This is a new question
896
- currentQuestion = { question: nextLine, response: null };
897
- }
898
- else if (currentQuestion && nextLine && !nextLine.startsWith('PACKAGE:') && !nextLine.startsWith('**')) {
899
- // This might be a response to the current question or description/findings
900
- if (!findings && !currentQuestion.response && questions.length === 0 && !nextLine.match(/^\d+\./)) {
901
- // This is findings content
902
- findings = nextLine;
903
- } else if (currentQuestion && !currentQuestion.response) {
904
- // This is a response to the current question
905
- currentQuestion.response = nextLine;
906
- } else {
907
- // Description line
908
- details.push(nextLine);
909
- }
910
- }
911
- else if (nextLine && !nextLine.startsWith('PACKAGE:') && !nextLine.startsWith('**')) {
912
- // Description line
913
- details.push(nextLine);
914
- }
915
- }
916
-
917
- // Save last question if exists
918
- if (currentQuestion) {
919
- questions.push(currentQuestion);
920
- }
921
-
922
- requirements.push({ title, details, pkg, questions, findings, lineIndex: i });
923
- }
924
- }
378
+ /**
379
+ * Show help
380
+ */
381
+ function showHelp() {
382
+ console.log(chalk.bold('\n❓ Help\n'));
383
+ console.log(chalk.yellow('Keyboard shortcuts:'));
384
+ console.log(chalk.gray(' • Ctrl+C or q: Quit'));
385
+ console.log(chalk.gray(' • ?: Show this help'));
386
+ console.log(chalk.gray(' • Enter: Select menu item'));
387
+ console.log(chalk.gray(' • Arrow keys: Navigate menus\n'));
388
+ }
925
389
 
926
- return requirements;
927
- };
390
+ /**
391
+ * Cleanup function
392
+ */
393
+ function cleanup() {
394
+ process.stdin.setRawMode(false);
395
+ process.stdin.pause();
396
+ }
928
397
 
929
- // Load all sections upfront to show counts immediately
930
- tree.todoReqs = await loadSection('todo', '⏳ Requirements not yet completed');
931
- tree.verifyReqs = await loadSection('verify', '✅ Verified by AI screenshot');
932
- tree.clarificationReqs = await loadClarification();
933
- tree.verifiedReqs = await loadVerified();
934
- tree.recycledReqs = await loadSection('recycled', '♻️ Recycled');
398
+ // Handle process termination
399
+ process.on('SIGINT', () => {
400
+ confirmAndExit();
401
+ });
935
402
 
936
- let inTree = true;
937
- await buildTree();
403
+ process.on('SIGTERM', () => {
404
+ showGoodbyeMessage();
405
+ process.exit(0);
406
+ });
938
407
 
939
- while (inTree) {
940
- console.clear();
941
- console.log(chalk.bold.cyan('\n📋 ' + t('requirements.navigator.title') + '\n'));
942
- console.log(chalk.gray(t('requirements.navigator.instructions') + '\n'));
408
+ // Export main functions
409
+ module.exports = {
410
+ startInteractive,
411
+ showProviderManagerMenu,
412
+ main,
413
+
414
+ // Re-export utility functions for backward compatibility
415
+ translateStage,
416
+ normalizeProjectDirName,
417
+ formatIDEName,
418
+ getCurrentAIProvider,
419
+ getAgentDisplayName,
420
+ formatPath,
421
+ countRequirements,
422
+ getRepoPath,
423
+ getSyncStatus,
424
+ getCurrentProgress,
425
+ showGoodbyeMessage,
426
+ indexToLetter,
427
+
428
+ // Navigation functions
429
+ showRequirementsTree,
430
+ getSectionTitle,
431
+ getRequirementList,
432
+
433
+ // Action functions
434
+ confirmAction,
435
+ confirmAndExit,
436
+ performRequirementAction,
437
+
438
+ // Prompt functions
439
+ showWelcomeScreen,
440
+ showMainMenu,
441
+ showSettingsMenu,
442
+ showStatistics,
443
+ showSyncStatus,
444
+ searchRequirements,
445
+ showConfirmDialog,
446
+ showInputDialog,
447
+ showSelectDialog,
448
+ showMultiSelectDialog,
449
+
450
+ // File operations
451
+ showRequirementsBySection,
452
+ showRequirementsFromChangelog,
453
+ createBackup,
454
+ restoreFromBackup,
455
+ listBackups,
456
+ validateRequirementsFile,
457
+ getRequirementsStats,
458
+ exportRequirements,
459
+ importRequirements
460
+ };
943
461
 
944
- // Safety check: ensure tree.selected is within bounds
945
- if (tree.items.length === 0) {
946
- console.log(chalk.yellow('No items to display.'));
947
- const inquirer = require('inquirer');
948
- await inquirer.prompt([{
949
- type: 'input',
950
- name: 'continue',
951
- message: `${t('interactive.press.any.key.return')}`
952
- }]);
953
- inTree = false;
954
- continue;
955
- }
956
-
957
- if (tree.selected >= tree.items.length) {
958
- tree.selected = tree.items.length - 1;
959
- }
960
-
961
- if (tree.selected < 0) {
962
- tree.selected = 0;
963
- }
964
-
965
- // Calculate window for scrolling (show max 20 items at a time)
966
- const maxVisible = 20;
967
- let startIdx = 0;
968
- let endIdx = tree.items.length;
969
-
970
- if (tree.items.length > maxVisible) {
971
- // Center the selected item in the window
972
- startIdx = Math.max(0, tree.selected - Math.floor(maxVisible / 2));
973
- endIdx = Math.min(tree.items.length, startIdx + maxVisible);
974
-
975
- // Adjust if we're near the end
976
- if (endIdx - startIdx < maxVisible) {
977
- startIdx = Math.max(0, endIdx - maxVisible);
978
- }
979
- }
980
-
981
- // Show indicator if there are items above
982
- if (startIdx > 0) {
983
- console.log(chalk.gray(` ↑ ${startIdx} more above...`));
984
- }
985
-
986
- // Display visible tree items
987
- for (let idx = startIdx; idx < endIdx; idx++) {
988
- const item = tree.items[idx];
989
- const indent = ' '.repeat(item.level);
990
- const arrow = tree.expanded[item.key] ? '▼' : (item.type === 'section' ? '▶' : ' ');
991
- const prefix = item.type === 'section' || item.type === 'root' ? arrow + ' ' : ' ';
992
- const selected = idx === tree.selected ? chalk.cyan('❯ ') : ' ';
993
-
994
- // Truncate long labels to fit terminal width (max 120 chars)
995
- const maxLabelWidth = 120;
996
- let label = item.label;
997
- if (label.length > maxLabelWidth) {
998
- label = label.substring(0, maxLabelWidth - 3) + '...';
999
- }
1000
-
1001
- console.log(selected + indent + prefix + (idx === tree.selected ? chalk.cyan(label) : chalk.gray(label)));
1002
- }
1003
-
1004
- // Show indicator if there are items below
1005
- if (endIdx < tree.items.length) {
1006
- console.log(chalk.gray(` ↓ ${tree.items.length - endIdx} more below...`));
1007
- }
1008
-
1009
- console.log();
1010
-
1011
- // Handle input
1012
- const key = await new Promise((resolve) => {
1013
- readline.emitKeypressEvents(process.stdin);
1014
- if (process.stdin.isTTY) {
1015
- process.stdin.setRawMode(true);
1016
- }
1017
-
1018
- const handler = (str, key) => {
1019
- process.stdin.removeListener('keypress', handler);
1020
- if (process.stdin.isTTY) {
1021
- process.stdin.setRawMode(false);
1022
- }
1023
- resolve(key);
1024
- };
1025
-
1026
- process.stdin.on('keypress', handler);
1027
- process.stdin.resume();
1028
- });
1029
-
1030
- if (!key) continue;
1031
-
1032
- // Handle key presses
1033
- if (key.ctrl && key.name === 'c') {
1034
- // Ctrl+C always exits immediately
1035
- process.exit(0);
1036
- } else if (key.name === 'x' || key.name === 'escape') {
1037
- // X or ESC key - exit CLI with confirmation
1038
- await confirmAndExit();
1039
- } else if (key.name === 'left') {
1040
- const current = tree.items[tree.selected];
1041
- if (!current) continue; // Safety check
1042
-
1043
- if (tree.expanded[current.key]) {
1044
- // Collapse expanded section
1045
- tree.expanded[current.key] = false;
1046
- await buildTree();
1047
- } else if (current.level > 0) {
1048
- // Go to parent
1049
- for (let i = tree.selected - 1; i >= 0; i--) {
1050
- if (tree.items[i].level < current.level) {
1051
- tree.selected = i;
1052
- break;
1053
- }
1054
- }
1055
- } else {
1056
- // At root level, go back to main menu
1057
- inTree = false;
1058
- }
1059
- } else if (key.name === 'k' || key.name === 'up') {
1060
- tree.selected = Math.max(0, tree.selected - 1);
1061
- await buildTree();
1062
- } else if (key.name === 'j' || key.name === 'down') {
1063
- tree.selected = Math.min(tree.items.length - 1, tree.selected + 1);
1064
- await buildTree();
1065
- } else if (key.name === 'right' || key.name === 'return' || key.name === 'space') {
1066
- const current = tree.items[tree.selected];
1067
- if (!current) continue; // Safety check
1068
- if (current.type === 'section') {
1069
- if (!tree.expanded[current.key]) {
1070
- tree.expanded[current.key] = true;
1071
- // Load requirements for this section
1072
- if (current.key === 'todo') {
1073
- tree.todoReqs = await loadSection(current.key, current.section);
1074
- } else if (current.key === 'verify') {
1075
- tree.verifyReqs = await loadSection(current.key, current.section);
1076
- } else if (current.key === 'verified') {
1077
- tree.verifiedReqs = await loadVerified();
1078
- } else if (current.key === 'recycled') {
1079
- tree.recycledReqs = await loadSection(current.key, current.section);
1080
- }
1081
- await buildTree();
1082
- } else {
1083
- tree.expanded[current.key] = false;
1084
- await buildTree();
1085
- }
1086
- } else if (current.type === 'requirement') {
1087
- // Show requirement actions
1088
- await showRequirementActions(current.req, current.sectionKey, tree);
1089
- await buildTree();
1090
- } else if (current.type === 'clarification') {
1091
- // Show clarification requirement with questions
1092
- await showClarificationActions(current.req, tree, loadClarification);
1093
- await buildTree();
1094
- } else if (current.type === 'verified') {
1095
- // Show verified item details (read-only)
1096
- console.clear();
1097
- console.log(chalk.bold.green(`\n${current.label}\n`));
1098
- console.log(chalk.gray('(From CHANGELOG.md - read only)'));
1099
- console.log(chalk.gray(`\n${t('interactive.press.any.key.back')}`));
1100
- await new Promise((resolve) => {
1101
- readline.emitKeypressEvents(process.stdin);
1102
- if (process.stdin.isTTY) {
1103
- process.stdin.setRawMode(true);
1104
- }
1105
- const handler = (str, key) => {
1106
- process.stdin.removeListener('keypress', handler);
1107
- if (process.stdin.isTTY) {
1108
- process.stdin.setRawMode(false);
1109
- }
1110
- resolve();
1111
- };
1112
- process.stdin.on('keypress', handler);
1113
- process.stdin.resume();
1114
- });
1115
- } else if (current.type === 'add') {
1116
- // Handle add requirement
1117
- await handleAddRequirement(current.key);
1118
- // Reload TODO section
1119
- tree.todoReqs = await loadSection('todo', '⏳ Requirements not yet completed');
1120
- await buildTree();
1121
- }
1122
- } else if (key.name === 'r') {
1123
- const current = tree.items[tree.selected];
1124
- if (!current) continue; // Safety check
1125
-
1126
- if (current.type === 'requirement') {
1127
- await deleteRequirement(current.req, current.sectionKey, tree);
1128
- // Reload the section that the requirement was deleted from
1129
- if (current.sectionKey === 'todo') {
1130
- tree.todoReqs = await loadSection('todo', '⏳ Requirements not yet completed');
1131
- } else if (current.sectionKey === 'verify') {
1132
- tree.verifyReqs = await loadSection('verify', '✅ Verified by AI screenshot');
1133
- }
1134
- await buildTree();
1135
- } else if (current.type === 'clarification') {
1136
- await deleteClarification(current.req, tree);
1137
- tree.clarificationReqs = await loadClarification();
1138
- await buildTree();
1139
- } else if (current.type === 'recycled') {
1140
- await permanentlyDeleteRequirement(current.req, current.sectionKey, tree);
1141
- tree.recycledReqs = await loadSection('recycled', '♻️ Recycled');
1142
- await buildTree();
1143
- }
1144
- } else if (key.name === 'j') {
1145
- const current = tree.items[tree.selected];
1146
- if (!current) continue; // Safety check
1147
-
1148
- if (current.type === 'requirement') {
1149
- await moveRequirementDown(current.req, current.sectionKey, tree);
1150
- await buildTree();
1151
- // Move selection down to follow the item
1152
- if (tree.selected < tree.items.length - 1) {
1153
- tree.selected++;
1154
- }
1155
- }
1156
- } else if (key.name === 'k') {
1157
- const current = tree.items[tree.selected];
1158
- if (!current) continue; // Safety check
1159
-
1160
- if (current.type === 'requirement') {
1161
- await moveRequirementUp(current.req, current.sectionKey, tree);
1162
- await buildTree();
1163
- // Move selection up to follow the item
1164
- if (tree.selected > 0) {
1165
- tree.selected--;
1166
- }
1167
- }
1168
- } else if (key.name === 'u') {
1169
- const current = tree.items[tree.selected];
1170
- if (!current) continue; // Safety check
1171
-
1172
- if (current.type === 'requirement') {
1173
- await promoteRequirement(current.req, current.sectionKey, tree, loadSection, loadVerified);
1174
- await buildTree();
1175
- }
1176
- } else if (key.name === 'd') {
1177
- const current = tree.items[tree.selected];
1178
- if (!current) continue; // Safety check
1179
-
1180
- if (current.type === 'clarification') {
1181
- // D on clarification item = Move to TODO
1182
- await moveClarificationToTodo(current.req, tree);
1183
- tree.clarificationReqs = await loadClarification();
1184
- tree.todoReqs = await loadSection('todo', '⏳ Requirements not yet completed');
1185
- await buildTree();
1186
- } else if (current.type === 'requirement' || current.type === 'verified') {
1187
- const sectionKey = current.type === 'verified' ? 'verified' : current.sectionKey;
1188
- const reqTitle = current.type === 'verified' ? current.label : current.req.title;
1189
- await demoteRequirement(reqTitle, sectionKey, tree, loadSection, loadVerified);
1190
- await buildTree();
1191
- }
1192
- }
1193
- }
1194
-
1195
- process.stdin.pause();
1196
- }
1197
-
1198
- // Helper to show goodbye message
1199
- function showGoodbyeMessage() {
1200
- const hour = new Date().getHours();
1201
- const message = hour < 21
1202
- ? '\n👋 ' + t('interactive.goodbye') + '\n'
1203
- : '\n👋 Goodbye! Go get some sleep!\n';
1204
- console.log(chalk.cyan(message));
1205
- }
1206
-
1207
- // Helper to get section title from section key
1208
- function getSectionTitle(sectionKey) {
1209
- if (sectionKey === 'todo') return '⏳ Requirements not yet completed';
1210
- if (sectionKey === 'verify') return '✅ Verified by AI screenshot';
1211
- if (sectionKey === 'recycled') return '♻️ Recycled';
1212
- return '';
1213
- }
1214
-
1215
- // Helper to get requirement list from tree by section key
1216
- function getRequirementList(tree, sectionKey) {
1217
- if (sectionKey === 'todo') return tree.todoReqs;
1218
- if (sectionKey === 'verify') return tree.verifyReqs;
1219
- if (sectionKey === 'clarification') return tree.clarificationReqs;
1220
- if (sectionKey === 'recycled') return tree.recycledReqs;
1221
- return [];
1222
- }
1223
-
1224
- // Helper to show confirmation prompt (r/y for yes, N for no, default N)
1225
- async function confirmAction(message) {
1226
- console.log();
1227
- process.stdout.write(chalk.yellow(`${message} `));
1228
-
1229
- const confirmed = await new Promise((resolve) => {
1230
- readline.emitKeypressEvents(process.stdin);
1231
- if (process.stdin.isTTY) {
1232
- process.stdin.setRawMode(true);
1233
- }
1234
-
1235
- const handler = (str, key) => {
1236
- process.stdin.removeListener('keypress', handler);
1237
- if (process.stdin.isTTY) {
1238
- process.stdin.setRawMode(false);
1239
- }
1240
-
1241
- if (key && (key.ctrl && key.name === 'c')) {
1242
- process.exit(0);
1243
- }
1244
-
1245
- const keyPressed = key ? key.name : str;
1246
-
1247
- // Handle Enter as default (No)
1248
- if (keyPressed === 'return') {
1249
- console.log('N');
1250
- resolve(false);
1251
- } else {
1252
- console.log(keyPressed || ''); // Echo the key
1253
- if (keyPressed === 'r' || keyPressed === 'y') {
1254
- resolve(true);
1255
- } else {
1256
- resolve(false);
1257
- }
1258
- }
1259
- };
1260
-
1261
- process.stdin.on('keypress', handler);
1262
- process.stdin.resume();
1263
- });
1264
-
1265
- return confirmed;
1266
- }
1267
-
1268
- // Helper to confirm exit and exit if confirmed (default N)
1269
- async function confirmAndExit() {
1270
- console.log(chalk.gray('\n[DEBUG] confirmAndExit called'));
1271
- console.log();
1272
- process.stdout.write(chalk.yellow(`${t('interactive.confirm.exit')} `));
1273
-
1274
- const confirmed = await new Promise((resolve) => {
1275
- readline.emitKeypressEvents(process.stdin);
1276
- if (process.stdin.isTTY) {
1277
- process.stdin.setRawMode(true);
1278
- }
1279
-
1280
- const handler = (str, key) => {
1281
- process.stdin.removeListener('keypress', handler);
1282
- if (process.stdin.isTTY) {
1283
- process.stdin.setRawMode(false);
1284
- }
1285
-
1286
- if (key && (key.ctrl && key.name === 'c')) {
1287
- process.exit(0);
1288
- }
1289
-
1290
- const keyPressed = key ? key.name : str;
1291
-
1292
- // Handle Enter as default (No)
1293
- if (keyPressed === 'return') {
1294
- console.log('N');
1295
- resolve(false);
1296
- } else {
1297
- console.log(keyPressed || ''); // Echo the key
1298
- if (keyPressed === 'x' || keyPressed === 'y') {
1299
- resolve(true);
1300
- } else {
1301
- resolve(false);
1302
- }
1303
- }
1304
- };
1305
-
1306
- process.stdin.on('keypress', handler);
1307
- process.stdin.resume();
1308
- });
1309
-
1310
- if (confirmed) {
1311
- showGoodbyeMessage();
1312
- process.exit(0);
1313
- }
1314
- }
1315
-
1316
- // Helper to edit clarification responses
1317
- async function editClarificationResponses(req, tree) {
1318
- const { getRequirementsPath } = require('vibecodingmachine-core');
1319
- const reqPath = await getRequirementsPath();
1320
-
1321
- console.clear();
1322
- console.log(chalk.bold.cyan('\n✍️ Edit Clarification Responses\n'));
1323
- console.log(chalk.white(`${req.title}\n`));
1324
-
1325
- const responses = [];
1326
-
1327
- for (let i = 0; i < req.questions.length; i++) {
1328
- const q = req.questions[i];
1329
- console.log(chalk.cyan(`\n${q.question}`));
1330
-
1331
- if (q.response) {
1332
- console.log(chalk.gray(`Current response: ${q.response}`));
1333
- }
1334
-
1335
- const answer = await inquirer.prompt([{
1336
- type: 'input',
1337
- name: 'response',
1338
- message: 'Your response (press Enter to skip):',
1339
- default: q.response || ''
1340
- }]);
1341
-
1342
- responses.push(answer.response);
1343
- }
1344
-
1345
- // Update the file with responses
1346
- const content = await fs.readFile(reqPath, 'utf8');
1347
- const lines = content.split('\n');
1348
-
1349
- let inClarificationSection = false;
1350
- let inCurrentReq = false;
1351
- let questionIndex = 0;
1352
- const newLines = [];
1353
-
1354
- for (let i = 0; i < lines.length; i++) {
1355
- const line = lines[i];
1356
-
1357
- if (line.includes('❓ Requirements needing manual feedback')) {
1358
- inClarificationSection = true;
1359
- newLines.push(line);
1360
- continue;
1361
- }
1362
-
1363
- if (inClarificationSection && line.startsWith('##') && !line.includes('❓ Requirements needing manual feedback')) {
1364
- inClarificationSection = false;
1365
- newLines.push(line);
1366
- continue;
1367
- }
1368
-
1369
- if (inClarificationSection && line.trim() === `- ${req.title}`) {
1370
- inCurrentReq = true;
1371
- questionIndex = 0;
1372
- newLines.push(line);
1373
- continue;
1374
- }
1375
-
1376
- if (inCurrentReq && (line.trim().startsWith('- ') || line.startsWith('##'))) {
1377
- inCurrentReq = false;
1378
- newLines.push(line);
1379
- continue;
1380
- }
1381
-
1382
- if (inCurrentReq && line.trim().match(/^\d+\./)) {
1383
- // This is a question line
1384
- newLines.push(line);
1385
- // Add or update response on next line
1386
- if (responses[questionIndex]) {
1387
- newLines.push(` Response: ${responses[questionIndex]}`);
1388
- }
1389
- questionIndex++;
1390
- // Skip existing response line if any
1391
- if (i + 1 < lines.length && lines[i + 1].trim().startsWith('Response:')) {
1392
- i++; // Skip the old response line
1393
- }
1394
- continue;
1395
- }
1396
-
1397
- if (!inCurrentReq || !line.trim().startsWith('Response:')) {
1398
- newLines.push(line);
1399
- }
1400
- }
1401
-
1402
- await fs.writeFile(reqPath, newLines.join('\n'));
1403
- console.log(chalk.green('\n✓ Responses saved!'));
1404
- await new Promise(resolve => setTimeout(resolve, 1000));
1405
- }
1406
-
1407
- // Helper to move clarification requirement back to TODO
1408
- async function moveClarificationToTodo(req, tree) {
1409
- const { getRequirementsPath } = require('vibecodingmachine-core');
1410
- const reqPath = await getRequirementsPath();
1411
-
1412
- const content = await fs.readFile(reqPath, 'utf8');
1413
- const lines = content.split('\n');
1414
-
1415
- let inClarificationSection = false;
1416
- let inCurrentReq = false;
1417
- let reqLines = [];
1418
- let clarificationSectionStart = -1;
1419
- let todoSectionStart = -1;
1420
- const newLines = [];
1421
-
1422
- // First pass: find sections and extract requirement
1423
- for (let i = 0; i < lines.length; i++) {
1424
- const line = lines[i];
1425
-
1426
- if (line.includes('❓ Requirements needing manual feedback')) {
1427
- inClarificationSection = true;
1428
- clarificationSectionStart = i;
1429
- continue;
1430
- }
1431
-
1432
- if (line.includes('⏳ Requirements not yet completed')) {
1433
- todoSectionStart = i;
1434
- inClarificationSection = false;
1435
- }
1436
-
1437
- if (inClarificationSection && line.trim() === `- ${req.title}`) {
1438
- inCurrentReq = true;
1439
- reqLines.push(line);
1440
- continue;
1441
- }
1442
-
1443
- if (inCurrentReq && (line.trim().startsWith('- ') || line.startsWith('##'))) {
1444
- inCurrentReq = false;
1445
- }
1446
-
1447
- if (inCurrentReq) {
1448
- reqLines.push(line);
1449
- }
1450
- }
1451
-
1452
- // Second pass: rebuild file
1453
- inClarificationSection = false;
1454
- inCurrentReq = false;
1455
-
1456
- for (let i = 0; i < lines.length; i++) {
1457
- const line = lines[i];
1458
-
1459
- if (line.includes('❓ Requirements needing manual feedback')) {
1460
- inClarificationSection = true;
1461
- newLines.push(line);
1462
- continue;
1463
- }
1464
-
1465
- if (line.includes('⏳ Requirements not yet completed')) {
1466
- inClarificationSection = false;
1467
- newLines.push(line);
1468
- // Add the requirement to TODO section (at the top)
1469
- newLines.push(`- ${req.title}`);
1470
- continue;
1471
- }
1472
-
1473
- if (inClarificationSection && line.trim() === `- ${req.title}`) {
1474
- inCurrentReq = true;
1475
- continue; // Skip this line
1476
- }
1477
-
1478
- if (inCurrentReq && (line.trim().startsWith('- ') || line.startsWith('##'))) {
1479
- inCurrentReq = false;
1480
- }
1481
-
1482
- if (!inCurrentReq) {
1483
- newLines.push(line);
1484
- }
1485
- }
1486
-
1487
- await fs.writeFile(reqPath, newLines.join('\n'));
1488
- console.log(chalk.green('\n✓ Moved to TODO!'));
1489
- await new Promise(resolve => setTimeout(resolve, 1000));
1490
- }
1491
-
1492
- // Helper to move clarification requirement to recycled (used to delete)
1493
- async function deleteClarification(req, tree) {
1494
- const { getRequirementsPath } = require('vibecodingmachine-core');
1495
- const reqPath = await getRequirementsPath();
1496
-
1497
- const truncatedTitle = req.title.length > 50 ? req.title.substring(0, 50) + '...' : req.title;
1498
-
1499
- if (await confirmAction(`Remove? (r/y/N)`)) {
1500
- // Move to recycled instead of deleting
1501
- await moveRequirementToRecycled(reqPath, req.title, '❓ Requirements needing manual feedback');
1502
-
1503
- const content = await fs.readFile(reqPath, 'utf8');
1504
- const lines = content.split('\n');
1505
-
1506
- let inClarificationSection = false;
1507
- let inCurrentReq = false;
1508
- const newLines = [];
1509
-
1510
- for (let i = 0; i < lines.length; i++) {
1511
- const line = lines[i];
1512
-
1513
- if (line.includes('❓ Requirements needing manual feedback')) {
1514
- inClarificationSection = true;
1515
- newLines.push(line);
1516
- continue;
1517
- }
1518
-
1519
- if (inClarificationSection && line.startsWith('##') && !line.includes('❓ Requirements needing manual feedback')) {
1520
- inClarificationSection = false;
1521
- }
1522
-
1523
- if (inClarificationSection && line.trim() === `- ${req.title}`) {
1524
- inCurrentReq = true;
1525
- continue; // Skip this line
1526
- }
1527
-
1528
- if (inCurrentReq && (line.trim().startsWith('- ') || line.startsWith('##'))) {
1529
- inCurrentReq = false;
1530
- }
1531
-
1532
- if (!inCurrentReq) {
1533
- newLines.push(line);
1534
- }
1535
- }
1536
-
1537
- await fs.writeFile(reqPath, newLines.join('\n'));
1538
- }
1539
- }
1540
-
1541
- // Helper to show actions for a requirement
1542
- async function showClarificationActions(req, tree, loadClarification) {
1543
- const actions = [
1544
- { label: '✍️ Add/Edit Responses', value: 'edit-responses' },
1545
- { label: '↩️ Move back to TODO (after clarification)', value: 'move-to-todo' },
1546
- { label: '🗑️ Delete', value: 'delete' }
1547
- ];
1548
-
1549
- let selected = 0;
1550
-
1551
- while (true) {
1552
- // Redraw entire screen on each selection change
1553
- console.clear();
1554
- console.log(chalk.bold.yellow(`\n❓ Requirement Needing Clarification\n`));
1555
- console.log(chalk.white(`${req.title}\n`));
1556
-
1557
- // Display AI findings if available
1558
- if (req.findings) {
1559
- console.log(chalk.bold.green('AI Found in Codebase:\n'));
1560
- console.log(chalk.gray(`${req.findings}\n`));
1561
- }
1562
-
1563
- // Display questions
1564
- console.log(chalk.bold.cyan('Clarifying Questions:\n'));
1565
- req.questions.forEach((q, idx) => {
1566
- console.log(chalk.cyan(`${idx + 1}. ${q.question}`));
1567
- if (q.response) {
1568
- console.log(chalk.green(` Response: ${q.response}`));
1569
- } else {
1570
- console.log(chalk.gray(` (No response yet)`));
1571
- }
1572
- console.log();
1573
- });
1574
-
1575
- // Display menu
1576
- console.log(chalk.gray('\nWhat would you like to do? (↑/↓/Enter to select, ESC/← to go back)\n'));
1577
- actions.forEach((action, idx) => {
1578
- if (idx === selected) {
1579
- console.log(chalk.cyan(`❯ ${action.label}`));
1580
- } else {
1581
- console.log(` ${action.label}`);
1582
- }
1583
- });
1584
-
1585
- // Handle input
1586
- const key = await new Promise((resolve) => {
1587
- readline.emitKeypressEvents(process.stdin);
1588
- if (process.stdin.isTTY) {
1589
- process.stdin.setRawMode(true);
1590
- }
1591
-
1592
- const handler = (str, key) => {
1593
- process.stdin.removeListener('keypress', handler);
1594
- if (process.stdin.isTTY) {
1595
- process.stdin.setRawMode(false);
1596
- }
1597
- resolve(key);
1598
- };
1599
-
1600
- process.stdin.once('keypress', handler);
1601
- });
1602
-
1603
- if (key.name === 'up') {
1604
- selected = Math.max(0, selected - 1);
1605
- } else if (key.name === 'down') {
1606
- selected = Math.min(actions.length - 1, selected + 1);
1607
- } else if (key.name === 'return' || key.name === 'space') {
1608
- const action = actions[selected].value;
1609
-
1610
- if (action === 'edit-responses') {
1611
- await editClarificationResponses(req, tree);
1612
- tree.clarificationReqs = await loadClarification();
1613
- return;
1614
- } else if (action === 'move-to-todo') {
1615
- await moveClarificationToTodo(req, tree);
1616
- tree.clarificationReqs = await loadClarification();
1617
- return;
1618
- } else if (action === 'delete') {
1619
- await deleteClarification(req, tree);
1620
- tree.clarificationReqs = await loadClarification();
1621
- return;
1622
- }
1623
- } else if (key.name === 'escape' || key.name === 'left') {
1624
- return;
1625
- }
1626
- }
1627
- }
1628
- async function showRequirementActions(req, sectionKey, tree) {
1629
- const actions = [
1630
- { label: '✏️ Rename/Edit', value: 'rename' },
1631
- { label: '👍 Thumbs up (promote to Verified)', value: 'thumbs-up' },
1632
- { label: '👎 Thumbs down (demote to TODO)', value: 'thumbs-down' },
1633
- { label: '⬆️ Move up', value: 'move-up' },
1634
- { label: '⬇️ Move down', value: 'move-down' },
1635
- { label: '🗑️ Delete', value: 'delete' }
1636
- ];
1637
-
1638
- let selected = 0;
1639
- let isFirstRender = true;
1640
- let lastMenuLines = 0;
1641
-
1642
- const displayMenu = () => {
1643
- // Clear previous menu (but not on first render)
1644
- if (!isFirstRender && lastMenuLines > 0) {
1645
- // Only move up by menu lines (header stays on screen)
1646
- readline.moveCursor(process.stdout, 0, -lastMenuLines);
1647
- readline.clearScreenDown(process.stdout);
1648
- }
1649
-
1650
- // Display requirement title and details (only on first render)
1651
- if (isFirstRender) {
1652
- console.log(chalk.bold.yellow(`\n${req.title}\n`));
1653
- if (req.details.length > 0) {
1654
- console.log(chalk.gray(req.details.join('\n')));
1655
- console.log();
1656
- }
1657
- }
1658
-
1659
- // Track menu lines printed this render (this is what we clear and reprint)
1660
- let menuLines = 0;
1661
-
1662
- // Display menu (always reprinted)
1663
- console.log();
1664
- console.log(chalk.gray('What would you like to do? (↑/↓/Enter to select, ESC/← to go back)'));
1665
- console.log();
1666
- menuLines += 3; // Blank line + help text + blank line
1667
- actions.forEach((action, idx) => {
1668
- if (idx === selected) {
1669
- console.log(chalk.cyan(`❯ ${action.label}`));
1670
- } else {
1671
- console.log(` ${action.label}`);
1672
- }
1673
- menuLines++;
1674
- });
1675
-
1676
- lastMenuLines = menuLines;
1677
- isFirstRender = false;
1678
- };
1679
-
1680
- while (true) {
1681
- displayMenu();
1682
-
1683
- // Handle input
1684
- const key = await new Promise((resolve) => {
1685
- readline.emitKeypressEvents(process.stdin);
1686
- if (process.stdin.isTTY) {
1687
- process.stdin.setRawMode(true);
1688
- }
1689
-
1690
- const handler = (str, key) => {
1691
- process.stdin.removeListener('keypress', handler);
1692
- if (process.stdin.isTTY) {
1693
- process.stdin.setRawMode(false);
1694
- }
1695
- resolve(key);
1696
- };
1697
-
1698
- process.stdin.on('keypress', handler);
1699
- process.stdin.resume();
1700
- });
1701
-
1702
- if (!key) continue;
1703
-
1704
- if (key.ctrl && key.name === 'c') {
1705
- process.exit(0);
1706
- } else if (key.name === 'escape' || key.name === 'left') {
1707
- return; // Go back
1708
- } else if (key.name === 'up') {
1709
- selected = Math.max(0, selected - 1);
1710
- } else if (key.name === 'down') {
1711
- selected = Math.min(actions.length - 1, selected + 1);
1712
- } else if (key.name === 'return' || key.name === 'right') {
1713
- await performRequirementAction(actions[selected].value, req, sectionKey, tree);
1714
- return;
1715
- }
1716
- }
1717
- }
1718
-
1719
- // Helper to perform action on requirement
1720
- async function performRequirementAction(action, req, sectionKey, tree) {
1721
- const reqList = getRequirementList(tree, sectionKey);
1722
- const sectionTitle = getSectionTitle(sectionKey);
1723
- const reqIndex = reqList.findIndex(r => r.title === req.title);
1724
-
1725
-
1726
- if (reqIndex === -1) return;
1727
-
1728
- const { getRequirementsPath } = require('vibecodingmachine-core');
1729
- const reqPath = await getRequirementsPath();
1730
-
1731
- switch (action) {
1732
- case 'thumbs-up':
1733
- const thumbsUpReq = reqList.splice(reqIndex, 1)[0];
1734
- reqList.unshift(thumbsUpReq);
1735
- await saveRequirementsOrder(reqPath, sectionTitle, reqList);
1736
- console.log(chalk.green('\n✓ Moved to top\n'));
1737
- break;
1738
- case 'thumbs-down':
1739
- const thumbsDownReq = reqList.splice(reqIndex, 1)[0];
1740
- reqList.push(thumbsDownReq);
1741
- await saveRequirementsOrder(reqPath, sectionTitle, reqList);
1742
- console.log(chalk.yellow('\n✓ Moved to bottom\n'));
1743
- break;
1744
- case 'move-up':
1745
- if (reqIndex > 0) {
1746
- [reqList[reqIndex], reqList[reqIndex - 1]] = [reqList[reqIndex - 1], reqList[reqIndex]];
1747
- await saveRequirementsOrder(reqPath, sectionTitle, reqList);
1748
- console.log(chalk.green('\n✓ Moved up\n'));
1749
- }
1750
- break;
1751
- case 'move-down':
1752
- if (reqIndex < reqList.length - 1) {
1753
- [reqList[reqIndex], reqList[reqIndex + 1]] = [reqList[reqIndex + 1], reqList[reqIndex]];
1754
- await saveRequirementsOrder(reqPath, sectionTitle, reqList);
1755
- console.log(chalk.green('\n✓ Moved down\n'));
1756
- }
1757
- break;
1758
- case 'delete':
1759
- if (await confirmAction('Are you sure you want to delete this requirement?')) {
1760
- reqList.splice(reqIndex, 1);
1761
- await saveRequirementsOrder(reqPath, sectionTitle, reqList);
1762
- console.log(chalk.green('\n✓ Deleted\n'));
1763
- }
1764
- break;
1765
- case 'rename':
1766
- await renameRequirement(req, sectionKey, tree);
1767
- break;
1768
- }
1769
-
1770
- await new Promise(resolve => setTimeout(resolve, 1000));
1771
- }
1772
-
1773
- // Helper to rename requirement (title and description)
1774
- async function renameRequirement(req, sectionKey, tree) {
1775
- const { getRequirementsPath } = require('vibecodingmachine-core');
1776
- const reqPath = await getRequirementsPath();
1777
-
1778
- console.log(chalk.cyan('\n✏️ Rename/Edit Requirement\n'));
1779
- console.log(chalk.gray('Current title:'), chalk.white(req.title));
1780
- if (req.details.length > 0) {
1781
- console.log(chalk.gray('Current description:'));
1782
- req.details.forEach(line => console.log(chalk.white(' ' + line)));
1783
- }
1784
- console.log();
1785
-
1786
- const answers = await inquirer.prompt([
1787
- {
1788
- type: 'input',
1789
- name: 'title',
1790
- message: 'New title (leave blank to keep current):',
1791
- default: ''
1792
- }
1793
- ]);
1794
-
1795
- const newTitle = answers.title.trim() || req.title;
1796
-
1797
- // Ask for description using multi-line input
1798
- console.log(chalk.gray('\nEnter new description (leave blank to keep current).'));
1799
- console.log(chalk.gray('Press Enter twice on empty line to finish:\n'));
1800
-
1801
- const descriptionLines = [];
1802
- let emptyLineCount = 0;
1803
- let isFirstLine = true;
1804
- let newDescription = '';
1805
- let keptCurrent = false;
1806
-
1807
- while (true) {
1808
- try {
1809
- const { line } = await inquirer.prompt([{
1810
- type: 'input',
1811
- name: 'line',
1812
- message: isFirstLine ? 'Description:' : ''
1813
- }]);
1814
-
1815
- if (isFirstLine && line.trim() === '') {
1816
- // If first line is empty, keep current description
1817
- keptCurrent = true;
1818
- break;
1819
- }
1820
-
1821
- isFirstLine = false;
1822
-
1823
- if (line.trim() === '') {
1824
- emptyLineCount++;
1825
- if (emptyLineCount >= 2) break;
1826
- } else {
1827
- emptyLineCount = 0;
1828
- descriptionLines.push(line);
1829
- }
1830
- } catch (err) {
1831
- break;
1832
- }
1833
- }
1834
-
1835
- if (!keptCurrent) {
1836
- newDescription = descriptionLines.join('\n');
1837
- }
1838
-
1839
- // Read the requirements file
1840
- const content = await fs.readFile(reqPath, 'utf8');
1841
- const lines = content.split('\n');
1842
-
1843
- // Find the requirement block
1844
- let requirementStartIndex = -1;
1845
- let requirementEndIndex = -1;
1846
-
1847
- for (let i = 0; i < lines.length; i++) {
1848
- const line = lines[i].trim();
1849
- if (line.startsWith('###')) {
1850
- const title = line.replace(/^###\s*/, '').trim();
1851
- if (title === req.title) {
1852
- requirementStartIndex = i;
1853
- // Find the end of this requirement (next ### or ## header)
1854
- for (let j = i + 1; j < lines.length; j++) {
1855
- const nextLine = lines[j].trim();
1856
- if (nextLine.startsWith('###') || (nextLine.startsWith('##') && !nextLine.startsWith('###'))) {
1857
- requirementEndIndex = j;
1858
- break;
1859
- }
1860
- }
1861
- if (requirementEndIndex === -1) {
1862
- requirementEndIndex = lines.length;
1863
- }
1864
- break;
1865
- }
1866
- }
1867
- }
1868
-
1869
- if (requirementStartIndex === -1) {
1870
- console.log(chalk.yellow('⚠️ Could not find requirement to rename'));
1871
- return;
1872
- }
1873
-
1874
- // Build new requirement block
1875
- const newBlock = [`### ${newTitle}`];
1876
-
1877
- // If new description provided, use it; otherwise keep existing details
1878
- if (newDescription) {
1879
- newDescription.split('\n').forEach(line => {
1880
- if (line.trim()) {
1881
- newBlock.push(line);
1882
- }
1883
- });
1884
- } else {
1885
- // Keep existing details (skip the ### header line)
1886
- for (let i = requirementStartIndex + 1; i < requirementEndIndex; i++) {
1887
- const line = lines[i];
1888
- // Skip empty lines at the start and PACKAGE lines if we want to preserve them
1889
- if (line.trim()) {
1890
- newBlock.push(line);
1891
- }
1892
- }
1893
- }
1894
-
1895
- newBlock.push(''); // Blank line after requirement
1896
-
1897
- // Replace the old block with the new one
1898
- lines.splice(requirementStartIndex, requirementEndIndex - requirementStartIndex, ...newBlock);
1899
-
1900
- // Save
1901
- await fs.writeFile(reqPath, lines.join('\n'));
1902
- console.log(chalk.green('\n✓ Requirement renamed/updated\n'));
1903
-
1904
- // Update the tree data
1905
- const reqList = getRequirementList(tree, sectionKey);
1906
- const reqIndex = reqList.findIndex(r => r.title === req.title);
1907
- if (reqIndex !== -1) {
1908
- reqList[reqIndex].title = newTitle;
1909
- if (newDescription) {
1910
- reqList[reqIndex].details = newDescription.split('\n').filter(line => line.trim());
1911
- }
1912
- }
1913
- }
1914
-
1915
- // Helper to move requirement to recycled section (used to delete)
1916
- async function deleteRequirement(req, sectionKey, tree) {
1917
- const reqList = getRequirementList(tree, sectionKey);
1918
- const sectionTitle = getSectionTitle(sectionKey);
1919
- const reqIndex = reqList.findIndex(r => r.title === req.title);
1920
-
1921
- if (reqIndex === -1) return;
1922
-
1923
- const { getRequirementsPath } = require('vibecodingmachine-core');
1924
- const reqPath = await getRequirementsPath();
1925
-
1926
- const truncatedTitle = req.title.substring(0, 50) + (req.title.length > 50 ? '...' : '');
1927
- if (await confirmAction(`Remove? (r/y/N)`)) {
1928
- // Move to recycled instead of deleting
1929
- await moveRequirementToRecycled(reqPath, req.title, sectionTitle);
1930
- reqList.splice(reqIndex, 1);
1931
- }
1932
- }
1933
-
1934
- // Helper to permanently delete requirement from file (used for recycled items)
1935
- async function permanentlyDeleteRequirement(req, sectionKey, tree) {
1936
- const reqList = getRequirementList(tree, sectionKey);
1937
- const reqIndex = reqList.findIndex(r => r.title === req.title);
1938
-
1939
- if (reqIndex === -1) return;
1940
-
1941
- const { getRequirementsPath } = require('vibecodingmachine-core');
1942
- const reqPath = await getRequirementsPath();
1943
-
1944
- const truncatedTitle = req.title.substring(0, 50) + (req.title.length > 50 ? '...' : '');
1945
- if (await confirmAction(`Permanently delete? (r/y/N)`)) {
1946
- const content = await fs.readFile(reqPath, 'utf8');
1947
- const lines = content.split('\n');
1948
-
1949
- // Find the requirement block (### header format)
1950
- let requirementStartIndex = -1;
1951
- let requirementEndIndex = -1;
1952
-
1953
- for (let i = 0; i < lines.length; i++) {
1954
- const line = lines[i].trim();
1955
- if (line.startsWith('###')) {
1956
- const title = line.replace(/^###\s*/, '').trim();
1957
- if (title && title === req.title) {
1958
- requirementStartIndex = i;
1959
- // Find the end of this requirement (next ### or ## header)
1960
- for (let j = i + 1; j < lines.length; j++) {
1961
- const nextLine = lines[j].trim();
1962
- if (nextLine.startsWith('###') || (nextLine.startsWith('##') && !nextLine.startsWith('###'))) {
1963
- requirementEndIndex = j;
1964
- break;
1965
- }
1966
- }
1967
- if (requirementEndIndex === -1) {
1968
- requirementEndIndex = lines.length;
1969
- }
1970
- break;
1971
- }
1972
- }
1973
- }
1974
-
1975
- if (requirementStartIndex === -1) {
1976
- console.log(chalk.yellow('⚠️ Could not find requirement to delete'));
1977
- return;
1978
- }
1979
-
1980
- // Remove the requirement from the file completely
1981
- lines.splice(requirementStartIndex, requirementEndIndex - requirementStartIndex);
1982
-
1983
- // Save
1984
- await fs.writeFile(reqPath, lines.join('\n'));
1985
- reqList.splice(reqIndex, 1);
1986
- }
1987
- }
1988
-
1989
- // Helper to move requirement to recycled section
1990
- async function moveRequirementToRecycled(reqPath, requirementTitle, fromSection) {
1991
- const content = await fs.readFile(reqPath, 'utf8');
1992
- const lines = content.split('\n');
1993
-
1994
- // Find the requirement block (### header format)
1995
- let requirementStartIndex = -1;
1996
- let requirementEndIndex = -1;
1997
-
1998
- for (let i = 0; i < lines.length; i++) {
1999
- const line = lines[i].trim();
2000
- if (line.startsWith('###')) {
2001
- const title = line.replace(/^###\s*/, '').trim();
2002
- if (title && title === requirementTitle) {
2003
- requirementStartIndex = i;
2004
- // Find the end of this requirement (next ### or ## header)
2005
- for (let j = i + 1; j < lines.length; j++) {
2006
- const nextLine = lines[j].trim();
2007
- if (nextLine.startsWith('###') || (nextLine.startsWith('##') && !nextLine.startsWith('###'))) {
2008
- requirementEndIndex = j;
2009
- break;
2010
- }
2011
- }
2012
- if (requirementEndIndex === -1) {
2013
- requirementEndIndex = lines.length;
2014
- }
2015
- break;
2016
- }
2017
- }
2018
- }
2019
-
2020
- if (requirementStartIndex === -1) {
2021
- console.log(chalk.yellow('⚠️ Could not find requirement to recycle'));
2022
- return;
2023
- }
2024
-
2025
- // Extract the requirement block
2026
- const requirementBlock = lines.slice(requirementStartIndex, requirementEndIndex);
2027
-
2028
- // Remove the requirement from its current location
2029
- // Also remove any trailing blank lines that might be left behind
2030
- lines.splice(requirementStartIndex, requirementEndIndex - requirementStartIndex);
2031
-
2032
- // Clean up: remove any orphaned blank lines or malformed entries left after removal
2033
- // Check if there's a blank line followed by a malformed requirement (just "cli", "core", etc.)
2034
- if (requirementStartIndex < lines.length) {
2035
- const nextLine = lines[requirementStartIndex]?.trim();
2036
- const packageNames = ['cli', 'core', 'electron-app', 'web', 'mobile', 'vscode-extension', 'sync-server'];
2037
- // If the next line is just a package name (likely orphaned), remove it
2038
- if (nextLine && packageNames.includes(nextLine.toLowerCase()) &&
2039
- !nextLine.startsWith('###') && !nextLine.startsWith('PACKAGE:')) {
2040
- lines.splice(requirementStartIndex, 1);
2041
- }
2042
- // Remove any blank lines left after removal
2043
- while (requirementStartIndex < lines.length && lines[requirementStartIndex]?.trim() === '') {
2044
- lines.splice(requirementStartIndex, 1);
2045
- }
2046
- }
2047
-
2048
- // Find or create Recycled section
2049
- let recycledIndex = -1;
2050
- for (let i = 0; i < lines.length; i++) {
2051
- if (lines[i].includes('♻️ Recycled') || lines[i].includes('🗑️ Recycled')) {
2052
- recycledIndex = i;
2053
- break;
2054
- }
2055
- }
2056
-
2057
- // If Recycled section doesn't exist, create it before the last section
2058
- if (recycledIndex === -1) {
2059
- // Find the last section header
2060
- let lastSectionIndex = -1;
2061
- for (let i = lines.length - 1; i >= 0; i--) {
2062
- if (lines[i].startsWith('##') && !lines[i].startsWith('###')) {
2063
- lastSectionIndex = i;
2064
- break;
2065
- }
2066
- }
2067
-
2068
- // Insert new Recycled section
2069
- const insertIndex = lastSectionIndex > 0 ? lastSectionIndex : lines.length;
2070
- lines.splice(insertIndex, 0, '', '## ♻️ Recycled', '');
2071
- recycledIndex = insertIndex + 1;
2072
- }
2073
-
2074
- // Insert requirement block at TOP of Recycled list
2075
- let insertIndex = recycledIndex + 1;
2076
- while (insertIndex < lines.length && lines[insertIndex].trim() === '') {
2077
- insertIndex++;
2078
- }
2079
- lines.splice(insertIndex, 0, ...requirementBlock);
2080
- // Add blank line after if needed
2081
- if (insertIndex + requirementBlock.length < lines.length && lines[insertIndex + requirementBlock.length].trim() !== '') {
2082
- lines.splice(insertIndex + requirementBlock.length, 0, '');
2083
- }
2084
-
2085
- // Save
2086
- await fs.writeFile(reqPath, lines.join('\n'));
2087
- }
2088
-
2089
- // Helper to move requirement down with 'j' key
2090
- async function moveRequirementDown(req, sectionKey, tree) {
2091
- const reqList = getRequirementList(tree, sectionKey);
2092
- const sectionTitle = getSectionTitle(sectionKey);
2093
- const reqIndex = reqList.findIndex(r => r.title === req.title);
2094
-
2095
- if (reqIndex === -1 || reqIndex >= reqList.length - 1) return;
2096
-
2097
- const { getRequirementsPath } = require('vibecodingmachine-core');
2098
- const reqPath = await getRequirementsPath();
2099
-
2100
- [reqList[reqIndex], reqList[reqIndex + 1]] = [reqList[reqIndex + 1], reqList[reqIndex]];
2101
- await saveRequirementsOrder(reqPath, sectionTitle, reqList);
2102
- }
2103
-
2104
- // Helper to move requirement up with 'k' key
2105
- async function moveRequirementUp(req, sectionKey, tree) {
2106
- const reqList = getRequirementList(tree, sectionKey);
2107
- const sectionTitle = getSectionTitle(sectionKey);
2108
- const reqIndex = reqList.findIndex(r => r.title === req.title);
2109
-
2110
- if (reqIndex === -1 || reqIndex === 0) return;
2111
-
2112
- const { getRequirementsPath } = require('vibecodingmachine-core');
2113
- const reqPath = await getRequirementsPath();
2114
-
2115
- [reqList[reqIndex], reqList[reqIndex - 1]] = [reqList[reqIndex - 1], reqList[reqIndex]];
2116
- await saveRequirementsOrder(reqPath, sectionTitle, reqList);
2117
- }
2118
-
2119
- // Helper to promote requirement to next list (TODO -> TO VERIFY -> VERIFIED)
2120
- async function promoteRequirement(req, sectionKey, tree, loadSection, loadVerified) {
2121
- const { getRequirementsPath, promoteTodoToVerify, promoteToVerified } = require('vibecodingmachine-core');
2122
- const reqPath = await getRequirementsPath();
2123
-
2124
- if (sectionKey === 'todo') {
2125
- // TODO -> TO VERIFY: Use shared function
2126
- const success = await promoteTodoToVerify(reqPath, req.title);
2127
- if (success) {
2128
- // Reload sections
2129
- tree.todoReqs = await loadSection('todo', '⏳ Requirements not yet completed');
2130
- tree.verifyReqs = await loadSection('verify', '✅ Verified by AI screenshot');
2131
- }
2132
- } else if (sectionKey === 'verify') {
2133
- // TO VERIFY -> VERIFIED: Use shared function
2134
- const success = await promoteToVerified(reqPath, req.title);
2135
- if (success) {
2136
- // Reload sections
2137
- tree.verifyReqs = await loadSection('verify', '✅ Verified by AI screenshot');
2138
- tree.verifiedReqs = await loadVerified();
2139
- }
2140
- }
2141
- }
2142
-
2143
- // Helper to demote requirement to previous list (VERIFIED -> TODO, TO VERIFY -> TODO)
2144
- async function demoteRequirement(reqTitle, sectionKey, tree, loadSection, loadVerified) {
2145
- const { getRequirementsPath, demoteVerifyToTodo, demoteFromVerifiedToTodo } = require('vibecodingmachine-core');
2146
- const reqPath = await getRequirementsPath();
2147
-
2148
- if (sectionKey === 'verify') {
2149
- // Prompt for explanation of what went wrong
2150
- // Prompt for explanation of what went wrong
2151
- console.log(chalk.gray('\nWhat went wrong? (This will be added to help the AI agent)'));
2152
- console.log(chalk.gray('Press Enter twice on empty line to finish:\n'));
2153
-
2154
- const explanationLines = [];
2155
- let emptyLineCount = 0;
2156
- let isFirstLine = true;
2157
-
2158
- while (true) {
2159
- try {
2160
- const { line } = await inquirer.prompt([{
2161
- type: 'input',
2162
- name: 'line',
2163
- message: isFirstLine ? 'Explanation:' : ''
2164
- }]);
2165
-
2166
- isFirstLine = false;
2167
-
2168
- if (line.trim() === '') {
2169
- emptyLineCount++;
2170
- if (emptyLineCount >= 2) break;
2171
- } else {
2172
- emptyLineCount = 0;
2173
- explanationLines.push(line);
2174
- }
2175
- } catch (err) {
2176
- break;
2177
- }
2178
- }
2179
-
2180
- const explanation = explanationLines.join('\n');
2181
-
2182
- // TO VERIFY -> TODO: Use shared function with explanation
2183
- const success = await demoteVerifyToTodo(reqPath, reqTitle, explanation);
2184
- if (success) {
2185
- // Reload sections
2186
- tree.todoReqs = await loadSection('todo', '⏳ Requirements not yet completed');
2187
- tree.verifyReqs = await loadSection('verify', '✅ Verified by AI screenshot');
2188
- }
2189
- } else if (sectionKey === 'verified') {
2190
- // VERIFIED -> TODO: Use shared function (with TRY AGAIN prefix)
2191
- const success = await demoteFromVerifiedToTodo(reqPath, reqTitle);
2192
- if (success) {
2193
- // Reload sections
2194
- tree.todoReqs = await loadSection('todo', '⏳ Requirements not yet completed');
2195
- tree.verifiedReqs = await loadVerified();
2196
- }
2197
- }
2198
- }
2199
-
2200
- // Helper to handle adding requirements
2201
- async function handleAddRequirement(type) {
2202
- const reqCommands = require('../commands/requirements');
2203
- const packages = ['all', 'cli', 'core', 'electron-app', 'web', 'mobile'];
2204
-
2205
- if (type === 'add-one') {
2206
- try {
2207
- // Get saved package from config
2208
- const config = await readConfig();
2209
- let selectedPackage = config.lastPackage || ['all'];
2210
- // Ensure it's an array
2211
- if (typeof selectedPackage === 'string') selectedPackage = [selectedPackage];
2212
-
2213
- while (true) {
2214
- const pkgDisplay = selectedPackage.join(', ');
2215
-
2216
- // Custom menu to allow "Up arrow" to select package
2217
- // Default to "Enter Requirement Name" (index 1) so user can just type
2218
- const { action } = await inquirer.prompt([{
2219
- type: 'list',
2220
- name: 'action',
2221
- message: 'New Requirement:',
2222
- choices: [
2223
- { name: `📦 Package: ${pkgDisplay}`, value: 'package' },
2224
- { name: '📝 Enter Requirement Name', value: 'name' },
2225
- { name: '❌ Cancel', value: 'cancel' }
2226
- ],
2227
- default: 1
2228
- }]);
2229
-
2230
- if (action === 'cancel') return;
2231
-
2232
- if (action === 'package') {
2233
- // When showing checkboxes, if "all" is currently selected along with specific packages,
2234
- // unselect "all" first so user sees only specific packages checked
2235
- let displayPackages = selectedPackage;
2236
- if (selectedPackage.includes('all') && selectedPackage.length > 1) {
2237
- displayPackages = selectedPackage.filter(p => p !== 'all');
2238
- }
2239
-
2240
- const { pkg } = await inquirer.prompt([{
2241
- type: 'checkbox',
2242
- name: 'pkg',
2243
- message: 'Select package(s):',
2244
- choices: packages.map(p => ({
2245
- name: p,
2246
- value: p,
2247
- checked: displayPackages.includes(p)
2248
- })),
2249
- validate: (answer) => {
2250
- if (answer.length < 1) return 'You must choose at least one package.';
2251
- return true;
2252
- }
2253
- }]);
2254
-
2255
- // When selecting specific packages, unselect "all"
2256
- let finalPackage = pkg;
2257
- // If both "all" and specific packages are selected, keep only specific packages
2258
- if (pkg.includes('all') && pkg.length > 1) {
2259
- finalPackage = pkg.filter(p => p !== 'all');
2260
- }
2261
-
2262
- selectedPackage = finalPackage;
2263
- config.lastPackage = selectedPackage;
2264
- await writeConfig(config);
2265
- continue;
2266
- }
2267
-
2268
- if (action === 'name') {
2269
- const { name } = await inquirer.prompt([{
2270
- type: 'input',
2271
- name: 'name',
2272
- message: `Enter requirement name (Package: ${pkgDisplay}):`
2273
- }]);
2274
-
2275
- if (!name || !name.trim()) continue;
2276
-
2277
- // Ask for description
2278
- console.log(chalk.gray('\nEnter description (press Enter twice on empty line to finish):\n'));
2279
- const descriptionLines = [];
2280
- let emptyLineCount = 0;
2281
- let isFirstLine = true;
2282
-
2283
- while (true) {
2284
- try {
2285
- const { line } = await inquirer.prompt([{
2286
- type: 'input',
2287
- name: 'line',
2288
- message: isFirstLine ? 'Description:' : ''
2289
- }]);
2290
-
2291
- isFirstLine = false;
2292
-
2293
- if (line.trim() === '') {
2294
- emptyLineCount++;
2295
- if (emptyLineCount >= 2) break;
2296
- } else {
2297
- emptyLineCount = 0;
2298
- descriptionLines.push(line);
2299
- }
2300
- } catch (err) {
2301
- break;
2302
- }
2303
- }
2304
-
2305
- const description = descriptionLines.join('\n');
2306
- await reqCommands.add(name, selectedPackage, description);
2307
- await new Promise(resolve => setTimeout(resolve, 1000));
2308
- return;
2309
- }
2310
- }
2311
- } catch (err) {
2312
- // ESC pressed
2313
- }
2314
- } else if (type === 'add-many') {
2315
- try {
2316
- console.log(chalk.cyan('\nAdding multiple requirements:\n'));
2317
- const requirements = [];
2318
- let done = false;
2319
-
2320
- while (!done) {
2321
- try {
2322
- // Ask for package
2323
- const { package: pkg } = await inquirer.prompt([{
2324
- type: 'list',
2325
- name: 'package',
2326
- message: `Package for requirement ${requirements.length + 1}:`,
2327
- choices: packages.map(p => ({ name: p, value: p })),
2328
- default: 'all'
2329
- }]);
2330
-
2331
- // Ask for name
2332
- const { name } = await inquirer.prompt([{
2333
- type: 'input',
2334
- name: 'name',
2335
- message: `Name for requirement ${requirements.length + 1}:`
2336
- }]);
2337
-
2338
- if (name.trim() === '') {
2339
- done = true;
2340
- } else {
2341
- // Ask for description
2342
- console.log(chalk.gray('\nEnter description (press Enter twice on empty line to finish):\n'));
2343
- const descriptionLines = [];
2344
- let emptyLineCount = 0;
2345
- let isFirstLine = true;
2346
-
2347
- while (true) {
2348
- try {
2349
- const { line } = await inquirer.prompt([{
2350
- type: 'input',
2351
- name: 'line',
2352
- message: isFirstLine ? 'Description:' : ''
2353
- }]);
2354
-
2355
- isFirstLine = false;
2356
-
2357
- if (line.trim() === '') {
2358
- emptyLineCount++;
2359
- if (emptyLineCount >= 2) {
2360
- break;
2361
- }
2362
- } else {
2363
- emptyLineCount = 0;
2364
- descriptionLines.push(line);
2365
- }
2366
- } catch (err) {
2367
- break;
2368
- }
2369
- }
2370
-
2371
- const description = descriptionLines.join('\n');
2372
- requirements.push({ name, package: pkg, description });
2373
- }
2374
- } catch (err) {
2375
- done = true;
2376
- }
2377
- }
2378
-
2379
- if (requirements.length > 0) {
2380
- for (const req of requirements) {
2381
- await reqCommands.add(req.name, req.package, req.description);
2382
- }
2383
- console.log(chalk.green(`\n✓ Added ${requirements.length} requirement(s)\n`));
2384
- await new Promise(resolve => setTimeout(resolve, 1000));
2385
- }
2386
- } catch (err) {
2387
- // ESC pressed
2388
- }
2389
- }
2390
- }
2391
-
2392
- // Helper to show requirements from a specific section with actions
2393
- async function showRequirementsBySection(sectionTitle) {
2394
- const { getRequirementsPath } = require('vibecodingmachine-core');
2395
- const reqPath = await getRequirementsPath();
2396
-
2397
- if (!reqPath || !await fs.pathExists(reqPath)) {
2398
- console.log(chalk.yellow('No requirements file found.'));
2399
- return;
2400
- }
2401
-
2402
- const content = await fs.readFile(reqPath, 'utf8');
2403
- const lines = content.split('\n');
2404
-
2405
- let inSection = false;
2406
- const requirements = [];
2407
-
2408
- for (let i = 0; i < lines.length; i++) {
2409
- const line = lines[i];
2410
-
2411
- // Check if we're entering the target section
2412
- if (line.includes(sectionTitle)) {
2413
- inSection = true;
2414
- continue;
2415
- }
2416
-
2417
- // Check if we're leaving the section (hit another ## section)
2418
- if (inSection && line.startsWith('## ') && !line.includes(sectionTitle)) {
2419
- break;
2420
- }
2421
-
2422
- // Collect requirements in the section (formatted as bullet points with -)
2423
- if (inSection && line.trim().startsWith('- ')) {
2424
- const title = line.trim().substring(2); // Remove "- " prefix
2425
- const details = [];
2426
-
2427
- // Collect details (continuation lines until next bullet point or section)
2428
- for (let j = i + 1; j < lines.length; j++) {
2429
- if (lines[j].trim().startsWith('- ') || lines[j].startsWith('##')) {
2430
- break;
2431
- }
2432
- if (lines[j].trim()) {
2433
- details.push(lines[j].trim());
2434
- }
2435
- }
2436
-
2437
- requirements.push({ title, details, lineIndex: i });
2438
- }
2439
- }
2440
-
2441
- if (requirements.length === 0) {
2442
- console.log(chalk.gray(' (No requirements in this section)'));
2443
- return;
2444
- }
2445
-
2446
- // Show interactive list
2447
- let inRequirementsList = true;
2448
- while (inRequirementsList) {
2449
- try {
2450
- console.log(chalk.bold.cyan(`\n${sectionTitle}\n`));
2451
- console.log(chalk.green(`Total: ${requirements.length} requirement(s)\n`));
2452
-
2453
- const choices = requirements.map((req, index) => ({
2454
- name: `${index + 1}. ${req.title}`,
2455
- value: index
2456
- }));
2457
-
2458
- const { selectedIndex } = await inquirer.prompt([{
2459
- type: 'list',
2460
- name: 'selectedIndex',
2461
- message: 'Select a requirement (ESC to go back):',
2462
- choices: choices,
2463
- pageSize: 15
2464
- }]);
2465
-
2466
- // Show actions for selected requirement
2467
- const requirement = requirements[selectedIndex];
2468
- console.log(chalk.bold.yellow(`\n${requirement.title}\n`));
2469
- if (requirement.details.length > 0) {
2470
- console.log(chalk.gray(requirement.details.join('\n')));
2471
- console.log();
2472
- }
2473
-
2474
- const { action } = await inquirer.prompt([{
2475
- type: 'list',
2476
- name: 'action',
2477
- message: 'What would you like to do? (ESC to go back)',
2478
- choices: [
2479
- { name: '👍 Thumbs up (prioritize)', value: 'thumbs-up' },
2480
- { name: '👎 Thumbs down (deprioritize)', value: 'thumbs-down' },
2481
- { name: '⬆️ Move up', value: 'move-up' },
2482
- { name: '⬇️ Move down', value: 'move-down' },
2483
- { name: '♻️ Recycle (move to Recycled)', value: 'recycle' },
2484
- { name: '🗑️ Delete', value: 'delete' }
2485
- ]
2486
- }]);
2487
-
2488
- // Perform action
2489
- switch (action) {
2490
- case 'thumbs-up':
2491
- // Move to top of list
2492
- const thumbsUpReq = requirements.splice(selectedIndex, 1)[0];
2493
- requirements.unshift(thumbsUpReq);
2494
- await saveRequirementsOrder(reqPath, sectionTitle, requirements);
2495
- console.log(chalk.green('\n✓ Moved to top (prioritized)\n'));
2496
- break;
2497
- case 'thumbs-down':
2498
- // Move to bottom of list
2499
- const thumbsDownReq = requirements.splice(selectedIndex, 1)[0];
2500
- requirements.push(thumbsDownReq);
2501
- await saveRequirementsOrder(reqPath, sectionTitle, requirements);
2502
- console.log(chalk.yellow('\n✓ Moved to bottom (deprioritized)\n'));
2503
- break;
2504
- case 'move-up':
2505
- if (selectedIndex > 0) {
2506
- [requirements[selectedIndex], requirements[selectedIndex - 1]] =
2507
- [requirements[selectedIndex - 1], requirements[selectedIndex]];
2508
- await saveRequirementsOrder(reqPath, sectionTitle, requirements);
2509
- console.log(chalk.green('\n✓ Moved up\n'));
2510
- } else {
2511
- console.log(chalk.yellow('\n⚠ Already at top\n'));
2512
- }
2513
- break;
2514
- case 'move-down':
2515
- if (selectedIndex < requirements.length - 1) {
2516
- [requirements[selectedIndex], requirements[selectedIndex + 1]] =
2517
- [requirements[selectedIndex + 1], requirements[selectedIndex]];
2518
- await saveRequirementsOrder(reqPath, sectionTitle, requirements);
2519
- console.log(chalk.green('\n✓ Moved down\n'));
2520
- } else {
2521
- console.log(chalk.yellow('\n⚠ Already at bottom\n'));
2522
- }
2523
- break;
2524
- case 'recycle':
2525
- const recycledReq = requirements.splice(selectedIndex, 1)[0];
2526
- await moveToRecycled(reqPath, recycledReq.title, sectionTitle);
2527
- console.log(chalk.cyan('\n✓ Moved to Recycled section\n'));
2528
- if (requirements.length === 0) {
2529
- console.log(chalk.gray('No more requirements in this section.\n'));
2530
- inRequirementsList = false;
2531
- }
2532
- break;
2533
- case 'delete':
2534
- const { confirmDelete } = await promptWithDefaultsOnce([{
2535
- type: 'confirm',
2536
- name: 'confirmDelete',
2537
- message: 'Are you sure you want to delete this requirement?',
2538
- default: false
2539
- }]);
2540
- if (confirmDelete) {
2541
- requirements.splice(selectedIndex, 1);
2542
- await saveRequirementsOrder(reqPath, sectionTitle, requirements);
2543
- console.log(chalk.green('\n✓ Requirement deleted\n'));
2544
- if (requirements.length === 0) {
2545
- console.log(chalk.gray('No more requirements in this section.\n'));
2546
- inRequirementsList = false;
2547
- }
2548
- } else {
2549
- console.log(chalk.yellow('\nCancelled\n'));
2550
- }
2551
- break;
2552
- }
2553
- } catch (error) {
2554
- // ESC pressed - exit list
2555
- inRequirementsList = false;
2556
- }
2557
- }
2558
- }
2559
-
2560
- // Helper to save reordered requirements back to file
2561
- async function saveRequirementsOrder(reqPath, sectionTitle, requirements) {
2562
- const content = await fs.readFile(reqPath, 'utf8');
2563
- const lines = content.split('\n');
2564
-
2565
- let inSection = false;
2566
- let sectionStartIndex = -1;
2567
- let sectionEndIndex = -1;
2568
-
2569
- // Find section boundaries
2570
- for (let i = 0; i < lines.length; i++) {
2571
- if (lines[i].includes(sectionTitle)) {
2572
- inSection = true;
2573
- sectionStartIndex = i;
2574
- continue;
2575
- }
2576
- // Stop at next ## header (but not ### which are requirement headers)
2577
- if (inSection && lines[i].startsWith('## ') && !lines[i].startsWith('###') && !lines[i].includes(sectionTitle)) {
2578
- sectionEndIndex = i;
2579
- break;
2580
- }
2581
- }
2582
-
2583
- if (sectionEndIndex === -1) {
2584
- sectionEndIndex = lines.length;
2585
- }
2586
-
2587
- // Rebuild section with new order (### header format)
2588
- const newSectionLines = [lines[sectionStartIndex]];
2589
- for (const req of requirements) {
2590
- // Add requirement header
2591
- newSectionLines.push(`### ${req.title}`);
2592
- // Add package if present
2593
- if (req.package && req.package !== 'all') {
2594
- newSectionLines.push(`PACKAGE: ${req.package}`);
2595
- }
2596
- // Add description if present
2597
- if (req.details && req.details.length > 0) {
2598
- req.details.forEach(line => {
2599
- if (line.trim()) {
2600
- newSectionLines.push(line);
2601
- }
2602
- });
2603
- }
2604
- // Add blank line after requirement
2605
- newSectionLines.push('');
2606
- }
2607
-
2608
- // Replace section in content
2609
- const newLines = [
2610
- ...lines.slice(0, sectionStartIndex),
2611
- ...newSectionLines,
2612
- ...lines.slice(sectionEndIndex)
2613
- ];
2614
-
2615
- await fs.writeFile(reqPath, newLines.join('\n'));
2616
- }
2617
-
2618
- // Helper to show verified requirements from CHANGELOG
2619
- async function showRequirementsFromChangelog() {
2620
- const { getVibeCodingMachineDir, checkVibeCodingMachineExists } = require('vibecodingmachine-core');
2621
-
2622
- const allnightStatus = await checkVibeCodingMachineExists();
2623
- let changelogPath;
2624
-
2625
- if (allnightStatus.insideExists) {
2626
- const allnightDir = await getVibeCodingMachineDir();
2627
- changelogPath = path.join(path.dirname(allnightDir), 'CHANGELOG.md');
2628
- } else if (allnightStatus.siblingExists) {
2629
- changelogPath = path.join(process.cwd(), 'CHANGELOG.md');
2630
- }
2631
-
2632
- if (!changelogPath || !await fs.pathExists(changelogPath)) {
2633
- console.log(chalk.yellow('No CHANGELOG.md found.'));
2634
- return;
2635
- }
2636
-
2637
- const content = await fs.readFile(changelogPath, 'utf8');
2638
- const lines = content.split('\n');
2639
- let count = 0;
2640
-
2641
- console.log(chalk.bold.cyan('Verified Requirements (from CHANGELOG.md):\n'));
2642
-
2643
- for (const line of lines) {
2644
- const trimmed = line.trim();
2645
- if (trimmed.startsWith('- ') && trimmed.length > 10) {
2646
- count++;
2647
- console.log(chalk.green(`${count}. ${trimmed.substring(2)}`));
2648
- }
2649
- }
2650
-
2651
- if (count === 0) {
2652
- console.log(chalk.gray(' (No verified requirements yet)'));
2653
- } else {
2654
- console.log(chalk.green(`\nTotal: ${count} requirement(s)`));
2655
- }
2656
- }
2657
-
2658
- // Custom menu with both arrow keys and letter shortcuts
2659
- async function showQuickMenu(items, initialSelectedIndex = 0) {
2660
- return new Promise((resolve) => {
2661
- // Skip blank and info items when setting initial index
2662
- let selectedIndex = initialSelectedIndex;
2663
- while (selectedIndex < items.length && (items[selectedIndex].type === 'blank' || items[selectedIndex].type === 'info')) {
2664
- selectedIndex++;
2665
- }
2666
- if (selectedIndex >= items.length) selectedIndex = 0;
2667
-
2668
- let isFirstRender = true;
2669
-
2670
- // Helper to calculate visual lines occupied by text
2671
- const getVisualLineCount = (text) => {
2672
- const columns = process.stdout.columns || 80;
2673
- let lineCount = 0;
2674
-
2675
- // Strip ANSI codes for length calculation
2676
- // Simple regex for stripping ANSI codes
2677
- const stripAnsi = (str) => str.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '');
2678
-
2679
- const lines = text.split('\n');
2680
- for (const line of lines) {
2681
- const visualLength = stripAnsi(line).length;
2682
- // If line is empty, it still takes 1 line
2683
- if (visualLength === 0) {
2684
- lineCount += 1;
2685
- } else {
2686
- // Calculate wrapped lines
2687
- lineCount += Math.ceil(visualLength / columns);
2688
- }
2689
- }
2690
- return lineCount;
2691
- };
2692
-
2693
- const displayMenu = async () => {
2694
- // Only clear and redraw on first render, not on arrow navigation
2695
- if (isFirstRender) {
2696
- // Clear entire screen only on first render
2697
- console.clear();
2698
- // Reprint the banner and status info
2699
- try {
2700
- await showWelcomeScreen();
2701
- } catch (err) {
2702
- console.error('Error displaying banner:', err);
2703
- }
2704
- } else {
2705
- // For arrow navigation, just move cursor to menu start position
2706
- // and clear only the menu area, not the entire screen
2707
- if (lastLinesPrinted > 0) {
2708
- // Move cursor up to the start of the menu and clear menu area
2709
- readline.moveCursor(process.stdout, 0, -lastLinesPrinted);
2710
- readline.clearScreenDown(process.stdout);
2711
- }
2712
- }
2713
- isFirstRender = false;
2714
-
2715
- // Track lines printed this render
2716
- let linesPrinted = 0;
2717
-
2718
- // Display menu with highlight - all items get letters except info items
2719
- let letterIndex = 0;
2720
- items.forEach((item, index) => {
2721
- const isSelected = index === selectedIndex;
2722
- let output = '';
2723
-
2724
- if (item.type === 'blank') {
2725
- // Blank separator line
2726
- console.log();
2727
- linesPrinted++;
2728
- } else if (item.type === 'info') {
2729
- // Info items are display-only, no letter, no selection
2730
- output = chalk.gray(` ${item.name}`);
2731
- console.log(output);
2732
- linesPrinted += getVisualLineCount(` ${item.name}`);
2733
- } else {
2734
- // All other items have letters (Exit always uses 'x')
2735
- let letter;
2736
- if (item.value === 'exit') {
2737
- letter = 'x';
2738
- } else {
2739
- letter = indexToLetter(letterIndex);
2740
- letterIndex++;
2741
- }
2742
-
2743
- if (isSelected) {
2744
- output = chalk.cyan(`❯ ${letter}) ${item.name}`);
2745
- console.log(output);
2746
- } else if (item.type === 'setting') {
2747
- // Settings in gray
2748
- output = chalk.gray(` ${letter}) ${item.name}`);
2749
- console.log(output);
2750
- } else {
2751
- // Actions in default color
2752
- output = ` ${letter}) ${item.name}`;
2753
- console.log(output);
2754
- }
2755
-
2756
- // Calculate lines based on the actual text printed (including indentation/prefix)
2757
- // We reconstruct the raw string that was logged to calculate wrapping correctly
2758
- let rawString = '';
2759
- if (isSelected) {
2760
- rawString = `❯ ${letter}) ${item.name}`;
2761
- } else {
2762
- rawString = ` ${letter}) ${item.name}`;
2763
- }
2764
- linesPrinted += getVisualLineCount(rawString);
2765
- }
2766
- });
2767
-
2768
- // Count all items with letters (excluding exit, blank, and info)
2769
- const letterCount = items.filter(item => item.type !== 'blank' && item.type !== 'info' && item.value !== 'exit').length;
2770
- const lastLetter = letterCount > 0 ? indexToLetter(letterCount - 1) : 'a';
2771
- const rangeText = letterCount > 1 ? `a-${lastLetter}` : (letterCount === 1 ? 'a' : '');
2772
- const helpText = rangeText ? `${rangeText}, x` : 'x';
2773
- const helpString = `\n ${t('interactive.navigation.help', { letters: helpText })}`;
2774
- console.log(chalk.gray(helpString));
2775
- linesPrinted += getVisualLineCount(helpString);
2776
-
2777
- // Save for next render
2778
- lastLinesPrinted = linesPrinted;
2779
- };
2780
-
2781
- const cleanup = () => {
2782
- if (process.stdin.isTTY && process.stdin.setRawMode) {
2783
- process.stdin.setRawMode(false);
2784
- }
2785
- process.stdin.removeListener('keypress', onKeypress);
2786
- process.stdin.pause();
2787
- };
2788
-
2789
- const selectOption = (index) => {
2790
- // Debug: Log what's being selected
2791
- console.log(chalk.yellow(`\n[DEBUG] selectOption called with index: ${index}`));
2792
- console.log(chalk.yellow(`[DEBUG] Item at index ${index}: value="${items[index].value}", name="${items[index].name}"\n`));
2793
-
2794
- cleanup();
2795
- // Don't clear screen for exit - we want to keep the status visible
2796
- if (items[index].value !== 'exit') {
2797
- // Clear the prompt line for non-exit options
2798
- readline.moveCursor(process.stdout, 0, -(items.length - index));
2799
- readline.clearScreenDown(process.stdout);
2800
- console.log(chalk.cyan(` → ${items[index].name}\n`));
2801
- }
2802
- // Always log what value is being resolved
2803
- console.log(chalk.yellow(`[DEBUG] Resolving value: "${items[index].value}" from index ${index}\n`));
2804
- resolve({ value: items[index].value, selectedIndex: index });
2805
- };
2806
-
2807
- displayMenu();
2808
-
2809
- // Set up keypress listener
2810
- readline.emitKeypressEvents(process.stdin);
2811
- if (process.stdin.isTTY) {
2812
- process.stdin.setRawMode(true);
2813
- }
2814
-
2815
- const onKeypress = async (str, key) => {
2816
- if (!key) return;
2817
-
2818
- // Ctrl+C to exit
2819
- if (key.ctrl && key.name === 'c') {
2820
- cleanup();
2821
- process.exit(0);
2822
- return;
2823
- }
2824
-
2825
- // ESC or left arrow to exit with confirmation
2826
- if (key.name === 'escape' || key.name === 'left') {
2827
- cleanup();
2828
- // Don't clear screen for exit - keep status visible
2829
- resolve({ value: 'exit', selectedIndex });
2830
- return;
2831
- }
2832
-
2833
- // Letter keys for instant selection - 'x' always maps to exit
2834
- if (str && str.length === 1) {
2835
- if (str === 'x') {
2836
- // 'x' always maps to exit (will trigger confirmAndExit)
2837
- cleanup();
2838
- // Don't clear screen for exit - keep status visible
2839
- resolve({ value: 'exit', selectedIndex });
2840
- return;
2841
- } else if (str >= 'a' && str <= 'z') {
2842
- // Other letters map to all items (settings + actions, excluding exit, blank, and info)
2843
- const letterIndex = str.charCodeAt(0) - 97; // Convert letter to index (a=0, b=1, etc.)
2844
-
2845
- // Find the nth item with a letter (excluding exit, blank, and info)
2846
- let letterCount = 0;
2847
- for (let i = 0; i < items.length; i++) {
2848
- if (items[i].type !== 'blank' && items[i].type !== 'info' && items[i].value !== 'exit') {
2849
- if (letterCount === letterIndex) {
2850
- selectOption(i);
2851
- return;
2852
- }
2853
- letterCount++;
2854
- }
2855
- }
2856
- }
2857
- }
2858
-
2859
- // Arrow keys for navigation
2860
- if (key.name === 'up') {
2861
- // Search backwards for the previous selectable item
2862
- let testIndex = selectedIndex - 1;
2863
- while (testIndex >= 0) {
2864
- if (items[testIndex].type !== 'blank' && items[testIndex].type !== 'info') {
2865
- selectedIndex = testIndex;
2866
- await displayMenu();
2867
- break;
2868
- }
2869
- testIndex--;
2870
- }
2871
- } else if (key.name === 'down') {
2872
- // Search forwards for the next selectable item
2873
- let testIndex = selectedIndex + 1;
2874
- while (testIndex < items.length) {
2875
- if (items[testIndex].type !== 'blank' && items[testIndex].type !== 'info') {
2876
- selectedIndex = testIndex;
2877
- await displayMenu();
2878
- break;
2879
- }
2880
- testIndex++;
2881
- }
2882
- } else if (key.name === 'return' || key.name === 'right') {
2883
- // Don't allow selecting blank or info lines
2884
- if (items[selectedIndex].type !== 'blank' && items[selectedIndex].type !== 'info') {
2885
- selectOption(selectedIndex);
2886
- }
2887
- }
2888
- };
2889
-
2890
- process.stdin.on('keypress', onKeypress);
2891
- process.stdin.resume();
2892
- });
2893
- }
2894
-
2895
- async function showProviderManagerMenu() {
2896
- const definitions = getProviderDefinitions();
2897
- const defMap = new Map(definitions.map(def => [def.id, def]));
2898
- const prefs = await getProviderPreferences();
2899
- let order = prefs.order.slice();
2900
- let enabled = { ...prefs.enabled };
2901
- let selectedIndex = 0;
2902
- let dirty = false;
2903
-
2904
- const { fetchQuotaForAgent } = require('vibecodingmachine-core/src/quota-management');
2905
-
2906
- const debugQuota = process.env.VCM_DEBUG_QUOTA === '1' || process.env.VCM_DEBUG_QUOTA === 'true';
2907
-
2908
- const formatDuration = (ms) => {
2909
- if (!ms || ms <= 0) return 'now';
2910
- const totalSeconds = Math.ceil(ms / 1000);
2911
- const hours = Math.floor(totalSeconds / 3600);
2912
- const minutes = Math.floor((totalSeconds % 3600) / 60);
2913
- const seconds = totalSeconds % 60;
2914
-
2915
- if (hours > 0) return `${hours}h ${minutes}m`;
2916
- if (minutes > 0) return `${minutes}m ${seconds}s`;
2917
- return `${seconds}s`;
2918
- };
2919
-
2920
- const render = async () => {
2921
- process.stdout.write('\x1Bc');
2922
- console.log(chalk.bold.cyan('⚙ ' + t('provider.title') + '\n'));
2923
-
2924
- // Fetch quota info
2925
- const sharedAuth = require('vibecodingmachine-core/src/auth/shared-auth-storage');
2926
- const autoConfig = await getAutoConfig();
2927
- const quotaInfo = await sharedAuth.canRunAutoMode();
2928
- const remaining = Math.max(0, (quotaInfo.maxIterations || 10) - (quotaInfo.todayUsage || 0));
2929
-
2930
- // Calculate time until reset (midnight)
2931
- const now = new Date();
2932
- const tonight = new Date(now);
2933
- tonight.setHours(24, 0, 0, 0);
2934
- const msUntilReset = tonight.getTime() - now.getTime();
2935
- const hoursUntilReset = Math.floor(msUntilReset / (1000 * 60 * 60));
2936
- const minsUntilReset = Math.floor((msUntilReset % (1000 * 60 * 60)) / (1000 * 60));
2937
-
2938
- // Display quota as time-based instead of numeric (0/1 or 1/1 format)
2939
- let quotaDisplay;
2940
- if (remaining === 0) {
2941
- // Rate limit active - show when it resets (in red)
2942
- quotaDisplay = chalk.gray(' ' + t('provider.overall.quota') + ': ') + chalk.red(`⏰ ${t('provider.rate.limit.resets')} ${hoursUntilReset}h ${minsUntilReset}m`);
2943
- } else {
2944
- // Quota available - show when it resets (in green)
2945
- quotaDisplay = chalk.gray(' ' + t('provider.overall.quota') + ': ') + chalk.green(`✓ ${t('provider.available')} (${remaining}/${quotaInfo.maxIterations})`) + chalk.gray(' • ' + t('provider.resets.in') + ' ') + chalk.cyan(`${hoursUntilReset}h ${minsUntilReset}m`);
2946
- }
2947
- console.log(quotaDisplay);
2948
- console.log(chalk.gray(' ' + t('provider.instructions') + '\n'));
2949
-
2950
- for (let idx = 0; idx < order.length; idx++) {
2951
- const id = order[idx];
2952
- const def = defMap.get(id);
2953
- if (!def) continue;
2954
- const isSelected = idx === selectedIndex;
2955
- const isEnabled = enabled[id] !== false;
2956
-
2957
- // Check installation status for all IDEs
2958
- let isInstalled = true;
2959
- if (def.type === 'ide') {
2960
- try {
2961
- if (id === 'kiro') {
2962
- const { isKiroInstalled } = require('./kiro-installer');
2963
- isInstalled = isKiroInstalled();
2964
- } else if (id === 'cursor') {
2965
- isInstalled = await checkAppOrBinary(['Cursor'], ['cursor']);
2966
- } else if (id === 'windsurf') {
2967
- isInstalled = await checkAppOrBinary(['Windsurf'], ['windsurf']);
2968
- } else if (id === 'vscode') {
2969
- isInstalled = await checkAppOrBinary(['Visual Studio Code', 'Visual Studio Code - Insiders'], ['code', 'code-insiders']);
2970
- } else if (id === 'antigravity') {
2971
- isInstalled = await checkAppOrBinary(['Antigravity'], ['antigravity']);
2972
- } else {
2973
- // For other IDEs, try binary name same as id
2974
- isInstalled = await checkAppOrBinary([], [id]);
2975
- }
2976
- } catch (e) {
2977
- // If detection fails, assume not installed
2978
- isInstalled = false;
2979
- }
2980
- }
2981
-
2982
- // Determine status emoji: disabled = red alert, not installed = yellow, enabled = green
2983
- let statusEmoji;
2984
- if (!isEnabled) {
2985
- statusEmoji = '🚨'; // Red for disabled
2986
- } else if (def.type === 'ide' && !isInstalled) {
2987
- statusEmoji = '🟡'; // Yellow for not installed
2988
- } else {
2989
- statusEmoji = '🟢'; // Green for enabled and installed (or LLM)
2990
- }
2991
-
2992
- const typeLabel = def.type === 'ide' ? chalk.cyan('IDE') : chalk.cyan('LLM');
2993
- const prefix = isSelected ? chalk.cyan('❯') : ' ';
2994
- let line = `${prefix} ${statusEmoji} ${idx + 1}. ${def.name} ${chalk.gray(`(${def.id})`)} ${typeLabel}`;
2995
-
2996
- // Fetch and display specific quota for this agent
2997
- try {
2998
- // Find the active model for this provider if possible
2999
- let model = def.defaultModel || id;
3000
- if (id === 'groq') model = autoConfig.groqModel || model;
3001
- else if (id === 'anthropic') model = autoConfig.anthropicModel || model;
3002
- else if (id === 'ollama') {
3003
- const preferredModel = autoConfig.llmModel && autoConfig.llmModel.includes('ollama/')
3004
- ? autoConfig.llmModel.split('/')[1]
3005
- : autoConfig.llmModel || autoConfig.aiderModel;
3006
- model = (preferredModel && preferredModel !== id) ? preferredModel : model;
3007
- }
3008
-
3009
- const agentId = `${id}:${model}`;
3010
- const quota = await fetchQuotaForAgent(agentId);
3011
-
3012
- if (debugQuota) {
3013
- const resetMs = quota?.resetsAt ? (new Date(quota.resetsAt).getTime() - Date.now()) : null;
3014
- console.error(`[VCM_DEBUG_QUOTA] provider=${id} model=${model} type=${quota?.type} remaining=${quota?.remaining} limit=${quota?.limit} resetsAt=${quota?.resetsAt ? new Date(quota.resetsAt).toISOString() : 'null'} resetIn=${resetMs !== null ? formatDuration(resetMs) : 'null'}`);
3015
- }
3016
-
3017
- if (quota.type === 'infinite') {
3018
- line += ` ${chalk.gray('[' + t('provider.status.quota.infinite') + ']')}`;
3019
- } else if (quota.type === 'rate-limit') {
3020
- if (quota.isExceeded()) {
3021
- if (quota.resetsAt) {
3022
- const msUntilReset = new Date(quota.resetsAt).getTime() - Date.now();
3023
- line += ` ${chalk.red(`[⏳ resets in ${formatDuration(msUntilReset)}]`)}`;
3024
- } else {
3025
- line += ` ${chalk.red('[Rate limited]')}`;
3026
- }
3027
- } else {
3028
- // Show time until rate limit starts (when it resets)
3029
- if (quota.resetsAt) {
3030
- const msUntilReset = new Date(quota.resetsAt).getTime() - Date.now();
3031
- line += ` ${chalk.green(`[✓ ${t('provider.status.available.resets')} ${formatDuration(msUntilReset)}]`)}`;
3032
- } else {
3033
- line += ` ${chalk.green('[' + t('provider.status.available') + ']')}`;
3034
- }
3035
- }
3036
- }
3037
- } catch (e) {
3038
- // Silently skip if quota fetch fails
3039
- }
3040
-
3041
- console.log(line);
3042
- }
3043
-
3044
- console.log();
3045
- if (dirty) {
3046
- console.log(chalk.yellow(t('provider.pending.changes')));
3047
- } else {
3048
- console.log(chalk.gray(t('provider.no.pending.changes')));
3049
- }
3050
- };
3051
-
3052
- if (process.env.VCM_RENDER_ONCE === '1' || process.env.VCM_RENDER_ONCE === 'true') {
3053
- await render();
3054
- return;
3055
- }
3056
-
3057
- return new Promise(async (resolve) => {
3058
- const cleanup = () => {
3059
- if (process.stdin.isTTY && process.stdin.setRawMode) {
3060
- process.stdin.setRawMode(false);
3061
- }
3062
- process.stdin.removeListener('keypress', onKeypress);
3063
- process.stdin.pause();
3064
- };
3065
-
3066
- const saveAndExit = async (selectedId) => {
3067
- cleanup();
3068
- if (dirty) {
3069
- await saveProviderPreferences(order, enabled);
3070
- }
3071
- if (selectedId) {
3072
- // Check for Kiro installation if selected
3073
- if (selectedId === 'kiro') {
3074
- try {
3075
- const { isKiroInstalled, installKiro } = require('./kiro-installer');
3076
- if (!isKiroInstalled()) {
3077
- await installKiro();
3078
- }
3079
- } catch (e) {
3080
- console.log(chalk.red('Error checking/installing Kiro: ' + e.message));
3081
- }
3082
- }
3083
-
3084
- const { setAutoConfig } = require('./config');
3085
- await setAutoConfig({ agent: selectedId, ide: selectedId });
3086
- const def = defMap.get(selectedId);
3087
- console.log(chalk.green(`\n✓ Active provider set to ${def?.name || selectedId}\n`));
3088
- } else {
3089
- console.log();
3090
- }
3091
- resolve(selectedId || null);
3092
- };
3093
-
3094
- const cancel = () => {
3095
- cleanup();
3096
- console.log('\n');
3097
- resolve(null);
3098
- };
3099
-
3100
- const moveSelection = async (delta) => {
3101
- const next = selectedIndex + delta;
3102
- if (next >= 0 && next < order.length) {
3103
- selectedIndex = next;
3104
- await render();
3105
- }
3106
- };
3107
-
3108
- const reorder = async (delta) => {
3109
- const target = selectedIndex + delta;
3110
- if (target < 0 || target >= order.length) return;
3111
- const temp = order[selectedIndex];
3112
- order[selectedIndex] = order[target];
3113
- order[target] = temp;
3114
- selectedIndex = target;
3115
- dirty = true;
3116
- await render();
3117
- };
3118
-
3119
- const toggle = async (value) => {
3120
- const id = order[selectedIndex];
3121
- enabled[id] = value;
3122
- dirty = true;
3123
- await render();
3124
- };
3125
-
3126
- const onKeypress = async (str, key = {}) => {
3127
- if (key.ctrl && key.name === 'c') {
3128
- cancel();
3129
- return;
3130
- }
3131
-
3132
- switch (key.name) {
3133
- case 'up':
3134
- await moveSelection(-1);
3135
- break;
3136
- case 'down':
3137
- await moveSelection(1);
3138
- break;
3139
- case 'j':
3140
- await reorder(1);
3141
- break;
3142
- case 'k':
3143
- await reorder(-1);
3144
- break;
3145
- case 'e':
3146
- await toggle(true);
3147
- break;
3148
- case 'd':
3149
- await toggle(false);
3150
- break;
3151
- case 'space':
3152
- await toggle(!(enabled[order[selectedIndex]] !== false));
3153
- break;
3154
- case 'return':
3155
- saveAndExit(order[selectedIndex]);
3156
- break;
3157
- case 'escape':
3158
- case 'left':
3159
- case 'x':
3160
- cancel();
3161
- break;
3162
- default:
3163
- break;
3164
- }
3165
- };
3166
-
3167
- if (process.stdin.isTTY && process.stdin.setRawMode) {
3168
- process.stdin.setRawMode(true);
3169
- }
3170
- readline.emitKeypressEvents(process.stdin);
3171
- process.stdin.on('keypress', onKeypress);
3172
- process.stdin.resume();
3173
-
3174
- await render();
462
+ // Run main function if this file is executed directly
463
+ if (require.main === module) {
464
+ setupKeyboardShortcuts();
465
+
466
+ main().catch(error => {
467
+ showError('Fatal error', error);
468
+ process.exit(1);
469
+ }).finally(() => {
470
+ cleanup();
3175
471
  });
3176
472
  }
3177
-
3178
- /* async function showSettings() {
3179
- console.log(chalk.bold.cyan('\n⚙️ Settings\n'));
3180
-
3181
- const { setConfigValue } = require('vibecodingmachine-core');
3182
- const { getAutoConfig, setAutoConfig } = require('./config');
3183
- const currentHostnameEnabled = await isComputerNameEnabled();
3184
- const hostname = getHostname();
3185
- const autoConfig = await getAutoConfig();
3186
- const currentIDE = autoConfig.ide || 'claude-code';
3187
-
3188
- // Show current settings
3189
- console.log(chalk.gray('Current settings:'));
3190
- console.log(chalk.gray(' Computer Name: '), chalk.cyan(hostname));
3191
- console.log(chalk.gray(' Current IDE: '), chalk.cyan(formatIDEName(currentIDE)));
3192
- console.log(chalk.gray(' Use Hostname in Req File: '), currentHostnameEnabled ? chalk.green('Enabled ✓') : chalk.yellow('Disabled ○'));
3193
- console.log();
3194
-
3195
- const { action } = await inquirer.prompt([
3196
- {
3197
- type: 'list',
3198
- name: 'action',
3199
- message: 'What would you like to do?',
3200
- choices: [
3201
- {
3202
- name: 'Change IDE',
3203
- value: 'change-ide'
3204
- },
3205
- {
3206
- name: currentHostnameEnabled ? 'Disable hostname in requirements file' : 'Enable hostname in requirements file',
3207
- value: 'toggle-hostname'
3208
- },
3209
- {
3210
- name: 'Back to main menu',
3211
- value: 'back'
3212
- }
3213
- ]
3214
- }
3215
- ]);
3216
-
3217
- if (action === 'change-ide') {
3218
- const { ide } = await inquirer.prompt([
3219
- {
3220
- type: 'list',
3221
- name: 'ide',
3222
- message: 'Select IDE:',
3223
- choices: [
3224
- { name: 'Claude Code CLI (recommended - Anthropic Claude)', value: 'claude-code' },
3225
- { name: 'Aider CLI (best for Ollama & Bedrock)', value: 'aider' },
3226
- { name: 'Continue CLI (Ollama support, but --auto mode doesn\'t execute code)', value: 'continue' },
3227
- { name: 'Cline CLI (local AI alternative, but has Ollama connection issues)', value: 'cline' },
3228
- { name: 'Cursor', value: 'cursor' },
3229
- { name: 'VS Code', value: 'vscode' },
3230
- { name: 'Windsurf', value: 'windsurf' }
3231
- ],
3232
- default: currentIDE
3233
- }
3234
- ]);
3235
-
3236
- // Save to config
3237
- const newConfig = { ...autoConfig, ide };
3238
- await setAutoConfig(newConfig);
3239
-
3240
- console.log(chalk.green('\n✓'), `IDE changed to ${chalk.cyan(formatIDEName(ide))}`);
3241
- console.log(chalk.gray(' Note: This will be used for the next Auto Mode session.'));
3242
- console.log();
3243
- } else if (action === 'toggle-hostname') {
3244
- const newValue = !currentHostnameEnabled;
3245
-
3246
- // Save to shared config (same location as Electron app)
3247
- await setConfigValue('computerNameEnabled', newValue);
3248
-
3249
- const statusText = newValue ? chalk.green('enabled') : chalk.yellow('disabled');
3250
- console.log(chalk.green('\n✓'), `Hostname in requirements file ${statusText}`);
3251
-
3252
- if (newValue) {
3253
- console.log(chalk.gray('\n Requirements file will be:'), chalk.cyan(`REQUIREMENTS-${hostname}.md`));
3254
- } else {
3255
- console.log(chalk.gray('\n Requirements file will be:'), chalk.cyan('REQUIREMENTS.md'));
3256
- }
3257
-
3258
- console.log(chalk.gray('\n Note: You may need to rename your existing requirements file.'));
3259
- console.log(chalk.gray(' Note: This setting is now synced with the Electron app.'));
3260
- console.log();
3261
- }
3262
- }
3263
-
3264
- /**
3265
- * Show cloud sync management menu
3266
- */
3267
- async function showCloudSyncMenu() {
3268
- console.clear();
3269
- console.log(chalk.bold.cyan(`\n☁️ ${t('cloud.sync.title')}\n`));
3270
-
3271
- // Check if cloud sync is configured
3272
- try {
3273
- const SyncEngine = require('vibecodingmachine-core/src/sync/sync-engine');
3274
- const testEngine = new SyncEngine();
3275
- await testEngine.initialize();
3276
- testEngine.stop();
3277
- } catch (error) {
3278
- console.log(chalk.yellow(`⚠️ ${t('cloud.sync.not.configured')}\n`));
3279
- console.log(chalk.white(`${t('cloud.sync.setup.title')}\n`));
3280
- console.log(chalk.gray(`1. ${t('cloud.sync.setup.step1')} `) + chalk.cyan('./scripts/setup-cloud-sync.sh'));
3281
- console.log(chalk.gray(`2. ${t('cloud.sync.setup.step2')}`));
3282
- console.log(chalk.gray(`3. ${t('cloud.sync.setup.step3')}\n`));
3283
- console.log(chalk.gray(`${t('cloud.sync.setup.info')} `) + chalk.cyan('docs/CLOUD_SYNC.md\n'));
3284
-
3285
- console.log(chalk.gray(t('interactive.press.enter.return')));
3286
- await new Promise(resolve => {
3287
- const rl = readline.createInterface({
3288
- input: process.stdin,
3289
- output: process.stdout
3290
- });
3291
- rl.question('', () => {
3292
- rl.close();
3293
- resolve();
3294
- });
3295
- });
3296
- return;
3297
- }
3298
-
3299
- const computerCommands = require('../commands/computers');
3300
- const syncCommands = require('../commands/sync');
3301
-
3302
- const choices = [
3303
- { name: t('cloud.sync.menu.view.computers'), value: 'computers' },
3304
- { name: t('cloud.sync.menu.manage.remote'), value: 'manage-remote' },
3305
- { name: t('cloud.sync.menu.sync.now'), value: 'sync-now' },
3306
- { name: t('cloud.sync.menu.sync.status'), value: 'sync-status' },
3307
- { name: t('cloud.sync.menu.sync.history'), value: 'sync-history' },
3308
- { name: t('cloud.sync.menu.offline.queue'), value: 'sync-queue' },
3309
- { name: t('cloud.sync.menu.register'), value: 'register' },
3310
- { name: t('cloud.sync.menu.update.focus'), value: 'update-focus' },
3311
- { name: chalk.gray(t('interactive.back.to.menu')), value: 'back' }
3312
- ];
3313
-
3314
- const { action } = await inquirer.prompt([
3315
- {
3316
- type: 'list',
3317
- name: 'action',
3318
- message: t('interactive.select.option'),
3319
- choices: choices
3320
- }
3321
- ]);
3322
-
3323
- switch (action) {
3324
- case 'computers':
3325
- try {
3326
- await computerCommands.listComputers();
3327
- } catch (error) {
3328
- console.log(chalk.red('\n✗ Error: ') + error.message);
3329
- console.log(chalk.gray('\nTip: Make sure AWS credentials are configured and DynamoDB tables exist.'));
3330
- }
3331
- console.log(chalk.gray(`\n${t('interactive.press.enter.continue')}`));
3332
- await new Promise(resolve => {
3333
- const rl = readline.createInterface({
3334
- input: process.stdin,
3335
- output: process.stdout
3336
- });
3337
- rl.question('', () => {
3338
- rl.close();
3339
- resolve();
3340
- });
3341
- });
3342
- await showCloudSyncMenu();
3343
- break;
3344
-
3345
- case 'manage-remote':
3346
- try {
3347
- // First, get list of computers
3348
- const SyncEngine = require('vibecodingmachine-core/src/sync/sync-engine');
3349
- const { ScanCommand } = require('@aws-sdk/lib-dynamodb');
3350
-
3351
- const syncEngine = new SyncEngine();
3352
- await syncEngine.initialize();
3353
-
3354
- const tableName = 'vibecodingmachine-computers';
3355
- const command = new ScanCommand({ TableName: tableName });
3356
- const response = await syncEngine.dynamoClient.send(command);
3357
- const computers = response.Items || [];
3358
-
3359
- syncEngine.stop();
3360
-
3361
- if (computers.length === 0) {
3362
- console.log(chalk.yellow(`\n⚠ ${t('computers.no.computers')}\n`));
3363
- } else {
3364
- // Let user select a computer
3365
- const computerChoices = computers.map(c => ({
3366
- name: `${c.hostname || c.computerId} - ${c.focusArea || t('computers.no.focus')}`,
3367
- value: c.computerId
3368
- }));
3369
- computerChoices.push({ name: chalk.gray(t('computers.cancel')), value: null });
3370
-
3371
- const { selectedComputer } = await inquirer.prompt([
3372
- {
3373
- type: 'list',
3374
- name: 'selectedComputer',
3375
- message: t('computers.select.to.manage'),
3376
- choices: computerChoices
3377
- }
3378
- ]);
3379
-
3380
- if (selectedComputer) {
3381
- const remoteReqCommands = require('../commands/requirements-remote');
3382
- await remoteReqCommands.manageRemoteRequirements(selectedComputer);
3383
- }
3384
- }
3385
- } catch (error) {
3386
- console.log(chalk.red('\n✗ Error: ') + error.message);
3387
- }
3388
- await showCloudSyncMenu();
3389
- break;
3390
-
3391
- case 'sync-now':
3392
- try {
3393
- await syncCommands.syncNow();
3394
- } catch (error) {
3395
- console.log(chalk.red('\n✗ Error: ') + error.message);
3396
- }
3397
- console.log(chalk.gray(`\n${t('interactive.press.enter.continue')}`));
3398
- await new Promise(resolve => {
3399
- const rl = readline.createInterface({
3400
- input: process.stdin,
3401
- output: process.stdout
3402
- });
3403
- rl.question('', () => {
3404
- rl.close();
3405
- resolve();
3406
- });
3407
- });
3408
- await showCloudSyncMenu();
3409
- break;
3410
-
3411
- case 'sync-status':
3412
- try {
3413
- await syncCommands.syncStatus();
3414
- } catch (error) {
3415
- console.log(chalk.red('\n✗ Error: ') + error.message);
3416
- }
3417
- console.log(chalk.gray(`\n${t('interactive.press.enter.continue')}`));
3418
- await new Promise(resolve => {
3419
- const rl = readline.createInterface({
3420
- input: process.stdin,
3421
- output: process.stdout
3422
- });
3423
- rl.question('', () => {
3424
- rl.close();
3425
- resolve();
3426
- });
3427
- });
3428
- await showCloudSyncMenu();
3429
- break;
3430
-
3431
- case 'sync-history':
3432
- try {
3433
- await syncCommands.viewHistory({ limit: 50 });
3434
- } catch (error) {
3435
- console.log(chalk.red('\n✗ Error: ') + error.message);
3436
- }
3437
- console.log(chalk.gray(`\n${t('interactive.press.enter.continue')}`));
3438
- await new Promise(resolve => {
3439
- const rl = readline.createInterface({
3440
- input: process.stdin,
3441
- output: process.stdout
3442
- });
3443
- rl.question('', () => {
3444
- rl.close();
3445
- resolve();
3446
- });
3447
- });
3448
- await showCloudSyncMenu();
3449
- break;
3450
-
3451
- case 'sync-queue':
3452
- try {
3453
- await syncCommands.viewQueue();
3454
- } catch (error) {
3455
- console.log(chalk.red('\n✗ Error: ') + error.message);
3456
- }
3457
- console.log(chalk.gray(`\n${t('interactive.press.enter.continue')}`));
3458
- await new Promise(resolve => {
3459
- const rl = readline.createInterface({
3460
- input: process.stdin,
3461
- output: process.stdout
3462
- });
3463
- rl.question('', () => {
3464
- rl.close();
3465
- resolve();
3466
- });
3467
- });
3468
- await showCloudSyncMenu();
3469
- break;
3470
-
3471
- case 'register':
3472
- try {
3473
- const { focusArea } = await inquirer.prompt([
3474
- {
3475
- type: 'input',
3476
- name: 'focusArea',
3477
- message: t('computers.register.focus.prompt'),
3478
- default: t('computers.register.focus.default')
3479
- }
3480
- ]);
3481
- await computerCommands.registerComputer(focusArea);
3482
- } catch (error) {
3483
- console.log(chalk.red('\n✗ Error: ') + error.message);
3484
- }
3485
- console.log(chalk.gray(`\n${t('interactive.press.enter.continue')}`));
3486
- await new Promise(resolve => {
3487
- const rl = readline.createInterface({
3488
- input: process.stdin,
3489
- output: process.stdout
3490
- });
3491
- rl.question('', () => {
3492
- rl.close();
3493
- resolve();
3494
- });
3495
- });
3496
- await showCloudSyncMenu();
3497
- break;
3498
-
3499
- case 'update-focus':
3500
- try {
3501
- const { newFocus } = await inquirer.prompt([
3502
- {
3503
- type: 'input',
3504
- name: 'newFocus',
3505
- message: 'Enter new focus area:'
3506
- }
3507
- ]);
3508
- if (newFocus) {
3509
- await computerCommands.updateFocus(newFocus);
3510
- }
3511
- } catch (error) {
3512
- console.log(chalk.red('\n✗ Error: ') + error.message);
3513
- }
3514
- console.log(chalk.gray(`\n${t('interactive.press.enter.continue')}`));
3515
- await new Promise(resolve => {
3516
- const rl = readline.createInterface({
3517
- input: process.stdin,
3518
- output: process.stdout
3519
- });
3520
- rl.question('', () => {
3521
- rl.close();
3522
- resolve();
3523
- });
3524
- });
3525
- await showCloudSyncMenu();
3526
- break;
3527
-
3528
- case 'back':
3529
- // Return to main menu
3530
- break;
3531
- }
3532
- }
3533
-
3534
-
3535
- module.exports = {
3536
- startInteractive,
3537
- bootstrapProjectIfInHomeDir,
3538
- showProviderManagerMenu,
3539
- translateStage,
3540
- normalizeProjectDirName
3541
- };