@donkeylabs/server 0.1.3 → 0.1.4

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 (55) hide show
  1. package/examples/starter/node_modules/@donkeylabs/server/README.md +15 -0
  2. package/examples/starter/node_modules/@donkeylabs/server/cli/commands/generate.ts +461 -0
  3. package/examples/starter/node_modules/@donkeylabs/server/cli/commands/init.ts +476 -0
  4. package/examples/starter/node_modules/@donkeylabs/server/cli/commands/interactive.ts +223 -0
  5. package/examples/starter/node_modules/@donkeylabs/server/cli/commands/plugin.ts +192 -0
  6. package/examples/starter/node_modules/@donkeylabs/server/cli/donkeylabs +106 -0
  7. package/examples/starter/node_modules/@donkeylabs/server/cli/index.ts +100 -0
  8. package/examples/starter/node_modules/@donkeylabs/server/context.d.ts +17 -0
  9. package/examples/starter/node_modules/@donkeylabs/server/docs/api-client.md +520 -0
  10. package/examples/starter/node_modules/@donkeylabs/server/docs/cache.md +437 -0
  11. package/examples/starter/node_modules/@donkeylabs/server/docs/cli.md +353 -0
  12. package/examples/starter/node_modules/@donkeylabs/server/docs/core-services.md +338 -0
  13. package/examples/starter/node_modules/@donkeylabs/server/docs/cron.md +465 -0
  14. package/examples/starter/node_modules/@donkeylabs/server/docs/errors.md +303 -0
  15. package/examples/starter/node_modules/@donkeylabs/server/docs/events.md +460 -0
  16. package/examples/starter/node_modules/@donkeylabs/server/docs/handlers.md +549 -0
  17. package/examples/starter/node_modules/@donkeylabs/server/docs/jobs.md +556 -0
  18. package/examples/starter/node_modules/@donkeylabs/server/docs/logger.md +316 -0
  19. package/examples/starter/node_modules/@donkeylabs/server/docs/middleware.md +682 -0
  20. package/examples/starter/node_modules/@donkeylabs/server/docs/plugins.md +524 -0
  21. package/examples/starter/node_modules/@donkeylabs/server/docs/project-structure.md +493 -0
  22. package/examples/starter/node_modules/@donkeylabs/server/docs/rate-limiter.md +525 -0
  23. package/examples/starter/node_modules/@donkeylabs/server/docs/router.md +566 -0
  24. package/examples/starter/node_modules/@donkeylabs/server/docs/sse.md +542 -0
  25. package/examples/starter/node_modules/@donkeylabs/server/docs/svelte-frontend.md +324 -0
  26. package/examples/starter/node_modules/@donkeylabs/server/mcp/donkeylabs-mcp +3238 -0
  27. package/examples/starter/node_modules/@donkeylabs/server/mcp/server.ts +3238 -0
  28. package/examples/starter/node_modules/@donkeylabs/server/package.json +77 -0
  29. package/examples/starter/node_modules/@donkeylabs/server/registry.d.ts +11 -0
  30. package/examples/starter/node_modules/@donkeylabs/server/src/client/base.ts +481 -0
  31. package/examples/starter/node_modules/@donkeylabs/server/src/client/index.ts +150 -0
  32. package/examples/starter/node_modules/@donkeylabs/server/src/core/cache.ts +183 -0
  33. package/examples/starter/node_modules/@donkeylabs/server/src/core/cron.ts +255 -0
  34. package/examples/starter/node_modules/@donkeylabs/server/src/core/errors.ts +320 -0
  35. package/examples/starter/node_modules/@donkeylabs/server/src/core/events.ts +163 -0
  36. package/examples/starter/node_modules/@donkeylabs/server/src/core/index.ts +94 -0
  37. package/examples/starter/node_modules/@donkeylabs/server/src/core/jobs.ts +334 -0
  38. package/examples/starter/node_modules/@donkeylabs/server/src/core/logger.ts +131 -0
  39. package/examples/starter/node_modules/@donkeylabs/server/src/core/rate-limiter.ts +193 -0
  40. package/examples/starter/node_modules/@donkeylabs/server/src/core/sse.ts +210 -0
  41. package/examples/starter/node_modules/@donkeylabs/server/src/core.ts +428 -0
  42. package/examples/starter/node_modules/@donkeylabs/server/src/handlers.ts +87 -0
  43. package/examples/starter/node_modules/@donkeylabs/server/src/harness.ts +70 -0
  44. package/examples/starter/node_modules/@donkeylabs/server/src/index.ts +38 -0
  45. package/examples/starter/node_modules/@donkeylabs/server/src/middleware.ts +34 -0
  46. package/examples/starter/node_modules/@donkeylabs/server/src/registry.ts +13 -0
  47. package/examples/starter/node_modules/@donkeylabs/server/src/router.ts +155 -0
  48. package/examples/starter/node_modules/@donkeylabs/server/src/server.ts +234 -0
  49. package/examples/starter/node_modules/@donkeylabs/server/templates/init/donkeylabs.config.ts.template +14 -0
  50. package/examples/starter/node_modules/@donkeylabs/server/templates/init/index.ts.template +41 -0
  51. package/examples/starter/node_modules/@donkeylabs/server/templates/plugin/index.ts.template +25 -0
  52. package/examples/starter/src/routes/health/ping/models/model.ts +11 -7
  53. package/package.json +3 -3
  54. package/examples/starter/node_modules/.svelte2tsx-language-server-files/svelte-native-jsx.d.ts +0 -32
  55. package/examples/starter/node_modules/.svelte2tsx-language-server-files/svelte-shims-v4.d.ts +0 -290
@@ -0,0 +1,476 @@
1
+ /**
2
+ * Init Command
3
+ *
4
+ * Initialize a new @donkeylabs/server project with proper database setup.
5
+ */
6
+
7
+ import { mkdir, writeFile, readFile, readdir, copyFile } from "node:fs/promises";
8
+ import { join, resolve, dirname } from "node:path";
9
+ import { existsSync } from "node:fs";
10
+ import { fileURLToPath } from "node:url";
11
+ import pc from "picocolors";
12
+ import prompts from "prompts";
13
+
14
+ interface InitOptions {
15
+ projectName: string;
16
+ useDatabase: boolean;
17
+ }
18
+
19
+ export async function initCommand(args: string[]): Promise<void> {
20
+ const projectDir = args[0] || ".";
21
+ const targetDir = resolve(process.cwd(), projectDir);
22
+
23
+ console.log(pc.bold("\nInitializing @donkeylabs/server project...\n"));
24
+
25
+ // Check for existing files
26
+ if (existsSync(targetDir)) {
27
+ const files = await readdir(targetDir);
28
+ const hasConflicts = files.some(
29
+ (f) => f === "src" || f === "donkeylabs.config.ts"
30
+ );
31
+
32
+ if (hasConflicts) {
33
+ const { overwrite } = await prompts({
34
+ type: "confirm",
35
+ name: "overwrite",
36
+ message: "Directory contains existing files. Overwrite?",
37
+ initial: false,
38
+ });
39
+
40
+ if (!overwrite) {
41
+ console.log(pc.yellow("Cancelled."));
42
+ return;
43
+ }
44
+ }
45
+ }
46
+
47
+ // Ask for project configuration
48
+ const responses = await prompts([
49
+ {
50
+ type: "text",
51
+ name: "projectName",
52
+ message: "Project name:",
53
+ initial: projectDir === "." ? "my-server" : projectDir,
54
+ },
55
+ {
56
+ type: "confirm",
57
+ name: "useDatabase",
58
+ message: "Set up SQLite database?",
59
+ initial: true,
60
+ },
61
+ ]);
62
+
63
+ if (!responses.projectName) {
64
+ console.log(pc.yellow("Cancelled."));
65
+ return;
66
+ }
67
+
68
+ const options: InitOptions = {
69
+ projectName: responses.projectName,
70
+ useDatabase: responses.useDatabase ?? true,
71
+ };
72
+
73
+ // Create directories
74
+ await mkdir(join(targetDir, "src/plugins"), { recursive: true });
75
+ await mkdir(join(targetDir, ".@donkeylabs/server"), { recursive: true });
76
+
77
+ // Create config file
78
+ await writeFile(
79
+ join(targetDir, "donkeylabs.config.ts"),
80
+ generateConfig()
81
+ );
82
+ console.log(pc.green(" Created:"), "donkeylabs.config.ts");
83
+
84
+ // Create database setup file if using SQLite
85
+ if (options.useDatabase) {
86
+ await writeFile(join(targetDir, "src/db.ts"), generateDatabaseSetup());
87
+ console.log(pc.green(" Created:"), "src/db.ts");
88
+
89
+ await writeFile(join(targetDir, ".env.example"), generateEnvExample());
90
+ console.log(pc.green(" Created:"), ".env.example");
91
+ }
92
+
93
+ // Create main index file
94
+ await writeFile(
95
+ join(targetDir, "src/index.ts"),
96
+ generateIndexFile(options.useDatabase)
97
+ );
98
+ console.log(pc.green(" Created:"), "src/index.ts");
99
+
100
+ // Copy docs and create CLAUDE.md for AI assistants
101
+ await copyDocsToProject(targetDir);
102
+ await writeFile(
103
+ join(targetDir, "CLAUDE.md"),
104
+ generateClaudeMd(options.projectName)
105
+ );
106
+ console.log(pc.green(" Created:"), "CLAUDE.md + docs/");
107
+
108
+ // Update .gitignore
109
+ const gitignorePath = join(targetDir, ".gitignore");
110
+ const gitignoreContent = existsSync(gitignorePath)
111
+ ? await readFile(gitignorePath, "utf-8")
112
+ : "";
113
+
114
+ const gitignoreAdditions: string[] = [];
115
+ if (!gitignoreContent.includes(".@donkeylabs")) {
116
+ gitignoreAdditions.push("# Generated types", ".@donkeylabs/");
117
+ }
118
+ if (!gitignoreContent.includes(".env") && options.useDatabase) {
119
+ gitignoreAdditions.push("# Environment", ".env", ".env.local");
120
+ }
121
+ if (options.useDatabase && !gitignoreContent.includes("*.db")) {
122
+ gitignoreAdditions.push("# SQLite", "*.db", "*.db-journal");
123
+ }
124
+
125
+ if (gitignoreAdditions.length > 0) {
126
+ const newGitignore = gitignoreContent + "\n" + gitignoreAdditions.join("\n") + "\n";
127
+ await writeFile(gitignorePath, newGitignore);
128
+ console.log(pc.green(" Updated:"), ".gitignore");
129
+ }
130
+
131
+ // Create tsconfig.json if not exists
132
+ if (!existsSync(join(targetDir, "tsconfig.json"))) {
133
+ await writeFile(
134
+ join(targetDir, "tsconfig.json"),
135
+ generateTsConfig()
136
+ );
137
+ console.log(pc.green(" Created:"), "tsconfig.json");
138
+ }
139
+
140
+ // Update package.json
141
+ const pkgPath = join(targetDir, "package.json");
142
+ let pkg: any = { name: options.projectName, type: "module", scripts: {}, dependencies: {} };
143
+
144
+ if (existsSync(pkgPath)) {
145
+ pkg = JSON.parse(await readFile(pkgPath, "utf-8"));
146
+ pkg.scripts = pkg.scripts || {};
147
+ pkg.dependencies = pkg.dependencies || {};
148
+ }
149
+
150
+ pkg.name = pkg.name || options.projectName;
151
+ pkg.type = "module";
152
+ pkg.scripts.dev = "bun --watch src/index.ts";
153
+ pkg.scripts.start = "bun src/index.ts";
154
+ pkg.scripts["gen:types"] = "donkeylabs generate";
155
+
156
+ // Add dependencies
157
+ pkg.dependencies["@donkeylabs/server"] = "latest";
158
+ pkg.dependencies["kysely"] = "^0.27.0";
159
+ pkg.dependencies["zod"] = "^3.24.0";
160
+ if (options.useDatabase) {
161
+ pkg.dependencies["kysely-bun-sqlite"] = "^0.3.0";
162
+ }
163
+
164
+ // Add dev dependencies
165
+ pkg.devDependencies = pkg.devDependencies || {};
166
+ pkg.devDependencies["@types/bun"] = "latest";
167
+
168
+ await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
169
+ console.log(pc.green(" Created:"), "package.json");
170
+
171
+ // Print next steps
172
+ console.log(`
173
+ ${pc.bold(pc.green("Success!"))} Project initialized.
174
+
175
+ ${pc.bold("Next steps:")}
176
+ 1. Install dependencies:
177
+ ${pc.cyan("bun install")}
178
+ ${options.useDatabase ? `
179
+ 2. Set up your database:
180
+ ${pc.cyan(`cp .env.example .env`)}
181
+ ` : ""}
182
+ ${options.useDatabase ? "3" : "2"}. Start development:
183
+ ${pc.cyan("bun run dev")}
184
+ `);
185
+ }
186
+
187
+ function generateConfig(): string {
188
+ return `import { defineConfig } from "@donkeylabs/server";
189
+
190
+ export default defineConfig({
191
+ plugins: ["./src/plugins/**/index.ts"],
192
+ outDir: ".@donkeylabs/server",
193
+ });
194
+ `;
195
+ }
196
+
197
+ function generateDatabaseSetup(): string {
198
+ return `import { Kysely } from "kysely";
199
+ import { BunSqliteDialect } from "kysely-bun-sqlite";
200
+ import { Database } from "bun:sqlite";
201
+
202
+ const dbPath = process.env.DATABASE_URL || "app.db";
203
+
204
+ export const db = new Kysely<any>({
205
+ dialect: new BunSqliteDialect({
206
+ database: new Database(dbPath),
207
+ }),
208
+ });
209
+
210
+ export type DB = typeof db;
211
+ `;
212
+ }
213
+
214
+ function generateIndexFile(useDatabase: boolean): string {
215
+ const dbImport = useDatabase
216
+ ? `import { db } from "./db";\n`
217
+ : `import {
218
+ Kysely,
219
+ DummyDriver,
220
+ SqliteAdapter,
221
+ SqliteIntrospector,
222
+ SqliteQueryCompiler,
223
+ } from "kysely";
224
+
225
+ // No database configured - using dummy driver for type compatibility
226
+ const db = new Kysely<any>({
227
+ dialect: {
228
+ createAdapter: () => new SqliteAdapter(),
229
+ createDriver: () => new DummyDriver(),
230
+ createIntrospector: (db) => new SqliteIntrospector(db),
231
+ createQueryCompiler: () => new SqliteQueryCompiler(),
232
+ },
233
+ });
234
+ `;
235
+
236
+ return `${dbImport}import { AppServer, createRouter } from "@donkeylabs/server";
237
+ import { z } from "zod";
238
+
239
+ // Create Server
240
+ const server = new AppServer({
241
+ port: Number(process.env.PORT) || 3000,
242
+ db,
243
+ config: { env: process.env.NODE_ENV || "development" },
244
+ });
245
+
246
+ // Define Routes
247
+ const router = createRouter("api")
248
+ .route("hello").typed({
249
+ input: z.object({ name: z.string().optional() }),
250
+ output: z.object({ message: z.string() }),
251
+ handle: async (input) => {
252
+ return { message: \`Hello, \${input.name || "World"}!\` };
253
+ },
254
+ })
255
+ .route("health").raw({
256
+ handle: async () => {
257
+ return Response.json({ status: "ok", timestamp: new Date().toISOString() });
258
+ },
259
+ });
260
+
261
+ server.use(router);
262
+
263
+ // Start Server
264
+ await server.start();
265
+ `;
266
+ }
267
+
268
+ function generateEnvExample(): string {
269
+ return `# Database (SQLite file path)
270
+ DATABASE_URL=app.db
271
+
272
+ # Server
273
+ PORT=3000
274
+ NODE_ENV=development
275
+ `;
276
+ }
277
+
278
+ function generateTsConfig(): string {
279
+ return `{
280
+ "compilerOptions": {
281
+ "lib": ["ESNext"],
282
+ "target": "ESNext",
283
+ "module": "Preserve",
284
+ "moduleDetection": "force",
285
+ "jsx": "react-jsx",
286
+ "allowJs": true,
287
+ "moduleResolution": "bundler",
288
+ "allowImportingTsExtensions": true,
289
+ "verbatimModuleSyntax": true,
290
+ "noEmit": true,
291
+ "strict": true,
292
+ "skipLibCheck": true,
293
+ "noFallthroughCasesInSwitch": true,
294
+ "noUncheckedIndexedAccess": true,
295
+ "noImplicitOverride": true
296
+ },
297
+ "include": ["src/**/*", "*.ts", ".@donkeylabs/**/*"]
298
+ }
299
+ `;
300
+ }
301
+
302
+ // Docs to copy to user projects (subset relevant for users)
303
+ const USER_DOCS = [
304
+ "plugins.md",
305
+ "router.md",
306
+ "handlers.md",
307
+ "core-services.md",
308
+ "errors.md",
309
+ "cache.md",
310
+ "logger.md",
311
+ "events.md",
312
+ "jobs.md",
313
+ "cron.md",
314
+ "sse.md",
315
+ "rate-limiter.md",
316
+ "middleware.md",
317
+ "api-client.md",
318
+ "svelte-frontend.md",
319
+ ];
320
+
321
+ async function copyDocsToProject(targetDir: string): Promise<void> {
322
+ const docsDir = join(targetDir, "docs");
323
+ await mkdir(docsDir, { recursive: true });
324
+
325
+ // Find the package's docs directory
326
+ const __filename = fileURLToPath(import.meta.url);
327
+ const __dirname = dirname(__filename);
328
+ const packageDocsDir = resolve(__dirname, "../../docs");
329
+
330
+ for (const doc of USER_DOCS) {
331
+ const srcPath = join(packageDocsDir, doc);
332
+ const destPath = join(docsDir, doc);
333
+
334
+ if (existsSync(srcPath)) {
335
+ await copyFile(srcPath, destPath);
336
+ }
337
+ }
338
+ }
339
+
340
+ function generateClaudeMd(projectName: string): string {
341
+ return `# ${projectName}
342
+
343
+ Built with @donkeylabs/server - a type-safe RPC framework for Bun.
344
+
345
+ ## Project Structure
346
+
347
+ \`\`\`
348
+ src/
349
+ ├── index.ts # Server entry + routes (start here)
350
+ ├── db.ts # Database setup
351
+ └── plugins/ # Your plugins
352
+ └── example/
353
+ ├── index.ts # Plugin definition
354
+ ├── schema.ts # DB types (if needed)
355
+ └── migrations/ # SQL migrations
356
+ docs/ # Framework documentation
357
+ \`\`\`
358
+
359
+ ## Quick Start
360
+
361
+ See \`src/index.ts\` for a working example with routes.
362
+
363
+ ## Plugins
364
+
365
+ Plugins encapsulate business logic with optional database schemas.
366
+
367
+ \`\`\`ts
368
+ import { createPlugin } from "@donkeylabs/server";
369
+
370
+ export const notesPlugin = createPlugin.define({
371
+ name: "notes",
372
+ service: async (ctx) => ({
373
+ async create(title: string) {
374
+ return ctx.db.insertInto("notes").values({ title }).execute();
375
+ },
376
+ }),
377
+ });
378
+ \`\`\`
379
+
380
+ → See **docs/plugins.md** for schemas, migrations, and dependencies.
381
+
382
+ ## Routes
383
+
384
+ Routes handle HTTP requests and call plugin services.
385
+
386
+ \`\`\`ts
387
+ const router = createRouter("notes")
388
+ .route("create").typed({
389
+ input: z.object({ title: z.string() }),
390
+ handle: async (input, ctx) => ctx.plugins.notes.create(input.title),
391
+ });
392
+ \`\`\`
393
+
394
+ → See **docs/router.md** for typed handlers, raw handlers, and middleware.
395
+
396
+ ## Handlers
397
+
398
+ Two types: \`typed\` (validated JSON) and \`raw\` (full Request/Response).
399
+
400
+ → See **docs/handlers.md** for input/output validation and error handling.
401
+
402
+ ## Errors
403
+
404
+ Use built-in error factories for proper HTTP responses.
405
+
406
+ \`\`\`ts
407
+ throw ctx.errors.NotFound("User not found");
408
+ throw ctx.errors.BadRequest("Invalid email");
409
+ \`\`\`
410
+
411
+ → See **docs/errors.md** for all error types and custom errors.
412
+
413
+ ## Core Services
414
+
415
+ Available via \`ctx.core\`. **Only use what you need.**
416
+
417
+ | Service | Purpose | Docs |
418
+ |---------|---------|------|
419
+ | \`logger\` | Structured logging | docs/logger.md |
420
+ | \`cache\` | In-memory key-value cache | docs/cache.md |
421
+ | \`events\` | Pub/sub between plugins | docs/events.md |
422
+ | \`jobs\` | Background job queue | docs/jobs.md |
423
+ | \`cron\` | Scheduled tasks | docs/cron.md |
424
+ | \`sse\` | Server-sent events | docs/sse.md |
425
+ | \`rateLimiter\` | Request rate limiting | docs/rate-limiter.md |
426
+
427
+ → See **docs/core-services.md** for overview.
428
+
429
+ ## Middleware
430
+
431
+ Add authentication, logging, or other cross-cutting concerns.
432
+
433
+ → See **docs/middleware.md** for usage patterns.
434
+
435
+ ## API Client
436
+
437
+ Generate a typed client for your frontend:
438
+
439
+ \`\`\`sh
440
+ bun run gen:client --output ./frontend/src/lib/api
441
+ \`\`\`
442
+
443
+ → See **docs/api-client.md** for client configuration and usage.
444
+
445
+ ## Svelte 5 Frontend
446
+
447
+ Build type-safe frontends with Svelte 5 and SvelteKit.
448
+
449
+ \`\`\`svelte
450
+ <script lang="ts">
451
+ import { api } from "$lib/api";
452
+ let items = $state<Item[]>([]);
453
+
454
+ $effect(() => {
455
+ api.items.list({}).then((r) => items = r.items);
456
+ });
457
+ </script>
458
+ \`\`\`
459
+
460
+ → See **docs/svelte-frontend.md** for patterns and SSE integration.
461
+
462
+ ## CLI Commands
463
+
464
+ \`\`\`sh
465
+ bun run dev # Start with hot reload
466
+ bun run gen:types # Generate types after adding plugins
467
+ \`\`\`
468
+
469
+ ## Guidelines
470
+
471
+ - **Keep it simple** - don't add services you don't need
472
+ - **One concern per plugin** - auth, notes, billing as separate plugins
473
+ - **Minimal logging** - log errors and key events, not every call
474
+ - **Read the docs** - check docs/*.md before implementing something complex
475
+ `;
476
+ }
@@ -0,0 +1,223 @@
1
+ /**
2
+ * Interactive CLI Menu
3
+ *
4
+ * Full interactive experience with context-aware menus
5
+ */
6
+
7
+ import prompts from "prompts";
8
+ import pc from "picocolors";
9
+ import { readdir, writeFile, mkdir } from "node:fs/promises";
10
+ import { join, basename } from "node:path";
11
+ import { existsSync } from "node:fs";
12
+ import { exec } from "node:child_process";
13
+ import { promisify } from "node:util";
14
+
15
+ const execAsync = promisify(exec);
16
+
17
+ export async function interactiveCommand() {
18
+ console.clear();
19
+ console.log(pc.magenta(pc.bold("\n @donkeylabs/server CLI\n")));
20
+
21
+ // Detect context - are we in a plugin directory?
22
+ const cwd = process.cwd();
23
+ const pathParts = cwd.split("/");
24
+ const parentDir = pathParts[pathParts.length - 2];
25
+ const currentDir = pathParts[pathParts.length - 1];
26
+
27
+ let contextPlugin: string | null = null;
28
+ if (parentDir === "plugins" && currentDir && existsSync(join(cwd, "index.ts"))) {
29
+ contextPlugin = currentDir;
30
+ }
31
+
32
+ // Run appropriate menu loop
33
+ if (contextPlugin) {
34
+ await pluginMenuLoop(contextPlugin);
35
+ } else {
36
+ await globalMenuLoop();
37
+ }
38
+ }
39
+
40
+ // ============================================
41
+ // Plugin Context Menu (Inside plugins/<name>/)
42
+ // ============================================
43
+
44
+ async function pluginMenuLoop(pluginName: string) {
45
+ while (true) {
46
+ console.log(pc.cyan(`\n Context: Plugin ${pc.bold(`'${pluginName}'`)}\n`));
47
+
48
+ const response = await prompts({
49
+ type: "select",
50
+ name: "action",
51
+ message: "What would you like to do?",
52
+ choices: [
53
+ { title: pc.yellow("1.") + " Generate Schema Types", value: "gen-types" },
54
+ { title: pc.yellow("2.") + " Create Migration", value: "migration" },
55
+ { title: pc.gray("─".repeat(35)), value: "separator", disabled: true },
56
+ { title: pc.blue("←") + " Back to Global Menu", value: "global" },
57
+ { title: pc.red("×") + " Exit", value: "exit" },
58
+ ],
59
+ });
60
+
61
+ if (!response.action || response.action === "exit") {
62
+ console.log(pc.gray("\nGoodbye!\n"));
63
+ process.exit(0);
64
+ }
65
+
66
+ if (response.action === "global") {
67
+ console.clear();
68
+ console.log(pc.magenta(pc.bold("\n @donkeylabs/server CLI\n")));
69
+ await globalMenuLoop();
70
+ return;
71
+ }
72
+
73
+ console.log(""); // spacing
74
+
75
+ switch (response.action) {
76
+ case "gen-types":
77
+ await runCommand(`bun scripts/generate-types.ts ${pluginName}`);
78
+ break;
79
+ case "migration":
80
+ await createMigration(pluginName);
81
+ break;
82
+ }
83
+
84
+ await pressEnterToContinue();
85
+ }
86
+ }
87
+
88
+ // ============================================
89
+ // Global Root Menu
90
+ // ============================================
91
+
92
+ async function globalMenuLoop() {
93
+ while (true) {
94
+ console.log(pc.cyan("\n Context: Project Root\n"));
95
+
96
+ const choices = [
97
+ { title: pc.yellow("1.") + " Create New Plugin", value: "new-plugin" },
98
+ { title: pc.yellow("2.") + " Initialize New Project", value: "init" },
99
+ { title: pc.gray("─".repeat(35)), value: "separator1", disabled: true },
100
+ { title: pc.yellow("3.") + " Generate Types", value: "generate" },
101
+ { title: pc.yellow("4.") + " Generate Registry", value: "gen-registry" },
102
+ { title: pc.yellow("5.") + " Generate Server Context", value: "gen-server" },
103
+ { title: pc.gray("─".repeat(35)), value: "separator2", disabled: true },
104
+ { title: pc.red("×") + " Exit", value: "exit" },
105
+ ];
106
+
107
+ const response = await prompts({
108
+ type: "select",
109
+ name: "action",
110
+ message: "Select a command:",
111
+ choices,
112
+ });
113
+
114
+ if (!response.action || response.action === "exit") {
115
+ console.log(pc.gray("\nGoodbye!\n"));
116
+ process.exit(0);
117
+ }
118
+
119
+ console.log(""); // spacing
120
+
121
+ switch (response.action) {
122
+ case "new-plugin":
123
+ const { pluginCommand } = await import("./plugin");
124
+ await pluginCommand(["create"]);
125
+ break;
126
+ case "init":
127
+ const { initCommand } = await import("./init");
128
+ await initCommand([]);
129
+ break;
130
+ case "generate":
131
+ const { generateCommand } = await import("./generate");
132
+ await generateCommand([]);
133
+ break;
134
+ case "gen-registry":
135
+ await runCommand("bun scripts/generate-registry.ts");
136
+ break;
137
+ case "gen-server":
138
+ await runCommand("bun scripts/generate-server.ts");
139
+ break;
140
+ }
141
+
142
+ await pressEnterToContinue();
143
+ }
144
+ }
145
+
146
+ // ============================================
147
+ // Commands
148
+ // ============================================
149
+
150
+ async function createMigration(pluginName: string) {
151
+ const nameRes = await prompts({
152
+ type: "text",
153
+ name: "migName",
154
+ message: "Migration name (e.g. add_comments):",
155
+ validate: (v) =>
156
+ /^[a-z0-9_]+$/.test(v) ? true : "Use lowercase letters, numbers, and underscores",
157
+ });
158
+
159
+ if (!nameRes.migName) return;
160
+
161
+ // Determine migrations directory
162
+ const cwd = process.cwd();
163
+ const isPluginDir = basename(join(cwd, "..")) === "plugins";
164
+ const migrationsDir = isPluginDir
165
+ ? join(cwd, "migrations")
166
+ : join(process.cwd(), "src/plugins", pluginName, "migrations");
167
+
168
+ // Generate sequential number
169
+ let nextNum = 1;
170
+ try {
171
+ const files = await readdir(migrationsDir);
172
+ const nums = files
173
+ .map((f) => parseInt(f.split("_")[0] || "", 10))
174
+ .filter((n) => !isNaN(n));
175
+ if (nums.length > 0) {
176
+ nextNum = Math.max(...nums) + 1;
177
+ }
178
+ } catch {}
179
+
180
+ const filename = `${String(nextNum).padStart(3, "0")}_${nameRes.migName}.ts`;
181
+ const content = `import type { Kysely } from "kysely";
182
+
183
+ export async function up(db: Kysely<any>): Promise<void> {
184
+ // await db.schema.createTable("...").execute();
185
+ }
186
+
187
+ export async function down(db: Kysely<any>): Promise<void> {
188
+ // await db.schema.dropTable("...").execute();
189
+ }
190
+ `;
191
+
192
+ if (!existsSync(migrationsDir)) {
193
+ await mkdir(migrationsDir, { recursive: true });
194
+ }
195
+
196
+ await writeFile(join(migrationsDir, filename), content);
197
+ console.log(pc.green(`Created migration: ${filename}`));
198
+ }
199
+
200
+ // ============================================
201
+ // Helpers
202
+ // ============================================
203
+
204
+ async function runCommand(cmd: string) {
205
+ console.log(pc.gray(`> ${cmd}\n`));
206
+ try {
207
+ const { stdout, stderr } = await execAsync(cmd);
208
+ if (stdout) console.log(stdout);
209
+ if (stderr) console.error(pc.yellow(stderr));
210
+ } catch (e: any) {
211
+ console.error(pc.red("Command failed:"), e.message);
212
+ }
213
+ }
214
+
215
+ async function pressEnterToContinue() {
216
+ await prompts({
217
+ type: "invisible",
218
+ name: "continue",
219
+ message: pc.gray("Press Enter to continue..."),
220
+ });
221
+ console.clear();
222
+ console.log(pc.magenta(pc.bold("\n @donkeylabs/server CLI\n")));
223
+ }