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.
- package/ARCHITECTURE.md +109 -0
- package/CHANGELOG.md +8 -1
- package/README.md +64 -202
- package/ROADMAP.md +17 -12
- package/cli/scaffold.js +465 -192
- package/cli/simulate.js +102 -0
- package/cli/templates/feature-grid.tsx +80 -0
- package/cli/templates/footer.tsx +121 -0
- package/cli/templates/hero-section.tsx +34 -0
- package/cli/templates/login-form.tsx +124 -0
- package/cli/templates/navbar.tsx +53 -0
- package/cli/templates/pricing-table.tsx +140 -0
- package/cli/templates/signup-form.tsx +111 -0
- package/demo.tape +2 -2
- package/examples/saas-dashboard.ebade.yaml +2 -0
- package/netlify.toml +7 -0
- package/package.json +1 -1
- package/packages/mcp-server/README.md +3 -3
- package/packages/mcp-server/package.json +2 -2
- package/packages/mcp-server/src/index.ts +12 -16
- package/packages/mcp-server/src/tools/scaffold.ts +153 -404
- package/packages/vscode-extension/README.md +11 -8
- package/packages/vscode-extension/ebade-0.3.0.vsix +0 -0
- package/packages/vscode-extension/ebade-0.3.1.vsix +0 -0
- package/packages/vscode-extension/ebade-0.3.2.vsix +0 -0
- package/packages/vscode-extension/images/icon.png +0 -0
- package/packages/vscode-extension/package.json +2 -1
- package/packages/vscode-extension/snippets/ebade.json +86 -0
- package/www/README.md +36 -0
- package/www/app/favicon.ico +0 -0
- package/{landing/style.css → www/app/globals.css} +691 -57
- package/www/app/layout.tsx +66 -0
- package/www/app/page.tsx +406 -0
- package/www/app/playground/page.tsx +610 -0
- package/www/components/Navbar.tsx +67 -0
- package/www/components/ThreeCanvas.tsx +156 -0
- package/www/next.config.ts +19 -0
- package/www/package-lock.json +1779 -0
- package/www/package.json +27 -0
- package/www/postcss.config.mjs +7 -0
- package/www/public/assets/demo.mp4 +0 -0
- package/www/public/logo.png +0 -0
- package/www/tsconfig.json +42 -0
- package/landing/index.html +0 -268
- package/landing/main.js +0 -147
- package/packages/vscode-extension/images/icon.svg +0 -6
- /package/{demo.gif → assets/demo.gif} +0 -0
- /package/{demo.mp4 → assets/demo.mp4} +0 -0
- /package/{landing → www/public}/_headers +0 -0
- /package/{landing → www/public}/favicon.svg +0 -0
- /package/{landing → www/public}/og-image.png +0 -0
- /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
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
98
|
-
|
|
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="
|
|
102
|
-
<div className="
|
|
103
|
-
<
|
|
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
|
-
<
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
|
131
|
-
|
|
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
|
-
<
|
|
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
|
-
|
|
186
|
+
</main>
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
150
189
|
);
|
|
151
190
|
}
|
|
152
191
|
|
|
153
|
-
// Auth
|
|
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: "
|
|
395
|
+
moduleResolution: "node",
|
|
261
396
|
resolveJsonModule: true,
|
|
262
397
|
isolatedModules: true,
|
|
263
|
-
jsx: "
|
|
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
|
|
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
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
.
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
.
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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
|
-
|
|
422
|
-
|
|
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
|
-
|
|
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
|
|
622
|
-
|
|
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(
|
|
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}`
|