@nclamvn/vibecode-cli 2.0.0 → 2.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/.vibecode/learning/fixes.json +1 -0
- package/.vibecode/learning/preferences.json +1 -0
- package/README.md +310 -49
- package/SESSION_NOTES.md +154 -0
- package/bin/vibecode.js +235 -2
- package/package.json +5 -2
- package/src/agent/decomposition.js +476 -0
- package/src/agent/index.js +391 -0
- package/src/agent/memory.js +542 -0
- package/src/agent/orchestrator.js +917 -0
- package/src/agent/self-healing.js +516 -0
- package/src/commands/agent.js +349 -0
- package/src/commands/ask.js +230 -0
- package/src/commands/assist.js +413 -0
- package/src/commands/build.js +345 -4
- package/src/commands/debug.js +565 -0
- package/src/commands/docs.js +167 -0
- package/src/commands/git.js +1024 -0
- package/src/commands/go.js +635 -0
- package/src/commands/learn.js +294 -0
- package/src/commands/migrate.js +341 -0
- package/src/commands/plan.js +8 -2
- package/src/commands/refactor.js +205 -0
- package/src/commands/review.js +126 -1
- package/src/commands/security.js +229 -0
- package/src/commands/shell.js +486 -0
- package/src/commands/templates.js +397 -0
- package/src/commands/test.js +194 -0
- package/src/commands/undo.js +281 -0
- package/src/commands/watch.js +556 -0
- package/src/commands/wizard.js +322 -0
- package/src/config/constants.js +5 -1
- package/src/config/templates.js +146 -15
- package/src/core/backup.js +325 -0
- 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/learning.js +295 -0
- package/src/core/session.js +18 -2
- package/src/core/test-runner.js +281 -0
- package/src/debug/analyzer.js +329 -0
- package/src/debug/evidence.js +228 -0
- package/src/debug/fixer.js +348 -0
- package/src/debug/image-analyzer.js +304 -0
- package/src/debug/index.js +378 -0
- package/src/debug/verifier.js +346 -0
- package/src/index.js +102 -0
- package/src/providers/claude-code.js +12 -7
- package/src/templates/index.js +724 -0
- package/src/ui/__tests__/error-translator.test.js +390 -0
- package/src/ui/dashboard.js +364 -0
- package/src/ui/error-translator.js +775 -0
- package/src/utils/image.js +222 -0
|
@@ -0,0 +1,917 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
2
|
+
// VIBECODE AGENT - Orchestrator
|
|
3
|
+
// Coordinates module builds in dependency order with Claude Code
|
|
4
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
5
|
+
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import ora from 'ora';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import fs from 'fs-extra';
|
|
10
|
+
|
|
11
|
+
import { spawnClaudeCode, isClaudeCodeAvailable } from '../providers/index.js';
|
|
12
|
+
import { runTests } from '../core/test-runner.js';
|
|
13
|
+
import { ensureDir, appendToFile } from '../utils/files.js';
|
|
14
|
+
import { ProgressDashboard } from '../ui/dashboard.js';
|
|
15
|
+
import { translateError, showError, inlineError } from '../ui/error-translator.js';
|
|
16
|
+
import { BackupManager } from '../core/backup.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Progress file for resume functionality
|
|
20
|
+
*/
|
|
21
|
+
const PROGRESS_FILE = '.vibecode/agent-progress.json';
|
|
22
|
+
const DECOMPOSITION_FILE = '.vibecode/agent-decomposition.json';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Save agent progress to file
|
|
26
|
+
*/
|
|
27
|
+
export async function saveProgress(projectPath, progress) {
|
|
28
|
+
const progressPath = path.join(projectPath, PROGRESS_FILE);
|
|
29
|
+
await fs.ensureDir(path.dirname(progressPath));
|
|
30
|
+
await fs.writeFile(progressPath, JSON.stringify(progress, null, 2));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Load agent progress from file
|
|
35
|
+
*/
|
|
36
|
+
export async function loadProgress(projectPath) {
|
|
37
|
+
try {
|
|
38
|
+
const progressPath = path.join(projectPath, PROGRESS_FILE);
|
|
39
|
+
const content = await fs.readFile(progressPath, 'utf-8');
|
|
40
|
+
return JSON.parse(content);
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Clear agent progress file
|
|
48
|
+
*/
|
|
49
|
+
export async function clearProgress(projectPath) {
|
|
50
|
+
try {
|
|
51
|
+
await fs.unlink(path.join(projectPath, PROGRESS_FILE));
|
|
52
|
+
} catch {
|
|
53
|
+
// Ignore if file doesn't exist
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Save decomposition for resume
|
|
59
|
+
*/
|
|
60
|
+
export async function saveDecomposition(projectPath, decomposition) {
|
|
61
|
+
const decompositionPath = path.join(projectPath, DECOMPOSITION_FILE);
|
|
62
|
+
await fs.ensureDir(path.dirname(decompositionPath));
|
|
63
|
+
await fs.writeFile(decompositionPath, JSON.stringify(decomposition, null, 2));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Load decomposition from file
|
|
68
|
+
*/
|
|
69
|
+
export async function loadDecomposition(projectPath) {
|
|
70
|
+
try {
|
|
71
|
+
const decompositionPath = path.join(projectPath, DECOMPOSITION_FILE);
|
|
72
|
+
const content = await fs.readFile(decompositionPath, 'utf-8');
|
|
73
|
+
return JSON.parse(content);
|
|
74
|
+
} catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Orchestrator states
|
|
81
|
+
*/
|
|
82
|
+
const ORCHESTRATOR_STATES = {
|
|
83
|
+
IDLE: 'idle',
|
|
84
|
+
INITIALIZING: 'initializing',
|
|
85
|
+
DECOMPOSING: 'decomposing',
|
|
86
|
+
BUILDING: 'building',
|
|
87
|
+
HEALING: 'healing',
|
|
88
|
+
TESTING: 'testing',
|
|
89
|
+
COMPLETED: 'completed',
|
|
90
|
+
FAILED: 'failed',
|
|
91
|
+
PAUSED: 'paused'
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Event types for callbacks
|
|
96
|
+
*/
|
|
97
|
+
const EVENTS = {
|
|
98
|
+
STATE_CHANGE: 'state_change',
|
|
99
|
+
MODULE_START: 'module_start',
|
|
100
|
+
MODULE_COMPLETE: 'module_complete',
|
|
101
|
+
MODULE_FAIL: 'module_fail',
|
|
102
|
+
BUILD_OUTPUT: 'build_output',
|
|
103
|
+
HEALING_START: 'healing_start',
|
|
104
|
+
HEALING_COMPLETE: 'healing_complete',
|
|
105
|
+
PROGRESS: 'progress'
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Orchestrator Class
|
|
110
|
+
* Main coordinator for multi-module builds
|
|
111
|
+
*/
|
|
112
|
+
export class Orchestrator {
|
|
113
|
+
constructor(options = {}) {
|
|
114
|
+
this.decompositionEngine = options.decompositionEngine;
|
|
115
|
+
this.memoryEngine = options.memoryEngine;
|
|
116
|
+
this.selfHealingEngine = options.selfHealingEngine;
|
|
117
|
+
|
|
118
|
+
this.state = ORCHESTRATOR_STATES.IDLE;
|
|
119
|
+
this.projectPath = options.projectPath || process.cwd();
|
|
120
|
+
this.logPath = null;
|
|
121
|
+
this.eventHandlers = {};
|
|
122
|
+
|
|
123
|
+
// Build configuration
|
|
124
|
+
this.config = {
|
|
125
|
+
maxModuleRetries: options.maxModuleRetries || 3,
|
|
126
|
+
maxTotalRetries: options.maxTotalRetries || 10,
|
|
127
|
+
testAfterEachModule: options.testAfterEachModule ?? true,
|
|
128
|
+
continueOnFailure: options.continueOnFailure ?? false,
|
|
129
|
+
parallelBuilds: options.parallelBuilds ?? false, // Future feature
|
|
130
|
+
timeout: options.timeout || 30 * 60 * 1000, // 30 minutes per module
|
|
131
|
+
useDashboard: options.useDashboard ?? true, // Use visual dashboard
|
|
132
|
+
verbose: options.verbose ?? false
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// Dashboard instance
|
|
136
|
+
this.dashboard = null;
|
|
137
|
+
|
|
138
|
+
// Build state
|
|
139
|
+
this.buildState = {
|
|
140
|
+
startTime: null,
|
|
141
|
+
currentModule: null,
|
|
142
|
+
completedModules: [],
|
|
143
|
+
failedModules: [],
|
|
144
|
+
skippedModules: [],
|
|
145
|
+
totalRetries: 0,
|
|
146
|
+
errors: []
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Register event handler
|
|
152
|
+
*/
|
|
153
|
+
on(event, handler) {
|
|
154
|
+
if (!this.eventHandlers[event]) {
|
|
155
|
+
this.eventHandlers[event] = [];
|
|
156
|
+
}
|
|
157
|
+
this.eventHandlers[event].push(handler);
|
|
158
|
+
return this;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Emit event
|
|
163
|
+
*/
|
|
164
|
+
emit(event, data) {
|
|
165
|
+
if (this.eventHandlers[event]) {
|
|
166
|
+
for (const handler of this.eventHandlers[event]) {
|
|
167
|
+
try {
|
|
168
|
+
handler(data);
|
|
169
|
+
} catch (e) {
|
|
170
|
+
console.error(`Event handler error: ${e.message}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Set state and emit event
|
|
178
|
+
*/
|
|
179
|
+
setState(newState) {
|
|
180
|
+
const oldState = this.state;
|
|
181
|
+
this.state = newState;
|
|
182
|
+
this.emit(EVENTS.STATE_CHANGE, { from: oldState, to: newState });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Initialize orchestrator
|
|
187
|
+
*/
|
|
188
|
+
async initialize(projectPath) {
|
|
189
|
+
this.setState(ORCHESTRATOR_STATES.INITIALIZING);
|
|
190
|
+
this.projectPath = projectPath;
|
|
191
|
+
|
|
192
|
+
// Setup log directory
|
|
193
|
+
const agentDir = path.join(projectPath, '.vibecode', 'agent');
|
|
194
|
+
await ensureDir(agentDir);
|
|
195
|
+
this.logPath = path.join(agentDir, 'orchestrator.log');
|
|
196
|
+
|
|
197
|
+
// Check Claude Code availability
|
|
198
|
+
const claudeAvailable = await isClaudeCodeAvailable();
|
|
199
|
+
if (!claudeAvailable) {
|
|
200
|
+
throw new Error('Claude Code CLI not available. Install with: npm install -g @anthropic-ai/claude-code');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Initialize memory if provided
|
|
204
|
+
if (this.memoryEngine) {
|
|
205
|
+
await this.memoryEngine.initialize();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Link self-healing to memory
|
|
209
|
+
if (this.selfHealingEngine && this.memoryEngine) {
|
|
210
|
+
this.selfHealingEngine.setMemoryEngine(this.memoryEngine);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
await this.log('Orchestrator initialized');
|
|
214
|
+
return this;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Log message to file
|
|
219
|
+
*/
|
|
220
|
+
async log(message, level = 'info') {
|
|
221
|
+
const timestamp = new Date().toISOString();
|
|
222
|
+
const line = `[${timestamp}] [${level.toUpperCase()}] ${message}\n`;
|
|
223
|
+
|
|
224
|
+
if (this.logPath) {
|
|
225
|
+
await appendToFile(this.logPath, line);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (level === 'error') {
|
|
229
|
+
console.error(chalk.red(message));
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Main build entry point
|
|
235
|
+
*/
|
|
236
|
+
async build(description, options = {}) {
|
|
237
|
+
this.buildState.startTime = Date.now();
|
|
238
|
+
|
|
239
|
+
// Create backup before agent build
|
|
240
|
+
const backup = new BackupManager(this.projectPath);
|
|
241
|
+
await backup.createBackup('agent-build');
|
|
242
|
+
|
|
243
|
+
try {
|
|
244
|
+
// Step 1: Decompose project
|
|
245
|
+
this.setState(ORCHESTRATOR_STATES.DECOMPOSING);
|
|
246
|
+
await this.log(`Decomposing: "${description}"`);
|
|
247
|
+
|
|
248
|
+
const decomposition = await this.decompositionEngine.decompose(description, options);
|
|
249
|
+
|
|
250
|
+
// Save decomposition for resume functionality
|
|
251
|
+
await saveDecomposition(this.projectPath, {
|
|
252
|
+
...decomposition,
|
|
253
|
+
description,
|
|
254
|
+
startedAt: new Date().toISOString()
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// Store in memory
|
|
258
|
+
if (this.memoryEngine) {
|
|
259
|
+
await this.memoryEngine.setProjectContext({
|
|
260
|
+
description,
|
|
261
|
+
type: decomposition.projectType,
|
|
262
|
+
complexity: decomposition.estimatedComplexity,
|
|
263
|
+
totalModules: decomposition.totalModules
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
await this.log(`Decomposed into ${decomposition.totalModules} modules: ${decomposition.buildOrder.join(', ')}`);
|
|
268
|
+
|
|
269
|
+
// Step 2: Build modules in order
|
|
270
|
+
this.setState(ORCHESTRATOR_STATES.BUILDING);
|
|
271
|
+
|
|
272
|
+
const results = await this.buildModules(decomposition, options);
|
|
273
|
+
|
|
274
|
+
// Step 3: Final summary
|
|
275
|
+
if (results.success) {
|
|
276
|
+
this.setState(ORCHESTRATOR_STATES.COMPLETED);
|
|
277
|
+
// Clear progress on success
|
|
278
|
+
await clearProgress(this.projectPath);
|
|
279
|
+
} else {
|
|
280
|
+
this.setState(ORCHESTRATOR_STATES.FAILED);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return this.generateBuildReport(decomposition, results);
|
|
284
|
+
|
|
285
|
+
} catch (error) {
|
|
286
|
+
this.setState(ORCHESTRATOR_STATES.FAILED);
|
|
287
|
+
await this.log(`Build failed: ${error.message}`, 'error');
|
|
288
|
+
throw error;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Resume build from previous progress
|
|
294
|
+
*/
|
|
295
|
+
async resumeBuild(options = {}) {
|
|
296
|
+
this.buildState.startTime = Date.now();
|
|
297
|
+
|
|
298
|
+
// Load progress
|
|
299
|
+
const progress = await loadProgress(this.projectPath);
|
|
300
|
+
if (!progress) {
|
|
301
|
+
throw new Error('No previous progress found. Start a new build with: vibecode agent "description"');
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Load decomposition
|
|
305
|
+
const savedDecomposition = await loadDecomposition(this.projectPath);
|
|
306
|
+
if (!savedDecomposition) {
|
|
307
|
+
throw new Error('No decomposition found. Start a new build with: vibecode agent "description"');
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
// Restore decomposition engine state
|
|
312
|
+
await this.decompositionEngine.decompose(savedDecomposition.description, options);
|
|
313
|
+
|
|
314
|
+
// Update module statuses from progress
|
|
315
|
+
for (const mod of progress.modules) {
|
|
316
|
+
if (mod.status === 'done') {
|
|
317
|
+
this.decompositionEngine.updateModuleStatus(mod.id, 'completed', {
|
|
318
|
+
files: mod.files || []
|
|
319
|
+
});
|
|
320
|
+
this.buildState.completedModules.push(mod.id);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Store in memory
|
|
325
|
+
if (this.memoryEngine) {
|
|
326
|
+
await this.memoryEngine.setProjectContext({
|
|
327
|
+
description: progress.description,
|
|
328
|
+
type: savedDecomposition.projectType,
|
|
329
|
+
complexity: savedDecomposition.estimatedComplexity,
|
|
330
|
+
totalModules: savedDecomposition.totalModules
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
await this.log(`Resuming from module ${progress.currentModule + 1}`);
|
|
335
|
+
|
|
336
|
+
// Determine start index
|
|
337
|
+
const startIndex = options.fromModule !== undefined
|
|
338
|
+
? options.fromModule
|
|
339
|
+
: progress.currentModule;
|
|
340
|
+
|
|
341
|
+
// Build remaining modules
|
|
342
|
+
this.setState(ORCHESTRATOR_STATES.BUILDING);
|
|
343
|
+
const results = await this.buildModules(savedDecomposition, {
|
|
344
|
+
...options,
|
|
345
|
+
startIndex
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
// Final summary
|
|
349
|
+
if (results.success) {
|
|
350
|
+
this.setState(ORCHESTRATOR_STATES.COMPLETED);
|
|
351
|
+
await clearProgress(this.projectPath);
|
|
352
|
+
} else {
|
|
353
|
+
this.setState(ORCHESTRATOR_STATES.FAILED);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return this.generateBuildReport(savedDecomposition, results);
|
|
357
|
+
|
|
358
|
+
} catch (error) {
|
|
359
|
+
this.setState(ORCHESTRATOR_STATES.FAILED);
|
|
360
|
+
await this.log(`Resume failed: ${error.message}`, 'error');
|
|
361
|
+
throw error;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Build all modules in dependency order
|
|
367
|
+
*/
|
|
368
|
+
async buildModules(decomposition, options = {}) {
|
|
369
|
+
const results = {
|
|
370
|
+
success: true,
|
|
371
|
+
modules: {},
|
|
372
|
+
totalTime: 0
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
// Determine start index for resume
|
|
376
|
+
const startIndex = options.startIndex || 0;
|
|
377
|
+
const buildOrder = decomposition.buildOrder;
|
|
378
|
+
|
|
379
|
+
// Create and start dashboard if enabled
|
|
380
|
+
if (this.config.useDashboard) {
|
|
381
|
+
this.dashboard = new ProgressDashboard({
|
|
382
|
+
title: startIndex > 0 ? 'VIBECODE AGENT (RESUME)' : 'VIBECODE AGENT',
|
|
383
|
+
projectName: path.basename(this.projectPath),
|
|
384
|
+
mode: `Agent (${decomposition.totalModules} modules)`
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
// Set modules for dashboard with correct status for resumed builds
|
|
388
|
+
this.dashboard.setModules(buildOrder.map((id, idx) => {
|
|
389
|
+
const mod = this.decompositionEngine.getModule(id);
|
|
390
|
+
return {
|
|
391
|
+
name: mod?.name || id,
|
|
392
|
+
status: idx < startIndex ? 'completed' : 'pending'
|
|
393
|
+
};
|
|
394
|
+
}));
|
|
395
|
+
|
|
396
|
+
this.dashboard.start();
|
|
397
|
+
|
|
398
|
+
// Show resume message
|
|
399
|
+
if (startIndex > 0) {
|
|
400
|
+
this.dashboard.addLog(`Resuming from module ${startIndex + 1}: ${buildOrder[startIndex]}`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
try {
|
|
405
|
+
for (let i = startIndex; i < buildOrder.length; i++) {
|
|
406
|
+
const moduleId = buildOrder[i];
|
|
407
|
+
|
|
408
|
+
// Check if we should stop
|
|
409
|
+
if (this.state === ORCHESTRATOR_STATES.PAUSED) {
|
|
410
|
+
await this.log('Build paused');
|
|
411
|
+
break;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Save progress before building this module
|
|
415
|
+
await saveProgress(this.projectPath, {
|
|
416
|
+
projectName: path.basename(this.projectPath),
|
|
417
|
+
description: decomposition.description,
|
|
418
|
+
totalModules: decomposition.totalModules,
|
|
419
|
+
completedModules: this.buildState.completedModules,
|
|
420
|
+
currentModule: i,
|
|
421
|
+
modules: buildOrder.map((id, idx) => {
|
|
422
|
+
const mod = this.decompositionEngine.getModule(id);
|
|
423
|
+
return {
|
|
424
|
+
id,
|
|
425
|
+
name: mod?.name || id,
|
|
426
|
+
status: idx < i ? 'done' : idx === i ? 'building' : 'pending',
|
|
427
|
+
files: mod?.files || []
|
|
428
|
+
};
|
|
429
|
+
}),
|
|
430
|
+
startedAt: decomposition.startedAt || new Date().toISOString(),
|
|
431
|
+
lastUpdated: new Date().toISOString(),
|
|
432
|
+
error: null
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
// Check if module can be built (dependencies satisfied)
|
|
436
|
+
if (!this.decompositionEngine.canBuildModule(moduleId)) {
|
|
437
|
+
const depStatus = decomposition.dependencyGraph[moduleId];
|
|
438
|
+
const failedDeps = depStatus?.dependsOn.filter(d =>
|
|
439
|
+
this.buildState.failedModules.includes(d)
|
|
440
|
+
);
|
|
441
|
+
|
|
442
|
+
if (failedDeps?.length > 0) {
|
|
443
|
+
await this.log(`Skipping ${moduleId}: dependencies failed (${failedDeps.join(', ')})`, 'warn');
|
|
444
|
+
this.buildState.skippedModules.push(moduleId);
|
|
445
|
+
results.modules[moduleId] = { status: 'skipped', reason: 'dependencies_failed' };
|
|
446
|
+
|
|
447
|
+
// Update dashboard
|
|
448
|
+
if (this.dashboard) {
|
|
449
|
+
const mod = this.decompositionEngine.getModule(moduleId);
|
|
450
|
+
this.dashboard.failModule(mod?.name || moduleId);
|
|
451
|
+
}
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Update dashboard - start module
|
|
457
|
+
if (this.dashboard) {
|
|
458
|
+
const mod = this.decompositionEngine.getModule(moduleId);
|
|
459
|
+
this.dashboard.startModule(mod?.name || moduleId);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Build the module
|
|
463
|
+
const moduleResult = await this.buildModule(moduleId, decomposition);
|
|
464
|
+
results.modules[moduleId] = moduleResult;
|
|
465
|
+
|
|
466
|
+
// Update dashboard - complete/fail module
|
|
467
|
+
if (this.dashboard) {
|
|
468
|
+
const mod = this.decompositionEngine.getModule(moduleId);
|
|
469
|
+
if (moduleResult.success) {
|
|
470
|
+
this.dashboard.completeModule(mod?.name || moduleId, true);
|
|
471
|
+
} else {
|
|
472
|
+
this.dashboard.failModule(mod?.name || moduleId);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Save progress after module completion
|
|
477
|
+
await saveProgress(this.projectPath, {
|
|
478
|
+
projectName: path.basename(this.projectPath),
|
|
479
|
+
description: decomposition.description,
|
|
480
|
+
totalModules: decomposition.totalModules,
|
|
481
|
+
completedModules: this.buildState.completedModules,
|
|
482
|
+
currentModule: moduleResult.success ? i + 1 : i,
|
|
483
|
+
failedModule: moduleResult.success ? null : i,
|
|
484
|
+
modules: buildOrder.map((id, idx) => {
|
|
485
|
+
const mod = this.decompositionEngine.getModule(id);
|
|
486
|
+
const status = idx < i ? 'done'
|
|
487
|
+
: idx === i ? (moduleResult.success ? 'done' : 'failed')
|
|
488
|
+
: 'pending';
|
|
489
|
+
return {
|
|
490
|
+
id,
|
|
491
|
+
name: mod?.name || id,
|
|
492
|
+
status,
|
|
493
|
+
files: mod?.files || []
|
|
494
|
+
};
|
|
495
|
+
}),
|
|
496
|
+
startedAt: decomposition.startedAt || new Date().toISOString(),
|
|
497
|
+
lastUpdated: new Date().toISOString(),
|
|
498
|
+
error: moduleResult.success ? null : moduleResult.error
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
if (!moduleResult.success) {
|
|
502
|
+
results.success = false;
|
|
503
|
+
|
|
504
|
+
if (!this.config.continueOnFailure) {
|
|
505
|
+
await this.log(`Stopping build due to module failure: ${moduleId}`, 'error');
|
|
506
|
+
console.log(chalk.yellow(`\n💡 Resume later with: vibecode agent --resume\n`));
|
|
507
|
+
break;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
} finally {
|
|
512
|
+
// Stop dashboard
|
|
513
|
+
if (this.dashboard) {
|
|
514
|
+
this.dashboard.stop();
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
results.totalTime = Date.now() - this.buildState.startTime;
|
|
519
|
+
return results;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Build a single module
|
|
524
|
+
*/
|
|
525
|
+
async buildModule(moduleId, decomposition) {
|
|
526
|
+
const module = this.decompositionEngine.getModule(moduleId);
|
|
527
|
+
if (!module) {
|
|
528
|
+
return { success: false, error: 'Module not found' };
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
this.buildState.currentModule = moduleId;
|
|
532
|
+
this.emit(EVENTS.MODULE_START, { moduleId, module });
|
|
533
|
+
|
|
534
|
+
// Only use spinner if dashboard is not enabled
|
|
535
|
+
const spinner = !this.config.useDashboard ? ora({
|
|
536
|
+
text: chalk.cyan(`Building module: ${module.name}`),
|
|
537
|
+
prefixText: this.getProgressPrefix()
|
|
538
|
+
}).start() : null;
|
|
539
|
+
|
|
540
|
+
// Record in memory
|
|
541
|
+
if (this.memoryEngine) {
|
|
542
|
+
await this.memoryEngine.startModule(moduleId, {
|
|
543
|
+
name: module.name,
|
|
544
|
+
description: module.description
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
let attempts = 0;
|
|
549
|
+
let lastError = null;
|
|
550
|
+
let healingPrompt = null; // Store fix prompt from self-healing
|
|
551
|
+
|
|
552
|
+
while (attempts < this.config.maxModuleRetries) {
|
|
553
|
+
attempts++;
|
|
554
|
+
module.buildAttempts = attempts;
|
|
555
|
+
|
|
556
|
+
try {
|
|
557
|
+
await this.log(`Building ${moduleId} (attempt ${attempts}/${this.config.maxModuleRetries})`);
|
|
558
|
+
|
|
559
|
+
// Use healing prompt if available (from previous retry), otherwise generate fresh
|
|
560
|
+
let prompt;
|
|
561
|
+
if (healingPrompt) {
|
|
562
|
+
prompt = healingPrompt;
|
|
563
|
+
healingPrompt = null; // Clear after use
|
|
564
|
+
} else {
|
|
565
|
+
prompt = this.generateBuildPrompt(module, decomposition);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Run Claude Code
|
|
569
|
+
const buildResult = await this.runClaudeBuild(prompt, moduleId);
|
|
570
|
+
|
|
571
|
+
if (buildResult.success) {
|
|
572
|
+
// Run tests if configured
|
|
573
|
+
if (this.config.testAfterEachModule) {
|
|
574
|
+
if (spinner) spinner.text = chalk.cyan(`Testing module: ${module.name}`);
|
|
575
|
+
if (this.dashboard) this.dashboard.addLog(`Testing: ${module.name}`);
|
|
576
|
+
this.setState(ORCHESTRATOR_STATES.TESTING);
|
|
577
|
+
|
|
578
|
+
const testResult = await runTests(this.projectPath);
|
|
579
|
+
|
|
580
|
+
if (!testResult.passed) {
|
|
581
|
+
throw new Error(`Tests failed: ${testResult.summary.failed} failures`);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Success!
|
|
586
|
+
if (spinner) spinner.succeed(chalk.green(`Module complete: ${module.name}`));
|
|
587
|
+
|
|
588
|
+
this.decompositionEngine.updateModuleStatus(moduleId, 'completed', {
|
|
589
|
+
files: buildResult.files || []
|
|
590
|
+
});
|
|
591
|
+
this.buildState.completedModules.push(moduleId);
|
|
592
|
+
|
|
593
|
+
// Record in memory
|
|
594
|
+
if (this.memoryEngine) {
|
|
595
|
+
await this.memoryEngine.completeModule(moduleId, {
|
|
596
|
+
files: buildResult.files,
|
|
597
|
+
attempts
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
this.emit(EVENTS.MODULE_COMPLETE, { moduleId, result: buildResult });
|
|
602
|
+
|
|
603
|
+
return {
|
|
604
|
+
success: true,
|
|
605
|
+
attempts,
|
|
606
|
+
files: buildResult.files
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
throw new Error(buildResult.error || 'Build failed');
|
|
611
|
+
|
|
612
|
+
} catch (error) {
|
|
613
|
+
lastError = error;
|
|
614
|
+
const translated = translateError(error);
|
|
615
|
+
await this.log(`Module ${moduleId} attempt ${attempts} failed: ${translated.title} - ${error.message}`, 'error');
|
|
616
|
+
|
|
617
|
+
// Show translated error if not using dashboard
|
|
618
|
+
if (!this.config.useDashboard) {
|
|
619
|
+
console.log(inlineError(error));
|
|
620
|
+
} else if (this.dashboard) {
|
|
621
|
+
this.dashboard.addLog(`Error: ${translated.title}`);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Try self-healing
|
|
625
|
+
if (this.selfHealingEngine && attempts < this.config.maxModuleRetries) {
|
|
626
|
+
if (spinner) spinner.text = chalk.yellow(`Healing module: ${module.name}`);
|
|
627
|
+
if (this.dashboard) this.dashboard.addLog(`Healing: ${module.name}`);
|
|
628
|
+
this.setState(ORCHESTRATOR_STATES.HEALING);
|
|
629
|
+
this.emit(EVENTS.HEALING_START, { moduleId, error });
|
|
630
|
+
|
|
631
|
+
const healing = await this.selfHealingEngine.heal(error.message, moduleId, {
|
|
632
|
+
attempt: attempts,
|
|
633
|
+
completedModules: this.buildState.completedModules
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
if (healing.shouldRetry) {
|
|
637
|
+
const errorCategory = healing.analysis?.category || 'UNKNOWN';
|
|
638
|
+
await this.log(`Self-healing: ${errorCategory} error, retrying...`);
|
|
639
|
+
this.buildState.totalRetries++;
|
|
640
|
+
|
|
641
|
+
// Store healing prompt for next iteration
|
|
642
|
+
healingPrompt = healing.prompt;
|
|
643
|
+
|
|
644
|
+
// Record healing attempt
|
|
645
|
+
if (this.memoryEngine) {
|
|
646
|
+
await this.memoryEngine.recordError({
|
|
647
|
+
message: error.message,
|
|
648
|
+
type: errorCategory,
|
|
649
|
+
moduleId,
|
|
650
|
+
healingAttempt: attempts
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
break; // Exit retry loop if can't heal
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// Module failed
|
|
663
|
+
if (spinner) spinner.fail(chalk.red(`Module failed: ${module.name}`));
|
|
664
|
+
|
|
665
|
+
this.decompositionEngine.updateModuleStatus(moduleId, 'failed', {
|
|
666
|
+
error: lastError?.message
|
|
667
|
+
});
|
|
668
|
+
this.buildState.failedModules.push(moduleId);
|
|
669
|
+
this.buildState.errors.push({ moduleId, error: lastError?.message });
|
|
670
|
+
|
|
671
|
+
if (this.memoryEngine) {
|
|
672
|
+
await this.memoryEngine.failModule(moduleId, lastError);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
this.emit(EVENTS.MODULE_FAIL, { moduleId, error: lastError, attempts });
|
|
676
|
+
|
|
677
|
+
return {
|
|
678
|
+
success: false,
|
|
679
|
+
attempts,
|
|
680
|
+
error: lastError?.message
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* Generate build prompt for a module
|
|
686
|
+
*/
|
|
687
|
+
generateBuildPrompt(module, decomposition) {
|
|
688
|
+
let prompt = `# Build Module: ${module.name}\n\n`;
|
|
689
|
+
|
|
690
|
+
// Module description
|
|
691
|
+
prompt += `## Description\n${module.description}\n\n`;
|
|
692
|
+
|
|
693
|
+
// Dependencies context
|
|
694
|
+
if (module.dependencies.length > 0) {
|
|
695
|
+
prompt += `## Dependencies (already built)\n`;
|
|
696
|
+
for (const depId of module.dependencies) {
|
|
697
|
+
const dep = this.decompositionEngine.getModule(depId);
|
|
698
|
+
if (dep) {
|
|
699
|
+
prompt += `- **${dep.name}**: ${dep.description}\n`;
|
|
700
|
+
if (dep.files?.length > 0) {
|
|
701
|
+
prompt += ` Files: ${dep.files.join(', ')}\n`;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
prompt += '\n';
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// Memory context
|
|
709
|
+
if (this.memoryEngine) {
|
|
710
|
+
const contextSummary = this.memoryEngine.generateContextSummary();
|
|
711
|
+
prompt += contextSummary;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// Project context
|
|
715
|
+
const projectContext = this.memoryEngine?.getProjectContext();
|
|
716
|
+
if (projectContext?.description) {
|
|
717
|
+
prompt += `## Project Goal\n${projectContext.description}\n\n`;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// Build instructions
|
|
721
|
+
prompt += `## Instructions\n`;
|
|
722
|
+
prompt += `1. Create all necessary files for the ${module.name} module\n`;
|
|
723
|
+
prompt += `2. Follow patterns established in completed modules\n`;
|
|
724
|
+
prompt += `3. Ensure compatibility with dependencies\n`;
|
|
725
|
+
prompt += `4. Add appropriate error handling\n`;
|
|
726
|
+
prompt += `5. Export any functions/components needed by dependent modules\n\n`;
|
|
727
|
+
|
|
728
|
+
// Specific instructions based on module type
|
|
729
|
+
const moduleInstructions = this.getModuleSpecificInstructions(module.id);
|
|
730
|
+
if (moduleInstructions) {
|
|
731
|
+
prompt += `## ${module.name} Specific Requirements\n${moduleInstructions}\n`;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
return prompt;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* Get module-specific build instructions
|
|
739
|
+
*/
|
|
740
|
+
getModuleSpecificInstructions(moduleId) {
|
|
741
|
+
const instructions = {
|
|
742
|
+
core: `- Setup project structure (src/, public/, etc.)
|
|
743
|
+
- Create configuration files (package.json, tsconfig.json if needed)
|
|
744
|
+
- Setup base utilities and helpers`,
|
|
745
|
+
|
|
746
|
+
auth: `- Implement login/signup forms or endpoints
|
|
747
|
+
- Setup session/token management
|
|
748
|
+
- Add password hashing if applicable
|
|
749
|
+
- Create auth middleware/guards`,
|
|
750
|
+
|
|
751
|
+
database: `- Define data models/schemas
|
|
752
|
+
- Setup database connection
|
|
753
|
+
- Create migration scripts if needed
|
|
754
|
+
- Add seed data for development`,
|
|
755
|
+
|
|
756
|
+
api: `- Create REST/GraphQL endpoints
|
|
757
|
+
- Add request validation
|
|
758
|
+
- Implement error handling middleware
|
|
759
|
+
- Add API documentation`,
|
|
760
|
+
|
|
761
|
+
ui: `- Create reusable components
|
|
762
|
+
- Setup styling (CSS/Tailwind/etc.)
|
|
763
|
+
- Ensure responsive design
|
|
764
|
+
- Add accessibility attributes`,
|
|
765
|
+
|
|
766
|
+
pages: `- Create page components/routes
|
|
767
|
+
- Connect to API endpoints
|
|
768
|
+
- Add loading and error states
|
|
769
|
+
- Implement navigation`,
|
|
770
|
+
|
|
771
|
+
tests: `- Write unit tests for core functions
|
|
772
|
+
- Add integration tests for API
|
|
773
|
+
- Create component tests for UI
|
|
774
|
+
- Setup test utilities and mocks`
|
|
775
|
+
};
|
|
776
|
+
|
|
777
|
+
return instructions[moduleId] || null;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
* Run Claude Code build
|
|
782
|
+
*/
|
|
783
|
+
async runClaudeBuild(prompt, moduleId) {
|
|
784
|
+
const evidencePath = path.join(this.projectPath, '.vibecode', 'agent', 'evidence');
|
|
785
|
+
await ensureDir(evidencePath);
|
|
786
|
+
const logPath = path.join(evidencePath, `${moduleId}.log`);
|
|
787
|
+
|
|
788
|
+
try {
|
|
789
|
+
const result = await spawnClaudeCode(prompt, {
|
|
790
|
+
cwd: this.projectPath,
|
|
791
|
+
logPath,
|
|
792
|
+
timeout: this.config.timeout
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
// Try to detect created files
|
|
796
|
+
const files = await this.detectCreatedFiles();
|
|
797
|
+
|
|
798
|
+
return {
|
|
799
|
+
success: result.code === 0,
|
|
800
|
+
code: result.code,
|
|
801
|
+
output: result.output,
|
|
802
|
+
files,
|
|
803
|
+
error: result.code !== 0 ? result.error : null
|
|
804
|
+
};
|
|
805
|
+
} catch (error) {
|
|
806
|
+
return {
|
|
807
|
+
success: false,
|
|
808
|
+
error: error.message
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/**
|
|
814
|
+
* Detect files created during build
|
|
815
|
+
*/
|
|
816
|
+
async detectCreatedFiles() {
|
|
817
|
+
// This is a simplified version - in real implementation,
|
|
818
|
+
// we would track git status or file system changes
|
|
819
|
+
try {
|
|
820
|
+
const srcPath = path.join(this.projectPath, 'src');
|
|
821
|
+
if (await fs.pathExists(srcPath)) {
|
|
822
|
+
const files = await fs.readdir(srcPath, { recursive: true });
|
|
823
|
+
return files.filter(f => !f.startsWith('.')).slice(0, 20);
|
|
824
|
+
}
|
|
825
|
+
} catch (e) {
|
|
826
|
+
// Ignore
|
|
827
|
+
}
|
|
828
|
+
return [];
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
/**
|
|
832
|
+
* Get progress prefix for spinner
|
|
833
|
+
*/
|
|
834
|
+
getProgressPrefix() {
|
|
835
|
+
const completed = this.buildState.completedModules.length;
|
|
836
|
+
const total = this.decompositionEngine?.modules?.length || 0;
|
|
837
|
+
const percent = total > 0 ? Math.round((completed / total) * 100) : 0;
|
|
838
|
+
return chalk.gray(`[${completed}/${total}] ${percent}%`);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* Generate final build report
|
|
843
|
+
*/
|
|
844
|
+
generateBuildReport(decomposition, results) {
|
|
845
|
+
const duration = ((Date.now() - this.buildState.startTime) / 1000 / 60).toFixed(1);
|
|
846
|
+
|
|
847
|
+
return {
|
|
848
|
+
success: results.success,
|
|
849
|
+
projectType: decomposition.projectType,
|
|
850
|
+
complexity: decomposition.estimatedComplexity,
|
|
851
|
+
duration: `${duration} minutes`,
|
|
852
|
+
|
|
853
|
+
modules: {
|
|
854
|
+
total: decomposition.totalModules,
|
|
855
|
+
completed: this.buildState.completedModules.length,
|
|
856
|
+
failed: this.buildState.failedModules.length,
|
|
857
|
+
skipped: this.buildState.skippedModules.length
|
|
858
|
+
},
|
|
859
|
+
|
|
860
|
+
buildOrder: decomposition.buildOrder,
|
|
861
|
+
completedModules: this.buildState.completedModules,
|
|
862
|
+
failedModules: this.buildState.failedModules,
|
|
863
|
+
skippedModules: this.buildState.skippedModules,
|
|
864
|
+
|
|
865
|
+
retries: {
|
|
866
|
+
total: this.buildState.totalRetries,
|
|
867
|
+
max: this.config.maxTotalRetries
|
|
868
|
+
},
|
|
869
|
+
|
|
870
|
+
errors: this.buildState.errors,
|
|
871
|
+
moduleResults: results.modules,
|
|
872
|
+
|
|
873
|
+
healingStats: this.selfHealingEngine?.getStats() || null,
|
|
874
|
+
memoryStats: this.memoryEngine?.getStats() || null
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* Pause the build
|
|
880
|
+
*/
|
|
881
|
+
pause() {
|
|
882
|
+
if (this.state === ORCHESTRATOR_STATES.BUILDING) {
|
|
883
|
+
this.setState(ORCHESTRATOR_STATES.PAUSED);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
/**
|
|
888
|
+
* Resume paused build
|
|
889
|
+
*/
|
|
890
|
+
resume() {
|
|
891
|
+
if (this.state === ORCHESTRATOR_STATES.PAUSED) {
|
|
892
|
+
this.setState(ORCHESTRATOR_STATES.BUILDING);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
/**
|
|
897
|
+
* Get current build state
|
|
898
|
+
*/
|
|
899
|
+
getState() {
|
|
900
|
+
return {
|
|
901
|
+
state: this.state,
|
|
902
|
+
currentModule: this.buildState.currentModule,
|
|
903
|
+
completedModules: this.buildState.completedModules,
|
|
904
|
+
failedModules: this.buildState.failedModules,
|
|
905
|
+
progress: this.getProgressPrefix()
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/**
|
|
911
|
+
* Create orchestrator instance
|
|
912
|
+
*/
|
|
913
|
+
export function createOrchestrator(options = {}) {
|
|
914
|
+
return new Orchestrator(options);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
export { ORCHESTRATOR_STATES, EVENTS };
|