vibecodingmachine-cli 2026.1.29-713 → 2026.2.20-423
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/vibecodingmachine.js +124 -0
- package/package.json +3 -2
- package/src/commands/agents-check.js +69 -0
- package/src/commands/auto-direct.js +930 -145
- package/src/commands/auto.js +26 -4
- package/src/commands/ide.js +2 -1
- package/src/commands/requirements.js +23 -27
- package/src/utils/auto-mode.js +4 -1
- package/src/utils/cline-js-handler.js +218 -0
- package/src/utils/config.js +22 -0
- package/src/utils/display-formatters-complete.js +229 -0
- package/src/utils/display-formatters-extracted.js +219 -0
- package/src/utils/display-formatters.js +157 -0
- package/src/utils/feedback-handler.js +143 -0
- package/src/utils/ide-detection-complete.js +126 -0
- package/src/utils/ide-detection-extracted.js +116 -0
- package/src/utils/ide-detection.js +124 -0
- package/src/utils/interactive-backup.js +5664 -0
- package/src/utils/interactive-broken.js +280 -0
- package/src/utils/interactive.js +31 -5534
- package/src/utils/provider-checker.js +410 -0
- package/src/utils/provider-manager.js +251 -0
- package/src/utils/provider-registry.js +18 -9
- package/src/utils/requirement-actions.js +884 -0
- package/src/utils/requirements-navigator.js +585 -0
- package/src/utils/rui-trui-adapter.js +311 -0
- package/src/utils/simple-trui.js +204 -0
- package/src/utils/status-helpers-extracted.js +125 -0
- package/src/utils/status-helpers.js +107 -0
- package/src/utils/trui-debug.js +261 -0
- package/src/utils/trui-feedback.js +133 -0
- package/src/utils/trui-nav-agents.js +119 -0
- package/src/utils/trui-nav-requirements.js +268 -0
- package/src/utils/trui-nav-settings.js +157 -0
- package/src/utils/trui-nav-specifications.js +139 -0
- package/src/utils/trui-navigation.js +303 -0
- package/src/utils/trui-provider-manager.js +182 -0
- package/src/utils/trui-quick-menu.js +365 -0
- package/src/utils/trui-req-actions.js +372 -0
- package/src/utils/trui-req-tree.js +534 -0
- package/src/utils/trui-specifications.js +359 -0
- package/src/utils/trui-text-editor.js +350 -0
- package/src/utils/trui-windsurf.js +336 -0
- package/src/utils/welcome-screen-extracted.js +135 -0
- package/src/utils/welcome-screen.js +134 -0
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { spawn, execSync } = require('child_process');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const chokidar = require('chokidar');
|
|
7
|
+
|
|
8
|
+
const CLI_ENTRY_POINT = path.join(__dirname, '../../bin/vibecodingmachine.js');
|
|
9
|
+
|
|
10
|
+
// Timeout for direct LLM round-trips via auto:direct (ms)
|
|
11
|
+
const DIRECT_TIMEOUT_MS = 60000;
|
|
12
|
+
// Timeout for IDE automation round-trips via auto:start (ms)
|
|
13
|
+
const IDE_TIMEOUT_MS = 90000;
|
|
14
|
+
|
|
15
|
+
// Direct CLI providers that can be auto-installed via npm
|
|
16
|
+
const CLI_AUTO_INSTALL = {
|
|
17
|
+
'cline': { cmd: 'cline', pkg: '@cline/cli' },
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Check if a CLI command is available in PATH.
|
|
22
|
+
*/
|
|
23
|
+
function isCLIAvailable(cmd) {
|
|
24
|
+
try {
|
|
25
|
+
execSync(`which "${cmd}"`, { stdio: 'ignore' });
|
|
26
|
+
return true;
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Install a CLI tool globally via npm and wait for it to complete.
|
|
34
|
+
* Returns { installed: bool, note: string }.
|
|
35
|
+
*/
|
|
36
|
+
async function installCLI(pkg, cmd) {
|
|
37
|
+
return new Promise((resolve) => {
|
|
38
|
+
const proc = spawn('npm', ['install', '-g', pkg], { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
39
|
+
let out = '';
|
|
40
|
+
proc.stdout.on('data', d => { out += d.toString(); });
|
|
41
|
+
proc.stderr.on('data', d => { out += d.toString(); });
|
|
42
|
+
proc.on('error', () => resolve({ installed: false, note: `Failed to run npm install -g ${pkg}` }));
|
|
43
|
+
proc.on('close', (code) => {
|
|
44
|
+
if (code === 0 && isCLIAvailable(cmd)) {
|
|
45
|
+
resolve({ installed: true, note: `Installed ${pkg} via npm` });
|
|
46
|
+
} else {
|
|
47
|
+
resolve({ installed: false, note: `npm install -g ${pkg} exited (${code})` });
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Map provider id → { process: macOS process name, app: macOS app name for `open -a` }
|
|
54
|
+
const IDE_INFO = {
|
|
55
|
+
windsurf: { process: 'Windsurf', app: 'Windsurf' },
|
|
56
|
+
cursor: { process: 'Cursor', app: 'Cursor' },
|
|
57
|
+
antigravity: { process: 'Antigravity', app: 'Antigravity' },
|
|
58
|
+
kiro: { process: 'Code', app: 'Kiro' },
|
|
59
|
+
'github-copilot': { process: 'Code', app: 'Visual Studio Code' },
|
|
60
|
+
'amazon-q': { process: 'Code', app: 'Visual Studio Code' },
|
|
61
|
+
cline: { process: 'Code', app: 'Visual Studio Code' },
|
|
62
|
+
vscode: { process: 'Code', app: 'Visual Studio Code' },
|
|
63
|
+
replit: { process: null, app: null }, // web-based
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Returns true if the IDE process is currently running.
|
|
68
|
+
*/
|
|
69
|
+
function isIDERunning(processName) {
|
|
70
|
+
if (!processName || process.platform !== 'darwin') return true; // assume running on non-mac
|
|
71
|
+
try {
|
|
72
|
+
execSync(`pgrep -x "${processName}"`, { stdio: 'ignore' });
|
|
73
|
+
return true;
|
|
74
|
+
} catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Open the IDE using `open -a <app> <repoPath>` (macOS).
|
|
81
|
+
* Returns true when the launch command was sent.
|
|
82
|
+
*/
|
|
83
|
+
async function openIDEApp(appName, repoPath) {
|
|
84
|
+
if (!appName || process.platform !== 'darwin') return false;
|
|
85
|
+
try {
|
|
86
|
+
const safeRepo = repoPath ? ` "${repoPath.replace(/"/g, '\\"')}"` : '';
|
|
87
|
+
execSync(`open -a "${appName}"${safeRepo}`, { stdio: 'ignore' });
|
|
88
|
+
// Give it a few seconds to start before the automation begins
|
|
89
|
+
await new Promise(r => setTimeout(r, 5000));
|
|
90
|
+
return true;
|
|
91
|
+
} catch {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const TEST_REQ_TITLE = 'VCM agent connectivity check';
|
|
97
|
+
const PENDING_HEADER = '## ⏳ Requirements not yet completed';
|
|
98
|
+
const VERIFIED_HEADER = '## ✅ Verified by AI';
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Return the absolute path to the result file used by the check.
|
|
102
|
+
*/
|
|
103
|
+
function getResultFilePath(repoPath) {
|
|
104
|
+
return path.join(repoPath, '.vibecodingmachine', 'temp', 'TEMP_agent_check.txt');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Add the test requirement at the TOP of the pending section.
|
|
109
|
+
* Idempotent — if it's already there, do nothing.
|
|
110
|
+
*/
|
|
111
|
+
function addTestRequirement(reqPath, resultFile) {
|
|
112
|
+
let content = '';
|
|
113
|
+
try { content = fs.readFileSync(reqPath, 'utf8'); } catch {}
|
|
114
|
+
|
|
115
|
+
// Already present?
|
|
116
|
+
if (content.includes(TEST_REQ_TITLE)) return;
|
|
117
|
+
|
|
118
|
+
const testBlock = `### 1. ${TEST_REQ_TITLE}
|
|
119
|
+
🚦 Current Status: PREPARE
|
|
120
|
+
Create a new file at the following absolute path: ${resultFile}
|
|
121
|
+
The file must contain exactly this text on the first line: VCM_CHECK_OK
|
|
122
|
+
Do not modify any other files. This is an automated connectivity test.
|
|
123
|
+
|
|
124
|
+
`;
|
|
125
|
+
|
|
126
|
+
const idx = content.indexOf(PENDING_HEADER);
|
|
127
|
+
if (idx !== -1) {
|
|
128
|
+
const afterHeader = idx + PENDING_HEADER.length;
|
|
129
|
+
// Insert after the header, then existing content follows
|
|
130
|
+
const before = content.slice(0, afterHeader);
|
|
131
|
+
const after = content.slice(afterHeader).replace(/^\n+/, '');
|
|
132
|
+
content = before + '\n\n' + testBlock + after;
|
|
133
|
+
} else {
|
|
134
|
+
// No pending section — prepend one
|
|
135
|
+
content = PENDING_HEADER + '\n\n' + testBlock + '\n' + (content || VERIFIED_HEADER + '\n');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
fs.writeFileSync(reqPath, content, 'utf8');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Remove the test requirement from REQUIREMENTS.md regardless of which section it's in.
|
|
143
|
+
* Also cleans up triple-blank lines left behind.
|
|
144
|
+
*/
|
|
145
|
+
function removeTestRequirement(reqPath) {
|
|
146
|
+
let content = '';
|
|
147
|
+
try { content = fs.readFileSync(reqPath, 'utf8'); } catch { return; }
|
|
148
|
+
|
|
149
|
+
if (!content.includes(TEST_REQ_TITLE)) return;
|
|
150
|
+
|
|
151
|
+
const lines = content.split('\n');
|
|
152
|
+
|
|
153
|
+
// Find the ### header line for this requirement
|
|
154
|
+
let blockStart = -1;
|
|
155
|
+
for (let i = 0; i < lines.length; i++) {
|
|
156
|
+
if (lines[i].includes(TEST_REQ_TITLE) || (lines[i].startsWith('###') && i + 1 < lines.length && lines[i + 1].includes(TEST_REQ_TITLE))) {
|
|
157
|
+
// Walk back to find the ### line
|
|
158
|
+
blockStart = lines[i].startsWith('###') ? i : i - 1;
|
|
159
|
+
if (blockStart < 0) blockStart = i;
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (blockStart === -1) return;
|
|
165
|
+
|
|
166
|
+
// Find end of block (next ### or ##, or EOF)
|
|
167
|
+
let blockEnd = blockStart + 1;
|
|
168
|
+
while (blockEnd < lines.length && !lines[blockEnd].startsWith('###') && !lines[blockEnd].startsWith('## ')) {
|
|
169
|
+
blockEnd++;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
lines.splice(blockStart, blockEnd - blockStart);
|
|
173
|
+
const cleaned = lines.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd() + '\n';
|
|
174
|
+
fs.writeFileSync(reqPath, cleaned, 'utf8');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Spawn a process and wait for it to exit. Returns { code, output }.
|
|
179
|
+
*/
|
|
180
|
+
function spawnAndWait(cmd, args, opts = {}) {
|
|
181
|
+
return new Promise((resolve) => {
|
|
182
|
+
let output = '';
|
|
183
|
+
const child = spawn(cmd, args, { stdio: ['ignore', 'pipe', 'pipe'], ...opts });
|
|
184
|
+
child.stdout && child.stdout.on('data', d => { output += d.toString(); });
|
|
185
|
+
child.stderr && child.stderr.on('data', d => { output += d.toString(); });
|
|
186
|
+
child.on('error', () => resolve({ code: -1, output }));
|
|
187
|
+
child.on('exit', (code) => resolve({ code, output }));
|
|
188
|
+
return child;
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Run an agent for 1 iteration and watch for the result file to be written.
|
|
194
|
+
* For IDE providers: ensure the IDE is open first, launching it if needed.
|
|
195
|
+
* Returns { status, message, checkedAt }.
|
|
196
|
+
*/
|
|
197
|
+
async function runAgentCheck(providerId, def, repoPath, resultFile, timeoutMs, signal = null) {
|
|
198
|
+
const checkedAt = new Date().toISOString();
|
|
199
|
+
|
|
200
|
+
// Ensure temp dir exists
|
|
201
|
+
fs.mkdirSync(path.dirname(resultFile), { recursive: true });
|
|
202
|
+
// Remove stale result file so we detect a fresh write
|
|
203
|
+
try { fs.unlinkSync(resultFile); } catch {}
|
|
204
|
+
|
|
205
|
+
// For IDE providers: verify the IDE is running, open it if not
|
|
206
|
+
let ideLaunchNote = '';
|
|
207
|
+
if (def.type === 'ide') {
|
|
208
|
+
const info = IDE_INFO[providerId] || IDE_INFO[def.ide] || null;
|
|
209
|
+
if (info && info.process) {
|
|
210
|
+
if (!isIDERunning(info.process)) {
|
|
211
|
+
if (info.app) {
|
|
212
|
+
await openIDEApp(info.app, repoPath);
|
|
213
|
+
ideLaunchNote = ` (VCM launched ${info.app})`;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return new Promise((resolve) => {
|
|
220
|
+
let resolved = false;
|
|
221
|
+
let child = null;
|
|
222
|
+
|
|
223
|
+
function done(result) {
|
|
224
|
+
if (resolved) return;
|
|
225
|
+
resolved = true;
|
|
226
|
+
try { watcher.close(); } catch {}
|
|
227
|
+
clearTimeout(timeout);
|
|
228
|
+
clearInterval(cancelCheckInterval);
|
|
229
|
+
try { if (child) child.kill(); } catch {}
|
|
230
|
+
resolve(result);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function checkResult() {
|
|
234
|
+
if (!resolved && fs.existsSync(resultFile)) {
|
|
235
|
+
try {
|
|
236
|
+
const content = fs.readFileSync(resultFile, 'utf8');
|
|
237
|
+
if (content.includes('VCM_CHECK_OK')) {
|
|
238
|
+
done({ status: 'success', message: 'End-to-end check passed', checkedAt });
|
|
239
|
+
}
|
|
240
|
+
} catch {}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const watcher = chokidar.watch(resultFile, { persistent: true, ignoreInitial: false, disableGlobbing: true });
|
|
245
|
+
watcher.on('add', checkResult);
|
|
246
|
+
watcher.on('change', checkResult);
|
|
247
|
+
|
|
248
|
+
const timeout = setTimeout(() => {
|
|
249
|
+
// Diagnose: is the IDE process still running?
|
|
250
|
+
let diagnosis = '';
|
|
251
|
+
if (def.type === 'ide') {
|
|
252
|
+
const info = IDE_INFO[providerId] || IDE_INFO[def.ide] || null;
|
|
253
|
+
if (info && info.process) {
|
|
254
|
+
diagnosis = isIDERunning(info.process)
|
|
255
|
+
? ` — ${info.app || info.process} is running but did not respond`
|
|
256
|
+
: ` — ${info.app || info.process} is not running`;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
done({ status: 'error', message: `No response within ${timeoutMs / 1000}s${ideLaunchNote}${diagnosis}`, checkedAt });
|
|
260
|
+
}, timeoutMs);
|
|
261
|
+
|
|
262
|
+
// Periodically check for cancellation signal
|
|
263
|
+
const cancelCheckInterval = setInterval(() => {
|
|
264
|
+
if (signal && signal.cancelled) {
|
|
265
|
+
done({ status: 'error', message: 'Check cancelled by user', checkedAt });
|
|
266
|
+
}
|
|
267
|
+
}, 1000); // Check every second
|
|
268
|
+
|
|
269
|
+
let args;
|
|
270
|
+
if (def.type === 'ide') {
|
|
271
|
+
args = [CLI_ENTRY_POINT, 'auto:start', '--ide', def.ide || providerId, '--max-chats', '1'];
|
|
272
|
+
if (def.defaultModel) args.push('--ide-model', String(def.defaultModel));
|
|
273
|
+
if (def.extension) args.push('--extension', String(def.extension));
|
|
274
|
+
} else {
|
|
275
|
+
// Direct LLM: use auto:direct --provider to force this specific provider
|
|
276
|
+
args = [CLI_ENTRY_POINT, 'auto:direct', '--provider', providerId, '--max-chats', '1'];
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
child = spawn(process.execPath, args, {
|
|
280
|
+
cwd: repoPath,
|
|
281
|
+
env: process.env,
|
|
282
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
let output = '';
|
|
286
|
+
child.stdout.on('data', d => { output += d.toString(); });
|
|
287
|
+
child.stderr.on('data', d => { output += d.toString(); });
|
|
288
|
+
|
|
289
|
+
child.on('error', (err) => {
|
|
290
|
+
done({ status: 'error', message: `Failed to start automation: ${err.message}`, checkedAt });
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
child.on('exit', (code) => {
|
|
294
|
+
if (resolved) return;
|
|
295
|
+
// Check one more time after process exits
|
|
296
|
+
checkResult();
|
|
297
|
+
if (!resolved) {
|
|
298
|
+
const tail = output.slice(-300).trim().replace(/\n/g, ' ');
|
|
299
|
+
done({ status: 'error', message: `Automation exited (code ${code})${tail ? ': ' + tail : ''}`, checkedAt });
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Check a single provider end-to-end using the actual REQUIREMENTS.md.
|
|
307
|
+
*
|
|
308
|
+
* Flow:
|
|
309
|
+
* 1. Add test requirement to top of REQUIREMENTS.md
|
|
310
|
+
* 2. Run the agent for 1 iteration
|
|
311
|
+
* 3. On success, requirement is auto-moved to Verified by the agent; remove it
|
|
312
|
+
* 4. On failure, leave requirement in pending for the NEXT agent (caller handles cleanup)
|
|
313
|
+
*
|
|
314
|
+
* Returns { status, message, checkedAt, requirementLeftPending }
|
|
315
|
+
*/
|
|
316
|
+
async function checkProvider(providerId, config = {}, repoPath, onProgress = null, signal = null) {
|
|
317
|
+
const { getProviderDefinition } = require('./provider-registry');
|
|
318
|
+
const def = getProviderDefinition(providerId);
|
|
319
|
+
if (!def) {
|
|
320
|
+
return { status: 'error', message: `Unknown provider: ${providerId}`, checkedAt: new Date().toISOString(), requirementLeftPending: false };
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (!repoPath) {
|
|
324
|
+
return { status: 'error', message: 'No repository path available', checkedAt: new Date().toISOString(), requirementLeftPending: false };
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const { getRequirementsPath } = require('vibecodingmachine-core');
|
|
328
|
+
let reqPath;
|
|
329
|
+
try {
|
|
330
|
+
reqPath = await getRequirementsPath(repoPath);
|
|
331
|
+
} catch {
|
|
332
|
+
reqPath = path.join(repoPath, '.vibecodingmachine', 'REQUIREMENTS.md');
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const resultFile = getResultFilePath(repoPath);
|
|
336
|
+
|
|
337
|
+
// For direct CLI providers: auto-install if missing
|
|
338
|
+
const cliInfo = CLI_AUTO_INSTALL[providerId];
|
|
339
|
+
if (cliInfo && !isCLIAvailable(cliInfo.cmd)) {
|
|
340
|
+
if (onProgress) onProgress(providerId, 'installing');
|
|
341
|
+
const { installed, note } = await installCLI(cliInfo.pkg, cliInfo.cmd);
|
|
342
|
+
if (!installed) {
|
|
343
|
+
return { status: 'error', message: `${def.name} not installed and auto-install failed: ${note}`, checkedAt: new Date().toISOString(), requirementLeftPending: false };
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Add test requirement (idempotent — safe to call even if already present from a prior failed agent)
|
|
348
|
+
try { addTestRequirement(reqPath, resultFile); } catch (err) {
|
|
349
|
+
return { status: 'error', message: `Could not write to REQUIREMENTS.md: ${err.message}`, checkedAt: new Date().toISOString(), requirementLeftPending: false };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const timeoutMs = def.type === 'ide' ? IDE_TIMEOUT_MS : DIRECT_TIMEOUT_MS;
|
|
353
|
+
const result = await runAgentCheck(providerId, def, repoPath, resultFile, timeoutMs, signal);
|
|
354
|
+
|
|
355
|
+
if (result.status === 'success') {
|
|
356
|
+
// Agent completed — requirement has been moved to Verified; remove it and the result file
|
|
357
|
+
try { removeTestRequirement(reqPath); } catch {}
|
|
358
|
+
try { fs.unlinkSync(resultFile); } catch {}
|
|
359
|
+
return { ...result, requirementLeftPending: false };
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Agent failed — leave the requirement in pending for the next agent
|
|
363
|
+
return { ...result, requirementLeftPending: true };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Check all providers sequentially.
|
|
368
|
+
* The test requirement is shared: it stays pending between agents until one
|
|
369
|
+
* succeeds, then it's removed. If all fail, it's removed at the end.
|
|
370
|
+
*
|
|
371
|
+
* Returns { [providerId]: checkResult }
|
|
372
|
+
*/
|
|
373
|
+
async function checkAllProviders(providerIds, config = {}, repoPath, onProgress = null, signal = null) {
|
|
374
|
+
const results = {};
|
|
375
|
+
|
|
376
|
+
for (const id of providerIds) {
|
|
377
|
+
if (signal && signal.cancelled) break;
|
|
378
|
+
if (onProgress) onProgress(id, 'checking');
|
|
379
|
+
results[id] = await checkProvider(id, config, repoPath, onProgress, signal);
|
|
380
|
+
if (onProgress) onProgress(id, 'done', results[id]);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Final cleanup — remove requirement if still pending (all agents failed or skipped)
|
|
384
|
+
if (repoPath) {
|
|
385
|
+
const { getRequirementsPath } = require('vibecodingmachine-core');
|
|
386
|
+
let reqPath;
|
|
387
|
+
try { reqPath = await getRequirementsPath(repoPath); } catch {
|
|
388
|
+
reqPath = path.join(repoPath, '.vibecodingmachine', 'REQUIREMENTS.md');
|
|
389
|
+
}
|
|
390
|
+
try { removeTestRequirement(reqPath); } catch {}
|
|
391
|
+
try { fs.unlinkSync(getResultFilePath(repoPath)); } catch {}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return results;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Format a checkedAt ISO timestamp for display.
|
|
399
|
+
* e.g. "April 12, 2025 at 4:23 pm"
|
|
400
|
+
*/
|
|
401
|
+
function formatCheckedAt(checkedAt) {
|
|
402
|
+
if (!checkedAt) return '';
|
|
403
|
+
const d = new Date(checkedAt);
|
|
404
|
+
if (isNaN(d.getTime())) return checkedAt;
|
|
405
|
+
const months = ['January','February','March','April','May','June','July','August','September','October','November','December'];
|
|
406
|
+
const timePart = d.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true }).toLowerCase();
|
|
407
|
+
return `${months[d.getMonth()]} ${d.getDate()}, ${d.getFullYear()} at ${timePart}`;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
module.exports = { checkProvider, checkAllProviders, formatCheckedAt, DIRECT_TIMEOUT_MS, IDE_TIMEOUT_MS };
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const readline = require('readline');
|
|
3
|
+
const { t } = require('vibecodingmachine-core');
|
|
4
|
+
const { getProviderDefinitions, getProviderPreferences, saveProviderPreferences, getProviderCache, setProviderCache } = require('../utils/provider-registry');
|
|
5
|
+
const { checkVSCodeExtension, checkAppOrBinary } = require('./ide-detection');
|
|
6
|
+
const { formatIDEName, getAgentDisplayName } = require('./display-formatters');
|
|
7
|
+
const { IDEHealthTracker } = require('vibecodingmachine-core');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Show provider manager menu for IDE and AI provider configuration
|
|
11
|
+
*/
|
|
12
|
+
async function showProviderManagerMenu() {
|
|
13
|
+
if (process.env.VCM_DEBUG_MENU === '1') {
|
|
14
|
+
const msg = `[DEBUG-MENU ${new Date().toISOString()}] showProviderManagerMenu() called\n` + new Error().stack.split('\n').slice(1, 6).join('\n') + '\n';
|
|
15
|
+
console.log(msg);
|
|
16
|
+
try { require('fs').appendFileSync('/tmp/vcm-menu-debug.log', msg); } catch (e) { }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const definitions = getProviderDefinitions();
|
|
20
|
+
const defMap = new Map(definitions.map(def => [def.id, def]));
|
|
21
|
+
const prefs = await getProviderPreferences();
|
|
22
|
+
let order = prefs.order.slice();
|
|
23
|
+
let enabled = { ...prefs.enabled };
|
|
24
|
+
let selectedIndex = 0;
|
|
25
|
+
let dirty = false;
|
|
26
|
+
|
|
27
|
+
const { fetchQuotaForAgent } = require('vibecodingmachine-core/src/quota-management');
|
|
28
|
+
const ProviderManager = require('vibecodingmachine-core/src/ide-integration/provider-manager.cjs');
|
|
29
|
+
const providerManager = new ProviderManager();
|
|
30
|
+
|
|
31
|
+
const debugQuota = process.env.VCM_DEBUG_QUOTA === '1' || process.env.VCM_DEBUG_QUOTA === 'true';
|
|
32
|
+
|
|
33
|
+
const formatDuration = (ms) => {
|
|
34
|
+
if (!ms || ms <= 0) return 'now';
|
|
35
|
+
const totalSeconds = Math.ceil(ms / 1000);
|
|
36
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
37
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
38
|
+
const seconds = totalSeconds % 60;
|
|
39
|
+
|
|
40
|
+
if (hours > 0) return `${hours}h ${minutes}m`;
|
|
41
|
+
if (minutes > 0) return `${minutes}m ${seconds}s`;
|
|
42
|
+
return `${seconds}s`;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const formatTimeAmPm = (date) => {
|
|
46
|
+
const hours24 = date.getHours();
|
|
47
|
+
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
48
|
+
const ampm = hours24 >= 12 ? 'pm' : 'am';
|
|
49
|
+
const hours12 = (hours24 % 12) || 12;
|
|
50
|
+
return `${hours12}:${minutes} ${ampm}`;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// Pre-fetch data to avoid lag in render loop
|
|
54
|
+
const sharedAuth = require('vibecodingmachine-core/src/auth/shared-auth-storage');
|
|
55
|
+
|
|
56
|
+
// Initialize caches from persistent storage
|
|
57
|
+
const savedCache = await getProviderCache();
|
|
58
|
+
|
|
59
|
+
// Hydrate local maps from saved cache
|
|
60
|
+
const installationStatus = new Map();
|
|
61
|
+
const agentQuotas = new Map();
|
|
62
|
+
|
|
63
|
+
Object.keys(savedCache).forEach(id => {
|
|
64
|
+
if (savedCache[id].installed !== undefined) {
|
|
65
|
+
installationStatus.set(id, savedCache[id].installed);
|
|
66
|
+
}
|
|
67
|
+
if (savedCache[id].quota) {
|
|
68
|
+
agentQuotas.set(id, savedCache[id].quota);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Also prefill agent quotas from the ProviderManager rate-limit file
|
|
73
|
+
try {
|
|
74
|
+
const { getProviderRateLimitedQuotas } = require('./provider-rate-cache');
|
|
75
|
+
const prefetched = getProviderRateLimitedQuotas(definitions);
|
|
76
|
+
for (const [id, q] of prefetched) {
|
|
77
|
+
const existing = agentQuotas.get(id);
|
|
78
|
+
if (!existing || existing.type !== 'rate-limit') {
|
|
79
|
+
agentQuotas.set(id, q);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
} catch (e) {
|
|
83
|
+
// Ignore — this is a non-critical optimization
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Default values while loading
|
|
87
|
+
let quotaInfo = { maxIterations: 10, todayUsage: 0 };
|
|
88
|
+
let autoConfig = {};
|
|
89
|
+
let isMenuActive = true;
|
|
90
|
+
|
|
91
|
+
// Load health metrics for all IDEs
|
|
92
|
+
let healthMetricsMap = new Map();
|
|
93
|
+
try {
|
|
94
|
+
const healthTracker = new IDEHealthTracker();
|
|
95
|
+
await healthTracker.load();
|
|
96
|
+
healthMetricsMap = await healthTracker.getAllHealthMetrics();
|
|
97
|
+
} catch (error) {
|
|
98
|
+
// Silently ignore health loading errors
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const render = async () => {
|
|
102
|
+
if (!isMenuActive) return;
|
|
103
|
+
process.stdout.write('\x1Bc');
|
|
104
|
+
console.log(chalk.bold.cyan('⚙ ' + t('provider.title') + '\n'));
|
|
105
|
+
|
|
106
|
+
// Header
|
|
107
|
+
console.log(chalk.gray('Use ↑↓ to move, Space to toggle, → to configure, X/ESC to exit\n'));
|
|
108
|
+
|
|
109
|
+
// Display providers
|
|
110
|
+
order.forEach((id, index) => {
|
|
111
|
+
const def = defMap.get(id);
|
|
112
|
+
if (!def) return;
|
|
113
|
+
|
|
114
|
+
const isSelected = index === selectedIndex;
|
|
115
|
+
const isEnabled = enabled[id];
|
|
116
|
+
const isInstalled = installationStatus.get(id) || false;
|
|
117
|
+
|
|
118
|
+
let statusIcon = '⚪';
|
|
119
|
+
let statusColor = chalk.gray;
|
|
120
|
+
let statusText = 'Not installed';
|
|
121
|
+
|
|
122
|
+
if (isInstalled) {
|
|
123
|
+
statusIcon = isEnabled ? '🟢' : '🔴';
|
|
124
|
+
statusColor = isEnabled ? chalk.green : chalk.red;
|
|
125
|
+
statusText = isEnabled ? 'Enabled' : 'Disabled';
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const prefix = isSelected ? chalk.cyan('❯ ') : ' ';
|
|
129
|
+
const name = getAgentDisplayName(id);
|
|
130
|
+
const line = `${prefix}${statusIcon} ${statusColor(name)} ${chalk.gray(`- ${statusText}`)}`;
|
|
131
|
+
|
|
132
|
+
console.log(line);
|
|
133
|
+
|
|
134
|
+
if (isSelected && def) {
|
|
135
|
+
// Show details for selected provider
|
|
136
|
+
console.log(chalk.gray(`\n ${def.description || 'No description available'}`));
|
|
137
|
+
|
|
138
|
+
// Show quota information
|
|
139
|
+
const quota = agentQuotas.get(id);
|
|
140
|
+
if (quota) {
|
|
141
|
+
console.log(chalk.gray(` Quota: ${quota.type === 'rate-limit' ? 'Rate limited' : `${quota.used || 0}/${quota.max || 'unlimited'}`}`));
|
|
142
|
+
if (quota.type === 'rate-limit' && quota.resetAt) {
|
|
143
|
+
const resetTime = new Date(quota.resetAt);
|
|
144
|
+
console.log(chalk.gray(` Resets at: ${formatTimeAmPm(resetTime)} (${formatDuration(resetTime - Date.now())})`));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Show health metrics if available
|
|
149
|
+
const health = healthMetricsMap.get(id);
|
|
150
|
+
if (health) {
|
|
151
|
+
const healthColor = health.score >= 80 ? chalk.green : health.score >= 50 ? chalk.yellow : chalk.red;
|
|
152
|
+
console.log(chalk.gray(` Health: ${healthColor(health.score + '%')}`));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Show configuration options
|
|
156
|
+
if (def.configOptions && def.configOptions.length > 0) {
|
|
157
|
+
console.log(chalk.gray(`\n Configuration options:`));
|
|
158
|
+
def.configOptions.forEach(option => {
|
|
159
|
+
console.log(chalk.gray(` • ${option.name}: ${option.description || 'No description'}`));
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
console.log();
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const handleKeypress = async (str, key) => {
|
|
169
|
+
if (!isMenuActive) return;
|
|
170
|
+
|
|
171
|
+
if (key.ctrl && key.name === 'c') {
|
|
172
|
+
process.exit(0);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
switch (key.name) {
|
|
177
|
+
case 'escape':
|
|
178
|
+
case 'x':
|
|
179
|
+
isMenuActive = false;
|
|
180
|
+
if (dirty) {
|
|
181
|
+
await saveProviderPreferences({ order, enabled });
|
|
182
|
+
await setProviderCache(Object.fromEntries(installationStatus));
|
|
183
|
+
}
|
|
184
|
+
return;
|
|
185
|
+
|
|
186
|
+
case 'up':
|
|
187
|
+
selectedIndex = Math.max(0, selectedIndex - 1);
|
|
188
|
+
break;
|
|
189
|
+
|
|
190
|
+
case 'down':
|
|
191
|
+
selectedIndex = Math.min(order.length - 1, selectedIndex + 1);
|
|
192
|
+
break;
|
|
193
|
+
|
|
194
|
+
case 'space':
|
|
195
|
+
case 'return':
|
|
196
|
+
const currentId = order[selectedIndex];
|
|
197
|
+
enabled[currentId] = !enabled[currentId];
|
|
198
|
+
dirty = true;
|
|
199
|
+
break;
|
|
200
|
+
|
|
201
|
+
case 'right':
|
|
202
|
+
// Configure selected provider
|
|
203
|
+
const selectedId = order[selectedIndex];
|
|
204
|
+
const def = defMap.get(selectedId);
|
|
205
|
+
if (def && def.configOptions && def.configOptions.length > 0) {
|
|
206
|
+
console.log(chalk.cyan(`\nConfiguring ${getAgentDisplayName(selectedId)}...\n`));
|
|
207
|
+
// Here you would implement configuration logic
|
|
208
|
+
console.log(chalk.gray('Configuration not yet implemented in CLI'));
|
|
209
|
+
await new Promise(resolve => {
|
|
210
|
+
process.stdin.once('keypress', () => resolve());
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
break;
|
|
214
|
+
|
|
215
|
+
default:
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
await render();
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
// Initial render
|
|
223
|
+
await render();
|
|
224
|
+
|
|
225
|
+
// Set up keypress handling
|
|
226
|
+
readline.emitKeypressEvents(process.stdin);
|
|
227
|
+
if (process.stdin.isTTY) {
|
|
228
|
+
process.stdin.setRawMode(true);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
process.stdin.on('keypress', handleKeypress);
|
|
232
|
+
process.stdin.resume();
|
|
233
|
+
|
|
234
|
+
// Wait for menu to close
|
|
235
|
+
return new Promise((resolve) => {
|
|
236
|
+
const checkInterval = setInterval(() => {
|
|
237
|
+
if (!isMenuActive) {
|
|
238
|
+
clearInterval(checkInterval);
|
|
239
|
+
process.stdin.removeListener('keypress', handleKeypress);
|
|
240
|
+
if (process.stdin.isTTY) {
|
|
241
|
+
process.stdin.setRawMode(false);
|
|
242
|
+
}
|
|
243
|
+
resolve();
|
|
244
|
+
}
|
|
245
|
+
}, 100);
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
module.exports = {
|
|
250
|
+
showProviderManagerMenu
|
|
251
|
+
};
|