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.
@@ -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);