@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.
- package/bin/vibecode.js +17 -0
- package/package.json +1 -1
- package/src/commands/build.js +496 -3
- package/src/commands/config.js +149 -0
- package/src/commands/plan.js +8 -2
- package/src/config/constants.js +1 -1
- package/src/config/templates.js +146 -15
- package/src/core/error-analyzer.js +237 -0
- package/src/core/fix-generator.js +195 -0
- package/src/core/iteration.js +226 -0
- package/src/core/session.js +18 -2
- package/src/core/test-runner.js +248 -0
- package/src/index.js +7 -0
- package/src/providers/claude-code.js +164 -0
- package/src/providers/index.js +45 -0
|
@@ -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
|
+
}
|
package/src/core/session.js
CHANGED
|
@@ -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
|
-
|
|
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';
|