@nclamvn/vibecode-cli 1.3.0 → 1.5.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,644 @@
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
+
15
+ /**
16
+ * Orchestrator states
17
+ */
18
+ const ORCHESTRATOR_STATES = {
19
+ IDLE: 'idle',
20
+ INITIALIZING: 'initializing',
21
+ DECOMPOSING: 'decomposing',
22
+ BUILDING: 'building',
23
+ HEALING: 'healing',
24
+ TESTING: 'testing',
25
+ COMPLETED: 'completed',
26
+ FAILED: 'failed',
27
+ PAUSED: 'paused'
28
+ };
29
+
30
+ /**
31
+ * Event types for callbacks
32
+ */
33
+ const EVENTS = {
34
+ STATE_CHANGE: 'state_change',
35
+ MODULE_START: 'module_start',
36
+ MODULE_COMPLETE: 'module_complete',
37
+ MODULE_FAIL: 'module_fail',
38
+ BUILD_OUTPUT: 'build_output',
39
+ HEALING_START: 'healing_start',
40
+ HEALING_COMPLETE: 'healing_complete',
41
+ PROGRESS: 'progress'
42
+ };
43
+
44
+ /**
45
+ * Orchestrator Class
46
+ * Main coordinator for multi-module builds
47
+ */
48
+ export class Orchestrator {
49
+ constructor(options = {}) {
50
+ this.decompositionEngine = options.decompositionEngine;
51
+ this.memoryEngine = options.memoryEngine;
52
+ this.selfHealingEngine = options.selfHealingEngine;
53
+
54
+ this.state = ORCHESTRATOR_STATES.IDLE;
55
+ this.projectPath = options.projectPath || process.cwd();
56
+ this.logPath = null;
57
+ this.eventHandlers = {};
58
+
59
+ // Build configuration
60
+ this.config = {
61
+ maxModuleRetries: options.maxModuleRetries || 3,
62
+ maxTotalRetries: options.maxTotalRetries || 10,
63
+ testAfterEachModule: options.testAfterEachModule ?? true,
64
+ continueOnFailure: options.continueOnFailure ?? false,
65
+ parallelBuilds: options.parallelBuilds ?? false, // Future feature
66
+ timeout: options.timeout || 30 * 60 * 1000 // 30 minutes per module
67
+ };
68
+
69
+ // Build state
70
+ this.buildState = {
71
+ startTime: null,
72
+ currentModule: null,
73
+ completedModules: [],
74
+ failedModules: [],
75
+ skippedModules: [],
76
+ totalRetries: 0,
77
+ errors: []
78
+ };
79
+ }
80
+
81
+ /**
82
+ * Register event handler
83
+ */
84
+ on(event, handler) {
85
+ if (!this.eventHandlers[event]) {
86
+ this.eventHandlers[event] = [];
87
+ }
88
+ this.eventHandlers[event].push(handler);
89
+ return this;
90
+ }
91
+
92
+ /**
93
+ * Emit event
94
+ */
95
+ emit(event, data) {
96
+ if (this.eventHandlers[event]) {
97
+ for (const handler of this.eventHandlers[event]) {
98
+ try {
99
+ handler(data);
100
+ } catch (e) {
101
+ console.error(`Event handler error: ${e.message}`);
102
+ }
103
+ }
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Set state and emit event
109
+ */
110
+ setState(newState) {
111
+ const oldState = this.state;
112
+ this.state = newState;
113
+ this.emit(EVENTS.STATE_CHANGE, { from: oldState, to: newState });
114
+ }
115
+
116
+ /**
117
+ * Initialize orchestrator
118
+ */
119
+ async initialize(projectPath) {
120
+ this.setState(ORCHESTRATOR_STATES.INITIALIZING);
121
+ this.projectPath = projectPath;
122
+
123
+ // Setup log directory
124
+ const agentDir = path.join(projectPath, '.vibecode', 'agent');
125
+ await ensureDir(agentDir);
126
+ this.logPath = path.join(agentDir, 'orchestrator.log');
127
+
128
+ // Check Claude Code availability
129
+ const claudeAvailable = await isClaudeCodeAvailable();
130
+ if (!claudeAvailable) {
131
+ throw new Error('Claude Code CLI not available. Install with: npm install -g @anthropic-ai/claude-code');
132
+ }
133
+
134
+ // Initialize memory if provided
135
+ if (this.memoryEngine) {
136
+ await this.memoryEngine.initialize();
137
+ }
138
+
139
+ // Link self-healing to memory
140
+ if (this.selfHealingEngine && this.memoryEngine) {
141
+ this.selfHealingEngine.setMemoryEngine(this.memoryEngine);
142
+ }
143
+
144
+ await this.log('Orchestrator initialized');
145
+ return this;
146
+ }
147
+
148
+ /**
149
+ * Log message to file
150
+ */
151
+ async log(message, level = 'info') {
152
+ const timestamp = new Date().toISOString();
153
+ const line = `[${timestamp}] [${level.toUpperCase()}] ${message}\n`;
154
+
155
+ if (this.logPath) {
156
+ await appendToFile(this.logPath, line);
157
+ }
158
+
159
+ if (level === 'error') {
160
+ console.error(chalk.red(message));
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Main build entry point
166
+ */
167
+ async build(description, options = {}) {
168
+ this.buildState.startTime = Date.now();
169
+
170
+ try {
171
+ // Step 1: Decompose project
172
+ this.setState(ORCHESTRATOR_STATES.DECOMPOSING);
173
+ await this.log(`Decomposing: "${description}"`);
174
+
175
+ const decomposition = await this.decompositionEngine.decompose(description, options);
176
+
177
+ // Store in memory
178
+ if (this.memoryEngine) {
179
+ await this.memoryEngine.setProjectContext({
180
+ description,
181
+ type: decomposition.projectType,
182
+ complexity: decomposition.estimatedComplexity,
183
+ totalModules: decomposition.totalModules
184
+ });
185
+ }
186
+
187
+ await this.log(`Decomposed into ${decomposition.totalModules} modules: ${decomposition.buildOrder.join(', ')}`);
188
+
189
+ // Step 2: Build modules in order
190
+ this.setState(ORCHESTRATOR_STATES.BUILDING);
191
+
192
+ const results = await this.buildModules(decomposition);
193
+
194
+ // Step 3: Final summary
195
+ if (results.success) {
196
+ this.setState(ORCHESTRATOR_STATES.COMPLETED);
197
+ } else {
198
+ this.setState(ORCHESTRATOR_STATES.FAILED);
199
+ }
200
+
201
+ return this.generateBuildReport(decomposition, results);
202
+
203
+ } catch (error) {
204
+ this.setState(ORCHESTRATOR_STATES.FAILED);
205
+ await this.log(`Build failed: ${error.message}`, 'error');
206
+ throw error;
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Build all modules in dependency order
212
+ */
213
+ async buildModules(decomposition) {
214
+ const results = {
215
+ success: true,
216
+ modules: {},
217
+ totalTime: 0
218
+ };
219
+
220
+ for (const moduleId of decomposition.buildOrder) {
221
+ // Check if we should stop
222
+ if (this.state === ORCHESTRATOR_STATES.PAUSED) {
223
+ await this.log('Build paused');
224
+ break;
225
+ }
226
+
227
+ // Check if module can be built (dependencies satisfied)
228
+ if (!this.decompositionEngine.canBuildModule(moduleId)) {
229
+ const depStatus = decomposition.dependencyGraph[moduleId];
230
+ const failedDeps = depStatus?.dependsOn.filter(d =>
231
+ this.buildState.failedModules.includes(d)
232
+ );
233
+
234
+ if (failedDeps?.length > 0) {
235
+ await this.log(`Skipping ${moduleId}: dependencies failed (${failedDeps.join(', ')})`, 'warn');
236
+ this.buildState.skippedModules.push(moduleId);
237
+ results.modules[moduleId] = { status: 'skipped', reason: 'dependencies_failed' };
238
+ continue;
239
+ }
240
+ }
241
+
242
+ // Build the module
243
+ const moduleResult = await this.buildModule(moduleId, decomposition);
244
+ results.modules[moduleId] = moduleResult;
245
+
246
+ if (!moduleResult.success) {
247
+ results.success = false;
248
+
249
+ if (!this.config.continueOnFailure) {
250
+ await this.log(`Stopping build due to module failure: ${moduleId}`, 'error');
251
+ break;
252
+ }
253
+ }
254
+ }
255
+
256
+ results.totalTime = Date.now() - this.buildState.startTime;
257
+ return results;
258
+ }
259
+
260
+ /**
261
+ * Build a single module
262
+ */
263
+ async buildModule(moduleId, decomposition) {
264
+ const module = this.decompositionEngine.getModule(moduleId);
265
+ if (!module) {
266
+ return { success: false, error: 'Module not found' };
267
+ }
268
+
269
+ this.buildState.currentModule = moduleId;
270
+ this.emit(EVENTS.MODULE_START, { moduleId, module });
271
+
272
+ const spinner = ora({
273
+ text: chalk.cyan(`Building module: ${module.name}`),
274
+ prefixText: this.getProgressPrefix()
275
+ }).start();
276
+
277
+ // Record in memory
278
+ if (this.memoryEngine) {
279
+ await this.memoryEngine.startModule(moduleId, {
280
+ name: module.name,
281
+ description: module.description
282
+ });
283
+ }
284
+
285
+ let attempts = 0;
286
+ let lastError = null;
287
+ let healingPrompt = null; // Store fix prompt from self-healing
288
+
289
+ while (attempts < this.config.maxModuleRetries) {
290
+ attempts++;
291
+ module.buildAttempts = attempts;
292
+
293
+ try {
294
+ await this.log(`Building ${moduleId} (attempt ${attempts}/${this.config.maxModuleRetries})`);
295
+
296
+ // Use healing prompt if available (from previous retry), otherwise generate fresh
297
+ let prompt;
298
+ if (healingPrompt) {
299
+ prompt = healingPrompt;
300
+ healingPrompt = null; // Clear after use
301
+ } else {
302
+ prompt = this.generateBuildPrompt(module, decomposition);
303
+ }
304
+
305
+ // Run Claude Code
306
+ const buildResult = await this.runClaudeBuild(prompt, moduleId);
307
+
308
+ if (buildResult.success) {
309
+ // Run tests if configured
310
+ if (this.config.testAfterEachModule) {
311
+ spinner.text = chalk.cyan(`Testing module: ${module.name}`);
312
+ this.setState(ORCHESTRATOR_STATES.TESTING);
313
+
314
+ const testResult = await runTests(this.projectPath);
315
+
316
+ if (!testResult.passed) {
317
+ throw new Error(`Tests failed: ${testResult.summary.failed} failures`);
318
+ }
319
+ }
320
+
321
+ // Success!
322
+ spinner.succeed(chalk.green(`Module complete: ${module.name}`));
323
+
324
+ this.decompositionEngine.updateModuleStatus(moduleId, 'completed', {
325
+ files: buildResult.files || []
326
+ });
327
+ this.buildState.completedModules.push(moduleId);
328
+
329
+ // Record in memory
330
+ if (this.memoryEngine) {
331
+ await this.memoryEngine.completeModule(moduleId, {
332
+ files: buildResult.files,
333
+ attempts
334
+ });
335
+ }
336
+
337
+ this.emit(EVENTS.MODULE_COMPLETE, { moduleId, result: buildResult });
338
+
339
+ return {
340
+ success: true,
341
+ attempts,
342
+ files: buildResult.files
343
+ };
344
+ }
345
+
346
+ throw new Error(buildResult.error || 'Build failed');
347
+
348
+ } catch (error) {
349
+ lastError = error;
350
+ await this.log(`Module ${moduleId} attempt ${attempts} failed: ${error.message}`, 'error');
351
+
352
+ // Try self-healing
353
+ if (this.selfHealingEngine && attempts < this.config.maxModuleRetries) {
354
+ spinner.text = chalk.yellow(`Healing module: ${module.name}`);
355
+ this.setState(ORCHESTRATOR_STATES.HEALING);
356
+ this.emit(EVENTS.HEALING_START, { moduleId, error });
357
+
358
+ const healing = await this.selfHealingEngine.heal(error.message, moduleId, {
359
+ attempt: attempts,
360
+ completedModules: this.buildState.completedModules
361
+ });
362
+
363
+ if (healing.shouldRetry) {
364
+ const errorCategory = healing.analysis?.category || 'UNKNOWN';
365
+ await this.log(`Self-healing: ${errorCategory} error, retrying...`);
366
+ this.buildState.totalRetries++;
367
+
368
+ // Store healing prompt for next iteration
369
+ healingPrompt = healing.prompt;
370
+
371
+ // Record healing attempt
372
+ if (this.memoryEngine) {
373
+ await this.memoryEngine.recordError({
374
+ message: error.message,
375
+ type: errorCategory,
376
+ moduleId,
377
+ healingAttempt: attempts
378
+ });
379
+ }
380
+
381
+ continue;
382
+ }
383
+ }
384
+
385
+ break; // Exit retry loop if can't heal
386
+ }
387
+ }
388
+
389
+ // Module failed
390
+ spinner.fail(chalk.red(`Module failed: ${module.name}`));
391
+
392
+ this.decompositionEngine.updateModuleStatus(moduleId, 'failed', {
393
+ error: lastError?.message
394
+ });
395
+ this.buildState.failedModules.push(moduleId);
396
+ this.buildState.errors.push({ moduleId, error: lastError?.message });
397
+
398
+ if (this.memoryEngine) {
399
+ await this.memoryEngine.failModule(moduleId, lastError);
400
+ }
401
+
402
+ this.emit(EVENTS.MODULE_FAIL, { moduleId, error: lastError, attempts });
403
+
404
+ return {
405
+ success: false,
406
+ attempts,
407
+ error: lastError?.message
408
+ };
409
+ }
410
+
411
+ /**
412
+ * Generate build prompt for a module
413
+ */
414
+ generateBuildPrompt(module, decomposition) {
415
+ let prompt = `# Build Module: ${module.name}\n\n`;
416
+
417
+ // Module description
418
+ prompt += `## Description\n${module.description}\n\n`;
419
+
420
+ // Dependencies context
421
+ if (module.dependencies.length > 0) {
422
+ prompt += `## Dependencies (already built)\n`;
423
+ for (const depId of module.dependencies) {
424
+ const dep = this.decompositionEngine.getModule(depId);
425
+ if (dep) {
426
+ prompt += `- **${dep.name}**: ${dep.description}\n`;
427
+ if (dep.files?.length > 0) {
428
+ prompt += ` Files: ${dep.files.join(', ')}\n`;
429
+ }
430
+ }
431
+ }
432
+ prompt += '\n';
433
+ }
434
+
435
+ // Memory context
436
+ if (this.memoryEngine) {
437
+ const contextSummary = this.memoryEngine.generateContextSummary();
438
+ prompt += contextSummary;
439
+ }
440
+
441
+ // Project context
442
+ const projectContext = this.memoryEngine?.getProjectContext();
443
+ if (projectContext?.description) {
444
+ prompt += `## Project Goal\n${projectContext.description}\n\n`;
445
+ }
446
+
447
+ // Build instructions
448
+ prompt += `## Instructions\n`;
449
+ prompt += `1. Create all necessary files for the ${module.name} module\n`;
450
+ prompt += `2. Follow patterns established in completed modules\n`;
451
+ prompt += `3. Ensure compatibility with dependencies\n`;
452
+ prompt += `4. Add appropriate error handling\n`;
453
+ prompt += `5. Export any functions/components needed by dependent modules\n\n`;
454
+
455
+ // Specific instructions based on module type
456
+ const moduleInstructions = this.getModuleSpecificInstructions(module.id);
457
+ if (moduleInstructions) {
458
+ prompt += `## ${module.name} Specific Requirements\n${moduleInstructions}\n`;
459
+ }
460
+
461
+ return prompt;
462
+ }
463
+
464
+ /**
465
+ * Get module-specific build instructions
466
+ */
467
+ getModuleSpecificInstructions(moduleId) {
468
+ const instructions = {
469
+ core: `- Setup project structure (src/, public/, etc.)
470
+ - Create configuration files (package.json, tsconfig.json if needed)
471
+ - Setup base utilities and helpers`,
472
+
473
+ auth: `- Implement login/signup forms or endpoints
474
+ - Setup session/token management
475
+ - Add password hashing if applicable
476
+ - Create auth middleware/guards`,
477
+
478
+ database: `- Define data models/schemas
479
+ - Setup database connection
480
+ - Create migration scripts if needed
481
+ - Add seed data for development`,
482
+
483
+ api: `- Create REST/GraphQL endpoints
484
+ - Add request validation
485
+ - Implement error handling middleware
486
+ - Add API documentation`,
487
+
488
+ ui: `- Create reusable components
489
+ - Setup styling (CSS/Tailwind/etc.)
490
+ - Ensure responsive design
491
+ - Add accessibility attributes`,
492
+
493
+ pages: `- Create page components/routes
494
+ - Connect to API endpoints
495
+ - Add loading and error states
496
+ - Implement navigation`,
497
+
498
+ tests: `- Write unit tests for core functions
499
+ - Add integration tests for API
500
+ - Create component tests for UI
501
+ - Setup test utilities and mocks`
502
+ };
503
+
504
+ return instructions[moduleId] || null;
505
+ }
506
+
507
+ /**
508
+ * Run Claude Code build
509
+ */
510
+ async runClaudeBuild(prompt, moduleId) {
511
+ const evidencePath = path.join(this.projectPath, '.vibecode', 'agent', 'evidence');
512
+ await ensureDir(evidencePath);
513
+ const logPath = path.join(evidencePath, `${moduleId}.log`);
514
+
515
+ try {
516
+ const result = await spawnClaudeCode(prompt, {
517
+ cwd: this.projectPath,
518
+ logPath,
519
+ timeout: this.config.timeout
520
+ });
521
+
522
+ // Try to detect created files
523
+ const files = await this.detectCreatedFiles();
524
+
525
+ return {
526
+ success: result.code === 0,
527
+ code: result.code,
528
+ output: result.output,
529
+ files,
530
+ error: result.code !== 0 ? result.error : null
531
+ };
532
+ } catch (error) {
533
+ return {
534
+ success: false,
535
+ error: error.message
536
+ };
537
+ }
538
+ }
539
+
540
+ /**
541
+ * Detect files created during build
542
+ */
543
+ async detectCreatedFiles() {
544
+ // This is a simplified version - in real implementation,
545
+ // we would track git status or file system changes
546
+ try {
547
+ const srcPath = path.join(this.projectPath, 'src');
548
+ if (await fs.pathExists(srcPath)) {
549
+ const files = await fs.readdir(srcPath, { recursive: true });
550
+ return files.filter(f => !f.startsWith('.')).slice(0, 20);
551
+ }
552
+ } catch (e) {
553
+ // Ignore
554
+ }
555
+ return [];
556
+ }
557
+
558
+ /**
559
+ * Get progress prefix for spinner
560
+ */
561
+ getProgressPrefix() {
562
+ const completed = this.buildState.completedModules.length;
563
+ const total = this.decompositionEngine?.modules?.length || 0;
564
+ const percent = total > 0 ? Math.round((completed / total) * 100) : 0;
565
+ return chalk.gray(`[${completed}/${total}] ${percent}%`);
566
+ }
567
+
568
+ /**
569
+ * Generate final build report
570
+ */
571
+ generateBuildReport(decomposition, results) {
572
+ const duration = ((Date.now() - this.buildState.startTime) / 1000 / 60).toFixed(1);
573
+
574
+ return {
575
+ success: results.success,
576
+ projectType: decomposition.projectType,
577
+ complexity: decomposition.estimatedComplexity,
578
+ duration: `${duration} minutes`,
579
+
580
+ modules: {
581
+ total: decomposition.totalModules,
582
+ completed: this.buildState.completedModules.length,
583
+ failed: this.buildState.failedModules.length,
584
+ skipped: this.buildState.skippedModules.length
585
+ },
586
+
587
+ buildOrder: decomposition.buildOrder,
588
+ completedModules: this.buildState.completedModules,
589
+ failedModules: this.buildState.failedModules,
590
+ skippedModules: this.buildState.skippedModules,
591
+
592
+ retries: {
593
+ total: this.buildState.totalRetries,
594
+ max: this.config.maxTotalRetries
595
+ },
596
+
597
+ errors: this.buildState.errors,
598
+ moduleResults: results.modules,
599
+
600
+ healingStats: this.selfHealingEngine?.getStats() || null,
601
+ memoryStats: this.memoryEngine?.getStats() || null
602
+ };
603
+ }
604
+
605
+ /**
606
+ * Pause the build
607
+ */
608
+ pause() {
609
+ if (this.state === ORCHESTRATOR_STATES.BUILDING) {
610
+ this.setState(ORCHESTRATOR_STATES.PAUSED);
611
+ }
612
+ }
613
+
614
+ /**
615
+ * Resume paused build
616
+ */
617
+ resume() {
618
+ if (this.state === ORCHESTRATOR_STATES.PAUSED) {
619
+ this.setState(ORCHESTRATOR_STATES.BUILDING);
620
+ }
621
+ }
622
+
623
+ /**
624
+ * Get current build state
625
+ */
626
+ getState() {
627
+ return {
628
+ state: this.state,
629
+ currentModule: this.buildState.currentModule,
630
+ completedModules: this.buildState.completedModules,
631
+ failedModules: this.buildState.failedModules,
632
+ progress: this.getProgressPrefix()
633
+ };
634
+ }
635
+ }
636
+
637
+ /**
638
+ * Create orchestrator instance
639
+ */
640
+ export function createOrchestrator(options = {}) {
641
+ return new Orchestrator(options);
642
+ }
643
+
644
+ export { ORCHESTRATOR_STATES, EVENTS };