@donkeylabs/cli 2.0.14 → 2.0.16

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.
Files changed (85) hide show
  1. package/package.json +1 -1
  2. package/src/commands/config.ts +610 -0
  3. package/src/commands/deploy-enhanced.ts +354 -0
  4. package/src/commands/deploy.ts +204 -0
  5. package/src/commands/generate.ts +11 -13
  6. package/src/commands/init-enhanced.ts +1994 -0
  7. package/src/deployment/manager.ts +356 -0
  8. package/src/index.ts +47 -19
  9. package/templates/starter/.env.example +0 -44
  10. package/templates/starter/.gitignore.template +0 -4
  11. package/templates/starter/donkeylabs.config.ts +0 -6
  12. package/templates/starter/package.json +0 -21
  13. package/templates/starter/src/index.ts +0 -54
  14. package/templates/starter/src/plugins/stats/index.ts +0 -105
  15. package/templates/starter/src/routes/health/handlers/ping.ts +0 -22
  16. package/templates/starter/src/routes/health/index.ts +0 -19
  17. package/templates/starter/tsconfig.json +0 -27
  18. package/templates/sveltekit-app/.env.example +0 -59
  19. package/templates/sveltekit-app/README.md +0 -103
  20. package/templates/sveltekit-app/bun.lock +0 -683
  21. package/templates/sveltekit-app/donkeylabs.config.ts +0 -12
  22. package/templates/sveltekit-app/package.json +0 -38
  23. package/templates/sveltekit-app/src/app.css +0 -40
  24. package/templates/sveltekit-app/src/app.html +0 -12
  25. package/templates/sveltekit-app/src/hooks.server.ts +0 -4
  26. package/templates/sveltekit-app/src/lib/components/ui/badge/badge.svelte +0 -30
  27. package/templates/sveltekit-app/src/lib/components/ui/badge/index.ts +0 -3
  28. package/templates/sveltekit-app/src/lib/components/ui/button/button.svelte +0 -48
  29. package/templates/sveltekit-app/src/lib/components/ui/button/index.ts +0 -9
  30. package/templates/sveltekit-app/src/lib/components/ui/card/card-content.svelte +0 -18
  31. package/templates/sveltekit-app/src/lib/components/ui/card/card-description.svelte +0 -18
  32. package/templates/sveltekit-app/src/lib/components/ui/card/card-footer.svelte +0 -18
  33. package/templates/sveltekit-app/src/lib/components/ui/card/card-header.svelte +0 -18
  34. package/templates/sveltekit-app/src/lib/components/ui/card/card-title.svelte +0 -18
  35. package/templates/sveltekit-app/src/lib/components/ui/card/card.svelte +0 -21
  36. package/templates/sveltekit-app/src/lib/components/ui/card/index.ts +0 -21
  37. package/templates/sveltekit-app/src/lib/components/ui/index.ts +0 -4
  38. package/templates/sveltekit-app/src/lib/components/ui/input/index.ts +0 -2
  39. package/templates/sveltekit-app/src/lib/components/ui/input/input.svelte +0 -20
  40. package/templates/sveltekit-app/src/lib/permissions.ts +0 -213
  41. package/templates/sveltekit-app/src/lib/utils/index.ts +0 -6
  42. package/templates/sveltekit-app/src/routes/+layout.svelte +0 -8
  43. package/templates/sveltekit-app/src/routes/+page.server.ts +0 -25
  44. package/templates/sveltekit-app/src/routes/+page.svelte +0 -680
  45. package/templates/sveltekit-app/src/routes/workflows/+page.server.ts +0 -23
  46. package/templates/sveltekit-app/src/routes/workflows/+page.svelte +0 -522
  47. package/templates/sveltekit-app/src/server/events.ts +0 -28
  48. package/templates/sveltekit-app/src/server/index.ts +0 -124
  49. package/templates/sveltekit-app/src/server/plugins/auth/auth.test.ts +0 -377
  50. package/templates/sveltekit-app/src/server/plugins/auth/index.ts +0 -815
  51. package/templates/sveltekit-app/src/server/plugins/auth/migrations/001_create_users.ts +0 -25
  52. package/templates/sveltekit-app/src/server/plugins/auth/migrations/002_create_sessions.ts +0 -32
  53. package/templates/sveltekit-app/src/server/plugins/auth/migrations/003_create_refresh_tokens.ts +0 -33
  54. package/templates/sveltekit-app/src/server/plugins/auth/migrations/004_create_passkeys.ts +0 -60
  55. package/templates/sveltekit-app/src/server/plugins/auth/schema.ts +0 -65
  56. package/templates/sveltekit-app/src/server/plugins/demo/index.ts +0 -262
  57. package/templates/sveltekit-app/src/server/plugins/email/email.test.ts +0 -369
  58. package/templates/sveltekit-app/src/server/plugins/email/index.ts +0 -411
  59. package/templates/sveltekit-app/src/server/plugins/email/migrations/001_create_email_tokens.ts +0 -33
  60. package/templates/sveltekit-app/src/server/plugins/email/schema.ts +0 -24
  61. package/templates/sveltekit-app/src/server/plugins/permissions/index.ts +0 -1048
  62. package/templates/sveltekit-app/src/server/plugins/permissions/migrations/001_create_tenants.ts +0 -63
  63. package/templates/sveltekit-app/src/server/plugins/permissions/migrations/002_create_roles.ts +0 -90
  64. package/templates/sveltekit-app/src/server/plugins/permissions/migrations/003_create_resource_grants.ts +0 -50
  65. package/templates/sveltekit-app/src/server/plugins/permissions/permissions.test.ts +0 -566
  66. package/templates/sveltekit-app/src/server/plugins/permissions/schema.ts +0 -67
  67. package/templates/sveltekit-app/src/server/plugins/workflow-demo/index.ts +0 -198
  68. package/templates/sveltekit-app/src/server/routes/auth/auth.schemas.ts +0 -66
  69. package/templates/sveltekit-app/src/server/routes/auth/handlers/login.handler.ts +0 -18
  70. package/templates/sveltekit-app/src/server/routes/auth/handlers/logout.handler.ts +0 -16
  71. package/templates/sveltekit-app/src/server/routes/auth/handlers/me.handler.ts +0 -20
  72. package/templates/sveltekit-app/src/server/routes/auth/handlers/refresh.handler.ts +0 -17
  73. package/templates/sveltekit-app/src/server/routes/auth/handlers/register.handler.ts +0 -19
  74. package/templates/sveltekit-app/src/server/routes/auth/handlers/update-profile.handler.ts +0 -21
  75. package/templates/sveltekit-app/src/server/routes/auth/index.ts +0 -73
  76. package/templates/sveltekit-app/src/server/routes/demo.ts +0 -464
  77. package/templates/sveltekit-app/src/server/routes/example/example.schemas.ts +0 -22
  78. package/templates/sveltekit-app/src/server/routes/example/handlers/greet.handler.ts +0 -21
  79. package/templates/sveltekit-app/src/server/routes/example/index.ts +0 -28
  80. package/templates/sveltekit-app/src/server/routes/permissions/index.ts +0 -248
  81. package/templates/sveltekit-app/src/server/routes/tenants/index.ts +0 -339
  82. package/templates/sveltekit-app/static/robots.txt +0 -3
  83. package/templates/sveltekit-app/svelte.config.ts +0 -17
  84. package/templates/sveltekit-app/tsconfig.json +0 -20
  85. package/templates/sveltekit-app/vite.config.ts +0 -12
@@ -0,0 +1,1994 @@
1
+ // packages/cli/src/commands/init-enhanced.ts
2
+ /**
3
+ * Enhanced project initialization with full configuration options
4
+ * Single template that adapts based on user choices
5
+ */
6
+
7
+ import { existsSync, mkdirSync, writeFileSync, readdirSync, statSync, readFileSync } from "fs";
8
+ import { join, dirname } from "path";
9
+ import pc from "picocolors";
10
+
11
+ export interface InitOptions {
12
+ projectName: string;
13
+ database: "sqlite" | "postgres" | "mysql";
14
+ frontend: "none" | "sveltekit";
15
+ plugins: string[];
16
+ includeDemo: boolean;
17
+ deployment: "docker" | "binary" | "pm2" | "vercel" | "cloudflare" | "aws";
18
+ enableBackup: boolean;
19
+ enableStorage: boolean;
20
+ setupMCP: boolean;
21
+ gitInit: boolean;
22
+ autoInstall: boolean;
23
+ useLocalPackages: boolean;
24
+ }
25
+
26
+ export interface InitCommandFlags {
27
+ useLocalPackages?: boolean;
28
+ }
29
+
30
+ // Available plugins to install (must exist in cli/plugins/)
31
+ // Note: cron is a core service (ctx.core.cron), not a plugin
32
+ const AVAILABLE_PLUGINS = [
33
+ { name: "users", description: "User management", default: true },
34
+ { name: "auth", description: "JWT authentication", default: true },
35
+ { name: "email", description: "Email sending (SMTP)", default: false },
36
+ { name: "storage", description: "File uploads (S3/Local)", default: false },
37
+ { name: "backup", description: "Database backups", default: false },
38
+ { name: "audit", description: "Audit logging", default: false },
39
+ { name: "images", description: "Image processing", default: false },
40
+ { name: "stripe", description: "Stripe payments", default: false },
41
+ ];
42
+
43
+ export async function initEnhancedCommand(args: string[], flags: InitCommandFlags = {}) {
44
+ const prompts = await import("prompts");
45
+
46
+ if (flags.useLocalPackages) {
47
+ console.log(pc.yellow("\n📦 Using local workspace packages (--local mode)\n"));
48
+ }
49
+
50
+ console.log(pc.cyan(pc.bold("\n🐴 DonkeyLabs Project Init\n")));
51
+
52
+ // 1. Project name
53
+ const projectName = args[0] || await prompts.default({
54
+ type: "text",
55
+ name: "name",
56
+ message: "Project name:",
57
+ initial: "my-app",
58
+ validate: (value) => value.length > 0 || "Project name is required",
59
+ }).then(r => r.name);
60
+
61
+ const projectDir = join(process.cwd(), projectName);
62
+
63
+ if (existsSync(projectDir)) {
64
+ console.error(pc.red(`❌ Directory ${projectName} already exists`));
65
+ process.exit(1);
66
+ }
67
+
68
+ // 2. Database selection
69
+ const database = await prompts.default({
70
+ type: "select",
71
+ name: "value",
72
+ message: "Choose database:",
73
+ choices: [
74
+ { title: "SQLite (file-based, perfect for VPS)", value: "sqlite" },
75
+ { title: "PostgreSQL (production grade)", value: "postgres" },
76
+ { title: "MySQL (compatible)", value: "mysql" },
77
+ ],
78
+ initial: 0,
79
+ }).then(r => r.value);
80
+
81
+ // 3. Frontend
82
+ const frontend = await prompts.default({
83
+ type: "select",
84
+ name: "value",
85
+ message: "Choose frontend:",
86
+ choices: [
87
+ { title: "None (API only)", value: "none" },
88
+ { title: "SvelteKit (full-stack)", value: "sveltekit" },
89
+ ],
90
+ initial: 1,
91
+ }).then(r => r.value);
92
+
93
+ // 4. Plugins selection
94
+ console.log(pc.cyan("\n📦 Select plugins:"));
95
+ const pluginChoices = AVAILABLE_PLUGINS.map(p => ({
96
+ title: `${p.name} - ${p.description}`,
97
+ value: p.name,
98
+ selected: p.default,
99
+ }));
100
+
101
+ const plugins = await prompts.default({
102
+ type: "multiselect",
103
+ name: "value",
104
+ message: "Choose plugins (space to toggle, enter to confirm):",
105
+ choices: pluginChoices,
106
+ instructions: false,
107
+ }).then(r => r.value);
108
+
109
+ // 5. Demo content
110
+ const includeDemo = await prompts.default({
111
+ type: "confirm",
112
+ name: "value",
113
+ message: "Include demo content?",
114
+ initial: true,
115
+ }).then(r => r.value);
116
+
117
+ // 6. Deployment strategy
118
+ const deployment = await prompts.default({
119
+ type: "select",
120
+ name: "value",
121
+ message: "Deployment strategy:",
122
+ choices: [
123
+ { title: "Docker (recommended for VPS)", value: "docker" },
124
+ { title: "Binary (compile & run)", value: "binary" },
125
+ { title: "PM2 (Node process manager)", value: "pm2" },
126
+ { title: "Vercel (serverless, needs PostgreSQL)", value: "vercel" },
127
+ { title: "Cloudflare Workers (edge, needs D1/PG)", value: "cloudflare" },
128
+ { title: "AWS Lambda (serverless, needs PostgreSQL)", value: "aws" },
129
+ ],
130
+ initial: 0,
131
+ }).then(r => r.value);
132
+
133
+ // Warn if serverless with SQLite
134
+ if (["vercel", "cloudflare", "aws"].includes(deployment) && database === "sqlite") {
135
+ console.log(pc.yellow("\n⚠️ Warning: SQLite won't work on serverless platforms!"));
136
+ console.log(pc.yellow("Consider using PostgreSQL instead.\n"));
137
+
138
+ const continueAnyway = await prompts.default({
139
+ type: "confirm",
140
+ name: "value",
141
+ message: "Continue with SQLite anyway?",
142
+ initial: false,
143
+ }).then(r => r.value);
144
+
145
+ if (!continueAnyway) {
146
+ console.log(pc.gray("Cancelled. Please re-run and select PostgreSQL."));
147
+ process.exit(0);
148
+ }
149
+ }
150
+
151
+ // 7. MCP setup
152
+ const setupMCP = await prompts.default({
153
+ type: "confirm",
154
+ name: "value",
155
+ message: "Setup MCP (AI-assisted development)?",
156
+ initial: true,
157
+ }).then(r => r.value);
158
+
159
+ // 8. Git init
160
+ const gitInit = await prompts.default({
161
+ type: "confirm",
162
+ name: "value",
163
+ message: "Initialize git repository?",
164
+ initial: true,
165
+ }).then(r => r.value);
166
+
167
+ // 9. Auto-install
168
+ const autoInstall = await prompts.default({
169
+ type: "confirm",
170
+ name: "value",
171
+ message: "Run bun install automatically?",
172
+ initial: true,
173
+ }).then(r => r.value);
174
+
175
+ const options: InitOptions = {
176
+ projectName,
177
+ database,
178
+ frontend,
179
+ plugins,
180
+ includeDemo,
181
+ deployment,
182
+ enableBackup: plugins.includes("backup"),
183
+ enableStorage: plugins.includes("storage"),
184
+ setupMCP,
185
+ gitInit,
186
+ autoInstall,
187
+ useLocalPackages: flags.useLocalPackages || false,
188
+ };
189
+
190
+ // Create project
191
+ console.log(pc.cyan(`\n🚀 Creating project ${projectName}...\n`));
192
+
193
+ if (options.useLocalPackages) {
194
+ console.log(pc.yellow(" Using local packages (--local mode):"));
195
+ console.log(pc.gray(" - @donkeylabs/server → file:../relative/path"));
196
+ console.log(pc.gray(" - @donkeylabs/adapter-sveltekit → file:../relative/path\n"));
197
+ }
198
+
199
+ await createProject(projectDir, options);
200
+
201
+ // Setup MCP if requested
202
+ if (options.setupMCP) {
203
+ console.log(pc.blue("\n🤖 Setting up MCP..."));
204
+ createMCPConfig(projectDir, options);
205
+ }
206
+
207
+ // Auto-install dependencies
208
+ if (options.autoInstall) {
209
+ console.log(pc.blue("\n📦 Installing dependencies..."));
210
+ console.log(pc.gray(` Running in: ${projectDir}`));
211
+ try {
212
+ const { execSync } = await import("child_process");
213
+ execSync("bun install", {
214
+ cwd: projectDir,
215
+ stdio: "inherit",
216
+ timeout: 120000, // 2 minute timeout
217
+ });
218
+ console.log(pc.green("✅ Dependencies installed!"));
219
+ } catch (error: any) {
220
+ console.log(pc.yellow("\n⚠️ Failed to install dependencies automatically"));
221
+ console.log(pc.red(` Error: ${error.message}`));
222
+ if (error.stderr) {
223
+ console.log(pc.gray(` Details: ${error.stderr}`));
224
+ }
225
+ console.log(pc.gray("\n Please run 'bun install' manually"));
226
+ }
227
+ }
228
+
229
+ console.log(pc.green(`\n✅ Project created successfully!\n`));
230
+
231
+ // Show next steps
232
+ console.log(pc.bold("Next steps:\n"));
233
+ console.log(` cd ${projectName}`);
234
+
235
+ if (!options.autoInstall) {
236
+ console.log(` bun install`);
237
+ }
238
+
239
+ if (frontend === "sveltekit") {
240
+ console.log(` bun run dev`);
241
+ } else {
242
+ console.log(` bun run start`);
243
+ }
244
+
245
+ if (deployment === "docker") {
246
+ console.log(pc.cyan(`\n🐳 To deploy with Docker:\n`));
247
+ console.log(` docker-compose up -d`);
248
+ }
249
+
250
+ if (options.setupMCP) {
251
+ console.log(pc.cyan(`\n🤖 MCP is configured!\n`));
252
+ console.log(` MCP config: .mcp.json`);
253
+ console.log(` Use donkeylabs MCP tools in your IDE`);
254
+ }
255
+
256
+ console.log(pc.gray(`\n📖 Documentation: https://donkeylabs.io/docs`));
257
+ }
258
+
259
+ export async function createProject(projectDir: string, options: InitOptions) {
260
+ // Create directory structure
261
+ mkdirSync(projectDir, { recursive: true });
262
+
263
+ // Base files
264
+ createPackageJson(projectDir, options);
265
+ createReadme(projectDir, options);
266
+ createGitignore(projectDir, options);
267
+ createDonkeylabsConfig(projectDir, options);
268
+ createTsconfig(projectDir, options);
269
+
270
+ // Server files
271
+ mkdirSync(join(projectDir, "src", "server"), { recursive: true });
272
+ createServerIndex(projectDir, options);
273
+
274
+ // Database configuration
275
+ createDatabaseConfig(projectDir, options);
276
+
277
+ // Plugins
278
+ if (options.plugins.length > 0) {
279
+ mkdirSync(join(projectDir, "src", "server", "plugins"), { recursive: true });
280
+ for (const pluginName of options.plugins) {
281
+ await createPlugin(projectDir, pluginName, options);
282
+ }
283
+ }
284
+
285
+ // Routes
286
+ mkdirSync(join(projectDir, "src", "server", "routes"), { recursive: true });
287
+ createRoutes(projectDir, options);
288
+
289
+ // Frontend (if selected)
290
+ if (options.frontend === "sveltekit") {
291
+ await createSvelteKitFrontend(projectDir, options);
292
+ }
293
+
294
+ // Demo content
295
+ if (options.includeDemo) {
296
+ createDemoContent(projectDir, options);
297
+ }
298
+
299
+ // Deployment files
300
+ createDeploymentFiles(projectDir, options);
301
+
302
+ // Environment files
303
+ createEnvFiles(projectDir, options);
304
+
305
+ // Git init
306
+ if (options.gitInit) {
307
+ await initGit(projectDir);
308
+ }
309
+ }
310
+
311
+ function createPackageJson(projectDir: string, options: InitOptions) {
312
+ const isSvelteKit = options.frontend === "sveltekit";
313
+
314
+ // Calculate relative paths to monorepo packages when using --local
315
+ // Assumes CLI is at packages/cli and project is created relative to cwd
316
+ const getPackagePath = (pkgName: string) => {
317
+ if (!options.useLocalPackages) return "latest";
318
+
319
+ // Find monorepo root by looking for root package.json with workspaces
320
+ const { existsSync, readFileSync } = require("fs");
321
+ const { join, relative, dirname } = require("path");
322
+
323
+ let searchDir = process.cwd();
324
+ let monorepoRoot = null;
325
+
326
+ // Walk up to find monorepo root
327
+ for (let i = 0; i < 10; i++) {
328
+ const pkgPath = join(searchDir, "package.json");
329
+ if (existsSync(pkgPath)) {
330
+ try {
331
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
332
+ if (pkg.workspaces) {
333
+ monorepoRoot = searchDir;
334
+ break;
335
+ }
336
+ } catch {}
337
+ }
338
+ const parent = dirname(searchDir);
339
+ if (parent === searchDir) break;
340
+ searchDir = parent;
341
+ }
342
+
343
+ if (!monorepoRoot) {
344
+ console.warn(`Warning: Could not find monorepo root, using "latest" for ${pkgName}`);
345
+ return "latest";
346
+ }
347
+
348
+ // Map package names to their paths
349
+ const packagePaths: Record<string, string> = {
350
+ "@donkeylabs/server": "packages/server",
351
+ "@donkeylabs/adapter-sveltekit": "packages/adapter-sveltekit",
352
+ "@donkeylabs/cli": "packages/cli",
353
+ "@donkeylabs/mcp": "packages/mcp",
354
+ };
355
+
356
+ const pkgRelPath = packagePaths[pkgName];
357
+ if (!pkgRelPath) return "latest";
358
+
359
+ const absolutePkgPath = join(monorepoRoot, pkgRelPath);
360
+ const relativePath = relative(projectDir, absolutePkgPath);
361
+
362
+ return `file:${relativePath}`;
363
+ };
364
+
365
+ const pkg = {
366
+ name: options.projectName,
367
+ version: "0.1.0",
368
+ private: true,
369
+ type: "module",
370
+ scripts: isSvelteKit ? {
371
+ "dev": "bun --bun vite dev",
372
+ "build": "bun run gen:types && vite build",
373
+ "preview": "bun build/server/entry.js",
374
+ "prepare": "bun --bun svelte-kit sync && bun run gen:types || echo ''",
375
+ "check": "bun --bun svelte-kit sync && bun --bun svelte-check --tsconfig ./tsconfig.json",
376
+ "gen:types": "donkeylabs generate",
377
+ "cli": "donkeylabs",
378
+ "test": "bun test",
379
+ ...(options.deployment === "docker" && {
380
+ "docker:build": "docker-compose build",
381
+ "docker:up": "docker-compose up -d",
382
+ "docker:down": "docker-compose down",
383
+ }),
384
+ } : {
385
+ "dev": "bun --watch run src/server/index.ts",
386
+ "build": "bun build src/server/index.ts --outdir=dist",
387
+ "start": "bun run dist/index.js",
388
+ "gen:types": "bunx donkeylabs generate",
389
+ "test": "bun test",
390
+ "lint": "bun --bun tsc --noEmit",
391
+ ...(options.deployment === "docker" && {
392
+ "docker:build": "docker-compose build",
393
+ "docker:up": "docker-compose up -d",
394
+ "docker:down": "docker-compose down",
395
+ }),
396
+ },
397
+ dependencies: {
398
+ "@donkeylabs/server": getPackagePath("@donkeylabs/server"),
399
+ "zod": "^3.24.0",
400
+ "kysely": "^0.27.6",
401
+ ...(options.database === "sqlite" && { "kysely-bun-sqlite": "^0.3.2" }),
402
+ ...(options.database === "postgres" && { "pg": "^8.11.0" }),
403
+ ...(options.database === "mysql" && { "mysql2": "^3.6.0" }),
404
+ ...(options.enableStorage && { "@aws-sdk/client-s3": "^3.450.0" }),
405
+ ...(isSvelteKit && {
406
+ "@donkeylabs/adapter-sveltekit": getPackagePath("@donkeylabs/adapter-sveltekit"),
407
+ "@donkeylabs/cli": getPackagePath("@donkeylabs/cli"),
408
+ "clsx": "^2.1.1",
409
+ "tailwind-merge": "^3.4.0",
410
+ "tailwind-variants": "^3.2.2",
411
+ }),
412
+ },
413
+ devDependencies: {
414
+ "@types/bun": "latest",
415
+ "typescript": "^5.9.3",
416
+ ...(isSvelteKit && {
417
+ "@sveltejs/kit": "^2.49.1",
418
+ "@sveltejs/vite-plugin-svelte": "^6.2.1",
419
+ "@tailwindcss/vite": "^4.1.18",
420
+ "svelte": "^5.45.6",
421
+ "svelte-check": "^4.3.4",
422
+ "tailwindcss": "^4.1.18",
423
+ "vite": "^7.2.6",
424
+ }),
425
+ },
426
+ };
427
+
428
+ writeFileSync(
429
+ join(projectDir, "package.json"),
430
+ JSON.stringify(pkg, null, 2)
431
+ );
432
+ }
433
+
434
+ function createServerIndex(projectDir: string, options: InitOptions) {
435
+ const isSvelteKit = options.frontend === "sveltekit";
436
+ const hasUsers = options.plugins.includes("users");
437
+ const hasAuth = options.plugins.includes("auth");
438
+ const hasBackup = options.plugins.includes("backup");
439
+
440
+ let content = `import { AppServer, type LogLevel } from "@donkeylabs/server";
441
+ import { db } from "./db";
442
+
443
+ // Global type declaration for hot reload guard
444
+ declare global {
445
+ var __donkeylabsServerStarted__: boolean | undefined;
446
+ }
447
+
448
+ const PORT = parseInt(process.env.PORT || "3000");
449
+ `;
450
+
451
+ // Import plugins
452
+ if (options.plugins.length > 0) {
453
+ content += `
454
+ // Plugins
455
+ `;
456
+ for (const plugin of options.plugins) {
457
+ content += `import { ${plugin}Plugin } from "./plugins/${plugin}";\n`;
458
+ }
459
+ }
460
+
461
+ // Import routes
462
+ content += `
463
+ // Routes
464
+ import { apiRouter } from "./routes/api";
465
+ `;
466
+
467
+ content += `
468
+ const server = new AppServer({
469
+ port: PORT,
470
+ db,
471
+
472
+ // Production logging
473
+ logger: {
474
+ level: (process.env.LOG_LEVEL as LogLevel) || "info",
475
+ format: process.env.NODE_ENV === "production" ? "json" : "pretty",
476
+ },
477
+
478
+ // Enable admin dashboard in development
479
+ admin: process.env.NODE_ENV !== "production" ? { enabled: true } : undefined,
480
+
481
+ // Cache
482
+ cache: {
483
+ defaultTtlMs: 300000,
484
+ maxSize: 10000,
485
+ },
486
+ });
487
+
488
+ // Register plugins
489
+ `;
490
+
491
+ // Register plugins with config
492
+ for (const plugin of options.plugins) {
493
+ if (plugin === "backup") {
494
+ content += `server.registerPlugin(${plugin}Plugin({
495
+ adapter: "litestream",
496
+ adapterConfig: {
497
+ url: process.env.BACKUP_S3_URL || "s3://my-backup-bucket/db",
498
+ accessKeyId: process.env.BACKUP_ACCESS_KEY || "",
499
+ secretAccessKey: process.env.BACKUP_SECRET_KEY || "",
500
+ region: process.env.BACKUP_REGION || "us-east-1",
501
+ },
502
+ schedule: "0 2 * * *", // Daily at 2 AM
503
+ retentionCount: 7,
504
+ }));\n`;
505
+ } else if (plugin === "storage") {
506
+ content += `server.registerPlugin(${plugin}Plugin({
507
+ adapter: process.env.STORAGE_ADAPTER || "local",
508
+ localConfig: {
509
+ uploadDir: process.env.UPLOAD_DIR || "./uploads",
510
+ },
511
+ s3Config: {
512
+ bucket: process.env.S3_BUCKET || "",
513
+ region: process.env.S3_REGION || "us-east-1",
514
+ accessKeyId: process.env.S3_ACCESS_KEY || "",
515
+ secretAccessKey: process.env.S3_SECRET_KEY || "",
516
+ },
517
+ }));\n`;
518
+ } else {
519
+ content += `server.registerPlugin(${plugin}Plugin);\n`;
520
+ }
521
+ }
522
+
523
+ content += `
524
+ // Register routes
525
+ server.use(apiRouter);
526
+
527
+ // Health check
528
+ server.onReady((ctx) => {
529
+ ctx.core.logger.info("Server ready", {
530
+ port: PORT,
531
+ plugins: [${options.plugins.map(p => `"${p}"`).join(", ")}],
532
+ });
533
+ });
534
+
535
+ // Graceful shutdown
536
+ process.on("SIGTERM", async () => {
537
+ await server.shutdown();
538
+ process.exit(0);
539
+ });
540
+
541
+ process.on("SIGINT", async () => {
542
+ await server.shutdown();
543
+ process.exit(0);
544
+ });
545
+
546
+ // Export server for adapter
547
+ export { server };
548
+
549
+ // Guard against re-initialization on hot reload
550
+ if (!globalThis.__donkeylabsServerStarted__) {
551
+ globalThis.__donkeylabsServerStarted__ = true;
552
+ await server.start();
553
+ console.log(\`🚀 Server running at http://localhost:\${PORT}\`);
554
+ }
555
+ `;
556
+
557
+ writeFileSync(join(projectDir, "src", "server", "index.ts"), content);
558
+ }
559
+
560
+ function createDatabaseConfig(projectDir: string, options: InitOptions) {
561
+ let content = `import { Kysely } from "kysely";
562
+ `;
563
+
564
+ if (options.database === "sqlite") {
565
+ content += `import { SqliteDialect } from "kysely";
566
+ import { Database as BunDatabase } from "bun:sqlite";
567
+ import { mkdirSync } from "fs";
568
+ import { dirname } from "path";
569
+
570
+ const dbPath = process.env.DATABASE_URL || "./data/app.db";
571
+
572
+ // Ensure directory exists
573
+ mkdirSync(dirname(dbPath), { recursive: true });
574
+
575
+ export const db = new Kysely<any>({
576
+ dialect: new SqliteDialect({
577
+ database: new BunDatabase(dbPath),
578
+ }),
579
+ });
580
+ `;
581
+ } else if (options.database === "postgres") {
582
+ content += `import { PostgresDialect } from "kysely";
583
+ import { Pool } from "pg";
584
+
585
+ export const db = new Kysely<any>({
586
+ dialect: new PostgresDialect({
587
+ pool: new Pool({
588
+ connectionString: process.env.DATABASE_URL,
589
+ max: 20,
590
+ }),
591
+ }),
592
+ });
593
+ `;
594
+ } else if (options.database === "mysql") {
595
+ content += `import { MysqlDialect } from "kysely";
596
+ import { createPool } from "mysql2";
597
+
598
+ export const db = new Kysely<any>({
599
+ dialect: new MysqlDialect({
600
+ pool: createPool({
601
+ uri: process.env.DATABASE_URL,
602
+ connectionLimit: 20,
603
+ }),
604
+ }),
605
+ });
606
+ `;
607
+ }
608
+
609
+ writeFileSync(join(projectDir, "src", "server", "db.ts"), content);
610
+ }
611
+
612
+ // Copy directory recursively
613
+ function copyDirRecursive(src: string, dest: string) {
614
+ mkdirSync(dest, { recursive: true });
615
+ const entries = readdirSync(src, { withFileTypes: true });
616
+
617
+ for (const entry of entries) {
618
+ const srcPath = join(src, entry.name);
619
+ const destPath = join(dest, entry.name);
620
+
621
+ if (entry.isDirectory()) {
622
+ copyDirRecursive(srcPath, destPath);
623
+ } else {
624
+ const content = readFileSync(srcPath);
625
+ writeFileSync(destPath, content);
626
+ }
627
+ }
628
+ }
629
+
630
+ // Create other files...
631
+ async function createPlugin(projectDir: string, pluginName: string, options: InitOptions) {
632
+ const cliPluginsDir = "/Users/franciscosainzwilliams/Documents/GitHub/server/packages/cli/plugins";
633
+ const sourcePluginDir = join(cliPluginsDir, pluginName);
634
+ const targetPluginDir = join(projectDir, "src", "server", "plugins", pluginName);
635
+
636
+ // Check if plugin exists in CLI plugins directory
637
+ if (existsSync(sourcePluginDir)) {
638
+ // Copy entire plugin directory recursively
639
+ copyDirRecursive(sourcePluginDir, targetPluginDir);
640
+ return;
641
+ }
642
+
643
+ // Fall back to template generation
644
+ const pluginTemplates: Record<string, string> = {
645
+ users: createUsersPluginTemplate(),
646
+ auth: createAuthPluginTemplate(),
647
+ backup: createBackupPluginTemplate(),
648
+ storage: createStoragePluginTemplate(),
649
+ email: createEmailPluginTemplate(),
650
+ audit: createAuditPluginTemplate(),
651
+ };
652
+
653
+ const content = pluginTemplates[pluginName] || createGenericPluginTemplate(pluginName);
654
+
655
+ mkdirSync(targetPluginDir, { recursive: true });
656
+ writeFileSync(join(targetPluginDir, "index.ts"), content);
657
+
658
+ // Create migrations if needed
659
+ if (["users", "auth", "storage", "audit"].includes(pluginName)) {
660
+ mkdirSync(join(targetPluginDir, "migrations"), { recursive: true });
661
+ }
662
+ }
663
+
664
+ function createUsersPluginTemplate(): string {
665
+ return `import { createPlugin } from "@donkeylabs/server";
666
+
667
+ export interface User {
668
+ id: string;
669
+ email: string;
670
+ name: string;
671
+ createdAt: string;
672
+ }
673
+
674
+ export const usersPlugin = createPlugin
675
+ .withSchema<{ users: User }>()
676
+ .define({
677
+ name: "users",
678
+ service: async (ctx) => ({
679
+ async getById(id: string) {
680
+ return ctx.db
681
+ .selectFrom("users")
682
+ .where("id", "=", id)
683
+ .selectAll()
684
+ .executeTakeFirst();
685
+ },
686
+
687
+ async getByEmail(email: string) {
688
+ return ctx.db
689
+ .selectFrom("users")
690
+ .where("email", "=", email)
691
+ .selectAll()
692
+ .executeTakeFirst();
693
+ },
694
+
695
+ async create(data: Omit<User, "id" | "createdAt">) {
696
+ const id = crypto.randomUUID();
697
+ return ctx.db
698
+ .insertInto("users")
699
+ .values({
700
+ id,
701
+ email: data.email,
702
+ name: data.name,
703
+ createdAt: new Date().toISOString(),
704
+ })
705
+ .returningAll()
706
+ .executeTakeFirstOrThrow();
707
+ },
708
+
709
+ async list() {
710
+ return ctx.db
711
+ .selectFrom("users")
712
+ .selectAll()
713
+ .execute();
714
+ },
715
+
716
+ async update(id: string, data: Partial<Omit<User, "id" | "createdAt">>) {
717
+ return ctx.db
718
+ .updateTable("users")
719
+ .set(data)
720
+ .where("id", "=", id)
721
+ .returningAll()
722
+ .executeTakeFirstOrThrow();
723
+ },
724
+
725
+ async delete(id: string) {
726
+ await ctx.db
727
+ .deleteFrom("users")
728
+ .where("id", "=", id)
729
+ .execute();
730
+ },
731
+ }),
732
+ });
733
+ `;
734
+ }
735
+
736
+ function createAuthPluginTemplate(): string {
737
+ return `import { createPlugin } from "@donkeylabs/server";
738
+ import { sign, verify } from "jsonwebtoken";
739
+
740
+ export interface AuthConfig {
741
+ jwtSecret: string;
742
+ tokenExpiry?: string;
743
+ }
744
+
745
+ export const authPlugin = createPlugin
746
+ .withConfig<AuthConfig>()
747
+ .define({
748
+ name: "auth",
749
+ dependencies: ["users"] as const,
750
+ service: async (ctx) => {
751
+ const jwtSecret = ctx.config.jwtSecret;
752
+
753
+ return {
754
+ async createToken(userId: string, email: string) {
755
+ return sign(
756
+ { userId, email },
757
+ jwtSecret,
758
+ { expiresIn: ctx.config.tokenExpiry || "7d" }
759
+ );
760
+ },
761
+
762
+ async verifyToken(token: string) {
763
+ try {
764
+ return verify(token, jwtSecret) as { userId: string; email: string };
765
+ } catch {
766
+ return null;
767
+ }
768
+ },
769
+
770
+ async authenticate(email: string, password: string) {
771
+ const user = await ctx.deps.users.getByEmail(email);
772
+ if (!user) return null;
773
+
774
+ // TODO: Add password hashing comparison
775
+ // For now, this is a placeholder
776
+ if (password === "password") {
777
+ const token = await this.createToken(user.id, user.email);
778
+ return { user, token };
779
+ }
780
+
781
+ return null;
782
+ },
783
+
784
+ middleware: (ctx, service) => ({
785
+ authRequired: async (req, reqCtx, next) => {
786
+ const authHeader = req.headers.get("Authorization");
787
+ if (!authHeader?.startsWith("Bearer ")) {
788
+ return Response.json({ error: "Unauthorized" }, { status: 401 });
789
+ }
790
+
791
+ const token = authHeader.replace("Bearer ", "");
792
+ const payload = await service.verifyToken(token);
793
+
794
+ if (!payload) {
795
+ return Response.json({ error: "Invalid token" }, { status: 401 });
796
+ }
797
+
798
+ reqCtx.user = payload;
799
+ return next();
800
+ },
801
+ }),
802
+ };
803
+ },
804
+ });
805
+ `;
806
+ }
807
+
808
+ function createBackupPluginTemplate(): string {
809
+ return `import { createPlugin } from "@donkeylabs/server";
810
+ import { execSync, spawn } from "child_process";
811
+ import { createReadStream, createWriteStream } from "fs";
812
+ import { mkdir, stat, readdir, unlink } from "fs/promises";
813
+ import { join } from "path";
814
+ import { pipeline } from "stream/promises";
815
+ import { createGzip } from "zlib";
816
+
817
+ export interface BackupConfig {
818
+ adapter: "s3" | "local";
819
+ schedule?: string;
820
+ retentionCount?: number;
821
+ s3Config?: {
822
+ bucket: string;
823
+ region: string;
824
+ accessKeyId: string;
825
+ secretAccessKey: string;
826
+ endpoint?: string;
827
+ };
828
+ localConfig?: {
829
+ backupDir: string;
830
+ };
831
+ }
832
+
833
+ export interface BackupInfo {
834
+ id: string;
835
+ timestamp: Date;
836
+ size: number;
837
+ type: "full";
838
+ status: "complete" | "in_progress" | "failed";
839
+ location: string;
840
+ }
841
+
842
+ export const backupPlugin = createPlugin
843
+ .withConfig<BackupConfig>()
844
+ .define({
845
+ name: "backup",
846
+ service: async (ctx) => {
847
+ const config = ctx.config;
848
+
849
+ // Schedule automatic backups if configured
850
+ if (config.schedule) {
851
+ ctx.core.cron.schedule(config.schedule, async () => {
852
+ ctx.core.logger.info("Running scheduled backup");
853
+ try {
854
+ await service.backup();
855
+ ctx.core.logger.info("Scheduled backup completed");
856
+ } catch (error) {
857
+ ctx.core.logger.error("Scheduled backup failed", { error });
858
+ }
859
+ }, { name: "backup-job" });
860
+ }
861
+
862
+ const service = {
863
+ /** Perform a manual backup */
864
+ async backup(): Promise<BackupInfo> {
865
+ ctx.core.logger.info("Backup requested");
866
+
867
+ const timestamp = new Date();
868
+ const backupId = \`backup-\${timestamp.toISOString().replace(/[:.]/g, "-")}\`;
869
+
870
+ // TODO: Implement backup logic based on config.adapter
871
+ // For now, return placeholder
872
+
873
+ const info: BackupInfo = {
874
+ id: backupId,
875
+ timestamp,
876
+ size: 0,
877
+ type: "full",
878
+ status: "complete",
879
+ location: config.adapter === "s3"
880
+ ? \`s3://\${config.s3Config?.bucket}/backups/\${backupId}.sql.gz\`
881
+ : join(config.localConfig?.backupDir || "./backups", \`\${backupId}.db.gz\`),
882
+ };
883
+
884
+ // Clean up old backups if retention is configured
885
+ if (config.retentionCount) {
886
+ await service.cleanupOldBackups(config.retentionCount);
887
+ }
888
+
889
+ return info;
890
+ },
891
+
892
+ /** List available backups */
893
+ async listBackups(): Promise<BackupInfo[]> {
894
+ // TODO: List backups from S3 or local
895
+ return [];
896
+ },
897
+
898
+ /** Clean up old backups */
899
+ async cleanupOldBackups(retainCount: number): Promise<void> {
900
+ const backups = await service.listBackups();
901
+ const sorted = backups.sort((a, b) =>
902
+ b.timestamp.getTime() - a.timestamp.getTime()
903
+ );
904
+
905
+ const toDelete = sorted.slice(retainCount);
906
+ for (const backup of toDelete) {
907
+ ctx.core.logger.info(\`Deleting old backup: \${backup.id}\`);
908
+ }
909
+ },
910
+ };
911
+
912
+ return service;
913
+ },
914
+ });
915
+ `;
916
+ }
917
+
918
+ function createStoragePluginTemplate(): string {
919
+ return `import { createPlugin } from "@donkeylabs/server";
920
+
921
+ export interface StorageConfig {
922
+ adapter: "local" | "s3";
923
+ localConfig?: { uploadDir: string };
924
+ s3Config?: {
925
+ bucket: string;
926
+ region: string;
927
+ accessKeyId: string;
928
+ secretAccessKey: string;
929
+ };
930
+ }
931
+
932
+ export const storagePlugin = createPlugin
933
+ .withConfig<StorageConfig>()
934
+ .define({
935
+ name: "storage",
936
+ service: async (ctx) => {
937
+ const config = ctx.config;
938
+
939
+ return {
940
+ async upload(file: File, key: string) {
941
+ if (config.adapter === "local") {
942
+ const { writeFileSync } = await import("fs");
943
+ const { join } = await import("path");
944
+ const buffer = await file.arrayBuffer();
945
+ const path = join(config.localConfig!.uploadDir, key);
946
+ writeFileSync(path, new Uint8Array(buffer));
947
+ return { key, url: \`/uploads/\${key}\` };
948
+ }
949
+
950
+ // S3 upload
951
+ // Implementation here...
952
+ return { key, url: \`https://\${config.s3Config!.bucket}.s3.amazonaws.com/\${key}\` };
953
+ },
954
+
955
+ async getUrl(key: string) {
956
+ if (config.adapter === "local") {
957
+ return \`/uploads/\${key}\`;
958
+ }
959
+ return \`https://\${config.s3Config!.bucket}.s3.amazonaws.com/\${key}\`;
960
+ },
961
+ };
962
+ },
963
+ });
964
+ `;
965
+ }
966
+
967
+ function createEmailPluginTemplate(): string {
968
+ return `import { createPlugin } from "@donkeylabs/server";
969
+
970
+ export interface EmailConfig {
971
+ smtp: {
972
+ host: string;
973
+ port: number;
974
+ user: string;
975
+ pass: string;
976
+ };
977
+ from: string;
978
+ }
979
+
980
+ export const emailPlugin = createPlugin
981
+ .withConfig<EmailConfig>()
982
+ .define({
983
+ name: "email",
984
+ service: async (ctx) => ({
985
+ async send(to: string, subject: string, body: string, html?: string) {
986
+ // SMTP implementation
987
+ ctx.core.logger.info("Sending email", { to, subject });
988
+
989
+ // TODO: Implement actual SMTP sending
990
+ // You can use nodemailer or similar
991
+
992
+ return { messageId: crypto.randomUUID() };
993
+ },
994
+
995
+ async sendTemplate(to: string, template: string, data: Record<string, string>) {
996
+ // Template email sending
997
+ return this.send(to, "Subject", "Body");
998
+ },
999
+ }),
1000
+ });
1001
+ `;
1002
+ }
1003
+
1004
+ function createCronPluginTemplate(): string {
1005
+ return `import { createPlugin } from "@donkeylabs/server";
1006
+
1007
+ export const cronPlugin = createPlugin.define({
1008
+ name: "cron",
1009
+ service: async (ctx) => {
1010
+ // Example: Schedule a daily cleanup job
1011
+ ctx.core.cron.schedule("0 0 * * *", async () => {
1012
+ ctx.core.logger.info("Running daily cleanup job");
1013
+ // Cleanup logic here
1014
+ }, { name: "daily-cleanup" });
1015
+
1016
+ return {
1017
+ async schedule(cronExpression: string, job: () => Promise<void>, options?: { name?: string }) {
1018
+ ctx.core.cron.schedule(cronExpression, job, options);
1019
+ },
1020
+ };
1021
+ },
1022
+ });
1023
+ `;
1024
+ }
1025
+
1026
+ function createAuditPluginTemplate(): string {
1027
+ return `import { createPlugin } from "@donkeylabs/server";
1028
+
1029
+ export interface AuditEvent {
1030
+ id: string;
1031
+ action: string;
1032
+ userId?: string;
1033
+ resource: string;
1034
+ resourceId: string;
1035
+ changes?: Record<string, any>;
1036
+ timestamp: string;
1037
+ }
1038
+
1039
+ export const auditPlugin = createPlugin
1040
+ .withSchema<{ audit_log: AuditEvent }>()
1041
+ .define({
1042
+ name: "audit",
1043
+ service: async (ctx) => ({
1044
+ async log(action: string, resource: string, resourceId: string, userId?: string, changes?: Record<string, any>) {
1045
+ await ctx.db
1046
+ .insertInto("audit_log")
1047
+ .values({
1048
+ id: crypto.randomUUID(),
1049
+ action,
1050
+ userId,
1051
+ resource,
1052
+ resourceId,
1053
+ changes: changes ? JSON.stringify(changes) : null,
1054
+ timestamp: new Date().toISOString(),
1055
+ })
1056
+ .execute();
1057
+ },
1058
+
1059
+ async getHistory(resource: string, resourceId: string) {
1060
+ return ctx.db
1061
+ .selectFrom("audit_log")
1062
+ .where("resource", "=", resource)
1063
+ .where("resourceId", "=", resourceId)
1064
+ .orderBy("timestamp", "desc")
1065
+ .selectAll()
1066
+ .execute();
1067
+ },
1068
+ }),
1069
+ });
1070
+ `;
1071
+ }
1072
+
1073
+ function createGenericPluginTemplate(name: string): string {
1074
+ return `import { createPlugin } from "@donkeylabs/server";
1075
+
1076
+ export const ${name}Plugin = createPlugin.define({
1077
+ name: "${name}",
1078
+ service: async (ctx) => ({
1079
+ // Add your service methods here
1080
+ async doSomething() {
1081
+ return { success: true };
1082
+ },
1083
+ }),
1084
+ });
1085
+ `;
1086
+ }
1087
+
1088
+ // ... continue with other functions
1089
+
1090
+ function createRoutes(projectDir: string, options: InitOptions) {
1091
+ const hasUsers = options.plugins.includes("users");
1092
+
1093
+ let routesContent = `import { createRouter, defineRoute } from "@donkeylabs/server";
1094
+ import { z } from "zod";
1095
+
1096
+ export const apiRouter = createRouter("api", {
1097
+ // Plugins are available via ctx.plugins
1098
+ });
1099
+
1100
+ // Health check - GET /api.health (raw handler to accept GET requests)
1101
+ apiRouter.route("health").raw(async (req, ctx) => {
1102
+ return Response.json({
1103
+ status: "healthy",
1104
+ timestamp: new Date().toISOString(),
1105
+ uptime: process.uptime(),
1106
+ });
1107
+ });
1108
+ `;
1109
+
1110
+ // Add users routes if plugin is included
1111
+ if (hasUsers) {
1112
+ routesContent += `
1113
+ // Users routes - requires users plugin
1114
+ apiRouter.route("users.list").typed(defineRoute({
1115
+ output: z.array(z.object({
1116
+ id: z.string(),
1117
+ email: z.string(),
1118
+ name: z.string(),
1119
+ })),
1120
+ handle: async (_, ctx) => {
1121
+ return ctx.plugins.users.list();
1122
+ },
1123
+ }));
1124
+ `;
1125
+ }
1126
+
1127
+ const content = routesContent;
1128
+
1129
+ writeFileSync(join(projectDir, "src", "server", "routes", "api.ts"), content);
1130
+ }
1131
+
1132
+ // Continue with other helper functions...
1133
+ async function createSvelteKitFrontend(projectDir: string, options: InitOptions) {
1134
+ // Create SvelteKit app structure
1135
+ const content = {
1136
+ "src/app.html": `<!doctype html>
1137
+ <html lang="en">
1138
+ <head>
1139
+ <meta charset="utf-8" />
1140
+ <link rel="icon" href="%sveltekit.assets%/favicon.png" />
1141
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
1142
+ %sveltekit.head%
1143
+ </head>
1144
+ <body data-sveltekit-preload-data="hover">
1145
+ <div style="display: contents">%sveltekit.body%</div>
1146
+ </body>
1147
+ </html>
1148
+ `,
1149
+ "src/routes/+layout.svelte": `<script>
1150
+ import '../app.css';
1151
+ let { children } = $props();
1152
+ </script>
1153
+
1154
+ {@render children()}
1155
+ `,
1156
+ "src/routes/+page.svelte": `<script lang="ts">
1157
+ import { onMount } from 'svelte';
1158
+
1159
+ let health = $state<{ status: string; timestamp?: string; uptime?: number } | null>(null);
1160
+ let error = $state<string | null>(null);
1161
+ let loading = $state(true);
1162
+
1163
+ onMount(async () => {
1164
+ try {
1165
+ const res = await fetch('/api.health');
1166
+ if (!res.ok) throw new Error(\`HTTP \${res.status}\`);
1167
+ health = await res.json();
1168
+ } catch (e: any) {
1169
+ error = e.message;
1170
+ } finally {
1171
+ loading = false;
1172
+ }
1173
+ });
1174
+ </script>
1175
+
1176
+ <svelte:head>
1177
+ <title>${options.projectName}</title>
1178
+ </svelte:head>
1179
+
1180
+ <div class="min-h-screen bg-gray-50">
1181
+ <div class="container mx-auto max-w-4xl py-16 px-4">
1182
+ <div class="text-center mb-12">
1183
+ <h1 class="text-4xl font-bold tracking-tight text-gray-900">${options.projectName}</h1>
1184
+ <p class="text-gray-600 mt-2 text-lg">Built with DonkeyLabs</p>
1185
+ </div>
1186
+
1187
+ <div class="bg-white rounded-xl shadow-sm border p-6 mb-6">
1188
+ <h2 class="text-xl font-semibold mb-4">Server Status</h2>
1189
+ {#if loading}
1190
+ <div class="text-gray-500">Checking server...</div>
1191
+ {:else if error}
1192
+ <div class="flex items-center gap-2 text-red-600">
1193
+ <span class="w-3 h-3 bg-red-500 rounded-full"></span>
1194
+ Error: {error}
1195
+ </div>
1196
+ {:else if health}
1197
+ <div class="flex items-center gap-2 text-green-600">
1198
+ <span class="w-3 h-3 bg-green-500 rounded-full"></span>
1199
+ {health.status}
1200
+ </div>
1201
+ {#if health.timestamp}
1202
+ <p class="text-gray-500 text-sm mt-2">Last checked: {new Date(health.timestamp).toLocaleString()}</p>
1203
+ {/if}
1204
+ {#if health.uptime}
1205
+ <p class="text-gray-500 text-sm">Uptime: {Math.floor(health.uptime)}s</p>
1206
+ {/if}
1207
+ {/if}
1208
+ </div>
1209
+
1210
+ <div class="bg-white rounded-xl shadow-sm border p-6 mb-6">
1211
+ <h2 class="text-xl font-semibold mb-4">Getting Started</h2>
1212
+ <ol class="list-decimal list-inside space-y-2 text-gray-700">
1213
+ <li>Edit <code class="bg-gray-100 px-2 py-0.5 rounded text-sm">src/server/routes/api.ts</code> to add API routes</li>
1214
+ <li>Edit <code class="bg-gray-100 px-2 py-0.5 rounded text-sm">src/routes/+page.svelte</code> to customize this page</li>
1215
+ <li>Run <code class="bg-gray-100 px-2 py-0.5 rounded text-sm">bun run gen:types</code> to generate typed API client</li>
1216
+ </ol>
1217
+ </div>
1218
+
1219
+ <div class="bg-white rounded-xl shadow-sm border p-6">
1220
+ <h2 class="text-xl font-semibold mb-4">Project Info</h2>
1221
+ <ul class="space-y-2 text-gray-700">
1222
+ <li><strong>Database:</strong> ${options.database}</li>
1223
+ <li><strong>Plugins:</strong> ${options.plugins.join(", ") || "None"}</li>
1224
+ <li><strong>Deployment:</strong> ${options.deployment}</li>
1225
+ </ul>
1226
+ </div>
1227
+ </div>
1228
+ </div>
1229
+ `,
1230
+ "src/app.css": `@import "tailwindcss";
1231
+
1232
+ /* Your global styles here */
1233
+ `,
1234
+ "vite.config.ts": `import { sveltekit } from '@sveltejs/kit/vite';
1235
+ import tailwindcss from '@tailwindcss/vite';
1236
+ import { defineConfig } from 'vite';
1237
+ import { donkeylabsDev } from '@donkeylabs/adapter-sveltekit/vite';
1238
+
1239
+ export default defineConfig({
1240
+ plugins: [donkeylabsDev(), tailwindcss(), sveltekit()],
1241
+ ssr: {
1242
+ // Bundle @donkeylabs packages in SSR so TypeScript files get transpiled
1243
+ noExternal: ['@donkeylabs/adapter-sveltekit', '@donkeylabs/server'],
1244
+ },
1245
+ });
1246
+ `,
1247
+ "svelte.config.ts": `import adapter from '@donkeylabs/adapter-sveltekit';
1248
+ import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
1249
+
1250
+ import type { Config } from '@sveltejs/kit';
1251
+
1252
+ const config: Config = {
1253
+ preprocess: vitePreprocess(),
1254
+
1255
+ kit: {
1256
+ adapter: adapter(),
1257
+ alias: {
1258
+ $server: '.@donkeylabs/server',
1259
+ }
1260
+ }
1261
+ };
1262
+
1263
+ export default config;
1264
+ `,
1265
+ "src/hooks.server.ts": `import { createHandle } from "@donkeylabs/adapter-sveltekit/hooks";
1266
+
1267
+ export const handle = createHandle();
1268
+ `,
1269
+ };
1270
+
1271
+ for (const [filePath, content_str] of Object.entries(content)) {
1272
+ const fullPath = join(projectDir, filePath);
1273
+ mkdirSync(dirname(fullPath), { recursive: true });
1274
+ writeFileSync(fullPath, content_str);
1275
+ }
1276
+ }
1277
+
1278
+ function createDemoContent(projectDir: string, options: InitOptions) {
1279
+ const isSvelteKit = options.frontend === "sveltekit";
1280
+
1281
+ // For SvelteKit, the base +page.svelte already includes demo content with Tailwind
1282
+ // This function can be used to add additional demo files if needed
1283
+ if (isSvelteKit) {
1284
+ // Add a +page.server.ts for SSR demo
1285
+ const pageServer = `import type { PageServerLoad } from './$types';
1286
+
1287
+ export const load: PageServerLoad = async ({ locals }) => {
1288
+ return {
1289
+ isSSR: true,
1290
+ loadedAt: new Date().toISOString(),
1291
+ };
1292
+ };
1293
+ `;
1294
+ writeFileSync(join(projectDir, "src/routes/+page.server.ts"), pageServer);
1295
+ return;
1296
+ }
1297
+
1298
+ // For API-only projects, no demo content needed
1299
+ }
1300
+
1301
+ function createDeploymentFiles(projectDir: string, options: InitOptions) {
1302
+ if (options.deployment === "docker") {
1303
+ createDockerFiles(projectDir, options);
1304
+ } else if (options.deployment === "pm2") {
1305
+ createPM2Files(projectDir, options);
1306
+ } else if (options.deployment === "vercel") {
1307
+ createVercelFiles(projectDir, options);
1308
+ } else if (options.deployment === "cloudflare") {
1309
+ createCloudflareFiles(projectDir, options);
1310
+ } else if (options.deployment === "aws") {
1311
+ createAWSFiles(projectDir, options);
1312
+ }
1313
+
1314
+ // Create MCP configuration for all deployment types
1315
+ createMCPConfig(projectDir, options);
1316
+ }
1317
+
1318
+ function createDockerFiles(projectDir: string, options: InitOptions) {
1319
+ const isSvelteKit = options.frontend === "sveltekit";
1320
+
1321
+ // Dockerfile
1322
+ const dockerfile = `# Build stage
1323
+ FROM oven/bun:1-alpine AS builder
1324
+ WORKDIR /app
1325
+
1326
+ COPY package.json bun.lockb ./
1327
+ RUN bun install --frozen-lockfile
1328
+
1329
+ COPY . .
1330
+ RUN bun run build
1331
+
1332
+ # Production stage
1333
+ FROM oven/bun:1-alpine
1334
+ WORKDIR /app
1335
+
1336
+ # Create non-root user
1337
+ RUN addgroup -g 1001 -S nodejs
1338
+ RUN adduser -S bunuser -u 1001
1339
+
1340
+ ${options.database === "sqlite" ? `# Create data directory for SQLite
1341
+ RUN mkdir -p /data && chown bunuser:nodejs /data
1342
+ ` : ""}
1343
+
1344
+ # Copy built app
1345
+ COPY --from=builder --chown=bunuser:nodejs /app/dist ./dist
1346
+ COPY --from=builder --chown=bunuser:nodejs /app/node_modules ./node_modules
1347
+ COPY --from=builder --chown=bunuser:nodejs /app/package.json ./package.json
1348
+
1349
+ # Switch to non-root user
1350
+ USER bunuser
1351
+
1352
+ EXPOSE 3000
1353
+
1354
+ ENV NODE_ENV=production
1355
+ ENV PORT=3000
1356
+ ${options.database === "sqlite" ? "ENV DATABASE_URL=/data/app.db" : ""}
1357
+
1358
+ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \\
1359
+ CMD curl -f http://localhost:3000/api.health || exit 1
1360
+
1361
+ CMD ["bun", "run", "start"]
1362
+ `;
1363
+
1364
+ writeFileSync(join(projectDir, "Dockerfile"), dockerfile);
1365
+
1366
+ // docker-compose.yml
1367
+ const compose = `services:
1368
+ app:
1369
+ build:
1370
+ context: .
1371
+ dockerfile: Dockerfile
1372
+ ports:
1373
+ - "3000:3000"
1374
+ environment:
1375
+ - NODE_ENV=production
1376
+ ${options.database === "sqlite" ? ` volumes:
1377
+ - sqlite_data:/data
1378
+ ` : options.database === "postgres" ? ` - DATABASE_URL=postgresql://postgres:postgres@db:5432/app
1379
+ depends_on:
1380
+ - db
1381
+
1382
+ db:
1383
+ image: postgres:15-alpine
1384
+ environment:
1385
+ - POSTGRES_USER=postgres
1386
+ - POSTGRES_PASSWORD=postgres
1387
+ - POSTGRES_DB=app
1388
+ volumes:
1389
+ - postgres_data:/var/lib/postgresql/data
1390
+ healthcheck:
1391
+ test: ["CMD-SHELL", "pg_isready -U postgres -d app"]
1392
+ interval: 5s
1393
+ timeout: 5s
1394
+ retries: 5
1395
+ restart: unless-stopped
1396
+ ` : ""}
1397
+ restart: unless-stopped
1398
+ ${options.enableBackup ? `
1399
+ # Litestream for SQLite backups (installed in app container)
1400
+ # Backup is handled by the backup plugin inside the app container
1401
+ # See: https://litestream.io/install/debian/
1402
+ ` : ""}
1403
+
1404
+ volumes:
1405
+ ${options.database === "sqlite" ? " sqlite_data:" : options.database === "postgres" ? " postgres_data:" : ""}
1406
+ `;
1407
+
1408
+ writeFileSync(join(projectDir, "docker-compose.yml"), compose);
1409
+
1410
+ // .dockerignore
1411
+ writeFileSync(join(projectDir, ".dockerignore"), `node_modules
1412
+ .git
1413
+ .env
1414
+ *.md
1415
+ dist
1416
+ .DS_Store
1417
+ `);
1418
+ }
1419
+
1420
+ function createPM2Files(projectDir: string, options: InitOptions) {
1421
+ const pm2Config = `module.exports = {
1422
+ apps: [{
1423
+ name: '${options.projectName}',
1424
+ script: './dist/index.js',
1425
+ instances: 1,
1426
+ exec_mode: 'fork',
1427
+ env: {
1428
+ NODE_ENV: 'production',
1429
+ PORT: 3000,
1430
+ },
1431
+ log_file: './logs/combined.log',
1432
+ out_file: './logs/out.log',
1433
+ error_file: './logs/error.log',
1434
+ log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
1435
+ merge_logs: true,
1436
+ time: true,
1437
+ }],
1438
+ };
1439
+ `;
1440
+
1441
+ writeFileSync(join(projectDir, "ecosystem.config.js"), pm2Config);
1442
+ mkdirSync(join(projectDir, "logs"), { recursive: true });
1443
+ }
1444
+
1445
+ function createMCPConfig(projectDir: string, options: InitOptions) {
1446
+ const mcpConfig = {
1447
+ mcpServers: {
1448
+ donkeylabs: {
1449
+ command: "bunx",
1450
+ args: ["-y", "@donkeylabs/mcp"],
1451
+ env: {
1452
+ DONKEYLABS_MCP: "true"
1453
+ }
1454
+ }
1455
+ }
1456
+ };
1457
+
1458
+ writeFileSync(
1459
+ join(projectDir, ".mcp.json"),
1460
+ JSON.stringify(mcpConfig, null, 2)
1461
+ );
1462
+ }
1463
+
1464
+ function createVercelFiles(projectDir: string, options: InitOptions) {
1465
+ // vercel.json
1466
+ const vercelConfig = {
1467
+ version: 2,
1468
+ builds: [
1469
+ {
1470
+ src: "api/index.ts",
1471
+ use: "@vercel/node"
1472
+ }
1473
+ ],
1474
+ routes: [
1475
+ {
1476
+ src: "/(.*)",
1477
+ dest: "api/index.ts"
1478
+ }
1479
+ ]
1480
+ };
1481
+
1482
+ writeFileSync(
1483
+ join(projectDir, "vercel.json"),
1484
+ JSON.stringify(vercelConfig, null, 2)
1485
+ );
1486
+
1487
+ // api/index.ts
1488
+ const apiIndex = `import { AppServer } from "@donkeylabs/server";
1489
+ import { db } from "../src/server/db";
1490
+
1491
+ const server = new AppServer({
1492
+ port: parseInt(process.env.PORT || "3000"),
1493
+ db,
1494
+ logger: {
1495
+ level: "info",
1496
+ format: "json",
1497
+ },
1498
+ });
1499
+
1500
+ export default async function handler(req: Request) {
1501
+ return server.handle(req);
1502
+ }
1503
+ `;
1504
+
1505
+ mkdirSync(join(projectDir, "api"), { recursive: true });
1506
+ writeFileSync(join(projectDir, "api", "index.ts"), apiIndex);
1507
+
1508
+ // .vercelignore
1509
+ writeFileSync(
1510
+ join(projectDir, ".vercelignore"),
1511
+ `node_modules
1512
+ .git
1513
+ .env
1514
+ *.md
1515
+ .DS_Store
1516
+ `
1517
+ );
1518
+ }
1519
+
1520
+ function createCloudflareFiles(projectDir: string, options: InitOptions) {
1521
+ // wrangler.toml
1522
+ const wranglerConfig = `name = "${options.projectName}"
1523
+ main = "src/index.ts"
1524
+ compatibility_date = "2024-01-01"
1525
+
1526
+ # D1 Database (if using SQLite on Cloudflare)
1527
+ [[d1_databases]]
1528
+ binding = "DB"
1529
+ database_name = "${options.projectName}-db"
1530
+ database_id = "your-database-id-here"
1531
+
1532
+ # Environment variables
1533
+ [vars]
1534
+ NODE_ENV = "production"
1535
+ `;
1536
+
1537
+ writeFileSync(join(projectDir, "wrangler.toml"), wranglerConfig);
1538
+
1539
+ // src/index.ts for Cloudflare Workers
1540
+ const workerIndex = `import { AppServer } from "@donkeylabs/server";
1541
+
1542
+ export interface Env {
1543
+ DB: D1Database;
1544
+ }
1545
+
1546
+ export default {
1547
+ async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
1548
+ const server = new AppServer({
1549
+ port: 3000,
1550
+ db: env.DB as any, // D1 binding
1551
+ logger: {
1552
+ level: "info",
1553
+ format: "json",
1554
+ },
1555
+ });
1556
+
1557
+ return server.handle(request);
1558
+ },
1559
+ };
1560
+ `;
1561
+
1562
+ writeFileSync(join(projectDir, "src", "index.ts"), workerIndex);
1563
+
1564
+ // .dev.vars.example
1565
+ writeFileSync(
1566
+ join(projectDir, ".dev.vars.example"),
1567
+ `NODE_ENV=development
1568
+ # Add your local development variables here
1569
+ `
1570
+ );
1571
+ }
1572
+
1573
+ function createAWSFiles(projectDir: string, options: InitOptions) {
1574
+ // template.yaml for SAM
1575
+ const templateYaml = `AWSTemplateFormatVersion: '2010-09-09'
1576
+ Transform: AWS::Serverless-2016-10-31
1577
+ Description: ${options.projectName} - DonkeyLabs Serverless Application
1578
+
1579
+ Globals:
1580
+ Function:
1581
+ Timeout: 30
1582
+ Runtime: nodejs20.x
1583
+ MemorySize: 512
1584
+
1585
+ Resources:
1586
+ ApiGatewayApi:
1587
+ Type: AWS::Serverless::Api
1588
+ Properties:
1589
+ StageName: prod
1590
+ Cors:
1591
+ AllowMethods: "'GET,POST,PUT,DELETE,OPTIONS'"
1592
+ AllowHeaders: "'Content-Type,Authorization'"
1593
+ AllowOrigin: "'*'"
1594
+
1595
+ LambdaFunction:
1596
+ Type: AWS::Serverless::Function
1597
+ Properties:
1598
+ FunctionName: ${options.projectName}
1599
+ Handler: index.handler
1600
+ CodeUri: ./dist
1601
+ Events:
1602
+ ApiEvent:
1603
+ Type: Api
1604
+ Properties:
1605
+ Path: /{proxy+}
1606
+ Method: ANY
1607
+ RestApiId: !Ref ApiGatewayApi
1608
+ Environment:
1609
+ Variables:
1610
+ NODE_ENV: production
1611
+ DATABASE_URL: !Ref DatabaseUrl
1612
+
1613
+ Parameters:
1614
+ DatabaseUrl:
1615
+ Type: String
1616
+ Description: PostgreSQL connection string
1617
+
1618
+ Outputs:
1619
+ ApiUrl:
1620
+ Description: API Gateway endpoint URL
1621
+ Value: !Sub "https://\${ApiGatewayApi}.execute-api.\${AWS::Region}.amazonaws.com/prod/"
1622
+ `;
1623
+
1624
+ writeFileSync(join(projectDir, "template.yaml"), templateYaml);
1625
+
1626
+ // samconfig.toml
1627
+ const samConfig = `version = 0.1
1628
+ [default]
1629
+ [default.global.parameters]
1630
+ stack_name = "${options.projectName}"
1631
+
1632
+ [default.build.parameters]
1633
+ cached = true
1634
+ parallel = true
1635
+
1636
+ [default.validate.parameters]
1637
+ lint = true
1638
+
1639
+ [default.deploy.parameters]
1640
+ capabilities = "CAPABILITY_IAM"
1641
+ confirm_changeset = true
1642
+ resolve_s3 = true
1643
+ region = "us-east-1"
1644
+
1645
+ [default.sync.parameters]
1646
+ watch = true
1647
+
1648
+ [default.local_start_api.parameters]
1649
+ warm_containers = EAGER
1650
+
1651
+ [default.local_start_lambda.parameters]
1652
+ warm_containers = EAGER
1653
+ `;
1654
+
1655
+ writeFileSync(join(projectDir, "samconfig.toml"), samConfig);
1656
+
1657
+ // Lambda handler
1658
+ const lambdaHandler = `import { AppServer } from "@donkeylabs/server";
1659
+ import { db } from "./server/db";
1660
+
1661
+ const server = new AppServer({
1662
+ port: 3000,
1663
+ db,
1664
+ logger: {
1665
+ level: "info",
1666
+ format: "json",
1667
+ },
1668
+ });
1669
+
1670
+ export const handler = async (event: any, context: any) => {
1671
+ // Convert Lambda event to Request
1672
+ const url = \`http://\${event.headers.Host || 'localhost'}\${event.path}\`;
1673
+ const request = new Request(url, {
1674
+ method: event.httpMethod,
1675
+ headers: event.headers,
1676
+ body: event.body ? Buffer.from(event.body, event.isBase64Encoded ? 'base64' : 'utf8') : undefined,
1677
+ });
1678
+
1679
+ const response = await server.handle(request);
1680
+
1681
+ // Convert Response to Lambda response format
1682
+ const body = await response.text();
1683
+ return {
1684
+ statusCode: response.status,
1685
+ headers: Object.fromEntries(response.headers.entries()),
1686
+ body: body,
1687
+ };
1688
+ };
1689
+ `;
1690
+
1691
+ writeFileSync(join(projectDir, "src", "lambda.ts"), lambdaHandler);
1692
+ }
1693
+
1694
+ function createEnvFiles(projectDir: string, options: InitOptions) {
1695
+ // .env.example
1696
+ let envExample = `# Environment Configuration
1697
+ NODE_ENV=development
1698
+ PORT=3000
1699
+
1700
+ # Database
1701
+ `;
1702
+
1703
+ if (options.database === "sqlite") {
1704
+ envExample += `DATABASE_URL=./data/app.db
1705
+ `;
1706
+ } else if (options.database === "postgres") {
1707
+ envExample += `DATABASE_URL=postgresql://user:password@localhost:5432/app
1708
+ `;
1709
+ } else if (options.database === "mysql") {
1710
+ envExample += `DATABASE_URL=mysql://user:password@localhost:3306/app
1711
+ `;
1712
+ }
1713
+
1714
+ if (options.plugins.includes("auth")) {
1715
+ envExample += `
1716
+ # Authentication
1717
+ JWT_SECRET=your-super-secret-jwt-key-change-this-in-production
1718
+ `;
1719
+ }
1720
+
1721
+ if (options.plugins.includes("backup")) {
1722
+ envExample += `
1723
+ # Backup (Litestream)
1724
+ BACKUP_S3_URL=s3://my-backup-bucket/db
1725
+ BACKUP_ACCESS_KEY=your-access-key
1726
+ BACKUP_SECRET_KEY=your-secret-key
1727
+ BACKUP_REGION=us-east-1
1728
+ `;
1729
+ }
1730
+
1731
+ if (options.plugins.includes("storage")) {
1732
+ envExample += `
1733
+ # File Storage
1734
+ STORAGE_ADAPTER=local
1735
+ UPLOAD_DIR=./uploads
1736
+
1737
+ # Or for S3:
1738
+ # STORAGE_ADAPTER=s3
1739
+ # S3_BUCKET=my-bucket
1740
+ # S3_REGION=us-east-1
1741
+ # S3_ACCESS_KEY=your-key
1742
+ # S3_SECRET_KEY=your-secret
1743
+ `;
1744
+ }
1745
+
1746
+ if (options.plugins.includes("email")) {
1747
+ envExample += `
1748
+ # Email
1749
+ SMTP_HOST=smtp.gmail.com
1750
+ SMTP_PORT=587
1751
+ SMTP_USER=your-email@gmail.com
1752
+ SMTP_PASS=your-app-password
1753
+ EMAIL_FROM=noreply@yourdomain.com
1754
+ `;
1755
+ }
1756
+
1757
+ writeFileSync(join(projectDir, ".env.example"), envExample);
1758
+ writeFileSync(join(projectDir, ".env"), envExample.replace(/your-.*?(\n|$)/g, "your-value-here$1"));
1759
+ }
1760
+
1761
+ function createReadme(projectDir: string, options: InitOptions) {
1762
+ const isSvelteKit = options.frontend === "sveltekit";
1763
+
1764
+ const readme = `# ${options.projectName}
1765
+
1766
+ Built with DonkeyLabs framework
1767
+
1768
+ ## Features
1769
+
1770
+ - **Database**: ${options.database}
1771
+ - **Frontend**: ${isSvelteKit ? "SvelteKit" : "None (API only)"}
1772
+ - **Plugins**: ${options.plugins.join(", ")}
1773
+ - **Deployment**: ${options.deployment}
1774
+
1775
+ ## Getting Started
1776
+
1777
+ \`\`\`bash
1778
+ # Install dependencies
1779
+ bun install
1780
+
1781
+ # Set up environment
1782
+ cp .env.example .env
1783
+ # Edit .env with your values
1784
+
1785
+ ${options.database === "sqlite" ? "# Create data directory\nmkdir -p data" : ""}
1786
+
1787
+ # Run migrations
1788
+ bun scripts/migrate.ts
1789
+
1790
+ # Start development
1791
+ ${isSvelteKit ? "bun run dev" : "bun --watch run src/server/index.ts"}
1792
+ \`\`\`
1793
+
1794
+ ## Project Structure
1795
+
1796
+ \`\`\`
1797
+ src/
1798
+ ├── server/
1799
+ │ ├── plugins/ # Business logic plugins
1800
+ │ ├── routes/ # API routes
1801
+ │ ├── index.ts # Server entry
1802
+ │ └── db.ts # Database configuration
1803
+ ${isSvelteKit ? `├── routes/ # SvelteKit pages
1804
+ ├── app.html
1805
+ └── app.css` : ""}
1806
+ \`\`\`
1807
+
1808
+ ## Available Plugins
1809
+
1810
+ ${options.plugins.map(p => `- **${p}**: ${getPluginDescription(p)}`).join("\n")}
1811
+
1812
+ ## Deployment
1813
+
1814
+ ${options.deployment === "docker" ? `### Docker (recommended)
1815
+
1816
+ \`\`\`bash
1817
+ docker-compose up -d
1818
+ \`\`\`` : options.deployment === "pm2" ? `### PM2
1819
+
1820
+ \`\`\`bash
1821
+ # Build first
1822
+ bun run build
1823
+
1824
+ # Start with PM2
1825
+ pm2 start ecosystem.config.js
1826
+
1827
+ # Save PM2 config
1828
+ pm2 save
1829
+ \`\`\`` : `### Binary
1830
+
1831
+ \`\`\`bash
1832
+ # Build
1833
+ bun run build
1834
+
1835
+ # Run
1836
+ bun run dist/index.js
1837
+ \`\`\``}
1838
+
1839
+ ## Documentation
1840
+
1841
+ - [DonkeyLabs Docs](https://donkeylabs.io/docs)
1842
+ - [API Reference](https://donkeylabs.io/docs/api)
1843
+
1844
+ ## License
1845
+
1846
+ MIT
1847
+ `;
1848
+
1849
+ writeFileSync(join(projectDir, "README.md"), readme);
1850
+ }
1851
+
1852
+ function getPluginDescription(name: string): string {
1853
+ const descriptions: Record<string, string> = {
1854
+ users: "User management",
1855
+ auth: "JWT authentication",
1856
+ email: "Email sending",
1857
+ storage: "File uploads",
1858
+ backup: "Database backups with Litestream",
1859
+ cron: "Scheduled jobs",
1860
+ audit: "Audit logging",
1861
+ };
1862
+ return descriptions[name] || name;
1863
+ }
1864
+
1865
+ function createGitignore(projectDir: string, options: InitOptions) {
1866
+ const content = `# Dependencies
1867
+ node_modules/
1868
+
1869
+ # Environment
1870
+ .env
1871
+ .env.local
1872
+
1873
+ # Build output
1874
+ dist/
1875
+ build/
1876
+
1877
+ # Database
1878
+ *.db
1879
+ *.db-journal
1880
+ data/
1881
+
1882
+ # Logs
1883
+ logs/
1884
+ *.log
1885
+
1886
+ # Uploads
1887
+ uploads/
1888
+
1889
+ # IDE
1890
+ .vscode/
1891
+ .idea/
1892
+ *.swp
1893
+ *.swo
1894
+
1895
+ # OS
1896
+ .DS_Store
1897
+ Thumbs.db
1898
+
1899
+ # DonkeyLabs
1900
+ .@donkeylabs/
1901
+ `;
1902
+
1903
+ writeFileSync(join(projectDir, ".gitignore"), content);
1904
+ }
1905
+
1906
+ function createDonkeylabsConfig(projectDir: string, options: InitOptions) {
1907
+ const config = `import { defineConfig } from "@donkeylabs/server";
1908
+
1909
+ export default defineConfig({
1910
+ plugins: ["./src/server/plugins/*/index.ts"],
1911
+ routes: "./src/server/routes/**/*.ts",
1912
+ outDir: ".@donkeylabs",
1913
+ ${options.frontend === "sveltekit" ? `adapter: "@donkeylabs/adapter-sveltekit",
1914
+ client: {
1915
+ output: "./src/lib/api.ts",
1916
+ },` : ""}
1917
+ });
1918
+ `;
1919
+
1920
+ writeFileSync(join(projectDir, "donkeylabs.config.ts"), config);
1921
+ }
1922
+
1923
+ function createTsconfig(projectDir: string, options: InitOptions) {
1924
+ const isSvelteKit = options.frontend === "sveltekit";
1925
+
1926
+ if (isSvelteKit) {
1927
+ // SvelteKit projects should extend the generated tsconfig
1928
+ const tsconfig = {
1929
+ extends: "./.svelte-kit/tsconfig.json",
1930
+ compilerOptions: {
1931
+ allowJs: true,
1932
+ checkJs: true,
1933
+ esModuleInterop: true,
1934
+ forceConsistentCasingInFileNames: true,
1935
+ resolveJsonModule: true,
1936
+ skipLibCheck: true,
1937
+ sourceMap: true,
1938
+ strict: true,
1939
+ moduleResolution: "bundler",
1940
+ },
1941
+ include: [
1942
+ ".@donkeylabs/server/**/*.ts",
1943
+ ".@donkeylabs/server/**/*.d.ts",
1944
+ "src/**/*.ts",
1945
+ "src/**/*.svelte",
1946
+ ],
1947
+ };
1948
+ writeFileSync(join(projectDir, "tsconfig.json"), JSON.stringify(tsconfig, null, 2));
1949
+ } else {
1950
+ // API-only projects use standalone tsconfig
1951
+ const tsconfig = {
1952
+ compilerOptions: {
1953
+ target: "ES2020",
1954
+ module: "ESNext",
1955
+ lib: ["ES2020"],
1956
+ moduleResolution: "bundler",
1957
+ allowImportingTsExtensions: true,
1958
+ noEmit: true,
1959
+ resolveJsonModule: true,
1960
+ verbatimModuleSyntax: true,
1961
+ strict: true,
1962
+ noUnusedLocals: true,
1963
+ noUnusedParameters: true,
1964
+ noFallthroughCasesInSwitch: true,
1965
+ declaration: true,
1966
+ declarationMap: true,
1967
+ sourceMap: true,
1968
+ esModuleInterop: true,
1969
+ skipLibCheck: true,
1970
+ forceConsistentCasingInFileNames: true,
1971
+ baseUrl: ".",
1972
+ paths: {
1973
+ "$server/*": ["src/server/*"],
1974
+ },
1975
+ types: ["bun"],
1976
+ },
1977
+ include: ["src/**/*", "tests/**/*"],
1978
+ exclude: ["node_modules"],
1979
+ };
1980
+ writeFileSync(join(projectDir, "tsconfig.json"), JSON.stringify(tsconfig, null, 2));
1981
+ }
1982
+ }
1983
+
1984
+ async function initGit(projectDir: string) {
1985
+ const { exec } = await import("child_process");
1986
+ return new Promise((resolve, reject) => {
1987
+ exec("git init && git add . && git commit -m 'Initial commit'", {
1988
+ cwd: projectDir,
1989
+ }, (error: any) => {
1990
+ if (error) reject(error);
1991
+ else resolve(undefined);
1992
+ });
1993
+ });
1994
+ }