codebakers 2.0.3 → 2.0.10
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/dist/advisors-J3S64IZK.js +7 -0
- package/dist/chunk-FWQNLNTI.js +565 -0
- package/dist/chunk-RCC7FYEU.js +319 -0
- package/dist/chunk-YGVDLNXY.js +326 -0
- package/dist/index.js +2813 -1559
- package/dist/prd-HBUCYLVG.js +7 -0
- package/package.json +1 -1
- package/src/commands/build.ts +989 -0
- package/src/commands/code.ts +102 -5
- package/src/commands/migrate.ts +419 -0
- package/src/commands/prd-maker.ts +587 -0
- package/src/index.ts +136 -4
- package/src/utils/files.ts +418 -0
- package/src/utils/nlp.ts +312 -0
- package/src/utils/voice.ts +323 -0
|
@@ -0,0 +1,989 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import * as fs from 'fs-extra';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import Anthropic from '@anthropic-ai/sdk';
|
|
6
|
+
import { execa } from 'execa';
|
|
7
|
+
import { Config } from '../utils/config.js';
|
|
8
|
+
import { EventEmitter } from 'events';
|
|
9
|
+
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// TYPES
|
|
12
|
+
// ============================================================================
|
|
13
|
+
|
|
14
|
+
interface AgentTask {
|
|
15
|
+
id: string;
|
|
16
|
+
name: string;
|
|
17
|
+
description: string;
|
|
18
|
+
folder: string;
|
|
19
|
+
dependencies: string[];
|
|
20
|
+
exports: string[];
|
|
21
|
+
status: 'waiting' | 'running' | 'done' | 'error' | 'healing' | 'asking';
|
|
22
|
+
progress: number;
|
|
23
|
+
currentAction: string;
|
|
24
|
+
branch: string;
|
|
25
|
+
files: string[];
|
|
26
|
+
error?: string;
|
|
27
|
+
healAttempts: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface BuildWave {
|
|
31
|
+
wave: number;
|
|
32
|
+
agents: AgentTask[];
|
|
33
|
+
parallel: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface BuildPlan {
|
|
37
|
+
projectName: string;
|
|
38
|
+
description: string;
|
|
39
|
+
sharedSetup: {
|
|
40
|
+
types: string[];
|
|
41
|
+
components: string[];
|
|
42
|
+
conventions: string;
|
|
43
|
+
};
|
|
44
|
+
waves: BuildWave[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface HealerSolution {
|
|
48
|
+
canFix: boolean;
|
|
49
|
+
solution: string;
|
|
50
|
+
type: 'retry' | 'modify-code' | 'wait' | 'skip-optional' | 'generate-missing';
|
|
51
|
+
newCode?: string;
|
|
52
|
+
waitTime?: number;
|
|
53
|
+
description: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface UserQuestion {
|
|
57
|
+
question: string;
|
|
58
|
+
options: string[];
|
|
59
|
+
context?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Global flag to pause/resume display during questions
|
|
63
|
+
let displayPaused = false;
|
|
64
|
+
|
|
65
|
+
// ============================================================================
|
|
66
|
+
// MAIN BUILD COMMAND
|
|
67
|
+
// ============================================================================
|
|
68
|
+
|
|
69
|
+
export async function buildCommand(prdPath?: string, options: { sequential?: boolean } = {}): Promise<void> {
|
|
70
|
+
const config = new Config();
|
|
71
|
+
|
|
72
|
+
if (!config.isConfigured()) {
|
|
73
|
+
p.log.error('Please run `codebakers setup` first.');
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const anthropicCreds = config.getCredentials('anthropic');
|
|
78
|
+
if (!anthropicCreds?.apiKey) {
|
|
79
|
+
p.log.error('Anthropic API key not configured.');
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Get PRD file
|
|
84
|
+
let prdFile = prdPath;
|
|
85
|
+
if (!prdFile) {
|
|
86
|
+
const file = await p.text({
|
|
87
|
+
message: 'Path to PRD file:',
|
|
88
|
+
placeholder: './my-app-prd.md',
|
|
89
|
+
validate: (v) => !v ? 'PRD file required' : undefined,
|
|
90
|
+
});
|
|
91
|
+
if (p.isCancel(file)) return;
|
|
92
|
+
prdFile = file as string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Read PRD
|
|
96
|
+
if (!await fs.pathExists(prdFile)) {
|
|
97
|
+
p.log.error(`PRD file not found: ${prdFile}`);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const prdContent = await fs.readFile(prdFile, 'utf-8');
|
|
102
|
+
|
|
103
|
+
console.log(chalk.cyan(`
|
|
104
|
+
╔═══════════════════════════════════════════════════════════════╗
|
|
105
|
+
║ 🚀 CODEBAKERS PARALLEL BUILD ║
|
|
106
|
+
╚═══════════════════════════════════════════════════════════════╝
|
|
107
|
+
`));
|
|
108
|
+
|
|
109
|
+
const anthropic = new Anthropic({ apiKey: anthropicCreds.apiKey });
|
|
110
|
+
|
|
111
|
+
// Step 1: Analyze PRD and create build plan
|
|
112
|
+
const spinner = p.spinner();
|
|
113
|
+
spinner.start('Analyzing PRD...');
|
|
114
|
+
|
|
115
|
+
const buildPlan = await analyzePRD(anthropic, prdContent);
|
|
116
|
+
|
|
117
|
+
spinner.stop('PRD analyzed');
|
|
118
|
+
|
|
119
|
+
// Show build plan
|
|
120
|
+
displayBuildPlan(buildPlan);
|
|
121
|
+
|
|
122
|
+
// Check if parallel makes sense
|
|
123
|
+
const totalAgents = buildPlan.waves.reduce((sum, w) => sum + w.agents.length, 0);
|
|
124
|
+
const useParallel = !options.sequential && totalAgents > 2;
|
|
125
|
+
|
|
126
|
+
if (!useParallel) {
|
|
127
|
+
console.log(chalk.dim('\nUsing sequential build (parallel not needed for small projects)\n'));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Confirm
|
|
131
|
+
const proceed = await p.confirm({
|
|
132
|
+
message: `Start ${useParallel ? 'parallel' : 'sequential'} build?`,
|
|
133
|
+
initialValue: true,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
if (!proceed || p.isCancel(proceed)) {
|
|
137
|
+
p.cancel('Build cancelled');
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Step 2: Create project directory
|
|
142
|
+
const projectPath = path.join(process.cwd(), buildPlan.projectName);
|
|
143
|
+
|
|
144
|
+
if (await fs.pathExists(projectPath)) {
|
|
145
|
+
const overwrite = await p.confirm({
|
|
146
|
+
message: `${buildPlan.projectName} already exists. Overwrite?`,
|
|
147
|
+
initialValue: false,
|
|
148
|
+
});
|
|
149
|
+
if (!overwrite || p.isCancel(overwrite)) return;
|
|
150
|
+
await fs.remove(projectPath);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
await fs.ensureDir(projectPath);
|
|
154
|
+
process.chdir(projectPath);
|
|
155
|
+
|
|
156
|
+
// Step 3: Initialize git
|
|
157
|
+
spinner.start('Initializing project...');
|
|
158
|
+
await execa('git', ['init'], { cwd: projectPath });
|
|
159
|
+
spinner.stop('Project initialized');
|
|
160
|
+
|
|
161
|
+
// Step 4: Run setup phase (shared code)
|
|
162
|
+
await runSetupPhase(anthropic, buildPlan, projectPath);
|
|
163
|
+
|
|
164
|
+
// Step 5: Execute build waves
|
|
165
|
+
const startTime = Date.now();
|
|
166
|
+
|
|
167
|
+
if (useParallel) {
|
|
168
|
+
await executeParallelBuild(anthropic, buildPlan, projectPath, config);
|
|
169
|
+
} else {
|
|
170
|
+
await executeSequentialBuild(anthropic, buildPlan, projectPath, config);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Step 6: Integration phase
|
|
174
|
+
await runIntegrationPhase(anthropic, buildPlan, projectPath);
|
|
175
|
+
|
|
176
|
+
// Step 7: Final setup
|
|
177
|
+
spinner.start('Installing dependencies...');
|
|
178
|
+
await execa('npm', ['install'], { cwd: projectPath, reject: false });
|
|
179
|
+
spinner.stop('Dependencies installed');
|
|
180
|
+
|
|
181
|
+
// Done!
|
|
182
|
+
const elapsed = Math.round((Date.now() - startTime) / 1000);
|
|
183
|
+
const minutes = Math.floor(elapsed / 60);
|
|
184
|
+
const seconds = elapsed % 60;
|
|
185
|
+
|
|
186
|
+
console.log(chalk.green(`
|
|
187
|
+
╔═══════════════════════════════════════════════════════════════╗
|
|
188
|
+
║ ✅ BUILD COMPLETE ║
|
|
189
|
+
╠═══════════════════════════════════════════════════════════════╣
|
|
190
|
+
║ ║
|
|
191
|
+
║ Project: ${buildPlan.projectName.padEnd(46)}║
|
|
192
|
+
║ Time: ${(minutes + 'm ' + seconds + 's').padEnd(46)}║
|
|
193
|
+
║ Features: ${(buildPlan.waves.reduce((s, w) => s + w.agents.length, 0) + ' built').padEnd(46)}║
|
|
194
|
+
║ ║
|
|
195
|
+
║ Next steps: ║
|
|
196
|
+
║ cd ${buildPlan.projectName.padEnd(52)}║
|
|
197
|
+
║ npm run dev ║
|
|
198
|
+
║ ║
|
|
199
|
+
╚═══════════════════════════════════════════════════════════════╝
|
|
200
|
+
`));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ============================================================================
|
|
204
|
+
// PRD ANALYZER
|
|
205
|
+
// ============================================================================
|
|
206
|
+
|
|
207
|
+
async function analyzePRD(anthropic: Anthropic, prdContent: string): Promise<BuildPlan> {
|
|
208
|
+
const response = await anthropic.messages.create({
|
|
209
|
+
model: 'claude-sonnet-4-20250514',
|
|
210
|
+
max_tokens: 4096,
|
|
211
|
+
messages: [{
|
|
212
|
+
role: 'user',
|
|
213
|
+
content: `Analyze this PRD and create a build plan with parallel-safe task assignment.
|
|
214
|
+
|
|
215
|
+
PRD:
|
|
216
|
+
${prdContent}
|
|
217
|
+
|
|
218
|
+
Rules:
|
|
219
|
+
1. Each feature gets its own folder (src/features/[name])
|
|
220
|
+
2. Features with no dependencies can run in parallel
|
|
221
|
+
3. Features that depend on others must wait
|
|
222
|
+
4. Group into "waves" - each wave runs after previous completes
|
|
223
|
+
5. Maximum 3 agents per wave
|
|
224
|
+
|
|
225
|
+
Return JSON only:
|
|
226
|
+
{
|
|
227
|
+
"projectName": "kebab-case-name",
|
|
228
|
+
"description": "one line description",
|
|
229
|
+
"sharedSetup": {
|
|
230
|
+
"types": ["User", "Invoice", etc - shared types needed],
|
|
231
|
+
"components": ["Button", "Card", etc - shared UI needed],
|
|
232
|
+
"conventions": "Next.js App Router, TypeScript, Tailwind, shadcn/ui"
|
|
233
|
+
},
|
|
234
|
+
"waves": [
|
|
235
|
+
{
|
|
236
|
+
"wave": 1,
|
|
237
|
+
"parallel": true,
|
|
238
|
+
"agents": [
|
|
239
|
+
{
|
|
240
|
+
"id": "auth",
|
|
241
|
+
"name": "Authentication",
|
|
242
|
+
"description": "Login, signup, session management",
|
|
243
|
+
"folder": "src/features/auth",
|
|
244
|
+
"dependencies": [],
|
|
245
|
+
"exports": ["AuthProvider", "useAuth", "LoginForm"]
|
|
246
|
+
}
|
|
247
|
+
]
|
|
248
|
+
}
|
|
249
|
+
]
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
Think about:
|
|
253
|
+
- What MUST come first (auth, database setup)?
|
|
254
|
+
- What can run at the same time (independent features)?
|
|
255
|
+
- What needs data from other features (dashboards, reports)?`
|
|
256
|
+
}],
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
const text = response.content[0].type === 'text' ? response.content[0].text : '';
|
|
260
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
261
|
+
|
|
262
|
+
if (!jsonMatch) {
|
|
263
|
+
throw new Error('Failed to parse PRD analysis');
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const plan = JSON.parse(jsonMatch[0]) as BuildPlan;
|
|
267
|
+
|
|
268
|
+
// Initialize agent task fields
|
|
269
|
+
for (const wave of plan.waves) {
|
|
270
|
+
for (const agent of wave.agents) {
|
|
271
|
+
(agent as AgentTask).status = 'waiting';
|
|
272
|
+
(agent as AgentTask).progress = 0;
|
|
273
|
+
(agent as AgentTask).currentAction = '';
|
|
274
|
+
(agent as AgentTask).branch = `codebakers/${agent.id}`;
|
|
275
|
+
(agent as AgentTask).files = [];
|
|
276
|
+
(agent as AgentTask).healAttempts = 0;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return plan;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ============================================================================
|
|
284
|
+
// DISPLAY
|
|
285
|
+
// ============================================================================
|
|
286
|
+
|
|
287
|
+
function displayBuildPlan(plan: BuildPlan): void {
|
|
288
|
+
console.log(chalk.bold(`\n📋 Build Plan: ${plan.projectName}\n`));
|
|
289
|
+
console.log(chalk.dim(` ${plan.description}\n`));
|
|
290
|
+
|
|
291
|
+
for (const wave of plan.waves) {
|
|
292
|
+
const parallel = wave.parallel && wave.agents.length > 1;
|
|
293
|
+
const mode = parallel ? chalk.green('parallel') : chalk.yellow('sequential');
|
|
294
|
+
|
|
295
|
+
console.log(chalk.bold(` Wave ${wave.wave} (${mode}):`));
|
|
296
|
+
|
|
297
|
+
for (const agent of wave.agents) {
|
|
298
|
+
const deps = agent.dependencies.length > 0
|
|
299
|
+
? chalk.dim(` ← needs: ${agent.dependencies.join(', ')}`)
|
|
300
|
+
: '';
|
|
301
|
+
console.log(` • ${agent.name} ${deps}`);
|
|
302
|
+
}
|
|
303
|
+
console.log('');
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const totalAgents = plan.waves.reduce((sum, w) => sum + w.agents.length, 0);
|
|
307
|
+
const parallelWaves = plan.waves.filter(w => w.parallel && w.agents.length > 1).length;
|
|
308
|
+
|
|
309
|
+
console.log(chalk.dim(` Total: ${totalAgents} features, ${plan.waves.length} waves, ${parallelWaves} parallel\n`));
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ============================================================================
|
|
313
|
+
// SETUP PHASE
|
|
314
|
+
// ============================================================================
|
|
315
|
+
|
|
316
|
+
async function runSetupPhase(
|
|
317
|
+
anthropic: Anthropic,
|
|
318
|
+
plan: BuildPlan,
|
|
319
|
+
projectPath: string
|
|
320
|
+
): Promise<void> {
|
|
321
|
+
const spinner = p.spinner();
|
|
322
|
+
spinner.start('Setting up project structure...');
|
|
323
|
+
|
|
324
|
+
// Create base structure
|
|
325
|
+
await fs.ensureDir(path.join(projectPath, 'src/app'));
|
|
326
|
+
await fs.ensureDir(path.join(projectPath, 'src/components/ui'));
|
|
327
|
+
await fs.ensureDir(path.join(projectPath, 'src/lib'));
|
|
328
|
+
await fs.ensureDir(path.join(projectPath, 'src/types'));
|
|
329
|
+
await fs.ensureDir(path.join(projectPath, 'src/features'));
|
|
330
|
+
await fs.ensureDir(path.join(projectPath, '.codebakers'));
|
|
331
|
+
|
|
332
|
+
// Generate shared setup code
|
|
333
|
+
const setupResponse = await anthropic.messages.create({
|
|
334
|
+
model: 'claude-sonnet-4-20250514',
|
|
335
|
+
max_tokens: 8192,
|
|
336
|
+
messages: [{
|
|
337
|
+
role: 'user',
|
|
338
|
+
content: `Generate the shared setup files for this project.
|
|
339
|
+
|
|
340
|
+
Project: ${plan.projectName}
|
|
341
|
+
Description: ${plan.description}
|
|
342
|
+
Conventions: ${plan.sharedSetup.conventions}
|
|
343
|
+
Shared Types: ${plan.sharedSetup.types.join(', ')}
|
|
344
|
+
Shared Components: ${plan.sharedSetup.components.join(', ')}
|
|
345
|
+
|
|
346
|
+
Generate these files:
|
|
347
|
+
|
|
348
|
+
1. package.json (Next.js 14, TypeScript, Tailwind, shadcn/ui)
|
|
349
|
+
2. tsconfig.json
|
|
350
|
+
3. tailwind.config.ts
|
|
351
|
+
4. src/lib/utils.ts (cn helper)
|
|
352
|
+
5. src/types/index.ts (shared types)
|
|
353
|
+
6. src/app/layout.tsx (basic layout)
|
|
354
|
+
7. src/app/page.tsx (simple home page placeholder)
|
|
355
|
+
8. .env.example
|
|
356
|
+
9. CLAUDE.md (CodeBakers patterns)
|
|
357
|
+
|
|
358
|
+
Output format:
|
|
359
|
+
<<<FILE: path/to/file>>>
|
|
360
|
+
content
|
|
361
|
+
<<<END_FILE>>>
|
|
362
|
+
|
|
363
|
+
Use TypeScript. Include all necessary imports.`
|
|
364
|
+
}],
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
const setupText = setupResponse.content[0].type === 'text' ? setupResponse.content[0].text : '';
|
|
368
|
+
await writeFilesFromResponse(setupText, projectPath);
|
|
369
|
+
|
|
370
|
+
// Initial git commit
|
|
371
|
+
await execa('git', ['add', '.'], { cwd: projectPath });
|
|
372
|
+
await execa('git', ['commit', '-m', 'Initial setup'], { cwd: projectPath });
|
|
373
|
+
|
|
374
|
+
spinner.stop('Project structure ready');
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// ============================================================================
|
|
378
|
+
// PARALLEL BUILD EXECUTION
|
|
379
|
+
// ============================================================================
|
|
380
|
+
|
|
381
|
+
async function executeParallelBuild(
|
|
382
|
+
anthropic: Anthropic,
|
|
383
|
+
plan: BuildPlan,
|
|
384
|
+
projectPath: string,
|
|
385
|
+
config: Config
|
|
386
|
+
): Promise<void> {
|
|
387
|
+
for (const wave of plan.waves) {
|
|
388
|
+
console.log(chalk.bold(`\n🏗️ Wave ${wave.wave} of ${plan.waves.length}`));
|
|
389
|
+
|
|
390
|
+
if (wave.parallel && wave.agents.length > 1) {
|
|
391
|
+
console.log(chalk.dim(` Running ${wave.agents.length} agents in parallel\n`));
|
|
392
|
+
await executeWaveParallel(anthropic, wave.agents as AgentTask[], projectPath, plan);
|
|
393
|
+
} else {
|
|
394
|
+
console.log(chalk.dim(` Running sequentially\n`));
|
|
395
|
+
for (const agent of wave.agents) {
|
|
396
|
+
await executeAgent(anthropic, agent as AgentTask, projectPath, plan);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Merge all branches from this wave
|
|
401
|
+
await mergeWaveBranches(wave.agents as AgentTask[], projectPath);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
async function executeWaveParallel(
|
|
406
|
+
anthropic: Anthropic,
|
|
407
|
+
agents: AgentTask[],
|
|
408
|
+
projectPath: string,
|
|
409
|
+
plan: BuildPlan
|
|
410
|
+
): Promise<void> {
|
|
411
|
+
// Create progress display
|
|
412
|
+
const display = new ProgressDisplay(agents);
|
|
413
|
+
display.start();
|
|
414
|
+
|
|
415
|
+
// Run agents in parallel
|
|
416
|
+
const promises = agents.map(agent =>
|
|
417
|
+
executeAgentWithProgress(anthropic, agent, projectPath, plan, display)
|
|
418
|
+
);
|
|
419
|
+
|
|
420
|
+
await Promise.all(promises);
|
|
421
|
+
|
|
422
|
+
display.stop();
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
async function executeAgentWithProgress(
|
|
426
|
+
anthropic: Anthropic,
|
|
427
|
+
agent: AgentTask,
|
|
428
|
+
projectPath: string,
|
|
429
|
+
plan: BuildPlan,
|
|
430
|
+
display: ProgressDisplay
|
|
431
|
+
): Promise<void> {
|
|
432
|
+
agent.status = 'running';
|
|
433
|
+
display.update();
|
|
434
|
+
|
|
435
|
+
try {
|
|
436
|
+
await executeAgent(anthropic, agent, projectPath, plan, (progress, action) => {
|
|
437
|
+
agent.progress = progress;
|
|
438
|
+
agent.currentAction = action;
|
|
439
|
+
display.update();
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
agent.status = 'done';
|
|
443
|
+
agent.progress = 100;
|
|
444
|
+
display.update();
|
|
445
|
+
} catch (error) {
|
|
446
|
+
agent.status = 'error';
|
|
447
|
+
agent.error = error instanceof Error ? error.message : 'Unknown error';
|
|
448
|
+
display.update();
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// ============================================================================
|
|
453
|
+
// SEQUENTIAL BUILD EXECUTION
|
|
454
|
+
// ============================================================================
|
|
455
|
+
|
|
456
|
+
async function executeSequentialBuild(
|
|
457
|
+
anthropic: Anthropic,
|
|
458
|
+
plan: BuildPlan,
|
|
459
|
+
projectPath: string,
|
|
460
|
+
config: Config
|
|
461
|
+
): Promise<void> {
|
|
462
|
+
for (const wave of plan.waves) {
|
|
463
|
+
for (const agent of wave.agents) {
|
|
464
|
+
const spinner = p.spinner();
|
|
465
|
+
spinner.start(`Building ${agent.name}...`);
|
|
466
|
+
|
|
467
|
+
try {
|
|
468
|
+
await executeAgent(anthropic, agent as AgentTask, projectPath, plan, (progress, action) => {
|
|
469
|
+
spinner.message = `${agent.name}: ${action} (${progress}%)`;
|
|
470
|
+
});
|
|
471
|
+
spinner.stop(`✓ ${agent.name} complete`);
|
|
472
|
+
} catch (error) {
|
|
473
|
+
spinner.stop(`✗ ${agent.name} failed`);
|
|
474
|
+
throw error;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Merge branch
|
|
478
|
+
await mergeWaveBranches([agent as AgentTask], projectPath);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// ============================================================================
|
|
484
|
+
// AGENT EXECUTION WITH SELF-HEALING
|
|
485
|
+
// ============================================================================
|
|
486
|
+
|
|
487
|
+
async function executeAgent(
|
|
488
|
+
anthropic: Anthropic,
|
|
489
|
+
agent: AgentTask,
|
|
490
|
+
projectPath: string,
|
|
491
|
+
plan: BuildPlan,
|
|
492
|
+
onProgress?: (progress: number, action: string) => void
|
|
493
|
+
): Promise<void> {
|
|
494
|
+
const maxAttempts = 3;
|
|
495
|
+
|
|
496
|
+
// Create branch for this agent
|
|
497
|
+
await execa('git', ['checkout', '-b', agent.branch], { cwd: projectPath });
|
|
498
|
+
|
|
499
|
+
while (agent.healAttempts < maxAttempts) {
|
|
500
|
+
try {
|
|
501
|
+
await runAgentTask(anthropic, agent, projectPath, plan, onProgress);
|
|
502
|
+
|
|
503
|
+
// Commit changes
|
|
504
|
+
await execa('git', ['add', '.'], { cwd: projectPath });
|
|
505
|
+
await execa('git', ['commit', '-m', `feat: ${agent.name}`], { cwd: projectPath, reject: false });
|
|
506
|
+
|
|
507
|
+
// Switch back to main
|
|
508
|
+
await execa('git', ['checkout', 'main'], { cwd: projectPath });
|
|
509
|
+
|
|
510
|
+
return; // Success!
|
|
511
|
+
|
|
512
|
+
} catch (error) {
|
|
513
|
+
agent.healAttempts++;
|
|
514
|
+
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
|
|
515
|
+
|
|
516
|
+
if (agent.healAttempts >= maxAttempts) {
|
|
517
|
+
// Switch back to main before throwing
|
|
518
|
+
await execa('git', ['checkout', 'main'], { cwd: projectPath, reject: false });
|
|
519
|
+
throw new Error(`Agent ${agent.name} failed after ${maxAttempts} attempts: ${errorMsg}`);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Try to heal
|
|
523
|
+
agent.status = 'healing';
|
|
524
|
+
if (onProgress) onProgress(agent.progress, 'Self-healing...');
|
|
525
|
+
|
|
526
|
+
const solution = await healerAgent(anthropic, agent, errorMsg, projectPath, plan);
|
|
527
|
+
|
|
528
|
+
if (solution.canFix) {
|
|
529
|
+
if (onProgress) onProgress(agent.progress, `Healing: ${solution.description}`);
|
|
530
|
+
|
|
531
|
+
if (solution.type === 'wait' && solution.waitTime) {
|
|
532
|
+
await sleep(solution.waitTime);
|
|
533
|
+
} else if (solution.type === 'modify-code' && solution.newCode) {
|
|
534
|
+
// Apply the fix
|
|
535
|
+
await writeFilesFromResponse(solution.newCode, projectPath);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
agent.status = 'running';
|
|
539
|
+
// Loop continues with fix applied
|
|
540
|
+
} else {
|
|
541
|
+
// Healer can't fix, switch back and throw
|
|
542
|
+
await execa('git', ['checkout', 'main'], { cwd: projectPath, reject: false });
|
|
543
|
+
throw new Error(`Agent ${agent.name} failed and could not self-heal: ${errorMsg}`);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
async function runAgentTask(
|
|
550
|
+
anthropic: Anthropic,
|
|
551
|
+
agent: AgentTask,
|
|
552
|
+
projectPath: string,
|
|
553
|
+
plan: BuildPlan,
|
|
554
|
+
onProgress?: (progress: number, action: string) => void
|
|
555
|
+
): Promise<void> {
|
|
556
|
+
if (onProgress) onProgress(10, 'Analyzing requirements...');
|
|
557
|
+
|
|
558
|
+
// Create feature folder
|
|
559
|
+
const featurePath = path.join(projectPath, agent.folder);
|
|
560
|
+
await fs.ensureDir(featurePath);
|
|
561
|
+
|
|
562
|
+
if (onProgress) onProgress(20, 'Generating code...');
|
|
563
|
+
|
|
564
|
+
// Conversation loop to handle questions
|
|
565
|
+
const messages: Array<{ role: 'user' | 'assistant'; content: string }> = [];
|
|
566
|
+
let userAnswers: Record<string, string> = {};
|
|
567
|
+
let iteration = 0;
|
|
568
|
+
const maxIterations = 5; // Prevent infinite loops
|
|
569
|
+
|
|
570
|
+
// Initial prompt
|
|
571
|
+
messages.push({
|
|
572
|
+
role: 'user',
|
|
573
|
+
content: `You are Agent "${agent.id}" building the "${agent.name}" feature.
|
|
574
|
+
|
|
575
|
+
Feature: ${agent.name}
|
|
576
|
+
Description: ${agent.description}
|
|
577
|
+
Your folder: ${agent.folder}
|
|
578
|
+
Exports needed: ${agent.exports.join(', ')}
|
|
579
|
+
|
|
580
|
+
Project context:
|
|
581
|
+
- Project: ${plan.projectName}
|
|
582
|
+
- Conventions: ${plan.sharedSetup.conventions}
|
|
583
|
+
- Shared types available: ${plan.sharedSetup.types.join(', ')}
|
|
584
|
+
|
|
585
|
+
Rules:
|
|
586
|
+
1. ONLY create files in your folder: ${agent.folder}/
|
|
587
|
+
2. Export everything from ${agent.folder}/index.ts
|
|
588
|
+
3. Import shared types from @/types
|
|
589
|
+
4. Import shared components from @/components/ui
|
|
590
|
+
5. Use TypeScript, proper error handling, loading states
|
|
591
|
+
6. Every form needs Zod validation
|
|
592
|
+
7. Every async operation needs try/catch
|
|
593
|
+
8. Every list needs empty state
|
|
594
|
+
|
|
595
|
+
IMPORTANT: If you need to make a significant decision (auth provider, database choice,
|
|
596
|
+
styling approach, etc.) and it's not specified, ASK THE USER first:
|
|
597
|
+
|
|
598
|
+
<<<ASK_USER>>>
|
|
599
|
+
question: Your question here?
|
|
600
|
+
options: Option 1, Option 2, Option 3
|
|
601
|
+
context: Brief context for why you're asking
|
|
602
|
+
<<<END_ASK>>>
|
|
603
|
+
|
|
604
|
+
Only ask for IMPORTANT decisions, not minor implementation details.
|
|
605
|
+
|
|
606
|
+
Generate ALL files needed for this feature.
|
|
607
|
+
|
|
608
|
+
Output format:
|
|
609
|
+
<<<FILE: ${agent.folder}/path/to/file.ts>>>
|
|
610
|
+
content
|
|
611
|
+
<<<END_FILE>>>
|
|
612
|
+
|
|
613
|
+
Start with the index.ts that exports everything.`
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
while (iteration < maxIterations) {
|
|
617
|
+
iteration++;
|
|
618
|
+
|
|
619
|
+
const response = await anthropic.messages.create({
|
|
620
|
+
model: 'claude-sonnet-4-20250514',
|
|
621
|
+
max_tokens: 8192,
|
|
622
|
+
messages,
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
const text = response.content[0].type === 'text' ? response.content[0].text : '';
|
|
626
|
+
messages.push({ role: 'assistant', content: text });
|
|
627
|
+
|
|
628
|
+
// Check if agent is asking a question
|
|
629
|
+
const questionMatch = text.match(/<<<ASK_USER>>>([\s\S]*?)<<<END_ASK>>>/);
|
|
630
|
+
|
|
631
|
+
if (questionMatch) {
|
|
632
|
+
// Parse the question
|
|
633
|
+
const questionBlock = questionMatch[1];
|
|
634
|
+
const questionLine = questionBlock.match(/question:\s*(.+)/);
|
|
635
|
+
const optionsLine = questionBlock.match(/options:\s*(.+)/);
|
|
636
|
+
const contextLine = questionBlock.match(/context:\s*(.+)/);
|
|
637
|
+
|
|
638
|
+
if (questionLine && optionsLine) {
|
|
639
|
+
const question = questionLine[1].trim();
|
|
640
|
+
const options = optionsLine[1].split(',').map(o => o.trim());
|
|
641
|
+
const context = contextLine ? contextLine[1].trim() : '';
|
|
642
|
+
|
|
643
|
+
// Ask the user
|
|
644
|
+
const answer = await askUserQuestion(agent, {
|
|
645
|
+
question,
|
|
646
|
+
options,
|
|
647
|
+
context,
|
|
648
|
+
}, onProgress);
|
|
649
|
+
|
|
650
|
+
if (answer) {
|
|
651
|
+
userAnswers[question] = answer;
|
|
652
|
+
|
|
653
|
+
// Send answer back to agent
|
|
654
|
+
messages.push({
|
|
655
|
+
role: 'user',
|
|
656
|
+
content: `User answered: "${answer}"
|
|
657
|
+
|
|
658
|
+
Now continue building the feature with this choice. Generate the code files.`
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
if (onProgress) onProgress(30 + (iteration * 10), 'Continuing with your choice...');
|
|
662
|
+
continue; // Loop to get the code
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// No question found, check for files
|
|
668
|
+
if (onProgress) onProgress(60, 'Writing files...');
|
|
669
|
+
|
|
670
|
+
const files = await writeFilesFromResponse(text, projectPath);
|
|
671
|
+
agent.files = files;
|
|
672
|
+
|
|
673
|
+
if (onProgress) onProgress(80, 'Validating...');
|
|
674
|
+
|
|
675
|
+
// Basic validation - check if files were created
|
|
676
|
+
if (files.length === 0) {
|
|
677
|
+
throw new Error('No files generated');
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// Check for index.ts
|
|
681
|
+
const hasIndex = files.some(f => f.endsWith('index.ts'));
|
|
682
|
+
if (!hasIndex) {
|
|
683
|
+
throw new Error('Missing index.ts export file');
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
if (onProgress) onProgress(100, 'Complete');
|
|
687
|
+
return; // Success!
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
throw new Error('Agent exceeded maximum iterations');
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// ============================================================================
|
|
694
|
+
// ASK USER QUESTION
|
|
695
|
+
// ============================================================================
|
|
696
|
+
|
|
697
|
+
async function askUserQuestion(
|
|
698
|
+
agent: AgentTask,
|
|
699
|
+
question: UserQuestion,
|
|
700
|
+
onProgress?: (progress: number, action: string) => void
|
|
701
|
+
): Promise<string | null> {
|
|
702
|
+
// Pause display and update status
|
|
703
|
+
displayPaused = true;
|
|
704
|
+
const previousStatus = agent.status;
|
|
705
|
+
agent.status = 'asking';
|
|
706
|
+
|
|
707
|
+
if (onProgress) onProgress(agent.progress, 'Waiting for your input...');
|
|
708
|
+
|
|
709
|
+
// Clear some space
|
|
710
|
+
console.log('\n');
|
|
711
|
+
|
|
712
|
+
// Show the question
|
|
713
|
+
console.log(chalk.cyan(` ┌─────────────────────────────────────────────────────────────`));
|
|
714
|
+
console.log(chalk.cyan(` │ 🤖 ${agent.name} needs your input`));
|
|
715
|
+
console.log(chalk.cyan(` └─────────────────────────────────────────────────────────────`));
|
|
716
|
+
|
|
717
|
+
if (question.context) {
|
|
718
|
+
console.log(chalk.dim(` ${question.context}\n`));
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
const answer = await p.select({
|
|
722
|
+
message: question.question,
|
|
723
|
+
options: question.options.map(o => ({ value: o, label: o })),
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
if (p.isCancel(answer)) {
|
|
727
|
+
// User cancelled - use first option as default
|
|
728
|
+
console.log(chalk.yellow(` Using default: ${question.options[0]}`));
|
|
729
|
+
agent.status = previousStatus;
|
|
730
|
+
displayPaused = false;
|
|
731
|
+
return question.options[0];
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
console.log(chalk.green(` ✓ Got it: ${answer}\n`));
|
|
735
|
+
|
|
736
|
+
// Resume
|
|
737
|
+
agent.status = previousStatus;
|
|
738
|
+
displayPaused = false;
|
|
739
|
+
|
|
740
|
+
return answer as string;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// ============================================================================
|
|
744
|
+
// HEALER AGENT
|
|
745
|
+
// ============================================================================
|
|
746
|
+
|
|
747
|
+
async function healerAgent(
|
|
748
|
+
anthropic: Anthropic,
|
|
749
|
+
agent: AgentTask,
|
|
750
|
+
error: string,
|
|
751
|
+
projectPath: string,
|
|
752
|
+
plan: BuildPlan
|
|
753
|
+
): Promise<HealerSolution> {
|
|
754
|
+
const response = await anthropic.messages.create({
|
|
755
|
+
model: 'claude-sonnet-4-20250514',
|
|
756
|
+
max_tokens: 4096,
|
|
757
|
+
messages: [{
|
|
758
|
+
role: 'user',
|
|
759
|
+
content: `You are the Healer Agent. An agent failed and you need to diagnose and fix it.
|
|
760
|
+
|
|
761
|
+
Failed Agent: ${agent.name} (${agent.id})
|
|
762
|
+
Folder: ${agent.folder}
|
|
763
|
+
Error: ${error}
|
|
764
|
+
Attempt: ${agent.healAttempts + 1} of 3
|
|
765
|
+
|
|
766
|
+
Files created so far: ${agent.files.join(', ') || 'none'}
|
|
767
|
+
|
|
768
|
+
Project context:
|
|
769
|
+
- Other features: ${plan.waves.flatMap(w => w.agents.map(a => a.id)).join(', ')}
|
|
770
|
+
- Shared types: ${plan.sharedSetup.types.join(', ')}
|
|
771
|
+
|
|
772
|
+
Diagnose the error and provide a solution.
|
|
773
|
+
|
|
774
|
+
Return JSON only:
|
|
775
|
+
{
|
|
776
|
+
"canFix": true,
|
|
777
|
+
"solution": "explanation of what went wrong",
|
|
778
|
+
"type": "retry" | "modify-code" | "wait" | "skip-optional" | "generate-missing",
|
|
779
|
+
"description": "short description for UI",
|
|
780
|
+
"waitTime": 5000, // if type is "wait", milliseconds
|
|
781
|
+
"newCode": "<<<FILE: path>>>\\ncontent\\n<<<END_FILE>>>" // if type is "modify-code" or "generate-missing"
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
Common fixes:
|
|
785
|
+
- Missing import → generate the missing file
|
|
786
|
+
- Type error → fix the types
|
|
787
|
+
- Module not found → check path or generate stub
|
|
788
|
+
- Syntax error → fix the syntax
|
|
789
|
+
- Timeout → retry with simpler approach`
|
|
790
|
+
}],
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
const text = response.content[0].type === 'text' ? response.content[0].text : '';
|
|
794
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
795
|
+
|
|
796
|
+
if (!jsonMatch) {
|
|
797
|
+
return {
|
|
798
|
+
canFix: false,
|
|
799
|
+
solution: 'Could not parse healer response',
|
|
800
|
+
type: 'retry',
|
|
801
|
+
description: 'Unknown error',
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
return JSON.parse(jsonMatch[0]);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// ============================================================================
|
|
809
|
+
// INTEGRATION PHASE
|
|
810
|
+
// ============================================================================
|
|
811
|
+
|
|
812
|
+
async function runIntegrationPhase(
|
|
813
|
+
anthropic: Anthropic,
|
|
814
|
+
plan: BuildPlan,
|
|
815
|
+
projectPath: string
|
|
816
|
+
): Promise<void> {
|
|
817
|
+
const spinner = p.spinner();
|
|
818
|
+
spinner.start('Running integration...');
|
|
819
|
+
|
|
820
|
+
// Get all feature exports
|
|
821
|
+
const features = plan.waves.flatMap(w => w.agents);
|
|
822
|
+
|
|
823
|
+
const response = await anthropic.messages.create({
|
|
824
|
+
model: 'claude-sonnet-4-20250514',
|
|
825
|
+
max_tokens: 8192,
|
|
826
|
+
messages: [{
|
|
827
|
+
role: 'user',
|
|
828
|
+
content: `You are the Integration Agent. Wire together all the features.
|
|
829
|
+
|
|
830
|
+
Features built:
|
|
831
|
+
${features.map(f => `- ${f.name}: ${f.folder} exports [${f.exports.join(', ')}]`).join('\n')}
|
|
832
|
+
|
|
833
|
+
Generate:
|
|
834
|
+
1. src/app/layout.tsx - Wire up providers (AuthProvider, etc.)
|
|
835
|
+
2. src/app/page.tsx - Main dashboard/home linking to features
|
|
836
|
+
3. src/app/[feature]/page.tsx - Route for each feature
|
|
837
|
+
4. src/components/Navigation.tsx - Nav linking all features
|
|
838
|
+
5. src/types/index.ts - Re-export all feature types
|
|
839
|
+
|
|
840
|
+
Output format:
|
|
841
|
+
<<<FILE: path>>>
|
|
842
|
+
content
|
|
843
|
+
<<<END_FILE>>>
|
|
844
|
+
|
|
845
|
+
Make sure all features are accessible and properly connected.`
|
|
846
|
+
}],
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
const text = response.content[0].type === 'text' ? response.content[0].text : '';
|
|
850
|
+
await writeFilesFromResponse(text, projectPath);
|
|
851
|
+
|
|
852
|
+
// Final commit
|
|
853
|
+
await execa('git', ['add', '.'], { cwd: projectPath });
|
|
854
|
+
await execa('git', ['commit', '-m', 'Integration: wire up all features'], { cwd: projectPath, reject: false });
|
|
855
|
+
|
|
856
|
+
spinner.stop('Integration complete');
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// ============================================================================
|
|
860
|
+
// GIT HELPERS
|
|
861
|
+
// ============================================================================
|
|
862
|
+
|
|
863
|
+
async function mergeWaveBranches(agents: AgentTask[], projectPath: string): Promise<void> {
|
|
864
|
+
for (const agent of agents) {
|
|
865
|
+
if (agent.status === 'done') {
|
|
866
|
+
try {
|
|
867
|
+
await execa('git', ['merge', agent.branch, '--no-edit'], { cwd: projectPath });
|
|
868
|
+
await execa('git', ['branch', '-d', agent.branch], { cwd: projectPath, reject: false });
|
|
869
|
+
} catch (error) {
|
|
870
|
+
// Merge conflict - shouldn't happen with isolated folders
|
|
871
|
+
console.log(chalk.yellow(` Warning: Merge issue with ${agent.name}, continuing...`));
|
|
872
|
+
await execa('git', ['merge', '--abort'], { cwd: projectPath, reject: false });
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// ============================================================================
|
|
879
|
+
// PROGRESS DISPLAY
|
|
880
|
+
// ============================================================================
|
|
881
|
+
|
|
882
|
+
class ProgressDisplay {
|
|
883
|
+
private agents: AgentTask[];
|
|
884
|
+
private interval: ReturnType<typeof setInterval> | null = null;
|
|
885
|
+
private lastLineCount: number = 0;
|
|
886
|
+
|
|
887
|
+
constructor(agents: AgentTask[]) {
|
|
888
|
+
this.agents = agents;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
start(): void {
|
|
892
|
+
this.render();
|
|
893
|
+
this.interval = setInterval(() => {
|
|
894
|
+
if (!displayPaused) {
|
|
895
|
+
this.render();
|
|
896
|
+
}
|
|
897
|
+
}, 500);
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
stop(): void {
|
|
901
|
+
if (this.interval) {
|
|
902
|
+
clearInterval(this.interval);
|
|
903
|
+
this.interval = null;
|
|
904
|
+
}
|
|
905
|
+
if (!displayPaused) {
|
|
906
|
+
this.render();
|
|
907
|
+
}
|
|
908
|
+
console.log('');
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
update(): void {
|
|
912
|
+
// Will be rendered on next interval
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
pause(): void {
|
|
916
|
+
displayPaused = true;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
resume(): void {
|
|
920
|
+
displayPaused = false;
|
|
921
|
+
// Re-render after question is answered
|
|
922
|
+
console.log(''); // Add spacing
|
|
923
|
+
this.render();
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
private render(): void {
|
|
927
|
+
if (displayPaused) return;
|
|
928
|
+
|
|
929
|
+
// Move cursor up and clear previous render
|
|
930
|
+
if (this.lastLineCount > 0) {
|
|
931
|
+
process.stdout.write(`\x1b[${this.lastLineCount}A\x1b[0J`);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
for (const agent of this.agents) {
|
|
935
|
+
const icon = this.getStatusIcon(agent.status);
|
|
936
|
+
const bar = this.getProgressBar(agent.progress);
|
|
937
|
+
const action = agent.currentAction ? chalk.dim(` ${agent.currentAction}`) : '';
|
|
938
|
+
|
|
939
|
+
console.log(` ${icon} ${agent.name.padEnd(15)} ${bar} ${agent.progress.toString().padStart(3)}%${action}`);
|
|
940
|
+
}
|
|
941
|
+
console.log('');
|
|
942
|
+
|
|
943
|
+
this.lastLineCount = this.agents.length + 1;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
private getStatusIcon(status: AgentTask['status']): string {
|
|
947
|
+
switch (status) {
|
|
948
|
+
case 'waiting': return chalk.gray('○');
|
|
949
|
+
case 'running': return chalk.blue('●');
|
|
950
|
+
case 'healing': return chalk.yellow('⚕');
|
|
951
|
+
case 'asking': return chalk.magenta('?');
|
|
952
|
+
case 'done': return chalk.green('✓');
|
|
953
|
+
case 'error': return chalk.red('✗');
|
|
954
|
+
default: return '?';
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
private getProgressBar(progress: number): string {
|
|
959
|
+
const width = 20;
|
|
960
|
+
const filled = Math.round((progress / 100) * width);
|
|
961
|
+
const empty = width - filled;
|
|
962
|
+
return chalk.green('█'.repeat(filled)) + chalk.gray('░'.repeat(empty));
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// ============================================================================
|
|
967
|
+
// FILE HELPERS
|
|
968
|
+
// ============================================================================
|
|
969
|
+
|
|
970
|
+
async function writeFilesFromResponse(text: string, projectPath: string): Promise<string[]> {
|
|
971
|
+
const fileRegex = /<<<FILE:\s*(.+?)>>>([\s\S]*?)<<<END_FILE>>>/g;
|
|
972
|
+
const files: string[] = [];
|
|
973
|
+
let match;
|
|
974
|
+
|
|
975
|
+
while ((match = fileRegex.exec(text)) !== null) {
|
|
976
|
+
const filePath = path.join(projectPath, match[1].trim());
|
|
977
|
+
const content = match[2].trim();
|
|
978
|
+
|
|
979
|
+
await fs.ensureDir(path.dirname(filePath));
|
|
980
|
+
await fs.writeFile(filePath, content);
|
|
981
|
+
files.push(match[1].trim());
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
return files;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
function sleep(ms: number): Promise<void> {
|
|
988
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
989
|
+
}
|