@nclamvn/vibecode-cli 1.3.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.vibecode/learning/fixes.json +1 -0
- package/.vibecode/learning/preferences.json +1 -0
- package/README.md +310 -49
- package/bin/vibecode.js +103 -2
- package/package.json +4 -2
- package/src/agent/decomposition.js +476 -0
- package/src/agent/index.js +325 -0
- package/src/agent/memory.js +542 -0
- package/src/agent/orchestrator.js +713 -0
- package/src/agent/self-healing.js +516 -0
- package/src/commands/agent.js +255 -0
- package/src/commands/assist.js +413 -0
- package/src/commands/build.js +13 -3
- package/src/commands/debug.js +457 -0
- package/src/commands/go.js +387 -0
- package/src/commands/learn.js +294 -0
- package/src/commands/undo.js +281 -0
- package/src/commands/wizard.js +322 -0
- package/src/core/backup.js +325 -0
- package/src/core/learning.js +295 -0
- package/src/core/test-runner.js +38 -5
- package/src/debug/analyzer.js +329 -0
- package/src/debug/evidence.js +228 -0
- package/src/debug/fixer.js +348 -0
- package/src/debug/index.js +378 -0
- package/src/debug/verifier.js +346 -0
- package/src/index.js +62 -0
- package/src/ui/__tests__/error-translator.test.js +390 -0
- package/src/ui/dashboard.js +364 -0
- package/src/ui/error-translator.js +775 -0
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
2
|
+
// VIBECODE CLI - Magic Mode (go command)
|
|
3
|
+
// One command = Full workflow
|
|
4
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
5
|
+
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import ora from 'ora';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import fs from 'fs-extra';
|
|
10
|
+
import { exec } from 'child_process';
|
|
11
|
+
import { promisify } from 'util';
|
|
12
|
+
|
|
13
|
+
import { createWorkspace, workspaceExists, getProjectName, loadState, saveState } from '../core/workspace.js';
|
|
14
|
+
import {
|
|
15
|
+
createSession,
|
|
16
|
+
getCurrentSessionId,
|
|
17
|
+
getCurrentSessionPath,
|
|
18
|
+
writeSessionFile,
|
|
19
|
+
readSessionFile,
|
|
20
|
+
sessionFileExists
|
|
21
|
+
} from '../core/session.js';
|
|
22
|
+
import { getCurrentState, transitionTo } from '../core/state-machine.js';
|
|
23
|
+
import { validateContract } from '../core/contract.js';
|
|
24
|
+
import { generateSpecHash } from '../utils/hash.js';
|
|
25
|
+
import { STATES } from '../config/constants.js';
|
|
26
|
+
import {
|
|
27
|
+
getIntakeTemplate,
|
|
28
|
+
getBlueprintTemplate,
|
|
29
|
+
getContractTemplate,
|
|
30
|
+
getPlanTemplate,
|
|
31
|
+
getCoderPackTemplate
|
|
32
|
+
} from '../config/templates.js';
|
|
33
|
+
import { printBox, printError, printSuccess } from '../ui/output.js';
|
|
34
|
+
import { StepProgress, updateProgress, completeProgress } from '../ui/dashboard.js';
|
|
35
|
+
import { BackupManager } from '../core/backup.js';
|
|
36
|
+
import {
|
|
37
|
+
spawnClaudeCode,
|
|
38
|
+
isClaudeCodeAvailable,
|
|
39
|
+
buildPromptWithContext
|
|
40
|
+
} from '../providers/index.js';
|
|
41
|
+
import { runTests } from '../core/test-runner.js';
|
|
42
|
+
import { analyzeErrors } from '../core/error-analyzer.js';
|
|
43
|
+
import { ensureDir, appendToFile } from '../utils/files.js';
|
|
44
|
+
|
|
45
|
+
const execAsync = promisify(exec);
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Magic Mode: One command, full build
|
|
49
|
+
* vibecode go "description" → Everything automated
|
|
50
|
+
*/
|
|
51
|
+
export async function goCommand(description, options = {}) {
|
|
52
|
+
const startTime = Date.now();
|
|
53
|
+
|
|
54
|
+
// Validate description
|
|
55
|
+
if (!description || description.trim().length < 5) {
|
|
56
|
+
printError('Description too short. Please provide more details.');
|
|
57
|
+
console.log(chalk.gray('Example: vibecode go "Landing page for my startup"'));
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Check Claude Code availability
|
|
62
|
+
const claudeAvailable = await isClaudeCodeAvailable();
|
|
63
|
+
if (!claudeAvailable) {
|
|
64
|
+
printError('Claude Code CLI not found.');
|
|
65
|
+
console.log(chalk.gray('Install with: npm install -g @anthropic-ai/claude-code'));
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Generate project name
|
|
70
|
+
const projectName = generateProjectName(description);
|
|
71
|
+
const projectPath = path.join(process.cwd(), projectName);
|
|
72
|
+
|
|
73
|
+
// Check if directory exists
|
|
74
|
+
if (await fs.pathExists(projectPath)) {
|
|
75
|
+
printError(`Directory already exists: ${projectName}`);
|
|
76
|
+
console.log(chalk.gray('Choose a different name or delete the existing directory.'));
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Show magic header
|
|
81
|
+
showMagicHeader(description, projectName);
|
|
82
|
+
|
|
83
|
+
// Create backup of current directory before go command
|
|
84
|
+
// (backup in parent directory since we're creating a new project)
|
|
85
|
+
const backup = new BackupManager(process.cwd());
|
|
86
|
+
await backup.createBackup('go-magic');
|
|
87
|
+
|
|
88
|
+
// Define steps
|
|
89
|
+
const steps = [
|
|
90
|
+
{ name: 'INIT', label: 'Creating project', weight: 5 },
|
|
91
|
+
{ name: 'INTAKE', label: 'Capturing requirements', weight: 5 },
|
|
92
|
+
{ name: 'BLUEPRINT', label: 'Designing architecture', weight: 5 },
|
|
93
|
+
{ name: 'CONTRACT', label: 'Generating contract', weight: 5 },
|
|
94
|
+
{ name: 'LOCK', label: 'Locking contract', weight: 5 },
|
|
95
|
+
{ name: 'PLAN', label: 'Creating execution plan', weight: 5 },
|
|
96
|
+
{ name: 'BUILD', label: 'Building with AI', weight: 60 },
|
|
97
|
+
{ name: 'REVIEW', label: 'Running tests', weight: 10 }
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
const results = {};
|
|
101
|
+
let currentProgress = 0;
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
// Step 1: INIT
|
|
105
|
+
await executeStep(steps[0], currentProgress, async () => {
|
|
106
|
+
await fs.ensureDir(projectPath);
|
|
107
|
+
process.chdir(projectPath);
|
|
108
|
+
await createWorkspace();
|
|
109
|
+
results.projectPath = projectPath;
|
|
110
|
+
});
|
|
111
|
+
currentProgress += steps[0].weight;
|
|
112
|
+
|
|
113
|
+
// Step 2: INTAKE
|
|
114
|
+
await executeStep(steps[1], currentProgress, async () => {
|
|
115
|
+
const sessionId = await createSession(projectName);
|
|
116
|
+
const intakeContent = getIntakeTemplate(projectName, description, sessionId);
|
|
117
|
+
await writeSessionFile('intake.md', intakeContent);
|
|
118
|
+
await transitionTo(STATES.INTAKE_CAPTURED, 'magic_intake');
|
|
119
|
+
results.sessionId = sessionId;
|
|
120
|
+
});
|
|
121
|
+
currentProgress += steps[1].weight;
|
|
122
|
+
|
|
123
|
+
// Step 3: BLUEPRINT
|
|
124
|
+
await executeStep(steps[2], currentProgress, async () => {
|
|
125
|
+
const sessionId = await getCurrentSessionId();
|
|
126
|
+
const blueprintContent = getBlueprintTemplate(projectName, sessionId);
|
|
127
|
+
await writeSessionFile('blueprint.md', blueprintContent);
|
|
128
|
+
await transitionTo(STATES.BLUEPRINT_DRAFTED, 'magic_blueprint');
|
|
129
|
+
});
|
|
130
|
+
currentProgress += steps[2].weight;
|
|
131
|
+
|
|
132
|
+
// Step 4: CONTRACT
|
|
133
|
+
await executeStep(steps[3], currentProgress, async () => {
|
|
134
|
+
const sessionId = await getCurrentSessionId();
|
|
135
|
+
const intakeContent = await readSessionFile('intake.md');
|
|
136
|
+
const blueprintContent = await readSessionFile('blueprint.md');
|
|
137
|
+
const contractContent = getContractTemplate(projectName, sessionId, intakeContent, blueprintContent);
|
|
138
|
+
await writeSessionFile('contract.md', contractContent);
|
|
139
|
+
await transitionTo(STATES.CONTRACT_DRAFTED, 'magic_contract');
|
|
140
|
+
});
|
|
141
|
+
currentProgress += steps[3].weight;
|
|
142
|
+
|
|
143
|
+
// Step 5: LOCK
|
|
144
|
+
await executeStep(steps[4], currentProgress, async () => {
|
|
145
|
+
const contractContent = await readSessionFile('contract.md');
|
|
146
|
+
const specHash = generateSpecHash(contractContent);
|
|
147
|
+
|
|
148
|
+
// Update contract with hash
|
|
149
|
+
const updatedContract = contractContent.replace(
|
|
150
|
+
/## Spec Hash: \[hash when locked\]/,
|
|
151
|
+
`## Spec Hash: ${specHash}`
|
|
152
|
+
).replace(
|
|
153
|
+
/## Status: DRAFT/,
|
|
154
|
+
'## Status: LOCKED'
|
|
155
|
+
);
|
|
156
|
+
await writeSessionFile('contract.md', updatedContract);
|
|
157
|
+
|
|
158
|
+
// Save to state
|
|
159
|
+
const stateData = await loadState();
|
|
160
|
+
stateData.spec_hash = specHash;
|
|
161
|
+
stateData.contract_locked = new Date().toISOString();
|
|
162
|
+
await saveState(stateData);
|
|
163
|
+
|
|
164
|
+
await transitionTo(STATES.CONTRACT_LOCKED, 'magic_lock');
|
|
165
|
+
results.specHash = specHash.substring(0, 8);
|
|
166
|
+
});
|
|
167
|
+
currentProgress += steps[4].weight;
|
|
168
|
+
|
|
169
|
+
// Step 6: PLAN
|
|
170
|
+
await executeStep(steps[5], currentProgress, async () => {
|
|
171
|
+
const sessionId = await getCurrentSessionId();
|
|
172
|
+
const contractContent = await readSessionFile('contract.md');
|
|
173
|
+
const blueprintContent = await readSessionFile('blueprint.md');
|
|
174
|
+
const intakeContent = await readSessionFile('intake.md');
|
|
175
|
+
|
|
176
|
+
const stateData = await loadState();
|
|
177
|
+
const specHash = stateData.spec_hash;
|
|
178
|
+
|
|
179
|
+
const planContent = getPlanTemplate(projectName, sessionId, specHash, contractContent);
|
|
180
|
+
await writeSessionFile('plan.md', planContent);
|
|
181
|
+
|
|
182
|
+
const coderPackContent = getCoderPackTemplate(
|
|
183
|
+
projectName, sessionId, specHash, contractContent, blueprintContent, intakeContent
|
|
184
|
+
);
|
|
185
|
+
await writeSessionFile('coder_pack.md', coderPackContent);
|
|
186
|
+
|
|
187
|
+
await transitionTo(STATES.PLAN_CREATED, 'magic_plan');
|
|
188
|
+
});
|
|
189
|
+
currentProgress += steps[5].weight;
|
|
190
|
+
|
|
191
|
+
// Step 7: BUILD
|
|
192
|
+
await executeStep(steps[6], currentProgress, async () => {
|
|
193
|
+
const sessionPath = await getCurrentSessionPath();
|
|
194
|
+
const evidencePath = path.join(sessionPath, 'evidence');
|
|
195
|
+
await ensureDir(evidencePath);
|
|
196
|
+
const logPath = path.join(evidencePath, 'build.log');
|
|
197
|
+
|
|
198
|
+
await transitionTo(STATES.BUILD_IN_PROGRESS, 'magic_build_start');
|
|
199
|
+
|
|
200
|
+
const coderPackContent = await readSessionFile('coder_pack.md');
|
|
201
|
+
const fullPrompt = await buildPromptWithContext(coderPackContent, process.cwd());
|
|
202
|
+
|
|
203
|
+
await appendToFile(logPath, `[${new Date().toISOString()}] Magic Mode Build Started\n`);
|
|
204
|
+
|
|
205
|
+
const buildResult = await spawnClaudeCode(fullPrompt, {
|
|
206
|
+
cwd: process.cwd(),
|
|
207
|
+
logPath: logPath
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
await appendToFile(logPath, `[${new Date().toISOString()}] Build completed with code: ${buildResult.code}\n`);
|
|
211
|
+
|
|
212
|
+
// Count files created
|
|
213
|
+
const files = await fs.readdir(process.cwd());
|
|
214
|
+
results.filesCreated = files.filter(f => !f.startsWith('.')).length;
|
|
215
|
+
|
|
216
|
+
// Reload state fresh before saving
|
|
217
|
+
const freshState = await loadState();
|
|
218
|
+
freshState.build_completed = new Date().toISOString();
|
|
219
|
+
await saveState(freshState);
|
|
220
|
+
|
|
221
|
+
await transitionTo(STATES.BUILD_DONE, 'magic_build_done');
|
|
222
|
+
});
|
|
223
|
+
currentProgress += steps[6].weight;
|
|
224
|
+
|
|
225
|
+
// Step 8: REVIEW
|
|
226
|
+
await executeStep(steps[7], currentProgress, async () => {
|
|
227
|
+
const testResult = await runTests(process.cwd());
|
|
228
|
+
results.testsPassed = testResult.summary.passed;
|
|
229
|
+
results.testsTotal = testResult.summary.total;
|
|
230
|
+
results.allPassed = testResult.passed;
|
|
231
|
+
|
|
232
|
+
if (testResult.passed) {
|
|
233
|
+
await transitionTo(STATES.REVIEW_PASSED, 'magic_review_passed');
|
|
234
|
+
} else {
|
|
235
|
+
const errors = analyzeErrors(testResult);
|
|
236
|
+
results.errors = errors.length;
|
|
237
|
+
// Still mark as review passed for magic mode (best effort)
|
|
238
|
+
await transitionTo(STATES.REVIEW_PASSED, 'magic_review_completed');
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
currentProgress = 100;
|
|
242
|
+
|
|
243
|
+
// Show final progress
|
|
244
|
+
console.log(renderProgressBar(100));
|
|
245
|
+
console.log();
|
|
246
|
+
|
|
247
|
+
// Show summary
|
|
248
|
+
const duration = ((Date.now() - startTime) / 1000 / 60).toFixed(1);
|
|
249
|
+
showMagicSummary(projectName, projectPath, duration, results, options);
|
|
250
|
+
|
|
251
|
+
// Auto-open if requested
|
|
252
|
+
if (options.open) {
|
|
253
|
+
await openProject(projectPath);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
} catch (error) {
|
|
257
|
+
console.log();
|
|
258
|
+
printError(`Magic mode failed: ${error.message}`);
|
|
259
|
+
console.log(chalk.gray(`Project location: ${projectPath}`));
|
|
260
|
+
console.log(chalk.gray('Check .vibecode/sessions/*/evidence/build.log for details.'));
|
|
261
|
+
process.exit(1);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Execute a single step with progress display
|
|
267
|
+
*/
|
|
268
|
+
async function executeStep(step, currentProgress, fn) {
|
|
269
|
+
const spinner = ora({
|
|
270
|
+
text: chalk.gray(step.label),
|
|
271
|
+
prefixText: renderProgressBar(currentProgress)
|
|
272
|
+
}).start();
|
|
273
|
+
|
|
274
|
+
try {
|
|
275
|
+
await fn();
|
|
276
|
+
spinner.stopAndPersist({
|
|
277
|
+
symbol: chalk.green('✓'),
|
|
278
|
+
text: chalk.green(step.name),
|
|
279
|
+
prefixText: renderProgressBar(currentProgress + step.weight)
|
|
280
|
+
});
|
|
281
|
+
} catch (error) {
|
|
282
|
+
spinner.stopAndPersist({
|
|
283
|
+
symbol: chalk.red('✗'),
|
|
284
|
+
text: chalk.red(`${step.name}: ${error.message}`),
|
|
285
|
+
prefixText: renderProgressBar(currentProgress)
|
|
286
|
+
});
|
|
287
|
+
throw error;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Generate project name from description
|
|
293
|
+
*/
|
|
294
|
+
function generateProjectName(description) {
|
|
295
|
+
const stopWords = ['a', 'an', 'the', 'for', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'with', 'my', 'our'];
|
|
296
|
+
|
|
297
|
+
const words = description
|
|
298
|
+
.toLowerCase()
|
|
299
|
+
.replace(/[^a-z0-9\s]/g, '')
|
|
300
|
+
.split(/\s+/)
|
|
301
|
+
.filter(w => w.length > 2 && !stopWords.includes(w))
|
|
302
|
+
.slice(0, 3);
|
|
303
|
+
|
|
304
|
+
if (words.length === 0) {
|
|
305
|
+
return `vibecode-${Date.now().toString(36)}`;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return words.join('-');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Render progress bar
|
|
313
|
+
*/
|
|
314
|
+
function renderProgressBar(percent, label = '') {
|
|
315
|
+
const width = 40;
|
|
316
|
+
const filled = Math.round(width * percent / 100);
|
|
317
|
+
const empty = width - filled;
|
|
318
|
+
const bar = chalk.green('█'.repeat(filled)) + chalk.gray('░'.repeat(empty));
|
|
319
|
+
return `[${bar}] ${String(percent).padStart(3)}%${label ? ' ' + label : ''}`;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Show magic mode header
|
|
324
|
+
*/
|
|
325
|
+
function showMagicHeader(description, projectName) {
|
|
326
|
+
const truncatedDesc = description.length > 50
|
|
327
|
+
? description.substring(0, 47) + '...'
|
|
328
|
+
: description;
|
|
329
|
+
|
|
330
|
+
console.log();
|
|
331
|
+
console.log(chalk.cyan('╭' + '─'.repeat(68) + '╮'));
|
|
332
|
+
console.log(chalk.cyan('│') + ' '.repeat(68) + chalk.cyan('│'));
|
|
333
|
+
console.log(chalk.cyan('│') + chalk.bold.white(' 🚀 VIBECODE MAGIC MODE') + ' '.repeat(42) + chalk.cyan('│'));
|
|
334
|
+
console.log(chalk.cyan('│') + ' '.repeat(68) + chalk.cyan('│'));
|
|
335
|
+
console.log(chalk.cyan('│') + chalk.gray(` "${truncatedDesc}"`) + ' '.repeat(Math.max(0, 65 - truncatedDesc.length - 3)) + chalk.cyan('│'));
|
|
336
|
+
console.log(chalk.cyan('│') + chalk.gray(` → ${projectName}`) + ' '.repeat(Math.max(0, 65 - projectName.length - 5)) + chalk.cyan('│'));
|
|
337
|
+
console.log(chalk.cyan('│') + ' '.repeat(68) + chalk.cyan('│'));
|
|
338
|
+
console.log(chalk.cyan('╰' + '─'.repeat(68) + '╯'));
|
|
339
|
+
console.log();
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Show magic mode summary
|
|
344
|
+
*/
|
|
345
|
+
function showMagicSummary(projectName, projectPath, duration, results, options) {
|
|
346
|
+
const testsStatus = results.allPassed
|
|
347
|
+
? chalk.green(`${results.testsPassed}/${results.testsTotal} passed`)
|
|
348
|
+
: chalk.yellow(`${results.testsPassed}/${results.testsTotal} passed`);
|
|
349
|
+
|
|
350
|
+
console.log(chalk.green('╭' + '─'.repeat(68) + '╮'));
|
|
351
|
+
console.log(chalk.green('│') + ' '.repeat(68) + chalk.green('│'));
|
|
352
|
+
console.log(chalk.green('│') + chalk.bold.white(' 🎉 BUILD COMPLETE!') + ' '.repeat(46) + chalk.green('│'));
|
|
353
|
+
console.log(chalk.green('│') + ' '.repeat(68) + chalk.green('│'));
|
|
354
|
+
console.log(chalk.green('│') + chalk.white(` 📁 Project: ${projectName}`) + ' '.repeat(Math.max(0, 51 - projectName.length)) + chalk.green('│'));
|
|
355
|
+
console.log(chalk.green('│') + chalk.white(` 📂 Location: ${projectPath}`) + ' '.repeat(Math.max(0, 51 - projectPath.length)) + chalk.green('│'));
|
|
356
|
+
console.log(chalk.green('│') + chalk.white(` 🔐 Spec: ${results.specHash}...`) + ' '.repeat(42) + chalk.green('│'));
|
|
357
|
+
console.log(chalk.green('│') + chalk.white(` 📄 Files: ${results.filesCreated} created`) + ' '.repeat(42) + chalk.green('│'));
|
|
358
|
+
console.log(chalk.green('│') + chalk.white(` 🧪 Tests: `) + testsStatus + ' '.repeat(Math.max(0, 40)) + chalk.green('│'));
|
|
359
|
+
console.log(chalk.green('│') + chalk.white(` ⏱️ Duration: ${duration} minutes`) + ' '.repeat(Math.max(0, 44 - duration.length)) + chalk.green('│'));
|
|
360
|
+
console.log(chalk.green('│') + ' '.repeat(68) + chalk.green('│'));
|
|
361
|
+
console.log(chalk.green('│') + chalk.gray(` 💡 Next: cd ${projectName}`) + ' '.repeat(Math.max(0, 51 - projectName.length)) + chalk.green('│'));
|
|
362
|
+
console.log(chalk.green('│') + ' '.repeat(68) + chalk.green('│'));
|
|
363
|
+
console.log(chalk.green('╰' + '─'.repeat(68) + '╯'));
|
|
364
|
+
console.log();
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Open project in file explorer / browser
|
|
369
|
+
*/
|
|
370
|
+
async function openProject(projectPath) {
|
|
371
|
+
try {
|
|
372
|
+
const platform = process.platform;
|
|
373
|
+
let cmd;
|
|
374
|
+
|
|
375
|
+
if (platform === 'darwin') {
|
|
376
|
+
cmd = `open "${projectPath}"`;
|
|
377
|
+
} else if (platform === 'win32') {
|
|
378
|
+
cmd = `explorer "${projectPath}"`;
|
|
379
|
+
} else {
|
|
380
|
+
cmd = `xdg-open "${projectPath}"`;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
await execAsync(cmd);
|
|
384
|
+
} catch (error) {
|
|
385
|
+
console.log(chalk.gray(`Could not auto-open: ${error.message}`));
|
|
386
|
+
}
|
|
387
|
+
}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
2
|
+
// VIBECODE CLI - Learn Command
|
|
3
|
+
// Phase H5: View and manage AI learnings
|
|
4
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
5
|
+
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import inquirer from 'inquirer';
|
|
8
|
+
import fs from 'fs/promises';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import { LearningEngine } from '../core/learning.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Learn Command - View and manage AI learnings
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* vibecode learn - Interactive menu
|
|
17
|
+
* vibecode learn --stats - Show learning statistics
|
|
18
|
+
* vibecode learn --clear - Clear all learnings
|
|
19
|
+
* vibecode learn --export - Export learnings to file
|
|
20
|
+
*/
|
|
21
|
+
export async function learnCommand(options = {}) {
|
|
22
|
+
const learning = new LearningEngine();
|
|
23
|
+
|
|
24
|
+
if (options.stats) {
|
|
25
|
+
await showStats(learning);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (options.clear) {
|
|
30
|
+
await clearLearnings(learning, options.force);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (options.export) {
|
|
35
|
+
await exportLearnings(learning);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Default: show interactive menu
|
|
40
|
+
await interactiveLearn(learning);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Show learning statistics
|
|
45
|
+
*/
|
|
46
|
+
async function showStats(learning) {
|
|
47
|
+
const stats = await learning.getStats();
|
|
48
|
+
|
|
49
|
+
console.log(chalk.cyan(`
|
|
50
|
+
╭────────────────────────────────────────────────────────────────────╮
|
|
51
|
+
│ 📊 LEARNING STATISTICS │
|
|
52
|
+
╰────────────────────────────────────────────────────────────────────╯
|
|
53
|
+
`));
|
|
54
|
+
|
|
55
|
+
console.log(chalk.white(` 📁 Project Learnings`));
|
|
56
|
+
console.log(chalk.gray(` Tổng fixes: ${stats.local.total}`));
|
|
57
|
+
console.log(chalk.gray(` Thành công: ${stats.local.success} (${stats.local.rate}%)`));
|
|
58
|
+
console.log('');
|
|
59
|
+
|
|
60
|
+
console.log(chalk.white(` 🌍 Global Learnings`));
|
|
61
|
+
console.log(chalk.gray(` Tổng fixes: ${stats.global.total}`));
|
|
62
|
+
console.log(chalk.gray(` Thành công: ${stats.global.success} (${stats.global.rate}%)`));
|
|
63
|
+
console.log('');
|
|
64
|
+
|
|
65
|
+
if (Object.keys(stats.byCategory).length > 0) {
|
|
66
|
+
console.log(chalk.white(` 📂 Theo Error Category`));
|
|
67
|
+
for (const [cat, data] of Object.entries(stats.byCategory)) {
|
|
68
|
+
const rate = data.total > 0 ? (data.success / data.total * 100).toFixed(0) : 0;
|
|
69
|
+
const bar = renderMiniBar(data.success, data.total);
|
|
70
|
+
console.log(chalk.gray(` ${cat.padEnd(12)} ${bar} ${data.success}/${data.total} (${rate}%)`));
|
|
71
|
+
}
|
|
72
|
+
console.log('');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
console.log(chalk.white(` ⚙️ Preferences đã lưu: ${stats.preferences}`));
|
|
76
|
+
|
|
77
|
+
if (stats.lastLearning) {
|
|
78
|
+
const lastDate = new Date(stats.lastLearning).toLocaleString('vi-VN');
|
|
79
|
+
console.log(chalk.gray(` 📅 Learning gần nhất: ${lastDate}`));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
console.log('');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Render a mini progress bar
|
|
87
|
+
*/
|
|
88
|
+
function renderMiniBar(value, total) {
|
|
89
|
+
const width = 10;
|
|
90
|
+
const filled = total > 0 ? Math.round(width * value / total) : 0;
|
|
91
|
+
const empty = width - filled;
|
|
92
|
+
return chalk.green('█'.repeat(filled)) + chalk.gray('░'.repeat(empty));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Clear all local learnings
|
|
97
|
+
*/
|
|
98
|
+
async function clearLearnings(learning, force) {
|
|
99
|
+
const stats = await learning.getStats();
|
|
100
|
+
|
|
101
|
+
if (stats.local.total === 0) {
|
|
102
|
+
console.log(chalk.yellow('\n📭 Không có learnings nào để xoá.\n'));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!force) {
|
|
107
|
+
console.log(chalk.yellow(`\n⚠️ Sắp xoá ${stats.local.total} learnings của project này.\n`));
|
|
108
|
+
|
|
109
|
+
const { confirm } = await inquirer.prompt([
|
|
110
|
+
{
|
|
111
|
+
type: 'confirm',
|
|
112
|
+
name: 'confirm',
|
|
113
|
+
message: 'Xác nhận xoá tất cả learnings?',
|
|
114
|
+
default: false
|
|
115
|
+
}
|
|
116
|
+
]);
|
|
117
|
+
|
|
118
|
+
if (!confirm) {
|
|
119
|
+
console.log(chalk.gray('\n👋 Đã huỷ.\n'));
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
await learning.clearLocal();
|
|
125
|
+
console.log(chalk.green(`\n✅ Đã xoá ${stats.local.total} learnings.\n`));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Export learnings to file
|
|
130
|
+
*/
|
|
131
|
+
async function exportLearnings(learning) {
|
|
132
|
+
const stats = await learning.getStats();
|
|
133
|
+
const fixes = await learning.loadJson(
|
|
134
|
+
path.join(learning.localPath, 'fixes.json'),
|
|
135
|
+
[]
|
|
136
|
+
);
|
|
137
|
+
const prefs = await learning.loadJson(
|
|
138
|
+
path.join(learning.localPath, 'preferences.json'),
|
|
139
|
+
{}
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
if (fixes.length === 0 && Object.keys(prefs).length === 0) {
|
|
143
|
+
console.log(chalk.yellow('\n📭 Không có learnings nào để export.\n'));
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const exportData = {
|
|
148
|
+
exportedAt: new Date().toISOString(),
|
|
149
|
+
projectPath: learning.projectPath,
|
|
150
|
+
stats,
|
|
151
|
+
fixes: fixes.map(f => ({
|
|
152
|
+
id: f.id,
|
|
153
|
+
errorType: f.errorType,
|
|
154
|
+
errorCategory: f.errorCategory,
|
|
155
|
+
success: f.success,
|
|
156
|
+
userFeedback: f.userFeedback,
|
|
157
|
+
projectType: f.projectType,
|
|
158
|
+
timestamp: f.timestamp
|
|
159
|
+
})),
|
|
160
|
+
preferences: prefs
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const exportPath = `vibecode-learnings-${Date.now()}.json`;
|
|
164
|
+
await fs.writeFile(exportPath, JSON.stringify(exportData, null, 2));
|
|
165
|
+
|
|
166
|
+
console.log(chalk.green(`\n✅ Đã export ${fixes.length} learnings → ${exportPath}\n`));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Interactive learning menu
|
|
171
|
+
*/
|
|
172
|
+
async function interactiveLearn(learning) {
|
|
173
|
+
const stats = await learning.getStats();
|
|
174
|
+
|
|
175
|
+
const successBar = renderMiniBar(stats.local.success, stats.local.total);
|
|
176
|
+
|
|
177
|
+
console.log(chalk.cyan(`
|
|
178
|
+
╭────────────────────────────────────────────────────────────────────╮
|
|
179
|
+
│ 🧠 VIBECODE LEARNING │
|
|
180
|
+
│ │
|
|
181
|
+
│ AI học từ feedback của bạn để cải thiện suggestions. │
|
|
182
|
+
│ │
|
|
183
|
+
│ Success rate: ${successBar} ${String(stats.local.rate + '%').padEnd(30)}│
|
|
184
|
+
│ Total learnings: ${String(stats.local.total).padEnd(44)}│
|
|
185
|
+
│ │
|
|
186
|
+
╰────────────────────────────────────────────────────────────────────╯
|
|
187
|
+
`));
|
|
188
|
+
|
|
189
|
+
const { action } = await inquirer.prompt([
|
|
190
|
+
{
|
|
191
|
+
type: 'list',
|
|
192
|
+
name: 'action',
|
|
193
|
+
message: 'Bạn muốn làm gì?',
|
|
194
|
+
choices: [
|
|
195
|
+
{ name: '📊 Xem thống kê chi tiết', value: 'stats' },
|
|
196
|
+
{ name: '📤 Export learnings', value: 'export' },
|
|
197
|
+
{ name: '🗑️ Xoá learnings', value: 'clear' },
|
|
198
|
+
new inquirer.Separator(),
|
|
199
|
+
{ name: '👋 Thoát', value: 'exit' }
|
|
200
|
+
]
|
|
201
|
+
}
|
|
202
|
+
]);
|
|
203
|
+
|
|
204
|
+
switch (action) {
|
|
205
|
+
case 'stats':
|
|
206
|
+
await showStats(learning);
|
|
207
|
+
break;
|
|
208
|
+
case 'export':
|
|
209
|
+
await exportLearnings(learning);
|
|
210
|
+
break;
|
|
211
|
+
case 'clear':
|
|
212
|
+
await clearLearnings(learning, false);
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Ask for feedback after a fix
|
|
219
|
+
* Called from debug/fix commands
|
|
220
|
+
*/
|
|
221
|
+
export async function askFeedback(fixInfo) {
|
|
222
|
+
console.log('');
|
|
223
|
+
const { feedback } = await inquirer.prompt([
|
|
224
|
+
{
|
|
225
|
+
type: 'list',
|
|
226
|
+
name: 'feedback',
|
|
227
|
+
message: 'Fix này có đúng không?',
|
|
228
|
+
choices: [
|
|
229
|
+
{ name: '✅ Đúng, hoạt động tốt', value: 'success' },
|
|
230
|
+
{ name: '❌ Không đúng', value: 'failed' },
|
|
231
|
+
{ name: '🔄 Đúng một phần', value: 'partial' },
|
|
232
|
+
{ name: '⏭️ Bỏ qua', value: 'skip' }
|
|
233
|
+
]
|
|
234
|
+
}
|
|
235
|
+
]);
|
|
236
|
+
|
|
237
|
+
if (feedback === 'skip') {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const learning = new LearningEngine();
|
|
242
|
+
let userCorrection = null;
|
|
243
|
+
|
|
244
|
+
if (feedback === 'failed' || feedback === 'partial') {
|
|
245
|
+
const { correction } = await inquirer.prompt([
|
|
246
|
+
{
|
|
247
|
+
type: 'input',
|
|
248
|
+
name: 'correction',
|
|
249
|
+
message: 'Mô tả ngắn vấn đề hoặc cách fix đúng (Enter để bỏ qua):',
|
|
250
|
+
}
|
|
251
|
+
]);
|
|
252
|
+
userCorrection = correction || null;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
await learning.recordFix({
|
|
256
|
+
errorType: fixInfo.errorType,
|
|
257
|
+
errorMessage: fixInfo.errorMessage,
|
|
258
|
+
errorCategory: fixInfo.errorCategory,
|
|
259
|
+
fixApplied: fixInfo.fixApplied,
|
|
260
|
+
success: feedback === 'success',
|
|
261
|
+
userFeedback: feedback,
|
|
262
|
+
userCorrection
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
if (feedback === 'success') {
|
|
266
|
+
console.log(chalk.green(' ✅ Đã ghi nhận. Cảm ơn!\n'));
|
|
267
|
+
} else {
|
|
268
|
+
console.log(chalk.yellow(' 📝 Đã ghi nhận feedback.\n'));
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return feedback;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Show learning-based suggestion (if available)
|
|
276
|
+
*/
|
|
277
|
+
export async function showLearningSuggestion(errorType, errorCategory) {
|
|
278
|
+
const learning = new LearningEngine();
|
|
279
|
+
const suggestion = await learning.getSuggestion(errorType, errorCategory);
|
|
280
|
+
|
|
281
|
+
if (suggestion && suggestion.confidence > 0.6) {
|
|
282
|
+
const confidencePercent = (suggestion.confidence * 100).toFixed(0);
|
|
283
|
+
console.log(chalk.cyan(` 💡 Dựa trên ${suggestion.basedOn} fixes trước (độ tin cậy: ${confidencePercent}%)`));
|
|
284
|
+
|
|
285
|
+
if (suggestion.suggestion) {
|
|
286
|
+
const shortSuggestion = suggestion.suggestion.substring(0, 100);
|
|
287
|
+
console.log(chalk.gray(` Gợi ý: ${shortSuggestion}${suggestion.suggestion.length > 100 ? '...' : ''}`));
|
|
288
|
+
}
|
|
289
|
+
console.log('');
|
|
290
|
+
return suggestion;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return null;
|
|
294
|
+
}
|