@donkeylabs/cli 2.0.6 → 2.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@donkeylabs/cli",
3
- "version": "2.0.6",
3
+ "version": "2.0.7",
4
4
  "type": "module",
5
5
  "description": "CLI for @donkeylabs/server - project scaffolding and code generation",
6
6
  "main": "./src/index.ts",
@@ -568,7 +568,7 @@ async function extractRoutesFromServer(entryPath: string): Promise<RouteInfo[]>
568
568
  timedOut = true;
569
569
  child.kill("SIGTERM");
570
570
  console.warn(pc.yellow(`Route extraction timed out after ${TIMEOUT_MS / 1000}s`));
571
- console.warn(pc.dim("Make sure your entry file handles DONKEYLABS_GENERATE=1 and calls process.exit(0)"));
571
+ console.warn(pc.dim("Make sure routes are registered with server.use() before any blocking operations"));
572
572
  resolve([]);
573
573
  }, TIMEOUT_MS);
574
574
 
@@ -767,31 +767,38 @@ export async function generateCommand(_args: string[]): Promise<void> {
767
767
  // Determine client output path
768
768
  const clientOutput = config.client?.output || join(outPath, "client.ts");
769
769
 
770
- // Check if adapter provides a custom generator
770
+ // Generate client using adapter (required for SvelteKit projects)
771
771
  if (config.adapter) {
772
+ // Resolve the adapter path from the project's node_modules
773
+ const adapterPath = join(process.cwd(), "node_modules", config.adapter, "src/generator/index.ts");
774
+
775
+ if (!existsSync(adapterPath)) {
776
+ console.error(pc.red(`Adapter not found: ${config.adapter}`));
777
+ console.error(pc.dim(`Expected path: ${adapterPath}`));
778
+ console.error(pc.dim(`Run: bun install`));
779
+ process.exit(1);
780
+ }
781
+
772
782
  try {
773
- // Resolve the adapter path from the project's node_modules
774
- const adapterPath = join(process.cwd(), "node_modules", config.adapter, "src/generator/index.ts");
775
- if (existsSync(adapterPath)) {
776
- const adapterModule = await import(adapterPath);
777
- if (adapterModule.generateClient) {
778
- await adapterModule.generateClient(config, serverRoutes, clientOutput);
779
- generated.push(`client (${config.adapter})`);
780
- console.log(pc.green("Generated:"), generated.map(g => pc.dim(g)).join(", "));
781
- return;
782
- }
783
+ const adapterModule = await import(adapterPath);
784
+ if (!adapterModule.generateClient) {
785
+ console.error(pc.red(`Adapter ${config.adapter} does not export generateClient`));
786
+ process.exit(1);
783
787
  }
788
+ await adapterModule.generateClient(config, serverRoutes, clientOutput);
789
+ generated.push(`client (${config.adapter})`);
784
790
  } catch (e: any) {
785
- // Adapter doesn't provide generator or import failed, fall back to default
786
- console.log(pc.dim(`Note: Adapter ${config.adapter} has no custom generator, using default`));
787
- console.log(pc.dim(`Error: ${e.message}`));
791
+ console.error(pc.red(`Failed to run adapter generator: ${config.adapter}`));
792
+ console.error(pc.dim(e.message));
793
+ if (e.stack) console.error(pc.dim(e.stack));
794
+ process.exit(1);
788
795
  }
796
+ } else {
797
+ // No adapter - skip client generation (user must specify adapter for typed clients)
798
+ console.log(pc.yellow("No adapter specified - skipping client generation"));
799
+ console.log(pc.dim('Add adapter: "@donkeylabs/adapter-sveltekit" to donkeylabs.config.ts for typed client'));
789
800
  }
790
801
 
791
- // Default client generation
792
- await generateClientFromRoutes(serverRoutes, clientOutput);
793
- generated.push("client");
794
-
795
802
  console.log(pc.green("Generated:"), generated.map(g => pc.dim(g)).join(", "));
796
803
  }
797
804
 
@@ -938,7 +945,7 @@ async function generateContext(
938
945
  const hasServices = serviceEntries.length > 0;
939
946
  const servicesTypeDecl = hasServices
940
947
  ? `export interface AppServices {\n${serviceEntries.join("\n")}\n }`
941
- : "export type AppServices = Record<string, never>;";
948
+ : "// eslint-disable-next-line @typescript-eslint/no-empty-interface\n export interface AppServices {}";
942
949
 
943
950
  const content = `// Auto-generated by donkeylabs generate
944
951
  // App context - import as: import type { AppContext } from ".@donkeylabs/server/context";
@@ -1266,97 +1273,3 @@ ${eventRegistryEntries.join("\n")}
1266
1273
  await writeFile(join(outPath, "events.ts"), content);
1267
1274
  }
1268
1275
 
1269
- async function generateClientFromRoutes(
1270
- routes: ExtractedRoute[],
1271
- outputPath: string
1272
- ): Promise<void> {
1273
- // Group routes by namespace (first part of route name)
1274
- // e.g., "api.hello.test" -> namespace "api", sub "hello", method "test"
1275
- const tree = new Map<string, Map<string, { method: string; fullName: string }[]>>();
1276
-
1277
- for (const route of routes) {
1278
- const parts = route.name.split(".");
1279
- if (parts.length < 2) {
1280
- // Single-level route like "ping" -> namespace "", method "ping"
1281
- const ns = "";
1282
- if (!tree.has(ns)) tree.set(ns, new Map());
1283
- const rootMethods = tree.get(ns)!;
1284
- if (!rootMethods.has("")) rootMethods.set("", []);
1285
- rootMethods.get("")!.push({ method: parts[0]!, fullName: route.name });
1286
- } else if (parts.length === 2) {
1287
- // Two-level route like "health.ping" -> namespace "health", method "ping"
1288
- const [ns, method] = parts;
1289
- if (!tree.has(ns!)) tree.set(ns!, new Map());
1290
- const nsMethods = tree.get(ns!)!;
1291
- if (!nsMethods.has("")) nsMethods.set("", []);
1292
- nsMethods.get("")!.push({ method: method!, fullName: route.name });
1293
- } else {
1294
- // Multi-level route like "api.hello.test" -> namespace "api", sub "hello", method "test"
1295
- const [ns, sub, ...rest] = parts;
1296
- const method = rest.join(".");
1297
- if (!tree.has(ns!)) tree.set(ns!, new Map());
1298
- const nsMethods = tree.get(ns!)!;
1299
- if (!nsMethods.has(sub!)) nsMethods.set(sub!, []);
1300
- nsMethods.get(sub!)!.push({ method: method || sub!, fullName: route.name });
1301
- }
1302
- }
1303
-
1304
- // Generate method definitions
1305
- const namespaceBlocks: string[] = [];
1306
-
1307
- for (const [namespace, subNamespaces] of tree) {
1308
- if (namespace === "") {
1309
- // Root-level methods
1310
- const rootMethods = subNamespaces.get("");
1311
- if (rootMethods && rootMethods.length > 0) {
1312
- for (const { method, fullName } of rootMethods) {
1313
- const methodName = method.replace(/-([a-z])/g, (_: string, c: string) => c.toUpperCase());
1314
- namespaceBlocks.push(` ${methodName} = (input: any) => this.request("${fullName}", input);`);
1315
- }
1316
- }
1317
- continue;
1318
- }
1319
-
1320
- const subBlocks: string[] = [];
1321
- for (const [sub, methods] of subNamespaces) {
1322
- if (sub === "") {
1323
- // Direct methods on namespace
1324
- for (const { method, fullName } of methods) {
1325
- const methodName = method.replace(/-([a-z])/g, (_: string, c: string) => c.toUpperCase());
1326
- subBlocks.push(` ${methodName}: (input: any) => this.request("${fullName}", input)`);
1327
- }
1328
- } else {
1329
- // Sub-namespace methods
1330
- const subMethods = methods.map(({ method, fullName }) => {
1331
- const methodName = method.replace(/-([a-z])/g, (_: string, c: string) => c.toUpperCase());
1332
- return ` ${methodName}: (input: any) => this.request("${fullName}", input)`;
1333
- });
1334
- subBlocks.push(` ${sub}: {\n${subMethods.join(",\n")}\n }`);
1335
- }
1336
- }
1337
-
1338
- namespaceBlocks.push(` ${namespace} = {\n${subBlocks.join(",\n")}\n };`);
1339
- }
1340
-
1341
- const content = `// Auto-generated by donkeylabs generate
1342
- // API Client
1343
-
1344
- import { ApiClientBase, type ApiClientOptions } from "@donkeylabs/server/client";
1345
-
1346
- export class ApiClient extends ApiClientBase<{}> {
1347
- constructor(baseUrl: string, options?: ApiClientOptions) {
1348
- super(baseUrl, options);
1349
- }
1350
-
1351
- ${namespaceBlocks.join("\n\n") || " // No routes defined"}
1352
- }
1353
-
1354
- export function createApiClient(baseUrl: string, options?: ApiClientOptions) {
1355
- return new ApiClient(baseUrl, options);
1356
- }
1357
- `;
1358
-
1359
- const outputDir = dirname(outputPath);
1360
- await mkdir(outputDir, { recursive: true });
1361
- await writeFile(outputPath, content);
1362
- }
@@ -9,13 +9,13 @@
9
9
  "gen:types": "donkeylabs generate"
10
10
  },
11
11
  "dependencies": {
12
- "@donkeylabs/server": "latest",
12
+ "@donkeylabs/server": "^2.0.10",
13
13
  "kysely": "^0.27.0",
14
14
  "kysely-bun-sqlite": "^0.3.0",
15
15
  "zod": "^3.24.0"
16
16
  },
17
17
  "devDependencies": {
18
- "@donkeylabs/cli": "latest",
18
+ "@donkeylabs/cli": "^2.0.7",
19
19
  "@types/bun": "latest"
20
20
  }
21
21
  }
@@ -4,8 +4,7 @@
4
4
  "version": "0.0.1",
5
5
  "type": "module",
6
6
  "scripts": {
7
- "dev": "bun scripts/dev.ts",
8
- "dev:watch": "bun --watch --no-clear-screen scripts/watch-server.ts",
7
+ "dev": "bun --bun vite dev",
9
8
  "build": "bun run gen:types && vite build",
10
9
  "preview": "bun build/server/entry.js",
11
10
  "prepare": "bun --bun svelte-kit sync || echo ''",
@@ -24,9 +23,9 @@
24
23
  "vite": "^7.2.6"
25
24
  },
26
25
  "dependencies": {
27
- "@donkeylabs/cli": "^1.1.19",
28
- "@donkeylabs/adapter-sveltekit": "^1.1.19",
29
- "@donkeylabs/server": "^1.1.19",
26
+ "@donkeylabs/cli": "^2.0.7",
27
+ "@donkeylabs/adapter-sveltekit": "^2.0.9",
28
+ "@donkeylabs/server": "^2.0.10",
30
29
  "bits-ui": "^2.15.4",
31
30
  "clsx": "^2.1.1",
32
31
  "kysely": "^0.27.6",
@@ -26,6 +26,11 @@ export const server = new AppServer({
26
26
  generateTypes: {
27
27
  output: "./src/lib/api.ts",
28
28
  },
29
+ // Admin dashboard - enabled in dev mode
30
+ admin: {
31
+ enabled: true,
32
+ prefix: "admin",
33
+ },
29
34
  });
30
35
 
31
36
  // =============================================================================
@@ -119,6 +124,3 @@ server.use(permissionsRouter);
119
124
  server.use(tenantsRouter);
120
125
  server.use(demoRoutes);
121
126
  server.use(exampleRouter);
122
-
123
- // Handle CLI type generation (must be after routes are registered)
124
- server.handleGenerateMode();
@@ -157,7 +157,11 @@ export const demoPlugin = createPlugin.define({
157
157
  return { success: true };
158
158
  },
159
159
  wsGetClients: (channel?: string) => {
160
- const clients = ctx.core.websocket.getClients(channel);
160
+ const allClients = ctx.core.websocket.getClients();
161
+ // Filter by channel if provided
162
+ const clients = channel
163
+ ? allClients.filter((c) => c.channels.includes(channel))
164
+ : allClients;
161
165
  return {
162
166
  count: clients.length,
163
167
  clients,
@@ -435,7 +435,13 @@ demo.route("websocket.clients").typed(
435
435
  }),
436
436
  output: z.object({
437
437
  count: z.number(),
438
- clients: z.array(z.string()),
438
+ clients: z.array(
439
+ z.object({
440
+ id: z.string(),
441
+ connectedAt: z.date(),
442
+ channels: z.array(z.string()),
443
+ })
444
+ ),
439
445
  }),
440
446
  handle: async (input, ctx) => {
441
447
  return ctx.plugins.demo.wsGetClients(input.channel);
@@ -1,53 +0,0 @@
1
- #!/usr/bin/env bun
2
- // Dev script that runs vite and watcher together, ensuring cleanup on exit
3
-
4
- import { spawn, type Subprocess } from "bun";
5
-
6
- const children: Subprocess[] = [];
7
-
8
- function cleanup() {
9
- for (const child of children) {
10
- try {
11
- child.kill();
12
- } catch {}
13
- }
14
- process.exit(0);
15
- }
16
-
17
- // Handle all exit signals
18
- process.on("SIGTERM", cleanup);
19
- process.on("SIGINT", cleanup);
20
- process.on("exit", cleanup);
21
-
22
- // Generate types first
23
- console.log("\x1b[36m[dev]\x1b[0m Generating types...");
24
- const genResult = Bun.spawnSync(["bun", "run", "gen:types"], {
25
- stdout: "inherit",
26
- stderr: "inherit",
27
- });
28
-
29
- if (genResult.exitCode !== 0) {
30
- console.error("\x1b[31m[dev]\x1b[0m Failed to generate types");
31
- process.exit(1);
32
- }
33
-
34
- // Start watcher
35
- console.log("\x1b[36m[dev]\x1b[0m Starting file watcher...");
36
- const watcher = spawn(["bun", "--watch", "--no-clear-screen", "scripts/watch-server.ts"], {
37
- stdout: "inherit",
38
- stderr: "inherit",
39
- });
40
- children.push(watcher);
41
-
42
- // Start vite
43
- console.log("\x1b[36m[dev]\x1b[0m Starting Vite dev server...");
44
- const vite = spawn(["bun", "--bun", "vite", "dev"], {
45
- stdout: "inherit",
46
- stderr: "inherit",
47
- stdin: "inherit",
48
- });
49
- children.push(vite);
50
-
51
- // When vite exits, cleanup everything
52
- await vite.exited;
53
- cleanup();
@@ -1,79 +0,0 @@
1
- // Watch server files and regenerate types on changes
2
- import { watch } from "node:fs";
3
- import { exec } from "node:child_process";
4
- import { promisify } from "node:util";
5
- import { join } from "node:path";
6
-
7
- const execAsync = promisify(exec);
8
-
9
- const serverDir = join(import.meta.dir, "..", "src", "server");
10
- let isGenerating = false;
11
- let lastGenerationTime = 0;
12
-
13
- // Files/patterns we generate - ignore changes to these
14
- const IGNORED_PATTERNS = [
15
- /schema\.ts$/, // Generated schema files
16
- /\.d\.ts$/, // Type declaration files
17
- ];
18
-
19
- // Cooldown period after generation (ms)
20
- const COOLDOWN_MS = 2000;
21
- const DEBOUNCE_MS = 500;
22
-
23
- // Handle signals for clean shutdown (from parent dev.ts)
24
- process.on("SIGTERM", () => process.exit(0));
25
- process.on("SIGINT", () => process.exit(0));
26
-
27
- function shouldIgnoreFile(filename: string): boolean {
28
- return IGNORED_PATTERNS.some(pattern => pattern.test(filename));
29
- }
30
-
31
- async function regenerate() {
32
- // Check cooldown
33
- const now = Date.now();
34
- if (now - lastGenerationTime < COOLDOWN_MS) {
35
- return;
36
- }
37
-
38
- if (isGenerating) {
39
- return;
40
- }
41
-
42
- isGenerating = true;
43
- lastGenerationTime = now;
44
- console.log("\x1b[36m[watch]\x1b[0m Server files changed, regenerating types...");
45
-
46
- try {
47
- await execAsync("bun run gen:types");
48
- console.log("\x1b[32m[watch]\x1b[0m Types regenerated successfully");
49
- } catch (e: any) {
50
- console.error("\x1b[31m[watch]\x1b[0m Error regenerating types:", e.message);
51
- } finally {
52
- isGenerating = false;
53
- lastGenerationTime = Date.now(); // Update after generation completes
54
- }
55
- }
56
-
57
- // Debounce to avoid multiple rapid regenerations
58
- let debounceTimer: Timer | null = null;
59
-
60
- function debouncedRegenerate() {
61
- if (debounceTimer) clearTimeout(debounceTimer);
62
- debounceTimer = setTimeout(regenerate, DEBOUNCE_MS);
63
- }
64
-
65
- // Watch server directory recursively
66
- watch(serverDir, { recursive: true }, (eventType, filename) => {
67
- if (!filename) return;
68
- if (!filename.endsWith(".ts")) return;
69
-
70
- // Ignore generated files
71
- if (shouldIgnoreFile(filename)) return;
72
-
73
- debouncedRegenerate();
74
- });
75
-
76
- console.log("\x1b[36m[watch]\x1b[0m Watching src/server/ for changes...");
77
-
78
- // Keep process alive
79
- await new Promise(() => {});