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