@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.
- package/package.json +4 -1
- package/src/commands/generate.ts +122 -1
- package/src/commands/init.ts +184 -33
- package/src/commands/mcp.ts +305 -0
- package/src/index.ts +6 -0
- package/templates/starter/src/index.ts +21 -4
- package/templates/sveltekit-app/package.json +3 -3
- package/templates/sveltekit-app/src/server/index.ts +14 -20
- package/templates/sveltekit-app/src/server/routes/demo.ts +268 -0
- package/templates/sveltekit-app/src/server/routes/cache/handlers/delete.ts +0 -15
- package/templates/sveltekit-app/src/server/routes/cache/handlers/get.ts +0 -15
- package/templates/sveltekit-app/src/server/routes/cache/handlers/keys.ts +0 -15
- package/templates/sveltekit-app/src/server/routes/cache/handlers/set.ts +0 -15
- package/templates/sveltekit-app/src/server/routes/cache/index.ts +0 -46
- package/templates/sveltekit-app/src/server/routes/counter/handlers/decrement.ts +0 -17
- package/templates/sveltekit-app/src/server/routes/counter/handlers/get.ts +0 -17
- package/templates/sveltekit-app/src/server/routes/counter/handlers/increment.ts +0 -17
- package/templates/sveltekit-app/src/server/routes/counter/handlers/reset.ts +0 -17
- package/templates/sveltekit-app/src/server/routes/counter/index.ts +0 -39
- package/templates/sveltekit-app/src/server/routes/cron/handlers/list.ts +0 -17
- package/templates/sveltekit-app/src/server/routes/cron/index.ts +0 -24
- package/templates/sveltekit-app/src/server/routes/events/handlers/emit.ts +0 -15
- package/templates/sveltekit-app/src/server/routes/events/index.ts +0 -19
- package/templates/sveltekit-app/src/server/routes/index.ts +0 -8
- package/templates/sveltekit-app/src/server/routes/jobs/handlers/enqueue.ts +0 -15
- package/templates/sveltekit-app/src/server/routes/jobs/handlers/stats.ts +0 -15
- package/templates/sveltekit-app/src/server/routes/jobs/index.ts +0 -28
- package/templates/sveltekit-app/src/server/routes/ratelimit/handlers/check.ts +0 -15
- package/templates/sveltekit-app/src/server/routes/ratelimit/handlers/reset.ts +0 -15
- package/templates/sveltekit-app/src/server/routes/ratelimit/index.ts +0 -29
- package/templates/sveltekit-app/src/server/routes/sse/handlers/broadcast.ts +0 -15
- package/templates/sveltekit-app/src/server/routes/sse/handlers/clients.ts +0 -15
- 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.
|
|
28
|
-
"@donkeylabs/adapter-sveltekit": "^0.4.
|
|
29
|
-
"@donkeylabs/server": "^0.4.
|
|
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
|
|
37
|
-
server.use(
|
|
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;
|