@nclamvn/vibecode-cli 1.1.0 → 1.3.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,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
+ }
@@ -114,9 +114,25 @@ export async function createBlueprint(projectName, sessionId) {
114
114
  }
115
115
 
116
116
  /**
117
- * Create contract file
117
+ * Create contract file from intake and blueprint
118
118
  */
119
119
  export async function createContract(projectName, sessionId) {
120
- const template = getContractTemplate(projectName, sessionId);
120
+ // Read intake and blueprint to extract real content
121
+ let intakeContent = '';
122
+ let blueprintContent = '';
123
+
124
+ try {
125
+ intakeContent = await readSessionFile('intake.md');
126
+ } catch (e) {
127
+ // Intake not found, use empty
128
+ }
129
+
130
+ try {
131
+ blueprintContent = await readSessionFile('blueprint.md');
132
+ } catch (e) {
133
+ // Blueprint not found, use empty
134
+ }
135
+
136
+ const template = getContractTemplate(projectName, sessionId, intakeContent, blueprintContent);
121
137
  await writeSessionFile('contract.md', template);
122
138
  }
@@ -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
+ }
package/src/index.js CHANGED
@@ -16,4 +16,11 @@ export { buildCommand } from './commands/build.js';
16
16
  export { reviewCommand } from './commands/review.js';
17
17
  export { snapshotCommand } from './commands/snapshot.js';
18
18
 
19
+ // Phase C Commands
20
+ export { configCommand } from './commands/config.js';
21
+
22
+ // Constants
19
23
  export { VERSION, SPEC_HASH, STATES } from './config/constants.js';
24
+
25
+ // Providers
26
+ export { PROVIDERS, getProvider, getDefaultProvider } from './providers/index.js';