@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.
@@ -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
- // Parse
181
+ // Phase H Commands - Undo/Rollback
180
182
  // ─────────────────────────────────────────────────────────────────────────────
181
183
 
182
- program.parse();
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.5.0",
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": "echo \"Tests coming in Phase B\" && exit 0"
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
- for (const moduleId of decomposition.buildOrder) {
221
- // Check if we should stop
222
- if (this.state === ORCHESTRATOR_STATES.PAUSED) {
223
- await this.log('Build paused');
224
- break;
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
- // Check if module can be built (dependencies satisfied)
228
- if (!this.decompositionEngine.canBuildModule(moduleId)) {
229
- const depStatus = decomposition.dependencyGraph[moduleId];
230
- const failedDeps = depStatus?.dependsOn.filter(d =>
231
- this.buildState.failedModules.includes(d)
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
- // Build the module
243
- const moduleResult = await this.buildModule(moduleId, decomposition);
244
- results.modules[moduleId] = moduleResult;
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
- if (!moduleResult.success) {
247
- results.success = false;
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
- if (!this.config.continueOnFailure) {
250
- await this.log(`Stopping build due to module failure: ${moduleId}`, 'error');
251
- break;
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
- const spinner = ora({
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
- await this.log(`Module ${moduleId} attempt ${attempts} failed: ${error.message}`, 'error');
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
@@ -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
- printError(error.message);
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
- printError(`Auto build failed: ${error.message}`);
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(chalk.red(`\n Build error: ${error.message}`));
464
+ console.log('\n' + inlineError(error));
455
465
  await logIteration(logPath, iteration, `Error: ${error.message}`);
456
466
 
457
467
  iterationState = finalizeIterationState(iterationState, 'error');
@@ -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
+ }