@donkeylabs/cli 2.0.14 → 2.0.16

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 (85) hide show
  1. package/package.json +1 -1
  2. package/src/commands/config.ts +610 -0
  3. package/src/commands/deploy-enhanced.ts +354 -0
  4. package/src/commands/deploy.ts +204 -0
  5. package/src/commands/generate.ts +11 -13
  6. package/src/commands/init-enhanced.ts +1994 -0
  7. package/src/deployment/manager.ts +356 -0
  8. package/src/index.ts +47 -19
  9. package/templates/starter/.env.example +0 -44
  10. package/templates/starter/.gitignore.template +0 -4
  11. package/templates/starter/donkeylabs.config.ts +0 -6
  12. package/templates/starter/package.json +0 -21
  13. package/templates/starter/src/index.ts +0 -54
  14. package/templates/starter/src/plugins/stats/index.ts +0 -105
  15. package/templates/starter/src/routes/health/handlers/ping.ts +0 -22
  16. package/templates/starter/src/routes/health/index.ts +0 -19
  17. package/templates/starter/tsconfig.json +0 -27
  18. package/templates/sveltekit-app/.env.example +0 -59
  19. package/templates/sveltekit-app/README.md +0 -103
  20. package/templates/sveltekit-app/bun.lock +0 -683
  21. package/templates/sveltekit-app/donkeylabs.config.ts +0 -12
  22. package/templates/sveltekit-app/package.json +0 -38
  23. package/templates/sveltekit-app/src/app.css +0 -40
  24. package/templates/sveltekit-app/src/app.html +0 -12
  25. package/templates/sveltekit-app/src/hooks.server.ts +0 -4
  26. package/templates/sveltekit-app/src/lib/components/ui/badge/badge.svelte +0 -30
  27. package/templates/sveltekit-app/src/lib/components/ui/badge/index.ts +0 -3
  28. package/templates/sveltekit-app/src/lib/components/ui/button/button.svelte +0 -48
  29. package/templates/sveltekit-app/src/lib/components/ui/button/index.ts +0 -9
  30. package/templates/sveltekit-app/src/lib/components/ui/card/card-content.svelte +0 -18
  31. package/templates/sveltekit-app/src/lib/components/ui/card/card-description.svelte +0 -18
  32. package/templates/sveltekit-app/src/lib/components/ui/card/card-footer.svelte +0 -18
  33. package/templates/sveltekit-app/src/lib/components/ui/card/card-header.svelte +0 -18
  34. package/templates/sveltekit-app/src/lib/components/ui/card/card-title.svelte +0 -18
  35. package/templates/sveltekit-app/src/lib/components/ui/card/card.svelte +0 -21
  36. package/templates/sveltekit-app/src/lib/components/ui/card/index.ts +0 -21
  37. package/templates/sveltekit-app/src/lib/components/ui/index.ts +0 -4
  38. package/templates/sveltekit-app/src/lib/components/ui/input/index.ts +0 -2
  39. package/templates/sveltekit-app/src/lib/components/ui/input/input.svelte +0 -20
  40. package/templates/sveltekit-app/src/lib/permissions.ts +0 -213
  41. package/templates/sveltekit-app/src/lib/utils/index.ts +0 -6
  42. package/templates/sveltekit-app/src/routes/+layout.svelte +0 -8
  43. package/templates/sveltekit-app/src/routes/+page.server.ts +0 -25
  44. package/templates/sveltekit-app/src/routes/+page.svelte +0 -680
  45. package/templates/sveltekit-app/src/routes/workflows/+page.server.ts +0 -23
  46. package/templates/sveltekit-app/src/routes/workflows/+page.svelte +0 -522
  47. package/templates/sveltekit-app/src/server/events.ts +0 -28
  48. package/templates/sveltekit-app/src/server/index.ts +0 -124
  49. package/templates/sveltekit-app/src/server/plugins/auth/auth.test.ts +0 -377
  50. package/templates/sveltekit-app/src/server/plugins/auth/index.ts +0 -815
  51. package/templates/sveltekit-app/src/server/plugins/auth/migrations/001_create_users.ts +0 -25
  52. package/templates/sveltekit-app/src/server/plugins/auth/migrations/002_create_sessions.ts +0 -32
  53. package/templates/sveltekit-app/src/server/plugins/auth/migrations/003_create_refresh_tokens.ts +0 -33
  54. package/templates/sveltekit-app/src/server/plugins/auth/migrations/004_create_passkeys.ts +0 -60
  55. package/templates/sveltekit-app/src/server/plugins/auth/schema.ts +0 -65
  56. package/templates/sveltekit-app/src/server/plugins/demo/index.ts +0 -262
  57. package/templates/sveltekit-app/src/server/plugins/email/email.test.ts +0 -369
  58. package/templates/sveltekit-app/src/server/plugins/email/index.ts +0 -411
  59. package/templates/sveltekit-app/src/server/plugins/email/migrations/001_create_email_tokens.ts +0 -33
  60. package/templates/sveltekit-app/src/server/plugins/email/schema.ts +0 -24
  61. package/templates/sveltekit-app/src/server/plugins/permissions/index.ts +0 -1048
  62. package/templates/sveltekit-app/src/server/plugins/permissions/migrations/001_create_tenants.ts +0 -63
  63. package/templates/sveltekit-app/src/server/plugins/permissions/migrations/002_create_roles.ts +0 -90
  64. package/templates/sveltekit-app/src/server/plugins/permissions/migrations/003_create_resource_grants.ts +0 -50
  65. package/templates/sveltekit-app/src/server/plugins/permissions/permissions.test.ts +0 -566
  66. package/templates/sveltekit-app/src/server/plugins/permissions/schema.ts +0 -67
  67. package/templates/sveltekit-app/src/server/plugins/workflow-demo/index.ts +0 -198
  68. package/templates/sveltekit-app/src/server/routes/auth/auth.schemas.ts +0 -66
  69. package/templates/sveltekit-app/src/server/routes/auth/handlers/login.handler.ts +0 -18
  70. package/templates/sveltekit-app/src/server/routes/auth/handlers/logout.handler.ts +0 -16
  71. package/templates/sveltekit-app/src/server/routes/auth/handlers/me.handler.ts +0 -20
  72. package/templates/sveltekit-app/src/server/routes/auth/handlers/refresh.handler.ts +0 -17
  73. package/templates/sveltekit-app/src/server/routes/auth/handlers/register.handler.ts +0 -19
  74. package/templates/sveltekit-app/src/server/routes/auth/handlers/update-profile.handler.ts +0 -21
  75. package/templates/sveltekit-app/src/server/routes/auth/index.ts +0 -73
  76. package/templates/sveltekit-app/src/server/routes/demo.ts +0 -464
  77. package/templates/sveltekit-app/src/server/routes/example/example.schemas.ts +0 -22
  78. package/templates/sveltekit-app/src/server/routes/example/handlers/greet.handler.ts +0 -21
  79. package/templates/sveltekit-app/src/server/routes/example/index.ts +0 -28
  80. package/templates/sveltekit-app/src/server/routes/permissions/index.ts +0 -248
  81. package/templates/sveltekit-app/src/server/routes/tenants/index.ts +0 -339
  82. package/templates/sveltekit-app/static/robots.txt +0 -3
  83. package/templates/sveltekit-app/svelte.config.ts +0 -17
  84. package/templates/sveltekit-app/tsconfig.json +0 -20
  85. package/templates/sveltekit-app/vite.config.ts +0 -12
@@ -0,0 +1,356 @@
1
+ // packages/cli/src/deployment/manager.ts
2
+ /**
3
+ * Deployment Management System
4
+ * Handles deployment history, versioning, rollbacks
5
+ */
6
+
7
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
8
+ import { join } from "path";
9
+ import { execSync } from "child_process";
10
+ import { randomUUID } from "crypto";
11
+
12
+ export interface DeploymentConfig {
13
+ projectName: string;
14
+ platform: "vercel" | "cloudflare" | "aws" | "vps";
15
+ environment: "production" | "staging" | "development";
16
+ versionStrategy: "semver" | "git-sha" | "timestamp";
17
+ }
18
+
19
+ export interface Deployment {
20
+ id: string;
21
+ version: string;
22
+ platform: string;
23
+ environment: string;
24
+ timestamp: string;
25
+ gitSha: string;
26
+ gitMessage: string;
27
+ status: "success" | "failed" | "rolling-back";
28
+ url?: string;
29
+ logs: string[];
30
+ metadata: Record<string, any>;
31
+ }
32
+
33
+ export interface DeploymentHistory {
34
+ deployments: Deployment[];
35
+ currentVersion: string;
36
+ lastDeployedAt: string;
37
+ }
38
+
39
+ export class DeploymentManager {
40
+ private projectDir: string;
41
+ private historyFile: string;
42
+ private config: DeploymentConfig;
43
+
44
+ constructor(projectDir: string, config: DeploymentConfig) {
45
+ this.projectDir = projectDir;
46
+ this.config = config;
47
+ this.historyFile = join(projectDir, ".donkeylabs", "deployments.json");
48
+ this.ensureHistoryDir();
49
+ }
50
+
51
+ /**
52
+ * Get next version based on strategy
53
+ */
54
+ getNextVersion(bump: "major" | "minor" | "patch" = "patch"): string {
55
+ const { versionStrategy } = this.config;
56
+
57
+ if (versionStrategy === "git-sha") {
58
+ return this.getGitSha();
59
+ }
60
+
61
+ if (versionStrategy === "timestamp") {
62
+ return Date.now().toString();
63
+ }
64
+
65
+ // Semver
66
+ const current = this.getCurrentVersion();
67
+ const [major, minor, patch] = current.split(".").map(Number);
68
+
69
+ switch (bump) {
70
+ case "major":
71
+ return `${major + 1}.0.0`;
72
+ case "minor":
73
+ return `${major}.${minor + 1}.0`;
74
+ case "patch":
75
+ default:
76
+ return `${major}.${minor}.${patch + 1}`;
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Record a new deployment
82
+ */
83
+ async recordDeployment(
84
+ version: string,
85
+ platform: string,
86
+ status: Deployment["status"],
87
+ url?: string,
88
+ metadata?: Record<string, any>
89
+ ): Promise<Deployment> {
90
+ const deployment: Deployment = {
91
+ id: randomUUID(),
92
+ version,
93
+ platform,
94
+ environment: this.config.environment,
95
+ timestamp: new Date().toISOString(),
96
+ gitSha: this.getGitSha(),
97
+ gitMessage: this.getGitMessage(),
98
+ status,
99
+ url,
100
+ logs: [],
101
+ metadata: metadata || {},
102
+ };
103
+
104
+ const history = this.getHistory();
105
+ history.deployments.unshift(deployment);
106
+ history.currentVersion = version;
107
+ history.lastDeployedAt = deployment.timestamp;
108
+
109
+ // Keep only last 50 deployments
110
+ history.deployments = history.deployments.slice(0, 50);
111
+
112
+ this.saveHistory(history);
113
+
114
+ return deployment;
115
+ }
116
+
117
+ /**
118
+ * Get deployment history
119
+ */
120
+ getHistory(): DeploymentHistory {
121
+ if (!existsSync(this.historyFile)) {
122
+ return {
123
+ deployments: [],
124
+ currentVersion: "0.0.0",
125
+ lastDeployedAt: "",
126
+ };
127
+ }
128
+
129
+ return JSON.parse(readFileSync(this.historyFile, "utf-8"));
130
+ }
131
+
132
+ /**
133
+ * Get specific deployment
134
+ */
135
+ getDeployment(deploymentId: string): Deployment | null {
136
+ const history = this.getHistory();
137
+ return history.deployments.find((d) => d.id === deploymentId) || null;
138
+ }
139
+
140
+ /**
141
+ * Get last successful deployment
142
+ */
143
+ getLastSuccessfulDeployment(): Deployment | null {
144
+ const history = this.getHistory();
145
+ return history.deployments.find((d) => d.status === "success") || null;
146
+ }
147
+
148
+ /**
149
+ * Rollback to previous version
150
+ */
151
+ async rollback(toVersion?: string): Promise<Deployment | null> {
152
+ const history = this.getHistory();
153
+
154
+ // Find target deployment
155
+ let targetDeployment: Deployment | undefined;
156
+
157
+ if (toVersion) {
158
+ targetDeployment = history.deployments.find((d) => d.version === toVersion);
159
+ } else {
160
+ // Find last successful deployment before current
161
+ const currentIndex = history.deployments.findIndex(
162
+ (d) => d.version === history.currentVersion
163
+ );
164
+ targetDeployment = history.deployments
165
+ .slice(currentIndex + 1)
166
+ .find((d) => d.status === "success");
167
+ }
168
+
169
+ if (!targetDeployment) {
170
+ throw new Error("No previous successful deployment found to rollback to");
171
+ }
172
+
173
+ // Checkout the git sha
174
+ try {
175
+ execSync(`git checkout ${targetDeployment.gitSha}`, {
176
+ cwd: this.projectDir,
177
+ stdio: "pipe",
178
+ });
179
+
180
+ // Mark as rolling back
181
+ targetDeployment.status = "rolling-back";
182
+ this.saveHistory(history);
183
+
184
+ // Redeploy
185
+ await this.redeploy(targetDeployment);
186
+
187
+ // Record the rollback
188
+ const rollbackDeployment = await this.recordDeployment(
189
+ `${targetDeployment.version}-rollback`,
190
+ targetDeployment.platform,
191
+ "success",
192
+ targetDeployment.url,
193
+ { rollbackFrom: history.currentVersion, originalDeployment: targetDeployment.id }
194
+ );
195
+
196
+ return rollbackDeployment;
197
+ } catch (error) {
198
+ // Revert git checkout
199
+ execSync("git checkout -", { cwd: this.projectDir, stdio: "pipe" });
200
+ throw error;
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Get deployment statistics
206
+ */
207
+ getStats(): {
208
+ totalDeployments: number;
209
+ successfulDeployments: number;
210
+ failedDeployments: number;
211
+ rollbackCount: number;
212
+ averageDeployTime: number;
213
+ deploymentsByPlatform: Record<string, number>;
214
+ } {
215
+ const history = this.getHistory();
216
+ const deployments = history.deployments;
217
+
218
+ const successful = deployments.filter((d) => d.status === "success");
219
+ const failed = deployments.filter((d) => d.status === "failed");
220
+ const rollbacks = deployments.filter((d) => d.metadata?.rollbackFrom);
221
+
222
+ const byPlatform: Record<string, number> = {};
223
+ for (const d of deployments) {
224
+ byPlatform[d.platform] = (byPlatform[d.platform] || 0) + 1;
225
+ }
226
+
227
+ return {
228
+ totalDeployments: deployments.length,
229
+ successfulDeployments: successful.length,
230
+ failedDeployments: failed.length,
231
+ rollbackCount: rollbacks.length,
232
+ averageDeployTime: 0, // Would need to track start/end times
233
+ deploymentsByPlatform: byPlatform,
234
+ };
235
+ }
236
+
237
+ /**
238
+ * List deployments with filtering
239
+ */
240
+ listDeployments(options?: {
241
+ platform?: string;
242
+ status?: Deployment["status"];
243
+ limit?: number;
244
+ }): Deployment[] {
245
+ let deployments = this.getHistory().deployments;
246
+
247
+ if (options?.platform) {
248
+ deployments = deployments.filter((d) => d.platform === options.platform);
249
+ }
250
+
251
+ if (options?.status) {
252
+ deployments = deployments.filter((d) => d.status === options.status);
253
+ }
254
+
255
+ if (options?.limit) {
256
+ deployments = deployments.slice(0, options.limit);
257
+ }
258
+
259
+ return deployments;
260
+ }
261
+
262
+ /**
263
+ * Compare two deployments
264
+ */
265
+ compareDeployments(deploymentId1: string, deploymentId2: string): {
266
+ deployment1: Deployment;
267
+ deployment2: Deployment;
268
+ gitDiff: string;
269
+ } {
270
+ const d1 = this.getDeployment(deploymentId1);
271
+ const d2 = this.getDeployment(deploymentId2);
272
+
273
+ if (!d1 || !d2) {
274
+ throw new Error("One or both deployments not found");
275
+ }
276
+
277
+ const diff = execSync(
278
+ `git log --oneline ${d2.gitSha}..${d1.gitSha}`,
279
+ { cwd: this.projectDir, encoding: "utf-8" }
280
+ );
281
+
282
+ return {
283
+ deployment1: d1,
284
+ deployment2: d2,
285
+ gitDiff: diff,
286
+ };
287
+ }
288
+
289
+ private ensureHistoryDir(): void {
290
+ const dir = join(this.projectDir, ".donkeylabs");
291
+ if (!existsSync(dir)) {
292
+ mkdirSync(dir, { recursive: true });
293
+ }
294
+ }
295
+
296
+ private saveHistory(history: DeploymentHistory): void {
297
+ writeFileSync(this.historyFile, JSON.stringify(history, null, 2));
298
+ }
299
+
300
+ private getCurrentVersion(): string {
301
+ const pkgPath = join(this.projectDir, "package.json");
302
+ if (!existsSync(pkgPath)) {
303
+ return "0.0.0";
304
+ }
305
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
306
+ return pkg.version || "0.0.0";
307
+ }
308
+
309
+ private getGitSha(): string {
310
+ try {
311
+ return execSync("git rev-parse --short HEAD", {
312
+ cwd: this.projectDir,
313
+ encoding: "utf-8",
314
+ }).trim();
315
+ } catch {
316
+ return "unknown";
317
+ }
318
+ }
319
+
320
+ private getGitMessage(): string {
321
+ try {
322
+ return execSync("git log -1 --pretty=%B", {
323
+ cwd: this.projectDir,
324
+ encoding: "utf-8",
325
+ }).trim();
326
+ } catch {
327
+ return "unknown";
328
+ }
329
+ }
330
+
331
+ private async redeploy(deployment: Deployment): Promise<void> {
332
+ // Platform-specific redeploy logic
333
+ switch (deployment.platform) {
334
+ case "vercel":
335
+ execSync("vercel --prod", {
336
+ cwd: this.projectDir,
337
+ stdio: "inherit",
338
+ });
339
+ break;
340
+ case "cloudflare":
341
+ execSync("wrangler deploy", {
342
+ cwd: this.projectDir,
343
+ stdio: "inherit",
344
+ });
345
+ break;
346
+ // Add other platforms
347
+ }
348
+ }
349
+ }
350
+
351
+ export function createDeploymentManager(
352
+ projectDir: string,
353
+ config: DeploymentConfig
354
+ ): DeploymentManager {
355
+ return new DeploymentManager(projectDir, config);
356
+ }
package/src/index.ts CHANGED
@@ -17,6 +17,7 @@ const { positionals, values } = parseArgs({
17
17
  help: { type: "boolean", short: "h" },
18
18
  version: { type: "boolean", short: "v" },
19
19
  type: { type: "string", short: "t" },
20
+ local: { type: "boolean", short: "l" },
20
21
  },
21
22
  allowPositionals: true,
22
23
  });
@@ -32,24 +33,33 @@ ${pc.bold("Usage:")}
32
33
  donkeylabs <command> [options]
33
34
 
34
35
  ${pc.bold("Commands:")}
35
- ${pc.cyan("init")} Initialize a new project
36
- ${pc.cyan("add")} Add optional plugins (images, auth, etc.)
37
- ${pc.cyan("generate")} Generate types (registry, context, client)
38
- ${pc.cyan("plugin")} Plugin management
39
- ${pc.cyan("mcp")} Setup MCP server for AI-assisted development
36
+ ${pc.cyan("init")} Initialize a new project
37
+ ${pc.cyan("add")} Add optional plugins (images, auth, etc.)
38
+ ${pc.cyan("generate")} Generate types (registry, context, client)
39
+ ${pc.cyan("plugin")} Plugin management
40
+ ${pc.cyan("deploy")} <platform> Deploy (vercel, cloudflare, aws, vps)
41
+ ${pc.cyan("deploy history")} Show deployment history
42
+ ${pc.cyan("deploy rollback")} Rollback to version
43
+ ${pc.cyan("deploy stats")} Show deployment statistics
44
+ ${pc.cyan("config")} Configure plugins, deployment, database
45
+ ${pc.cyan("mcp")} Setup MCP server for AI-assisted development
40
46
 
41
47
  ${pc.bold("Options:")}
42
48
  -h, --help Show this help message
43
49
  -v, --version Show version number
44
50
  -t, --type <type> Project type for init (server, sveltekit)
45
-
46
- ${pc.bold("Examples:")}
47
- donkeylabs # Interactive menu
48
- donkeylabs init # Interactive project setup
49
- donkeylabs init --type server # Server-only project
50
- donkeylabs init --type sveltekit # SvelteKit + adapter project
51
- donkeylabs generate
52
- donkeylabs plugin create myPlugin
51
+ -l, --local Use local workspace packages (for monorepo dev)
52
+
53
+ ${pc.bold("Examples:")}
54
+ donkeylabs # Interactive menu
55
+ donkeylabs init # Interactive project setup
56
+ donkeylabs init --type server # Server-only project
57
+ donkeylabs init --type sveltekit # SvelteKit + adapter project
58
+ donkeylabs generate
59
+ donkeylabs plugin create myPlugin
60
+ donkeylabs deploy vercel # Deploy to Vercel
61
+ donkeylabs config # Interactive configuration
62
+ donkeylabs config set DATABASE_URL postgresql://...
53
63
  `);
54
64
  }
55
65
 
@@ -78,12 +88,8 @@ async function main() {
78
88
 
79
89
  switch (command) {
80
90
  case "init":
81
- const { initCommand } = await import("./commands/init");
82
- const initArgs = [...positionals.slice(1)];
83
- if (values.type) {
84
- initArgs.push("--type", values.type);
85
- }
86
- await initCommand(initArgs);
91
+ const { initEnhancedCommand } = await import("./commands/init-enhanced");
92
+ await initEnhancedCommand(positionals.slice(1), { useLocalPackages: values.local });
87
93
  break;
88
94
 
89
95
  case "add":
@@ -107,6 +113,28 @@ async function main() {
107
113
  await mcpCommand(positionals.slice(1));
108
114
  break;
109
115
 
116
+ case "deploy":
117
+ const subcommand = positionals[1];
118
+ if (subcommand === "history") {
119
+ const { deployHistoryCommand } = await import("./commands/deploy-enhanced");
120
+ await deployHistoryCommand(positionals.slice(2));
121
+ } else if (subcommand === "rollback") {
122
+ const { deployRollbackCommand } = await import("./commands/deploy-enhanced");
123
+ await deployRollbackCommand(positionals.slice(2));
124
+ } else if (subcommand === "stats") {
125
+ const { deployStatsCommand } = await import("./commands/deploy-enhanced");
126
+ await deployStatsCommand();
127
+ } else {
128
+ const { deployEnhancedCommand } = await import("./commands/deploy-enhanced");
129
+ await deployEnhancedCommand(positionals.slice(1));
130
+ }
131
+ break;
132
+
133
+ case "config":
134
+ const { configCommand } = await import("./commands/config");
135
+ await configCommand(positionals.slice(1));
136
+ break;
137
+
110
138
  default:
111
139
  console.error(pc.red(`Unknown command: ${command}`));
112
140
  console.log(`Run ${pc.cyan("donkeylabs --help")} for available commands.`);
@@ -1,44 +0,0 @@
1
- # =============================================================================
2
- # DATABASE
3
- # =============================================================================
4
-
5
- # SQLite database path (relative to project root)
6
- # Use ":memory:" for in-memory database during development
7
- DATABASE_URL=":memory:"
8
-
9
- # For production, use a file path:
10
- # DATABASE_URL="./data/app.db"
11
-
12
- # =============================================================================
13
- # SERVER
14
- # =============================================================================
15
-
16
- # Port for the API server
17
- PORT=3000
18
-
19
- # Node environment
20
- NODE_ENV=development
21
-
22
- # =============================================================================
23
- # AUTHENTICATION (if using auth plugin)
24
- # =============================================================================
25
-
26
- # JWT secret for signing tokens (generate with: openssl rand -base64 32)
27
- # JWT_SECRET=your-secret-key-here
28
-
29
- # =============================================================================
30
- # EXTERNAL SERVICES (examples)
31
- # =============================================================================
32
-
33
- # Email service
34
- # RESEND_API_KEY=re_xxxxxxxxxxxx
35
-
36
- # Payment processing
37
- # STRIPE_SECRET_KEY=sk_test_xxxxxxxxxxxx
38
-
39
- # =============================================================================
40
- # FEATURE FLAGS
41
- # =============================================================================
42
-
43
- # Enable debug logging
44
- # DEBUG=true
@@ -1,4 +0,0 @@
1
- node_modules/
2
- .@donkeylabs/
3
- *.db
4
- .env
@@ -1,6 +0,0 @@
1
- import { defineConfig } from "@donkeylabs/server";
2
-
3
- export default defineConfig({
4
- plugins: ["./src/plugins/**/index.ts"],
5
- outDir: ".@donkeylabs/server",
6
- });
@@ -1,21 +0,0 @@
1
- {
2
- "name": "my-donkeylabs-app",
3
- "version": "0.0.1",
4
- "type": "module",
5
- "scripts": {
6
- "dev": "bun --watch src/index.ts",
7
- "start": "bun src/index.ts",
8
- "test": "bun test",
9
- "gen:types": "donkeylabs generate"
10
- },
11
- "dependencies": {
12
- "@donkeylabs/server": "^2.0.15",
13
- "kysely": "^0.27.0",
14
- "kysely-bun-sqlite": "^0.3.0",
15
- "zod": "^3.24.0"
16
- },
17
- "devDependencies": {
18
- "@donkeylabs/cli": "^2.0.11",
19
- "@types/bun": "latest"
20
- }
21
- }
@@ -1,54 +0,0 @@
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";
5
- import { healthRouter } from "./routes/health";
6
- import { statsPlugin } from "./plugins/stats";
7
-
8
- // Simple in-memory database
9
- const db = new Kysely<{}>({
10
- dialect: new BunSqliteDialect({ database: new Database(":memory:") }),
11
- });
12
-
13
- const server = new AppServer({
14
- port: Number(process.env.PORT) || 3000,
15
- db,
16
- config: { env: process.env.NODE_ENV || "development" },
17
- generateTypes: {
18
- output: "./.@donkeylabs/server/api.ts",
19
- baseImport: 'import { ApiClientBase, type ApiClientOptions } from "@donkeylabs/server/client";',
20
- baseClass: "ApiClientBase",
21
- constructorSignature: "baseUrl: string, options?: ApiClientOptions",
22
- constructorBody: "super(baseUrl, options);",
23
- factoryFunction: `/**
24
- * Create an API client instance
25
- * @param baseUrl - The base URL of the API server
26
- */
27
- export function createApi(baseUrl: string, options?: ApiClientOptions) {
28
- return new ApiClient(baseUrl, options);
29
- }`,
30
- },
31
- });
32
-
33
- // Register plugins
34
- server.registerPlugin(statsPlugin);
35
-
36
- const api = createRouter("api");
37
- // Register routes
38
- api.router(healthRouter);
39
-
40
- server.use(api);
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
- }
53
-
54
- await server.start();
@@ -1,105 +0,0 @@
1
- import { createPlugin, createMiddleware } from "@donkeylabs/server";
2
-
3
- export interface RequestStats {
4
- totalRequests: number;
5
- avgResponseTime: number;
6
- minResponseTime: number;
7
- maxResponseTime: number;
8
- requestsPerRoute: Map<string, number>;
9
- }
10
-
11
- export interface StatsService {
12
- /** Record a request with its duration */
13
- recordRequest(route: string, durationMs: number): void;
14
- /** Get current stats snapshot */
15
- getStats(): RequestStats;
16
- /** Reset all stats */
17
- reset(): void;
18
- }
19
-
20
- export const statsPlugin = createPlugin.define({
21
- name: "stats",
22
- version: "1.0.0",
23
-
24
- // Service must come before middleware for TypeScript to infer the Service type
25
- service: async (ctx): Promise<StatsService> => {
26
- const logger = ctx.core.logger.child({ plugin: "stats" });
27
-
28
- // In-memory stats
29
- let totalRequests = 0;
30
- let totalTime = 0;
31
- let minTime = Infinity;
32
- let maxTime = 0;
33
- const requestsPerRoute = new Map<string, number>();
34
-
35
- function getStats(): RequestStats {
36
- return {
37
- totalRequests,
38
- avgResponseTime: totalRequests > 0 ? totalTime / totalRequests : 0,
39
- minResponseTime: minTime,
40
- maxResponseTime: maxTime,
41
- requestsPerRoute: new Map(requestsPerRoute),
42
- };
43
- }
44
-
45
- logger.info("Stats plugin initialized");
46
-
47
- return {
48
- recordRequest(route: string, durationMs: number) {
49
- totalRequests++;
50
- totalTime += durationMs;
51
- minTime = Math.min(minTime, durationMs);
52
- maxTime = Math.max(maxTime, durationMs);
53
- requestsPerRoute.set(route, (requestsPerRoute.get(route) ?? 0) + 1);
54
-
55
- logger.debug("Request recorded", { route, durationMs: durationMs.toFixed(2) });
56
- },
57
-
58
- getStats,
59
-
60
- reset() {
61
- totalRequests = 0;
62
- totalTime = 0;
63
- minTime = Infinity;
64
- maxTime = 0;
65
- requestsPerRoute.clear();
66
- logger.info("Stats reset");
67
- },
68
- };
69
- },
70
-
71
- // Middleware - ctx is typed PluginContext, service is typed StatsService
72
- middleware: (ctx, service) => ({
73
- /** Timing middleware - records request duration and updates stats */
74
- timing: createMiddleware(async (req, _reqCtx, next) => {
75
- const logger = ctx.core.logger;
76
- const route = new URL(req.url).pathname.slice(1);
77
- const start = performance.now();
78
- const response = await next();
79
- const duration = performance.now() - start;
80
-
81
- // Use own service to record stats - service is typed!
82
- service.recordRequest(route, duration);
83
- logger.info("Request processed", { route, durationMs: duration.toFixed(2) });
84
-
85
- return response;
86
- }),
87
- }),
88
-
89
- // Register crons, events, etc. after service is created
90
- init: (ctx, service) => {
91
- const logger = ctx.core.logger.child({ plugin: "stats" });
92
-
93
- // Log stats every minute
94
- ctx.core.cron.schedule("* * * * *", () => {
95
- const stats = service.getStats();
96
- logger.info("Server stats", {
97
- requests: stats.totalRequests,
98
- avgMs: stats.avgResponseTime.toFixed(2),
99
- minMs: stats.minResponseTime === Infinity ? 0 : stats.minResponseTime.toFixed(2),
100
- maxMs: stats.maxResponseTime.toFixed(2),
101
- routes: Object.fromEntries(stats.requestsPerRoute),
102
- });
103
- }, { name: "stats-reporter" });
104
- },
105
- });