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