@donkeylabs/server 0.3.0 → 0.4.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.
Files changed (49) hide show
  1. package/LICENSE +1 -1
  2. package/docs/api-client.md +7 -7
  3. package/docs/cache.md +1 -74
  4. package/docs/core-services.md +4 -116
  5. package/docs/cron.md +1 -1
  6. package/docs/errors.md +2 -2
  7. package/docs/events.md +3 -98
  8. package/docs/handlers.md +13 -48
  9. package/docs/logger.md +3 -58
  10. package/docs/middleware.md +2 -2
  11. package/docs/plugins.md +13 -64
  12. package/docs/project-structure.md +4 -142
  13. package/docs/rate-limiter.md +4 -136
  14. package/docs/router.md +6 -14
  15. package/docs/sse.md +1 -99
  16. package/docs/sveltekit-adapter.md +420 -0
  17. package/package.json +8 -11
  18. package/registry.d.ts +15 -14
  19. package/src/core/cache.ts +0 -75
  20. package/src/core/cron.ts +3 -96
  21. package/src/core/errors.ts +78 -11
  22. package/src/core/events.ts +1 -47
  23. package/src/core/index.ts +0 -4
  24. package/src/core/jobs.ts +0 -112
  25. package/src/core/logger.ts +12 -79
  26. package/src/core/rate-limiter.ts +29 -108
  27. package/src/core/sse.ts +1 -84
  28. package/src/core.ts +13 -104
  29. package/src/generator/index.ts +566 -0
  30. package/src/generator/zod-to-ts.ts +114 -0
  31. package/src/handlers.ts +14 -110
  32. package/src/index.ts +30 -24
  33. package/src/middleware.ts +2 -5
  34. package/src/registry.ts +4 -0
  35. package/src/router.ts +47 -1
  36. package/src/server.ts +618 -332
  37. package/README.md +0 -254
  38. package/cli/commands/dev.ts +0 -134
  39. package/cli/commands/generate.ts +0 -605
  40. package/cli/commands/init.ts +0 -205
  41. package/cli/commands/interactive.ts +0 -417
  42. package/cli/commands/plugin.ts +0 -192
  43. package/cli/commands/route.ts +0 -195
  44. package/cli/donkeylabs +0 -2
  45. package/cli/index.ts +0 -114
  46. package/docs/svelte-frontend.md +0 -324
  47. package/docs/testing.md +0 -438
  48. package/mcp/donkeylabs-mcp +0 -3238
  49. package/mcp/server.ts +0 -3238
@@ -1,192 +0,0 @@
1
- import { mkdir, writeFile } from "node:fs/promises";
2
- import { join } from "node:path";
3
- import { existsSync } from "node:fs";
4
- import pc from "picocolors";
5
- import prompts from "prompts";
6
-
7
- export async function pluginCommand(args: string[]): Promise<void> {
8
- const subcommand = args[0];
9
-
10
- switch (subcommand) {
11
- case "create":
12
- case "new":
13
- await createPlugin(args[1]);
14
- break;
15
-
16
- case "list":
17
- await listPlugins();
18
- break;
19
-
20
- default:
21
- console.log(`
22
- ${pc.bold("Plugin Management")}
23
-
24
- ${pc.bold("Commands:")}
25
- ${pc.cyan("create <name>")} Create a new plugin
26
- ${pc.cyan("list")} List installed plugins
27
-
28
- ${pc.bold("Examples:")}
29
- donkeylabs plugin create auth
30
- donkeylabs plugin list
31
- `);
32
- }
33
- }
34
-
35
- async function createPlugin(name?: string): Promise<void> {
36
- if (!name) {
37
- const response = await prompts({
38
- type: "text",
39
- name: "name",
40
- message: "Plugin name:",
41
- validate: (v) =>
42
- /^[a-z][a-z0-9-]*$/.test(v) ||
43
- "Name must be lowercase alphanumeric with dashes",
44
- });
45
- name = response.name;
46
- }
47
-
48
- if (!name) {
49
- console.log(pc.yellow("Cancelled."));
50
- return;
51
- }
52
-
53
- const { hasSchema, hasDependencies } = await prompts([
54
- {
55
- type: "confirm",
56
- name: "hasSchema",
57
- message: "Does this plugin need a database schema?",
58
- initial: false,
59
- },
60
- {
61
- type: "confirm",
62
- name: "hasDependencies",
63
- message: "Does this plugin depend on other plugins?",
64
- initial: false,
65
- },
66
- ]);
67
-
68
- let dependencies: string[] = [];
69
- if (hasDependencies) {
70
- const { deps } = await prompts({
71
- type: "list",
72
- name: "deps",
73
- message: "Enter dependency names (comma-separated):",
74
- separator: ",",
75
- });
76
- dependencies = deps?.filter(Boolean) || [];
77
- }
78
-
79
- const pluginDir = join(process.cwd(), "src/plugins", name);
80
-
81
- if (existsSync(pluginDir)) {
82
- console.log(pc.red(`Plugin directory already exists: ${pluginDir}`));
83
- return;
84
- }
85
-
86
- await mkdir(pluginDir, { recursive: true });
87
-
88
- const camelName = name.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
89
- const pascalName = camelName.charAt(0).toUpperCase() + camelName.slice(1);
90
-
91
- const schemaImport = hasSchema
92
- ? `import type { DB as ${pascalName}Schema } from "./schema";\n`
93
- : "";
94
-
95
- const schemaType = hasSchema ? `<${pascalName}Schema>()` : "";
96
-
97
- const depsLine =
98
- dependencies.length > 0
99
- ? ` dependencies: [${dependencies.map((d) => `"${d}"`).join(", ")}] as const,\n`
100
- : "";
101
-
102
- const pluginContent = `import { createPlugin } from "@donkeylabs/server";
103
- ${schemaImport}
104
- export interface ${pascalName}Service {
105
- // Define your service interface here
106
- getData(): Promise<string>;
107
- }
108
-
109
- export const ${camelName}Plugin = createPlugin${hasSchema ? `\n .withSchema${schemaType}` : ""}
110
- .define({
111
- name: "${name}",
112
- version: "1.0.0",
113
- ${depsLine}
114
- service: async (ctx): Promise<${pascalName}Service> => {
115
- console.log("[${pascalName}Plugin] Initializing...");
116
- ${
117
- dependencies.length > 0
118
- ? dependencies.map((d) => ` // Access ${d} via: ctx.deps.${d}`).join("\n") + "\n"
119
- : ""
120
- }
121
- return {
122
- getData: async () => {
123
- return "Hello from ${name} plugin!";
124
- },
125
- };
126
- },
127
- });
128
- `;
129
-
130
- await writeFile(join(pluginDir, "index.ts"), pluginContent);
131
- console.log(pc.green(" Created:"), `src/plugins/${name}/index.ts`);
132
-
133
- if (hasSchema) {
134
- const schemaContent = `// Database schema types for ${name} plugin
135
- // Run \`bun run gen:types\` to regenerate from database
136
-
137
- export interface DB {
138
- // Define your table interfaces here
139
- // Example:
140
- // ${name}: {
141
- // id: Generated<number>;
142
- // name: string;
143
- // created_at: Generated<string>;
144
- // };
145
- }
146
- `;
147
- await writeFile(join(pluginDir, "schema.ts"), schemaContent);
148
- console.log(pc.green(" Created:"), `src/plugins/${name}/schema.ts`);
149
-
150
- await mkdir(join(pluginDir, "migrations"), { recursive: true });
151
-
152
- const migrationContent = `import { Kysely } from "kysely";
153
-
154
- export async function up(db: Kysely<any>) {
155
- // Create your tables here
156
- // await db.schema
157
- // .createTable("${name}")
158
- // .ifNotExists()
159
- // .addColumn("id", "integer", (col) => col.primaryKey().autoIncrement())
160
- // .addColumn("name", "text", (col) => col.notNull())
161
- // .addColumn("created_at", "text", (col) => col.defaultTo(sql\`CURRENT_TIMESTAMP\`))
162
- // .execute();
163
- }
164
-
165
- export async function down(db: Kysely<any>) {
166
- // Drop your tables here
167
- // await db.schema.dropTable("${name}").ifExists().execute();
168
- }
169
- `;
170
- await writeFile(
171
- join(pluginDir, "migrations", "001_initial.ts"),
172
- migrationContent
173
- );
174
- console.log(
175
- pc.green(" Created:"),
176
- `src/plugins/${name}/migrations/001_initial.ts`
177
- );
178
- }
179
-
180
- console.log(`
181
- ${pc.bold(pc.green("Plugin created!"))}
182
-
183
- ${pc.bold("Next steps:")}
184
- 1. Edit your plugin at ${pc.cyan(`src/plugins/${name}/index.ts`)}
185
- ${hasSchema ? ` 2. Define your schema in ${pc.cyan(`src/plugins/${name}/schema.ts`)}\n 3. Add migrations in ${pc.cyan(`src/plugins/${name}/migrations/`)}\n` : ""} ${hasSchema ? "4" : "2"}. Regenerate types: ${pc.cyan("donkeylabs generate")}
186
- `);
187
- }
188
-
189
- async function listPlugins() {
190
- console.log(pc.yellow("Plugin listing not yet implemented."));
191
- console.log("Run 'donkeylabs generate' to see discovered plugins.");
192
- }
@@ -1,195 +0,0 @@
1
- /**
2
- * Route Command
3
- *
4
- * Create new routes from the command line
5
- *
6
- * Usage:
7
- * donkeylabs route <router> <route-name>
8
- * donkeylabs route users create
9
- * donkeylabs route orders get-by-id
10
- */
11
-
12
- import { writeFile, mkdir } from "node:fs/promises";
13
- import { join } from "node:path";
14
- import { existsSync } from "node:fs";
15
- import pc from "picocolors";
16
-
17
- function toPascalCase(str: string): string {
18
- return str
19
- .replace(/-([a-z])/g, (_, c) => c.toUpperCase())
20
- .replace(/^./, (c) => c.toUpperCase());
21
- }
22
-
23
- export async function routeCommand(args: string[]): Promise<void> {
24
- const routerName = args[0];
25
- const routeName = args[1];
26
-
27
- if (!routerName || !routeName) {
28
- console.log(`
29
- ${pc.bold("Usage:")}
30
- donkeylabs route <router> <route-name>
31
-
32
- ${pc.bold("Examples:")}
33
- donkeylabs route users create
34
- donkeylabs route orders get-by-id
35
- donkeylabs route health check
36
-
37
- ${pc.bold("This creates:")}
38
- src/routes/<router>/<route-name>/
39
- ├── index.ts # Route definition
40
- ├── models/model.ts # Schema + Model class
41
- └── tests/
42
- ├── unit.test.ts
43
- └── integ.test.ts
44
- `);
45
- process.exit(1);
46
- }
47
-
48
- // Validate names
49
- if (!/^[a-z][a-z0-9-]*$/.test(routerName)) {
50
- console.error(pc.red("Router name must use lowercase letters, numbers, and hyphens"));
51
- process.exit(1);
52
- }
53
-
54
- if (!/^[a-z][a-z0-9-]*$/.test(routeName)) {
55
- console.error(pc.red("Route name must use lowercase letters, numbers, and hyphens"));
56
- process.exit(1);
57
- }
58
-
59
- const routesDir = join(process.cwd(), "src/routes");
60
- const routePath = join(routesDir, routerName, routeName);
61
-
62
- if (existsSync(routePath)) {
63
- console.error(pc.red(`Route already exists: src/routes/${routerName}/${routeName}`));
64
- process.exit(1);
65
- }
66
-
67
- // Create directory structure
68
- await mkdir(join(routePath, "models"), { recursive: true });
69
- await mkdir(join(routePath, "tests"), { recursive: true });
70
-
71
- // Generate PascalCase and camelCase names
72
- const pascalRouter = toPascalCase(routerName);
73
- const pascalRoute = toPascalCase(routeName);
74
- const camelRoute = routeName.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase());
75
-
76
- // Create model.ts with Model class
77
- const modelContent = `import { z } from "zod";
78
-
79
- // After running \`donkeylabs generate\`, use typed Handler:
80
- // import type { Handler } from "@donkeylabs/server";
81
- // import type { ${pascalRouter} } from "$server/routes";
82
- // export class ${pascalRoute}Model implements Handler<${pascalRouter}.${pascalRoute}> { ... }
83
-
84
- // Input/Output schemas
85
- export const Input = z.object({
86
- // Define your input schema here
87
- });
88
-
89
- export const Output = z.object({
90
- success: z.boolean(),
91
- });
92
-
93
- export type Input = z.infer<typeof Input>;
94
- export type Output = z.infer<typeof Output>;
95
-
96
- /**
97
- * Model class with handler logic.
98
- * After gen:types, implement Handler<${pascalRouter}.${pascalRoute}> for full typing.
99
- */
100
- export class ${pascalRoute}Model {
101
- constructor(private ctx: any) {}
102
-
103
- handle(input: Input): Output {
104
- return {
105
- success: true,
106
- };
107
- }
108
- }
109
- `;
110
- await writeFile(join(routePath, "models/model.ts"), modelContent);
111
-
112
- // Create index.ts (route definition)
113
- const routeIndexContent = `// Route definition
114
- // After gen:types, use: Route<${pascalRouter}.${pascalRoute}> for full typing
115
- import type { AppRoute } from "@donkeylabs/server";
116
- import { Input, Output, ${pascalRoute}Model } from "./models/model";
117
-
118
- export const ${camelRoute}Route: AppRoute = {
119
- input: Input,
120
- output: Output,
121
- handle: async (input, ctx) => {
122
- const model = new ${pascalRoute}Model(ctx);
123
- return model.handle(input);
124
- },
125
- };
126
- `;
127
- await writeFile(join(routePath, "index.ts"), routeIndexContent);
128
-
129
- // Create unit.test.ts
130
- const unitTestContent = `import { describe, it, expect } from "bun:test";
131
- import { ${pascalRoute}Model, Input } from "../models/model";
132
-
133
- describe("${routerName}.${routeName} model", () => {
134
- it("should return success", () => {
135
- const input = Input.parse({});
136
- const ctx = {} as any;
137
- const model = new ${pascalRoute}Model(ctx);
138
- const result = model.handle(input);
139
- expect(result.success).toBe(true);
140
- });
141
- });
142
- `;
143
- await writeFile(join(routePath, "tests/unit.test.ts"), unitTestContent);
144
-
145
- // Create integ.test.ts
146
- const integTestContent = `import { describe, it, expect } from "bun:test";
147
-
148
- const BASE_URL = process.env.TEST_SERVER_URL || "http://localhost:3000";
149
-
150
- describe("${routerName}.${routeName} (integration)", () => {
151
- // Server must be running for integration tests
152
- it("POST /${routerName}.${routeName} returns success", async () => {
153
- try {
154
- const res = await fetch(\`\${BASE_URL}/${routerName}.${routeName}\`, {
155
- method: "POST",
156
- headers: { "Content-Type": "application/json" },
157
- body: JSON.stringify({}),
158
- });
159
- expect(res.ok).toBe(true);
160
- } catch (e: any) {
161
- if (e.code === "ConnectionRefused") {
162
- console.log("Skipping: Server not running");
163
- return;
164
- }
165
- throw e;
166
- }
167
- });
168
- });
169
- `;
170
- await writeFile(join(routePath, "tests/integ.test.ts"), integTestContent);
171
-
172
- // Check if router index exists, if not create it
173
- const routerIndexPath = join(routesDir, routerName, "index.ts");
174
- if (!existsSync(routerIndexPath)) {
175
- const routerIndexContent = `import { createRouter } from "@donkeylabs/server";
176
- import { ${camelRoute}Route } from "./${routeName}";
177
-
178
- export const ${camelRoute}Router = createRouter("${routerName}")
179
- .route("${routeName}").typed(${camelRoute}Route);
180
- `;
181
- await writeFile(routerIndexPath, routerIndexContent);
182
- console.log(pc.green(`Created router: src/routes/${routerName}/index.ts`));
183
- } else {
184
- console.log(pc.yellow(`\nNote: Add the route to src/routes/${routerName}/index.ts:`));
185
- console.log(pc.gray(` import { ${camelRoute}Route } from "./${routeName}";`));
186
- console.log(pc.gray(` .route("${routeName}").typed(${camelRoute}Route)`));
187
- }
188
-
189
- console.log(pc.green(`\nCreated route: src/routes/${routerName}/${routeName}/`));
190
- console.log(pc.gray(` - index.ts`));
191
- console.log(pc.gray(` - models/model.ts`));
192
- console.log(pc.gray(` - tests/unit.test.ts`));
193
- console.log(pc.gray(` - tests/integ.test.ts`));
194
- console.log(pc.cyan(`\nRun ${pc.bold("donkeylabs generate")} to update types.`));
195
- }
package/cli/donkeylabs DELETED
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env bun
2
- import "./index.ts";
package/cli/index.ts DELETED
@@ -1,114 +0,0 @@
1
- #!/usr/bin/env bun
2
- /**
3
- * @donkeylabs/server CLI
4
- *
5
- * Commands:
6
- * init Initialize a new project
7
- * generate Generate types (registry, context, client)
8
- * plugin Plugin management (create, list)
9
- */
10
-
11
- import { parseArgs } from "node:util";
12
- import pc from "picocolors";
13
-
14
- const { positionals, values } = parseArgs({
15
- args: process.argv.slice(2),
16
- options: {
17
- help: { type: "boolean", short: "h" },
18
- version: { type: "boolean", short: "v" },
19
- },
20
- allowPositionals: true,
21
- });
22
-
23
- const command = positionals[0];
24
-
25
- function printHelp() {
26
- console.log(`
27
- ${pc.bold("@donkeylabs/server")} - Type-safe plugin system for Bun
28
-
29
- ${pc.bold("Usage:")}
30
- donkeylabs Interactive menu
31
- donkeylabs <command> [options]
32
-
33
- ${pc.bold("Commands:")}
34
- ${pc.cyan("init")} Initialize a new project
35
- ${pc.cyan("generate")} Generate types (registry, context, client)
36
- ${pc.cyan("route")} Create a new route
37
- ${pc.cyan("dev")} Start dev server with auto-regeneration
38
- ${pc.cyan("plugin")} Plugin management
39
-
40
- ${pc.bold("Options:")}
41
- -h, --help Show this help message
42
- -v, --version Show version number
43
-
44
- ${pc.bold("Examples:")}
45
- donkeylabs # Interactive menu
46
- donkeylabs init
47
- donkeylabs generate
48
- donkeylabs route users create
49
- donkeylabs dev
50
- donkeylabs plugin create myPlugin
51
- `);
52
- }
53
-
54
- async function printVersion() {
55
- const pkg = await Bun.file(new URL("../package.json", import.meta.url)).json();
56
- console.log(pkg.version);
57
- }
58
-
59
- async function main() {
60
- if (values.help) {
61
- printHelp();
62
- process.exit(0);
63
- }
64
-
65
- if (values.version) {
66
- await printVersion();
67
- process.exit(0);
68
- }
69
-
70
- // No command provided - launch interactive mode
71
- if (!command) {
72
- const { interactiveCommand } = await import("./commands/interactive");
73
- await interactiveCommand();
74
- return;
75
- }
76
-
77
- switch (command) {
78
- case "init":
79
- const { initCommand } = await import("./commands/init");
80
- await initCommand(positionals.slice(1));
81
- break;
82
-
83
- case "generate":
84
- case "gen":
85
- const { generateCommand } = await import("./commands/generate");
86
- await generateCommand(positionals.slice(1));
87
- break;
88
-
89
- case "plugin":
90
- const { pluginCommand } = await import("./commands/plugin");
91
- await pluginCommand(positionals.slice(1));
92
- break;
93
-
94
- case "route":
95
- const { routeCommand } = await import("./commands/route");
96
- await routeCommand(positionals.slice(1));
97
- break;
98
-
99
- case "dev":
100
- const { devCommand } = await import("./commands/dev");
101
- await devCommand(positionals.slice(1));
102
- break;
103
-
104
- default:
105
- console.error(pc.red(`Unknown command: ${command}`));
106
- console.log(`Run ${pc.cyan("donkeylabs --help")} for available commands.`);
107
- process.exit(1);
108
- }
109
- }
110
-
111
- main().catch((error) => {
112
- console.error(pc.red("Error:"), error.message);
113
- process.exit(1);
114
- });