@nclamvn/vibecode-cli 1.1.0 → 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 +17 -0
- package/package.json +1 -1
- package/src/commands/build.js +495 -3
- package/src/commands/config.js +149 -0
- package/src/config/constants.js +1 -1
- 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/test-runner.js +248 -0
- package/src/index.js +7 -0
- package/src/providers/claude-code.js +159 -0
- package/src/providers/index.js +45 -0
|
@@ -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
|
+
}
|
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';
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
2
|
+
// VIBECODE CLI - Claude Code Provider
|
|
3
|
+
// "Claude/LLM là PIPELINE, là KIẾN TRÚC SƯ"
|
|
4
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
5
|
+
|
|
6
|
+
import { spawn } from 'child_process';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { pathExists, appendToFile } from '../utils/files.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Claude Code optimal configuration
|
|
12
|
+
* Contract LOCKED = License to build (không cần hỏi thêm)
|
|
13
|
+
*/
|
|
14
|
+
export const CLAUDE_CODE_CONFIG = {
|
|
15
|
+
command: 'claude',
|
|
16
|
+
flags: [
|
|
17
|
+
'--dangerously-skip-permissions', // Trust the AI - Contract đã locked
|
|
18
|
+
],
|
|
19
|
+
timeout: 30 * 60 * 1000, // 30 minutes max
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Check if Claude Code CLI is available
|
|
24
|
+
*/
|
|
25
|
+
export async function isClaudeCodeAvailable() {
|
|
26
|
+
return new Promise((resolve) => {
|
|
27
|
+
const proc = spawn('which', ['claude'], { shell: true });
|
|
28
|
+
proc.on('close', (code) => {
|
|
29
|
+
resolve(code === 0);
|
|
30
|
+
});
|
|
31
|
+
proc.on('error', () => {
|
|
32
|
+
resolve(false);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Spawn Claude Code with optimal settings
|
|
39
|
+
*
|
|
40
|
+
* @param {string} prompt - The coder pack / prompt to send
|
|
41
|
+
* @param {object} options - Configuration options
|
|
42
|
+
* @param {string} options.cwd - Working directory
|
|
43
|
+
* @param {string} options.logPath - Path to write build logs
|
|
44
|
+
* @param {function} options.onOutput - Callback for output
|
|
45
|
+
* @returns {Promise<{success: boolean, code: number}>}
|
|
46
|
+
*/
|
|
47
|
+
export async function spawnClaudeCode(prompt, options = {}) {
|
|
48
|
+
const { cwd, logPath, onOutput } = options;
|
|
49
|
+
const fs = await import('fs-extra');
|
|
50
|
+
const os = await import('os');
|
|
51
|
+
|
|
52
|
+
// Check if Claude Code is available
|
|
53
|
+
const available = await isClaudeCodeAvailable();
|
|
54
|
+
if (!available) {
|
|
55
|
+
throw new Error('Claude Code CLI not found. Install with: npm install -g @anthropic/claude-code');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Write prompt to temp file to avoid shell escaping issues
|
|
59
|
+
const tempDir = os.default.tmpdir();
|
|
60
|
+
const promptFile = path.join(tempDir, `vibecode-prompt-${Date.now()}.md`);
|
|
61
|
+
await fs.default.writeFile(promptFile, prompt, 'utf-8');
|
|
62
|
+
|
|
63
|
+
return new Promise((resolve, reject) => {
|
|
64
|
+
// Use cat to pipe the prompt file content to claude
|
|
65
|
+
const command = `cat "${promptFile}" | claude ${CLAUDE_CODE_CONFIG.flags.join(' ')}`;
|
|
66
|
+
|
|
67
|
+
// Log the command being run
|
|
68
|
+
if (logPath) {
|
|
69
|
+
appendToFile(logPath, `\n[${new Date().toISOString()}] Running: claude with prompt from ${promptFile}\n`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const proc = spawn(command, [], {
|
|
73
|
+
cwd: cwd || process.cwd(),
|
|
74
|
+
stdio: 'inherit', // Stream directly to terminal
|
|
75
|
+
shell: true,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
let timeoutId = setTimeout(() => {
|
|
79
|
+
proc.kill();
|
|
80
|
+
// Cleanup temp file
|
|
81
|
+
fs.default.remove(promptFile).catch(() => {});
|
|
82
|
+
reject(new Error('Claude Code process timed out'));
|
|
83
|
+
}, CLAUDE_CODE_CONFIG.timeout);
|
|
84
|
+
|
|
85
|
+
proc.on('close', async (code) => {
|
|
86
|
+
clearTimeout(timeoutId);
|
|
87
|
+
|
|
88
|
+
// Cleanup temp file
|
|
89
|
+
await fs.default.remove(promptFile).catch(() => {});
|
|
90
|
+
|
|
91
|
+
const result = {
|
|
92
|
+
success: code === 0,
|
|
93
|
+
code: code || 0,
|
|
94
|
+
timestamp: new Date().toISOString()
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
if (logPath) {
|
|
98
|
+
const status = result.success ? 'SUCCESS' : 'FAILED';
|
|
99
|
+
appendToFile(logPath, `\n[${result.timestamp}] Claude Code ${status} (exit code: ${code})\n`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
resolve(result);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
proc.on('error', async (error) => {
|
|
106
|
+
clearTimeout(timeoutId);
|
|
107
|
+
// Cleanup temp file
|
|
108
|
+
await fs.default.remove(promptFile).catch(() => {});
|
|
109
|
+
|
|
110
|
+
if (logPath) {
|
|
111
|
+
appendToFile(logPath, `\n[${new Date().toISOString()}] ERROR: ${error.message}\n`);
|
|
112
|
+
}
|
|
113
|
+
reject(error);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Build prompt with optional CLAUDE.md injection
|
|
120
|
+
*
|
|
121
|
+
* @param {string} coderPackContent - Content of coder_pack.md
|
|
122
|
+
* @param {string} projectRoot - Project root directory
|
|
123
|
+
* @returns {Promise<string>} - Final prompt
|
|
124
|
+
*/
|
|
125
|
+
export async function buildPromptWithContext(coderPackContent, projectRoot) {
|
|
126
|
+
let fullPrompt = coderPackContent;
|
|
127
|
+
|
|
128
|
+
// Check for CLAUDE.md in project root
|
|
129
|
+
const claudeMdPath = path.join(projectRoot, 'CLAUDE.md');
|
|
130
|
+
if (await pathExists(claudeMdPath)) {
|
|
131
|
+
const fs = await import('fs-extra');
|
|
132
|
+
const claudeMd = await fs.default.readFile(claudeMdPath, 'utf-8');
|
|
133
|
+
|
|
134
|
+
// Inject CLAUDE.md rules before coder pack
|
|
135
|
+
fullPrompt = `# PROJECT RULES (from CLAUDE.md)
|
|
136
|
+
|
|
137
|
+
${claudeMd}
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
# BUILD INSTRUCTIONS
|
|
142
|
+
|
|
143
|
+
${coderPackContent}`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return fullPrompt;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Get provider info for status display
|
|
151
|
+
*/
|
|
152
|
+
export function getProviderInfo() {
|
|
153
|
+
return {
|
|
154
|
+
name: 'Claude Code',
|
|
155
|
+
command: CLAUDE_CODE_CONFIG.command,
|
|
156
|
+
mode: '--dangerously-skip-permissions',
|
|
157
|
+
description: 'AI coding with guardrails disabled (contract-approved)'
|
|
158
|
+
};
|
|
159
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
2
|
+
// VIBECODE CLI - AI Provider Manager
|
|
3
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
4
|
+
|
|
5
|
+
export {
|
|
6
|
+
spawnClaudeCode,
|
|
7
|
+
isClaudeCodeAvailable,
|
|
8
|
+
buildPromptWithContext,
|
|
9
|
+
getProviderInfo,
|
|
10
|
+
CLAUDE_CODE_CONFIG
|
|
11
|
+
} from './claude-code.js';
|
|
12
|
+
|
|
13
|
+
// Future providers:
|
|
14
|
+
// export { callAnthropicAPI } from './anthropic-api.js';
|
|
15
|
+
// export { spawnCursor } from './cursor.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Available providers
|
|
19
|
+
*/
|
|
20
|
+
export const PROVIDERS = {
|
|
21
|
+
'claude-code': {
|
|
22
|
+
name: 'Claude Code',
|
|
23
|
+
description: 'Official Claude CLI for coding',
|
|
24
|
+
available: true
|
|
25
|
+
},
|
|
26
|
+
'anthropic-api': {
|
|
27
|
+
name: 'Anthropic API',
|
|
28
|
+
description: 'Direct API calls (coming soon)',
|
|
29
|
+
available: false
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get provider by name
|
|
35
|
+
*/
|
|
36
|
+
export function getProvider(name) {
|
|
37
|
+
return PROVIDERS[name] || null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get default provider
|
|
42
|
+
*/
|
|
43
|
+
export function getDefaultProvider() {
|
|
44
|
+
return 'claude-code';
|
|
45
|
+
}
|