@nclamvn/vibecode-cli 1.5.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/bin/vibecode.js +38 -2
- package/package.json +4 -2
- package/src/agent/orchestrator.js +104 -35
- package/src/commands/build.js +13 -3
- package/src/commands/go.js +9 -2
- 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/debug/index.js +30 -1
- package/src/index.js +31 -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 @@
|
|
|
1
|
+
[]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{}
|
package/bin/vibecode.js
CHANGED
|
@@ -21,6 +21,8 @@ import {
|
|
|
21
21
|
agentCommand,
|
|
22
22
|
debugCommand,
|
|
23
23
|
assistCommand,
|
|
24
|
+
undoCommand,
|
|
25
|
+
learnCommand,
|
|
24
26
|
VERSION
|
|
25
27
|
} from '../src/index.js';
|
|
26
28
|
|
|
@@ -176,7 +178,41 @@ program
|
|
|
176
178
|
});
|
|
177
179
|
|
|
178
180
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
179
|
-
//
|
|
181
|
+
// Phase H Commands - Undo/Rollback
|
|
180
182
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
181
183
|
|
|
182
|
-
program
|
|
184
|
+
program
|
|
185
|
+
.command('undo')
|
|
186
|
+
.description('⏪ Undo/rollback: Restore files to previous state')
|
|
187
|
+
.option('-l, --list', 'List available backups')
|
|
188
|
+
.option('-s, --step <n>', 'Restore to N steps ago', parseInt)
|
|
189
|
+
.option('-c, --clear', 'Clear all backups')
|
|
190
|
+
.option('-f, --force', 'Force operation without confirmation')
|
|
191
|
+
.action(undoCommand);
|
|
192
|
+
|
|
193
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
194
|
+
// Phase H5 Commands - Learning Mode
|
|
195
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
196
|
+
|
|
197
|
+
program
|
|
198
|
+
.command('learn')
|
|
199
|
+
.description('🧠 View and manage AI learnings from feedback')
|
|
200
|
+
.option('-s, --stats', 'Show learning statistics')
|
|
201
|
+
.option('-c, --clear', 'Clear all learnings')
|
|
202
|
+
.option('-e, --export', 'Export learnings to file')
|
|
203
|
+
.option('-f, --force', 'Skip confirmation prompts')
|
|
204
|
+
.action(learnCommand);
|
|
205
|
+
|
|
206
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
207
|
+
// Parse - If no command provided, show interactive wizard
|
|
208
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
209
|
+
|
|
210
|
+
if (process.argv.length === 2) {
|
|
211
|
+
// No command provided - show interactive wizard
|
|
212
|
+
import('../src/commands/wizard.js').then(m => m.wizardCommand()).catch(err => {
|
|
213
|
+
console.error('Failed to load wizard:', err.message);
|
|
214
|
+
program.help();
|
|
215
|
+
});
|
|
216
|
+
} else {
|
|
217
|
+
program.parse();
|
|
218
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nclamvn/vibecode-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "Build software with discipline - AI coding with guardrails",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
"scripts": {
|
|
11
11
|
"start": "node bin/vibecode.js",
|
|
12
12
|
"lint": "eslint src/",
|
|
13
|
-
"test": "
|
|
13
|
+
"test": "node src/ui/__tests__/error-translator.test.js",
|
|
14
|
+
"test:errors": "node src/ui/__tests__/error-translator.test.js",
|
|
15
|
+
"test:errors:verbose": "node src/ui/__tests__/error-translator.test.js --verbose"
|
|
14
16
|
},
|
|
15
17
|
"keywords": [
|
|
16
18
|
"cli",
|
|
@@ -11,6 +11,9 @@ import fs from 'fs-extra';
|
|
|
11
11
|
import { spawnClaudeCode, isClaudeCodeAvailable } from '../providers/index.js';
|
|
12
12
|
import { runTests } from '../core/test-runner.js';
|
|
13
13
|
import { ensureDir, appendToFile } from '../utils/files.js';
|
|
14
|
+
import { ProgressDashboard } from '../ui/dashboard.js';
|
|
15
|
+
import { translateError, showError, inlineError } from '../ui/error-translator.js';
|
|
16
|
+
import { BackupManager } from '../core/backup.js';
|
|
14
17
|
|
|
15
18
|
/**
|
|
16
19
|
* Orchestrator states
|
|
@@ -63,9 +66,14 @@ export class Orchestrator {
|
|
|
63
66
|
testAfterEachModule: options.testAfterEachModule ?? true,
|
|
64
67
|
continueOnFailure: options.continueOnFailure ?? false,
|
|
65
68
|
parallelBuilds: options.parallelBuilds ?? false, // Future feature
|
|
66
|
-
timeout: options.timeout || 30 * 60 * 1000 // 30 minutes per module
|
|
69
|
+
timeout: options.timeout || 30 * 60 * 1000, // 30 minutes per module
|
|
70
|
+
useDashboard: options.useDashboard ?? true, // Use visual dashboard
|
|
71
|
+
verbose: options.verbose ?? false
|
|
67
72
|
};
|
|
68
73
|
|
|
74
|
+
// Dashboard instance
|
|
75
|
+
this.dashboard = null;
|
|
76
|
+
|
|
69
77
|
// Build state
|
|
70
78
|
this.buildState = {
|
|
71
79
|
startTime: null,
|
|
@@ -167,6 +175,10 @@ export class Orchestrator {
|
|
|
167
175
|
async build(description, options = {}) {
|
|
168
176
|
this.buildState.startTime = Date.now();
|
|
169
177
|
|
|
178
|
+
// Create backup before agent build
|
|
179
|
+
const backup = new BackupManager(this.projectPath);
|
|
180
|
+
await backup.createBackup('agent-build');
|
|
181
|
+
|
|
170
182
|
try {
|
|
171
183
|
// Step 1: Decompose project
|
|
172
184
|
this.setState(ORCHESTRATOR_STATES.DECOMPOSING);
|
|
@@ -217,40 +229,86 @@ export class Orchestrator {
|
|
|
217
229
|
totalTime: 0
|
|
218
230
|
};
|
|
219
231
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
232
|
+
// Create and start dashboard if enabled
|
|
233
|
+
if (this.config.useDashboard) {
|
|
234
|
+
this.dashboard = new ProgressDashboard({
|
|
235
|
+
title: 'VIBECODE AGENT',
|
|
236
|
+
projectName: path.basename(this.projectPath),
|
|
237
|
+
mode: `Agent (${decomposition.totalModules} modules)`
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// Set modules for dashboard
|
|
241
|
+
this.dashboard.setModules(decomposition.buildOrder.map(id => {
|
|
242
|
+
const mod = this.decompositionEngine.getModule(id);
|
|
243
|
+
return { name: mod?.name || id };
|
|
244
|
+
}));
|
|
245
|
+
|
|
246
|
+
this.dashboard.start();
|
|
247
|
+
}
|
|
226
248
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
this.
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
if (failedDeps?.length > 0) {
|
|
235
|
-
await this.log(`Skipping ${moduleId}: dependencies failed (${failedDeps.join(', ')})`, 'warn');
|
|
236
|
-
this.buildState.skippedModules.push(moduleId);
|
|
237
|
-
results.modules[moduleId] = { status: 'skipped', reason: 'dependencies_failed' };
|
|
238
|
-
continue;
|
|
249
|
+
try {
|
|
250
|
+
for (const moduleId of decomposition.buildOrder) {
|
|
251
|
+
// Check if we should stop
|
|
252
|
+
if (this.state === ORCHESTRATOR_STATES.PAUSED) {
|
|
253
|
+
await this.log('Build paused');
|
|
254
|
+
break;
|
|
239
255
|
}
|
|
240
|
-
}
|
|
241
256
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
257
|
+
// Check if module can be built (dependencies satisfied)
|
|
258
|
+
if (!this.decompositionEngine.canBuildModule(moduleId)) {
|
|
259
|
+
const depStatus = decomposition.dependencyGraph[moduleId];
|
|
260
|
+
const failedDeps = depStatus?.dependsOn.filter(d =>
|
|
261
|
+
this.buildState.failedModules.includes(d)
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
if (failedDeps?.length > 0) {
|
|
265
|
+
await this.log(`Skipping ${moduleId}: dependencies failed (${failedDeps.join(', ')})`, 'warn');
|
|
266
|
+
this.buildState.skippedModules.push(moduleId);
|
|
267
|
+
results.modules[moduleId] = { status: 'skipped', reason: 'dependencies_failed' };
|
|
268
|
+
|
|
269
|
+
// Update dashboard
|
|
270
|
+
if (this.dashboard) {
|
|
271
|
+
const mod = this.decompositionEngine.getModule(moduleId);
|
|
272
|
+
this.dashboard.failModule(mod?.name || moduleId);
|
|
273
|
+
}
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
245
277
|
|
|
246
|
-
|
|
247
|
-
|
|
278
|
+
// Update dashboard - start module
|
|
279
|
+
if (this.dashboard) {
|
|
280
|
+
const mod = this.decompositionEngine.getModule(moduleId);
|
|
281
|
+
this.dashboard.startModule(mod?.name || moduleId);
|
|
282
|
+
}
|
|
248
283
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
284
|
+
// Build the module
|
|
285
|
+
const moduleResult = await this.buildModule(moduleId, decomposition);
|
|
286
|
+
results.modules[moduleId] = moduleResult;
|
|
287
|
+
|
|
288
|
+
// Update dashboard - complete/fail module
|
|
289
|
+
if (this.dashboard) {
|
|
290
|
+
const mod = this.decompositionEngine.getModule(moduleId);
|
|
291
|
+
if (moduleResult.success) {
|
|
292
|
+
this.dashboard.completeModule(mod?.name || moduleId, true);
|
|
293
|
+
} else {
|
|
294
|
+
this.dashboard.failModule(mod?.name || moduleId);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (!moduleResult.success) {
|
|
299
|
+
results.success = false;
|
|
300
|
+
|
|
301
|
+
if (!this.config.continueOnFailure) {
|
|
302
|
+
await this.log(`Stopping build due to module failure: ${moduleId}`, 'error');
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
252
305
|
}
|
|
253
306
|
}
|
|
307
|
+
} finally {
|
|
308
|
+
// Stop dashboard
|
|
309
|
+
if (this.dashboard) {
|
|
310
|
+
this.dashboard.stop();
|
|
311
|
+
}
|
|
254
312
|
}
|
|
255
313
|
|
|
256
314
|
results.totalTime = Date.now() - this.buildState.startTime;
|
|
@@ -269,10 +327,11 @@ export class Orchestrator {
|
|
|
269
327
|
this.buildState.currentModule = moduleId;
|
|
270
328
|
this.emit(EVENTS.MODULE_START, { moduleId, module });
|
|
271
329
|
|
|
272
|
-
|
|
330
|
+
// Only use spinner if dashboard is not enabled
|
|
331
|
+
const spinner = !this.config.useDashboard ? ora({
|
|
273
332
|
text: chalk.cyan(`Building module: ${module.name}`),
|
|
274
333
|
prefixText: this.getProgressPrefix()
|
|
275
|
-
}).start();
|
|
334
|
+
}).start() : null;
|
|
276
335
|
|
|
277
336
|
// Record in memory
|
|
278
337
|
if (this.memoryEngine) {
|
|
@@ -308,7 +367,8 @@ export class Orchestrator {
|
|
|
308
367
|
if (buildResult.success) {
|
|
309
368
|
// Run tests if configured
|
|
310
369
|
if (this.config.testAfterEachModule) {
|
|
311
|
-
spinner.text = chalk.cyan(`Testing module: ${module.name}`);
|
|
370
|
+
if (spinner) spinner.text = chalk.cyan(`Testing module: ${module.name}`);
|
|
371
|
+
if (this.dashboard) this.dashboard.addLog(`Testing: ${module.name}`);
|
|
312
372
|
this.setState(ORCHESTRATOR_STATES.TESTING);
|
|
313
373
|
|
|
314
374
|
const testResult = await runTests(this.projectPath);
|
|
@@ -319,7 +379,7 @@ export class Orchestrator {
|
|
|
319
379
|
}
|
|
320
380
|
|
|
321
381
|
// Success!
|
|
322
|
-
spinner.succeed(chalk.green(`Module complete: ${module.name}`));
|
|
382
|
+
if (spinner) spinner.succeed(chalk.green(`Module complete: ${module.name}`));
|
|
323
383
|
|
|
324
384
|
this.decompositionEngine.updateModuleStatus(moduleId, 'completed', {
|
|
325
385
|
files: buildResult.files || []
|
|
@@ -347,11 +407,20 @@ export class Orchestrator {
|
|
|
347
407
|
|
|
348
408
|
} catch (error) {
|
|
349
409
|
lastError = error;
|
|
350
|
-
|
|
410
|
+
const translated = translateError(error);
|
|
411
|
+
await this.log(`Module ${moduleId} attempt ${attempts} failed: ${translated.title} - ${error.message}`, 'error');
|
|
412
|
+
|
|
413
|
+
// Show translated error if not using dashboard
|
|
414
|
+
if (!this.config.useDashboard) {
|
|
415
|
+
console.log(inlineError(error));
|
|
416
|
+
} else if (this.dashboard) {
|
|
417
|
+
this.dashboard.addLog(`Error: ${translated.title}`);
|
|
418
|
+
}
|
|
351
419
|
|
|
352
420
|
// Try self-healing
|
|
353
421
|
if (this.selfHealingEngine && attempts < this.config.maxModuleRetries) {
|
|
354
|
-
spinner.text = chalk.yellow(`Healing module: ${module.name}`);
|
|
422
|
+
if (spinner) spinner.text = chalk.yellow(`Healing module: ${module.name}`);
|
|
423
|
+
if (this.dashboard) this.dashboard.addLog(`Healing: ${module.name}`);
|
|
355
424
|
this.setState(ORCHESTRATOR_STATES.HEALING);
|
|
356
425
|
this.emit(EVENTS.HEALING_START, { moduleId, error });
|
|
357
426
|
|
|
@@ -387,7 +456,7 @@ export class Orchestrator {
|
|
|
387
456
|
}
|
|
388
457
|
|
|
389
458
|
// Module failed
|
|
390
|
-
spinner.fail(chalk.red(`Module failed: ${module.name}`));
|
|
459
|
+
if (spinner) spinner.fail(chalk.red(`Module failed: ${module.name}`));
|
|
391
460
|
|
|
392
461
|
this.decompositionEngine.updateModuleStatus(moduleId, 'failed', {
|
|
393
462
|
error: lastError?.message
|
package/src/commands/build.js
CHANGED
|
@@ -22,6 +22,8 @@ import { STATES } from '../config/constants.js';
|
|
|
22
22
|
import { getBuildReportTemplate } from '../config/templates.js';
|
|
23
23
|
import { ensureDir, pathExists, appendToFile, readMarkdown, writeJson } from '../utils/files.js';
|
|
24
24
|
import { printBox, printError, printSuccess, printWarning, printNextStep } from '../ui/output.js';
|
|
25
|
+
import { showError, inlineError } from '../ui/error-translator.js';
|
|
26
|
+
import { BackupManager } from '../core/backup.js';
|
|
25
27
|
import {
|
|
26
28
|
spawnClaudeCode,
|
|
27
29
|
isClaudeCodeAvailable,
|
|
@@ -75,7 +77,7 @@ export async function buildCommand(options = {}) {
|
|
|
75
77
|
}
|
|
76
78
|
|
|
77
79
|
} catch (error) {
|
|
78
|
-
|
|
80
|
+
showError(error, { verbose: options.verbose });
|
|
79
81
|
process.exit(1);
|
|
80
82
|
}
|
|
81
83
|
}
|
|
@@ -85,6 +87,10 @@ export async function buildCommand(options = {}) {
|
|
|
85
87
|
* "Contract LOCKED = License to build"
|
|
86
88
|
*/
|
|
87
89
|
async function handleAutoBuild(currentState, projectName, sessionId, sessionPath, specHash, options) {
|
|
90
|
+
// Create backup before build
|
|
91
|
+
const backup = new BackupManager();
|
|
92
|
+
await backup.createBackup('build-auto');
|
|
93
|
+
|
|
88
94
|
// Check state - must be PLAN_CREATED or BUILD_IN_PROGRESS or REVIEW_FAILED
|
|
89
95
|
const validStates = [STATES.PLAN_CREATED, STATES.BUILD_IN_PROGRESS, STATES.REVIEW_FAILED];
|
|
90
96
|
if (!validStates.includes(currentState)) {
|
|
@@ -227,7 +233,7 @@ ${evidence.screenshots > 0 ? ` ✅ ${evidence.screenshots} screenshots` : '
|
|
|
227
233
|
|
|
228
234
|
} catch (error) {
|
|
229
235
|
await appendToFile(logPath, `\nERROR: ${error.message}\n`);
|
|
230
|
-
|
|
236
|
+
showError(error);
|
|
231
237
|
process.exit(1);
|
|
232
238
|
}
|
|
233
239
|
}
|
|
@@ -240,6 +246,10 @@ async function handleIterativeBuild(currentState, projectName, sessionId, sessio
|
|
|
240
246
|
const maxIterations = options.max || 3;
|
|
241
247
|
const strictMode = options.strict || false;
|
|
242
248
|
|
|
249
|
+
// Create backup before iterative build
|
|
250
|
+
const backup = new BackupManager();
|
|
251
|
+
await backup.createBackup('build-iterate');
|
|
252
|
+
|
|
243
253
|
// Check state - must be PLAN_CREATED or BUILD_IN_PROGRESS or REVIEW_FAILED
|
|
244
254
|
const validStates = [STATES.PLAN_CREATED, STATES.BUILD_IN_PROGRESS, STATES.REVIEW_FAILED];
|
|
245
255
|
if (!validStates.includes(currentState)) {
|
|
@@ -451,7 +461,7 @@ Starting build-test-fix loop...`;
|
|
|
451
461
|
}
|
|
452
462
|
|
|
453
463
|
} catch (error) {
|
|
454
|
-
console.log(
|
|
464
|
+
console.log('\n' + inlineError(error));
|
|
455
465
|
await logIteration(logPath, iteration, `Error: ${error.message}`);
|
|
456
466
|
|
|
457
467
|
iterationState = finalizeIterationState(iterationState, 'error');
|
package/src/commands/go.js
CHANGED
|
@@ -31,6 +31,8 @@ import {
|
|
|
31
31
|
getCoderPackTemplate
|
|
32
32
|
} from '../config/templates.js';
|
|
33
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';
|
|
34
36
|
import {
|
|
35
37
|
spawnClaudeCode,
|
|
36
38
|
isClaudeCodeAvailable,
|
|
@@ -78,6 +80,11 @@ export async function goCommand(description, options = {}) {
|
|
|
78
80
|
// Show magic header
|
|
79
81
|
showMagicHeader(description, projectName);
|
|
80
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
|
+
|
|
81
88
|
// Define steps
|
|
82
89
|
const steps = [
|
|
83
90
|
{ name: 'INIT', label: 'Creating project', weight: 5 },
|
|
@@ -304,12 +311,12 @@ function generateProjectName(description) {
|
|
|
304
311
|
/**
|
|
305
312
|
* Render progress bar
|
|
306
313
|
*/
|
|
307
|
-
function renderProgressBar(percent) {
|
|
314
|
+
function renderProgressBar(percent, label = '') {
|
|
308
315
|
const width = 40;
|
|
309
316
|
const filled = Math.round(width * percent / 100);
|
|
310
317
|
const empty = width - filled;
|
|
311
318
|
const bar = chalk.green('█'.repeat(filled)) + chalk.gray('░'.repeat(empty));
|
|
312
|
-
return `[${bar}] ${String(percent).padStart(3)}
|
|
319
|
+
return `[${bar}] ${String(percent).padStart(3)}%${label ? ' ' + label : ''}`;
|
|
313
320
|
}
|
|
314
321
|
|
|
315
322
|
/**
|
|
@@ -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
|
+
}
|