codebakers 2.1.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.
@@ -0,0 +1,658 @@
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 { textWithVoice } from '../utils/voice.js';
9
+
10
+ // ============================================================================
11
+ // WEBSITE TEMPLATES
12
+ // ============================================================================
13
+
14
+ interface WebsiteTemplate {
15
+ id: string;
16
+ name: string;
17
+ description: string;
18
+ icon: string;
19
+ sections: string[];
20
+ style: 'minimal' | 'bold' | 'elegant' | 'playful' | 'corporate';
21
+ }
22
+
23
+ const TEMPLATES: WebsiteTemplate[] = [
24
+ {
25
+ id: 'landing',
26
+ name: 'Landing Page',
27
+ description: 'Convert visitors into customers',
28
+ icon: '🚀',
29
+ sections: ['hero', 'features', 'testimonials', 'pricing', 'cta', 'footer'],
30
+ style: 'bold',
31
+ },
32
+ {
33
+ id: 'saas',
34
+ name: 'SaaS Website',
35
+ description: 'Software product marketing site',
36
+ icon: '💻',
37
+ sections: ['hero', 'features', 'how-it-works', 'pricing', 'faq', 'testimonials', 'cta', 'footer'],
38
+ style: 'minimal',
39
+ },
40
+ {
41
+ id: 'portfolio',
42
+ name: 'Portfolio',
43
+ description: 'Showcase your work',
44
+ icon: '🎨',
45
+ sections: ['hero', 'about', 'projects', 'skills', 'contact', 'footer'],
46
+ style: 'elegant',
47
+ },
48
+ {
49
+ id: 'agency',
50
+ name: 'Agency Website',
51
+ description: 'Professional services company',
52
+ icon: '🏢',
53
+ sections: ['hero', 'services', 'portfolio', 'team', 'testimonials', 'contact', 'footer'],
54
+ style: 'corporate',
55
+ },
56
+ {
57
+ id: 'startup',
58
+ name: 'Startup Page',
59
+ description: 'Early stage company',
60
+ icon: '⚡',
61
+ sections: ['hero', 'problem', 'solution', 'features', 'team', 'waitlist', 'footer'],
62
+ style: 'bold',
63
+ },
64
+ {
65
+ id: 'restaurant',
66
+ name: 'Restaurant',
67
+ description: 'Food & dining establishment',
68
+ icon: '🍽️',
69
+ sections: ['hero', 'menu', 'about', 'gallery', 'reservations', 'location', 'footer'],
70
+ style: 'elegant',
71
+ },
72
+ {
73
+ id: 'ecommerce',
74
+ name: 'E-commerce',
75
+ description: 'Online store',
76
+ icon: '🛒',
77
+ sections: ['hero', 'featured-products', 'categories', 'bestsellers', 'newsletter', 'footer'],
78
+ style: 'minimal',
79
+ },
80
+ {
81
+ id: 'blog',
82
+ name: 'Blog',
83
+ description: 'Content & articles',
84
+ icon: '📝',
85
+ sections: ['hero', 'featured-posts', 'categories', 'newsletter', 'about', 'footer'],
86
+ style: 'minimal',
87
+ },
88
+ {
89
+ id: 'event',
90
+ name: 'Event Page',
91
+ description: 'Conference or meetup',
92
+ icon: '🎉',
93
+ sections: ['hero', 'speakers', 'schedule', 'venue', 'sponsors', 'tickets', 'footer'],
94
+ style: 'bold',
95
+ },
96
+ {
97
+ id: 'nonprofit',
98
+ name: 'Nonprofit',
99
+ description: 'Charity or cause',
100
+ icon: '💚',
101
+ sections: ['hero', 'mission', 'impact', 'programs', 'donate', 'volunteer', 'footer'],
102
+ style: 'elegant',
103
+ },
104
+ {
105
+ id: 'personal',
106
+ name: 'Personal Website',
107
+ description: 'Your online presence',
108
+ icon: '👤',
109
+ sections: ['hero', 'about', 'experience', 'projects', 'blog', 'contact', 'footer'],
110
+ style: 'minimal',
111
+ },
112
+ {
113
+ id: 'coming-soon',
114
+ name: 'Coming Soon',
115
+ description: 'Pre-launch teaser',
116
+ icon: '⏳',
117
+ sections: ['hero', 'countdown', 'features-preview', 'waitlist', 'footer'],
118
+ style: 'bold',
119
+ },
120
+ {
121
+ id: 'custom',
122
+ name: 'Custom',
123
+ description: 'Build from scratch with AI',
124
+ icon: '✨',
125
+ sections: [],
126
+ style: 'minimal',
127
+ },
128
+ ];
129
+
130
+ // ============================================================================
131
+ // MAIN COMMAND
132
+ // ============================================================================
133
+
134
+ export async function websiteCommand(): Promise<void> {
135
+ const config = new Config();
136
+
137
+ if (!config.isConfigured()) {
138
+ console.log(chalk.yellow(`
139
+ ⚠️ CodeBakers isn't set up yet.
140
+
141
+ Run this first:
142
+ ${chalk.cyan('codebakers setup')}
143
+ `));
144
+ return;
145
+ }
146
+
147
+ const anthropicCreds = config.getCredentials('anthropic');
148
+ if (!anthropicCreds?.apiKey) {
149
+ console.log(chalk.yellow(`
150
+ ⚠️ Anthropic API key not configured.
151
+
152
+ The website builder needs Claude AI to generate code.
153
+
154
+ Run this to add your API key:
155
+ ${chalk.cyan('codebakers setup')}
156
+
157
+ Get an API key at:
158
+ ${chalk.dim('https://console.anthropic.com/settings/keys')}
159
+ `));
160
+ return;
161
+ }
162
+
163
+ console.log(chalk.cyan(`
164
+ ╔═══════════════════════════════════════════════════════════════╗
165
+ ║ 🌐 WEBSITE BUILDER ║
166
+ ║ ║
167
+ ║ Describe your website in plain English. ║
168
+ ║ AI builds it in minutes. ║
169
+ ╚═══════════════════════════════════════════════════════════════╝
170
+ `));
171
+
172
+ // Step 1: Choose approach
173
+ const approach = await p.select({
174
+ message: 'How would you like to build your website?',
175
+ options: [
176
+ { value: 'describe', label: '💬 Describe it', hint: 'Tell me what you want in plain English' },
177
+ { value: 'template', label: '📋 Start from template', hint: 'Choose a pre-made structure' },
178
+ { value: 'url', label: '🔗 Clone a design', hint: 'Describe a site you like' },
179
+ ],
180
+ });
181
+
182
+ if (p.isCancel(approach)) return;
183
+
184
+ const anthropic = new Anthropic({ apiKey: anthropicCreds.apiKey });
185
+ let websiteSpec: WebsiteSpec;
186
+
187
+ if (approach === 'describe') {
188
+ websiteSpec = await describeWebsite(anthropic);
189
+ } else if (approach === 'template') {
190
+ websiteSpec = await templateWebsite(anthropic);
191
+ } else {
192
+ websiteSpec = await cloneDesign(anthropic);
193
+ }
194
+
195
+ if (!websiteSpec) return;
196
+
197
+ // Step 2: Confirm spec
198
+ console.log(chalk.cyan(`\n 📋 Website Plan:\n`));
199
+ console.log(chalk.bold(` ${websiteSpec.name}`));
200
+ console.log(chalk.dim(` ${websiteSpec.description}\n`));
201
+ console.log(chalk.dim(` Style: ${websiteSpec.style}`));
202
+ console.log(chalk.dim(` Sections: ${websiteSpec.sections.join(', ')}\n`));
203
+
204
+ const confirm = await p.confirm({
205
+ message: 'Build this website?',
206
+ initialValue: true,
207
+ });
208
+
209
+ if (!confirm || p.isCancel(confirm)) return;
210
+
211
+ // Step 3: Build it
212
+ await buildWebsite(anthropic, websiteSpec, config);
213
+ }
214
+
215
+ // ============================================================================
216
+ // TYPES
217
+ // ============================================================================
218
+
219
+ interface WebsiteSpec {
220
+ name: string;
221
+ description: string;
222
+ style: string;
223
+ colorScheme: {
224
+ primary: string;
225
+ secondary: string;
226
+ accent: string;
227
+ background: string;
228
+ text: string;
229
+ };
230
+ sections: string[];
231
+ content: Record<string, any>;
232
+ features: string[];
233
+ }
234
+
235
+ // ============================================================================
236
+ // DESCRIBE WEBSITE
237
+ // ============================================================================
238
+
239
+ async function describeWebsite(anthropic: Anthropic): Promise<WebsiteSpec | null> {
240
+ console.log(chalk.dim('\n Describe your website. Be as detailed as you want.\n'));
241
+ console.log(chalk.dim(' Examples:'));
242
+ console.log(chalk.dim(' • "A landing page for my AI writing tool called WriteBot"'));
243
+ console.log(chalk.dim(' • "Portfolio site for a photographer, dark theme, minimal"'));
244
+ console.log(chalk.dim(' • "Coffee shop website with menu, location, and online ordering"\n'));
245
+
246
+ const description = await textWithVoice({
247
+ message: 'Describe your website:',
248
+ placeholder: 'A landing page for...',
249
+ });
250
+
251
+ if (p.isCancel(description)) return null;
252
+
253
+ const spinner = p.spinner();
254
+ spinner.start('Understanding your vision...');
255
+
256
+ const response = await anthropic.messages.create({
257
+ model: 'claude-sonnet-4-20250514',
258
+ max_tokens: 2048,
259
+ messages: [{
260
+ role: 'user',
261
+ content: `Based on this website description, create a detailed specification.
262
+
263
+ Description: "${description}"
264
+
265
+ Return JSON only:
266
+ {
267
+ "name": "Project name (kebab-case)",
268
+ "description": "One line description",
269
+ "style": "minimal | bold | elegant | playful | corporate",
270
+ "colorScheme": {
271
+ "primary": "#hex",
272
+ "secondary": "#hex",
273
+ "accent": "#hex",
274
+ "background": "#hex",
275
+ "text": "#hex"
276
+ },
277
+ "sections": ["hero", "features", ...],
278
+ "content": {
279
+ "hero": {
280
+ "headline": "...",
281
+ "subheadline": "...",
282
+ "cta": "..."
283
+ },
284
+ "features": [
285
+ { "title": "...", "description": "...", "icon": "..." }
286
+ ]
287
+ // etc for each section
288
+ },
289
+ "features": ["responsive", "dark-mode", "animations", etc]
290
+ }
291
+
292
+ Make the content compelling and professional. Use appropriate sections for the type of website.`
293
+ }],
294
+ });
295
+
296
+ spinner.stop('Got it!');
297
+
298
+ const text = response.content[0].type === 'text' ? response.content[0].text : '';
299
+ const jsonMatch = text.match(/\{[\s\S]*\}/);
300
+
301
+ if (!jsonMatch) {
302
+ p.log.error('Failed to understand website description');
303
+ return null;
304
+ }
305
+
306
+ return JSON.parse(jsonMatch[0]);
307
+ }
308
+
309
+ // ============================================================================
310
+ // TEMPLATE WEBSITE
311
+ // ============================================================================
312
+
313
+ async function templateWebsite(anthropic: Anthropic): Promise<WebsiteSpec | null> {
314
+ const template = await p.select({
315
+ message: 'Choose a template:',
316
+ options: TEMPLATES.map(t => ({
317
+ value: t.id,
318
+ label: `${t.icon} ${t.name}`,
319
+ hint: t.description,
320
+ })),
321
+ });
322
+
323
+ if (p.isCancel(template)) return null;
324
+
325
+ const selectedTemplate = TEMPLATES.find(t => t.id === template)!;
326
+
327
+ // Get customization details
328
+ const name = await p.text({
329
+ message: 'What is your business/project name?',
330
+ placeholder: 'My Awesome Project',
331
+ });
332
+
333
+ if (p.isCancel(name)) return null;
334
+
335
+ const tagline = await p.text({
336
+ message: 'Tagline or one-line description:',
337
+ placeholder: 'The best solution for...',
338
+ });
339
+
340
+ if (p.isCancel(tagline)) return null;
341
+
342
+ const details = await textWithVoice({
343
+ message: 'Any other details? (or press enter to skip)',
344
+ placeholder: 'We help startups with..., Our colors are blue and white...',
345
+ });
346
+
347
+ if (p.isCancel(details)) return null;
348
+
349
+ const spinner = p.spinner();
350
+ spinner.start('Customizing template...');
351
+
352
+ const response = await anthropic.messages.create({
353
+ model: 'claude-sonnet-4-20250514',
354
+ max_tokens: 2048,
355
+ messages: [{
356
+ role: 'user',
357
+ content: `Create a website spec based on this template and customization.
358
+
359
+ Template: ${selectedTemplate.name}
360
+ Sections: ${selectedTemplate.sections.join(', ')}
361
+ Default Style: ${selectedTemplate.style}
362
+
363
+ Business Name: ${name}
364
+ Tagline: ${tagline}
365
+ Additional Details: ${details || 'None provided'}
366
+
367
+ Return JSON only:
368
+ {
369
+ "name": "project-name-kebab",
370
+ "description": "${tagline}",
371
+ "style": "${selectedTemplate.style}",
372
+ "colorScheme": {
373
+ "primary": "#hex",
374
+ "secondary": "#hex",
375
+ "accent": "#hex",
376
+ "background": "#hex",
377
+ "text": "#hex"
378
+ },
379
+ "sections": ${JSON.stringify(selectedTemplate.sections)},
380
+ "content": {
381
+ // Content for each section, tailored to the business
382
+ },
383
+ "features": ["responsive", "dark-mode", etc]
384
+ }
385
+
386
+ Make the content specific and compelling for this business.`
387
+ }],
388
+ });
389
+
390
+ spinner.stop('Template customized!');
391
+
392
+ const text = response.content[0].type === 'text' ? response.content[0].text : '';
393
+ const jsonMatch = text.match(/\{[\s\S]*\}/);
394
+
395
+ if (!jsonMatch) {
396
+ p.log.error('Failed to customize template');
397
+ return null;
398
+ }
399
+
400
+ return JSON.parse(jsonMatch[0]);
401
+ }
402
+
403
+ // ============================================================================
404
+ // CLONE DESIGN
405
+ // ============================================================================
406
+
407
+ async function cloneDesign(anthropic: Anthropic): Promise<WebsiteSpec | null> {
408
+ console.log(chalk.dim('\n Describe a website design you like.\n'));
409
+ console.log(chalk.dim(' Examples:'));
410
+ console.log(chalk.dim(' • "Like Linear.app - minimal, clean, dark mode"'));
411
+ console.log(chalk.dim(' • "Like Stripe - professional, lots of gradients"'));
412
+ console.log(chalk.dim(' • "Like Notion - simple, friendly, illustrated"\n'));
413
+
414
+ const inspiration = await textWithVoice({
415
+ message: 'What site do you want to be inspired by?',
416
+ placeholder: 'Like Linear.app but for...',
417
+ });
418
+
419
+ if (p.isCancel(inspiration)) return null;
420
+
421
+ const purpose = await p.text({
422
+ message: 'What is YOUR website for?',
423
+ placeholder: 'A project management tool for designers',
424
+ });
425
+
426
+ if (p.isCancel(purpose)) return null;
427
+
428
+ const spinner = p.spinner();
429
+ spinner.start('Analyzing design inspiration...');
430
+
431
+ const response = await anthropic.messages.create({
432
+ model: 'claude-sonnet-4-20250514',
433
+ max_tokens: 2048,
434
+ messages: [{
435
+ role: 'user',
436
+ content: `Create a website spec inspired by another site's design.
437
+
438
+ Inspiration: "${inspiration}"
439
+ Purpose: "${purpose}"
440
+
441
+ Analyze the design style of the inspiration (based on your knowledge of popular websites) and create a spec that captures that aesthetic for this new purpose.
442
+
443
+ Return JSON only:
444
+ {
445
+ "name": "project-name-kebab",
446
+ "description": "One line description",
447
+ "style": "minimal | bold | elegant | playful | corporate",
448
+ "colorScheme": {
449
+ "primary": "#hex - inspired by the reference",
450
+ "secondary": "#hex",
451
+ "accent": "#hex",
452
+ "background": "#hex",
453
+ "text": "#hex"
454
+ },
455
+ "sections": ["appropriate sections for the purpose"],
456
+ "content": {
457
+ // Compelling content for each section
458
+ },
459
+ "features": ["responsive", "dark-mode", "animations", "gradients", etc - based on inspiration]
460
+ }
461
+
462
+ Capture the FEEL of the inspiration but make it original.`
463
+ }],
464
+ });
465
+
466
+ spinner.stop('Design analyzed!');
467
+
468
+ const text = response.content[0].type === 'text' ? response.content[0].text : '';
469
+ const jsonMatch = text.match(/\{[\s\S]*\}/);
470
+
471
+ if (!jsonMatch) {
472
+ p.log.error('Failed to analyze design');
473
+ return null;
474
+ }
475
+
476
+ return JSON.parse(jsonMatch[0]);
477
+ }
478
+
479
+ // ============================================================================
480
+ // BUILD WEBSITE
481
+ // ============================================================================
482
+
483
+ async function buildWebsite(
484
+ anthropic: Anthropic,
485
+ spec: WebsiteSpec,
486
+ config: Config
487
+ ): Promise<void> {
488
+ const projectPath = path.join(process.cwd(), spec.name);
489
+
490
+ // Check if exists
491
+ if (await fs.pathExists(projectPath)) {
492
+ const overwrite = await p.confirm({
493
+ message: `${spec.name} already exists. Overwrite?`,
494
+ initialValue: false,
495
+ });
496
+ if (!overwrite || p.isCancel(overwrite)) return;
497
+ await fs.remove(projectPath);
498
+ }
499
+
500
+ console.log(chalk.cyan(`\n 🏗️ Building ${spec.name}...\n`));
501
+
502
+ const spinner = p.spinner();
503
+
504
+ // Step 1: Create project
505
+ spinner.start('Creating Next.js project...');
506
+
507
+ await execa('npx', [
508
+ 'create-next-app@latest',
509
+ spec.name,
510
+ '--typescript',
511
+ '--tailwind',
512
+ '--eslint',
513
+ '--app',
514
+ '--src-dir',
515
+ '--import-alias', '@/*',
516
+ '--no-git',
517
+ ], { cwd: process.cwd(), reject: false });
518
+
519
+ spinner.stop('Project created');
520
+
521
+ // Step 2: Install shadcn
522
+ spinner.start('Setting up shadcn/ui...');
523
+
524
+ await execa('npx', ['shadcn@latest', 'init', '-y', '-d'], {
525
+ cwd: projectPath,
526
+ reject: false,
527
+ });
528
+
529
+ // Install common components
530
+ await execa('npx', ['shadcn@latest', 'add', 'button', 'card', 'input', 'badge', '-y'], {
531
+ cwd: projectPath,
532
+ reject: false,
533
+ });
534
+
535
+ spinner.stop('UI components ready');
536
+
537
+ // Step 3: Generate pages
538
+ spinner.start('Generating website code...');
539
+
540
+ const response = await anthropic.messages.create({
541
+ model: 'claude-sonnet-4-20250514',
542
+ max_tokens: 16000,
543
+ messages: [{
544
+ role: 'user',
545
+ content: `Generate a complete Next.js website based on this specification.
546
+
547
+ Website Spec:
548
+ ${JSON.stringify(spec, null, 2)}
549
+
550
+ Generate these files:
551
+
552
+ 1. src/app/page.tsx - Main landing page with ALL sections
553
+ 2. src/app/layout.tsx - Root layout with fonts, metadata
554
+ 3. src/app/globals.css - Tailwind config with custom colors
555
+ 4. src/components/sections/Hero.tsx
556
+ 5. src/components/sections/[OtherSections].tsx - One for each section
557
+ 6. src/components/ui/Navbar.tsx
558
+ 7. src/components/ui/Footer.tsx
559
+ 8. tailwind.config.ts - With custom colors from colorScheme
560
+
561
+ Requirements:
562
+ - Use TypeScript
563
+ - Use Tailwind CSS for styling
564
+ - Use shadcn/ui components (Button, Card, Input, Badge)
565
+ - Make it responsive (mobile-first)
566
+ - Add smooth scroll behavior
567
+ - Use modern design patterns
568
+ - Include hover effects and transitions
569
+ - Use Lucide icons where appropriate
570
+
571
+ Color scheme to use:
572
+ - Primary: ${spec.colorScheme.primary}
573
+ - Secondary: ${spec.colorScheme.secondary}
574
+ - Accent: ${spec.colorScheme.accent}
575
+ - Background: ${spec.colorScheme.background}
576
+ - Text: ${spec.colorScheme.text}
577
+
578
+ Style: ${spec.style}
579
+ ${spec.style === 'minimal' ? 'Clean, lots of whitespace, simple' : ''}
580
+ ${spec.style === 'bold' ? 'Strong colors, big typography, impactful' : ''}
581
+ ${spec.style === 'elegant' ? 'Refined, sophisticated, subtle animations' : ''}
582
+ ${spec.style === 'playful' ? 'Fun, colorful, friendly illustrations' : ''}
583
+ ${spec.style === 'corporate' ? 'Professional, trustworthy, structured' : ''}
584
+
585
+ Output format:
586
+ <<<FILE: path/to/file.tsx>>>
587
+ content
588
+ <<<END_FILE>>>
589
+
590
+ Make it production-quality and visually impressive.`
591
+ }],
592
+ });
593
+
594
+ const text = response.content[0].type === 'text' ? response.content[0].text : '';
595
+
596
+ // Write files
597
+ const fileRegex = /<<<FILE:\s*(.+?)>>>([\s\S]*?)<<<END_FILE>>>/g;
598
+ let match;
599
+ let fileCount = 0;
600
+
601
+ while ((match = fileRegex.exec(text)) !== null) {
602
+ const filePath = path.join(projectPath, match[1].trim());
603
+ const content = match[2].trim();
604
+
605
+ await fs.ensureDir(path.dirname(filePath));
606
+ await fs.writeFile(filePath, content);
607
+ fileCount++;
608
+ }
609
+
610
+ spinner.stop(`Generated ${fileCount} files`);
611
+
612
+ // Step 4: Git init
613
+ spinner.start('Initializing git...');
614
+ await execa('git', ['init'], { cwd: projectPath, reject: false });
615
+ await execa('git', ['add', '.'], { cwd: projectPath, reject: false });
616
+ await execa('git', ['commit', '-m', 'Initial website build by CodeBakers'], { cwd: projectPath, reject: false });
617
+ spinner.stop('Git initialized');
618
+
619
+ // Done!
620
+ console.log(chalk.green(`
621
+ ╔═══════════════════════════════════════════════════════════════╗
622
+ ║ ✅ Website built successfully! ║
623
+ ╠═══════════════════════════════════════════════════════════════╣
624
+ ║ ║
625
+ ║ ${spec.name.padEnd(55)}║
626
+ ║ ${spec.description.substring(0, 55).padEnd(55)}║
627
+ ║ ║
628
+ ║ Next steps: ║
629
+ ║ cd ${spec.name.padEnd(52)}║
630
+ ║ npm run dev ║
631
+ ║ ║
632
+ ║ Then open http://localhost:3000 ║
633
+ ║ ║
634
+ ║ Ready to deploy? ║
635
+ ║ codebakers deploy ║
636
+ ║ ║
637
+ ╚═══════════════════════════════════════════════════════════════╝
638
+ `));
639
+
640
+ // Offer to open in browser
641
+ const openDev = await p.confirm({
642
+ message: 'Start development server now?',
643
+ initialValue: true,
644
+ });
645
+
646
+ if (openDev && !p.isCancel(openDev)) {
647
+ console.log(chalk.dim('\n Starting dev server...\n'));
648
+
649
+ // Change to project directory and run dev
650
+ process.chdir(projectPath);
651
+ await execa('npm', ['run', 'dev'], {
652
+ stdio: 'inherit',
653
+ reject: false,
654
+ });
655
+ }
656
+ }
657
+
658
+ export { TEMPLATES };