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