@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
@@ -0,0 +1,305 @@
1
+ /**
2
+ * MCP (Model Context Protocol) setup command
3
+ *
4
+ * Sets up the @donkeylabs/mcp server for AI-assisted development
5
+ */
6
+
7
+ import { existsSync } from "node:fs";
8
+ import { readFile, writeFile } from "node:fs/promises";
9
+ import { join } from "node:path";
10
+ import { spawn } from "node:child_process";
11
+ import pc from "picocolors";
12
+ import prompts from "prompts";
13
+
14
+ interface McpConfig {
15
+ mcpServers?: Record<string, {
16
+ command: string;
17
+ args: string[];
18
+ cwd?: string;
19
+ env?: Record<string, string>;
20
+ }>;
21
+ }
22
+
23
+ async function detectPackageManager(): Promise<"bun" | "npm" | "pnpm" | "yarn"> {
24
+ if (existsSync("bun.lockb") || existsSync("bun.lock")) return "bun";
25
+ if (existsSync("pnpm-lock.yaml")) return "pnpm";
26
+ if (existsSync("yarn.lock")) return "yarn";
27
+ return "npm";
28
+ }
29
+
30
+ async function installPackage(pkg: string, dev: boolean = true): Promise<boolean> {
31
+ const pm = await detectPackageManager();
32
+
33
+ const args: string[] = [];
34
+ switch (pm) {
35
+ case "bun":
36
+ args.push("add", dev ? "-d" : "", pkg);
37
+ break;
38
+ case "pnpm":
39
+ args.push("add", dev ? "-D" : "", pkg);
40
+ break;
41
+ case "yarn":
42
+ args.push("add", dev ? "-D" : "", pkg);
43
+ break;
44
+ default:
45
+ args.push("install", dev ? "--save-dev" : "--save", pkg);
46
+ }
47
+
48
+ console.log(pc.dim(`$ ${pm} ${args.filter(Boolean).join(" ")}`));
49
+
50
+ return new Promise((resolve) => {
51
+ const child = spawn(pm, args.filter(Boolean), {
52
+ stdio: "inherit",
53
+ cwd: process.cwd(),
54
+ });
55
+
56
+ child.on("close", (code) => {
57
+ resolve(code === 0);
58
+ });
59
+
60
+ child.on("error", () => {
61
+ resolve(false);
62
+ });
63
+ });
64
+ }
65
+
66
+ async function readMcpConfig(): Promise<McpConfig> {
67
+ const configPath = join(process.cwd(), ".mcp.json");
68
+
69
+ if (!existsSync(configPath)) {
70
+ return {};
71
+ }
72
+
73
+ try {
74
+ const content = await readFile(configPath, "utf-8");
75
+ return JSON.parse(content);
76
+ } catch {
77
+ return {};
78
+ }
79
+ }
80
+
81
+ async function writeMcpConfig(config: McpConfig): Promise<void> {
82
+ const configPath = join(process.cwd(), ".mcp.json");
83
+ await writeFile(configPath, JSON.stringify(config, null, 2) + "\n");
84
+ }
85
+
86
+ async function setupClaudeCode(): Promise<void> {
87
+ console.log(pc.cyan("\nClaude Code Setup:"));
88
+ console.log(pc.dim("─".repeat(40)));
89
+ console.log(`
90
+ The .mcp.json file has been created in your project root.
91
+ Claude Code will automatically detect and use this configuration.
92
+
93
+ ${pc.bold("To verify:")}
94
+ 1. Open Claude Code in this project
95
+ 2. The MCP tools should be available automatically
96
+ 3. Try asking Claude to "list plugins" or "get project info"
97
+
98
+ ${pc.bold("Manual setup (if needed):")}
99
+ Add to your Claude Code settings:
100
+ ${pc.dim(JSON.stringify({
101
+ "mcpServers": {
102
+ "donkeylabs": {
103
+ "command": "bunx",
104
+ "args": ["@donkeylabs/mcp"],
105
+ "cwd": "${workspaceFolder}"
106
+ }
107
+ }
108
+ }, null, 2))}
109
+ `);
110
+ }
111
+
112
+ async function setupCursor(): Promise<void> {
113
+ console.log(pc.cyan("\nCursor Setup:"));
114
+ console.log(pc.dim("─".repeat(40)));
115
+ console.log(`
116
+ ${pc.bold("To configure Cursor:")}
117
+ 1. Open Cursor Settings (Cmd/Ctrl + ,)
118
+ 2. Search for "MCP" or "Model Context Protocol"
119
+ 3. Add the donkeylabs server configuration:
120
+
121
+ ${pc.dim(JSON.stringify({
122
+ "donkeylabs": {
123
+ "command": "bunx",
124
+ "args": ["@donkeylabs/mcp"],
125
+ "cwd": "${workspaceFolder}"
126
+ }
127
+ }, null, 2))}
128
+
129
+ 4. Restart Cursor to apply changes
130
+ `);
131
+ }
132
+
133
+ async function setupWindsurf(): Promise<void> {
134
+ console.log(pc.cyan("\nWindsurf Setup:"));
135
+ console.log(pc.dim("─".repeat(40)));
136
+ console.log(`
137
+ ${pc.bold("To configure Windsurf:")}
138
+ 1. Open Windsurf settings
139
+ 2. Navigate to AI / MCP configuration
140
+ 3. Add the donkeylabs server:
141
+
142
+ ${pc.dim(JSON.stringify({
143
+ "donkeylabs": {
144
+ "command": "bunx",
145
+ "args": ["@donkeylabs/mcp"],
146
+ "cwd": "${workspaceFolder}"
147
+ }
148
+ }, null, 2))}
149
+ `);
150
+ }
151
+
152
+ export async function mcpCommand(args: string[]): Promise<void> {
153
+ const subcommand = args[0];
154
+
155
+ if (!subcommand || subcommand === "setup") {
156
+ await setupMcp(args.slice(1));
157
+ } else if (subcommand === "help" || subcommand === "--help") {
158
+ printMcpHelp();
159
+ } else {
160
+ console.error(pc.red(`Unknown mcp subcommand: ${subcommand}`));
161
+ printMcpHelp();
162
+ process.exit(1);
163
+ }
164
+ }
165
+
166
+ function printMcpHelp(): void {
167
+ console.log(`
168
+ ${pc.bold("donkeylabs mcp")} - Setup MCP server for AI-assisted development
169
+
170
+ ${pc.bold("Usage:")}
171
+ donkeylabs mcp Interactive MCP setup
172
+ donkeylabs mcp setup Setup MCP (interactive)
173
+ donkeylabs mcp setup --claude Setup for Claude Code
174
+ donkeylabs mcp setup --cursor Setup for Cursor
175
+ donkeylabs mcp setup --all Setup for all IDEs
176
+
177
+ ${pc.bold("Options:")}
178
+ --claude Configure for Claude Code
179
+ --cursor Configure for Cursor
180
+ --windsurf Configure for Windsurf
181
+ --all Show setup for all IDEs
182
+ --skip-install Skip installing @donkeylabs/mcp package
183
+
184
+ ${pc.bold("What this does:")}
185
+ 1. Installs @donkeylabs/mcp as a dev dependency
186
+ 2. Creates/updates .mcp.json in your project
187
+ 3. Provides IDE-specific setup instructions
188
+
189
+ ${pc.bold("MCP Tools Available:")}
190
+ - get_project_info - View project structure and routes
191
+ - create_plugin - Create new plugins
192
+ - add_service_method - Add methods to plugin services
193
+ - add_migration - Create database migrations
194
+ - create_router - Create new routers
195
+ - add_route - Add routes to routers
196
+ - add_cron - Schedule cron jobs
197
+ - add_event - Register events
198
+ - add_async_job - Register background jobs
199
+ - generate_types - Regenerate types
200
+ - generate_client - Generate API client
201
+ `);
202
+ }
203
+
204
+ async function setupMcp(args: string[]): Promise<void> {
205
+ console.log(pc.bold("\n🔧 Setting up @donkeylabs/mcp\n"));
206
+
207
+ // Check if we're in a donkeylabs project
208
+ const configPath = join(process.cwd(), "donkeylabs.config.ts");
209
+ const hasConfig = existsSync(configPath);
210
+
211
+ if (!hasConfig) {
212
+ console.log(pc.yellow("⚠ No donkeylabs.config.ts found in current directory."));
213
+ console.log(pc.dim(" The MCP server works best in a @donkeylabs/server project."));
214
+ console.log(pc.dim(" Run 'donkeylabs init' to create a new project first.\n"));
215
+
216
+ const { proceed } = await prompts({
217
+ type: "confirm",
218
+ name: "proceed",
219
+ message: "Continue anyway?",
220
+ initial: false,
221
+ });
222
+
223
+ if (!proceed) {
224
+ console.log(pc.dim("Aborted."));
225
+ return;
226
+ }
227
+ }
228
+
229
+ // Parse args for flags
230
+ const skipInstall = args.includes("--skip-install");
231
+ const forClaude = args.includes("--claude");
232
+ const forCursor = args.includes("--cursor");
233
+ const forWindsurf = args.includes("--windsurf");
234
+ const forAll = args.includes("--all");
235
+
236
+ // Install @donkeylabs/mcp if not skipped
237
+ if (!skipInstall) {
238
+ console.log(pc.cyan("Installing @donkeylabs/mcp..."));
239
+ const success = await installPackage("@donkeylabs/mcp");
240
+
241
+ if (!success) {
242
+ console.log(pc.yellow("\n⚠ Package installation failed, but continuing with config setup."));
243
+ console.log(pc.dim(" You can manually install with: bun add -d @donkeylabs/mcp\n"));
244
+ } else {
245
+ console.log(pc.green("✓ Installed @donkeylabs/mcp\n"));
246
+ }
247
+ }
248
+
249
+ // Create/update .mcp.json
250
+ console.log(pc.cyan("Configuring .mcp.json..."));
251
+
252
+ const config = await readMcpConfig();
253
+ config.mcpServers = config.mcpServers || {};
254
+
255
+ config.mcpServers.donkeylabs = {
256
+ command: "bunx",
257
+ args: ["@donkeylabs/mcp"],
258
+ cwd: "${workspaceFolder}",
259
+ };
260
+
261
+ await writeMcpConfig(config);
262
+ console.log(pc.green("✓ Created .mcp.json\n"));
263
+
264
+ // Show IDE-specific instructions
265
+ if (forAll || (!forClaude && !forCursor && !forWindsurf)) {
266
+ // Interactive mode or --all
267
+ if (!forClaude && !forCursor && !forWindsurf && !forAll) {
268
+ const { ide } = await prompts({
269
+ type: "select",
270
+ name: "ide",
271
+ message: "Which IDE are you using?",
272
+ choices: [
273
+ { title: "Claude Code", value: "claude" },
274
+ { title: "Cursor", value: "cursor" },
275
+ { title: "Windsurf", value: "windsurf" },
276
+ { title: "Show all", value: "all" },
277
+ ],
278
+ });
279
+
280
+ if (ide === "claude") await setupClaudeCode();
281
+ else if (ide === "cursor") await setupCursor();
282
+ else if (ide === "windsurf") await setupWindsurf();
283
+ else {
284
+ await setupClaudeCode();
285
+ await setupCursor();
286
+ await setupWindsurf();
287
+ }
288
+ } else {
289
+ await setupClaudeCode();
290
+ await setupCursor();
291
+ await setupWindsurf();
292
+ }
293
+ } else {
294
+ if (forClaude) await setupClaudeCode();
295
+ if (forCursor) await setupCursor();
296
+ if (forWindsurf) await setupWindsurf();
297
+ }
298
+
299
+ console.log(pc.green("\n✓ MCP setup complete!\n"));
300
+ console.log(pc.dim("The AI assistant can now help you with:"));
301
+ console.log(pc.dim(" - Creating plugins, routes, and handlers"));
302
+ console.log(pc.dim(" - Adding migrations and service methods"));
303
+ console.log(pc.dim(" - Setting up cron jobs and background tasks"));
304
+ console.log(pc.dim(" - Generating types and API clients\n"));
305
+ }
package/src/index.ts CHANGED
@@ -35,6 +35,7 @@ ${pc.bold("Commands:")}
35
35
  ${pc.cyan("init")} Initialize a new project
36
36
  ${pc.cyan("generate")} Generate types (registry, context, client)
37
37
  ${pc.cyan("plugin")} Plugin management
38
+ ${pc.cyan("mcp")} Setup MCP server for AI-assisted development
38
39
 
39
40
  ${pc.bold("Options:")}
40
41
  -h, --help Show this help message
@@ -95,6 +96,11 @@ async function main() {
95
96
  await pluginCommand(positionals.slice(1));
96
97
  break;
97
98
 
99
+ case "mcp":
100
+ const { mcpCommand } = await import("./commands/mcp");
101
+ await mcpCommand(positionals.slice(1));
102
+ break;
103
+
98
104
  default:
99
105
  console.error(pc.red(`Unknown command: ${command}`));
100
106
  console.log(`Run ${pc.cyan("donkeylabs --help")} for available commands.`);
@@ -1,8 +1,15 @@
1
- import { db } from "./db";
2
1
  import { AppServer, createRouter } from "@donkeylabs/server";
2
+ import { Kysely } from "kysely";
3
+ import { BunSqliteDialect } from "kysely-bun-sqlite";
4
+ import { Database } from "bun:sqlite";
3
5
  import { healthRouter } from "./routes/health";
4
6
  import { statsPlugin } from "./plugins/stats";
5
7
 
8
+ // Simple in-memory database
9
+ const db = new Kysely<{}>({
10
+ dialect: new BunSqliteDialect({ database: new Database(":memory:") }),
11
+ });
12
+
6
13
  const server = new AppServer({
7
14
  port: Number(process.env.PORT) || 3000,
8
15
  db,
@@ -26,12 +33,22 @@ export function createApi(baseUrl: string, options?: ApiClientOptions) {
26
33
  // Register plugins
27
34
  server.registerPlugin(statsPlugin);
28
35
 
29
- const api = createRouter("api")
36
+ const api = createRouter("api");
30
37
  // Register routes
31
38
  api.router(healthRouter);
32
39
 
40
+ server.use(api);
33
41
 
42
+ // Handle DONKEYLABS_GENERATE mode for CLI type generation
43
+ if (process.env.DONKEYLABS_GENERATE === "1") {
44
+ const routes = api.getRoutes().map((route) => ({
45
+ name: route.name,
46
+ handler: route.handler || "typed",
47
+ inputType: route.input ? "(generated)" : undefined,
48
+ outputType: route.output ? "(generated)" : undefined,
49
+ }));
50
+ console.log(JSON.stringify({ routes }));
51
+ process.exit(0);
52
+ }
34
53
 
35
-
36
- server.use(api);
37
54
  await server.start();
@@ -24,9 +24,9 @@
24
24
  "vite": "^7.2.6"
25
25
  },
26
26
  "dependencies": {
27
- "@donkeylabs/cli": "^0.4.0",
28
- "@donkeylabs/adapter-sveltekit": "^0.4.0",
29
- "@donkeylabs/server": "^0.4.0",
27
+ "@donkeylabs/cli": "^0.4.3",
28
+ "@donkeylabs/adapter-sveltekit": "^0.4.3",
29
+ "@donkeylabs/server": "^0.4.3",
30
30
  "bits-ui": "^2.15.4",
31
31
  "clsx": "^2.1.1",
32
32
  "kysely": "^0.27.6",
@@ -4,17 +4,7 @@ import { Kysely } from "kysely";
4
4
  import { BunSqliteDialect } from "kysely-bun-sqlite";
5
5
  import { Database } from "bun:sqlite";
6
6
  import { demoPlugin } from "./plugins/demo";
7
-
8
- // Import routes
9
- import {
10
- counterRoutes,
11
- cacheRoutes,
12
- jobsRoutes,
13
- cronRoutes,
14
- ratelimitRoutes,
15
- eventsRoutes,
16
- sseRoutes,
17
- } from "./routes";
7
+ import demoRoutes from "./routes/demo";
18
8
 
19
9
  // Simple in-memory database
20
10
  const db = new Kysely<{}>({
@@ -33,13 +23,17 @@ export const server = new AppServer({
33
23
  // Register plugin
34
24
  server.registerPlugin(demoPlugin);
35
25
 
36
- // Register all routes
37
- server.use(counterRoutes);
38
- server.use(cacheRoutes);
39
- server.use(jobsRoutes);
40
- server.use(cronRoutes);
41
- server.use(ratelimitRoutes);
42
- server.use(eventsRoutes);
43
- server.use(sseRoutes);
44
-
26
+ // Register routes
27
+ server.use(demoRoutes);
45
28
 
29
+ // Handle DONKEYLABS_GENERATE mode for CLI type generation
30
+ if (process.env.DONKEYLABS_GENERATE === "1") {
31
+ const routes = demoRoutes.getRoutes().map((route) => ({
32
+ name: route.name,
33
+ handler: route.handler || "typed",
34
+ inputType: route.input ? "(generated)" : undefined,
35
+ outputType: route.output ? "(generated)" : undefined,
36
+ }));
37
+ console.log(JSON.stringify({ routes }));
38
+ process.exit(0);
39
+ }
@@ -0,0 +1,268 @@
1
+ /**
2
+ * Demo Router - Showcases @donkeylabs/server core features
3
+ *
4
+ * This single router demonstrates:
5
+ * - Counter: Basic state management via plugin service
6
+ * - Cache: In-memory caching with TTL
7
+ * - SSE: Server-sent events broadcasting
8
+ * - Jobs: Background job queue
9
+ * - Events: Pub/sub event system
10
+ * - Rate Limiting: Request throttling
11
+ * - Cron: Scheduled tasks
12
+ */
13
+
14
+ import { createRouter, defineRoute } from "@donkeylabs/server";
15
+ import { z } from "zod";
16
+
17
+ const demo = createRouter("api");
18
+
19
+ // =============================================================================
20
+ // COUNTER - Uses plugin service for state
21
+ // =============================================================================
22
+
23
+ demo.route("counter.get").typed(
24
+ defineRoute({
25
+ output: z.object({ count: z.number() }),
26
+ handle: async (_, ctx) => {
27
+ return { count: ctx.plugins.demo.getCount() };
28
+ },
29
+ })
30
+ );
31
+
32
+ demo.route("counter.increment").typed(
33
+ defineRoute({
34
+ output: z.object({ count: z.number() }),
35
+ handle: async (_, ctx) => {
36
+ return { count: ctx.plugins.demo.increment() };
37
+ },
38
+ })
39
+ );
40
+
41
+ demo.route("counter.decrement").typed(
42
+ defineRoute({
43
+ output: z.object({ count: z.number() }),
44
+ handle: async (_, ctx) => {
45
+ return { count: ctx.plugins.demo.decrement() };
46
+ },
47
+ })
48
+ );
49
+
50
+ demo.route("counter.reset").typed(
51
+ defineRoute({
52
+ output: z.object({ count: z.number() }),
53
+ handle: async (_, ctx) => {
54
+ ctx.plugins.demo.reset();
55
+ return { count: 0 };
56
+ },
57
+ })
58
+ );
59
+
60
+ // =============================================================================
61
+ // CACHE - In-memory caching
62
+ // =============================================================================
63
+
64
+ demo.route("cache.set").typed(
65
+ defineRoute({
66
+ input: z.object({
67
+ key: z.string(),
68
+ value: z.any(),
69
+ ttl: z.number().optional(),
70
+ }),
71
+ output: z.object({ success: z.boolean() }),
72
+ handle: async (input, ctx) => {
73
+ ctx.core.cache.set(input.key, input.value, input.ttl);
74
+ return { success: true };
75
+ },
76
+ })
77
+ );
78
+
79
+ demo.route("cache.get").typed(
80
+ defineRoute({
81
+ input: z.object({ key: z.string() }),
82
+ output: z.object({ value: z.any().optional(), exists: z.boolean() }),
83
+ handle: async (input, ctx) => {
84
+ const value = ctx.core.cache.get(input.key);
85
+ return { value, exists: value !== undefined };
86
+ },
87
+ })
88
+ );
89
+
90
+ demo.route("cache.delete").typed(
91
+ defineRoute({
92
+ input: z.object({ key: z.string() }),
93
+ output: z.object({ success: z.boolean() }),
94
+ handle: async (input, ctx) => {
95
+ ctx.core.cache.delete(input.key);
96
+ return { success: true };
97
+ },
98
+ })
99
+ );
100
+
101
+ demo.route("cache.keys").typed(
102
+ defineRoute({
103
+ output: z.object({ keys: z.array(z.string()) }),
104
+ handle: async (_, ctx) => {
105
+ return { keys: ctx.core.cache.keys() };
106
+ },
107
+ })
108
+ );
109
+
110
+ // =============================================================================
111
+ // SSE - Server-Sent Events
112
+ // =============================================================================
113
+
114
+ demo.route("sse.broadcast").typed(
115
+ defineRoute({
116
+ input: z.object({
117
+ channel: z.string().default("events"),
118
+ event: z.string().default("manual"),
119
+ data: z.any(),
120
+ }),
121
+ output: z.object({ success: z.boolean(), recipients: z.number() }),
122
+ handle: async (input, ctx) => {
123
+ const count = ctx.core.sse.broadcast(input.channel, input.event, input.data);
124
+ return { success: true, recipients: count };
125
+ },
126
+ })
127
+ );
128
+
129
+ demo.route("sse.clients").typed(
130
+ defineRoute({
131
+ output: z.object({ total: z.number(), byChannel: z.number() }),
132
+ handle: async (_, ctx) => {
133
+ const stats = ctx.core.sse.getStats();
134
+ return { total: stats.totalClients, byChannel: stats.clientsByChannel.events || 0 };
135
+ },
136
+ })
137
+ );
138
+
139
+ // =============================================================================
140
+ // JOBS - Background job queue
141
+ // =============================================================================
142
+
143
+ demo.route("jobs.enqueue").typed(
144
+ defineRoute({
145
+ input: z.object({
146
+ name: z.string().default("demo-job"),
147
+ data: z.record(z.any()).optional(),
148
+ delay: z.number().optional(),
149
+ }),
150
+ output: z.object({ success: z.boolean(), jobId: z.string() }),
151
+ handle: async (input, ctx) => {
152
+ const jobId = await ctx.core.jobs.enqueue(input.name, input.data || {}, { delay: input.delay });
153
+ return { success: true, jobId };
154
+ },
155
+ })
156
+ );
157
+
158
+ demo.route("jobs.stats").typed(
159
+ defineRoute({
160
+ output: z.object({
161
+ pending: z.number(),
162
+ running: z.number(),
163
+ completed: z.number(),
164
+ failed: z.number(),
165
+ }),
166
+ handle: async (_, ctx) => {
167
+ const stats = ctx.core.jobs.getStats();
168
+ return {
169
+ pending: stats.pending,
170
+ running: stats.processing,
171
+ completed: stats.completed,
172
+ failed: stats.failed,
173
+ };
174
+ },
175
+ })
176
+ );
177
+
178
+ // =============================================================================
179
+ // EVENTS - Pub/sub system
180
+ // =============================================================================
181
+
182
+ demo.route("events.emit").typed(
183
+ defineRoute({
184
+ input: z.object({
185
+ event: z.string(),
186
+ data: z.record(z.any()).optional(),
187
+ }),
188
+ output: z.object({ success: z.boolean() }),
189
+ handle: async (input, ctx) => {
190
+ await ctx.core.events.emit(input.event, input.data || {});
191
+ return { success: true };
192
+ },
193
+ })
194
+ );
195
+
196
+ // =============================================================================
197
+ // RATE LIMITING
198
+ // =============================================================================
199
+
200
+ demo.route("ratelimit.check").typed(
201
+ defineRoute({
202
+ input: z.object({
203
+ key: z.string(),
204
+ limit: z.number().default(10),
205
+ window: z.number().default(60),
206
+ }),
207
+ output: z.object({
208
+ allowed: z.boolean(),
209
+ remaining: z.number(),
210
+ resetAt: z.string(),
211
+ }),
212
+ handle: async (input, ctx) => {
213
+ const result = ctx.core.rateLimiter.check(input.key, input.limit, input.window * 1000);
214
+ return {
215
+ allowed: result.allowed,
216
+ remaining: result.remaining,
217
+ resetAt: new Date(result.resetAt).toISOString(),
218
+ };
219
+ },
220
+ })
221
+ );
222
+
223
+ demo.route("ratelimit.reset").typed(
224
+ defineRoute({
225
+ input: z.object({ key: z.string() }),
226
+ output: z.object({ success: z.boolean() }),
227
+ handle: async (input, ctx) => {
228
+ ctx.core.rateLimiter.reset(input.key);
229
+ return { success: true };
230
+ },
231
+ })
232
+ );
233
+
234
+ // =============================================================================
235
+ // CRON - Scheduled tasks info
236
+ // =============================================================================
237
+
238
+ demo.route("cron.list").typed(
239
+ defineRoute({
240
+ output: z.object({
241
+ tasks: z.array(
242
+ z.object({
243
+ id: z.string(),
244
+ name: z.string(),
245
+ expression: z.string(),
246
+ enabled: z.boolean(),
247
+ lastRun: z.string().optional(),
248
+ nextRun: z.string().optional(),
249
+ })
250
+ ),
251
+ }),
252
+ handle: async (_, ctx) => {
253
+ const jobs = ctx.core.cron.list();
254
+ return {
255
+ tasks: jobs.map((j, i) => ({
256
+ id: `cron-${i}`,
257
+ name: j.name,
258
+ expression: j.schedule,
259
+ enabled: true,
260
+ lastRun: j.lastRun?.toISOString(),
261
+ nextRun: j.nextRun?.toISOString(),
262
+ })),
263
+ };
264
+ },
265
+ })
266
+ );
267
+
268
+ export default demo;