codebakers 2.0.0 ā 2.0.2
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/index.js +1277 -127
- package/package.json +1 -1
- package/src/commands/advisors.ts +699 -0
- package/src/commands/design.ts +298 -0
- package/src/commands/prd.ts +419 -0
- package/src/index.ts +33 -0
|
@@ -0,0 +1,298 @@
|
|
|
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 { Config } from '../utils/config.js';
|
|
6
|
+
|
|
7
|
+
const DESIGN_PROFILES = {
|
|
8
|
+
minimal: {
|
|
9
|
+
name: 'Minimal',
|
|
10
|
+
inspiration: 'Linear, Notion, Vercel',
|
|
11
|
+
fonts: { heading: 'Inter', body: 'Inter' },
|
|
12
|
+
corners: 'rounded-md',
|
|
13
|
+
shadows: 'none',
|
|
14
|
+
spacing: 'generous',
|
|
15
|
+
},
|
|
16
|
+
bold: {
|
|
17
|
+
name: 'Bold',
|
|
18
|
+
inspiration: 'Stripe, Ramp, Mercury',
|
|
19
|
+
fonts: { heading: 'Plus Jakarta Sans', body: 'Inter' },
|
|
20
|
+
corners: 'rounded-2xl',
|
|
21
|
+
shadows: 'elevated',
|
|
22
|
+
spacing: 'generous',
|
|
23
|
+
},
|
|
24
|
+
editorial: {
|
|
25
|
+
name: 'Editorial',
|
|
26
|
+
inspiration: 'Medium, Substack, NY Times',
|
|
27
|
+
fonts: { heading: 'Playfair Display', body: 'Source Serif Pro' },
|
|
28
|
+
corners: 'rounded-none',
|
|
29
|
+
shadows: 'none',
|
|
30
|
+
spacing: 'reading',
|
|
31
|
+
},
|
|
32
|
+
playful: {
|
|
33
|
+
name: 'Playful',
|
|
34
|
+
inspiration: 'Figma, Slack, Notion',
|
|
35
|
+
fonts: { heading: 'Nunito', body: 'Nunito' },
|
|
36
|
+
corners: 'rounded-full',
|
|
37
|
+
shadows: 'soft',
|
|
38
|
+
spacing: 'comfortable',
|
|
39
|
+
},
|
|
40
|
+
premium: {
|
|
41
|
+
name: 'Premium',
|
|
42
|
+
inspiration: 'Apple, Porsche, Amex',
|
|
43
|
+
fonts: { heading: 'Cormorant Garamond', body: 'Lato' },
|
|
44
|
+
corners: 'rounded-lg',
|
|
45
|
+
shadows: 'subtle',
|
|
46
|
+
spacing: 'luxurious',
|
|
47
|
+
},
|
|
48
|
+
dashboard: {
|
|
49
|
+
name: 'Dashboard',
|
|
50
|
+
inspiration: 'Datadog, Grafana, Linear',
|
|
51
|
+
fonts: { heading: 'Inter', body: 'Inter' },
|
|
52
|
+
corners: 'rounded-md',
|
|
53
|
+
shadows: 'card',
|
|
54
|
+
spacing: 'compact',
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export async function designCommand(subcommand?: string): Promise<void> {
|
|
59
|
+
const config = new Config();
|
|
60
|
+
|
|
61
|
+
if (!config.isInProject()) {
|
|
62
|
+
p.log.error('Not in a CodeBakers project. Run `codebakers init` first.');
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
p.intro(chalk.bgCyan.black(' Design System '));
|
|
67
|
+
|
|
68
|
+
const action = subcommand || await p.select({
|
|
69
|
+
message: 'What do you want to do?',
|
|
70
|
+
options: [
|
|
71
|
+
{ value: 'profile', label: 'šØ Set design profile' },
|
|
72
|
+
{ value: 'palette', label: 'š Generate color palette' },
|
|
73
|
+
{ value: 'check', label: 'ā
Check design quality' },
|
|
74
|
+
{ value: 'view', label: 'š View current settings' },
|
|
75
|
+
],
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
if (p.isCancel(action)) return;
|
|
79
|
+
|
|
80
|
+
switch (action) {
|
|
81
|
+
case 'profile':
|
|
82
|
+
await setProfile();
|
|
83
|
+
break;
|
|
84
|
+
case 'palette':
|
|
85
|
+
await generatePalette();
|
|
86
|
+
break;
|
|
87
|
+
case 'check':
|
|
88
|
+
await checkDesign();
|
|
89
|
+
break;
|
|
90
|
+
case 'view':
|
|
91
|
+
await viewSettings();
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function setProfile(): Promise<void> {
|
|
97
|
+
const profile = await p.select({
|
|
98
|
+
message: 'Choose your design profile:',
|
|
99
|
+
options: Object.entries(DESIGN_PROFILES).map(([key, value]) => ({
|
|
100
|
+
value: key,
|
|
101
|
+
label: `${value.name}`,
|
|
102
|
+
hint: value.inspiration,
|
|
103
|
+
})),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
if (p.isCancel(profile)) return;
|
|
107
|
+
|
|
108
|
+
const selected = DESIGN_PROFILES[profile as keyof typeof DESIGN_PROFILES];
|
|
109
|
+
|
|
110
|
+
// Save to project config
|
|
111
|
+
const configPath = path.join(process.cwd(), '.codebakers', 'design.json');
|
|
112
|
+
await fs.ensureDir(path.dirname(configPath));
|
|
113
|
+
await fs.writeJson(configPath, {
|
|
114
|
+
profile,
|
|
115
|
+
...selected,
|
|
116
|
+
updatedAt: new Date().toISOString(),
|
|
117
|
+
}, { spaces: 2 });
|
|
118
|
+
|
|
119
|
+
p.log.success(`Design profile set to ${selected.name}`);
|
|
120
|
+
|
|
121
|
+
console.log(chalk.dim(`
|
|
122
|
+
Fonts: ${selected.fonts.heading} / ${selected.fonts.body}
|
|
123
|
+
Corners: ${selected.corners}
|
|
124
|
+
Shadows: ${selected.shadows}
|
|
125
|
+
Spacing: ${selected.spacing}
|
|
126
|
+
`));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function generatePalette(): Promise<void> {
|
|
130
|
+
const brandColor = await p.text({
|
|
131
|
+
message: 'Enter your brand color (hex):',
|
|
132
|
+
placeholder: '#6366F1',
|
|
133
|
+
validate: (v) => {
|
|
134
|
+
if (!v.match(/^#[0-9A-Fa-f]{6}$/)) return 'Enter a valid hex color (#RRGGBB)';
|
|
135
|
+
return undefined;
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
if (p.isCancel(brandColor)) return;
|
|
140
|
+
|
|
141
|
+
const palette = generateColorPalette(brandColor as string);
|
|
142
|
+
|
|
143
|
+
// Save to project
|
|
144
|
+
const configPath = path.join(process.cwd(), '.codebakers', 'design.json');
|
|
145
|
+
let config = {};
|
|
146
|
+
if (await fs.pathExists(configPath)) {
|
|
147
|
+
config = await fs.readJson(configPath);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
await fs.writeJson(configPath, {
|
|
151
|
+
...config,
|
|
152
|
+
colors: palette,
|
|
153
|
+
updatedAt: new Date().toISOString(),
|
|
154
|
+
}, { spaces: 2 });
|
|
155
|
+
|
|
156
|
+
p.log.success('Color palette generated!');
|
|
157
|
+
|
|
158
|
+
console.log(`
|
|
159
|
+
${chalk.bgHex(palette.brand[500]).black(' Brand ')} ${palette.brand[500]}
|
|
160
|
+
${chalk.bgHex(palette.brand[100]).black(' Light ')} ${palette.brand[100]}
|
|
161
|
+
${chalk.bgHex(palette.brand[900]).white(' Dark ')} ${palette.brand[900]}
|
|
162
|
+
${chalk.bgHex(palette.accent).black(' Accent ')} ${palette.accent}
|
|
163
|
+
`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function generateColorPalette(hex: string): Record<string, any> {
|
|
167
|
+
// Convert hex to HSL
|
|
168
|
+
const r = parseInt(hex.slice(1, 3), 16) / 255;
|
|
169
|
+
const g = parseInt(hex.slice(3, 5), 16) / 255;
|
|
170
|
+
const b = parseInt(hex.slice(5, 7), 16) / 255;
|
|
171
|
+
|
|
172
|
+
const max = Math.max(r, g, b);
|
|
173
|
+
const min = Math.min(r, g, b);
|
|
174
|
+
let h = 0, s = 0, l = (max + min) / 2;
|
|
175
|
+
|
|
176
|
+
if (max !== min) {
|
|
177
|
+
const d = max - min;
|
|
178
|
+
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
179
|
+
switch (max) {
|
|
180
|
+
case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break;
|
|
181
|
+
case g: h = ((b - r) / d + 2) / 6; break;
|
|
182
|
+
case b: h = ((r - g) / d + 4) / 6; break;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
h = Math.round(h * 360);
|
|
187
|
+
s = Math.round(s * 100);
|
|
188
|
+
l = Math.round(l * 100);
|
|
189
|
+
|
|
190
|
+
// Generate scale
|
|
191
|
+
const hslToHex = (h: number, s: number, l: number): string => {
|
|
192
|
+
l /= 100;
|
|
193
|
+
const a = s * Math.min(l, 1 - l) / 100;
|
|
194
|
+
const f = (n: number) => {
|
|
195
|
+
const k = (n + h / 30) % 12;
|
|
196
|
+
const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
|
|
197
|
+
return Math.round(255 * color).toString(16).padStart(2, '0');
|
|
198
|
+
};
|
|
199
|
+
return `#${f(0)}${f(8)}${f(4)}`;
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
brand: {
|
|
204
|
+
50: hslToHex(h, s * 0.3, 97),
|
|
205
|
+
100: hslToHex(h, s * 0.4, 94),
|
|
206
|
+
200: hslToHex(h, s * 0.5, 86),
|
|
207
|
+
300: hslToHex(h, s * 0.6, 74),
|
|
208
|
+
400: hslToHex(h, s * 0.8, 62),
|
|
209
|
+
500: hex, // Original
|
|
210
|
+
600: hslToHex(h, s, l * 0.85),
|
|
211
|
+
700: hslToHex(h, s, l * 0.7),
|
|
212
|
+
800: hslToHex(h, s, l * 0.55),
|
|
213
|
+
900: hslToHex(h, s, l * 0.4),
|
|
214
|
+
},
|
|
215
|
+
accent: hslToHex((h + 180) % 360, s, l), // Complementary
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function checkDesign(): Promise<void> {
|
|
220
|
+
const spinner = p.spinner();
|
|
221
|
+
spinner.start('Checking design quality...');
|
|
222
|
+
|
|
223
|
+
const cwd = process.cwd();
|
|
224
|
+
const issues: string[] = [];
|
|
225
|
+
|
|
226
|
+
// Check for anti-patterns in code
|
|
227
|
+
const glob = (await import('fast-glob')).default;
|
|
228
|
+
const files = await glob(['src/**/*.{tsx,jsx}'], { cwd });
|
|
229
|
+
|
|
230
|
+
for (const file of files) {
|
|
231
|
+
const content = await fs.readFile(path.join(cwd, file), 'utf-8');
|
|
232
|
+
|
|
233
|
+
// Check for generic gradient hero
|
|
234
|
+
if (content.includes('bg-gradient-to-r from-blue-500') ||
|
|
235
|
+
content.includes('bg-gradient-to-r from-purple-500')) {
|
|
236
|
+
issues.push(`${file}: Generic gradient hero detected`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Check for icon spam
|
|
240
|
+
const iconMatches = content.match(/<(Rocket|Shield|Zap|Star|Check|Lightning)/g);
|
|
241
|
+
if (iconMatches && iconMatches.length > 3) {
|
|
242
|
+
issues.push(`${file}: Too many generic icons (${iconMatches.length})`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Check for generic CTAs
|
|
246
|
+
if (content.includes('>Get Started<') || content.includes('>Learn More<')) {
|
|
247
|
+
issues.push(`${file}: Generic CTA text - be more specific`);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Check for lazy empty states
|
|
251
|
+
if (content.includes('No data') || content.includes('Nothing here')) {
|
|
252
|
+
issues.push(`${file}: Lazy empty state - add helpful message`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Check for small section padding
|
|
256
|
+
if (content.includes('py-4') || content.includes('py-6') || content.includes('py-8')) {
|
|
257
|
+
if (content.includes('<section')) {
|
|
258
|
+
issues.push(`${file}: Section padding too small - use py-16 or larger`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
spinner.stop('Check complete');
|
|
264
|
+
|
|
265
|
+
if (issues.length === 0) {
|
|
266
|
+
console.log(chalk.green('\nā No design issues found!\n'));
|
|
267
|
+
} else {
|
|
268
|
+
console.log(chalk.yellow(`\nā ļø ${issues.length} design issues found:\n`));
|
|
269
|
+
for (const issue of issues) {
|
|
270
|
+
console.log(chalk.dim(` ⢠${issue}`));
|
|
271
|
+
}
|
|
272
|
+
console.log('');
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async function viewSettings(): Promise<void> {
|
|
277
|
+
const configPath = path.join(process.cwd(), '.codebakers', 'design.json');
|
|
278
|
+
|
|
279
|
+
if (!await fs.pathExists(configPath)) {
|
|
280
|
+
p.log.info('No design settings configured. Run `codebakers design profile` to set up.');
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const config = await fs.readJson(configPath);
|
|
285
|
+
|
|
286
|
+
console.log(chalk.bold('\nCurrent Design Settings:\n'));
|
|
287
|
+
console.log(` Profile: ${config.name || config.profile}`);
|
|
288
|
+
console.log(` Fonts: ${config.fonts?.heading} / ${config.fonts?.body}`);
|
|
289
|
+
console.log(` Corners: ${config.corners}`);
|
|
290
|
+
console.log(` Shadows: ${config.shadows}`);
|
|
291
|
+
console.log(` Spacing: ${config.spacing}`);
|
|
292
|
+
|
|
293
|
+
if (config.colors) {
|
|
294
|
+
console.log(` Brand: ${config.colors.brand?.[500]}`);
|
|
295
|
+
console.log(` Accent: ${config.colors.accent}`);
|
|
296
|
+
}
|
|
297
|
+
console.log('');
|
|
298
|
+
}
|
|
@@ -0,0 +1,419 @@
|
|
|
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 { Config } from '../utils/config.js';
|
|
7
|
+
|
|
8
|
+
interface PRDSection {
|
|
9
|
+
title: string;
|
|
10
|
+
content: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface ParsedPRD {
|
|
14
|
+
name: string;
|
|
15
|
+
description: string;
|
|
16
|
+
features: string[];
|
|
17
|
+
pages: string[];
|
|
18
|
+
database: string[];
|
|
19
|
+
integrations: string[];
|
|
20
|
+
design: {
|
|
21
|
+
profile?: string;
|
|
22
|
+
brandColor?: string;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function prdCommand(filePath?: string): Promise<void> {
|
|
27
|
+
const config = new Config();
|
|
28
|
+
|
|
29
|
+
if (!config.isConfigured()) {
|
|
30
|
+
p.log.error('Please run `codebakers setup` first.');
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
p.intro(chalk.bgCyan.black(' Build from PRD '));
|
|
35
|
+
|
|
36
|
+
// Get PRD file
|
|
37
|
+
let prdPath = filePath;
|
|
38
|
+
if (!prdPath) {
|
|
39
|
+
const file = await p.text({
|
|
40
|
+
message: 'Path to PRD file:',
|
|
41
|
+
placeholder: './PRD.md or paste URL',
|
|
42
|
+
validate: (v) => !v ? 'File path required' : undefined,
|
|
43
|
+
});
|
|
44
|
+
if (p.isCancel(file)) return;
|
|
45
|
+
prdPath = file as string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Read PRD
|
|
49
|
+
const spinner = p.spinner();
|
|
50
|
+
spinner.start('Reading PRD...');
|
|
51
|
+
|
|
52
|
+
let prdContent: string;
|
|
53
|
+
try {
|
|
54
|
+
if (prdPath.startsWith('http')) {
|
|
55
|
+
const response = await fetch(prdPath);
|
|
56
|
+
prdContent = await response.text();
|
|
57
|
+
} else {
|
|
58
|
+
prdContent = await fs.readFile(prdPath, 'utf-8');
|
|
59
|
+
}
|
|
60
|
+
} catch (error) {
|
|
61
|
+
spinner.stop('Error');
|
|
62
|
+
p.log.error(`Could not read PRD: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
spinner.stop('PRD loaded');
|
|
67
|
+
|
|
68
|
+
// Parse PRD with AI
|
|
69
|
+
spinner.start('Analyzing PRD...');
|
|
70
|
+
|
|
71
|
+
const anthropicCreds = config.getCredentials('anthropic');
|
|
72
|
+
if (!anthropicCreds?.apiKey) {
|
|
73
|
+
spinner.stop('Error');
|
|
74
|
+
p.log.error('Anthropic API key not configured.');
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const anthropic = new Anthropic({ apiKey: anthropicCreds.apiKey });
|
|
79
|
+
|
|
80
|
+
const parsed = await parsePRD(anthropic, prdContent);
|
|
81
|
+
spinner.stop('PRD analyzed');
|
|
82
|
+
|
|
83
|
+
// Show what was extracted
|
|
84
|
+
console.log(chalk.bold('\nš Extracted from PRD:\n'));
|
|
85
|
+
console.log(` ${chalk.cyan('Name:')} ${parsed.name}`);
|
|
86
|
+
console.log(` ${chalk.cyan('Description:')} ${parsed.description}`);
|
|
87
|
+
console.log(` ${chalk.cyan('Features:')} ${parsed.features.length} features`);
|
|
88
|
+
console.log(` ${chalk.cyan('Pages:')} ${parsed.pages.join(', ') || 'Auto-detect'}`);
|
|
89
|
+
console.log(` ${chalk.cyan('Database:')} ${parsed.database.length} tables`);
|
|
90
|
+
console.log(` ${chalk.cyan('Integrations:')} ${parsed.integrations.join(', ') || 'None'}`);
|
|
91
|
+
console.log('');
|
|
92
|
+
|
|
93
|
+
// Show features
|
|
94
|
+
if (parsed.features.length > 0) {
|
|
95
|
+
console.log(chalk.bold('Features to build:'));
|
|
96
|
+
parsed.features.slice(0, 10).forEach((f, i) => {
|
|
97
|
+
console.log(chalk.dim(` ${i + 1}. ${f}`));
|
|
98
|
+
});
|
|
99
|
+
if (parsed.features.length > 10) {
|
|
100
|
+
console.log(chalk.dim(` ... and ${parsed.features.length - 10} more`));
|
|
101
|
+
}
|
|
102
|
+
console.log('');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Confirm
|
|
106
|
+
const proceed = await p.confirm({
|
|
107
|
+
message: 'Build this project?',
|
|
108
|
+
initialValue: true,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
if (!proceed || p.isCancel(proceed)) {
|
|
112
|
+
p.cancel('Cancelled');
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Build options
|
|
117
|
+
const options = await p.group({
|
|
118
|
+
createInfra: () => p.confirm({
|
|
119
|
+
message: 'Create GitHub + Vercel + Supabase?',
|
|
120
|
+
initialValue: true,
|
|
121
|
+
}),
|
|
122
|
+
designProfile: () => p.select({
|
|
123
|
+
message: 'Design profile:',
|
|
124
|
+
options: [
|
|
125
|
+
{ value: 'minimal', label: 'Minimal (Linear, Notion)' },
|
|
126
|
+
{ value: 'bold', label: 'Bold (Stripe, Ramp)' },
|
|
127
|
+
{ value: 'editorial', label: 'Editorial (Medium, Substack)' },
|
|
128
|
+
{ value: 'playful', label: 'Playful (Figma, Slack)' },
|
|
129
|
+
{ value: 'premium', label: 'Premium (Apple, Porsche)' },
|
|
130
|
+
{ value: 'dashboard', label: 'Dashboard (Datadog, Linear)' },
|
|
131
|
+
],
|
|
132
|
+
initialValue: parsed.design.profile || 'minimal',
|
|
133
|
+
}),
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
if (p.isCancel(options)) return;
|
|
137
|
+
|
|
138
|
+
// Build the project
|
|
139
|
+
await buildFromPRD(parsed, options, anthropic, config);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function parsePRD(anthropic: Anthropic, content: string): Promise<ParsedPRD> {
|
|
143
|
+
const response = await anthropic.messages.create({
|
|
144
|
+
model: 'claude-sonnet-4-20250514',
|
|
145
|
+
max_tokens: 4096,
|
|
146
|
+
messages: [{
|
|
147
|
+
role: 'user',
|
|
148
|
+
content: `Analyze this PRD and extract structured information. Return JSON only, no explanation.
|
|
149
|
+
|
|
150
|
+
PRD:
|
|
151
|
+
${content}
|
|
152
|
+
|
|
153
|
+
Return this exact JSON structure:
|
|
154
|
+
{
|
|
155
|
+
"name": "project name (lowercase, hyphenated)",
|
|
156
|
+
"description": "one sentence description",
|
|
157
|
+
"features": ["feature 1", "feature 2", ...],
|
|
158
|
+
"pages": ["page1", "page2", ...],
|
|
159
|
+
"database": ["table1", "table2", ...],
|
|
160
|
+
"integrations": ["stripe", "supabase", ...],
|
|
161
|
+
"design": {
|
|
162
|
+
"profile": "minimal|bold|editorial|playful|premium|dashboard or null",
|
|
163
|
+
"brandColor": "#hexcolor or null"
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
Extract ALL features mentioned. Include auth, payments, dashboards, etc.
|
|
168
|
+
For pages, list actual pages/routes needed.
|
|
169
|
+
For database, list tables/entities needed.
|
|
170
|
+
For integrations, list third-party services mentioned.`
|
|
171
|
+
}],
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const text = response.content[0].type === 'text' ? response.content[0].text : '';
|
|
175
|
+
|
|
176
|
+
// Extract JSON from response
|
|
177
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
178
|
+
if (!jsonMatch) {
|
|
179
|
+
throw new Error('Could not parse PRD');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return JSON.parse(jsonMatch[0]);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function buildFromPRD(
|
|
186
|
+
prd: ParsedPRD,
|
|
187
|
+
options: { createInfra: boolean; designProfile: string },
|
|
188
|
+
anthropic: Anthropic,
|
|
189
|
+
config: Config
|
|
190
|
+
): Promise<void> {
|
|
191
|
+
const spinner = p.spinner();
|
|
192
|
+
const projectPath = path.join(process.cwd(), prd.name);
|
|
193
|
+
|
|
194
|
+
// Step 1: Create project structure
|
|
195
|
+
spinner.start('Creating project structure...');
|
|
196
|
+
await fs.ensureDir(projectPath);
|
|
197
|
+
await fs.ensureDir(path.join(projectPath, '.codebakers'));
|
|
198
|
+
await fs.ensureDir(path.join(projectPath, 'src', 'app'));
|
|
199
|
+
await fs.ensureDir(path.join(projectPath, 'src', 'components'));
|
|
200
|
+
await fs.ensureDir(path.join(projectPath, 'src', 'lib'));
|
|
201
|
+
spinner.stop('Project structure created');
|
|
202
|
+
|
|
203
|
+
// Step 2: Save PRD to project
|
|
204
|
+
spinner.start('Saving PRD...');
|
|
205
|
+
await fs.writeFile(path.join(projectPath, 'PRD.md'), await fs.readFile(process.cwd(), 'utf-8').catch(() => JSON.stringify(prd, null, 2)));
|
|
206
|
+
await fs.writeJson(path.join(projectPath, '.codebakers', 'prd.json'), prd, { spaces: 2 });
|
|
207
|
+
await fs.writeJson(path.join(projectPath, '.codebakers', 'design.json'), {
|
|
208
|
+
profile: options.designProfile,
|
|
209
|
+
colors: prd.design.brandColor ? { brand: prd.design.brandColor } : undefined,
|
|
210
|
+
}, { spaces: 2 });
|
|
211
|
+
spinner.stop('PRD saved');
|
|
212
|
+
|
|
213
|
+
// Step 3: Generate build plan
|
|
214
|
+
spinner.start('Generating build plan...');
|
|
215
|
+
const buildPlan = await generateBuildPlan(anthropic, prd, options.designProfile);
|
|
216
|
+
await fs.writeJson(path.join(projectPath, '.codebakers', 'build-plan.json'), buildPlan, { spaces: 2 });
|
|
217
|
+
spinner.stop('Build plan generated');
|
|
218
|
+
|
|
219
|
+
// Show build plan
|
|
220
|
+
console.log(chalk.bold('\nšļø Build Plan:\n'));
|
|
221
|
+
buildPlan.phases.forEach((phase: any, i: number) => {
|
|
222
|
+
console.log(chalk.cyan(`Phase ${i + 1}: ${phase.name}`));
|
|
223
|
+
phase.tasks.forEach((task: string) => {
|
|
224
|
+
console.log(chalk.dim(` ⢠${task}`));
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
console.log('');
|
|
228
|
+
|
|
229
|
+
// Step 4: Execute build plan
|
|
230
|
+
const startBuild = await p.confirm({
|
|
231
|
+
message: 'Start building?',
|
|
232
|
+
initialValue: true,
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
if (!startBuild || p.isCancel(startBuild)) {
|
|
236
|
+
p.log.info(`Build plan saved to ${prd.name}/.codebakers/build-plan.json`);
|
|
237
|
+
p.log.info(`Run \`cd ${prd.name} && codebakers code\` to continue building.`);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Execute each phase
|
|
242
|
+
for (let i = 0; i < buildPlan.phases.length; i++) {
|
|
243
|
+
const phase = buildPlan.phases[i];
|
|
244
|
+
console.log(chalk.bold(`\nš¦ Phase ${i + 1}: ${phase.name}\n`));
|
|
245
|
+
|
|
246
|
+
for (const task of phase.tasks) {
|
|
247
|
+
spinner.start(task);
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
await executeTask(anthropic, projectPath, task, prd, options.designProfile);
|
|
251
|
+
spinner.stop(`ā ${task}`);
|
|
252
|
+
} catch (error) {
|
|
253
|
+
spinner.stop(`ā ${task}`);
|
|
254
|
+
p.log.error(error instanceof Error ? error.message : 'Task failed');
|
|
255
|
+
|
|
256
|
+
const continueBuilding = await p.confirm({
|
|
257
|
+
message: 'Continue with next task?',
|
|
258
|
+
initialValue: true,
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
if (!continueBuilding || p.isCancel(continueBuilding)) {
|
|
262
|
+
p.log.info('Build paused. Run `codebakers code` to continue.');
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Step 5: Infrastructure (if selected)
|
|
270
|
+
if (options.createInfra) {
|
|
271
|
+
console.log(chalk.bold('\nš Setting up infrastructure...\n'));
|
|
272
|
+
|
|
273
|
+
spinner.start('Creating GitHub repository...');
|
|
274
|
+
// Would call GitHub service
|
|
275
|
+
spinner.stop('GitHub repository created');
|
|
276
|
+
|
|
277
|
+
spinner.start('Creating Supabase project...');
|
|
278
|
+
// Would call Supabase service
|
|
279
|
+
spinner.stop('Supabase project created');
|
|
280
|
+
|
|
281
|
+
spinner.start('Creating Vercel project...');
|
|
282
|
+
// Would call Vercel service
|
|
283
|
+
spinner.stop('Vercel project created');
|
|
284
|
+
|
|
285
|
+
spinner.start('Deploying...');
|
|
286
|
+
// Would deploy
|
|
287
|
+
spinner.stop('Deployed!');
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Done!
|
|
291
|
+
p.outro(chalk.green(`
|
|
292
|
+
ā Project built from PRD!
|
|
293
|
+
|
|
294
|
+
${chalk.bold('Your project:')}
|
|
295
|
+
${chalk.cyan(`cd ${prd.name}`)}
|
|
296
|
+
${chalk.cyan('npm run dev')}
|
|
297
|
+
|
|
298
|
+
${chalk.bold('Continue building:')}
|
|
299
|
+
${chalk.cyan('codebakers code')} ā AI agent
|
|
300
|
+
${chalk.cyan('codebakers check')} ā Verify patterns
|
|
301
|
+
${chalk.cyan('codebakers deploy')} ā Deploy changes
|
|
302
|
+
`));
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async function generateBuildPlan(
|
|
306
|
+
anthropic: Anthropic,
|
|
307
|
+
prd: ParsedPRD,
|
|
308
|
+
designProfile: string
|
|
309
|
+
): Promise<{ phases: Array<{ name: string; tasks: string[] }> }> {
|
|
310
|
+
const response = await anthropic.messages.create({
|
|
311
|
+
model: 'claude-sonnet-4-20250514',
|
|
312
|
+
max_tokens: 4096,
|
|
313
|
+
messages: [{
|
|
314
|
+
role: 'user',
|
|
315
|
+
content: `Create a build plan for this project. Return JSON only.
|
|
316
|
+
|
|
317
|
+
Project: ${prd.name}
|
|
318
|
+
Description: ${prd.description}
|
|
319
|
+
Features: ${prd.features.join(', ')}
|
|
320
|
+
Pages: ${prd.pages.join(', ')}
|
|
321
|
+
Database tables: ${prd.database.join(', ')}
|
|
322
|
+
Integrations: ${prd.integrations.join(', ')}
|
|
323
|
+
Design: ${designProfile}
|
|
324
|
+
|
|
325
|
+
Return this structure:
|
|
326
|
+
{
|
|
327
|
+
"phases": [
|
|
328
|
+
{
|
|
329
|
+
"name": "Phase name",
|
|
330
|
+
"tasks": ["task 1", "task 2", ...]
|
|
331
|
+
}
|
|
332
|
+
]
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
Phases should be:
|
|
336
|
+
1. Setup (package.json, config, base files)
|
|
337
|
+
2. Database (schema, migrations, types)
|
|
338
|
+
3. Auth (if needed)
|
|
339
|
+
4. Core Features (main functionality)
|
|
340
|
+
5. UI/Pages (frontend)
|
|
341
|
+
6. Integrations (third-party services)
|
|
342
|
+
7. Polish (loading states, error handling, empty states)
|
|
343
|
+
|
|
344
|
+
Keep tasks specific and actionable.`
|
|
345
|
+
}],
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
const text = response.content[0].type === 'text' ? response.content[0].text : '';
|
|
349
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
350
|
+
if (!jsonMatch) {
|
|
351
|
+
throw new Error('Could not generate build plan');
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return JSON.parse(jsonMatch[0]);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async function executeTask(
|
|
358
|
+
anthropic: Anthropic,
|
|
359
|
+
projectPath: string,
|
|
360
|
+
task: string,
|
|
361
|
+
prd: ParsedPRD,
|
|
362
|
+
designProfile: string
|
|
363
|
+
): Promise<void> {
|
|
364
|
+
const response = await anthropic.messages.create({
|
|
365
|
+
model: 'claude-sonnet-4-20250514',
|
|
366
|
+
max_tokens: 8192,
|
|
367
|
+
messages: [{
|
|
368
|
+
role: 'user',
|
|
369
|
+
content: `Execute this task for the project.
|
|
370
|
+
|
|
371
|
+
Project: ${prd.name}
|
|
372
|
+
Task: ${task}
|
|
373
|
+
Design Profile: ${designProfile}
|
|
374
|
+
|
|
375
|
+
Context:
|
|
376
|
+
- Features: ${prd.features.join(', ')}
|
|
377
|
+
- Database: ${prd.database.join(', ')}
|
|
378
|
+
|
|
379
|
+
Output files in this format:
|
|
380
|
+
|
|
381
|
+
<<<FILE: path/to/file.ts>>>
|
|
382
|
+
file content here
|
|
383
|
+
<<<END_FILE>>>
|
|
384
|
+
|
|
385
|
+
<<<FILE: another/file.tsx>>>
|
|
386
|
+
file content here
|
|
387
|
+
<<<END_FILE>>>
|
|
388
|
+
|
|
389
|
+
Follow these rules:
|
|
390
|
+
- Use TypeScript
|
|
391
|
+
- Use Next.js App Router
|
|
392
|
+
- Use Tailwind CSS
|
|
393
|
+
- Use shadcn/ui components
|
|
394
|
+
- Every button needs onClick handler
|
|
395
|
+
- Every form needs Zod validation
|
|
396
|
+
- Every async operation needs loading/error states
|
|
397
|
+
- Every list needs empty state
|
|
398
|
+
- No generic gradient heroes
|
|
399
|
+
- No icon spam
|
|
400
|
+
- Generous spacing (py-16 or larger for sections)
|
|
401
|
+
|
|
402
|
+
Generate ALL files needed for this task.`
|
|
403
|
+
}],
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
const text = response.content[0].type === 'text' ? response.content[0].text : '';
|
|
407
|
+
|
|
408
|
+
// Parse and write files
|
|
409
|
+
const fileRegex = /<<<FILE:\s*(.+?)>>>([\s\S]*?)<<<END_FILE>>>/g;
|
|
410
|
+
let match;
|
|
411
|
+
|
|
412
|
+
while ((match = fileRegex.exec(text)) !== null) {
|
|
413
|
+
const filePath = path.join(projectPath, match[1].trim());
|
|
414
|
+
const content = match[2].trim();
|
|
415
|
+
|
|
416
|
+
await fs.ensureDir(path.dirname(filePath));
|
|
417
|
+
await fs.writeFile(filePath, content);
|
|
418
|
+
}
|
|
419
|
+
}
|