@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.
- package/package.json +1 -1
- package/src/commands/config.ts +610 -0
- package/src/commands/deploy-enhanced.ts +354 -0
- package/src/commands/deploy.ts +204 -0
- package/src/commands/generate.ts +11 -13
- package/src/commands/init-enhanced.ts +1994 -0
- package/src/deployment/manager.ts +356 -0
- package/src/index.ts +47 -19
- package/templates/starter/.env.example +0 -44
- package/templates/starter/.gitignore.template +0 -4
- package/templates/starter/donkeylabs.config.ts +0 -6
- package/templates/starter/package.json +0 -21
- package/templates/starter/src/index.ts +0 -54
- package/templates/starter/src/plugins/stats/index.ts +0 -105
- package/templates/starter/src/routes/health/handlers/ping.ts +0 -22
- package/templates/starter/src/routes/health/index.ts +0 -19
- package/templates/starter/tsconfig.json +0 -27
- package/templates/sveltekit-app/.env.example +0 -59
- package/templates/sveltekit-app/README.md +0 -103
- package/templates/sveltekit-app/bun.lock +0 -683
- package/templates/sveltekit-app/donkeylabs.config.ts +0 -12
- package/templates/sveltekit-app/package.json +0 -38
- package/templates/sveltekit-app/src/app.css +0 -40
- package/templates/sveltekit-app/src/app.html +0 -12
- package/templates/sveltekit-app/src/hooks.server.ts +0 -4
- package/templates/sveltekit-app/src/lib/components/ui/badge/badge.svelte +0 -30
- package/templates/sveltekit-app/src/lib/components/ui/badge/index.ts +0 -3
- package/templates/sveltekit-app/src/lib/components/ui/button/button.svelte +0 -48
- package/templates/sveltekit-app/src/lib/components/ui/button/index.ts +0 -9
- package/templates/sveltekit-app/src/lib/components/ui/card/card-content.svelte +0 -18
- package/templates/sveltekit-app/src/lib/components/ui/card/card-description.svelte +0 -18
- package/templates/sveltekit-app/src/lib/components/ui/card/card-footer.svelte +0 -18
- package/templates/sveltekit-app/src/lib/components/ui/card/card-header.svelte +0 -18
- package/templates/sveltekit-app/src/lib/components/ui/card/card-title.svelte +0 -18
- package/templates/sveltekit-app/src/lib/components/ui/card/card.svelte +0 -21
- package/templates/sveltekit-app/src/lib/components/ui/card/index.ts +0 -21
- package/templates/sveltekit-app/src/lib/components/ui/index.ts +0 -4
- package/templates/sveltekit-app/src/lib/components/ui/input/index.ts +0 -2
- package/templates/sveltekit-app/src/lib/components/ui/input/input.svelte +0 -20
- package/templates/sveltekit-app/src/lib/permissions.ts +0 -213
- package/templates/sveltekit-app/src/lib/utils/index.ts +0 -6
- package/templates/sveltekit-app/src/routes/+layout.svelte +0 -8
- package/templates/sveltekit-app/src/routes/+page.server.ts +0 -25
- package/templates/sveltekit-app/src/routes/+page.svelte +0 -680
- package/templates/sveltekit-app/src/routes/workflows/+page.server.ts +0 -23
- package/templates/sveltekit-app/src/routes/workflows/+page.svelte +0 -522
- package/templates/sveltekit-app/src/server/events.ts +0 -28
- package/templates/sveltekit-app/src/server/index.ts +0 -124
- package/templates/sveltekit-app/src/server/plugins/auth/auth.test.ts +0 -377
- package/templates/sveltekit-app/src/server/plugins/auth/index.ts +0 -815
- package/templates/sveltekit-app/src/server/plugins/auth/migrations/001_create_users.ts +0 -25
- package/templates/sveltekit-app/src/server/plugins/auth/migrations/002_create_sessions.ts +0 -32
- package/templates/sveltekit-app/src/server/plugins/auth/migrations/003_create_refresh_tokens.ts +0 -33
- package/templates/sveltekit-app/src/server/plugins/auth/migrations/004_create_passkeys.ts +0 -60
- package/templates/sveltekit-app/src/server/plugins/auth/schema.ts +0 -65
- package/templates/sveltekit-app/src/server/plugins/demo/index.ts +0 -262
- package/templates/sveltekit-app/src/server/plugins/email/email.test.ts +0 -369
- package/templates/sveltekit-app/src/server/plugins/email/index.ts +0 -411
- package/templates/sveltekit-app/src/server/plugins/email/migrations/001_create_email_tokens.ts +0 -33
- package/templates/sveltekit-app/src/server/plugins/email/schema.ts +0 -24
- package/templates/sveltekit-app/src/server/plugins/permissions/index.ts +0 -1048
- package/templates/sveltekit-app/src/server/plugins/permissions/migrations/001_create_tenants.ts +0 -63
- package/templates/sveltekit-app/src/server/plugins/permissions/migrations/002_create_roles.ts +0 -90
- package/templates/sveltekit-app/src/server/plugins/permissions/migrations/003_create_resource_grants.ts +0 -50
- package/templates/sveltekit-app/src/server/plugins/permissions/permissions.test.ts +0 -566
- package/templates/sveltekit-app/src/server/plugins/permissions/schema.ts +0 -67
- package/templates/sveltekit-app/src/server/plugins/workflow-demo/index.ts +0 -198
- package/templates/sveltekit-app/src/server/routes/auth/auth.schemas.ts +0 -66
- package/templates/sveltekit-app/src/server/routes/auth/handlers/login.handler.ts +0 -18
- package/templates/sveltekit-app/src/server/routes/auth/handlers/logout.handler.ts +0 -16
- package/templates/sveltekit-app/src/server/routes/auth/handlers/me.handler.ts +0 -20
- package/templates/sveltekit-app/src/server/routes/auth/handlers/refresh.handler.ts +0 -17
- package/templates/sveltekit-app/src/server/routes/auth/handlers/register.handler.ts +0 -19
- package/templates/sveltekit-app/src/server/routes/auth/handlers/update-profile.handler.ts +0 -21
- package/templates/sveltekit-app/src/server/routes/auth/index.ts +0 -73
- package/templates/sveltekit-app/src/server/routes/demo.ts +0 -464
- package/templates/sveltekit-app/src/server/routes/example/example.schemas.ts +0 -22
- package/templates/sveltekit-app/src/server/routes/example/handlers/greet.handler.ts +0 -21
- package/templates/sveltekit-app/src/server/routes/example/index.ts +0 -28
- package/templates/sveltekit-app/src/server/routes/permissions/index.ts +0 -248
- package/templates/sveltekit-app/src/server/routes/tenants/index.ts +0 -339
- package/templates/sveltekit-app/static/robots.txt +0 -3
- package/templates/sveltekit-app/svelte.config.ts +0 -17
- package/templates/sveltekit-app/tsconfig.json +0 -20
- 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
|
+
}
|