ebade 0.1.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.
- package/CHANGELOG.md +36 -0
- package/CONTRIBUTING.md +177 -0
- package/LICENSE +21 -0
- package/MANIFESTO.md +170 -0
- package/README.md +263 -0
- package/ROADMAP.md +119 -0
- package/SYNTAX.md +515 -0
- package/benchmarks/RESULTS.md +119 -0
- package/benchmarks/token-benchmark.js +197 -0
- package/cli/scaffold.js +706 -0
- package/docs/GREEN-AI.md +86 -0
- package/examples/ecommerce.ebade.yaml +192 -0
- package/landing/favicon.svg +6 -0
- package/landing/index.html +227 -0
- package/landing/main.js +147 -0
- package/landing/og-image.png +0 -0
- package/landing/style.css +616 -0
- package/package.json +43 -0
- package/packages/mcp-server/README.md +144 -0
- package/packages/mcp-server/package-lock.json +1178 -0
- package/packages/mcp-server/package.json +32 -0
- package/packages/mcp-server/src/index.ts +316 -0
- package/packages/mcp-server/src/tools/compile.ts +269 -0
- package/packages/mcp-server/src/tools/generate.ts +420 -0
- package/packages/mcp-server/src/tools/scaffold.ts +474 -0
- package/packages/mcp-server/src/tools/validate.ts +233 -0
- package/packages/mcp-server/tsconfig.json +16 -0
- package/schema/project.schema.json +195 -0
package/cli/scaffold.js
ADDED
|
@@ -0,0 +1,706 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ebade CLI Scaffold Tool
|
|
5
|
+
*
|
|
6
|
+
* Bu araç bir .ebade.yaml dosyasını okur ve
|
|
7
|
+
* AI Agent'ın anlayacağı şekilde proje yapısını oluşturur.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import fs from "fs";
|
|
11
|
+
import path from "path";
|
|
12
|
+
import yaml from "yaml";
|
|
13
|
+
|
|
14
|
+
// ============================================
|
|
15
|
+
// ANSI Renk Kodları (Terminal çıktısı için)
|
|
16
|
+
// ============================================
|
|
17
|
+
const colors = {
|
|
18
|
+
reset: "\x1b[0m",
|
|
19
|
+
bright: "\x1b[1m",
|
|
20
|
+
dim: "\x1b[2m",
|
|
21
|
+
green: "\x1b[32m",
|
|
22
|
+
blue: "\x1b[34m",
|
|
23
|
+
yellow: "\x1b[33m",
|
|
24
|
+
cyan: "\x1b[36m",
|
|
25
|
+
magenta: "\x1b[35m",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const log = {
|
|
29
|
+
info: (msg) => console.log(`${colors.blue}ℹ${colors.reset} ${msg}`),
|
|
30
|
+
success: (msg) => console.log(`${colors.green}✓${colors.reset} ${msg}`),
|
|
31
|
+
warn: (msg) => console.log(`${colors.yellow}⚠${colors.reset} ${msg}`),
|
|
32
|
+
file: (msg) => console.log(`${colors.cyan} →${colors.reset} ${msg}`),
|
|
33
|
+
section: (msg) =>
|
|
34
|
+
console.log(`\n${colors.bright}${colors.magenta}▸ ${msg}${colors.reset}`),
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// ============================================
|
|
38
|
+
// ebade Parser
|
|
39
|
+
// ============================================
|
|
40
|
+
function parseEbade(ebadePath) {
|
|
41
|
+
const content = fs.readFileSync(ebadePath, "utf-8");
|
|
42
|
+
return yaml.parse(content);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ============================================
|
|
46
|
+
// Component Generator Templates
|
|
47
|
+
// ============================================
|
|
48
|
+
const componentTemplates = {
|
|
49
|
+
"hero-section": (design) => `
|
|
50
|
+
export function HeroSection() {
|
|
51
|
+
return (
|
|
52
|
+
<section className="hero-section">
|
|
53
|
+
<div className="hero-content">
|
|
54
|
+
<h1 className="hero-title">Welcome to Our Store</h1>
|
|
55
|
+
<p className="hero-subtitle">Discover amazing products</p>
|
|
56
|
+
<button className="hero-cta" style={{ backgroundColor: '${
|
|
57
|
+
design.colors?.primary || "#6366f1"
|
|
58
|
+
}' }}>
|
|
59
|
+
Shop Now
|
|
60
|
+
</button>
|
|
61
|
+
</div>
|
|
62
|
+
</section>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
`,
|
|
66
|
+
"product-grid": (design) => `
|
|
67
|
+
export function ProductGrid({ products }) {
|
|
68
|
+
return (
|
|
69
|
+
<div className="product-grid">
|
|
70
|
+
{products.map((product) => (
|
|
71
|
+
<ProductCard key={product.id} product={product} />
|
|
72
|
+
))}
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
`,
|
|
77
|
+
"add-to-cart": (design) => `
|
|
78
|
+
import { useState } from 'react';
|
|
79
|
+
|
|
80
|
+
export function AddToCart({ product, onAdd }) {
|
|
81
|
+
const [quantity, setQuantity] = useState(1);
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<div className="add-to-cart">
|
|
85
|
+
<div className="quantity-selector">
|
|
86
|
+
<button onClick={() => setQuantity(q => Math.max(1, q - 1))}>-</button>
|
|
87
|
+
<span>{quantity}</span>
|
|
88
|
+
<button onClick={() => setQuantity(q => q + 1)}>+</button>
|
|
89
|
+
</div>
|
|
90
|
+
<button
|
|
91
|
+
className="add-btn"
|
|
92
|
+
style={{ backgroundColor: '${design.colors?.primary || "#6366f1"}' }}
|
|
93
|
+
onClick={() => onAdd(product, quantity)}
|
|
94
|
+
>
|
|
95
|
+
Add to Cart
|
|
96
|
+
</button>
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
`,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// ============================================
|
|
104
|
+
// Page Generator
|
|
105
|
+
// ============================================
|
|
106
|
+
function generatePage(page, design) {
|
|
107
|
+
const componentImports =
|
|
108
|
+
page.components
|
|
109
|
+
?.map((c) => `import { ${toPascalCase(c)} } from '@/components/${c}';`)
|
|
110
|
+
.join("\n") || "";
|
|
111
|
+
|
|
112
|
+
const componentUsage =
|
|
113
|
+
page.components?.map((c) => ` <${toPascalCase(c)} />`).join("\n") ||
|
|
114
|
+
" {/* No components defined */}";
|
|
115
|
+
|
|
116
|
+
return `
|
|
117
|
+
/**
|
|
118
|
+
* @page('${page.path}')
|
|
119
|
+
* @ebade('${page.intent}')
|
|
120
|
+
* Built with ebade
|
|
121
|
+
*/
|
|
122
|
+
|
|
123
|
+
${componentImports}
|
|
124
|
+
|
|
125
|
+
export default function ${toPascalCase(page.intent)}Page() {
|
|
126
|
+
return (
|
|
127
|
+
<main className="page ${page.intent}">
|
|
128
|
+
${componentUsage}
|
|
129
|
+
</main>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Auth requirement: ${page.auth || "public"}
|
|
134
|
+
`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ============================================
|
|
138
|
+
// API Route Generator
|
|
139
|
+
// ============================================
|
|
140
|
+
function generateApiRoute(endpoint) {
|
|
141
|
+
const handlers = endpoint.methods
|
|
142
|
+
.map(
|
|
143
|
+
(method) => `
|
|
144
|
+
export async function ${method}(request) {
|
|
145
|
+
// TODO: Implement ${method} handler for ${endpoint.path}
|
|
146
|
+
// Auth: ${endpoint.auth || "none"}
|
|
147
|
+
// Built with ebade
|
|
148
|
+
|
|
149
|
+
return Response.json({
|
|
150
|
+
message: "${method} ${endpoint.path} - Not implemented"
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
`
|
|
154
|
+
)
|
|
155
|
+
.join("\n");
|
|
156
|
+
|
|
157
|
+
return handlers;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ============================================
|
|
161
|
+
// Database Schema Generator (Supabase SQL)
|
|
162
|
+
// ============================================
|
|
163
|
+
function generateDatabaseSchema(data) {
|
|
164
|
+
let sql = "-- ebade Generated Database Schema\n\n";
|
|
165
|
+
|
|
166
|
+
for (const [modelName, model] of Object.entries(data)) {
|
|
167
|
+
sql += `-- Table: ${modelName}\n`;
|
|
168
|
+
sql += `CREATE TABLE IF NOT EXISTS ${toSnakeCase(modelName)} (\n`;
|
|
169
|
+
|
|
170
|
+
const fields = Object.entries(model.fields).map(([fieldName, fieldDef]) => {
|
|
171
|
+
const sqlType = mapToSqlType(fieldDef.type);
|
|
172
|
+
const constraints = [];
|
|
173
|
+
if (fieldDef.required) constraints.push("NOT NULL");
|
|
174
|
+
if (fieldDef.unique) constraints.push("UNIQUE");
|
|
175
|
+
return ` ${toSnakeCase(fieldName)} ${sqlType}${
|
|
176
|
+
constraints.length ? " " + constraints.join(" ") : ""
|
|
177
|
+
}`;
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
sql += fields.join(",\n");
|
|
181
|
+
sql += "\n);\n\n";
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return sql;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ============================================
|
|
188
|
+
// Next.js Config Generators
|
|
189
|
+
// ============================================
|
|
190
|
+
function generatePackageJson(config) {
|
|
191
|
+
return JSON.stringify(
|
|
192
|
+
{
|
|
193
|
+
name: config.name,
|
|
194
|
+
version: "0.1.0",
|
|
195
|
+
private: true,
|
|
196
|
+
scripts: {
|
|
197
|
+
dev: "next dev",
|
|
198
|
+
build: "next build",
|
|
199
|
+
start: "next start",
|
|
200
|
+
lint: "next lint",
|
|
201
|
+
},
|
|
202
|
+
dependencies: {
|
|
203
|
+
next: "^14.0.0",
|
|
204
|
+
react: "^18.2.0",
|
|
205
|
+
"react-dom": "^18.2.0",
|
|
206
|
+
},
|
|
207
|
+
devDependencies: {
|
|
208
|
+
"@types/node": "^20.0.0",
|
|
209
|
+
"@types/react": "^18.2.0",
|
|
210
|
+
"@types/react-dom": "^18.2.0",
|
|
211
|
+
typescript: "^5.0.0",
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
null,
|
|
215
|
+
2
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function generateNextConfig() {
|
|
220
|
+
return `/** @type {import('next').NextConfig} */
|
|
221
|
+
const nextConfig = {};
|
|
222
|
+
export default nextConfig;
|
|
223
|
+
`;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function generateTsConfig() {
|
|
227
|
+
return JSON.stringify(
|
|
228
|
+
{
|
|
229
|
+
compilerOptions: {
|
|
230
|
+
lib: ["dom", "dom.iterable", "esnext"],
|
|
231
|
+
allowJs: true,
|
|
232
|
+
skipLibCheck: true,
|
|
233
|
+
strict: true,
|
|
234
|
+
noEmit: true,
|
|
235
|
+
esModuleInterop: true,
|
|
236
|
+
module: "esnext",
|
|
237
|
+
moduleResolution: "bundler",
|
|
238
|
+
resolveJsonModule: true,
|
|
239
|
+
isolatedModules: true,
|
|
240
|
+
jsx: "preserve",
|
|
241
|
+
incremental: true,
|
|
242
|
+
plugins: [{ name: "next" }],
|
|
243
|
+
paths: { "@/*": ["./*"] },
|
|
244
|
+
},
|
|
245
|
+
include: ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
246
|
+
exclude: ["node_modules"],
|
|
247
|
+
},
|
|
248
|
+
null,
|
|
249
|
+
2
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function generateLayout(config) {
|
|
254
|
+
const fontFamily = config.design?.font || "Inter";
|
|
255
|
+
return `import type { Metadata } from "next";
|
|
256
|
+
import "./globals.css";
|
|
257
|
+
|
|
258
|
+
export const metadata: Metadata = {
|
|
259
|
+
title: "${config.name}",
|
|
260
|
+
description: "Built with ebade - The Agent-First Framework",
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
export default function RootLayout({
|
|
264
|
+
children,
|
|
265
|
+
}: Readonly<{
|
|
266
|
+
children: React.ReactNode;
|
|
267
|
+
}>) {
|
|
268
|
+
return (
|
|
269
|
+
<html lang="en">
|
|
270
|
+
<head>
|
|
271
|
+
<link
|
|
272
|
+
href="https://fonts.googleapis.com/css2?family=${fontFamily.replace(
|
|
273
|
+
" ",
|
|
274
|
+
"+"
|
|
275
|
+
)}:wght@400;500;600;700;800&display=swap"
|
|
276
|
+
rel="stylesheet"
|
|
277
|
+
/>
|
|
278
|
+
</head>
|
|
279
|
+
<body>{children}</body>
|
|
280
|
+
</html>
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
`;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function generateGlobalsCss(design) {
|
|
287
|
+
return `/* ebade Generated Design System */
|
|
288
|
+
/* Style: ${design.style || "minimal-modern"} */
|
|
289
|
+
|
|
290
|
+
:root {
|
|
291
|
+
--color-primary: ${design.colors?.primary || "#6366f1"};
|
|
292
|
+
--color-secondary: ${design.colors?.secondary || "#f59e0b"};
|
|
293
|
+
--color-accent: ${design.colors?.accent || "#10b981"};
|
|
294
|
+
--font-family: '${design.font || "Inter"}', system-ui, sans-serif;
|
|
295
|
+
--radius-lg: 1rem;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
* {
|
|
299
|
+
box-sizing: border-box;
|
|
300
|
+
margin: 0;
|
|
301
|
+
padding: 0;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
body {
|
|
305
|
+
font-family: var(--font-family);
|
|
306
|
+
line-height: 1.6;
|
|
307
|
+
color: #1f2937;
|
|
308
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
309
|
+
min-height: 100vh;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
.page {
|
|
313
|
+
min-height: 100vh;
|
|
314
|
+
display: flex;
|
|
315
|
+
flex-direction: column;
|
|
316
|
+
align-items: center;
|
|
317
|
+
justify-content: center;
|
|
318
|
+
padding: 2rem;
|
|
319
|
+
color: white;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.hero-section {
|
|
323
|
+
text-align: center;
|
|
324
|
+
max-width: 800px;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
.hero-title {
|
|
328
|
+
font-size: 3.5rem;
|
|
329
|
+
font-weight: 800;
|
|
330
|
+
margin-bottom: 1rem;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
.hero-subtitle {
|
|
334
|
+
font-size: 1.25rem;
|
|
335
|
+
opacity: 0.9;
|
|
336
|
+
margin-bottom: 2rem;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
.hero-cta {
|
|
340
|
+
background: white;
|
|
341
|
+
color: var(--color-primary);
|
|
342
|
+
padding: 1rem 2rem;
|
|
343
|
+
font-size: 1.1rem;
|
|
344
|
+
border-radius: var(--radius-lg);
|
|
345
|
+
border: none;
|
|
346
|
+
cursor: pointer;
|
|
347
|
+
font-weight: 600;
|
|
348
|
+
transition: transform 0.2s, box-shadow 0.2s;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
.hero-cta:hover {
|
|
352
|
+
transform: scale(1.05);
|
|
353
|
+
box-shadow: 0 8px 24px rgba(0,0,0,0.2);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
.btn-primary {
|
|
357
|
+
background-color: var(--color-primary);
|
|
358
|
+
color: white;
|
|
359
|
+
padding: 0.75rem 1.5rem;
|
|
360
|
+
border-radius: var(--radius-lg);
|
|
361
|
+
border: none;
|
|
362
|
+
cursor: pointer;
|
|
363
|
+
transition: opacity 0.2s;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
.btn-primary:hover {
|
|
367
|
+
opacity: 0.9;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.product-grid {
|
|
371
|
+
display: grid;
|
|
372
|
+
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
373
|
+
gap: 1.5rem;
|
|
374
|
+
padding: 2rem;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
.add-to-cart {
|
|
378
|
+
display: flex;
|
|
379
|
+
gap: 1rem;
|
|
380
|
+
align-items: center;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
.quantity-selector {
|
|
384
|
+
display: flex;
|
|
385
|
+
align-items: center;
|
|
386
|
+
gap: 0.5rem;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
.quantity-selector button {
|
|
390
|
+
width: 32px;
|
|
391
|
+
height: 32px;
|
|
392
|
+
border: 1px solid #e5e7eb;
|
|
393
|
+
background: white;
|
|
394
|
+
border-radius: 4px;
|
|
395
|
+
cursor: pointer;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
.add-btn {
|
|
399
|
+
flex: 1;
|
|
400
|
+
}
|
|
401
|
+
`;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// ============================================
|
|
405
|
+
// Design System CSS Generator
|
|
406
|
+
// ============================================
|
|
407
|
+
function generateDesignSystem(design) {
|
|
408
|
+
return `
|
|
409
|
+
/* ebade Generated Design System */
|
|
410
|
+
/* Style: ${design.style || "minimal-modern"} */
|
|
411
|
+
|
|
412
|
+
:root {
|
|
413
|
+
--color-primary: ${design.colors?.primary || "#6366f1"};
|
|
414
|
+
--color-secondary: ${design.colors?.secondary || "#f59e0b"};
|
|
415
|
+
--color-accent: ${design.colors?.accent || "#10b981"};
|
|
416
|
+
|
|
417
|
+
--font-family: '${design.font || "Inter"}', system-ui, sans-serif;
|
|
418
|
+
|
|
419
|
+
--radius-sm: 0.25rem;
|
|
420
|
+
--radius-md: 0.5rem;
|
|
421
|
+
--radius-lg: 1rem;
|
|
422
|
+
--radius-full: 9999px;
|
|
423
|
+
--radius-default: var(--radius-${design.borderRadius || "md"});
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
* {
|
|
427
|
+
box-sizing: border-box;
|
|
428
|
+
margin: 0;
|
|
429
|
+
padding: 0;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
body {
|
|
433
|
+
font-family: var(--font-family);
|
|
434
|
+
line-height: 1.6;
|
|
435
|
+
color: #1f2937;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
.btn-primary {
|
|
439
|
+
background-color: var(--color-primary);
|
|
440
|
+
color: white;
|
|
441
|
+
padding: 0.75rem 1.5rem;
|
|
442
|
+
border-radius: var(--radius-default);
|
|
443
|
+
border: none;
|
|
444
|
+
cursor: pointer;
|
|
445
|
+
transition: opacity 0.2s;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
.btn-primary:hover {
|
|
449
|
+
opacity: 0.9;
|
|
450
|
+
}
|
|
451
|
+
`;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// ============================================
|
|
455
|
+
// Utility Functions
|
|
456
|
+
// ============================================
|
|
457
|
+
function toPascalCase(str) {
|
|
458
|
+
return str
|
|
459
|
+
.split("-")
|
|
460
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
461
|
+
.join("");
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function toSnakeCase(str) {
|
|
465
|
+
return str
|
|
466
|
+
.replace(/([A-Z])/g, "_$1")
|
|
467
|
+
.toLowerCase()
|
|
468
|
+
.replace(/^_/, "");
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function mapToSqlType(type) {
|
|
472
|
+
const typeMap = {
|
|
473
|
+
uuid: "UUID PRIMARY KEY DEFAULT gen_random_uuid()",
|
|
474
|
+
string: "VARCHAR(255)",
|
|
475
|
+
text: "TEXT",
|
|
476
|
+
integer: "INTEGER",
|
|
477
|
+
decimal: "DECIMAL(10,2)",
|
|
478
|
+
boolean: "BOOLEAN",
|
|
479
|
+
timestamp: "TIMESTAMP WITH TIME ZONE DEFAULT NOW()",
|
|
480
|
+
json: "JSONB",
|
|
481
|
+
array: "JSONB",
|
|
482
|
+
enum: "VARCHAR(50)",
|
|
483
|
+
};
|
|
484
|
+
return typeMap[type] || "VARCHAR(255)";
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function ensureDir(dir) {
|
|
488
|
+
if (!fs.existsSync(dir)) {
|
|
489
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// ============================================
|
|
494
|
+
// Main Scaffold Function
|
|
495
|
+
// ============================================
|
|
496
|
+
function scaffold(ebadePath, outputDir) {
|
|
497
|
+
console.log(`
|
|
498
|
+
${colors.bright}${colors.magenta}╔════════════════════════════════════════╗
|
|
499
|
+
命 ebade Scaffold CLI 命
|
|
500
|
+
命 🤖 Agent-First Framework Generator 命
|
|
501
|
+
╚════════════════════════════════════════╝${colors.reset}
|
|
502
|
+
`);
|
|
503
|
+
|
|
504
|
+
// Parse ebade file
|
|
505
|
+
log.info(`Reading ebade file: ${ebadePath}`);
|
|
506
|
+
const config = parseEbade(ebadePath);
|
|
507
|
+
log.success(
|
|
508
|
+
`Parsed ebade for project: ${colors.bright}${config.name}${colors.reset}`
|
|
509
|
+
);
|
|
510
|
+
|
|
511
|
+
// Create output directory structure
|
|
512
|
+
const projectDir = path.join(outputDir, config.name);
|
|
513
|
+
ensureDir(projectDir);
|
|
514
|
+
|
|
515
|
+
// ========== Generate Structure ==========
|
|
516
|
+
log.section("Creating directory structure");
|
|
517
|
+
|
|
518
|
+
const dirs = [
|
|
519
|
+
"app",
|
|
520
|
+
"app/api",
|
|
521
|
+
"components",
|
|
522
|
+
"lib",
|
|
523
|
+
"styles",
|
|
524
|
+
"public",
|
|
525
|
+
"types",
|
|
526
|
+
];
|
|
527
|
+
|
|
528
|
+
dirs.forEach((dir) => {
|
|
529
|
+
ensureDir(path.join(projectDir, dir));
|
|
530
|
+
log.file(`${dir}/`);
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
// ========== Generate Pages ==========
|
|
534
|
+
log.section("Generating pages");
|
|
535
|
+
|
|
536
|
+
if (config.pages) {
|
|
537
|
+
config.pages.forEach((page) => {
|
|
538
|
+
const pagePath =
|
|
539
|
+
page.path === "/"
|
|
540
|
+
? "app/page.tsx"
|
|
541
|
+
: `app${page.path.replace("[", "(").replace("]", ")")}/page.tsx`;
|
|
542
|
+
|
|
543
|
+
const pageDir = path.dirname(path.join(projectDir, pagePath));
|
|
544
|
+
ensureDir(pageDir);
|
|
545
|
+
|
|
546
|
+
const pageContent = generatePage(page, config.design);
|
|
547
|
+
fs.writeFileSync(path.join(projectDir, pagePath), pageContent.trim());
|
|
548
|
+
log.file(`${pagePath} (${page.intent})`);
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// ========== Generate Components ==========
|
|
553
|
+
log.section("Generating components");
|
|
554
|
+
|
|
555
|
+
const allComponents = new Set();
|
|
556
|
+
config.pages?.forEach((page) => {
|
|
557
|
+
page.components?.forEach((c) => allComponents.add(c));
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
allComponents.forEach((component) => {
|
|
561
|
+
const componentPath = `components/${component}.tsx`;
|
|
562
|
+
const template = componentTemplates[component];
|
|
563
|
+
const content = template
|
|
564
|
+
? template(config.design)
|
|
565
|
+
: `// TODO: Implement ${toPascalCase(
|
|
566
|
+
component
|
|
567
|
+
)} component\nexport function ${toPascalCase(
|
|
568
|
+
component
|
|
569
|
+
)}() {\n return <div>${component}</div>;\n}\n`;
|
|
570
|
+
|
|
571
|
+
fs.writeFileSync(path.join(projectDir, componentPath), content.trim());
|
|
572
|
+
log.file(componentPath);
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
// ========== Generate API Routes ==========
|
|
576
|
+
log.section("Generating API routes");
|
|
577
|
+
|
|
578
|
+
if (config.api) {
|
|
579
|
+
config.api.forEach((endpoint) => {
|
|
580
|
+
const apiPath = `app/api${endpoint.path}/route.ts`;
|
|
581
|
+
const apiDir = path.dirname(path.join(projectDir, apiPath));
|
|
582
|
+
ensureDir(apiDir);
|
|
583
|
+
|
|
584
|
+
const routeContent = generateApiRoute(endpoint);
|
|
585
|
+
fs.writeFileSync(path.join(projectDir, apiPath), routeContent.trim());
|
|
586
|
+
log.file(`${apiPath} [${endpoint.methods.join(", ")}]`);
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// ========== Generate Design System ==========
|
|
591
|
+
log.section("Generating design system");
|
|
592
|
+
|
|
593
|
+
const cssContent = generateGlobalsCss(config.design || {});
|
|
594
|
+
fs.writeFileSync(path.join(projectDir, "app/globals.css"), cssContent.trim());
|
|
595
|
+
log.file("app/globals.css");
|
|
596
|
+
|
|
597
|
+
// ========== Generate Next.js Config Files ==========
|
|
598
|
+
log.section("Generating Next.js config files");
|
|
599
|
+
|
|
600
|
+
// package.json
|
|
601
|
+
fs.writeFileSync(
|
|
602
|
+
path.join(projectDir, "package.json"),
|
|
603
|
+
generatePackageJson(config)
|
|
604
|
+
);
|
|
605
|
+
log.file("package.json");
|
|
606
|
+
|
|
607
|
+
// next.config.mjs
|
|
608
|
+
fs.writeFileSync(
|
|
609
|
+
path.join(projectDir, "next.config.mjs"),
|
|
610
|
+
generateNextConfig()
|
|
611
|
+
);
|
|
612
|
+
log.file("next.config.mjs");
|
|
613
|
+
|
|
614
|
+
// tsconfig.json
|
|
615
|
+
fs.writeFileSync(path.join(projectDir, "tsconfig.json"), generateTsConfig());
|
|
616
|
+
log.file("tsconfig.json");
|
|
617
|
+
|
|
618
|
+
// app/layout.tsx
|
|
619
|
+
fs.writeFileSync(
|
|
620
|
+
path.join(projectDir, "app/layout.tsx"),
|
|
621
|
+
generateLayout(config)
|
|
622
|
+
);
|
|
623
|
+
log.file("app/layout.tsx");
|
|
624
|
+
|
|
625
|
+
// next-env.d.ts
|
|
626
|
+
fs.writeFileSync(
|
|
627
|
+
path.join(projectDir, "next-env.d.ts"),
|
|
628
|
+
`/// <reference types="next" />
|
|
629
|
+
/// <reference types="next/image-types/global" />
|
|
630
|
+
`
|
|
631
|
+
);
|
|
632
|
+
log.file("next-env.d.ts");
|
|
633
|
+
|
|
634
|
+
// ========== Generate Database Schema ==========
|
|
635
|
+
if (config.data) {
|
|
636
|
+
log.section("Generating database schema");
|
|
637
|
+
|
|
638
|
+
ensureDir(path.join(projectDir, "database"));
|
|
639
|
+
const schemaContent = generateDatabaseSchema(config.data);
|
|
640
|
+
fs.writeFileSync(
|
|
641
|
+
path.join(projectDir, "database/schema.sql"),
|
|
642
|
+
schemaContent.trim()
|
|
643
|
+
);
|
|
644
|
+
log.file("database/schema.sql");
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// ========== Generate ebade Copy ==========
|
|
648
|
+
log.section("Copying ebade file");
|
|
649
|
+
|
|
650
|
+
fs.copyFileSync(ebadePath, path.join(projectDir, "project.ebade.yaml"));
|
|
651
|
+
log.file("project.ebade.yaml (for agent reference)");
|
|
652
|
+
|
|
653
|
+
// ========== Summary ==========
|
|
654
|
+
console.log(`
|
|
655
|
+
${colors.bright}${colors.green}════════════════════════════════════════${colors.reset}
|
|
656
|
+
${colors.green}✓${colors.reset} ebade scaffold complete!
|
|
657
|
+
|
|
658
|
+
${colors.dim}Project:${colors.reset} ${config.name}
|
|
659
|
+
${colors.dim}Type:${colors.reset} ${config.type}
|
|
660
|
+
${colors.dim}Output:${colors.reset} ${projectDir}
|
|
661
|
+
|
|
662
|
+
${colors.dim}Next steps:${colors.reset}
|
|
663
|
+
1. cd ${projectDir}
|
|
664
|
+
2. npm install
|
|
665
|
+
3. npm run dev
|
|
666
|
+
|
|
667
|
+
${colors.yellow}Note:${colors.reset} The generated code is a starting point.
|
|
668
|
+
An AI Agent can now read project.ebade.yaml
|
|
669
|
+
to understand and iterate on this project.
|
|
670
|
+
${colors.bright}${colors.green}════════════════════════════════════════${colors.reset}
|
|
671
|
+
`);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// ============================================
|
|
675
|
+
// CLI Entry Point
|
|
676
|
+
// ============================================
|
|
677
|
+
const args = process.argv.slice(2);
|
|
678
|
+
|
|
679
|
+
if (args.length === 0) {
|
|
680
|
+
console.log(`
|
|
681
|
+
${colors.bright}ebade Scaffold CLI${colors.reset}
|
|
682
|
+
|
|
683
|
+
Usage:
|
|
684
|
+
npx ebade scaffold <ebade-file> [output-dir]
|
|
685
|
+
|
|
686
|
+
Example:
|
|
687
|
+
npx ebade scaffold examples/ecommerce.ebade.yaml ./output
|
|
688
|
+
|
|
689
|
+
Options:
|
|
690
|
+
ebade-file Path to .ebade.yaml file
|
|
691
|
+
output-dir Output directory (default: ./output)
|
|
692
|
+
`);
|
|
693
|
+
process.exit(1);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
const ebadeFile = args[0];
|
|
697
|
+
const outputDir = args[1] || "./output";
|
|
698
|
+
|
|
699
|
+
if (!fs.existsSync(ebadeFile)) {
|
|
700
|
+
console.error(
|
|
701
|
+
`${colors.red}Error:${colors.reset} ebade file not found: ${ebadeFile}`
|
|
702
|
+
);
|
|
703
|
+
process.exit(1);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
scaffold(ebadeFile, outputDir);
|