@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.
- package/LICENSE +1 -1
- package/docs/api-client.md +7 -7
- package/docs/cache.md +1 -74
- package/docs/core-services.md +4 -116
- package/docs/cron.md +1 -1
- package/docs/errors.md +2 -2
- package/docs/events.md +3 -98
- package/docs/handlers.md +13 -48
- package/docs/logger.md +3 -58
- package/docs/middleware.md +2 -2
- package/docs/plugins.md +13 -64
- package/docs/project-structure.md +4 -142
- package/docs/rate-limiter.md +4 -136
- package/docs/router.md +6 -14
- package/docs/sse.md +1 -99
- package/docs/sveltekit-adapter.md +420 -0
- package/package.json +8 -11
- package/registry.d.ts +15 -14
- package/src/core/cache.ts +0 -75
- package/src/core/cron.ts +3 -96
- package/src/core/errors.ts +78 -11
- package/src/core/events.ts +1 -47
- package/src/core/index.ts +0 -4
- package/src/core/jobs.ts +0 -112
- package/src/core/logger.ts +12 -79
- package/src/core/rate-limiter.ts +29 -108
- package/src/core/sse.ts +1 -84
- package/src/core.ts +13 -104
- package/src/generator/index.ts +566 -0
- package/src/generator/zod-to-ts.ts +114 -0
- package/src/handlers.ts +14 -110
- package/src/index.ts +30 -24
- package/src/middleware.ts +2 -5
- package/src/registry.ts +4 -0
- package/src/router.ts +47 -1
- package/src/server.ts +618 -332
- package/README.md +0 -254
- package/cli/commands/dev.ts +0 -134
- package/cli/commands/generate.ts +0 -605
- package/cli/commands/init.ts +0 -205
- package/cli/commands/interactive.ts +0 -417
- package/cli/commands/plugin.ts +0 -192
- package/cli/commands/route.ts +0 -195
- package/cli/donkeylabs +0 -2
- package/cli/index.ts +0 -114
- package/docs/svelte-frontend.md +0 -324
- package/docs/testing.md +0 -438
- package/mcp/donkeylabs-mcp +0 -3238
- package/mcp/server.ts +0 -3238
package/cli/commands/init.ts
DELETED
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @donkeylabs/server - Init Command
|
|
3
|
-
*
|
|
4
|
-
* Initialize a new project by copying from examples/starter.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { mkdir, writeFile, readFile, readdir, copyFile, unlink } from "node:fs/promises";
|
|
8
|
-
import { join, resolve, dirname } from "node:path";
|
|
9
|
-
import { existsSync } from "node:fs";
|
|
10
|
-
import { fileURLToPath } from "node:url";
|
|
11
|
-
import pc from "picocolors";
|
|
12
|
-
import prompts from "prompts";
|
|
13
|
-
|
|
14
|
-
interface InitOptions {
|
|
15
|
-
projectName: string;
|
|
16
|
-
useDatabase: boolean;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export async function initCommand(args: string[]) {
|
|
20
|
-
const projectDir = args[0] || ".";
|
|
21
|
-
const targetDir = resolve(process.cwd(), projectDir);
|
|
22
|
-
|
|
23
|
-
console.log(pc.bold("\nInitializing @donkeylabs/server project...\n"));
|
|
24
|
-
|
|
25
|
-
// Check for existing files
|
|
26
|
-
if (existsSync(targetDir)) {
|
|
27
|
-
const files = await readdir(targetDir);
|
|
28
|
-
const hasConflicts = files.some(
|
|
29
|
-
(f) => f === "src" || f === "donkeylabs.config.ts"
|
|
30
|
-
);
|
|
31
|
-
|
|
32
|
-
if (hasConflicts) {
|
|
33
|
-
const { overwrite } = await prompts({
|
|
34
|
-
type: "confirm",
|
|
35
|
-
name: "overwrite",
|
|
36
|
-
message: "Directory contains existing files. Overwrite?",
|
|
37
|
-
initial: false,
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
if (!overwrite) {
|
|
41
|
-
console.log(pc.yellow("Cancelled."));
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Ask for project configuration
|
|
48
|
-
const responses = await prompts([
|
|
49
|
-
{
|
|
50
|
-
type: "text",
|
|
51
|
-
name: "projectName",
|
|
52
|
-
message: "Project name:",
|
|
53
|
-
initial: projectDir === "." ? "my-server" : projectDir,
|
|
54
|
-
},
|
|
55
|
-
{
|
|
56
|
-
type: "confirm",
|
|
57
|
-
name: "useDatabase",
|
|
58
|
-
message: "Set up SQLite database?",
|
|
59
|
-
initial: true,
|
|
60
|
-
},
|
|
61
|
-
]);
|
|
62
|
-
|
|
63
|
-
if (!responses.projectName) {
|
|
64
|
-
console.log(pc.yellow("Cancelled."));
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const options: InitOptions = {
|
|
69
|
-
projectName: responses.projectName,
|
|
70
|
-
useDatabase: responses.useDatabase ?? true,
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
// Find the examples/starter directory
|
|
74
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
75
|
-
const __dirname = dirname(__filename);
|
|
76
|
-
const starterDir = resolve(__dirname, "../../examples/starter");
|
|
77
|
-
const docsDir = resolve(__dirname, "../../docs");
|
|
78
|
-
|
|
79
|
-
if (!existsSync(starterDir)) {
|
|
80
|
-
console.error(pc.red("Error: examples/starter not found"));
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Copy starter project
|
|
85
|
-
await copyDirectory(starterDir, targetDir);
|
|
86
|
-
console.log(pc.green(" Copied:"), "project files");
|
|
87
|
-
|
|
88
|
-
// Replace placeholders in files
|
|
89
|
-
await replaceInFile(
|
|
90
|
-
join(targetDir, "package.json"),
|
|
91
|
-
"{{PROJECT_NAME}}",
|
|
92
|
-
options.projectName
|
|
93
|
-
);
|
|
94
|
-
await replaceInFile(
|
|
95
|
-
join(targetDir, "CLAUDE.md"),
|
|
96
|
-
"{{PROJECT_NAME}}",
|
|
97
|
-
options.projectName
|
|
98
|
-
);
|
|
99
|
-
|
|
100
|
-
// Rename .gitignore.template to .gitignore
|
|
101
|
-
const gitignoreTemplate = join(targetDir, ".gitignore.template");
|
|
102
|
-
if (existsSync(gitignoreTemplate)) {
|
|
103
|
-
const content = await readFile(gitignoreTemplate, "utf-8");
|
|
104
|
-
await writeFile(join(targetDir, ".gitignore"), content);
|
|
105
|
-
await unlink(gitignoreTemplate);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// If not using database, replace db.ts with dummy driver
|
|
109
|
-
if (!options.useDatabase) {
|
|
110
|
-
await writeFile(join(targetDir, "src/db.ts"), generateDummyDb());
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Copy docs
|
|
114
|
-
await mkdir(join(targetDir, "docs"), { recursive: true });
|
|
115
|
-
await copyDocs(docsDir, join(targetDir, "docs"));
|
|
116
|
-
console.log(pc.green(" Copied:"), "docs/");
|
|
117
|
-
|
|
118
|
-
// Print next steps
|
|
119
|
-
console.log(`
|
|
120
|
-
${pc.bold(pc.green("Success!"))} Project initialized.
|
|
121
|
-
|
|
122
|
-
${pc.bold("Next steps:")}
|
|
123
|
-
1. Install dependencies:
|
|
124
|
-
${pc.cyan("bun install")}
|
|
125
|
-
${options.useDatabase ? `
|
|
126
|
-
2. Set up your database:
|
|
127
|
-
${pc.cyan("cp .env.example .env")}
|
|
128
|
-
` : ""}
|
|
129
|
-
${options.useDatabase ? "3" : "2"}. Start development:
|
|
130
|
-
${pc.cyan("bun run dev")}
|
|
131
|
-
|
|
132
|
-
Run tests:
|
|
133
|
-
${pc.cyan("bun test")}
|
|
134
|
-
`);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
async function copyDirectory(src: string, dest: string) {
|
|
138
|
-
await mkdir(dest, { recursive: true });
|
|
139
|
-
const entries = await readdir(src, { withFileTypes: true });
|
|
140
|
-
|
|
141
|
-
for (const entry of entries) {
|
|
142
|
-
const srcPath = join(src, entry.name);
|
|
143
|
-
const destPath = join(dest, entry.name);
|
|
144
|
-
|
|
145
|
-
if (entry.isDirectory()) {
|
|
146
|
-
await copyDirectory(srcPath, destPath);
|
|
147
|
-
} else {
|
|
148
|
-
await copyFile(srcPath, destPath);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
async function replaceInFile(filePath: string, search: string, replace: string) {
|
|
154
|
-
if (!existsSync(filePath)) return;
|
|
155
|
-
const content = await readFile(filePath, "utf-8");
|
|
156
|
-
await writeFile(filePath, content.replaceAll(search, replace));
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
async function copyDocs(src: string, dest: string) {
|
|
160
|
-
const docs = [
|
|
161
|
-
"plugins.md",
|
|
162
|
-
"router.md",
|
|
163
|
-
"handlers.md",
|
|
164
|
-
"core-services.md",
|
|
165
|
-
"errors.md",
|
|
166
|
-
"cache.md",
|
|
167
|
-
"logger.md",
|
|
168
|
-
"events.md",
|
|
169
|
-
"jobs.md",
|
|
170
|
-
"cron.md",
|
|
171
|
-
"sse.md",
|
|
172
|
-
"rate-limiter.md",
|
|
173
|
-
"middleware.md",
|
|
174
|
-
"api-client.md",
|
|
175
|
-
"svelte-frontend.md",
|
|
176
|
-
];
|
|
177
|
-
|
|
178
|
-
for (const doc of docs) {
|
|
179
|
-
const srcPath = join(src, doc);
|
|
180
|
-
if (existsSync(srcPath)) {
|
|
181
|
-
await copyFile(srcPath, join(dest, doc));
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function generateDummyDb(): string {
|
|
187
|
-
return `import {
|
|
188
|
-
Kysely,
|
|
189
|
-
DummyDriver,
|
|
190
|
-
SqliteAdapter,
|
|
191
|
-
SqliteIntrospector,
|
|
192
|
-
SqliteQueryCompiler,
|
|
193
|
-
} from "kysely";
|
|
194
|
-
|
|
195
|
-
// No database configured - using dummy driver for type compatibility
|
|
196
|
-
export const db = new Kysely<any>({
|
|
197
|
-
dialect: {
|
|
198
|
-
createAdapter: () => new SqliteAdapter(),
|
|
199
|
-
createDriver: () => new DummyDriver(),
|
|
200
|
-
createIntrospector: (db) => new SqliteIntrospector(db),
|
|
201
|
-
createQueryCompiler: () => new SqliteQueryCompiler(),
|
|
202
|
-
},
|
|
203
|
-
});
|
|
204
|
-
`;
|
|
205
|
-
}
|
|
@@ -1,417 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Interactive CLI Menu
|
|
3
|
-
*
|
|
4
|
-
* Full interactive experience with context-aware menus
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import prompts from "prompts";
|
|
8
|
-
import pc from "picocolors";
|
|
9
|
-
import { readdir, writeFile, mkdir } from "node:fs/promises";
|
|
10
|
-
import { join, basename } from "node:path";
|
|
11
|
-
import { existsSync } from "node:fs";
|
|
12
|
-
import { exec } from "node:child_process";
|
|
13
|
-
import { promisify } from "node:util";
|
|
14
|
-
|
|
15
|
-
const execAsync = promisify(exec);
|
|
16
|
-
|
|
17
|
-
export async function interactiveCommand() {
|
|
18
|
-
console.clear();
|
|
19
|
-
console.log(pc.magenta(pc.bold("\n @donkeylabs/server CLI\n")));
|
|
20
|
-
|
|
21
|
-
// Detect context - are we in a plugin directory?
|
|
22
|
-
const cwd = process.cwd();
|
|
23
|
-
const pathParts = cwd.split("/");
|
|
24
|
-
const parentDir = pathParts[pathParts.length - 2];
|
|
25
|
-
const currentDir = pathParts[pathParts.length - 1];
|
|
26
|
-
|
|
27
|
-
let contextPlugin: string | null = null;
|
|
28
|
-
if (parentDir === "plugins" && currentDir && existsSync(join(cwd, "index.ts"))) {
|
|
29
|
-
contextPlugin = currentDir;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Run appropriate menu loop
|
|
33
|
-
if (contextPlugin) {
|
|
34
|
-
await pluginMenuLoop(contextPlugin);
|
|
35
|
-
} else {
|
|
36
|
-
await globalMenuLoop();
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// ============================================
|
|
41
|
-
// Plugin Context Menu (Inside plugins/<name>/)
|
|
42
|
-
// ============================================
|
|
43
|
-
|
|
44
|
-
async function pluginMenuLoop(pluginName: string) {
|
|
45
|
-
while (true) {
|
|
46
|
-
console.log(pc.cyan(`\n Context: Plugin ${pc.bold(`'${pluginName}'`)}\n`));
|
|
47
|
-
|
|
48
|
-
const response = await prompts({
|
|
49
|
-
type: "select",
|
|
50
|
-
name: "action",
|
|
51
|
-
message: "What would you like to do?",
|
|
52
|
-
choices: [
|
|
53
|
-
{ title: pc.yellow("1.") + " Generate Schema Types", value: "gen-types" },
|
|
54
|
-
{ title: pc.yellow("2.") + " Create Migration", value: "migration" },
|
|
55
|
-
{ title: pc.gray("─".repeat(35)), value: "separator", disabled: true },
|
|
56
|
-
{ title: pc.blue("←") + " Back to Global Menu", value: "global" },
|
|
57
|
-
{ title: pc.red("×") + " Exit", value: "exit" },
|
|
58
|
-
],
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
if (!response.action || response.action === "exit") {
|
|
62
|
-
console.log(pc.gray("\nGoodbye!\n"));
|
|
63
|
-
process.exit(0);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (response.action === "global") {
|
|
67
|
-
console.clear();
|
|
68
|
-
console.log(pc.magenta(pc.bold("\n @donkeylabs/server CLI\n")));
|
|
69
|
-
await globalMenuLoop();
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
console.log(""); // spacing
|
|
74
|
-
|
|
75
|
-
switch (response.action) {
|
|
76
|
-
case "gen-types":
|
|
77
|
-
await runCommand(`bun scripts/generate-types.ts ${pluginName}`);
|
|
78
|
-
break;
|
|
79
|
-
case "migration":
|
|
80
|
-
await createMigration(pluginName);
|
|
81
|
-
break;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
await pressEnterToContinue();
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// ============================================
|
|
89
|
-
// Global Root Menu
|
|
90
|
-
// ============================================
|
|
91
|
-
|
|
92
|
-
async function globalMenuLoop() {
|
|
93
|
-
while (true) {
|
|
94
|
-
console.log(pc.cyan("\n Context: Project Root\n"));
|
|
95
|
-
|
|
96
|
-
const choices = [
|
|
97
|
-
{ title: pc.yellow("1.") + " Add Route", value: "add-route" },
|
|
98
|
-
{ title: pc.yellow("2.") + " Create New Plugin", value: "new-plugin" },
|
|
99
|
-
{ title: pc.yellow("3.") + " Initialize New Project", value: "init" },
|
|
100
|
-
{ title: pc.gray("─".repeat(35)), value: "separator1", disabled: true },
|
|
101
|
-
{ title: pc.yellow("4.") + " Generate Types", value: "generate" },
|
|
102
|
-
{ title: pc.yellow("5.") + " Generate Registry", value: "gen-registry" },
|
|
103
|
-
{ title: pc.yellow("6.") + " Generate Server Context", value: "gen-server" },
|
|
104
|
-
{ title: pc.gray("─".repeat(35)), value: "separator2", disabled: true },
|
|
105
|
-
{ title: pc.red("×") + " Exit", value: "exit" },
|
|
106
|
-
];
|
|
107
|
-
|
|
108
|
-
const response = await prompts({
|
|
109
|
-
type: "select",
|
|
110
|
-
name: "action",
|
|
111
|
-
message: "Select a command:",
|
|
112
|
-
choices,
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
if (!response.action || response.action === "exit") {
|
|
116
|
-
console.log(pc.gray("\nGoodbye!\n"));
|
|
117
|
-
process.exit(0);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
console.log(""); // spacing
|
|
121
|
-
|
|
122
|
-
switch (response.action) {
|
|
123
|
-
case "add-route":
|
|
124
|
-
await addRoute();
|
|
125
|
-
break;
|
|
126
|
-
case "new-plugin":
|
|
127
|
-
const { pluginCommand } = await import("./plugin");
|
|
128
|
-
await pluginCommand(["create"]);
|
|
129
|
-
break;
|
|
130
|
-
case "init":
|
|
131
|
-
const { initCommand } = await import("./init");
|
|
132
|
-
await initCommand([]);
|
|
133
|
-
break;
|
|
134
|
-
case "generate":
|
|
135
|
-
const { generateCommand } = await import("./generate");
|
|
136
|
-
await generateCommand([]);
|
|
137
|
-
break;
|
|
138
|
-
case "gen-registry":
|
|
139
|
-
await runCommand("bun scripts/generate-registry.ts");
|
|
140
|
-
break;
|
|
141
|
-
case "gen-server":
|
|
142
|
-
await runCommand("bun scripts/generate-server.ts");
|
|
143
|
-
break;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
await pressEnterToContinue();
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// ============================================
|
|
151
|
-
// Commands
|
|
152
|
-
// ============================================
|
|
153
|
-
|
|
154
|
-
async function addRoute() {
|
|
155
|
-
const routesDir = join(process.cwd(), "src/routes");
|
|
156
|
-
|
|
157
|
-
// Find existing routers (namespace directories)
|
|
158
|
-
let existingRouters: string[] = [];
|
|
159
|
-
if (existsSync(routesDir)) {
|
|
160
|
-
const entries = await readdir(routesDir, { withFileTypes: true });
|
|
161
|
-
existingRouters = entries
|
|
162
|
-
.filter((e) => e.isDirectory() && !e.name.startsWith("."))
|
|
163
|
-
.map((e) => e.name);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// Choose router
|
|
167
|
-
const routerChoices = [
|
|
168
|
-
...existingRouters.map((r) => ({ title: r, value: r })),
|
|
169
|
-
{ title: pc.green("+ Create new router"), value: "__new__" },
|
|
170
|
-
];
|
|
171
|
-
|
|
172
|
-
const routerRes = await prompts({
|
|
173
|
-
type: "select",
|
|
174
|
-
name: "router",
|
|
175
|
-
message: "Select router (namespace):",
|
|
176
|
-
choices: routerChoices,
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
if (!routerRes.router) return;
|
|
180
|
-
|
|
181
|
-
let routerName = routerRes.router;
|
|
182
|
-
if (routerName === "__new__") {
|
|
183
|
-
const newRouterRes = await prompts({
|
|
184
|
-
type: "text",
|
|
185
|
-
name: "name",
|
|
186
|
-
message: "Router name (e.g. users, orders):",
|
|
187
|
-
validate: (v) =>
|
|
188
|
-
/^[a-z][a-z0-9-]*$/.test(v) ? true : "Use lowercase letters, numbers, and hyphens",
|
|
189
|
-
});
|
|
190
|
-
if (!newRouterRes.name) return;
|
|
191
|
-
routerName = newRouterRes.name;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// Get route name
|
|
195
|
-
const routeRes = await prompts({
|
|
196
|
-
type: "text",
|
|
197
|
-
name: "name",
|
|
198
|
-
message: "Route name (e.g. create, get-by-id):",
|
|
199
|
-
validate: (v) =>
|
|
200
|
-
/^[a-z][a-z0-9-]*$/.test(v) ? true : "Use lowercase letters, numbers, and hyphens",
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
if (!routeRes.name) return;
|
|
204
|
-
|
|
205
|
-
const routeName = routeRes.name;
|
|
206
|
-
const routePath = join(routesDir, routerName, routeName);
|
|
207
|
-
|
|
208
|
-
// Create directory structure
|
|
209
|
-
await mkdir(join(routePath, "models"), { recursive: true });
|
|
210
|
-
await mkdir(join(routePath, "tests"), { recursive: true });
|
|
211
|
-
|
|
212
|
-
// Generate PascalCase names
|
|
213
|
-
const pascalRouter = toPascalCase(routerName);
|
|
214
|
-
const pascalRoute = toPascalCase(routeName);
|
|
215
|
-
const camelRoute = routeName.replace(/-([a-z])/g, (_: string, c: string) => c.toUpperCase());
|
|
216
|
-
|
|
217
|
-
// Create model.ts with Model class
|
|
218
|
-
const modelContent = `import { z } from "zod";
|
|
219
|
-
|
|
220
|
-
// After running \`donkeylabs generate\`, use typed Handler:
|
|
221
|
-
// import type { Handler } from "@donkeylabs/server";
|
|
222
|
-
// import type { ${pascalRouter} } from "$server/routes";
|
|
223
|
-
// export class ${pascalRoute}Model implements Handler<${pascalRouter}.${pascalRoute}> { ... }
|
|
224
|
-
|
|
225
|
-
// Input/Output schemas
|
|
226
|
-
export const Input = z.object({
|
|
227
|
-
// Define your input schema here
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
export const Output = z.object({
|
|
231
|
-
success: z.boolean(),
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
export type Input = z.infer<typeof Input>;
|
|
235
|
-
export type Output = z.infer<typeof Output>;
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* Model class with handler logic.
|
|
239
|
-
* After gen:types, implement Handler<${pascalRouter}.${pascalRoute}> for full typing.
|
|
240
|
-
*/
|
|
241
|
-
export class ${pascalRoute}Model {
|
|
242
|
-
constructor(private ctx: any) {}
|
|
243
|
-
|
|
244
|
-
handle(input: Input): Output {
|
|
245
|
-
return {
|
|
246
|
-
success: true,
|
|
247
|
-
};
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
`;
|
|
251
|
-
await writeFile(join(routePath, "models/model.ts"), modelContent);
|
|
252
|
-
|
|
253
|
-
// Create index.ts (route definition)
|
|
254
|
-
const routeIndexContent = `// Route definition
|
|
255
|
-
// After gen:types, use: Route<${pascalRouter}.${pascalRoute}> for full typing
|
|
256
|
-
import type { AppRoute } from "@donkeylabs/server";
|
|
257
|
-
import { Input, Output, ${pascalRoute}Model } from "./models/model";
|
|
258
|
-
|
|
259
|
-
export const ${camelRoute}Route: AppRoute = {
|
|
260
|
-
input: Input,
|
|
261
|
-
output: Output,
|
|
262
|
-
handle: async (input, ctx) => {
|
|
263
|
-
const model = new ${pascalRoute}Model(ctx);
|
|
264
|
-
return model.handle(input);
|
|
265
|
-
},
|
|
266
|
-
};
|
|
267
|
-
`;
|
|
268
|
-
await writeFile(join(routePath, "index.ts"), routeIndexContent);
|
|
269
|
-
|
|
270
|
-
// Create unit.test.ts
|
|
271
|
-
const unitTestContent = `import { describe, it, expect } from "bun:test";
|
|
272
|
-
import { ${pascalRoute}Model, Input } from "../models/model";
|
|
273
|
-
|
|
274
|
-
describe("${routerName}.${routeName} model", () => {
|
|
275
|
-
it("should return success", () => {
|
|
276
|
-
const input = Input.parse({});
|
|
277
|
-
const ctx = {} as any;
|
|
278
|
-
const model = new ${pascalRoute}Model(ctx);
|
|
279
|
-
const result = model.handle(input);
|
|
280
|
-
expect(result.success).toBe(true);
|
|
281
|
-
});
|
|
282
|
-
});
|
|
283
|
-
`;
|
|
284
|
-
await writeFile(join(routePath, "tests/unit.test.ts"), unitTestContent);
|
|
285
|
-
|
|
286
|
-
// Create integ.test.ts
|
|
287
|
-
const integTestContent = `import { describe, it, expect } from "bun:test";
|
|
288
|
-
|
|
289
|
-
const BASE_URL = process.env.TEST_SERVER_URL || "http://localhost:3000";
|
|
290
|
-
|
|
291
|
-
describe("${routerName}.${routeName} (integration)", () => {
|
|
292
|
-
// Server must be running for integration tests
|
|
293
|
-
it("POST /${routerName}.${routeName} returns success", async () => {
|
|
294
|
-
try {
|
|
295
|
-
const res = await fetch(\`\${BASE_URL}/${routerName}.${routeName}\`, {
|
|
296
|
-
method: "POST",
|
|
297
|
-
headers: { "Content-Type": "application/json" },
|
|
298
|
-
body: JSON.stringify({}),
|
|
299
|
-
});
|
|
300
|
-
expect(res.ok).toBe(true);
|
|
301
|
-
} catch (e: any) {
|
|
302
|
-
if (e.code === "ConnectionRefused") {
|
|
303
|
-
console.log("Skipping: Server not running");
|
|
304
|
-
return;
|
|
305
|
-
}
|
|
306
|
-
throw e;
|
|
307
|
-
}
|
|
308
|
-
});
|
|
309
|
-
});
|
|
310
|
-
`;
|
|
311
|
-
await writeFile(join(routePath, "tests/integ.test.ts"), integTestContent);
|
|
312
|
-
|
|
313
|
-
// Check if router index exists, if not create it
|
|
314
|
-
const routerIndexPath = join(routesDir, routerName, "index.ts");
|
|
315
|
-
if (!existsSync(routerIndexPath)) {
|
|
316
|
-
const routerIndexContent = `import { createRouter } from "@donkeylabs/server";
|
|
317
|
-
import { ${camelRoute}Route } from "./${routeName}";
|
|
318
|
-
|
|
319
|
-
export const ${camelRoute}Router = createRouter("${routerName}")
|
|
320
|
-
.route("${routeName}").typed(${camelRoute}Route);
|
|
321
|
-
`;
|
|
322
|
-
await writeFile(routerIndexPath, routerIndexContent);
|
|
323
|
-
console.log(pc.green(`Created router: src/routes/${routerName}/index.ts`));
|
|
324
|
-
} else {
|
|
325
|
-
console.log(pc.yellow(`\nNote: Add the route to src/routes/${routerName}/index.ts:`));
|
|
326
|
-
console.log(pc.gray(` import { ${camelRoute}Route } from "./${routeName}";`));
|
|
327
|
-
console.log(pc.gray(` .route("${routeName}").typed(${camelRoute}Route)`));
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
console.log(pc.green(`\nCreated route: src/routes/${routerName}/${routeName}/`));
|
|
331
|
-
console.log(pc.gray(` - index.ts`));
|
|
332
|
-
console.log(pc.gray(` - models/model.ts`));
|
|
333
|
-
console.log(pc.gray(` - tests/unit.test.ts`));
|
|
334
|
-
console.log(pc.gray(` - tests/integ.test.ts`));
|
|
335
|
-
console.log(pc.cyan(`\nRun ${pc.bold("donkeylabs generate")} to update types.`));
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
function toPascalCase(str: string): string {
|
|
339
|
-
return str
|
|
340
|
-
.replace(/-([a-z])/g, (_, c) => c.toUpperCase())
|
|
341
|
-
.replace(/^./, (c) => c.toUpperCase());
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
async function createMigration(pluginName: string) {
|
|
345
|
-
const nameRes = await prompts({
|
|
346
|
-
type: "text",
|
|
347
|
-
name: "migName",
|
|
348
|
-
message: "Migration name (e.g. add_comments):",
|
|
349
|
-
validate: (v) =>
|
|
350
|
-
/^[a-z0-9_]+$/.test(v) ? true : "Use lowercase letters, numbers, and underscores",
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
if (!nameRes.migName) return;
|
|
354
|
-
|
|
355
|
-
// Determine migrations directory
|
|
356
|
-
const cwd = process.cwd();
|
|
357
|
-
const isPluginDir = basename(join(cwd, "..")) === "plugins";
|
|
358
|
-
const migrationsDir = isPluginDir
|
|
359
|
-
? join(cwd, "migrations")
|
|
360
|
-
: join(process.cwd(), "src/plugins", pluginName, "migrations");
|
|
361
|
-
|
|
362
|
-
// Generate sequential number
|
|
363
|
-
let nextNum = 1;
|
|
364
|
-
try {
|
|
365
|
-
const files = await readdir(migrationsDir);
|
|
366
|
-
const nums = files
|
|
367
|
-
.map((f) => parseInt(f.split("_")[0] || "", 10))
|
|
368
|
-
.filter((n) => !isNaN(n));
|
|
369
|
-
if (nums.length > 0) {
|
|
370
|
-
nextNum = Math.max(...nums) + 1;
|
|
371
|
-
}
|
|
372
|
-
} catch {}
|
|
373
|
-
|
|
374
|
-
const filename = `${String(nextNum).padStart(3, "0")}_${nameRes.migName}.ts`;
|
|
375
|
-
const content = `import type { Kysely } from "kysely";
|
|
376
|
-
|
|
377
|
-
export async function up(db: Kysely<any>): Promise<void> {
|
|
378
|
-
// await db.schema.createTable("...").execute();
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
export async function down(db: Kysely<any>): Promise<void> {
|
|
382
|
-
// await db.schema.dropTable("...").execute();
|
|
383
|
-
}
|
|
384
|
-
`;
|
|
385
|
-
|
|
386
|
-
if (!existsSync(migrationsDir)) {
|
|
387
|
-
await mkdir(migrationsDir, { recursive: true });
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
await writeFile(join(migrationsDir, filename), content);
|
|
391
|
-
console.log(pc.green(`Created migration: ${filename}`));
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
// ============================================
|
|
395
|
-
// Helpers
|
|
396
|
-
// ============================================
|
|
397
|
-
|
|
398
|
-
async function runCommand(cmd: string) {
|
|
399
|
-
console.log(pc.gray(`> ${cmd}\n`));
|
|
400
|
-
try {
|
|
401
|
-
const { stdout, stderr } = await execAsync(cmd);
|
|
402
|
-
if (stdout) console.log(stdout);
|
|
403
|
-
if (stderr) console.error(pc.yellow(stderr));
|
|
404
|
-
} catch (e: any) {
|
|
405
|
-
console.error(pc.red("Command failed:"), e.message);
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
async function pressEnterToContinue() {
|
|
410
|
-
await prompts({
|
|
411
|
-
type: "invisible",
|
|
412
|
-
name: "continue",
|
|
413
|
-
message: pc.gray("Press Enter to continue..."),
|
|
414
|
-
});
|
|
415
|
-
console.clear();
|
|
416
|
-
console.log(pc.magenta(pc.bold("\n @donkeylabs/server CLI\n")));
|
|
417
|
-
}
|