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
|
@@ -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
|
+
}
|