@donkeylabs/cli 0.4.1 → 0.4.3

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 (33) hide show
  1. package/package.json +4 -1
  2. package/src/commands/generate.ts +122 -1
  3. package/src/commands/init.ts +184 -33
  4. package/src/commands/mcp.ts +305 -0
  5. package/src/index.ts +6 -0
  6. package/templates/starter/src/index.ts +21 -4
  7. package/templates/sveltekit-app/package.json +3 -3
  8. package/templates/sveltekit-app/src/server/index.ts +14 -20
  9. package/templates/sveltekit-app/src/server/routes/demo.ts +268 -0
  10. package/templates/sveltekit-app/src/server/routes/cache/handlers/delete.ts +0 -15
  11. package/templates/sveltekit-app/src/server/routes/cache/handlers/get.ts +0 -15
  12. package/templates/sveltekit-app/src/server/routes/cache/handlers/keys.ts +0 -15
  13. package/templates/sveltekit-app/src/server/routes/cache/handlers/set.ts +0 -15
  14. package/templates/sveltekit-app/src/server/routes/cache/index.ts +0 -46
  15. package/templates/sveltekit-app/src/server/routes/counter/handlers/decrement.ts +0 -17
  16. package/templates/sveltekit-app/src/server/routes/counter/handlers/get.ts +0 -17
  17. package/templates/sveltekit-app/src/server/routes/counter/handlers/increment.ts +0 -17
  18. package/templates/sveltekit-app/src/server/routes/counter/handlers/reset.ts +0 -17
  19. package/templates/sveltekit-app/src/server/routes/counter/index.ts +0 -39
  20. package/templates/sveltekit-app/src/server/routes/cron/handlers/list.ts +0 -17
  21. package/templates/sveltekit-app/src/server/routes/cron/index.ts +0 -24
  22. package/templates/sveltekit-app/src/server/routes/events/handlers/emit.ts +0 -15
  23. package/templates/sveltekit-app/src/server/routes/events/index.ts +0 -19
  24. package/templates/sveltekit-app/src/server/routes/index.ts +0 -8
  25. package/templates/sveltekit-app/src/server/routes/jobs/handlers/enqueue.ts +0 -15
  26. package/templates/sveltekit-app/src/server/routes/jobs/handlers/stats.ts +0 -15
  27. package/templates/sveltekit-app/src/server/routes/jobs/index.ts +0 -28
  28. package/templates/sveltekit-app/src/server/routes/ratelimit/handlers/check.ts +0 -15
  29. package/templates/sveltekit-app/src/server/routes/ratelimit/handlers/reset.ts +0 -15
  30. package/templates/sveltekit-app/src/server/routes/ratelimit/index.ts +0 -29
  31. package/templates/sveltekit-app/src/server/routes/sse/handlers/broadcast.ts +0 -15
  32. package/templates/sveltekit-app/src/server/routes/sse/handlers/clients.ts +0 -15
  33. package/templates/sveltekit-app/src/server/routes/sse/index.ts +0 -28
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@donkeylabs/cli",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "type": "module",
5
5
  "description": "CLI for @donkeylabs/server - project scaffolding and code generation",
6
6
  "main": "./src/index.ts",
@@ -24,6 +24,9 @@
24
24
  "typecheck": "bun --bun tsc --noEmit"
25
25
  },
26
26
  "dependencies": {
27
+ "kysely": "^0.27.0",
28
+ "kysely-bun-sqlite": "^0.3.0",
29
+ "kysely-codegen": "^0.17.0",
27
30
  "picocolors": "^1.1.1",
28
31
  "prompts": "^2.4.2"
29
32
  },
@@ -1,8 +1,12 @@
1
- import { readdir, writeFile, readFile, mkdir } from "node:fs/promises";
1
+ import { readdir, writeFile, readFile, mkdir, unlink } from "node:fs/promises";
2
2
  import { join, relative, dirname, basename } from "node:path";
3
3
  import { existsSync } from "node:fs";
4
4
  import { spawn } from "node:child_process";
5
5
  import pc from "picocolors";
6
+ import { Kysely, Migrator, FileMigrationProvider } from "kysely";
7
+ import { BunSqliteDialect } from "kysely-bun-sqlite";
8
+ import { Database } from "bun:sqlite";
9
+ import { generate, KyselyBunSqliteDialect } from "kysely-codegen";
6
10
 
7
11
  interface DonkeylabsConfig {
8
12
  plugins: string[];
@@ -325,6 +329,8 @@ async function extractRoutesFromServer(entryPath: string): Promise<RouteInfo[]>
325
329
  return [];
326
330
  }
327
331
 
332
+ const TIMEOUT_MS = 10000; // 10 second timeout
333
+
328
334
  return new Promise((resolve) => {
329
335
  const child = spawn("bun", [fullPath], {
330
336
  env: { ...process.env, DONKEYLABS_GENERATE: "1" },
@@ -334,6 +340,16 @@ async function extractRoutesFromServer(entryPath: string): Promise<RouteInfo[]>
334
340
 
335
341
  let stdout = "";
336
342
  let stderr = "";
343
+ let timedOut = false;
344
+
345
+ // Timeout handler
346
+ const timeout = setTimeout(() => {
347
+ timedOut = true;
348
+ child.kill("SIGTERM");
349
+ console.warn(pc.yellow(`Route extraction timed out after ${TIMEOUT_MS / 1000}s`));
350
+ console.warn(pc.dim("Make sure your entry file handles DONKEYLABS_GENERATE=1 and calls process.exit(0)"));
351
+ resolve([]);
352
+ }, TIMEOUT_MS);
337
353
 
338
354
  child.stdout?.on("data", (data) => {
339
355
  stdout += data.toString();
@@ -344,6 +360,9 @@ async function extractRoutesFromServer(entryPath: string): Promise<RouteInfo[]>
344
360
  });
345
361
 
346
362
  child.on("close", (code) => {
363
+ clearTimeout(timeout);
364
+ if (timedOut) return; // Already resolved
365
+
347
366
  if (code !== 0) {
348
367
  console.warn(pc.yellow(`Failed to extract routes from server (exit code ${code})`));
349
368
  if (stderr) console.warn(pc.dim(stderr));
@@ -374,12 +393,105 @@ async function extractRoutesFromServer(entryPath: string): Promise<RouteInfo[]>
374
393
  });
375
394
 
376
395
  child.on("error", (err) => {
396
+ clearTimeout(timeout);
377
397
  console.warn(pc.yellow(`Failed to run entry file: ${err.message}`));
378
398
  resolve([]);
379
399
  });
380
400
  });
381
401
  }
382
402
 
403
+ /**
404
+ * Generate schema.ts from plugin migrations using kysely-codegen
405
+ */
406
+ async function generatePluginSchemas(
407
+ plugins: { name: string; path: string; exportName: string }[]
408
+ ): Promise<string[]> {
409
+ const generated: string[] = [];
410
+
411
+ for (const plugin of plugins) {
412
+ const pluginDir = dirname(join(process.cwd(), plugin.path));
413
+ const migrationsDir = join(pluginDir, "migrations");
414
+
415
+ // Skip plugins without migrations folder
416
+ if (!existsSync(migrationsDir)) {
417
+ continue;
418
+ }
419
+
420
+ // Check if there are any migration files
421
+ const migrationFiles = await readdir(migrationsDir);
422
+ const hasMigrations = migrationFiles.some(
423
+ (f) => f.endsWith(".ts") && !f.endsWith(".d.ts")
424
+ );
425
+
426
+ if (!hasMigrations) {
427
+ continue;
428
+ }
429
+
430
+ console.log(pc.dim(` Generating schema for ${plugin.name}...`));
431
+
432
+ const dbPath = join(process.cwd(), `.temp_schema_${plugin.name}.db`);
433
+
434
+ try {
435
+ // Create temp SQLite database
436
+ const db = new Kysely<any>({
437
+ dialect: new BunSqliteDialect({
438
+ database: new Database(dbPath),
439
+ }),
440
+ });
441
+
442
+ // Run migrations using Kysely's FileMigrationProvider
443
+ const migrator = new Migrator({
444
+ db,
445
+ provider: new FileMigrationProvider({
446
+ fs: await import("node:fs/promises"),
447
+ path: await import("node:path"),
448
+ migrationFolder: migrationsDir,
449
+ }),
450
+ });
451
+
452
+ const { error } = await migrator.migrateToLatest();
453
+
454
+ if (error) {
455
+ console.warn(
456
+ pc.yellow(` Warning: Migration failed for ${plugin.name}: ${error}`)
457
+ );
458
+ await db.destroy();
459
+ try {
460
+ await unlink(dbPath);
461
+ } catch {}
462
+ continue;
463
+ }
464
+
465
+ // Generate schema.ts using kysely-codegen
466
+ const schemaPath = join(pluginDir, "schema.ts");
467
+
468
+ await generate({
469
+ db,
470
+ outFile: schemaPath,
471
+ dialect: new KyselyBunSqliteDialect(),
472
+ });
473
+
474
+ generated.push(plugin.name);
475
+
476
+ // Cleanup
477
+ await db.destroy();
478
+ try {
479
+ await unlink(dbPath);
480
+ } catch {}
481
+ } catch (err: any) {
482
+ console.warn(
483
+ pc.yellow(` Warning: Schema generation failed for ${plugin.name}: ${err.message}`)
484
+ );
485
+ // Cleanup on error
486
+ try {
487
+ await unlink(dbPath);
488
+ } catch {}
489
+ }
490
+ }
491
+
492
+ return generated;
493
+ }
494
+
383
495
  export async function generateCommand(_args: string[]): Promise<void> {
384
496
  const config = await loadConfig();
385
497
  const outDir = config.outDir || ".@donkeylabs/server";
@@ -390,6 +502,15 @@ export async function generateCommand(_args: string[]): Promise<void> {
390
502
  const plugins = await findPlugins(config.plugins);
391
503
  const fileRoutes = await findRoutes(config.routes || "./src/routes/**/schema.ts");
392
504
 
505
+ // Generate schema.ts from migrations for plugins that have them
506
+ const schemaPlugins = await generatePluginSchemas(plugins);
507
+ if (schemaPlugins.length > 0) {
508
+ console.log(
509
+ pc.green("Generated schemas:"),
510
+ schemaPlugins.map((p) => pc.dim(p)).join(", ")
511
+ );
512
+ }
513
+
393
514
  // Extract routes by running the server with DONKEYLABS_GENERATE=1
394
515
  const entryPath = config.entry || "./src/index.ts";
395
516
  const serverRoutes = await extractRoutesFromServer(entryPath);
@@ -8,6 +8,7 @@ import { mkdir, writeFile, readFile, readdir, copyFile, stat } from "node:fs/pro
8
8
  import { join, resolve, dirname, basename } from "node:path";
9
9
  import { existsSync } from "node:fs";
10
10
  import { fileURLToPath } from "node:url";
11
+ import { spawn } from "node:child_process";
11
12
  import pc from "picocolors";
12
13
  import prompts from "prompts";
13
14
 
@@ -38,21 +39,45 @@ const RENAME_MAP: Record<string, string> = {
38
39
 
39
40
  export async function initCommand(args: string[]) {
40
41
  // Parse --type flag if provided
41
- let projectDir = ".";
42
+ let projectDir: string | null = null;
42
43
  let typeArg: string | null = null;
43
44
 
44
45
  for (let i = 0; i < args.length; i++) {
45
46
  if (args[i] === "--type" && args[i + 1]) {
46
47
  typeArg = args[i + 1];
47
48
  i++; // skip next arg
48
- } else if (!args[i].startsWith("-")) {
49
- projectDir = args[i];
49
+ } else if (!args[i]?.startsWith("-")) {
50
+ projectDir = args[i] ?? null;
50
51
  }
51
52
  }
52
53
 
53
- const targetDir = resolve(process.cwd(), projectDir);
54
+ console.log(pc.bold("\n🚀 Create a new @donkeylabs/server project\n"));
55
+
56
+ // If no project directory provided, prompt for it
57
+ if (!projectDir) {
58
+ const { name } = await prompts({
59
+ type: "text",
60
+ name: "name",
61
+ message: "Project name:",
62
+ initial: "my-donkeylabs-app",
63
+ validate: (value) => {
64
+ if (!value) return "Project name is required";
65
+ if (!/^[a-zA-Z0-9-_]+$/.test(value)) {
66
+ return "Project name can only contain letters, numbers, dashes, and underscores";
67
+ }
68
+ return true;
69
+ },
70
+ });
71
+
72
+ if (!name) {
73
+ console.log(pc.yellow("Cancelled."));
74
+ return;
75
+ }
76
+
77
+ projectDir = name;
78
+ }
54
79
 
55
- console.log(pc.bold("\nInitializing @donkeylabs/server project...\n"));
80
+ const targetDir = resolve(process.cwd(), projectDir!);
56
81
 
57
82
  let projectType: ProjectType;
58
83
 
@@ -130,45 +155,143 @@ export async function initCommand(args: string[]) {
130
155
  await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
131
156
  }
132
157
 
133
- // Print success message
134
- if (projectType === "server") {
135
- console.log(`
136
- ${pc.bold(pc.green("Success!"))} Server project initialized.
137
-
138
- ${pc.bold("Next steps:")}
139
- 1. Install dependencies:
140
- ${pc.cyan("bun install")}
158
+ console.log(pc.green("\n✓ Project files created\n"));
141
159
 
142
- 2. Start development:
143
- ${pc.cyan("bun run dev")}
160
+ // Auto-install dependencies
161
+ console.log(pc.cyan("Installing dependencies...\n"));
162
+ const installSuccess = await runCommand("bun", ["install"], targetDir);
144
163
 
145
- 3. Generate types after adding plugins:
146
- ${pc.cyan("bun run gen:types")}
147
- `);
164
+ if (!installSuccess) {
165
+ console.log(pc.yellow("\n⚠ Dependency installation failed."));
166
+ console.log(pc.dim(" Run 'bun install' manually to install dependencies.\n"));
148
167
  } else {
149
- console.log(`
150
- ${pc.bold(pc.green("Success!"))} SvelteKit project initialized.
168
+ console.log(pc.green("\n✓ Dependencies installed\n"));
151
169
 
152
- ${pc.bold("Next steps:")}
153
- 1. Install dependencies:
154
- ${pc.cyan("bun install")}
170
+ // Copy CLAUDE.md and docs/ from @donkeylabs/server to project root
171
+ await copyDocsFromServer(targetDir);
172
+ }
155
173
 
156
- 2. Start development:
157
- ${pc.cyan("bun run dev")}
174
+ // Ask about MCP setup
175
+ const { setupMcp } = await prompts({
176
+ type: "confirm",
177
+ name: "setupMcp",
178
+ message: `Setup MCP for AI-assisted development? ${pc.dim("(Highly recommended)")}`,
179
+ initial: true,
180
+ });
181
+
182
+ if (setupMcp) {
183
+ // Ask which IDE
184
+ const { ide } = await prompts({
185
+ type: "select",
186
+ name: "ide",
187
+ message: "Which AI IDE are you using?",
188
+ choices: [
189
+ { title: "Claude Code", value: "claude", description: "Anthropic's Claude Code CLI" },
190
+ { title: "Cursor", value: "cursor", description: "Cursor AI IDE" },
191
+ { title: "Windsurf", value: "windsurf", description: "Codeium Windsurf" },
192
+ { title: "Other / Skip instructions", value: "skip" },
193
+ ],
194
+ });
195
+
196
+ // Install @donkeylabs/mcp
197
+ console.log(pc.cyan("\nInstalling @donkeylabs/mcp...\n"));
198
+ const mcpInstallSuccess = await runCommand("bun", ["add", "-d", "@donkeylabs/mcp"], targetDir);
158
199
 
159
- 3. Build for production:
160
- ${pc.cyan("bun run build")}
200
+ if (mcpInstallSuccess) {
201
+ console.log(pc.green(" Installed @donkeylabs/mcp\n"));
202
+ }
203
+
204
+ // Create .mcp.json
205
+ const mcpConfig = {
206
+ mcpServers: {
207
+ donkeylabs: {
208
+ command: "bunx",
209
+ args: ["@donkeylabs/mcp"],
210
+ cwd: "${workspaceFolder}",
211
+ },
212
+ },
213
+ };
214
+
215
+ await writeFile(join(targetDir, ".mcp.json"), JSON.stringify(mcpConfig, null, 2) + "\n");
216
+ console.log(pc.green("✓ Created .mcp.json\n"));
217
+
218
+ // Show IDE-specific instructions
219
+ if (ide === "claude") {
220
+ console.log(pc.cyan("Claude Code Setup:"));
221
+ console.log(pc.dim("─".repeat(40)));
222
+ console.log(`
223
+ The .mcp.json file has been created in your project.
224
+ Claude Code will automatically detect and use this configuration.
225
+
226
+ ${pc.bold("To verify:")}
227
+ 1. Open Claude Code in this project directory
228
+ 2. The MCP tools should be available automatically
229
+ 3. Try asking Claude to "list plugins" or "get project info"
230
+ `);
231
+ } else if (ide === "cursor") {
232
+ console.log(pc.cyan("Cursor Setup:"));
233
+ console.log(pc.dim("─".repeat(40)));
234
+ console.log(`
235
+ ${pc.bold("To configure Cursor:")}
236
+ 1. Open Cursor Settings (Cmd/Ctrl + ,)
237
+ 2. Search for "MCP" or "Model Context Protocol"
238
+ 3. Add the donkeylabs server from .mcp.json
239
+ 4. Restart Cursor to apply changes
240
+ `);
241
+ } else if (ide === "windsurf") {
242
+ console.log(pc.cyan("Windsurf Setup:"));
243
+ console.log(pc.dim("─".repeat(40)));
244
+ console.log(`
245
+ ${pc.bold("To configure Windsurf:")}
246
+ 1. Open Windsurf settings
247
+ 2. Navigate to AI / MCP configuration
248
+ 3. Add the donkeylabs server from .mcp.json
249
+ `);
250
+ }
251
+ }
161
252
 
162
- 4. Preview production build:
163
- ${pc.cyan("bun run preview")}
253
+ // Print final success message
254
+ console.log(pc.bold(pc.green("\n🎉 Project ready!\n")));
255
+
256
+ if (projectType === "server") {
257
+ console.log(`${pc.bold("Start development:")}
258
+ ${pc.cyan("cd " + (projectDir !== "." ? projectDir : ""))}
259
+ ${pc.cyan("bun run dev")}
260
+ `);
261
+ } else {
262
+ console.log(`${pc.bold("Start development:")}
263
+ ${projectDir !== "." ? pc.cyan("cd " + projectDir) + "\n " : ""}${pc.cyan("bun run dev")}
164
264
 
165
265
  ${pc.bold("Project structure:")}
166
- src/server/index.ts - Your @donkeylabs/server API
167
- src/lib/api.ts - Typed API client
168
- src/routes/ - SvelteKit pages
169
- src/hooks.server.ts - Server hooks for SSR
266
+ src/server/ - @donkeylabs/server API
267
+ src/lib/api.ts - Typed API client
268
+ src/routes/ - SvelteKit pages
170
269
  `);
171
270
  }
271
+
272
+ if (setupMcp) {
273
+ console.log(pc.dim("MCP is configured. Your AI assistant can now help you build!"));
274
+ }
275
+ }
276
+
277
+ /**
278
+ * Run a command and return success status
279
+ */
280
+ async function runCommand(cmd: string, args: string[], cwd: string): Promise<boolean> {
281
+ return new Promise((resolve) => {
282
+ const child = spawn(cmd, args, {
283
+ stdio: "inherit",
284
+ cwd,
285
+ });
286
+
287
+ child.on("close", (code) => {
288
+ resolve(code === 0);
289
+ });
290
+
291
+ child.on("error", () => {
292
+ resolve(false);
293
+ });
294
+ });
172
295
  }
173
296
 
174
297
  /**
@@ -199,3 +322,31 @@ async function copyDirectory(src: string, dest: string): Promise<void> {
199
322
  }
200
323
  }
201
324
  }
325
+
326
+ /**
327
+ * Copy CLAUDE.md and docs/ from @donkeylabs/server to project root
328
+ * for AI-assisted development
329
+ */
330
+ async function copyDocsFromServer(targetDir: string): Promise<void> {
331
+ const serverPkgPath = join(targetDir, "node_modules", "@donkeylabs", "server");
332
+
333
+ if (!existsSync(serverPkgPath)) {
334
+ return; // Server package not installed
335
+ }
336
+
337
+ // Copy CLAUDE.md
338
+ const claudeMdSrc = join(serverPkgPath, "CLAUDE.md");
339
+ if (existsSync(claudeMdSrc)) {
340
+ const claudeMdDest = join(targetDir, "CLAUDE.md");
341
+ await copyFile(claudeMdSrc, claudeMdDest);
342
+ console.log(pc.green(" Created:"), "CLAUDE.md (AI instructions)");
343
+ }
344
+
345
+ // Copy docs/ directory
346
+ const docsSrc = join(serverPkgPath, "docs");
347
+ if (existsSync(docsSrc)) {
348
+ const docsDest = join(targetDir, "docs");
349
+ await copyDirectory(docsSrc, docsDest);
350
+ console.log(pc.green(" Created:"), "docs/ (detailed documentation)");
351
+ }
352
+ }