ebade 0.4.6 → 0.4.7
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/.agent/workflows/prompt-to-product.md +18 -22
- package/ARCHITECTURE.md +17 -5
- package/CHANGELOG.md +10 -2
- package/MANIFESTO.md +25 -8
- package/README.md +63 -27
- package/assets/build-demo.gif +0 -0
- package/assets/build-demo.mp4 +0 -0
- package/cli/adapters/base.js +38 -0
- package/cli/adapters/html-vanilla.js +73 -0
- package/cli/adapters/nextjs.js +368 -0
- package/cli/scaffold.js +97 -1338
- package/cli/utils.js +98 -0
- package/docs/GREEN-AI.md +3 -3
- package/package.json +4 -1
- package/packages/mcp-server/package.json +2 -2
- package/packages/mcp-server/src/index.ts +2 -2
- package/packages/mcp-server/src/tools/build.ts +1 -1
- package/tests/cli/multi-target.test.js +60 -0
- package/tests/framework/architect.test.js +1 -1
- package/vhs/build-demo.tape +54 -0
- package/{demo.tape → vhs/demo.tape} +3 -3
- package/www/app/page.tsx +1 -1
- package/www/package.json +1 -1
- package/www/public/assets/build-demo.gif +0 -0
- package/www/public/assets/build-demo.mp4 +0 -0
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import ora from "ora";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import { TargetAdapter } from "./base.js";
|
|
6
|
+
import {
|
|
7
|
+
toPascalCase,
|
|
8
|
+
toSnakeCase,
|
|
9
|
+
hexToHsl,
|
|
10
|
+
ensureDir,
|
|
11
|
+
mapToSqlType,
|
|
12
|
+
} from "../utils.js";
|
|
13
|
+
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
+
const __dirname = path.dirname(__filename);
|
|
16
|
+
|
|
17
|
+
export class NextJsAdapter extends TargetAdapter {
|
|
18
|
+
constructor(colors, log) {
|
|
19
|
+
super(colors, log);
|
|
20
|
+
// Path to templates relative to this file (cli/adapters/nextjs.js)
|
|
21
|
+
this.templateBaseDir = path.join(__dirname, "..", "templates");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
generateBoilerplate(config, projectDir) {
|
|
25
|
+
this.log.section("Creating Next.js directory structure");
|
|
26
|
+
|
|
27
|
+
const dirs = [
|
|
28
|
+
"app",
|
|
29
|
+
"app/api",
|
|
30
|
+
"components",
|
|
31
|
+
"lib",
|
|
32
|
+
"styles",
|
|
33
|
+
"public",
|
|
34
|
+
"types",
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
dirs.forEach((dir) => {
|
|
38
|
+
ensureDir(path.join(projectDir, dir));
|
|
39
|
+
this.log.file(`${dir}/`);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// lib/utils.ts
|
|
43
|
+
fs.writeFileSync(
|
|
44
|
+
path.join(projectDir, "lib/utils.ts"),
|
|
45
|
+
this.generateUtils()
|
|
46
|
+
);
|
|
47
|
+
this.log.file("lib/utils.ts");
|
|
48
|
+
|
|
49
|
+
this.log.section("Generating Next.js config files");
|
|
50
|
+
|
|
51
|
+
// package.json
|
|
52
|
+
fs.writeFileSync(
|
|
53
|
+
path.join(projectDir, "package.json"),
|
|
54
|
+
this.generatePackageJson(config)
|
|
55
|
+
);
|
|
56
|
+
this.log.file("package.json");
|
|
57
|
+
|
|
58
|
+
// next.config.mjs
|
|
59
|
+
fs.writeFileSync(
|
|
60
|
+
path.join(projectDir, "next.config.mjs"),
|
|
61
|
+
this.generateNextConfig()
|
|
62
|
+
);
|
|
63
|
+
this.log.file("next.config.mjs");
|
|
64
|
+
|
|
65
|
+
// tsconfig.json
|
|
66
|
+
fs.writeFileSync(
|
|
67
|
+
path.join(projectDir, "tsconfig.json"),
|
|
68
|
+
this.generateTsConfig()
|
|
69
|
+
);
|
|
70
|
+
this.log.file("tsconfig.json");
|
|
71
|
+
|
|
72
|
+
// tailwind.config.js
|
|
73
|
+
fs.writeFileSync(
|
|
74
|
+
path.join(projectDir, "tailwind.config.js"),
|
|
75
|
+
this.generateTailwindConfig()
|
|
76
|
+
);
|
|
77
|
+
this.log.file("tailwind.config.js");
|
|
78
|
+
|
|
79
|
+
// postcss.config.js
|
|
80
|
+
fs.writeFileSync(
|
|
81
|
+
path.join(projectDir, "postcss.config.js"),
|
|
82
|
+
this.generatePostcssConfig()
|
|
83
|
+
);
|
|
84
|
+
this.log.file("postcss.config.js");
|
|
85
|
+
|
|
86
|
+
// vitest.config.ts
|
|
87
|
+
fs.writeFileSync(
|
|
88
|
+
path.join(projectDir, "vitest.config.ts"),
|
|
89
|
+
this.generateVitestConfig()
|
|
90
|
+
);
|
|
91
|
+
this.log.file("vitest.config.ts");
|
|
92
|
+
|
|
93
|
+
// .gitignore
|
|
94
|
+
fs.writeFileSync(
|
|
95
|
+
path.join(projectDir, ".gitignore"),
|
|
96
|
+
this.generateGitignore()
|
|
97
|
+
);
|
|
98
|
+
this.log.file(".gitignore");
|
|
99
|
+
|
|
100
|
+
// .env.example
|
|
101
|
+
fs.writeFileSync(
|
|
102
|
+
path.join(projectDir, ".env.example"),
|
|
103
|
+
this.generateEnvExample(config)
|
|
104
|
+
);
|
|
105
|
+
this.log.file(".env.example");
|
|
106
|
+
|
|
107
|
+
// app/layout.tsx
|
|
108
|
+
fs.writeFileSync(
|
|
109
|
+
path.join(projectDir, "app/layout.tsx"),
|
|
110
|
+
this.generateLayout(config)
|
|
111
|
+
);
|
|
112
|
+
this.log.file("app/layout.tsx");
|
|
113
|
+
|
|
114
|
+
// next-env.d.ts
|
|
115
|
+
fs.writeFileSync(
|
|
116
|
+
path.join(projectDir, "next-env.d.ts"),
|
|
117
|
+
`/// <reference types="next" />\n/// <reference types="next/image-types/global" />\n`
|
|
118
|
+
);
|
|
119
|
+
this.log.file("next-env.d.ts");
|
|
120
|
+
|
|
121
|
+
// Design System
|
|
122
|
+
this.log.section("Generating design system");
|
|
123
|
+
const cssContent = this.generateGlobalsCss(config.design || {});
|
|
124
|
+
fs.writeFileSync(
|
|
125
|
+
path.join(projectDir, "app/globals.css"),
|
|
126
|
+
cssContent.trim()
|
|
127
|
+
);
|
|
128
|
+
this.log.file("app/globals.css");
|
|
129
|
+
|
|
130
|
+
// Agent Rules
|
|
131
|
+
const agentRules = this.generateAgentRules(config).trim();
|
|
132
|
+
fs.writeFileSync(path.join(projectDir, ".cursorrules"), agentRules);
|
|
133
|
+
fs.writeFileSync(path.join(projectDir, ".clauderules"), agentRules);
|
|
134
|
+
ensureDir(path.join(projectDir, ".github"));
|
|
135
|
+
fs.writeFileSync(
|
|
136
|
+
path.join(projectDir, ".github/copilot-instructions.md"),
|
|
137
|
+
agentRules
|
|
138
|
+
);
|
|
139
|
+
this.log.file(
|
|
140
|
+
".cursorrules, .clauderules, .github/copilot-instructions.md"
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
// Database Schema
|
|
144
|
+
if (config.data) {
|
|
145
|
+
this.log.section("Generating database schema");
|
|
146
|
+
ensureDir(path.join(projectDir, "database"));
|
|
147
|
+
const schemaContent = this.generateDatabaseSchema(config.data);
|
|
148
|
+
fs.writeFileSync(
|
|
149
|
+
path.join(projectDir, "database/schema.sql"),
|
|
150
|
+
schemaContent.trim()
|
|
151
|
+
);
|
|
152
|
+
this.log.file("database/schema.sql");
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
generatePage(page, design) {
|
|
157
|
+
const componentImports =
|
|
158
|
+
page.components
|
|
159
|
+
?.map((c) => `import { ${toPascalCase(c)} } from '@/components/${c}';`)
|
|
160
|
+
.join("\n") || "";
|
|
161
|
+
const componentUsage =
|
|
162
|
+
page.components
|
|
163
|
+
?.map((c) => ` <${toPascalCase(c)} />`)
|
|
164
|
+
.join("\n") || " {/* No components defined */}";
|
|
165
|
+
|
|
166
|
+
return `import React from 'react';\n${componentImports}\n\n/**\n * 🧠 Generated via ebade - The Agent-First Framework\n * https://github.com/hasankemaldemirci/ebade\n * \n * @page('${
|
|
167
|
+
page.path
|
|
168
|
+
}')\n * @intent('${
|
|
169
|
+
page.intent
|
|
170
|
+
}')\n */\nexport default function ${toPascalCase(
|
|
171
|
+
page.intent
|
|
172
|
+
)}Page() {\n return (\n <div className="min-h-screen bg-slate-950 text-white selection:bg-indigo-500/30 selection:text-indigo-200">\n <main className="relative overflow-hidden">\n {/* Ambient background glow */}\n <div className="absolute top-0 left-1/2 -translate-x-1/2 w-full h-[500px] bg-indigo-600/10 blur-[120px] rounded-full pointer-events-none" />\n \n <div className="relative z-10">\n${componentUsage}\n </div>\n </main>\n </div>\n );\n}\n\n// Auth: ${
|
|
173
|
+
page.auth || "public"
|
|
174
|
+
}\n`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
generateComponent(componentName, design) {
|
|
178
|
+
const templatePath = path.join(
|
|
179
|
+
this.templateBaseDir,
|
|
180
|
+
`${componentName}.tsx`
|
|
181
|
+
);
|
|
182
|
+
let content = "";
|
|
183
|
+
|
|
184
|
+
if (fs.existsSync(templatePath)) {
|
|
185
|
+
content = fs.readFileSync(templatePath, "utf-8");
|
|
186
|
+
const primaryColor = design?.colors?.primary || "#6366f1";
|
|
187
|
+
content = content.replace(/\{\{primary\}\}/g, primaryColor);
|
|
188
|
+
} else {
|
|
189
|
+
content = `import React from 'react';\nimport { cn } from "@/lib/utils";\n\n/**\n * 🧠 Generated via ebade\n * Component: ${toPascalCase(
|
|
190
|
+
componentName
|
|
191
|
+
)}\n * Status: Intent needs implementation\n */\nexport function ${toPascalCase(
|
|
192
|
+
componentName
|
|
193
|
+
)}() {\n return (\n <div className="p-12 glass-card rounded-[2.5rem] text-center min-h-[300px] flex flex-col items-center justify-center group hover:border-primary/50 transition-all">\n <div className="w-20 h-20 bg-primary/10 rounded-[2rem] flex items-center justify-center mb-6 group-hover:scale-110 transition-transform">\n <span className="text-3xl">✨</span>\n </div>\n <h3 className="text-2xl font-bold mb-3 text-white">${toPascalCase(
|
|
194
|
+
componentName
|
|
195
|
+
)}</h3>\n <p className="text-slate-400 max-w-sm mx-auto leading-relaxed">\n This intent is defined for your AI agent. To customize, edit <code>components/${componentName}.tsx</code> or use the ebade compiler.\n </p>\n </div>\n );\n}\n`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const testContent = `import { describe, it, expect } from 'vitest';\nimport { render } from '@testing-library/react';\nimport { ${toPascalCase(
|
|
199
|
+
componentName
|
|
200
|
+
)} } from './${componentName}';\nimport React from 'react';\n\ndescribe('${toPascalCase(
|
|
201
|
+
componentName
|
|
202
|
+
)} Component', () => {\n it('renders without crashing', () => {\n render(<${toPascalCase(
|
|
203
|
+
componentName
|
|
204
|
+
)} />);\n expect(document.body).toBeDefined();\n });\n});\n`;
|
|
205
|
+
|
|
206
|
+
return { content, testContent };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
generateApiRoute(endpoint) {
|
|
210
|
+
return endpoint.methods
|
|
211
|
+
.map(
|
|
212
|
+
(method) =>
|
|
213
|
+
`\n/**\n * 🧠 Generated via ebade - The Agent-First Framework\n * ${method} ${
|
|
214
|
+
endpoint.path
|
|
215
|
+
}\n * Auth: ${
|
|
216
|
+
endpoint.auth || "none"
|
|
217
|
+
}\n */\nexport async function ${method}(request) {\n // TODO: Implement ${method} handler\n \n return Response.json({ \n message: "${method} ${
|
|
218
|
+
endpoint.path
|
|
219
|
+
} - Not implemented" \n });\n}\n`
|
|
220
|
+
)
|
|
221
|
+
.join("\n");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Helper Methods for Boilerplate
|
|
225
|
+
generateUtils() {
|
|
226
|
+
return `import { type ClassValue, clsx } from "clsx"\nimport { twMerge } from "tailwind-merge"\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs))\n}\n`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
generatePackageJson(config) {
|
|
230
|
+
return JSON.stringify(
|
|
231
|
+
{
|
|
232
|
+
name: config.name.toLowerCase().replace(/[^a-z0-9]/g, "-"),
|
|
233
|
+
version: "0.1.0",
|
|
234
|
+
private: true,
|
|
235
|
+
scripts: {
|
|
236
|
+
dev: "next dev",
|
|
237
|
+
build: "next build",
|
|
238
|
+
start: "next start",
|
|
239
|
+
lint: "next lint",
|
|
240
|
+
test: "vitest",
|
|
241
|
+
},
|
|
242
|
+
dependencies: {
|
|
243
|
+
next: "^14.2.0",
|
|
244
|
+
react: "^18.3.0",
|
|
245
|
+
"react-dom": "^18.3.0",
|
|
246
|
+
"lucide-react": "^0.400.0",
|
|
247
|
+
clsx: "^2.1.0",
|
|
248
|
+
"tailwind-merge": "^2.2.0",
|
|
249
|
+
"class-variance-authority": "^0.7.0",
|
|
250
|
+
"framer-motion": "^11.0.0",
|
|
251
|
+
},
|
|
252
|
+
devDependencies: {
|
|
253
|
+
"@types/node": "^20.0.0",
|
|
254
|
+
"@types/react": "^18.2.0",
|
|
255
|
+
"@types/react-dom": "^18.2.0",
|
|
256
|
+
"@testing-library/react": "^14.1.2",
|
|
257
|
+
"@vitejs/plugin-react": "^4.2.0",
|
|
258
|
+
jsdom: "^22.1.0",
|
|
259
|
+
vitest: "^0.34.6",
|
|
260
|
+
autoprefixer: "^10.0.1",
|
|
261
|
+
postcss: "^8.4.0",
|
|
262
|
+
tailwindcss: "^3.4.0",
|
|
263
|
+
"tailwindcss-animate": "^1.0.7",
|
|
264
|
+
typescript: "^5.0.0",
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
null,
|
|
268
|
+
2
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
generateNextConfig() {
|
|
273
|
+
return `/** @type {import('next').NextConfig} */\nconst nextConfig = {};\nexport default nextConfig;\n`;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
generateTsConfig() {
|
|
277
|
+
return JSON.stringify(
|
|
278
|
+
{
|
|
279
|
+
compilerOptions: {
|
|
280
|
+
target: "es5",
|
|
281
|
+
lib: ["dom", "dom.iterable", "esnext"],
|
|
282
|
+
allowJs: true,
|
|
283
|
+
skipLibCheck: true,
|
|
284
|
+
strict: true,
|
|
285
|
+
noEmit: true,
|
|
286
|
+
esModuleInterop: true,
|
|
287
|
+
module: "esnext",
|
|
288
|
+
moduleResolution: "node",
|
|
289
|
+
resolveJsonModule: true,
|
|
290
|
+
isolatedModules: true,
|
|
291
|
+
jsx: "preserve",
|
|
292
|
+
incremental: true,
|
|
293
|
+
plugins: [{ name: "next" }],
|
|
294
|
+
paths: { "@/*": ["./*"] },
|
|
295
|
+
},
|
|
296
|
+
include: [
|
|
297
|
+
"next-env.d.ts",
|
|
298
|
+
"**/*.ts",
|
|
299
|
+
"**/*.tsx",
|
|
300
|
+
".next/types/**/*.ts",
|
|
301
|
+
],
|
|
302
|
+
exclude: ["node_modules"],
|
|
303
|
+
},
|
|
304
|
+
null,
|
|
305
|
+
2
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
generateTailwindConfig() {
|
|
310
|
+
return `/** @type {import('tailwindcss').Config} */\nmodule.exports = {\n darkMode: ["class"],\n content: ['./pages/**/*.{ts,tsx}', './components/**/*.{ts,tsx}', './app/**/*.{ts,tsx}', './src/**/*.{ts,tsx}'],\n theme: {\n extend: {\n colors: {\n border: "hsl(var(--border))",\n input: "hsl(var(--input))",\n ring: "hsl(var(--ring))",\n background: "hsl(var(--background))",\n foreground: "hsl(var(--foreground))",\n primary: { DEFAULT: "hsl(var(--primary))", foreground: "hsl(var(--primary-foreground))" },\n secondary: { DEFAULT: "hsl(var(--secondary))", foreground: "hsl(var(--secondary-foreground))" },\n destructive: { DEFAULT: "hsl(var(--destructive))", foreground: "hsl(var(--destructive-foreground))" },\n muted: { DEFAULT: "hsl(var(--muted))", foreground: "hsl(var(--muted-foreground))" },\n accent: { DEFAULT: "hsl(var(--accent))", foreground: "hsl(var(--accent-foreground))" },\n popover: { DEFAULT: "hsl(var(--popover))", foreground: "hsl(var(--popover-foreground))" },\n card: { DEFAULT: "hsl(var(--card))", foreground: "hsl(var(--card-foreground))" },\n },\n borderRadius: { lg: "var(--radius)", md: "calc(var(--radius) - 2px)", sm: "calc(var(--radius) - 4px)" },\n },\n },\n plugins: [require("tailwindcss-animate")],\n}\n`;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
generatePostcssConfig() {
|
|
314
|
+
return `module.exports = { plugins: { tailwindcss: {}, autoprefixer: {} } }\n`;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
generateVitestConfig() {
|
|
318
|
+
return `import { defineConfig } from 'vitest/config';\nimport react from '@vitejs/plugin-react';\nimport path from 'path';\n\nexport default defineConfig({ plugins: [react()], test: { environment: 'jsdom', globals: true }, resolve: { alias: { '@': path.resolve(__dirname, './') } } });\n`;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
generateGitignore() {
|
|
322
|
+
return `/node_modules\n/.next/\n/out/\n/build\n.DS_Store\n*.pem\n.env*.local\n.env\n.vercel\nnext-env.d.ts\n`;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
generateEnvExample(config) {
|
|
326
|
+
return `# ebade Generated Environment Variables\n# Project: ${config.name}\n\nDATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres"\nNEXTAUTH_SECRET="your-secret-here"\n`;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
generateLayout(config) {
|
|
330
|
+
const fontFamily = config.design?.font || "Inter";
|
|
331
|
+
return `import React from 'react';\nimport type { Metadata } from "next";\nimport "./globals.css";\n\nexport const metadata: Metadata = { title: "${
|
|
332
|
+
config.name
|
|
333
|
+
}", description: "Built with ebade" };\n\nexport default function RootLayout({ children }: { children: React.ReactNode }) {\n return (\n <html lang="en">\n <head>\n <link href="https://fonts.googleapis.com/css2?family=${fontFamily.replace(
|
|
334
|
+
" ",
|
|
335
|
+
"+"
|
|
336
|
+
)}:wght@400;500;600;700;800&display=swap" rel="stylesheet" />\n </head>\n <body>{children}</body>\n </html>\n );\n}\n`;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
generateGlobalsCss(design) {
|
|
340
|
+
const primary = design.colors?.primary || "#6366f1";
|
|
341
|
+
return `@tailwind base;\n@tailwind components;\n@tailwind utilities;\n \n@layer base {\n :root {\n --background: 222 47% 4%;\n --foreground: 213 31% 91%;\n --primary: ${hexToHsl(
|
|
342
|
+
primary
|
|
343
|
+
)};\n --border: 216 34% 17%;\n --radius: 1rem;\n }\n}\n \n@layer base {\n * { @apply border-border; }\n body { @apply bg-background text-foreground antialiased; }\n}\n\n.glass-card { @apply bg-white/[0.03] border border-white/10 backdrop-blur-xl; }\n`;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
generateDatabaseSchema(data) {
|
|
347
|
+
let sql = "-- ebade Generated Database Schema\n\n";
|
|
348
|
+
for (const [modelName, model] of Object.entries(data)) {
|
|
349
|
+
sql += `-- Table: ${modelName}\nCREATE TABLE IF NOT EXISTS ${toSnakeCase(
|
|
350
|
+
modelName
|
|
351
|
+
)} (\n`;
|
|
352
|
+
const fields = Object.entries(model.fields).map(
|
|
353
|
+
([fieldName, fieldDef]) => {
|
|
354
|
+
const sqlType = mapToSqlType(fieldDef.type);
|
|
355
|
+
return ` ${toSnakeCase(fieldName)} ${sqlType}${
|
|
356
|
+
fieldDef.required ? " NOT NULL" : ""
|
|
357
|
+
}${fieldDef.unique ? " UNIQUE" : ""}`;
|
|
358
|
+
}
|
|
359
|
+
);
|
|
360
|
+
sql += fields.join(",\n") + "\n);\n\n";
|
|
361
|
+
}
|
|
362
|
+
return sql;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
generateAgentRules(config) {
|
|
366
|
+
return `# ebade Rules\n- Intent > Implementation\n- Source of Truth: project.ebade.yaml\n- Use CSS Variables: var(--color-primary)\n`;
|
|
367
|
+
}
|
|
368
|
+
}
|