@nclamvn/vibecode-cli 1.0.1 → 1.2.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/bin/vibecode.js CHANGED
@@ -79,6 +79,9 @@ program
79
79
  .option('-c, --complete', 'Mark build as complete')
80
80
  .option('-e, --evidence', 'Capture evidence snapshot')
81
81
  .option('-a, --auto', 'Auto-build with Claude Code (--dangerously-skip-permissions)')
82
+ .option('-i, --iterate', 'Iterative build: build-test-fix loop until tests pass')
83
+ .option('-m, --max <n>', 'Max iterations for --iterate mode', parseInt)
84
+ .option('--strict', 'Strict mode: exit with error if tests fail after max iterations')
82
85
  .option('--provider <name>', 'Provider to use: claude-code', 'claude-code')
83
86
  .action(buildCommand);
84
87
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nclamvn/vibecode-cli",
3
- "version": "1.0.1",
3
+ "version": "1.2.0",
4
4
  "description": "Build software with discipline - AI coding with guardrails",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -20,7 +20,7 @@ import { getCurrentState, transitionTo } from '../core/state-machine.js';
20
20
  import { getSpecHash } from '../core/contract.js';
21
21
  import { STATES } from '../config/constants.js';
22
22
  import { getBuildReportTemplate } from '../config/templates.js';
23
- import { ensureDir, pathExists, appendToFile, readMarkdown } from '../utils/files.js';
23
+ import { ensureDir, pathExists, appendToFile, readMarkdown, writeJson } from '../utils/files.js';
24
24
  import { printBox, printError, printSuccess, printWarning, printNextStep } from '../ui/output.js';
25
25
  import {
26
26
  spawnClaudeCode,
@@ -28,6 +28,19 @@ import {
28
28
  buildPromptWithContext,
29
29
  getProviderInfo
30
30
  } from '../providers/index.js';
31
+ // Phase D: Iterative Build
32
+ import { runTests, formatTestResults } from '../core/test-runner.js';
33
+ import { analyzeErrors, formatErrors, createErrorSummary } from '../core/error-analyzer.js';
34
+ import { generateFixPrompt, areErrorsFixable, estimateFixComplexity } from '../core/fix-generator.js';
35
+ import {
36
+ createIterationState,
37
+ recordIteration,
38
+ canContinue,
39
+ finalizeIterationState,
40
+ saveIterationState,
41
+ formatIterationSummary,
42
+ logIteration
43
+ } from '../core/iteration.js';
31
44
 
32
45
  const execAsync = promisify(exec);
33
46
 
@@ -46,7 +59,10 @@ export async function buildCommand(options = {}) {
46
59
  const specHash = await getSpecHash();
47
60
 
48
61
  // Handle different build modes
49
- if (options.auto) {
62
+ if (options.iterate) {
63
+ // Phase D: Iterative Build
64
+ await handleIterativeBuild(currentState, projectName, sessionId, sessionPath, specHash, options);
65
+ } else if (options.auto) {
50
66
  await handleAutoBuild(currentState, projectName, sessionId, sessionPath, specHash, options);
51
67
  } else if (options.start) {
52
68
  await handleBuildStart(currentState, projectName, sessionId, sessionPath, specHash);
@@ -216,6 +232,320 @@ ${evidence.screenshots > 0 ? ` ✅ ${evidence.screenshots} screenshots` : '
216
232
  }
217
233
  }
218
234
 
235
+ /**
236
+ * Handle --iterate mode: Build-Test-Fix loop
237
+ * "Build until tests pass or max iterations reached"
238
+ */
239
+ async function handleIterativeBuild(currentState, projectName, sessionId, sessionPath, specHash, options) {
240
+ const maxIterations = options.max || 3;
241
+ const strictMode = options.strict || false;
242
+
243
+ // Check state - must be PLAN_CREATED or BUILD_IN_PROGRESS or REVIEW_FAILED
244
+ const validStates = [STATES.PLAN_CREATED, STATES.BUILD_IN_PROGRESS, STATES.REVIEW_FAILED];
245
+ if (!validStates.includes(currentState)) {
246
+ printError(`Cannot iterate in state: ${currentState}`);
247
+ console.log('Run `vibecode plan` first to create execution plan.');
248
+ process.exit(1);
249
+ }
250
+
251
+ // Check if Claude Code is available
252
+ const available = await isClaudeCodeAvailable();
253
+ if (!available) {
254
+ printError('Claude Code CLI not found.');
255
+ console.log(chalk.gray('Install with: npm install -g @anthropic-ai/claude-code'));
256
+ process.exit(1);
257
+ }
258
+
259
+ // Check coder_pack.md exists
260
+ if (!await sessionFileExists('coder_pack.md')) {
261
+ printError('coder_pack.md not found. Run `vibecode plan` first.');
262
+ process.exit(1);
263
+ }
264
+
265
+ // Setup directories
266
+ const evidencePath = path.join(sessionPath, 'evidence');
267
+ await ensureDir(evidencePath);
268
+ await ensureDir(path.join(evidencePath, 'screenshots'));
269
+ const iterationDir = path.join(sessionPath, 'iterations');
270
+ await ensureDir(iterationDir);
271
+ const logPath = path.join(evidencePath, 'build.log');
272
+
273
+ // Initialize iteration state
274
+ let iterationState = createIterationState(sessionId, maxIterations);
275
+
276
+ // Save initial state
277
+ const stateData = await loadState();
278
+ const startTime = new Date().toISOString();
279
+ stateData.build_started = startTime;
280
+ stateData.build_provider = 'claude-code';
281
+ stateData.build_mode = 'iterate';
282
+ stateData.max_iterations = maxIterations;
283
+ await saveState(stateData);
284
+
285
+ // Transition to BUILD_IN_PROGRESS if not already
286
+ if (currentState !== STATES.BUILD_IN_PROGRESS) {
287
+ await transitionTo(STATES.BUILD_IN_PROGRESS, 'iterative_build_started');
288
+ }
289
+
290
+ // Load coder pack
291
+ const originalCoderPack = await readSessionFile('coder_pack.md');
292
+
293
+ // Log start
294
+ await appendToFile(logPath, `\n${'='.repeat(60)}\n`);
295
+ await appendToFile(logPath, `ITERATIVE BUILD STARTED: ${startTime}\n`);
296
+ await appendToFile(logPath, `Max Iterations: ${maxIterations}\n`);
297
+ await appendToFile(logPath, `Strict Mode: ${strictMode}\n`);
298
+ await appendToFile(logPath, `${'='.repeat(60)}\n\n`);
299
+
300
+ // Show starting message
301
+ const providerInfo = getProviderInfo();
302
+
303
+ const content = `🔄 ITERATIVE BUILD
304
+
305
+ Project: ${projectName}
306
+ Session: ${sessionId}
307
+ Spec Hash: ${specHash}
308
+
309
+ Provider: ${providerInfo.name}
310
+ Max Iterations: ${maxIterations}
311
+ Strict Mode: ${strictMode ? 'Yes' : 'No'}
312
+
313
+ Starting build-test-fix loop...`;
314
+
315
+ console.log();
316
+ printBox(content, { borderColor: 'magenta' });
317
+ console.log();
318
+
319
+ // Build-Test-Fix Loop
320
+ let currentPrompt = await buildPromptWithContext(originalCoderPack, process.cwd());
321
+ let loopResult = { success: false, reason: '' };
322
+
323
+ while (true) {
324
+ const iteration = iterationState.currentIteration + 1;
325
+ const iterationStart = Date.now();
326
+
327
+ console.log(chalk.cyan(`\n${'─'.repeat(60)}`));
328
+ console.log(chalk.cyan(`│ ITERATION ${iteration}/${maxIterations}`));
329
+ console.log(chalk.cyan(`${'─'.repeat(60)}\n`));
330
+
331
+ await logIteration(logPath, iteration, 'Starting iteration');
332
+
333
+ // Step 1: Run Claude Code
334
+ console.log(chalk.yellow('▶ Running Claude Code...'));
335
+ console.log();
336
+
337
+ try {
338
+ const buildResult = await spawnClaudeCode(currentPrompt, {
339
+ cwd: process.cwd(),
340
+ logPath: logPath
341
+ });
342
+
343
+ console.log();
344
+ await logIteration(logPath, iteration, `Claude Code exited with code: ${buildResult.code}`);
345
+
346
+ // Capture evidence for this iteration
347
+ await captureGitDiff(evidencePath);
348
+ const iterEvidencePath = path.join(iterationDir, `iteration-${iteration}-diff.txt`);
349
+ try {
350
+ const { stdout } = await execAsync('git diff HEAD', { maxBuffer: 10 * 1024 * 1024 });
351
+ if (stdout.trim()) {
352
+ const fs = await import('fs-extra');
353
+ await fs.default.writeFile(iterEvidencePath, stdout, 'utf-8');
354
+ }
355
+ } catch (e) { /* ignore */ }
356
+
357
+ // Step 2: Run Tests
358
+ console.log(chalk.yellow('▶ Running tests...'));
359
+ const spinner = ora('Testing...').start();
360
+
361
+ const testResult = await runTests(process.cwd());
362
+ const iterationDuration = Date.now() - iterationStart;
363
+
364
+ if (testResult.passed) {
365
+ spinner.succeed('All tests passed!');
366
+
367
+ // Record successful iteration
368
+ iterationState = recordIteration(iterationState, {
369
+ passed: true,
370
+ errorCount: 0,
371
+ errorTypes: [],
372
+ affectedFiles: [],
373
+ duration: iterationDuration,
374
+ action: 'build'
375
+ });
376
+
377
+ // Finalize as success
378
+ iterationState = finalizeIterationState(iterationState, 'success');
379
+ await saveIterationState(sessionPath, iterationState);
380
+
381
+ loopResult = { success: true, reason: 'All tests passed' };
382
+ break;
383
+
384
+ } else {
385
+ spinner.fail(`Tests failed: ${testResult.summary.failed}/${testResult.summary.total}`);
386
+
387
+ // Step 3: Analyze Errors
388
+ const analyzedErrors = analyzeErrors(testResult);
389
+ const summary = createErrorSummary(analyzedErrors);
390
+
391
+ console.log();
392
+ console.log(formatErrors(analyzedErrors));
393
+
394
+ await logIteration(logPath, iteration, `Found ${analyzedErrors.length} errors`);
395
+
396
+ // Save error analysis for this iteration
397
+ await writeJson(path.join(iterationDir, `iteration-${iteration}-errors.json`), {
398
+ iteration,
399
+ timestamp: new Date().toISOString(),
400
+ summary,
401
+ errors: analyzedErrors
402
+ });
403
+
404
+ // Record failed iteration
405
+ iterationState = recordIteration(iterationState, {
406
+ passed: false,
407
+ errorCount: analyzedErrors.length,
408
+ errorTypes: [...new Set(analyzedErrors.map(e => e.type))],
409
+ affectedFiles: [...new Set(analyzedErrors.filter(e => e.file).map(e => e.file))],
410
+ duration: iterationDuration,
411
+ action: 'build'
412
+ });
413
+
414
+ // Check if errors are fixable
415
+ const fixableCheck = areErrorsFixable(analyzedErrors);
416
+ if (!fixableCheck.fixable) {
417
+ console.log(chalk.red(`\n⚠️ ${fixableCheck.reason}`));
418
+ await logIteration(logPath, iteration, `Errors not fixable: ${fixableCheck.reason}`);
419
+
420
+ iterationState = finalizeIterationState(iterationState, 'unfixable');
421
+ await saveIterationState(sessionPath, iterationState);
422
+
423
+ loopResult = { success: false, reason: fixableCheck.reason };
424
+ break;
425
+ }
426
+
427
+ // Check if we can continue
428
+ const continueCheck = canContinue(iterationState);
429
+ if (!continueCheck.canContinue) {
430
+ console.log(chalk.yellow(`\n⚠️ ${continueCheck.reason}`));
431
+ await logIteration(logPath, iteration, continueCheck.reason);
432
+
433
+ iterationState = finalizeIterationState(iterationState, 'max_reached');
434
+ await saveIterationState(sessionPath, iterationState);
435
+
436
+ loopResult = { success: false, reason: continueCheck.reason };
437
+ break;
438
+ }
439
+
440
+ // Step 4: Generate Fix Prompt
441
+ const complexity = estimateFixComplexity(analyzedErrors);
442
+ console.log(chalk.gray(`\nFix complexity: ${complexity}`));
443
+ console.log(chalk.yellow(`\n▶ Generating fix prompt for iteration ${iteration + 1}...`));
444
+
445
+ currentPrompt = generateFixPrompt(analyzedErrors, originalCoderPack, iteration + 1);
446
+
447
+ // Save fix prompt for evidence
448
+ await writeSessionFile(`iterations/fix-prompt-${iteration + 1}.md`, currentPrompt);
449
+
450
+ await logIteration(logPath, iteration, 'Generated fix prompt for next iteration');
451
+ }
452
+
453
+ } catch (error) {
454
+ console.log(chalk.red(`\n❌ Build error: ${error.message}`));
455
+ await logIteration(logPath, iteration, `Error: ${error.message}`);
456
+
457
+ iterationState = finalizeIterationState(iterationState, 'error');
458
+ await saveIterationState(sessionPath, iterationState);
459
+
460
+ loopResult = { success: false, reason: error.message };
461
+ break;
462
+ }
463
+ }
464
+
465
+ // Final Summary
466
+ const endTime = new Date().toISOString();
467
+ await appendToFile(logPath, `\n${'='.repeat(60)}\n`);
468
+ await appendToFile(logPath, `ITERATIVE BUILD COMPLETED: ${endTime}\n`);
469
+ await appendToFile(logPath, `Result: ${loopResult.success ? 'SUCCESS' : 'FAILED'}\n`);
470
+ await appendToFile(logPath, `Iterations: ${iterationState.currentIteration}\n`);
471
+ await appendToFile(logPath, `${'='.repeat(60)}\n`);
472
+
473
+ // Save final iteration state
474
+ await saveIterationState(sessionPath, iterationState);
475
+
476
+ // Check evidence
477
+ const evidence = await checkEvidence(evidencePath);
478
+
479
+ // Generate build report
480
+ const reportContent = getBuildReportTemplate(
481
+ projectName,
482
+ sessionId,
483
+ specHash,
484
+ startTime,
485
+ endTime,
486
+ evidence
487
+ );
488
+ await writeSessionFile('build_report.md', reportContent);
489
+
490
+ // Update state
491
+ stateData.build_completed = endTime;
492
+ stateData.iterations = iterationState.currentIteration;
493
+ stateData.iteration_result = loopResult.success ? 'success' : 'failed';
494
+ await saveState(stateData);
495
+
496
+ console.log();
497
+ console.log(chalk.cyan('─'.repeat(60)));
498
+ console.log();
499
+
500
+ if (loopResult.success) {
501
+ await transitionTo(STATES.BUILD_DONE, 'iterative_build_success');
502
+
503
+ const duration = Math.round((new Date(endTime) - new Date(startTime)) / 1000 / 60);
504
+
505
+ const successContent = `✅ ITERATIVE BUILD SUCCESS
506
+
507
+ Project: ${projectName}
508
+ Iterations: ${iterationState.currentIteration}/${maxIterations}
509
+ Duration: ${duration} minutes
510
+ Result: All tests passed!
511
+
512
+ Evidence:
513
+ ${evidence.hasDiff ? ' ✅ changes.diff' : ' ⬜ changes.diff'}
514
+ ${evidence.hasLog ? ' ✅ build.log' : ' ⬜ build.log'}
515
+ ✅ ${iterationState.currentIteration} iteration records`;
516
+
517
+ printBox(successContent, { borderColor: 'green' });
518
+ printNextStep('Run `vibecode review` to validate your build');
519
+
520
+ } else {
521
+ // Still transition to BUILD_DONE but with failure note
522
+ await transitionTo(STATES.BUILD_DONE, 'iterative_build_completed_with_errors');
523
+
524
+ const failContent = `⚠️ ITERATIVE BUILD INCOMPLETE
525
+
526
+ Project: ${projectName}
527
+ Iterations: ${iterationState.currentIteration}/${maxIterations}
528
+ Result: ${loopResult.reason}
529
+
530
+ Evidence saved in:
531
+ ${iterationDir}/
532
+
533
+ Check iteration logs for details.`;
534
+
535
+ printBox(failContent, { borderColor: 'yellow' });
536
+
537
+ if (strictMode) {
538
+ printError('Strict mode: Build failed with errors');
539
+ process.exit(1);
540
+ } else {
541
+ console.log(chalk.gray('\nYou can:'));
542
+ console.log(chalk.gray(' • Run `vibecode build --iterate` to try again'));
543
+ console.log(chalk.gray(' • Run `vibecode review` to review current state'));
544
+ console.log(chalk.gray(' • Fix errors manually and run `vibecode build --complete`'));
545
+ }
546
+ }
547
+ }
548
+
219
549
  async function handleBuildStart(currentState, projectName, sessionId, sessionPath, specHash) {
220
550
  const spinner = ora('Starting build...').start();
221
551
 
@@ -3,7 +3,7 @@
3
3
  // Spec Hash: 0fe43335f5a325e3279a079ce616c052
4
4
  // ═══════════════════════════════════════════════════════════════════════════════
5
5
 
6
- export const VERSION = '2.0.0';
6
+ export const VERSION = '1.0.1';
7
7
  export const SPEC_HASH = '0fe43335f5a325e3279a079ce616c052';
8
8
 
9
9
  // ─────────────────────────────────────────────────────────────────────────────
@@ -0,0 +1,237 @@
1
+ // ═══════════════════════════════════════════════════════════════════════════════
2
+ // VIBECODE CLI - Error Analyzer
3
+ // Intelligent error analysis for iterative builds
4
+ // ═══════════════════════════════════════════════════════════════════════════════
5
+
6
+ /**
7
+ * Analyze test results and extract actionable errors
8
+ * @param {TestResults} testResult - Results from test runner
9
+ * @returns {AnalyzedError[]}
10
+ */
11
+ export function analyzeErrors(testResult) {
12
+ const errors = [];
13
+
14
+ for (const test of testResult.tests) {
15
+ if (!test.passed) {
16
+ const testErrors = test.errors || [];
17
+
18
+ for (const error of testErrors) {
19
+ errors.push({
20
+ source: test.name,
21
+ type: categorizeError(error),
22
+ file: error.file || null,
23
+ line: error.line || null,
24
+ column: error.column || null,
25
+ message: error.message,
26
+ suggestion: generateSuggestion(error),
27
+ priority: calculatePriority(error),
28
+ raw: error.raw
29
+ });
30
+ }
31
+
32
+ // If no specific errors but test failed, add generic error
33
+ if (testErrors.length === 0) {
34
+ errors.push({
35
+ source: test.name,
36
+ type: 'unknown',
37
+ message: test.error || `${test.name} failed`,
38
+ suggestion: `Check ${test.name} output for details`,
39
+ priority: 'medium',
40
+ raw: test.output
41
+ });
42
+ }
43
+ }
44
+ }
45
+
46
+ // Sort by priority
47
+ return errors.sort((a, b) => {
48
+ const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
49
+ return (priorityOrder[a.priority] || 2) - (priorityOrder[b.priority] || 2);
50
+ });
51
+ }
52
+
53
+ /**
54
+ * Categorize error type
55
+ */
56
+ function categorizeError(error) {
57
+ const message = (error.message || '').toLowerCase();
58
+ const raw = (error.raw || '').toLowerCase();
59
+ const combined = message + ' ' + raw;
60
+
61
+ if (combined.includes('syntaxerror') || combined.includes('unexpected token')) {
62
+ return 'syntax';
63
+ }
64
+ if (combined.includes('typeerror') || combined.includes('type error') || combined.includes('is not a function')) {
65
+ return 'type';
66
+ }
67
+ if (combined.includes('referenceerror') || combined.includes('is not defined')) {
68
+ return 'reference';
69
+ }
70
+ if (combined.includes('import') || combined.includes('require') || combined.includes('module not found')) {
71
+ return 'import';
72
+ }
73
+ if (combined.includes('eslint') || combined.includes('lint')) {
74
+ return 'lint';
75
+ }
76
+ if (combined.includes('test') || combined.includes('expect') || combined.includes('assert')) {
77
+ return 'test';
78
+ }
79
+ if (combined.includes('typescript') || combined.includes('ts(')) {
80
+ return 'typescript';
81
+ }
82
+ if (combined.includes('build') || combined.includes('compile')) {
83
+ return 'build';
84
+ }
85
+
86
+ return 'unknown';
87
+ }
88
+
89
+ /**
90
+ * Generate fix suggestion based on error type
91
+ */
92
+ function generateSuggestion(error) {
93
+ const type = categorizeError(error);
94
+ const message = error.message || '';
95
+
96
+ switch (type) {
97
+ case 'syntax':
98
+ return 'Check for missing brackets, semicolons, or typos near the error location';
99
+
100
+ case 'type':
101
+ if (message.includes('undefined')) {
102
+ return 'Check if the variable/property is properly initialized';
103
+ }
104
+ if (message.includes('is not a function')) {
105
+ return 'Verify the function exists and is properly imported';
106
+ }
107
+ return 'Check type compatibility and ensure proper type handling';
108
+
109
+ case 'reference':
110
+ return 'Ensure the variable/function is defined or imported before use';
111
+
112
+ case 'import':
113
+ if (message.includes('module not found')) {
114
+ return 'Install missing package with npm install or fix import path';
115
+ }
116
+ return 'Check import path and ensure the module exports correctly';
117
+
118
+ case 'lint':
119
+ return 'Fix the linting issue as specified in the error message';
120
+
121
+ case 'test':
122
+ return 'Update the implementation to match expected behavior, or fix the test assertion';
123
+
124
+ case 'typescript':
125
+ return 'Fix type errors by adding proper types or type guards';
126
+
127
+ case 'build':
128
+ return 'Check build configuration and dependencies';
129
+
130
+ default:
131
+ return 'Review the error message and fix accordingly';
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Calculate error priority
137
+ */
138
+ function calculatePriority(error) {
139
+ const type = categorizeError(error);
140
+
141
+ // Critical - blocks everything
142
+ if (type === 'syntax' || type === 'import') {
143
+ return 'critical';
144
+ }
145
+
146
+ // High - likely causes cascading failures
147
+ if (type === 'reference' || type === 'type') {
148
+ return 'high';
149
+ }
150
+
151
+ // Medium - should be fixed
152
+ if (type === 'typescript' || type === 'build' || type === 'test') {
153
+ return 'medium';
154
+ }
155
+
156
+ // Low - nice to fix
157
+ if (type === 'lint') {
158
+ return 'low';
159
+ }
160
+
161
+ return 'medium';
162
+ }
163
+
164
+ /**
165
+ * Group errors by file for better organization
166
+ */
167
+ export function groupErrorsByFile(errors) {
168
+ const grouped = {};
169
+
170
+ for (const error of errors) {
171
+ const file = error.file || 'unknown';
172
+ if (!grouped[file]) {
173
+ grouped[file] = [];
174
+ }
175
+ grouped[file].push(error);
176
+ }
177
+
178
+ return grouped;
179
+ }
180
+
181
+ /**
182
+ * Get unique files with errors
183
+ */
184
+ export function getAffectedFiles(errors) {
185
+ const files = new Set();
186
+ for (const error of errors) {
187
+ if (error.file) {
188
+ files.add(error.file);
189
+ }
190
+ }
191
+ return Array.from(files);
192
+ }
193
+
194
+ /**
195
+ * Format errors for display
196
+ */
197
+ export function formatErrors(errors) {
198
+ const lines = [];
199
+
200
+ lines.push(`Found ${errors.length} error(s):`);
201
+ lines.push('');
202
+
203
+ const grouped = groupErrorsByFile(errors);
204
+
205
+ for (const [file, fileErrors] of Object.entries(grouped)) {
206
+ lines.push(`📄 ${file}`);
207
+ for (const error of fileErrors) {
208
+ const loc = error.line ? `:${error.line}` : '';
209
+ const priority = error.priority === 'critical' ? '🔴' :
210
+ error.priority === 'high' ? '🟠' :
211
+ error.priority === 'medium' ? '🟡' : '🟢';
212
+ lines.push(` ${priority} ${error.type}: ${error.message?.substring(0, 60) || 'Unknown error'}`);
213
+ }
214
+ }
215
+
216
+ return lines.join('\n');
217
+ }
218
+
219
+ /**
220
+ * Create a summary of errors for logging
221
+ */
222
+ export function createErrorSummary(errors) {
223
+ const byType = {};
224
+ const byPriority = { critical: 0, high: 0, medium: 0, low: 0 };
225
+
226
+ for (const error of errors) {
227
+ byType[error.type] = (byType[error.type] || 0) + 1;
228
+ byPriority[error.priority] = (byPriority[error.priority] || 0) + 1;
229
+ }
230
+
231
+ return {
232
+ total: errors.length,
233
+ byType,
234
+ byPriority,
235
+ files: getAffectedFiles(errors)
236
+ };
237
+ }
@@ -0,0 +1,195 @@
1
+ // ═══════════════════════════════════════════════════════════════════════════════
2
+ // VIBECODE CLI - Fix Generator
3
+ // Generate fix prompts for iterative builds
4
+ // ═══════════════════════════════════════════════════════════════════════════════
5
+
6
+ import { formatErrors, createErrorSummary } from './error-analyzer.js';
7
+
8
+ /**
9
+ * Generate a fix prompt based on errors from previous iteration
10
+ * @param {AnalyzedError[]} errors - Analyzed errors from test runner
11
+ * @param {string} originalPack - Original coder pack content
12
+ * @param {number} iteration - Current iteration number
13
+ * @returns {string} - Fix prompt for Claude Code
14
+ */
15
+ export function generateFixPrompt(errors, originalPack, iteration = 1) {
16
+ const summary = createErrorSummary(errors);
17
+
18
+ const sections = [
19
+ '# 🔧 FIX REQUIRED - Iteration ' + iteration,
20
+ '',
21
+ `The previous build had **${errors.length} error(s)**. Please fix them.`,
22
+ '',
23
+ '---',
24
+ '',
25
+ '## 📋 Error Summary',
26
+ '',
27
+ `- **Total Errors:** ${summary.total}`,
28
+ `- **Critical:** ${summary.byPriority.critical}`,
29
+ `- **High:** ${summary.byPriority.high}`,
30
+ `- **Medium:** ${summary.byPriority.medium}`,
31
+ `- **Low:** ${summary.byPriority.low}`,
32
+ '',
33
+ `**Affected Files:** ${summary.files.length > 0 ? summary.files.join(', ') : 'Unknown'}`,
34
+ '',
35
+ '---',
36
+ '',
37
+ '## 🚨 Errors to Fix',
38
+ '',
39
+ ];
40
+
41
+ // Add detailed errors grouped by priority
42
+ const byPriority = groupByPriority(errors);
43
+
44
+ for (const [priority, priorityErrors] of Object.entries(byPriority)) {
45
+ if (priorityErrors.length === 0) continue;
46
+
47
+ const emoji = priority === 'critical' ? '🔴' :
48
+ priority === 'high' ? '🟠' :
49
+ priority === 'medium' ? '🟡' : '🟢';
50
+
51
+ sections.push(`### ${emoji} ${priority.toUpperCase()} Priority`);
52
+ sections.push('');
53
+
54
+ for (const error of priorityErrors) {
55
+ const location = error.file
56
+ ? `\`${error.file}${error.line ? ':' + error.line : ''}\``
57
+ : 'Unknown location';
58
+
59
+ sections.push(`**${error.type}** at ${location}`);
60
+ sections.push(`- Message: ${error.message}`);
61
+ sections.push(`- Suggestion: ${error.suggestion}`);
62
+ if (error.raw && error.raw !== error.message) {
63
+ sections.push(`- Raw output: \`${truncate(error.raw, 200)}\``);
64
+ }
65
+ sections.push('');
66
+ }
67
+ }
68
+
69
+ sections.push('---');
70
+ sections.push('');
71
+ sections.push('## 📝 Original Task Reference');
72
+ sections.push('');
73
+ sections.push('<details>');
74
+ sections.push('<summary>Click to expand original task</summary>');
75
+ sections.push('');
76
+ sections.push(originalPack);
77
+ sections.push('');
78
+ sections.push('</details>');
79
+ sections.push('');
80
+ sections.push('---');
81
+ sections.push('');
82
+ sections.push('## ⚡ Fix Instructions');
83
+ sections.push('');
84
+ sections.push('1. **Fix ONLY the errors listed above** - Do not refactor or change working code');
85
+ sections.push('2. **Start with CRITICAL errors** - They likely cause cascading failures');
86
+ sections.push('3. **Run tests after each fix** - Verify the error is resolved');
87
+ sections.push('4. **Keep changes minimal** - Focus on the specific issue');
88
+ sections.push('');
89
+ sections.push('When all errors are fixed, the build will be validated again.');
90
+ sections.push('');
91
+
92
+ return sections.join('\n');
93
+ }
94
+
95
+ /**
96
+ * Generate a minimal fix prompt for single error
97
+ */
98
+ export function generateSingleFixPrompt(error) {
99
+ const location = error.file
100
+ ? `${error.file}${error.line ? ':' + error.line : ''}`
101
+ : 'unknown location';
102
+
103
+ return `# Fix Required
104
+
105
+ **Error Type:** ${error.type}
106
+ **Location:** ${location}
107
+ **Message:** ${error.message}
108
+
109
+ **Suggestion:** ${error.suggestion}
110
+
111
+ Please fix this specific error. Keep the change minimal and focused.`;
112
+ }
113
+
114
+ /**
115
+ * Generate iteration context for logging
116
+ */
117
+ export function generateIterationContext(iteration, errors, duration) {
118
+ return {
119
+ iteration,
120
+ timestamp: new Date().toISOString(),
121
+ errorCount: errors.length,
122
+ errorTypes: [...new Set(errors.map(e => e.type))],
123
+ affectedFiles: [...new Set(errors.filter(e => e.file).map(e => e.file))],
124
+ duration
125
+ };
126
+ }
127
+
128
+ /**
129
+ * Group errors by priority
130
+ */
131
+ function groupByPriority(errors) {
132
+ const grouped = {
133
+ critical: [],
134
+ high: [],
135
+ medium: [],
136
+ low: []
137
+ };
138
+
139
+ for (const error of errors) {
140
+ const priority = error.priority || 'medium';
141
+ if (grouped[priority]) {
142
+ grouped[priority].push(error);
143
+ } else {
144
+ grouped.medium.push(error);
145
+ }
146
+ }
147
+
148
+ return grouped;
149
+ }
150
+
151
+ /**
152
+ * Truncate string with ellipsis
153
+ */
154
+ function truncate(str, maxLen) {
155
+ if (!str) return '';
156
+ if (str.length <= maxLen) return str;
157
+ return str.substring(0, maxLen - 3) + '...';
158
+ }
159
+
160
+ /**
161
+ * Check if errors are fixable (not system/config errors)
162
+ */
163
+ export function areErrorsFixable(errors) {
164
+ // If all errors are unknown type with no file info, might be config issue
165
+ const unknownWithoutFile = errors.filter(e => e.type === 'unknown' && !e.file);
166
+
167
+ if (unknownWithoutFile.length === errors.length) {
168
+ return {
169
+ fixable: false,
170
+ reason: 'All errors are unstructured with no file information. This may indicate a configuration or environment issue.'
171
+ };
172
+ }
173
+
174
+ return { fixable: true };
175
+ }
176
+
177
+ /**
178
+ * Estimate fix complexity
179
+ */
180
+ export function estimateFixComplexity(errors) {
181
+ let score = 0;
182
+
183
+ for (const error of errors) {
184
+ switch (error.priority) {
185
+ case 'critical': score += 3; break;
186
+ case 'high': score += 2; break;
187
+ case 'medium': score += 1; break;
188
+ case 'low': score += 0.5; break;
189
+ }
190
+ }
191
+
192
+ if (score <= 3) return 'simple';
193
+ if (score <= 8) return 'moderate';
194
+ return 'complex';
195
+ }
@@ -0,0 +1,226 @@
1
+ // ═══════════════════════════════════════════════════════════════════════════════
2
+ // VIBECODE CLI - Iteration Tracker
3
+ // Manage and track build-test-fix iterations
4
+ // ═══════════════════════════════════════════════════════════════════════════════
5
+
6
+ import path from 'path';
7
+ import { ensureDir, writeJson, readJson, pathExists, appendToFile } from '../utils/files.js';
8
+
9
+ /**
10
+ * @typedef {Object} IterationRecord
11
+ * @property {number} iteration - Iteration number
12
+ * @property {string} timestamp - ISO timestamp
13
+ * @property {boolean} passed - Whether tests passed
14
+ * @property {number} errorCount - Number of errors
15
+ * @property {string[]} errorTypes - Types of errors found
16
+ * @property {string[]} affectedFiles - Files with errors
17
+ * @property {number} duration - Duration in ms
18
+ * @property {string} action - What action was taken (build/fix)
19
+ */
20
+
21
+ /**
22
+ * @typedef {Object} IterationState
23
+ * @property {string} sessionId - Session identifier
24
+ * @property {string} startTime - When iteration started
25
+ * @property {number} currentIteration - Current iteration number
26
+ * @property {number} maxIterations - Max allowed iterations
27
+ * @property {IterationRecord[]} history - History of iterations
28
+ * @property {boolean} completed - Whether iteration loop completed
29
+ * @property {string} result - Final result (success/max_reached/error)
30
+ */
31
+
32
+ /**
33
+ * Create new iteration state
34
+ * @param {string} sessionId - Session identifier
35
+ * @param {number} maxIterations - Maximum iterations allowed
36
+ * @returns {IterationState}
37
+ */
38
+ export function createIterationState(sessionId, maxIterations = 3) {
39
+ return {
40
+ sessionId,
41
+ startTime: new Date().toISOString(),
42
+ currentIteration: 0,
43
+ maxIterations,
44
+ history: [],
45
+ completed: false,
46
+ result: null
47
+ };
48
+ }
49
+
50
+ /**
51
+ * Record an iteration result
52
+ * @param {IterationState} state - Current iteration state
53
+ * @param {Object} result - Iteration result
54
+ * @returns {IterationState} - Updated state
55
+ */
56
+ export function recordIteration(state, result) {
57
+ const record = {
58
+ iteration: state.currentIteration + 1,
59
+ timestamp: new Date().toISOString(),
60
+ passed: result.passed,
61
+ errorCount: result.errorCount || 0,
62
+ errorTypes: result.errorTypes || [],
63
+ affectedFiles: result.affectedFiles || [],
64
+ duration: result.duration || 0,
65
+ action: result.action || 'build'
66
+ };
67
+
68
+ return {
69
+ ...state,
70
+ currentIteration: state.currentIteration + 1,
71
+ history: [...state.history, record]
72
+ };
73
+ }
74
+
75
+ /**
76
+ * Check if can continue iterating
77
+ * @param {IterationState} state - Current state
78
+ * @returns {{canContinue: boolean, reason: string}}
79
+ */
80
+ export function canContinue(state) {
81
+ if (state.completed) {
82
+ return { canContinue: false, reason: 'Iteration already completed' };
83
+ }
84
+
85
+ if (state.currentIteration >= state.maxIterations) {
86
+ return { canContinue: false, reason: `Max iterations (${state.maxIterations}) reached` };
87
+ }
88
+
89
+ // Check if last iteration passed
90
+ const lastRecord = state.history[state.history.length - 1];
91
+ if (lastRecord && lastRecord.passed) {
92
+ return { canContinue: false, reason: 'Tests passed - no more iterations needed' };
93
+ }
94
+
95
+ // Check for stuck loop (same errors repeated 3 times)
96
+ if (state.history.length >= 3) {
97
+ const last3 = state.history.slice(-3);
98
+ const errorCounts = last3.map(r => r.errorCount);
99
+ if (errorCounts.every(c => c === errorCounts[0]) && errorCounts[0] > 0) {
100
+ return { canContinue: false, reason: 'Stuck in loop - same error count for 3 iterations' };
101
+ }
102
+ }
103
+
104
+ return { canContinue: true, reason: '' };
105
+ }
106
+
107
+ /**
108
+ * Finalize iteration state
109
+ * @param {IterationState} state - Current state
110
+ * @param {string} result - Result type (success/max_reached/error/stuck)
111
+ * @returns {IterationState}
112
+ */
113
+ export function finalizeIterationState(state, result) {
114
+ return {
115
+ ...state,
116
+ completed: true,
117
+ result,
118
+ endTime: new Date().toISOString(),
119
+ totalDuration: state.history.reduce((sum, r) => sum + r.duration, 0)
120
+ };
121
+ }
122
+
123
+ /**
124
+ * Save iteration state to session directory
125
+ * @param {string} sessionDir - Session directory path
126
+ * @param {IterationState} state - Iteration state
127
+ */
128
+ export async function saveIterationState(sessionDir, state) {
129
+ const iterationDir = path.join(sessionDir, 'iterations');
130
+ await ensureDir(iterationDir);
131
+
132
+ const stateFile = path.join(iterationDir, 'state.json');
133
+ await writeJson(stateFile, state, { spaces: 2 });
134
+
135
+ // Also write individual iteration files for evidence
136
+ for (const record of state.history) {
137
+ const recordFile = path.join(iterationDir, `iteration-${record.iteration}.json`);
138
+ if (!await pathExists(recordFile)) {
139
+ await writeJson(recordFile, record, { spaces: 2 });
140
+ }
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Load iteration state from session directory
146
+ * @param {string} sessionDir - Session directory path
147
+ * @returns {Promise<IterationState|null>}
148
+ */
149
+ export async function loadIterationState(sessionDir) {
150
+ const stateFile = path.join(sessionDir, 'iterations', 'state.json');
151
+ if (await pathExists(stateFile)) {
152
+ return await readJson(stateFile);
153
+ }
154
+ return null;
155
+ }
156
+
157
+ /**
158
+ * Format iteration summary for display
159
+ * @param {IterationState} state - Iteration state
160
+ * @returns {string}
161
+ */
162
+ export function formatIterationSummary(state) {
163
+ const lines = [];
164
+
165
+ lines.push(`Iteration Summary (${state.sessionId})`);
166
+ lines.push('═'.repeat(50));
167
+ lines.push(`Total Iterations: ${state.currentIteration}/${state.maxIterations}`);
168
+ lines.push(`Result: ${state.result || 'In Progress'}`);
169
+ lines.push('');
170
+
171
+ if (state.history.length > 0) {
172
+ lines.push('History:');
173
+ for (const record of state.history) {
174
+ const status = record.passed ? '✅' : '❌';
175
+ const errors = record.errorCount > 0 ? ` (${record.errorCount} errors)` : '';
176
+ lines.push(` ${record.iteration}. ${status} ${record.action}${errors} - ${formatDuration(record.duration)}`);
177
+ }
178
+ }
179
+
180
+ if (state.totalDuration) {
181
+ lines.push('');
182
+ lines.push(`Total Duration: ${formatDuration(state.totalDuration)}`);
183
+ }
184
+
185
+ return lines.join('\n');
186
+ }
187
+
188
+ /**
189
+ * Format duration in human readable format
190
+ */
191
+ function formatDuration(ms) {
192
+ if (ms < 1000) return `${ms}ms`;
193
+ if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
194
+ return `${Math.floor(ms / 60000)}m ${Math.round((ms % 60000) / 1000)}s`;
195
+ }
196
+
197
+ /**
198
+ * Write iteration log entry
199
+ */
200
+ export async function logIteration(logPath, iteration, message) {
201
+ const timestamp = new Date().toISOString();
202
+ const entry = `[${timestamp}] [Iteration ${iteration}] ${message}\n`;
203
+ await appendToFile(logPath, entry);
204
+ }
205
+
206
+ /**
207
+ * Get progress percentage
208
+ */
209
+ export function getProgressPercent(state) {
210
+ if (state.completed && state.result === 'success') {
211
+ return 100;
212
+ }
213
+ return Math.round((state.currentIteration / state.maxIterations) * 100);
214
+ }
215
+
216
+ /**
217
+ * Check if errors are improving (decreasing)
218
+ */
219
+ export function isImproving(state) {
220
+ if (state.history.length < 2) return true;
221
+
222
+ const last = state.history[state.history.length - 1];
223
+ const prev = state.history[state.history.length - 2];
224
+
225
+ return last.errorCount < prev.errorCount;
226
+ }
@@ -0,0 +1,248 @@
1
+ // ═══════════════════════════════════════════════════════════════════════════════
2
+ // VIBECODE CLI - Test Runner
3
+ // Automated test execution for iterative builds
4
+ // ═══════════════════════════════════════════════════════════════════════════════
5
+
6
+ import { exec } from 'child_process';
7
+ import { promisify } from 'util';
8
+ import path from 'path';
9
+ import { pathExists, readJson } from '../utils/files.js';
10
+
11
+ const execAsync = promisify(exec);
12
+
13
+ /**
14
+ * Run all available tests for a project
15
+ * @param {string} projectPath - Path to the project
16
+ * @returns {Promise<TestResults>}
17
+ */
18
+ export async function runTests(projectPath) {
19
+ const results = {
20
+ passed: true,
21
+ tests: [],
22
+ errors: [],
23
+ summary: {
24
+ total: 0,
25
+ passed: 0,
26
+ failed: 0
27
+ },
28
+ duration: 0
29
+ };
30
+
31
+ const startTime = Date.now();
32
+
33
+ // 1. Check if package.json exists
34
+ const packageJsonPath = path.join(projectPath, 'package.json');
35
+ const hasPackageJson = await pathExists(packageJsonPath);
36
+
37
+ if (hasPackageJson) {
38
+ const pkg = await readJson(packageJsonPath);
39
+
40
+ // 2. Run npm test (if script exists)
41
+ if (pkg.scripts?.test && !pkg.scripts.test.includes('no test specified')) {
42
+ const npmTest = await runCommand('npm test', projectPath, 'npm test');
43
+ results.tests.push(npmTest);
44
+ }
45
+
46
+ // 3. Run npm run lint (if script exists)
47
+ if (pkg.scripts?.lint) {
48
+ const npmLint = await runCommand('npm run lint', projectPath, 'npm lint');
49
+ results.tests.push(npmLint);
50
+ }
51
+
52
+ // 4. Run npm run build (if script exists) - check for build errors
53
+ if (pkg.scripts?.build) {
54
+ const npmBuild = await runCommand('npm run build', projectPath, 'npm build');
55
+ results.tests.push(npmBuild);
56
+ }
57
+
58
+ // 5. Run TypeScript check if tsconfig exists
59
+ const tsconfigPath = path.join(projectPath, 'tsconfig.json');
60
+ if (await pathExists(tsconfigPath)) {
61
+ const tscCheck = await runCommand('npx tsc --noEmit', projectPath, 'typescript');
62
+ results.tests.push(tscCheck);
63
+ }
64
+ }
65
+
66
+ // 6. Check for syntax errors in JS files
67
+ const syntaxCheck = await checkJsSyntax(projectPath);
68
+ if (syntaxCheck.ran) {
69
+ results.tests.push(syntaxCheck);
70
+ }
71
+
72
+ // 7. Aggregate results
73
+ results.summary.total = results.tests.length;
74
+ results.summary.passed = results.tests.filter(t => t.passed).length;
75
+ results.summary.failed = results.tests.filter(t => !t.passed).length;
76
+ results.passed = results.tests.length === 0 || results.tests.every(t => t.passed);
77
+ results.errors = results.tests.filter(t => !t.passed).flatMap(t => t.errors || []);
78
+ results.duration = Date.now() - startTime;
79
+
80
+ return results;
81
+ }
82
+
83
+ /**
84
+ * Run a single command and capture results
85
+ */
86
+ async function runCommand(command, cwd, name) {
87
+ const result = {
88
+ name,
89
+ command,
90
+ passed: false,
91
+ ran: true,
92
+ output: '',
93
+ errors: [],
94
+ duration: 0
95
+ };
96
+
97
+ const startTime = Date.now();
98
+
99
+ try {
100
+ const { stdout, stderr } = await execAsync(command, {
101
+ cwd,
102
+ timeout: 120000, // 2 minute timeout
103
+ maxBuffer: 10 * 1024 * 1024
104
+ });
105
+
106
+ result.passed = true;
107
+ result.output = stdout + stderr;
108
+ result.duration = Date.now() - startTime;
109
+
110
+ } catch (error) {
111
+ result.passed = false;
112
+ result.output = error.stdout || '';
113
+ result.error = error.stderr || error.message;
114
+ result.exitCode = error.code;
115
+ result.duration = Date.now() - startTime;
116
+
117
+ // Parse errors from output
118
+ result.errors = parseErrors(error.stderr || error.stdout || error.message, name);
119
+ }
120
+
121
+ return result;
122
+ }
123
+
124
+ /**
125
+ * Check JavaScript syntax errors
126
+ */
127
+ async function checkJsSyntax(projectPath) {
128
+ const result = {
129
+ name: 'syntax-check',
130
+ passed: true,
131
+ ran: false,
132
+ errors: []
133
+ };
134
+
135
+ try {
136
+ // Find JS/TS files (limited to src/ to avoid node_modules)
137
+ const { stdout } = await execAsync(
138
+ 'find src -name "*.js" -o -name "*.ts" -o -name "*.jsx" -o -name "*.tsx" 2>/dev/null | head -20',
139
+ { cwd: projectPath }
140
+ );
141
+
142
+ const files = stdout.trim().split('\n').filter(f => f);
143
+ if (files.length === 0) return result;
144
+
145
+ result.ran = true;
146
+
147
+ // Check each file for syntax errors using node --check
148
+ for (const file of files) {
149
+ if (file.endsWith('.ts') || file.endsWith('.tsx')) continue; // Skip TS files
150
+
151
+ try {
152
+ await execAsync(`node --check "${file}"`, { cwd: projectPath });
153
+ } catch (error) {
154
+ result.passed = false;
155
+ result.errors.push({
156
+ file,
157
+ message: error.message,
158
+ type: 'syntax'
159
+ });
160
+ }
161
+ }
162
+ } catch (error) {
163
+ // find command failed, skip syntax check
164
+ }
165
+
166
+ return result;
167
+ }
168
+
169
+ /**
170
+ * Parse error messages into structured format
171
+ */
172
+ function parseErrors(errorOutput, source) {
173
+ const errors = [];
174
+ const lines = errorOutput.split('\n');
175
+
176
+ for (const line of lines) {
177
+ // Match common error patterns
178
+ // Pattern: file.js:10:5: error message
179
+ const fileLineMatch = line.match(/([^\s:]+):(\d+):(\d+)?:?\s*(.+)/);
180
+ if (fileLineMatch) {
181
+ errors.push({
182
+ source,
183
+ file: fileLineMatch[1],
184
+ line: parseInt(fileLineMatch[2]),
185
+ column: fileLineMatch[3] ? parseInt(fileLineMatch[3]) : null,
186
+ message: fileLineMatch[4].trim(),
187
+ raw: line
188
+ });
189
+ continue;
190
+ }
191
+
192
+ // Pattern: Error: message
193
+ const errorMatch = line.match(/^(Error|TypeError|SyntaxError|ReferenceError):\s*(.+)/);
194
+ if (errorMatch) {
195
+ errors.push({
196
+ source,
197
+ type: errorMatch[1],
198
+ message: errorMatch[2].trim(),
199
+ raw: line
200
+ });
201
+ continue;
202
+ }
203
+
204
+ // Pattern: ✖ or ✗ or FAIL
205
+ if (line.includes('✖') || line.includes('✗') || line.includes('FAIL')) {
206
+ errors.push({
207
+ source,
208
+ message: line.trim(),
209
+ raw: line
210
+ });
211
+ }
212
+ }
213
+
214
+ // If no structured errors found, add the whole output
215
+ if (errors.length === 0 && errorOutput.trim()) {
216
+ errors.push({
217
+ source,
218
+ message: errorOutput.substring(0, 500),
219
+ raw: errorOutput
220
+ });
221
+ }
222
+
223
+ return errors;
224
+ }
225
+
226
+ /**
227
+ * Format test results for display
228
+ */
229
+ export function formatTestResults(results) {
230
+ const lines = [];
231
+
232
+ lines.push(`Tests: ${results.summary.passed}/${results.summary.total} passed`);
233
+ lines.push(`Duration: ${(results.duration / 1000).toFixed(1)}s`);
234
+
235
+ if (!results.passed) {
236
+ lines.push('');
237
+ lines.push('Failed tests:');
238
+ for (const test of results.tests.filter(t => !t.passed)) {
239
+ lines.push(` ❌ ${test.name}`);
240
+ for (const error of test.errors || []) {
241
+ const loc = error.file ? `${error.file}:${error.line || '?'}` : '';
242
+ lines.push(` ${loc} ${error.message?.substring(0, 80) || ''}`);
243
+ }
244
+ }
245
+ }
246
+
247
+ return lines.join('\n');
248
+ }