ebade 0.3.0 → 0.4.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.
Files changed (49) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/README.md +30 -30
  3. package/ROADMAP.md +17 -12
  4. package/cli/scaffold.js +315 -183
  5. package/cli/simulate.js +102 -0
  6. package/cli/templates/feature-grid.tsx +80 -0
  7. package/cli/templates/footer.tsx +121 -0
  8. package/cli/templates/hero-section.tsx +34 -0
  9. package/cli/templates/login-form.tsx +124 -0
  10. package/cli/templates/navbar.tsx +53 -0
  11. package/cli/templates/pricing-table.tsx +140 -0
  12. package/cli/templates/signup-form.tsx +111 -0
  13. package/demo.tape +2 -2
  14. package/examples/saas-dashboard.ebade.yaml +2 -0
  15. package/netlify.toml +7 -0
  16. package/package.json +1 -1
  17. package/packages/mcp-server/README.md +3 -3
  18. package/packages/mcp-server/package.json +2 -2
  19. package/packages/mcp-server/src/index.ts +12 -16
  20. package/packages/mcp-server/src/tools/scaffold.ts +153 -404
  21. package/packages/vscode-extension/README.md +11 -8
  22. package/packages/vscode-extension/ebade-0.3.0.vsix +0 -0
  23. package/packages/vscode-extension/ebade-0.3.1.vsix +0 -0
  24. package/packages/vscode-extension/ebade-0.3.2.vsix +0 -0
  25. package/packages/vscode-extension/images/icon.png +0 -0
  26. package/packages/vscode-extension/package.json +2 -1
  27. package/packages/vscode-extension/snippets/ebade.json +86 -0
  28. package/www/README.md +36 -0
  29. package/www/app/favicon.ico +0 -0
  30. package/{landing/style.css → www/app/globals.css} +390 -19
  31. package/www/app/layout.tsx +66 -0
  32. package/www/app/page.tsx +374 -0
  33. package/www/app/playground/page.tsx +627 -0
  34. package/www/components/ThreeCanvas.tsx +156 -0
  35. package/www/next.config.ts +19 -0
  36. package/www/package-lock.json +1779 -0
  37. package/www/package.json +27 -0
  38. package/www/postcss.config.mjs +7 -0
  39. package/www/public/logo.png +0 -0
  40. package/www/tsconfig.json +42 -0
  41. package/landing/index.html +0 -268
  42. package/landing/main.js +0 -147
  43. package/packages/vscode-extension/images/icon.svg +0 -6
  44. /package/{demo.gif → assets/demo.gif} +0 -0
  45. /package/{demo.mp4 → assets/demo.mp4} +0 -0
  46. /package/{landing → www/public}/_headers +0 -0
  47. /package/{landing → www/public}/favicon.svg +0 -0
  48. /package/{landing → www/public}/og-image.png +0 -0
  49. /package/{landing → www/public}/og-readme.png +0 -0
package/cli/scaffold.js CHANGED
@@ -62,60 +62,86 @@ function parseEbade(ebadePath) {
62
62
  // ============================================
63
63
  // Component Generator Templates
64
64
  // ============================================
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>
65
+ // ============================================
66
+ // Template Resolver
67
+ // ============================================
68
+ function getComponentTemplate(componentName, design) {
69
+ const templatePath = path.join(
70
+ process.cwd(),
71
+ "cli/templates",
72
+ `${componentName}.tsx`
91
73
  );
92
- }
93
- `,
94
- "add-to-cart": (design) => `
95
- import { useState } from 'react';
96
74
 
97
- export function AddToCart({ product, onAdd }) {
98
- const [quantity, setQuantity] = useState(1);
99
-
75
+ if (fs.existsSync(templatePath)) {
76
+ let content = fs.readFileSync(templatePath, "utf-8");
77
+
78
+ // Config-based replacement (e.g., {{primary}})
79
+ const primaryColor = design?.colors?.primary || "#6366f1";
80
+ content = content.replace(/\{\{primary\}\}/g, primaryColor);
81
+
82
+ return content;
83
+ }
84
+
85
+ // Fallback to placeholder if template file doesn't exist
86
+ return `import React from 'react';
87
+ import { cn } from "@/lib/utils";
88
+
89
+ /**
90
+ * 🧠 Generated via ebade
91
+ * Component: ${toPascalCase(componentName)}
92
+ * Status: Placeholder (No template found in cli/templates)
93
+ */
94
+ export function ${toPascalCase(componentName)}() {
100
95
  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>
96
+ <div className="p-12 border-2 border-dashed border-border rounded-3xl text-center bg-muted/30">
97
+ <div className="w-16 h-16 bg-primary/10 rounded-2xl flex items-center justify-center mx-auto mb-4">
98
+ <span className="text-2xl">🧩</span>
106
99
  </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>
100
+ <h3 className="text-xl font-bold mb-2">${toPascalCase(componentName)}</h3>
101
+ <p className="text-sm text-muted-foreground max-w-xs mx-auto">
102
+ No template found for this intent. Create a file at <code>cli/templates/${componentName}.tsx</code> to customize.
103
+ </p>
114
104
  </div>
115
105
  );
116
106
  }
117
- `,
118
- };
107
+ `;
108
+ }
109
+
110
+ function generateComponentTest(componentName) {
111
+ const name = toPascalCase(componentName);
112
+ return `import { describe, it, expect } from 'vitest';
113
+ import { render } from '@testing-library/react';
114
+ import { ${name} } from './${componentName}';
115
+ import React from 'react';
116
+
117
+ describe('${name} Component', () => {
118
+ it('renders without crashing', () => {
119
+ render(<${name} />);
120
+ expect(document.body).toBeDefined();
121
+ });
122
+ });
123
+ `;
124
+ }
125
+
126
+ function generateVitestConfig() {
127
+ return `import { defineConfig } from 'vitest/config';
128
+ import react from '@vitejs/plugin-react';
129
+ import path from 'path';
130
+
131
+ export default defineConfig({
132
+ plugins: [react()],
133
+ test: {
134
+ environment: 'jsdom',
135
+ globals: true,
136
+ },
137
+ resolve: {
138
+ alias: {
139
+ '@': path.resolve(__dirname, './'),
140
+ },
141
+ },
142
+ });
143
+ `;
144
+ }
119
145
 
120
146
  // ============================================
121
147
  // Page Generator
@@ -127,10 +153,13 @@ function generatePage(page, design) {
127
153
  .join("\n") || "";
128
154
 
129
155
  const componentUsage =
130
- page.components?.map((c) => ` <${toPascalCase(c)} />`).join("\n") ||
131
- " {/* No components defined */}";
156
+ page.components
157
+ ?.map((c) => ` <${toPascalCase(c)} />`)
158
+ .join("\n") || " {/* No components defined */}";
159
+
160
+ return `import React from 'react';
161
+ ${componentImports}
132
162
 
133
- return `
134
163
  /**
135
164
  * 🧠 Generated via ebade - The Agent-First Framework
136
165
  * https://github.com/hasankemaldemirci/ebade
@@ -138,19 +167,26 @@ function generatePage(page, design) {
138
167
  * @page('${page.path}')
139
168
  * @intent('${page.intent}')
140
169
  */
141
-
142
- ${componentImports}
143
-
144
-
145
170
  export default function ${toPascalCase(page.intent)}Page() {
146
171
  return (
147
- <main className="page ${page.intent}">
172
+ <div className="min-h-screen bg-slate-950 text-white">
173
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-10">
174
+ <header className="mb-12">
175
+ <h1 className="text-3xl font-bold tracking-tight opacity-90">${toPascalCase(
176
+ page.intent
177
+ )}</h1>
178
+ <p className="text-sm opacity-40 mt-1">Route: ${page.path}</p>
179
+ </header>
180
+
181
+ <main className="space-y-12">
148
182
  ${componentUsage}
149
- </main>
183
+ </main>
184
+ </div>
185
+ </div>
150
186
  );
151
187
  }
152
188
 
153
- // Auth requirement: ${page.auth || "public"}
189
+ // Auth: ${page.auth || "public"}
154
190
  `;
155
191
  }
156
192
 
@@ -221,16 +257,30 @@ function generatePackageJson(config) {
221
257
  build: "next build",
222
258
  start: "next start",
223
259
  lint: "next lint",
260
+ test: "vitest",
224
261
  },
225
262
  dependencies: {
226
263
  next: "^14.0.0",
227
264
  react: "^18.2.0",
228
265
  "react-dom": "^18.2.0",
266
+ "lucide-react": "^0.300.0",
267
+ clsx: "^2.1.0",
268
+ "tailwind-merge": "^2.2.0",
269
+ "class-variance-authority": "^0.7.0",
270
+ "framer-motion": "^11.0.0",
229
271
  },
230
272
  devDependencies: {
231
273
  "@types/node": "^20.0.0",
232
274
  "@types/react": "^18.2.0",
233
275
  "@types/react-dom": "^18.2.0",
276
+ "@testing-library/react": "^14.1.2",
277
+ "@vitejs/plugin-react": "^4.2.0",
278
+ jsdom: "^22.1.0",
279
+ vitest: "^0.34.6",
280
+ autoprefixer: "^10.0.1",
281
+ postcss: "^8.4.0",
282
+ tailwindcss: "^3.4.0",
283
+ "tailwindcss-animate": "^1.0.7",
234
284
  typescript: "^5.0.0",
235
285
  },
236
286
  },
@@ -239,6 +289,87 @@ function generatePackageJson(config) {
239
289
  );
240
290
  }
241
291
 
292
+ function generateTailwindConfig() {
293
+ return `/** @type {import('tailwindcss').Config} */
294
+ module.exports = {
295
+ darkMode: ["class"],
296
+ content: [
297
+ './pages/**/*.{ts,tsx}',
298
+ './components/**/*.{ts,tsx}',
299
+ './app/**/*.{ts,tsx}',
300
+ './src/**/*.{ts,tsx}',
301
+ ],
302
+ prefix: "",
303
+ theme: {
304
+ container: {
305
+ center: true,
306
+ padding: "2rem",
307
+ screens: {
308
+ "2xl": "1400px",
309
+ },
310
+ },
311
+ extend: {
312
+ colors: {
313
+ border: "hsl(var(--border))",
314
+ input: "hsl(var(--input))",
315
+ ring: "hsl(var(--ring))",
316
+ background: "hsl(var(--background))",
317
+ foreground: "hsl(var(--foreground))",
318
+ primary: {
319
+ DEFAULT: "hsl(var(--primary))",
320
+ foreground: "hsl(var(--primary-foreground))",
321
+ },
322
+ secondary: {
323
+ DEFAULT: "hsl(var(--secondary))",
324
+ foreground: "hsl(var(--secondary-foreground))",
325
+ },
326
+ destructive: {
327
+ DEFAULT: "hsl(var(--destructive))",
328
+ foreground: "hsl(var(--destructive-foreground))",
329
+ },
330
+ muted: {
331
+ DEFAULT: "hsl(var(--muted))",
332
+ foreground: "hsl(var(--muted-foreground))",
333
+ },
334
+ accent: {
335
+ DEFAULT: "hsl(var(--accent))",
336
+ foreground: "hsl(var(--accent-foreground))",
337
+ },
338
+ popover: {
339
+ DEFAULT: "hsl(var(--popover))",
340
+ foreground: "hsl(var(--popover-foreground))",
341
+ },
342
+ card: {
343
+ DEFAULT: "hsl(var(--card))",
344
+ foreground: "hsl(var(--card-foreground))",
345
+ },
346
+ },
347
+ borderRadius: {
348
+ lg: "var(--radius)",
349
+ md: "calc(var(--radius) - 2px)",
350
+ sm: "calc(var(--radius) - 4px)",
351
+ },
352
+ keyframes: {
353
+ "accordion-down": {
354
+ from: { height: "0" },
355
+ to: { height: "var(--radix-accordion-content-height)" },
356
+ },
357
+ "accordion-up": {
358
+ from: { height: "var(--radix-accordion-content-height)" },
359
+ to: { height: "0" },
360
+ },
361
+ },
362
+ animation: {
363
+ "accordion-down": "accordion-down 0.2s ease-out",
364
+ "accordion-up": "accordion-up 0.2s ease-out",
365
+ },
366
+ },
367
+ },
368
+ plugins: [require("tailwindcss-animate")],
369
+ }
370
+ `;
371
+ }
372
+
242
373
  function generateNextConfig() {
243
374
  return `/** @type {import('next').NextConfig} */
244
375
  const nextConfig = {};
@@ -250,6 +381,7 @@ function generateTsConfig() {
250
381
  return JSON.stringify(
251
382
  {
252
383
  compilerOptions: {
384
+ target: "es5",
253
385
  lib: ["dom", "dom.iterable", "esnext"],
254
386
  allowJs: true,
255
387
  skipLibCheck: true,
@@ -257,10 +389,10 @@ function generateTsConfig() {
257
389
  noEmit: true,
258
390
  esModuleInterop: true,
259
391
  module: "esnext",
260
- moduleResolution: "bundler",
392
+ moduleResolution: "node",
261
393
  resolveJsonModule: true,
262
394
  isolatedModules: true,
263
- jsx: "preserve",
395
+ jsx: "react-jsx",
264
396
  incremental: true,
265
397
  plugins: [{ name: "next" }],
266
398
  paths: { "@/*": ["./*"] },
@@ -275,7 +407,8 @@ function generateTsConfig() {
275
407
 
276
408
  function generateLayout(config) {
277
409
  const fontFamily = config.design?.font || "Inter";
278
- return `import type { Metadata } from "next";
410
+ return `import React from 'react';
411
+ import type { Metadata } from "next";
279
412
  import "./globals.css";
280
413
 
281
414
  export const metadata: Metadata = {
@@ -307,119 +440,95 @@ export default function RootLayout({
307
440
  }
308
441
 
309
442
  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;
443
+ const primary = design.colors?.primary || "#6366f1";
444
+
445
+ // Helper to convert hex to HSL for CSS vars if needed
446
+ // For now we'll use standard shadcn slate
447
+ return `@tailwind base;
448
+ @tailwind components;
449
+ @tailwind utilities;
450
+
451
+ @layer base {
452
+ :root {
453
+ --background: 0 0% 100%;
454
+ --foreground: 222.2 84% 4.9%;
455
+
456
+ --card: 0 0% 100%;
457
+ --card-foreground: 222.2 84% 4.9%;
458
+
459
+ --popover: 0 0% 100%;
460
+ --popover-foreground: 222.2 84% 4.9%;
461
+
462
+ --primary: 221.2 83.2% 53.3%;
463
+ --primary-foreground: 210 40% 98%;
464
+
465
+ --secondary: 210 40% 96.1%;
466
+ --secondary-foreground: 222.2 47.4% 11.2%;
467
+
468
+ --muted: 210 40% 96.1%;
469
+ --muted-foreground: 215.4 16.3% 46.9%;
470
+
471
+ --accent: 210 40% 96.1%;
472
+ --accent-foreground: 222.2 47.4% 11.2%;
473
+
474
+ --destructive: 0 84.2% 60.2%;
475
+ --destructive-foreground: 210 40% 98%;
476
+
477
+ --border: 214.3 31.8% 91.4%;
478
+ --input: 214.3 31.8% 91.4%;
479
+ --ring: 221.2 83.2% 53.3%;
480
+
481
+ --radius: 0.5rem;
482
+ }
483
+
484
+ .dark {
485
+ --background: 222.2 84% 4.9%;
486
+ --foreground: 210 40% 98%;
487
+
488
+ --card: 222.2 84% 4.9%;
489
+ --card-foreground: 210 40% 98%;
490
+
491
+ --popover: 222.2 84% 4.9%;
492
+ --popover-foreground: 210 40% 98%;
493
+
494
+ --primary: 217.2 91.2% 59.8%;
495
+ --primary-foreground: 222.2 47.4% 11.2%;
496
+
497
+ --secondary: 217.2 32.6% 17.5%;
498
+ --secondary-foreground: 210 40% 98%;
499
+
500
+ --muted: 217.2 32.6% 17.5%;
501
+ --muted-foreground: 215 20.2% 65.1%;
502
+
503
+ --accent: 217.2 32.6% 17.5%;
504
+ --accent-foreground: 210 40% 98%;
505
+
506
+ --destructive: 0 62.8% 30.6%;
507
+ --destructive-foreground: 210 40% 98%;
508
+
509
+ --border: 217.2 32.6% 17.5%;
510
+ --input: 217.2 32.6% 17.5%;
511
+ --ring: 224.3 76.3% 48%;
512
+ }
398
513
  }
399
-
400
- .add-to-cart {
401
- display: flex;
402
- gap: 1rem;
403
- align-items: center;
514
+
515
+ @layer base {
516
+ * {
517
+ @apply border-border;
518
+ }
519
+ body {
520
+ @apply bg-background text-foreground;
521
+ }
404
522
  }
405
-
406
- .quantity-selector {
407
- display: flex;
408
- align-items: center;
409
- gap: 0.5rem;
523
+ `;
410
524
  }
411
525
 
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
- }
526
+ function generateUtils() {
527
+ return `import { type ClassValue, clsx } from "clsx"
528
+ import { twMerge } from "tailwind-merge"
420
529
 
421
- .add-btn {
422
- flex: 1;
530
+ export function cn(...inputs: ClassValue[]) {
531
+ return twMerge(clsx(inputs))
423
532
  }
424
533
  `;
425
534
  }
@@ -550,7 +659,13 @@ function ensureDir(dir) {
550
659
  // ============================================
551
660
  function scaffold(ebadePath, outputDir) {
552
661
  const startTime = Date.now();
553
- let stats = { pages: 0, components: 0, apiRoutes: 0, files: 0 };
662
+ const stats = {
663
+ pages: 0,
664
+ components: 0,
665
+ apiRoutes: 0,
666
+ files: 0,
667
+ tokenSavings: 0,
668
+ };
554
669
 
555
670
  console.log(LOGO);
556
671
 
@@ -583,6 +698,10 @@ function scaffold(ebadePath, outputDir) {
583
698
  log.file(`${dir}/`);
584
699
  });
585
700
 
701
+ // lib/utils.ts
702
+ fs.writeFileSync(path.join(projectDir, "lib/utils.ts"), generateUtils());
703
+ log.file("lib/utils.ts");
704
+
586
705
  // ========== Generate Pages ==========
587
706
  log.section("Generating pages");
588
707
 
@@ -618,21 +737,23 @@ function scaffold(ebadePath, outputDir) {
618
737
  const spinner2 = ora("Generating components...").start();
619
738
  allComponents.forEach((component) => {
620
739
  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`;
740
+ const content = getComponentTemplate(component, config.design);
741
+ stats.tokenSavings += Math.floor(content.length / 4);
629
742
 
630
743
  fs.writeFileSync(path.join(projectDir, componentPath), content.trim());
744
+
745
+ // Generate unit test
746
+ const testPath = `components/${component}.test.tsx`;
747
+ fs.writeFileSync(
748
+ path.join(projectDir, testPath),
749
+ generateComponentTest(component).trim()
750
+ );
751
+
631
752
  stats.components++;
632
- stats.files++;
753
+ stats.files += 2; // Component + Test
633
754
  });
634
755
  spinner2.succeed(
635
- `Generated ${colors.bright}${stats.components}${colors.reset} components`
756
+ `Generated ${colors.bright}${stats.components}${colors.reset} components (+ tests)`
636
757
  );
637
758
 
638
759
  // ========== Generate API Routes ==========
@@ -683,6 +804,20 @@ function scaffold(ebadePath, outputDir) {
683
804
  fs.writeFileSync(path.join(projectDir, "tsconfig.json"), generateTsConfig());
684
805
  log.file("tsconfig.json");
685
806
 
807
+ // tailwind.config.js
808
+ fs.writeFileSync(
809
+ path.join(projectDir, "tailwind.config.js"),
810
+ generateTailwindConfig()
811
+ );
812
+ log.file("tailwind.config.js");
813
+
814
+ // vitest.config.ts
815
+ fs.writeFileSync(
816
+ path.join(projectDir, "vitest.config.ts"),
817
+ generateVitestConfig()
818
+ );
819
+ log.file("vitest.config.ts");
820
+
686
821
  // app/layout.tsx
687
822
  fs.writeFileSync(
688
823
  path.join(projectDir, "app/layout.tsx"),
@@ -739,7 +874,6 @@ function scaffold(ebadePath, outputDir) {
739
874
 
740
875
  // ========== Summary ==========
741
876
  const duration = ((Date.now() - startTime) / 1000).toFixed(1);
742
- const estimatedTokenSavings = Math.round(stats.files * 35); // ~35 tokens saved per file
743
877
 
744
878
  console.log(`
745
879
  ${colors.bright}${colors.green} ┌${"─".repeat(41)}┐${colors.reset}
@@ -759,9 +893,7 @@ ${colors.green} │${colors.reset} ${colors.cyan}📁 Files Created:${
759
893
  } ${String(stats.files).padEnd(18)} ${colors.green}│${colors.reset}
760
894
  ${colors.green} │${colors.reset} ${colors.cyan}📊 Token Savings:${
761
895
  colors.reset
762
- } ~${String(estimatedTokenSavings).padEnd(17)} ${colors.green}│${
763
- colors.reset
764
- }
896
+ } ~${String(stats.tokenSavings).padEnd(17)} ${colors.green}│${colors.reset}
765
897
  ${colors.green} │${colors.reset} ${colors.cyan}⏱ Completed in:${
766
898
  colors.reset
767
899
  } ${String(duration + "s").padEnd(18)} ${colors.green}│${colors.reset}