ebade 0.3.0 → 0.4.1

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.
Files changed (52) hide show
  1. package/ARCHITECTURE.md +109 -0
  2. package/CHANGELOG.md +8 -1
  3. package/README.md +64 -202
  4. package/ROADMAP.md +17 -12
  5. package/cli/scaffold.js +465 -192
  6. package/cli/simulate.js +102 -0
  7. package/cli/templates/feature-grid.tsx +80 -0
  8. package/cli/templates/footer.tsx +121 -0
  9. package/cli/templates/hero-section.tsx +34 -0
  10. package/cli/templates/login-form.tsx +124 -0
  11. package/cli/templates/navbar.tsx +53 -0
  12. package/cli/templates/pricing-table.tsx +140 -0
  13. package/cli/templates/signup-form.tsx +111 -0
  14. package/demo.tape +2 -2
  15. package/examples/saas-dashboard.ebade.yaml +2 -0
  16. package/netlify.toml +7 -0
  17. package/package.json +1 -1
  18. package/packages/mcp-server/README.md +3 -3
  19. package/packages/mcp-server/package.json +2 -2
  20. package/packages/mcp-server/src/index.ts +12 -16
  21. package/packages/mcp-server/src/tools/scaffold.ts +153 -404
  22. package/packages/vscode-extension/README.md +11 -8
  23. package/packages/vscode-extension/ebade-0.3.0.vsix +0 -0
  24. package/packages/vscode-extension/ebade-0.3.1.vsix +0 -0
  25. package/packages/vscode-extension/ebade-0.3.2.vsix +0 -0
  26. package/packages/vscode-extension/images/icon.png +0 -0
  27. package/packages/vscode-extension/package.json +2 -1
  28. package/packages/vscode-extension/snippets/ebade.json +86 -0
  29. package/www/README.md +36 -0
  30. package/www/app/favicon.ico +0 -0
  31. package/{landing/style.css → www/app/globals.css} +691 -57
  32. package/www/app/layout.tsx +66 -0
  33. package/www/app/page.tsx +406 -0
  34. package/www/app/playground/page.tsx +610 -0
  35. package/www/components/Navbar.tsx +67 -0
  36. package/www/components/ThreeCanvas.tsx +156 -0
  37. package/www/next.config.ts +19 -0
  38. package/www/package-lock.json +1779 -0
  39. package/www/package.json +27 -0
  40. package/www/postcss.config.mjs +7 -0
  41. package/www/public/assets/demo.mp4 +0 -0
  42. package/www/public/logo.png +0 -0
  43. package/www/tsconfig.json +42 -0
  44. package/landing/index.html +0 -268
  45. package/landing/main.js +0 -147
  46. package/packages/vscode-extension/images/icon.svg +0 -6
  47. /package/{demo.gif → assets/demo.gif} +0 -0
  48. /package/{demo.mp4 → assets/demo.mp4} +0 -0
  49. /package/{landing → www/public}/_headers +0 -0
  50. /package/{landing → www/public}/favicon.svg +0 -0
  51. /package/{landing → www/public}/og-image.png +0 -0
  52. /package/{landing → www/public}/og-readme.png +0 -0
package/cli/scaffold.js CHANGED
@@ -13,6 +13,7 @@ import yaml from "yaml";
13
13
  import ora from "ora";
14
14
  import prompts from "prompts";
15
15
  import chokidar from "chokidar";
16
+ import { execSync } from "child_process";
16
17
 
17
18
  // ============================================
18
19
  // ANSI Renk Kodları (Terminal çıktısı için)
@@ -39,7 +40,7 @@ ${colors.magenta} ██╔══╝ ${colors.cyan}██╔══██╗$
39
40
  ${colors.magenta} ███████╗${colors.cyan}██████╔╝${colors.magenta}██║ ██║${colors.cyan}██████╔╝${colors.magenta}███████╗
40
41
  ${colors.magenta} ╚══════╝${colors.cyan}╚═════╝ ${colors.magenta}╚═╝ ╚═╝${colors.cyan}╚═════╝ ${colors.magenta}╚══════╝${colors.reset}
41
42
 
42
- ${colors.dim}✨ Agent-First Framework ${colors.yellow}v0.1.0${colors.reset}
43
+ ${colors.dim}✨ Agent-First Framework ${colors.yellow}v0.4.1${colors.reset}
43
44
  `;
44
45
 
45
46
  const log = {
@@ -47,6 +48,8 @@ const log = {
47
48
  success: (msg) => console.log(`${colors.green}✓${colors.reset} ${msg}`),
48
49
  warn: (msg) => console.log(`${colors.yellow}⚠${colors.reset} ${msg}`),
49
50
  file: (msg) => console.log(`${colors.cyan} →${colors.reset} ${msg}`),
51
+ verify: (msg) => console.log(`${colors.yellow} 🔍${colors.reset} ${msg}`),
52
+ error: (msg) => console.log(`${colors.red} ✘${colors.reset} ${msg}`),
50
53
  section: (msg) =>
51
54
  console.log(`\n${colors.bright}${colors.magenta}▸ ${msg}${colors.reset}`),
52
55
  };
@@ -62,60 +65,86 @@ function parseEbade(ebadePath) {
62
65
  // ============================================
63
66
  // Component Generator Templates
64
67
  // ============================================
65
- const componentTemplates = {
66
- "hero-section": (design) => `
67
- export function HeroSection() {
68
- return (
69
- <section className="hero-section">
70
- <div className="hero-content">
71
- <h1 className="hero-title">Welcome to Our Store</h1>
72
- <p className="hero-subtitle">Discover amazing products</p>
73
- <button className="hero-cta" style={{ backgroundColor: '${
74
- design.colors?.primary || "#6366f1"
75
- }' }}>
76
- Shop Now
77
- </button>
78
- </div>
79
- </section>
80
- );
81
- }
82
- `,
83
- "product-grid": (design) => `
84
- export function ProductGrid({ products }) {
85
- return (
86
- <div className="product-grid">
87
- {products.map((product) => (
88
- <ProductCard key={product.id} product={product} />
89
- ))}
90
- </div>
68
+ // ============================================
69
+ // Template Resolver
70
+ // ============================================
71
+ function getComponentTemplate(componentName, design) {
72
+ const templatePath = path.join(
73
+ process.cwd(),
74
+ "cli/templates",
75
+ `${componentName}.tsx`
91
76
  );
92
- }
93
- `,
94
- "add-to-cart": (design) => `
95
- import { useState } from 'react';
96
77
 
97
- export function AddToCart({ product, onAdd }) {
98
- const [quantity, setQuantity] = useState(1);
99
-
78
+ if (fs.existsSync(templatePath)) {
79
+ let content = fs.readFileSync(templatePath, "utf-8");
80
+
81
+ // Config-based replacement (e.g., {{primary}})
82
+ const primaryColor = design?.colors?.primary || "#6366f1";
83
+ content = content.replace(/\{\{primary\}\}/g, primaryColor);
84
+
85
+ return content;
86
+ }
87
+
88
+ // Fallback to placeholder if template file doesn't exist
89
+ return `import React from 'react';
90
+ import { cn } from "@/lib/utils";
91
+
92
+ /**
93
+ * 🧠 Generated via ebade
94
+ * Component: ${toPascalCase(componentName)}
95
+ * Status: Placeholder (No template found in cli/templates)
96
+ */
97
+ export function ${toPascalCase(componentName)}() {
100
98
  return (
101
- <div className="add-to-cart">
102
- <div className="quantity-selector">
103
- <button onClick={() => setQuantity(q => Math.max(1, q - 1))}>-</button>
104
- <span>{quantity}</span>
105
- <button onClick={() => setQuantity(q => q + 1)}>+</button>
99
+ <div className="p-12 border-2 border-dashed border-border rounded-3xl text-center bg-muted/30">
100
+ <div className="w-16 h-16 bg-primary/10 rounded-2xl flex items-center justify-center mx-auto mb-4">
101
+ <span className="text-2xl">🧩</span>
106
102
  </div>
107
- <button
108
- className="add-btn"
109
- style={{ backgroundColor: '${design.colors?.primary || "#6366f1"}' }}
110
- onClick={() => onAdd(product, quantity)}
111
- >
112
- Add to Cart
113
- </button>
103
+ <h3 className="text-xl font-bold mb-2">${toPascalCase(componentName)}</h3>
104
+ <p className="text-sm text-muted-foreground max-w-xs mx-auto">
105
+ No template found for this intent. Create a file at <code>cli/templates/${componentName}.tsx</code> to customize.
106
+ </p>
114
107
  </div>
115
108
  );
116
109
  }
117
- `,
118
- };
110
+ `;
111
+ }
112
+
113
+ function generateComponentTest(componentName) {
114
+ const name = toPascalCase(componentName);
115
+ return `import { describe, it, expect } from 'vitest';
116
+ import { render } from '@testing-library/react';
117
+ import { ${name} } from './${componentName}';
118
+ import React from 'react';
119
+
120
+ describe('${name} Component', () => {
121
+ it('renders without crashing', () => {
122
+ render(<${name} />);
123
+ expect(document.body).toBeDefined();
124
+ });
125
+ });
126
+ `;
127
+ }
128
+
129
+ function generateVitestConfig() {
130
+ return `import { defineConfig } from 'vitest/config';
131
+ import react from '@vitejs/plugin-react';
132
+ import path from 'path';
133
+
134
+ export default defineConfig({
135
+ plugins: [react()],
136
+ test: {
137
+ environment: 'jsdom',
138
+ globals: true,
139
+ },
140
+ resolve: {
141
+ alias: {
142
+ '@': path.resolve(__dirname, './'),
143
+ },
144
+ },
145
+ });
146
+ `;
147
+ }
119
148
 
120
149
  // ============================================
121
150
  // Page Generator
@@ -127,10 +156,13 @@ function generatePage(page, design) {
127
156
  .join("\n") || "";
128
157
 
129
158
  const componentUsage =
130
- page.components?.map((c) => ` <${toPascalCase(c)} />`).join("\n") ||
131
- " {/* No components defined */}";
159
+ page.components
160
+ ?.map((c) => ` <${toPascalCase(c)} />`)
161
+ .join("\n") || " {/* No components defined */}";
162
+
163
+ return `import React from 'react';
164
+ ${componentImports}
132
165
 
133
- return `
134
166
  /**
135
167
  * 🧠 Generated via ebade - The Agent-First Framework
136
168
  * https://github.com/hasankemaldemirci/ebade
@@ -138,19 +170,26 @@ function generatePage(page, design) {
138
170
  * @page('${page.path}')
139
171
  * @intent('${page.intent}')
140
172
  */
141
-
142
- ${componentImports}
143
-
144
-
145
173
  export default function ${toPascalCase(page.intent)}Page() {
146
174
  return (
147
- <main className="page ${page.intent}">
175
+ <div className="min-h-screen bg-slate-950 text-white">
176
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-10">
177
+ <header className="mb-12">
178
+ <h1 className="text-3xl font-bold tracking-tight opacity-90">${toPascalCase(
179
+ page.intent
180
+ )}</h1>
181
+ <p className="text-sm opacity-40 mt-1">Route: ${page.path}</p>
182
+ </header>
183
+
184
+ <main className="space-y-12">
148
185
  ${componentUsage}
149
- </main>
186
+ </main>
187
+ </div>
188
+ </div>
150
189
  );
151
190
  }
152
191
 
153
- // Auth requirement: ${page.auth || "public"}
192
+ // Auth: ${page.auth || "public"}
154
193
  `;
155
194
  }
156
195
 
@@ -221,16 +260,30 @@ function generatePackageJson(config) {
221
260
  build: "next build",
222
261
  start: "next start",
223
262
  lint: "next lint",
263
+ test: "vitest",
224
264
  },
225
265
  dependencies: {
226
266
  next: "^14.0.0",
227
267
  react: "^18.2.0",
228
268
  "react-dom": "^18.2.0",
269
+ "lucide-react": "^0.300.0",
270
+ clsx: "^2.1.0",
271
+ "tailwind-merge": "^2.2.0",
272
+ "class-variance-authority": "^0.7.0",
273
+ "framer-motion": "^11.0.0",
229
274
  },
230
275
  devDependencies: {
231
276
  "@types/node": "^20.0.0",
232
277
  "@types/react": "^18.2.0",
233
278
  "@types/react-dom": "^18.2.0",
279
+ "@testing-library/react": "^14.1.2",
280
+ "@vitejs/plugin-react": "^4.2.0",
281
+ jsdom: "^22.1.0",
282
+ vitest: "^0.34.6",
283
+ autoprefixer: "^10.0.1",
284
+ postcss: "^8.4.0",
285
+ tailwindcss: "^3.4.0",
286
+ "tailwindcss-animate": "^1.0.7",
234
287
  typescript: "^5.0.0",
235
288
  },
236
289
  },
@@ -239,6 +292,87 @@ function generatePackageJson(config) {
239
292
  );
240
293
  }
241
294
 
295
+ function generateTailwindConfig() {
296
+ return `/** @type {import('tailwindcss').Config} */
297
+ module.exports = {
298
+ darkMode: ["class"],
299
+ content: [
300
+ './pages/**/*.{ts,tsx}',
301
+ './components/**/*.{ts,tsx}',
302
+ './app/**/*.{ts,tsx}',
303
+ './src/**/*.{ts,tsx}',
304
+ ],
305
+ prefix: "",
306
+ theme: {
307
+ container: {
308
+ center: true,
309
+ padding: "2rem",
310
+ screens: {
311
+ "2xl": "1400px",
312
+ },
313
+ },
314
+ extend: {
315
+ colors: {
316
+ border: "hsl(var(--border))",
317
+ input: "hsl(var(--input))",
318
+ ring: "hsl(var(--ring))",
319
+ background: "hsl(var(--background))",
320
+ foreground: "hsl(var(--foreground))",
321
+ primary: {
322
+ DEFAULT: "hsl(var(--primary))",
323
+ foreground: "hsl(var(--primary-foreground))",
324
+ },
325
+ secondary: {
326
+ DEFAULT: "hsl(var(--secondary))",
327
+ foreground: "hsl(var(--secondary-foreground))",
328
+ },
329
+ destructive: {
330
+ DEFAULT: "hsl(var(--destructive))",
331
+ foreground: "hsl(var(--destructive-foreground))",
332
+ },
333
+ muted: {
334
+ DEFAULT: "hsl(var(--muted))",
335
+ foreground: "hsl(var(--muted-foreground))",
336
+ },
337
+ accent: {
338
+ DEFAULT: "hsl(var(--accent))",
339
+ foreground: "hsl(var(--accent-foreground))",
340
+ },
341
+ popover: {
342
+ DEFAULT: "hsl(var(--popover))",
343
+ foreground: "hsl(var(--popover-foreground))",
344
+ },
345
+ card: {
346
+ DEFAULT: "hsl(var(--card))",
347
+ foreground: "hsl(var(--card-foreground))",
348
+ },
349
+ },
350
+ borderRadius: {
351
+ lg: "var(--radius)",
352
+ md: "calc(var(--radius) - 2px)",
353
+ sm: "calc(var(--radius) - 4px)",
354
+ },
355
+ keyframes: {
356
+ "accordion-down": {
357
+ from: { height: "0" },
358
+ to: { height: "var(--radix-accordion-content-height)" },
359
+ },
360
+ "accordion-up": {
361
+ from: { height: "var(--radix-accordion-content-height)" },
362
+ to: { height: "0" },
363
+ },
364
+ },
365
+ animation: {
366
+ "accordion-down": "accordion-down 0.2s ease-out",
367
+ "accordion-up": "accordion-up 0.2s ease-out",
368
+ },
369
+ },
370
+ },
371
+ plugins: [require("tailwindcss-animate")],
372
+ }
373
+ `;
374
+ }
375
+
242
376
  function generateNextConfig() {
243
377
  return `/** @type {import('next').NextConfig} */
244
378
  const nextConfig = {};
@@ -250,6 +384,7 @@ function generateTsConfig() {
250
384
  return JSON.stringify(
251
385
  {
252
386
  compilerOptions: {
387
+ target: "es5",
253
388
  lib: ["dom", "dom.iterable", "esnext"],
254
389
  allowJs: true,
255
390
  skipLibCheck: true,
@@ -257,10 +392,10 @@ function generateTsConfig() {
257
392
  noEmit: true,
258
393
  esModuleInterop: true,
259
394
  module: "esnext",
260
- moduleResolution: "bundler",
395
+ moduleResolution: "node",
261
396
  resolveJsonModule: true,
262
397
  isolatedModules: true,
263
- jsx: "preserve",
398
+ jsx: "react-jsx",
264
399
  incremental: true,
265
400
  plugins: [{ name: "next" }],
266
401
  paths: { "@/*": ["./*"] },
@@ -275,7 +410,8 @@ function generateTsConfig() {
275
410
 
276
411
  function generateLayout(config) {
277
412
  const fontFamily = config.design?.font || "Inter";
278
- return `import type { Metadata } from "next";
413
+ return `import React from 'react';
414
+ import type { Metadata } from "next";
279
415
  import "./globals.css";
280
416
 
281
417
  export const metadata: Metadata = {
@@ -307,119 +443,95 @@ export default function RootLayout({
307
443
  }
308
444
 
309
445
  function generateGlobalsCss(design) {
310
- return `/* ebade Generated Design System */
311
- /* Style: ${design.style || "minimal-modern"} */
312
-
313
- :root {
314
- --color-primary: ${design.colors?.primary || "#6366f1"};
315
- --color-secondary: ${design.colors?.secondary || "#f59e0b"};
316
- --color-accent: ${design.colors?.accent || "#10b981"};
317
- --font-family: '${design.font || "Inter"}', system-ui, sans-serif;
318
- --radius-lg: 1rem;
319
- }
320
-
321
- * {
322
- box-sizing: border-box;
323
- margin: 0;
324
- padding: 0;
325
- }
326
-
327
- body {
328
- font-family: var(--font-family);
329
- line-height: 1.6;
330
- color: #1f2937;
331
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
332
- min-height: 100vh;
333
- }
334
-
335
- .page {
336
- min-height: 100vh;
337
- display: flex;
338
- flex-direction: column;
339
- align-items: center;
340
- justify-content: center;
341
- padding: 2rem;
342
- color: white;
343
- }
344
-
345
- .hero-section {
346
- text-align: center;
347
- max-width: 800px;
348
- }
349
-
350
- .hero-title {
351
- font-size: 3.5rem;
352
- font-weight: 800;
353
- margin-bottom: 1rem;
354
- }
355
-
356
- .hero-subtitle {
357
- font-size: 1.25rem;
358
- opacity: 0.9;
359
- margin-bottom: 2rem;
360
- }
361
-
362
- .hero-cta {
363
- background: white;
364
- color: var(--color-primary);
365
- padding: 1rem 2rem;
366
- font-size: 1.1rem;
367
- border-radius: var(--radius-lg);
368
- border: none;
369
- cursor: pointer;
370
- font-weight: 600;
371
- transition: transform 0.2s, box-shadow 0.2s;
372
- }
373
-
374
- .hero-cta:hover {
375
- transform: scale(1.05);
376
- box-shadow: 0 8px 24px rgba(0,0,0,0.2);
377
- }
378
-
379
- .btn-primary {
380
- background-color: var(--color-primary);
381
- color: white;
382
- padding: 0.75rem 1.5rem;
383
- border-radius: var(--radius-lg);
384
- border: none;
385
- cursor: pointer;
386
- transition: opacity 0.2s;
387
- }
388
-
389
- .btn-primary:hover {
390
- opacity: 0.9;
391
- }
392
-
393
- .product-grid {
394
- display: grid;
395
- grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
396
- gap: 1.5rem;
397
- padding: 2rem;
446
+ const primary = design.colors?.primary || "#6366f1";
447
+
448
+ // Helper to convert hex to HSL for CSS vars if needed
449
+ // For now we'll use standard shadcn slate
450
+ return `@tailwind base;
451
+ @tailwind components;
452
+ @tailwind utilities;
453
+
454
+ @layer base {
455
+ :root {
456
+ --background: 0 0% 100%;
457
+ --foreground: 222.2 84% 4.9%;
458
+
459
+ --card: 0 0% 100%;
460
+ --card-foreground: 222.2 84% 4.9%;
461
+
462
+ --popover: 0 0% 100%;
463
+ --popover-foreground: 222.2 84% 4.9%;
464
+
465
+ --primary: 221.2 83.2% 53.3%;
466
+ --primary-foreground: 210 40% 98%;
467
+
468
+ --secondary: 210 40% 96.1%;
469
+ --secondary-foreground: 222.2 47.4% 11.2%;
470
+
471
+ --muted: 210 40% 96.1%;
472
+ --muted-foreground: 215.4 16.3% 46.9%;
473
+
474
+ --accent: 210 40% 96.1%;
475
+ --accent-foreground: 222.2 47.4% 11.2%;
476
+
477
+ --destructive: 0 84.2% 60.2%;
478
+ --destructive-foreground: 210 40% 98%;
479
+
480
+ --border: 214.3 31.8% 91.4%;
481
+ --input: 214.3 31.8% 91.4%;
482
+ --ring: 221.2 83.2% 53.3%;
483
+
484
+ --radius: 0.5rem;
485
+ }
486
+
487
+ .dark {
488
+ --background: 222.2 84% 4.9%;
489
+ --foreground: 210 40% 98%;
490
+
491
+ --card: 222.2 84% 4.9%;
492
+ --card-foreground: 210 40% 98%;
493
+
494
+ --popover: 222.2 84% 4.9%;
495
+ --popover-foreground: 210 40% 98%;
496
+
497
+ --primary: 217.2 91.2% 59.8%;
498
+ --primary-foreground: 222.2 47.4% 11.2%;
499
+
500
+ --secondary: 217.2 32.6% 17.5%;
501
+ --secondary-foreground: 210 40% 98%;
502
+
503
+ --muted: 217.2 32.6% 17.5%;
504
+ --muted-foreground: 215 20.2% 65.1%;
505
+
506
+ --accent: 217.2 32.6% 17.5%;
507
+ --accent-foreground: 210 40% 98%;
508
+
509
+ --destructive: 0 62.8% 30.6%;
510
+ --destructive-foreground: 210 40% 98%;
511
+
512
+ --border: 217.2 32.6% 17.5%;
513
+ --input: 217.2 32.6% 17.5%;
514
+ --ring: 224.3 76.3% 48%;
515
+ }
398
516
  }
399
-
400
- .add-to-cart {
401
- display: flex;
402
- gap: 1rem;
403
- align-items: center;
517
+
518
+ @layer base {
519
+ * {
520
+ @apply border-border;
521
+ }
522
+ body {
523
+ @apply bg-background text-foreground;
524
+ }
404
525
  }
405
-
406
- .quantity-selector {
407
- display: flex;
408
- align-items: center;
409
- gap: 0.5rem;
526
+ `;
410
527
  }
411
528
 
412
- .quantity-selector button {
413
- width: 32px;
414
- height: 32px;
415
- border: 1px solid #e5e7eb;
416
- background: white;
417
- border-radius: 4px;
418
- cursor: pointer;
419
- }
529
+ function generateUtils() {
530
+ return `import { type ClassValue, clsx } from "clsx"
531
+ import { twMerge } from "tailwind-merge"
420
532
 
421
- .add-btn {
422
- flex: 1;
533
+ export function cn(...inputs: ClassValue[]) {
534
+ return twMerge(clsx(inputs))
423
535
  }
424
536
  `;
425
537
  }
@@ -548,9 +660,15 @@ function ensureDir(dir) {
548
660
  // ============================================
549
661
  // Main Scaffold Function
550
662
  // ============================================
551
- function scaffold(ebadePath, outputDir) {
663
+ async function scaffold(ebadePath, outputDir) {
552
664
  const startTime = Date.now();
553
- let stats = { pages: 0, components: 0, apiRoutes: 0, files: 0 };
665
+ const stats = {
666
+ pages: 0,
667
+ components: 0,
668
+ apiRoutes: 0,
669
+ files: 0,
670
+ tokenSavings: 0,
671
+ };
554
672
 
555
673
  console.log(LOGO);
556
674
 
@@ -583,6 +701,10 @@ function scaffold(ebadePath, outputDir) {
583
701
  log.file(`${dir}/`);
584
702
  });
585
703
 
704
+ // lib/utils.ts
705
+ fs.writeFileSync(path.join(projectDir, "lib/utils.ts"), generateUtils());
706
+ log.file("lib/utils.ts");
707
+
586
708
  // ========== Generate Pages ==========
587
709
  log.section("Generating pages");
588
710
 
@@ -618,21 +740,23 @@ function scaffold(ebadePath, outputDir) {
618
740
  const spinner2 = ora("Generating components...").start();
619
741
  allComponents.forEach((component) => {
620
742
  const componentPath = `components/${component}.tsx`;
621
- const template = componentTemplates[component];
622
- const content = template
623
- ? template(config.design)
624
- : `// TODO: Implement ${toPascalCase(
625
- component
626
- )} component\nexport function ${toPascalCase(
627
- component
628
- )}() {\n return <div>${component}</div>;\n}\n`;
743
+ const content = getComponentTemplate(component, config.design);
744
+ stats.tokenSavings += Math.floor(content.length / 4);
629
745
 
630
746
  fs.writeFileSync(path.join(projectDir, componentPath), content.trim());
747
+
748
+ // Generate unit test
749
+ const testPath = `components/${component}.test.tsx`;
750
+ fs.writeFileSync(
751
+ path.join(projectDir, testPath),
752
+ generateComponentTest(component).trim()
753
+ );
754
+
631
755
  stats.components++;
632
- stats.files++;
756
+ stats.files += 2; // Component + Test
633
757
  });
634
758
  spinner2.succeed(
635
- `Generated ${colors.bright}${stats.components}${colors.reset} components`
759
+ `Generated ${colors.bright}${stats.components}${colors.reset} components (+ tests)`
636
760
  );
637
761
 
638
762
  // ========== Generate API Routes ==========
@@ -683,6 +807,20 @@ function scaffold(ebadePath, outputDir) {
683
807
  fs.writeFileSync(path.join(projectDir, "tsconfig.json"), generateTsConfig());
684
808
  log.file("tsconfig.json");
685
809
 
810
+ // tailwind.config.js
811
+ fs.writeFileSync(
812
+ path.join(projectDir, "tailwind.config.js"),
813
+ generateTailwindConfig()
814
+ );
815
+ log.file("tailwind.config.js");
816
+
817
+ // vitest.config.ts
818
+ fs.writeFileSync(
819
+ path.join(projectDir, "vitest.config.ts"),
820
+ generateVitestConfig()
821
+ );
822
+ log.file("vitest.config.ts");
823
+
686
824
  // app/layout.tsx
687
825
  fs.writeFileSync(
688
826
  path.join(projectDir, "app/layout.tsx"),
@@ -737,9 +875,12 @@ function scaffold(ebadePath, outputDir) {
737
875
  fs.copyFileSync(ebadePath, path.join(projectDir, "project.ebade.yaml"));
738
876
  log.file("project.ebade.yaml (for agent reference)");
739
877
 
878
+ // ========== Summary ==========
879
+ // ========== Verify Output ==========
880
+ const verificationResult = await verifyOutput(projectDir, config);
881
+
740
882
  // ========== Summary ==========
741
883
  const duration = ((Date.now() - startTime) / 1000).toFixed(1);
742
- const estimatedTokenSavings = Math.round(stats.files * 35); // ~35 tokens saved per file
743
884
 
744
885
  console.log(`
745
886
  ${colors.bright}${colors.green} ┌${"─".repeat(41)}┐${colors.reset}
@@ -759,14 +900,20 @@ ${colors.green} │${colors.reset} ${colors.cyan}📁 Files Created:${
759
900
  } ${String(stats.files).padEnd(18)} ${colors.green}│${colors.reset}
760
901
  ${colors.green} │${colors.reset} ${colors.cyan}📊 Token Savings:${
761
902
  colors.reset
762
- } ~${String(estimatedTokenSavings).padEnd(17)} ${colors.green}│${
763
- colors.reset
764
- }
903
+ } ~${String(stats.tokenSavings).padEnd(17)} ${colors.green}│${colors.reset}
765
904
  ${colors.green} │${colors.reset} ${colors.cyan}⏱ Completed in:${
766
905
  colors.reset
767
906
  } ${String(duration + "s").padEnd(18)} ${colors.green}│${colors.reset}
907
+ ${colors.green} ├${"─".repeat(41)}┤${colors.reset}
908
+ ${colors.green} │${colors.reset} ${colors.yellow}🔍 Integrity:${
909
+ colors.reset
910
+ } ${(verificationResult.passed ? "PASSED" : "ISSUES FOUND").padEnd(
911
+ 18
912
+ )} ${colors.green}│${colors.reset}
768
913
  ${colors.green} └${"─".repeat(41)}┘${colors.reset}
769
914
 
915
+ ${verificationResult.report}
916
+
770
917
  ${colors.dim}Next steps:${colors.reset}
771
918
  ${colors.gray}1.${colors.reset} cd ${colors.cyan}${projectDir}${colors.reset}
772
919
  ${colors.gray}2.${colors.reset} npm install
@@ -779,6 +926,117 @@ ${colors.yellow}💡 Tip:${colors.reset} AI Agents can read ${
779
926
  `);
780
927
  }
781
928
 
929
+ /**
930
+ * ebade Output Verifier
931
+ * Performs a sanity check on the generated codebase.
932
+ */
933
+ async function verifyOutput(projectDir, config) {
934
+ log.section("Verifying output integrity");
935
+ const spinner = ora("Running ebade verification protocols...").start();
936
+
937
+ const results = {
938
+ structure: true,
939
+ syntax: true,
940
+ tests: true,
941
+ issues: [],
942
+ };
943
+
944
+ // 1. Structure Check
945
+ const requiredFiles = [
946
+ "package.json",
947
+ "tsconfig.json",
948
+ "app/layout.tsx",
949
+ "app/page.tsx",
950
+ "lib/utils.ts",
951
+ ];
952
+
953
+ for (const file of requiredFiles) {
954
+ if (!fs.existsSync(path.join(projectDir, file))) {
955
+ results.structure = false;
956
+ results.issues.push(`Missing core file: ${file}`);
957
+ }
958
+ }
959
+
960
+ // 2. Syntax Check (Lightweight)
961
+ // We check if exports match the intent in some key files
962
+ if (config.pages) {
963
+ config.pages.forEach((page) => {
964
+ const pagePath =
965
+ page.path === "/"
966
+ ? "app/page.tsx"
967
+ : `app${page.path.replace("[", "(").replace("]", ")")}/page.tsx`;
968
+ const fullPath = path.join(projectDir, pagePath);
969
+ if (fs.existsSync(fullPath)) {
970
+ const content = fs.readFileSync(fullPath, "utf-8");
971
+ const expectedExport = `export default function ${toPascalCase(
972
+ page.intent
973
+ )}Page()`;
974
+ if (!content.includes(expectedExport)) {
975
+ results.syntax = false;
976
+ results.issues.push(
977
+ `Syntax mismatch in ${pagePath}: Export name should be ${toPascalCase(
978
+ page.intent
979
+ )}Page`
980
+ );
981
+ }
982
+ }
983
+ });
984
+ }
985
+
986
+ // 3. Test Coverage Check
987
+ // Ensure every component has a matching test file
988
+ const components = fs
989
+ .readdirSync(path.join(projectDir, "components"))
990
+ .filter((f) => f.endsWith(".tsx") && !f.endsWith(".test.tsx"));
991
+ components.forEach((comp) => {
992
+ const testFile = comp.replace(".tsx", ".test.tsx");
993
+ if (!fs.existsSync(path.join(projectDir, "components", testFile))) {
994
+ results.tests = false;
995
+ results.issues.push(`Missing test for component: ${comp}`);
996
+ }
997
+ });
998
+
999
+ // 4. Semantic Integrity Check
1000
+ // Check if layout imports globals.css
1001
+ const layoutPath = path.join(projectDir, "app/layout.tsx");
1002
+ if (fs.existsSync(layoutPath)) {
1003
+ const content = fs.readFileSync(layoutPath, "utf-8");
1004
+ if (!content.includes('import "./globals.css"')) {
1005
+ results.issues.push(
1006
+ "Semantic Error: Root layout missing global styles import"
1007
+ );
1008
+ }
1009
+ }
1010
+
1011
+ // Check if home page has at least one intent-defined component
1012
+ const homePath = path.join(projectDir, "app/page.tsx");
1013
+ if (fs.existsSync(homePath) && config.pages?.[0]?.components?.length > 0) {
1014
+ const content = fs.readFileSync(homePath, "utf-8");
1015
+ const firstComp = toPascalCase(config.pages[0].components[0]);
1016
+ if (!content.includes(`<${firstComp} />`)) {
1017
+ results.issues.push(
1018
+ `Semantic Warning: Home page might be missing the ${firstComp} component`
1019
+ );
1020
+ }
1021
+ }
1022
+
1023
+ spinner.stop();
1024
+
1025
+ const passed = results.issues.length === 0;
1026
+
1027
+ let report = "";
1028
+ if (passed) {
1029
+ report = `${colors.green} ✓ All integrity checks passed! Code is production-ready.${colors.reset}`;
1030
+ } else {
1031
+ report = `${colors.red} ⚠ Verification found ${results.issues.length} issue(s):${colors.reset}\n`;
1032
+ results.issues.forEach((issue) => {
1033
+ report += ` ${colors.red}•${colors.reset} ${issue}\n`;
1034
+ });
1035
+ }
1036
+
1037
+ return { passed, report };
1038
+ }
1039
+
782
1040
  // ============================================
783
1041
  // CLI Entry Point
784
1042
  // ============================================
@@ -796,6 +1054,7 @@ ${colors.dim}Commands:${colors.reset}
796
1054
  init Create a new ebade project interactively
797
1055
  scaffold <file> [output] Scaffold a project from ebade file
798
1056
  dev <file> [output] Watch ebade file and re-scaffold on changes
1057
+ playground Open the ebade playground
799
1058
 
800
1059
  ${colors.dim}Examples:${colors.reset}
801
1060
  npx ebade init
@@ -915,7 +1174,7 @@ ${colors.green}✓${colors.reset} Created ${colors.cyan}${ebadeFilePath}${colors
915
1174
  `);
916
1175
 
917
1176
  if (response.autoScaffold) {
918
- scaffold(ebadeFilePath, outputDir);
1177
+ await scaffold(ebadeFilePath, outputDir);
919
1178
  } else {
920
1179
  console.log(`
921
1180
  ${colors.dim}Next steps:${colors.reset}
@@ -928,7 +1187,7 @@ ${colors.dim}Next steps:${colors.reset}
928
1187
  // ============================================
929
1188
  // Dev Command (Watch Mode)
930
1189
  // ============================================
931
- function dev(ebadeFile, outputDir) {
1190
+ async function dev(ebadeFile, outputDir) {
932
1191
  console.log(`
933
1192
  ${LOGO}
934
1193
  `);
@@ -939,7 +1198,7 @@ ${LOGO}
939
1198
  console.log(`${colors.dim}Press Ctrl+C to stop.${colors.reset}\n`);
940
1199
 
941
1200
  // Initial scaffold
942
- scaffold(ebadeFile, outputDir);
1201
+ await scaffold(ebadeFile, outputDir);
943
1202
 
944
1203
  // Watch for changes
945
1204
  const watcher = chokidar.watch(ebadeFile, {
@@ -947,11 +1206,11 @@ ${LOGO}
947
1206
  ignoreInitial: true,
948
1207
  });
949
1208
 
950
- watcher.on("change", () => {
1209
+ watcher.on("change", async () => {
951
1210
  console.log(
952
1211
  `\n${colors.yellow}⚡ Change detected!${colors.reset} Re-scaffolding...\n`
953
1212
  );
954
- scaffold(ebadeFile, outputDir);
1213
+ await scaffold(ebadeFile, outputDir);
955
1214
  });
956
1215
 
957
1216
  watcher.on("error", (error) => {
@@ -995,7 +1254,21 @@ if (command === "init") {
995
1254
  process.exit(1);
996
1255
  }
997
1256
 
998
- scaffold(ebadeFile, outputDir);
1257
+ await scaffold(ebadeFile, outputDir);
1258
+ } else if (command === "playground") {
1259
+ console.log(`\n${colors.cyan}🌐 Opening ebade playground...${colors.reset}`);
1260
+ const url = "https://ebade.dev/playground";
1261
+ const start =
1262
+ process.platform === "darwin"
1263
+ ? "open"
1264
+ : process.platform === "win32"
1265
+ ? "start"
1266
+ : "xdg-open";
1267
+ try {
1268
+ execSync(`${start} ${url}`);
1269
+ } catch (e) {
1270
+ console.log(`\n${colors.yellow}Please open:${colors.reset} ${url}`);
1271
+ }
999
1272
  } else if (command === "dev") {
1000
1273
  const ebadeFile = args[1];
1001
1274
  const outputDir = args[2] || "./output";
@@ -1017,7 +1290,7 @@ if (command === "init") {
1017
1290
  process.exit(1);
1018
1291
  }
1019
1292
 
1020
- dev(ebadeFile, outputDir);
1293
+ await dev(ebadeFile, outputDir);
1021
1294
  } else {
1022
1295
  console.error(
1023
1296
  `${colors.red}Error:${colors.reset} Unknown command: ${command}`