@donkeylabs/cli 2.1.0 → 2.3.0

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.1.0",
3
+ "version": "2.3.0",
4
4
  "type": "module",
5
5
  "description": "CLI for @donkeylabs/server - project scaffolding and code generation",
6
6
  "main": "./src/index.ts",
@@ -18,7 +18,8 @@ export async function generateClientCommand(
18
18
  // Extract routes from server
19
19
  const entryPath = config.entry || "./src/index.ts";
20
20
  console.log(pc.dim(`Extracting routes from ${entryPath}...`));
21
- const routes = await extractRoutesFromServer(entryPath);
21
+ const serverOutput = await extractRoutesFromServer(entryPath);
22
+ const routes = serverOutput.routes;
22
23
 
23
24
  if (routes.length === 0) {
24
25
  console.warn(pc.yellow("No routes found - generating empty client"));
@@ -36,15 +36,27 @@ export interface RouteInfo {
36
36
  eventsSource?: Record<string, string>;
37
37
  }
38
38
 
39
+ export interface ProcessInfo {
40
+ name: string;
41
+ events?: Record<string, string>;
42
+ commands?: Record<string, string>;
43
+ }
44
+
45
+ export interface ServerOutput {
46
+ routes: RouteInfo[];
47
+ processes: ProcessInfo[];
48
+ coreEvents?: Record<string, string>;
49
+ }
50
+
39
51
  /**
40
52
  * Run the server entry file with DONKEYLABS_GENERATE=1 to get typed route metadata
41
53
  */
42
- export async function extractRoutesFromServer(entryPath: string): Promise<RouteInfo[]> {
54
+ export async function extractRoutesFromServer(entryPath: string): Promise<ServerOutput> {
43
55
  const fullPath = join(process.cwd(), entryPath);
44
56
 
45
57
  if (!existsSync(fullPath)) {
46
58
  console.warn(pc.yellow(`Entry file not found: ${entryPath}`));
47
- return [];
59
+ return { routes: [], processes: [] };
48
60
  }
49
61
 
50
62
  const TIMEOUT_MS = 10000; // 10 second timeout
@@ -66,7 +78,7 @@ export async function extractRoutesFromServer(entryPath: string): Promise<RouteI
66
78
  child.kill("SIGTERM");
67
79
  console.warn(pc.yellow(`Route extraction timed out after ${TIMEOUT_MS / 1000}s`));
68
80
  console.warn(pc.dim("Make sure routes are registered with server.use() before any blocking operations"));
69
- resolve([]);
81
+ resolve({ routes: [], processes: [] });
70
82
  }, TIMEOUT_MS);
71
83
 
72
84
  child.stdout?.on("data", (data) => {
@@ -84,7 +96,7 @@ export async function extractRoutesFromServer(entryPath: string): Promise<RouteI
84
96
  if (code !== 0) {
85
97
  console.warn(pc.yellow(`Failed to extract routes from server (exit code ${code})`));
86
98
  if (stderr) console.warn(pc.dim(stderr));
87
- resolve([]);
99
+ resolve({ routes: [], processes: [] });
88
100
  return;
89
101
  }
90
102
 
@@ -105,17 +117,25 @@ export async function extractRoutesFromServer(entryPath: string): Promise<RouteI
105
117
  eventsSource: r.eventsType,
106
118
  };
107
119
  });
108
- resolve(routes);
120
+
121
+ // Parse process definitions
122
+ const processes: ProcessInfo[] = (result.processes || []).map((p: any) => ({
123
+ name: p.name,
124
+ events: p.events,
125
+ commands: p.commands,
126
+ }));
127
+
128
+ resolve({ routes, processes, coreEvents: result.coreEvents });
109
129
  } catch (e) {
110
130
  console.warn(pc.yellow("Failed to parse route data from server"));
111
- resolve([]);
131
+ resolve({ routes: [], processes: [] });
112
132
  }
113
133
  });
114
134
 
115
135
  child.on("error", (err) => {
116
136
  clearTimeout(timeout);
117
137
  console.warn(pc.yellow(`Failed to run entry file: ${err.message}`));
118
- resolve([]);
138
+ resolve({ routes: [], processes: [] });
119
139
  });
120
140
  });
121
141
  }
@@ -7,7 +7,7 @@ import { BunSqliteDialect } from "kysely-bun-sqlite";
7
7
  import { Database } from "bun:sqlite";
8
8
  import { generate, KyselyBunSqliteDialect } from "kysely-codegen";
9
9
  import * as ts from "typescript";
10
- import { loadConfig, extractRoutesFromServer, type DonkeylabsConfig, type RouteInfo } from "./generate-utils";
10
+ import { loadConfig, extractRoutesFromServer, type DonkeylabsConfig, type RouteInfo, type ProcessInfo } from "./generate-utils";
11
11
 
12
12
  async function getPluginExportName(pluginPath: string): Promise<string | null> {
13
13
  try {
@@ -918,9 +918,11 @@ export async function generateCommand(_args: string[]): Promise<void> {
918
918
  );
919
919
  }
920
920
 
921
- // Extract routes by running the server with DONKEYLABS_GENERATE=1
921
+ // Extract routes and processes by running the server with DONKEYLABS_GENERATE=1
922
922
  const entryPath = config.entry || "./src/index.ts";
923
- const serverRoutes = await extractRoutesFromServer(entryPath);
923
+ const serverOutput = await extractRoutesFromServer(entryPath);
924
+ const serverRoutes = serverOutput.routes;
925
+ const serverProcesses = serverOutput.processes;
924
926
 
925
927
  // Find custom service definitions
926
928
  const services = await findServiceDefinitions(entryPath);
@@ -940,13 +942,22 @@ export async function generateCommand(_args: string[]): Promise<void> {
940
942
  );
941
943
  }
942
944
 
945
+ // Log process definitions found
946
+ if (serverProcesses.length > 0) {
947
+ console.log(
948
+ pc.green("Found processes:"),
949
+ serverProcesses.map((p) => pc.dim(p.name)).join(", ")
950
+ );
951
+ }
952
+
943
953
  // Generate all files
944
954
  await generateRegistry(plugins, outPath);
945
955
  await generateContext(plugins, services, outPath);
946
956
  await generateRouteTypes(fileRoutes, outPath);
947
- await generateEventTypes(serverEvents, outPath);
957
+ await generateEventTypes(serverEvents, outPath, serverOutput.coreEvents);
958
+ await generateProcessTypes(serverProcesses, outPath);
948
959
 
949
- const generated = ["registry", "context", "routes", "events"];
960
+ const generated = ["registry", "context", "routes", "events", "processes"];
950
961
 
951
962
  // Determine client output path
952
963
  const clientOutput = config.client?.output || join(outPath, "client.ts");
@@ -1362,9 +1373,17 @@ function parseZodObjectFields(objectContent: string): string {
1362
1373
 
1363
1374
  /**
1364
1375
  * Generate events.ts with namespace-nested types and EventMap.
1376
+ * Includes both core framework events and user-defined events.
1365
1377
  */
1366
- async function generateEventTypes(events: EventDefinitionInfo[], outPath: string): Promise<void> {
1367
- if (events.length === 0) {
1378
+ async function generateEventTypes(
1379
+ events: EventDefinitionInfo[],
1380
+ outPath: string,
1381
+ coreEvents?: Record<string, string>
1382
+ ): Promise<void> {
1383
+ const hasCoreEvents = coreEvents && Object.keys(coreEvents).length > 0;
1384
+ const hasUserEvents = events.length > 0;
1385
+
1386
+ if (!hasCoreEvents && !hasUserEvents) {
1368
1387
  // Still generate an empty file for consistency
1369
1388
  const content = `// Auto-generated by donkeylabs generate
1370
1389
  // Events - import as: import { type EventMap, type EventName } from ".@donkeylabs/server/events";
@@ -1387,10 +1406,32 @@ declare module "@donkeylabs/server" {
1387
1406
  return;
1388
1407
  }
1389
1408
 
1390
- // Group events by namespace (first part of event name)
1409
+ // Group ALL events by namespace (first part of event name)
1391
1410
  // e.g., "order.created" -> namespace "Order", event "Created"
1392
- const byNamespace = new Map<string, { eventName: string; pascalName: string; schemaSource: string; fullName: string }[]>();
1411
+ // e.g., "workflow.step.started" -> namespace "Workflow", event "StepStarted"
1412
+ const byNamespace = new Map<string, { eventName: string; pascalName: string; tsType: string; fullName: string; isCore: boolean }[]>();
1413
+
1414
+ // Add core events first (using pre-computed TS type strings)
1415
+ if (hasCoreEvents) {
1416
+ for (const [eventFullName, tsTypeString] of Object.entries(coreEvents!)) {
1417
+ const parts = eventFullName.split(".");
1418
+ const namespace = toPascalCase(parts[0] || "App");
1419
+ const eventName = parts.slice(1).map(p => toPascalCase(p)).join("") || toPascalCase(parts[0] || "Event");
1420
+
1421
+ if (!byNamespace.has(namespace)) {
1422
+ byNamespace.set(namespace, []);
1423
+ }
1424
+ byNamespace.get(namespace)!.push({
1425
+ eventName,
1426
+ pascalName: eventName,
1427
+ tsType: tsTypeString,
1428
+ fullName: eventFullName,
1429
+ isCore: true,
1430
+ });
1431
+ }
1432
+ }
1393
1433
 
1434
+ // Add user-defined events (from Zod schemas)
1394
1435
  for (const event of events) {
1395
1436
  const parts = event.name.split(".");
1396
1437
  const namespace = toPascalCase(parts[0] || "App");
@@ -1402,25 +1443,41 @@ declare module "@donkeylabs/server" {
1402
1443
  byNamespace.get(namespace)!.push({
1403
1444
  eventName,
1404
1445
  pascalName: eventName,
1405
- schemaSource: event.schemaSource,
1446
+ tsType: zodSchemaToTypeScript(event.schemaSource),
1406
1447
  fullName: event.name,
1448
+ isCore: false,
1407
1449
  });
1408
1450
  }
1409
1451
 
1410
1452
  // Generate namespace blocks
1411
1453
  const namespaceBlocks: string[] = [];
1454
+ const coreNamespaceBlocks: string[] = [];
1455
+ const userNamespaceBlocks: string[] = [];
1412
1456
  const eventMapEntries: string[] = [];
1413
1457
  const eventRegistryEntries: string[] = [];
1414
1458
  const eventNames: string[] = [];
1415
1459
 
1460
+ // Track which namespaces are core-only, user-only, or mixed
1416
1461
  for (const [namespace, nsEvents] of byNamespace) {
1462
+ const hasCoreInNs = nsEvents.some(e => e.isCore);
1463
+ const hasUserInNs = nsEvents.some(e => !e.isCore);
1464
+
1417
1465
  const eventTypeDecls = nsEvents.map(e => {
1418
- const tsType = zodSchemaToTypeScript(e.schemaSource);
1419
1466
  return ` /** Event data for "${e.fullName}" */
1420
- export type ${e.pascalName} = ${tsType};`;
1467
+ export type ${e.pascalName} = ${e.tsType};`;
1421
1468
  }).join("\n\n");
1422
1469
 
1423
- namespaceBlocks.push(`export namespace ${namespace} {\n${eventTypeDecls}\n}`);
1470
+ const block = `export namespace ${namespace} {\n${eventTypeDecls}\n}`;
1471
+
1472
+ if (hasCoreInNs && !hasUserInNs) {
1473
+ coreNamespaceBlocks.push(block);
1474
+ } else if (!hasCoreInNs && hasUserInNs) {
1475
+ userNamespaceBlocks.push(block);
1476
+ } else {
1477
+ // Mixed - put in core section
1478
+ coreNamespaceBlocks.push(block);
1479
+ }
1480
+ namespaceBlocks.push(block);
1424
1481
 
1425
1482
  // Add to EventMap and EventRegistry
1426
1483
  for (const e of nsEvents) {
@@ -1430,11 +1487,20 @@ declare module "@donkeylabs/server" {
1430
1487
  }
1431
1488
  }
1432
1489
 
1490
+ // Build the output with clear sections
1491
+ const sections: string[] = [];
1492
+
1493
+ if (coreNamespaceBlocks.length > 0) {
1494
+ sections.push(`// Core framework events\n${coreNamespaceBlocks.join("\n\n")}`);
1495
+ }
1496
+ if (userNamespaceBlocks.length > 0) {
1497
+ sections.push(`// User-defined events\n${userNamespaceBlocks.join("\n\n")}`);
1498
+ }
1499
+
1433
1500
  const content = `// Auto-generated by donkeylabs generate
1434
1501
  // Events - import as: import { type EventMap, type EventName, Order, User } from ".@donkeylabs/server/events";
1435
1502
 
1436
- // Event type namespaces - use as Order.Created, User.Signup, etc.
1437
- ${namespaceBlocks.join("\n\n")}
1503
+ ${sections.join("\n\n")}
1438
1504
 
1439
1505
  /** Map of all event names to their data types */
1440
1506
  export interface EventMap {
@@ -1454,3 +1520,99 @@ ${eventRegistryEntries.join("\n")}
1454
1520
 
1455
1521
  await writeFile(join(outPath, "events.ts"), content);
1456
1522
  }
1523
+
1524
+ /**
1525
+ * Generate processes.ts with typed events, commands, and ProcessRegistry augmentation.
1526
+ */
1527
+ async function generateProcessTypes(processes: ProcessInfo[], outPath: string): Promise<void> {
1528
+ if (processes.length === 0) {
1529
+ const content = `// Auto-generated by donkeylabs generate
1530
+ // Processes - import as: import { type ProcessEventMap, type ProcessCommandMap } from ".@donkeylabs/server/processes";
1531
+
1532
+ /** Map of all process event names to their data types */
1533
+ export interface ProcessEventMap {}
1534
+
1535
+ /** Map of all process command names to their data types */
1536
+ export interface ProcessCommandMap {}
1537
+
1538
+ // Augment ProcessRegistry for typed send() (empty when no processes defined)
1539
+ declare module "@donkeylabs/server" {
1540
+ interface ProcessRegistry {}
1541
+ }
1542
+ `;
1543
+ await writeFile(join(outPath, "processes.ts"), content);
1544
+ return;
1545
+ }
1546
+
1547
+ const namespaceBlocks: string[] = [];
1548
+ const eventMapEntries: string[] = [];
1549
+ const commandMapEntries: string[] = [];
1550
+ const registryEntries: string[] = [];
1551
+
1552
+ for (const proc of processes) {
1553
+ const pascalName = toPascalCase(proc.name);
1554
+
1555
+ const eventDecls: string[] = [];
1556
+ const commandDecls: string[] = [];
1557
+
1558
+ // Generate event types
1559
+ if (proc.events) {
1560
+ for (const [eventName, tsType] of Object.entries(proc.events)) {
1561
+ const pascalEvent = toPascalCase(eventName);
1562
+ eventDecls.push(` export type ${pascalEvent} = ${tsType};`);
1563
+ eventMapEntries.push(` "${proc.name}.${eventName}": ${pascalName}.Events.${pascalEvent};`);
1564
+ }
1565
+ }
1566
+
1567
+ // Generate command types
1568
+ if (proc.commands) {
1569
+ for (const [cmdName, tsType] of Object.entries(proc.commands)) {
1570
+ const pascalCmd = toPascalCase(cmdName);
1571
+ commandDecls.push(` export type ${pascalCmd} = ${tsType};`);
1572
+ commandMapEntries.push(` "${proc.name}.${cmdName}": ${pascalName}.Commands.${pascalCmd};`);
1573
+ }
1574
+ }
1575
+
1576
+ // Build namespace
1577
+ const eventsNs = eventDecls.length > 0
1578
+ ? ` export namespace Events {\n${eventDecls.join("\n")}\n }`
1579
+ : ` export namespace Events {}`;
1580
+ const commandsNs = commandDecls.length > 0
1581
+ ? ` export namespace Commands {\n${commandDecls.join("\n")}\n }`
1582
+ : ` export namespace Commands {}`;
1583
+
1584
+ namespaceBlocks.push(`export namespace ${pascalName} {\n${eventsNs}\n${commandsNs}\n}`);
1585
+
1586
+ // Registry entry
1587
+ registryEntries.push(` "${proc.name}": {
1588
+ events: ${pascalName}.Events;
1589
+ commands: ${pascalName}.Commands;
1590
+ };`);
1591
+ }
1592
+
1593
+ const content = `// Auto-generated by donkeylabs generate
1594
+ // Processes - import as: import { type ProcessEventMap, type ProcessCommandMap } from ".@donkeylabs/server/processes";
1595
+
1596
+ // Process type namespaces
1597
+ ${namespaceBlocks.join("\n\n")}
1598
+
1599
+ /** Map of all process event names to their data types */
1600
+ export interface ProcessEventMap {
1601
+ ${eventMapEntries.join("\n")}
1602
+ }
1603
+
1604
+ /** Map of all process command names to their data types */
1605
+ export interface ProcessCommandMap {
1606
+ ${commandMapEntries.join("\n")}
1607
+ }
1608
+
1609
+ // Augment ProcessRegistry for typed send()
1610
+ declare module "@donkeylabs/server" {
1611
+ interface ProcessRegistry {
1612
+ ${registryEntries.join("\n")}
1613
+ }
1614
+ }
1615
+ `;
1616
+
1617
+ await writeFile(join(outPath, "processes.ts"), content);
1618
+ }