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,474 @@
1
+ /**
2
+ * Scaffold Tool
3
+ *
4
+ * Creates a complete project structure from an intent definition.
5
+ */
6
+
7
+ import fs from "fs";
8
+ import path from "path";
9
+ import yaml from "yaml";
10
+
11
+ interface ScaffoldArgs {
12
+ projectName: string;
13
+ projectType: string;
14
+ features?: string[];
15
+ outputDir: string;
16
+ }
17
+
18
+ // Project type templates
19
+ const projectTemplates: Record<string, any> = {
20
+ "e-commerce": {
21
+ pages: [
22
+ {
23
+ path: "/",
24
+ intent: "landing-page",
25
+ components: ["hero-section", "featured-products", "testimonials"],
26
+ },
27
+ {
28
+ path: "/products",
29
+ intent: "product-listing",
30
+ components: ["search-bar", "filter-sidebar", "product-grid"],
31
+ },
32
+ {
33
+ path: "/products/[slug]",
34
+ intent: "product-detail",
35
+ components: [
36
+ "product-gallery",
37
+ "product-info",
38
+ "add-to-cart",
39
+ "reviews",
40
+ ],
41
+ },
42
+ {
43
+ path: "/cart",
44
+ intent: "shopping-cart",
45
+ components: ["cart-items", "cart-summary", "checkout-cta"],
46
+ },
47
+ {
48
+ path: "/checkout",
49
+ intent: "checkout-flow",
50
+ auth: "required",
51
+ components: ["checkout-form", "payment-section", "order-summary"],
52
+ },
53
+ ],
54
+ data: {
55
+ Product: {
56
+ fields: {
57
+ id: { type: "uuid", required: true },
58
+ name: { type: "string", required: true },
59
+ price: { type: "decimal", required: true },
60
+ images: { type: "array" },
61
+ category: { type: "string" },
62
+ stock: { type: "integer" },
63
+ },
64
+ },
65
+ Cart: {
66
+ fields: {
67
+ id: { type: "uuid", required: true },
68
+ items: { type: "array", required: true },
69
+ total: { type: "decimal", required: true },
70
+ },
71
+ },
72
+ },
73
+ },
74
+ "saas-dashboard": {
75
+ pages: [
76
+ {
77
+ path: "/",
78
+ intent: "landing-page",
79
+ components: ["hero", "features", "pricing", "cta"],
80
+ },
81
+ {
82
+ path: "/dashboard",
83
+ intent: "dashboard",
84
+ auth: "required",
85
+ components: ["stats-cards", "chart-section", "recent-activity"],
86
+ },
87
+ {
88
+ path: "/settings",
89
+ intent: "settings",
90
+ auth: "required",
91
+ components: ["profile-form", "security-settings", "billing-section"],
92
+ },
93
+ ],
94
+ data: {
95
+ User: {
96
+ fields: {
97
+ id: { type: "uuid", required: true },
98
+ email: { type: "string", required: true, unique: true },
99
+ name: { type: "string", required: true },
100
+ plan: { type: "enum" },
101
+ },
102
+ },
103
+ },
104
+ },
105
+ blog: {
106
+ pages: [
107
+ {
108
+ path: "/",
109
+ intent: "blog-home",
110
+ components: ["featured-post", "post-grid", "newsletter"],
111
+ },
112
+ {
113
+ path: "/posts",
114
+ intent: "post-listing",
115
+ components: ["post-list", "search", "categories"],
116
+ },
117
+ {
118
+ path: "/posts/[slug]",
119
+ intent: "post-detail",
120
+ components: ["post-content", "author-bio", "comments", "related-posts"],
121
+ },
122
+ ],
123
+ data: {
124
+ Post: {
125
+ fields: {
126
+ id: { type: "uuid", required: true },
127
+ title: { type: "string", required: true },
128
+ slug: { type: "string", required: true, unique: true },
129
+ content: { type: "text", required: true },
130
+ publishedAt: { type: "timestamp" },
131
+ },
132
+ },
133
+ },
134
+ },
135
+ "landing-page": {
136
+ pages: [
137
+ {
138
+ path: "/",
139
+ intent: "landing",
140
+ components: [
141
+ "hero",
142
+ "features",
143
+ "testimonials",
144
+ "pricing",
145
+ "faq",
146
+ "cta",
147
+ "footer",
148
+ ],
149
+ },
150
+ ],
151
+ data: {},
152
+ },
153
+ portfolio: {
154
+ pages: [
155
+ {
156
+ path: "/",
157
+ intent: "portfolio-home",
158
+ components: ["hero", "about", "projects-grid", "skills", "contact"],
159
+ },
160
+ {
161
+ path: "/projects/[slug]",
162
+ intent: "project-detail",
163
+ components: ["project-gallery", "project-info", "tech-stack"],
164
+ },
165
+ ],
166
+ data: {
167
+ Project: {
168
+ fields: {
169
+ id: { type: "uuid", required: true },
170
+ title: { type: "string", required: true },
171
+ description: { type: "text" },
172
+ images: { type: "array" },
173
+ link: { type: "string" },
174
+ },
175
+ },
176
+ },
177
+ },
178
+ };
179
+
180
+ // Component templates
181
+ const componentTemplates: Record<string, (name: string) => string> = {
182
+ default: (name: string) => {
183
+ const pascalName = name
184
+ .split("-")
185
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
186
+ .join("");
187
+ return `"use client";
188
+
189
+ import { useState } from "react";
190
+
191
+ interface ${pascalName}Props {
192
+ className?: string;
193
+ }
194
+
195
+ export function ${pascalName}({ className }: ${pascalName}Props) {
196
+ return (
197
+ <section className={\`${name} \${className || ""}\`}>
198
+ <div className="container">
199
+ {/* ${pascalName} content */}
200
+ <h2>${pascalName}</h2>
201
+ </div>
202
+ </section>
203
+ );
204
+ }
205
+ `;
206
+ },
207
+ };
208
+
209
+ // Page template
210
+ function generatePageTemplate(page: any): string {
211
+ const componentImports = (page.components || [])
212
+ .map((c: string) => {
213
+ const pascalName = c
214
+ .split("-")
215
+ .map((w: string) => w.charAt(0).toUpperCase() + w.slice(1))
216
+ .join("");
217
+ return `import { ${pascalName} } from "@/components/${c}";`;
218
+ })
219
+ .join("\n");
220
+
221
+ const componentUsage = (page.components || [])
222
+ .map((c: string) => {
223
+ const pascalName = c
224
+ .split("-")
225
+ .map((w: string) => w.charAt(0).toUpperCase() + w.slice(1))
226
+ .join("");
227
+ return ` <${pascalName} />`;
228
+ })
229
+ .join("\n");
230
+
231
+ const pageName = page.intent
232
+ .split("-")
233
+ .map((w: string) => w.charAt(0).toUpperCase() + w.slice(1))
234
+ .join("");
235
+
236
+ return `/**
237
+ * @page('${page.path}')
238
+ * @ebade('${page.intent}')
239
+ * ${page.auth ? `@requires({ auth: '${page.auth}' })` : ""}
240
+ */
241
+
242
+ ${componentImports}
243
+
244
+ export default function ${pageName}Page() {
245
+ return (
246
+ <main className="page ${page.intent}">
247
+ ${componentUsage}
248
+ </main>
249
+ );
250
+ }
251
+ `;
252
+ }
253
+
254
+ // Generate package.json
255
+ function generatePackageJson(projectName: string, projectType: string): string {
256
+ return JSON.stringify(
257
+ {
258
+ name: projectName,
259
+ version: "0.1.0",
260
+ private: true,
261
+ scripts: {
262
+ dev: "next dev",
263
+ build: "next build",
264
+ start: "next start",
265
+ lint: "next lint",
266
+ },
267
+ dependencies: {
268
+ next: "^14.0.0",
269
+ react: "^18.2.0",
270
+ "react-dom": "^18.2.0",
271
+ },
272
+ devDependencies: {
273
+ "@types/node": "^20.0.0",
274
+ "@types/react": "^18.2.0",
275
+ typescript: "^5.3.0",
276
+ },
277
+ ebade: {
278
+ type: projectType,
279
+ version: "0.1.0",
280
+ },
281
+ },
282
+ null,
283
+ 2
284
+ );
285
+ }
286
+
287
+ // Generate globals.css
288
+ function generateGlobalsCss(): string {
289
+ return `/* ebade Generated Design System */
290
+
291
+ :root {
292
+ --color-primary: #6366f1;
293
+ --color-secondary: #f59e0b;
294
+ --color-accent: #10b981;
295
+
296
+ --font-family: 'Inter', system-ui, sans-serif;
297
+
298
+ --radius-sm: 0.25rem;
299
+ --radius-md: 0.5rem;
300
+ --radius-lg: 1rem;
301
+ }
302
+
303
+ * {
304
+ box-sizing: border-box;
305
+ margin: 0;
306
+ padding: 0;
307
+ }
308
+
309
+ body {
310
+ font-family: var(--font-family);
311
+ line-height: 1.6;
312
+ color: #1f2937;
313
+ }
314
+
315
+ .container {
316
+ max-width: 1200px;
317
+ margin: 0 auto;
318
+ padding: 0 1rem;
319
+ }
320
+
321
+ .page {
322
+ min-height: 100vh;
323
+ }
324
+ `;
325
+ }
326
+
327
+ export async function scaffoldProject(args: ScaffoldArgs) {
328
+ const { projectName, projectType, features = [], outputDir } = args;
329
+
330
+ // Get template for project type
331
+ const template = projectTemplates[projectType];
332
+ if (!template) {
333
+ throw new Error(
334
+ `Unknown project type: ${projectType}. Available: ${Object.keys(
335
+ projectTemplates
336
+ ).join(", ")}`
337
+ );
338
+ }
339
+
340
+ // Create project directory
341
+ const projectDir = path.join(outputDir, projectName);
342
+
343
+ // Create directory structure
344
+ const dirs = ["", "app", "components", "lib", "styles", "public", "types"];
345
+
346
+ for (const dir of dirs) {
347
+ const fullPath = path.join(projectDir, dir);
348
+ if (!fs.existsSync(fullPath)) {
349
+ fs.mkdirSync(fullPath, { recursive: true });
350
+ }
351
+ }
352
+
353
+ // Track created files
354
+ const createdFiles: string[] = [];
355
+
356
+ // Generate pages
357
+ for (const page of template.pages) {
358
+ // Create page directory if needed
359
+ const pagePath =
360
+ page.path === "/"
361
+ ? "app/page.tsx"
362
+ : `app${page.path.replace("[", "(").replace("]", ")")}/page.tsx`;
363
+
364
+ const pageDir = path.dirname(path.join(projectDir, pagePath));
365
+ if (!fs.existsSync(pageDir)) {
366
+ fs.mkdirSync(pageDir, { recursive: true });
367
+ }
368
+
369
+ // Write page file
370
+ const pageContent = generatePageTemplate(page);
371
+ fs.writeFileSync(path.join(projectDir, pagePath), pageContent);
372
+ createdFiles.push(pagePath);
373
+
374
+ // Generate component files
375
+ for (const component of page.components || []) {
376
+ const componentPath = `components/${component}.tsx`;
377
+ const fullComponentPath = path.join(projectDir, componentPath);
378
+
379
+ if (!fs.existsSync(fullComponentPath)) {
380
+ const componentContent = componentTemplates.default(component);
381
+ fs.writeFileSync(fullComponentPath, componentContent);
382
+ createdFiles.push(componentPath);
383
+ }
384
+ }
385
+ }
386
+
387
+ // Generate package.json
388
+ fs.writeFileSync(
389
+ path.join(projectDir, "package.json"),
390
+ generatePackageJson(projectName, projectType)
391
+ );
392
+ createdFiles.push("package.json");
393
+
394
+ // Generate globals.css
395
+ fs.writeFileSync(
396
+ path.join(projectDir, "styles/globals.css"),
397
+ generateGlobalsCss()
398
+ );
399
+ createdFiles.push("styles/globals.css");
400
+
401
+ // Generate intent file for reference
402
+ const intentContent = yaml.stringify({
403
+ name: projectName,
404
+ type: projectType,
405
+ features: features,
406
+ pages: template.pages,
407
+ data: template.data,
408
+ design: {
409
+ style: "minimal-modern",
410
+ colors: {
411
+ primary: "#6366f1",
412
+ secondary: "#f59e0b",
413
+ accent: "#10b981",
414
+ },
415
+ },
416
+ });
417
+ fs.writeFileSync(path.join(projectDir, "project.ebade.yaml"), intentContent);
418
+ createdFiles.push("project.ebade.yaml");
419
+
420
+ // Generate layout
421
+ const layoutContent = `import "./globals.css";
422
+ import type { Metadata } from "next";
423
+ import { Inter } from "next/font/google";
424
+
425
+ const inter = Inter({ subsets: ["latin"] });
426
+
427
+ export const metadata: Metadata = {
428
+ title: "${projectName}",
429
+ description: "Built with ebade",
430
+ };
431
+
432
+ export default function RootLayout({
433
+ children,
434
+ }: {
435
+ children: React.ReactNode;
436
+ }) {
437
+ return (
438
+ <html lang="en">
439
+ <body className={inter.className}>{children}</body>
440
+ </html>
441
+ );
442
+ }
443
+ `;
444
+ fs.writeFileSync(path.join(projectDir, "app/layout.tsx"), layoutContent);
445
+ // Copy globals.css to app folder for Next.js
446
+ fs.writeFileSync(
447
+ path.join(projectDir, "app/globals.css"),
448
+ generateGlobalsCss()
449
+ );
450
+ createdFiles.push("app/layout.tsx", "app/globals.css");
451
+
452
+ return {
453
+ content: [
454
+ {
455
+ type: "text",
456
+ text: `✅ Successfully scaffolded "${projectName}" (${projectType})
457
+
458
+ 📁 Project created at: ${projectDir}
459
+
460
+ 📄 Files created:
461
+ ${createdFiles.map((f) => ` • ${f}`).join("\n")}
462
+
463
+ 🚀 Next steps:
464
+ 1. cd ${projectDir}
465
+ 2. npm install
466
+ 3. npm run dev
467
+
468
+ 📋 ebade file saved as: project.ebade.yaml
469
+ This can be used by AI agents to understand and iterate on the project.
470
+ `,
471
+ },
472
+ ],
473
+ };
474
+ }
@@ -0,0 +1,233 @@
1
+ /**
2
+ * Validate Tool
3
+ *
4
+ * Validates an ebade definition file for correctness.
5
+ */
6
+
7
+ import yaml from "yaml";
8
+ import { z } from "zod";
9
+
10
+ interface ValidateArgs {
11
+ intentContent: string;
12
+ }
13
+
14
+ // Schema for intent validation
15
+ const PageSchema = z.object({
16
+ path: z.string().startsWith("/"),
17
+ intent: z.string(),
18
+ auth: z.enum(["none", "required", "optional"]).optional(),
19
+ components: z.array(z.string()).optional(),
20
+ });
21
+
22
+ const FieldSchema = z.object({
23
+ type: z.enum([
24
+ "uuid",
25
+ "string",
26
+ "text",
27
+ "integer",
28
+ "decimal",
29
+ "boolean",
30
+ "timestamp",
31
+ "json",
32
+ "array",
33
+ "enum",
34
+ ]),
35
+ required: z.boolean().optional(),
36
+ unique: z.boolean().optional(),
37
+ });
38
+
39
+ const DataModelSchema = z.object({
40
+ fields: z.record(FieldSchema),
41
+ relations: z.array(z.string()).optional(),
42
+ });
43
+
44
+ const DesignSchema = z.object({
45
+ style: z
46
+ .enum([
47
+ "minimal-modern",
48
+ "bold-vibrant",
49
+ "corporate-clean",
50
+ "playful-rounded",
51
+ "dark-premium",
52
+ "glassmorphism",
53
+ ])
54
+ .optional(),
55
+ colors: z
56
+ .object({
57
+ primary: z
58
+ .string()
59
+ .regex(/^#[0-9a-fA-F]{6}$/)
60
+ .optional(),
61
+ secondary: z
62
+ .string()
63
+ .regex(/^#[0-9a-fA-F]{6}$/)
64
+ .optional(),
65
+ accent: z
66
+ .string()
67
+ .regex(/^#[0-9a-fA-F]{6}$/)
68
+ .optional(),
69
+ })
70
+ .optional(),
71
+ font: z.string().optional(),
72
+ borderRadius: z.enum(["none", "sm", "md", "lg", "full"]).optional(),
73
+ });
74
+
75
+ const IntentFileSchema = z.object({
76
+ name: z
77
+ .string()
78
+ .regex(/^[a-z][a-z0-9-]*$/, "Project name must be kebab-case"),
79
+ type: z.enum([
80
+ "e-commerce",
81
+ "saas-dashboard",
82
+ "blog",
83
+ "landing-page",
84
+ "portfolio",
85
+ "api-only",
86
+ ]),
87
+ description: z.string().optional(),
88
+ features: z.array(z.string()).min(1, "At least one feature is required"),
89
+ pages: z.array(PageSchema).optional(),
90
+ design: DesignSchema.optional(),
91
+ data: z.record(DataModelSchema).optional(),
92
+ });
93
+
94
+ interface ValidationIssue {
95
+ type: "error" | "warning";
96
+ path: string;
97
+ message: string;
98
+ }
99
+
100
+ export async function validateIntent(args: ValidateArgs) {
101
+ const { intentContent } = args;
102
+ const issues: ValidationIssue[] = [];
103
+
104
+ // Try to parse YAML
105
+ let parsed: any;
106
+ try {
107
+ parsed = yaml.parse(intentContent);
108
+ } catch (e) {
109
+ return {
110
+ content: [
111
+ {
112
+ type: "text",
113
+ text: `❌ YAML Parse Error: ${
114
+ e instanceof Error ? e.message : String(e)
115
+ }`,
116
+ },
117
+ ],
118
+ isError: true,
119
+ };
120
+ }
121
+
122
+ // Validate against schema
123
+ const result = IntentFileSchema.safeParse(parsed);
124
+
125
+ if (!result.success) {
126
+ for (const issue of result.error.issues) {
127
+ issues.push({
128
+ type: "error",
129
+ path: issue.path.join("."),
130
+ message: issue.message,
131
+ });
132
+ }
133
+ }
134
+
135
+ // Additional semantic validations
136
+ if (parsed.pages) {
137
+ const paths = new Set<string>();
138
+ for (const page of parsed.pages) {
139
+ // Check for duplicate paths
140
+ if (paths.has(page.path)) {
141
+ issues.push({
142
+ type: "error",
143
+ path: `pages.${page.path}`,
144
+ message: `Duplicate page path: ${page.path}`,
145
+ });
146
+ }
147
+ paths.add(page.path);
148
+
149
+ // Warn about pages requiring auth without auth feature
150
+ if (page.auth === "required" && !parsed.features?.includes("user-auth")) {
151
+ issues.push({
152
+ type: "warning",
153
+ path: `pages.${page.path}`,
154
+ message: `Page requires auth but 'user-auth' feature is not enabled`,
155
+ });
156
+ }
157
+ }
158
+ }
159
+
160
+ // Check data model relations
161
+ if (parsed.data) {
162
+ const modelNames = new Set(Object.keys(parsed.data));
163
+ for (const [modelName, model] of Object.entries(
164
+ parsed.data as Record<string, any>
165
+ )) {
166
+ if (model.relations) {
167
+ for (const relation of model.relations) {
168
+ // Extract target model from relation (e.g., "has_many: Review" → "Review")
169
+ const match = relation.match(/:\s*(\w+)/);
170
+ if (match && !modelNames.has(match[1])) {
171
+ issues.push({
172
+ type: "warning",
173
+ path: `data.${modelName}.relations`,
174
+ message: `Relation references unknown model: ${match[1]}`,
175
+ });
176
+ }
177
+ }
178
+ }
179
+ }
180
+ }
181
+
182
+ // Build response
183
+ const errors = issues.filter((i) => i.type === "error");
184
+ const warnings = issues.filter((i) => i.type === "warning");
185
+
186
+ if (errors.length === 0 && warnings.length === 0) {
187
+ return {
188
+ content: [
189
+ {
190
+ type: "text",
191
+ text: `✅ ebade file is valid!
192
+
193
+ 📋 Summary:
194
+ • Name: ${parsed.name}
195
+ • Type: ${parsed.type}
196
+ • Features: ${parsed.features?.length || 0}
197
+ • Pages: ${parsed.pages?.length || 0}
198
+ • Data Models: ${Object.keys(parsed.data || {}).length}
199
+
200
+ Ready to scaffold with ebade_scaffold tool.
201
+ `,
202
+ },
203
+ ],
204
+ };
205
+ }
206
+
207
+ let responseText = "";
208
+
209
+ if (errors.length > 0) {
210
+ responseText += `❌ ${errors.length} Error(s):\n`;
211
+ for (const error of errors) {
212
+ responseText += ` • [${error.path}] ${error.message}\n`;
213
+ }
214
+ responseText += "\n";
215
+ }
216
+
217
+ if (warnings.length > 0) {
218
+ responseText += `⚠️ ${warnings.length} Warning(s):\n`;
219
+ for (const warning of warnings) {
220
+ responseText += ` • [${warning.path}] ${warning.message}\n`;
221
+ }
222
+ }
223
+
224
+ return {
225
+ content: [
226
+ {
227
+ type: "text",
228
+ text: responseText,
229
+ },
230
+ ],
231
+ isError: errors.length > 0,
232
+ };
233
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "node",
6
+ "esModuleInterop": true,
7
+ "strict": true,
8
+ "outDir": "./dist",
9
+ "rootDir": "./src",
10
+ "declaration": true,
11
+ "skipLibCheck": true,
12
+ "resolveJsonModule": true
13
+ },
14
+ "include": ["src/**/*"],
15
+ "exclude": ["node_modules", "dist"]
16
+ }