@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.
- package/package.json +1 -1
- package/src/commands/config.ts +610 -0
- package/src/commands/deploy-enhanced.ts +354 -0
- package/src/commands/deploy.ts +204 -0
- package/src/commands/generate.ts +11 -13
- package/src/commands/init-enhanced.ts +1994 -0
- package/src/deployment/manager.ts +356 -0
- package/src/index.ts +47 -19
- package/templates/starter/.env.example +0 -44
- package/templates/starter/.gitignore.template +0 -4
- package/templates/starter/donkeylabs.config.ts +0 -6
- package/templates/starter/package.json +0 -21
- package/templates/starter/src/index.ts +0 -54
- package/templates/starter/src/plugins/stats/index.ts +0 -105
- package/templates/starter/src/routes/health/handlers/ping.ts +0 -22
- package/templates/starter/src/routes/health/index.ts +0 -19
- package/templates/starter/tsconfig.json +0 -27
- package/templates/sveltekit-app/.env.example +0 -59
- package/templates/sveltekit-app/README.md +0 -103
- package/templates/sveltekit-app/bun.lock +0 -683
- package/templates/sveltekit-app/donkeylabs.config.ts +0 -12
- package/templates/sveltekit-app/package.json +0 -38
- package/templates/sveltekit-app/src/app.css +0 -40
- package/templates/sveltekit-app/src/app.html +0 -12
- package/templates/sveltekit-app/src/hooks.server.ts +0 -4
- package/templates/sveltekit-app/src/lib/components/ui/badge/badge.svelte +0 -30
- package/templates/sveltekit-app/src/lib/components/ui/badge/index.ts +0 -3
- package/templates/sveltekit-app/src/lib/components/ui/button/button.svelte +0 -48
- package/templates/sveltekit-app/src/lib/components/ui/button/index.ts +0 -9
- package/templates/sveltekit-app/src/lib/components/ui/card/card-content.svelte +0 -18
- package/templates/sveltekit-app/src/lib/components/ui/card/card-description.svelte +0 -18
- package/templates/sveltekit-app/src/lib/components/ui/card/card-footer.svelte +0 -18
- package/templates/sveltekit-app/src/lib/components/ui/card/card-header.svelte +0 -18
- package/templates/sveltekit-app/src/lib/components/ui/card/card-title.svelte +0 -18
- package/templates/sveltekit-app/src/lib/components/ui/card/card.svelte +0 -21
- package/templates/sveltekit-app/src/lib/components/ui/card/index.ts +0 -21
- package/templates/sveltekit-app/src/lib/components/ui/index.ts +0 -4
- package/templates/sveltekit-app/src/lib/components/ui/input/index.ts +0 -2
- package/templates/sveltekit-app/src/lib/components/ui/input/input.svelte +0 -20
- package/templates/sveltekit-app/src/lib/permissions.ts +0 -213
- package/templates/sveltekit-app/src/lib/utils/index.ts +0 -6
- package/templates/sveltekit-app/src/routes/+layout.svelte +0 -8
- package/templates/sveltekit-app/src/routes/+page.server.ts +0 -25
- package/templates/sveltekit-app/src/routes/+page.svelte +0 -680
- package/templates/sveltekit-app/src/routes/workflows/+page.server.ts +0 -23
- package/templates/sveltekit-app/src/routes/workflows/+page.svelte +0 -522
- package/templates/sveltekit-app/src/server/events.ts +0 -28
- package/templates/sveltekit-app/src/server/index.ts +0 -124
- package/templates/sveltekit-app/src/server/plugins/auth/auth.test.ts +0 -377
- package/templates/sveltekit-app/src/server/plugins/auth/index.ts +0 -815
- package/templates/sveltekit-app/src/server/plugins/auth/migrations/001_create_users.ts +0 -25
- package/templates/sveltekit-app/src/server/plugins/auth/migrations/002_create_sessions.ts +0 -32
- package/templates/sveltekit-app/src/server/plugins/auth/migrations/003_create_refresh_tokens.ts +0 -33
- package/templates/sveltekit-app/src/server/plugins/auth/migrations/004_create_passkeys.ts +0 -60
- package/templates/sveltekit-app/src/server/plugins/auth/schema.ts +0 -65
- package/templates/sveltekit-app/src/server/plugins/demo/index.ts +0 -262
- package/templates/sveltekit-app/src/server/plugins/email/email.test.ts +0 -369
- package/templates/sveltekit-app/src/server/plugins/email/index.ts +0 -411
- package/templates/sveltekit-app/src/server/plugins/email/migrations/001_create_email_tokens.ts +0 -33
- package/templates/sveltekit-app/src/server/plugins/email/schema.ts +0 -24
- package/templates/sveltekit-app/src/server/plugins/permissions/index.ts +0 -1048
- package/templates/sveltekit-app/src/server/plugins/permissions/migrations/001_create_tenants.ts +0 -63
- package/templates/sveltekit-app/src/server/plugins/permissions/migrations/002_create_roles.ts +0 -90
- package/templates/sveltekit-app/src/server/plugins/permissions/migrations/003_create_resource_grants.ts +0 -50
- package/templates/sveltekit-app/src/server/plugins/permissions/permissions.test.ts +0 -566
- package/templates/sveltekit-app/src/server/plugins/permissions/schema.ts +0 -67
- package/templates/sveltekit-app/src/server/plugins/workflow-demo/index.ts +0 -198
- package/templates/sveltekit-app/src/server/routes/auth/auth.schemas.ts +0 -66
- package/templates/sveltekit-app/src/server/routes/auth/handlers/login.handler.ts +0 -18
- package/templates/sveltekit-app/src/server/routes/auth/handlers/logout.handler.ts +0 -16
- package/templates/sveltekit-app/src/server/routes/auth/handlers/me.handler.ts +0 -20
- package/templates/sveltekit-app/src/server/routes/auth/handlers/refresh.handler.ts +0 -17
- package/templates/sveltekit-app/src/server/routes/auth/handlers/register.handler.ts +0 -19
- package/templates/sveltekit-app/src/server/routes/auth/handlers/update-profile.handler.ts +0 -21
- package/templates/sveltekit-app/src/server/routes/auth/index.ts +0 -73
- package/templates/sveltekit-app/src/server/routes/demo.ts +0 -464
- package/templates/sveltekit-app/src/server/routes/example/example.schemas.ts +0 -22
- package/templates/sveltekit-app/src/server/routes/example/handlers/greet.handler.ts +0 -21
- package/templates/sveltekit-app/src/server/routes/example/index.ts +0 -28
- package/templates/sveltekit-app/src/server/routes/permissions/index.ts +0 -248
- package/templates/sveltekit-app/src/server/routes/tenants/index.ts +0 -339
- package/templates/sveltekit-app/static/robots.txt +0 -3
- package/templates/sveltekit-app/svelte.config.ts +0 -17
- package/templates/sveltekit-app/tsconfig.json +0 -20
- 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")}
|
|
36
|
-
${pc.cyan("add")}
|
|
37
|
-
${pc.cyan("generate")}
|
|
38
|
-
${pc.cyan("plugin")}
|
|
39
|
-
${pc.cyan("
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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 {
|
|
82
|
-
|
|
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,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
|
-
});
|