@donkeylabs/cli 2.0.16 → 2.0.18

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@donkeylabs/cli",
3
- "version": "2.0.16",
3
+ "version": "2.0.18",
4
4
  "type": "module",
5
5
  "description": "CLI for @donkeylabs/server - project scaffolding and code generation",
6
6
  "main": "./src/index.ts",
@@ -0,0 +1,207 @@
1
+ /**
2
+ * Docs Command
3
+ *
4
+ * Syncs documentation from the installed @donkeylabs/server package
5
+ * to the user's project. This ensures users always have access to
6
+ * the latest documentation for their installed version.
7
+ *
8
+ * Usage:
9
+ * donkeylabs docs # Sync all docs to ./docs/donkeylabs/
10
+ * donkeylabs docs --list # List available docs
11
+ * donkeylabs docs workflows # Sync specific doc
12
+ */
13
+
14
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, statSync } from "fs";
15
+ import { join, dirname } from "path";
16
+ import pc from "picocolors";
17
+
18
+ const DEFAULT_DOCS_DIR = "docs/donkeylabs";
19
+
20
+ interface DocsCommandOptions {
21
+ list?: boolean;
22
+ output?: string;
23
+ }
24
+
25
+ /**
26
+ * Find the docs directory from installed @donkeylabs/server
27
+ */
28
+ function findDocsPath(): string | null {
29
+ // Try common locations
30
+ const possiblePaths = [
31
+ // node_modules (standard install)
32
+ join(process.cwd(), "node_modules", "@donkeylabs", "server", "docs"),
33
+ // bun's .bun cache
34
+ join(process.cwd(), "node_modules", ".bun", "@donkeylabs", "server", "docs"),
35
+ // Workspace/monorepo
36
+ join(process.cwd(), "..", "server", "docs"),
37
+ join(process.cwd(), "..", "..", "packages", "server", "docs"),
38
+ ];
39
+
40
+ for (const path of possiblePaths) {
41
+ if (existsSync(path) && statSync(path).isDirectory()) {
42
+ return path;
43
+ }
44
+ }
45
+
46
+ // Try to resolve from require
47
+ try {
48
+ const serverPkgPath = require.resolve("@donkeylabs/server/package.json", {
49
+ paths: [process.cwd()],
50
+ });
51
+ const serverDir = dirname(serverPkgPath);
52
+ const docsPath = join(serverDir, "docs");
53
+ if (existsSync(docsPath)) {
54
+ return docsPath;
55
+ }
56
+ } catch {
57
+ // Package not found
58
+ }
59
+
60
+ return null;
61
+ }
62
+
63
+ /**
64
+ * Get list of available doc files
65
+ */
66
+ function getAvailableDocs(docsPath: string): string[] {
67
+ return readdirSync(docsPath)
68
+ .filter((f) => f.endsWith(".md"))
69
+ .map((f) => f.replace(".md", ""));
70
+ }
71
+
72
+ /**
73
+ * Sync a single doc file
74
+ */
75
+ function syncDoc(docsPath: string, docName: string, outputDir: string): boolean {
76
+ const sourcePath = join(docsPath, `${docName}.md`);
77
+ if (!existsSync(sourcePath)) {
78
+ return false;
79
+ }
80
+
81
+ const content = readFileSync(sourcePath, "utf-8");
82
+ const outputPath = join(outputDir, `${docName}.md`);
83
+
84
+ // Create output directory if needed
85
+ mkdirSync(dirname(outputPath), { recursive: true });
86
+
87
+ writeFileSync(outputPath, content);
88
+ return true;
89
+ }
90
+
91
+ /**
92
+ * Sync all docs
93
+ */
94
+ function syncAllDocs(docsPath: string, outputDir: string): number {
95
+ const docs = getAvailableDocs(docsPath);
96
+ let synced = 0;
97
+
98
+ for (const doc of docs) {
99
+ if (syncDoc(docsPath, doc, outputDir)) {
100
+ synced++;
101
+ }
102
+ }
103
+
104
+ return synced;
105
+ }
106
+
107
+ /**
108
+ * Get version from installed package
109
+ */
110
+ function getInstalledVersion(): string | null {
111
+ try {
112
+ const pkgPath = require.resolve("@donkeylabs/server/package.json", {
113
+ paths: [process.cwd()],
114
+ });
115
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
116
+ return pkg.version;
117
+ } catch {
118
+ return null;
119
+ }
120
+ }
121
+
122
+ export async function docsCommand(args: string[], options: DocsCommandOptions = {}): Promise<void> {
123
+ const docsPath = findDocsPath();
124
+
125
+ if (!docsPath) {
126
+ console.error(pc.red("Error: Could not find @donkeylabs/server docs."));
127
+ console.log(pc.dim("Make sure @donkeylabs/server is installed in your project."));
128
+ console.log(pc.dim("\nRun: bun add @donkeylabs/server"));
129
+ process.exit(1);
130
+ }
131
+
132
+ const version = getInstalledVersion();
133
+ const availableDocs = getAvailableDocs(docsPath);
134
+
135
+ // List mode
136
+ if (options.list || args[0] === "--list" || args[0] === "-l") {
137
+ console.log(pc.bold("\nAvailable Documentation"));
138
+ console.log(pc.dim(`Version: ${version || "unknown"}\n`));
139
+
140
+ // Group docs by category
141
+ const categories: Record<string, string[]> = {
142
+ "Core Services": ["logger", "cache", "events", "cron", "jobs", "external-jobs", "processes", "workflows", "sse", "rate-limiter", "errors"],
143
+ "API": ["router", "handlers", "middleware"],
144
+ "Server": ["lifecycle-hooks", "services", "core-services"],
145
+ "Infrastructure": ["database", "plugins", "sveltekit-adapter", "api-client"],
146
+ "Testing": ["testing"],
147
+ "Other": [],
148
+ };
149
+
150
+ // Categorize docs
151
+ const categorized = new Set<string>();
152
+ for (const [category, docs] of Object.entries(categories)) {
153
+ const matching = availableDocs.filter((d) => docs.includes(d));
154
+ if (matching.length > 0) {
155
+ console.log(pc.cyan(` ${category}:`));
156
+ for (const doc of matching) {
157
+ console.log(` ${pc.green("•")} ${doc}`);
158
+ categorized.add(doc);
159
+ }
160
+ console.log();
161
+ }
162
+ }
163
+
164
+ // Uncategorized
165
+ const uncategorized = availableDocs.filter((d) => !categorized.has(d));
166
+ if (uncategorized.length > 0) {
167
+ console.log(pc.cyan(" Other:"));
168
+ for (const doc of uncategorized) {
169
+ console.log(` ${pc.green("•")} ${doc}`);
170
+ }
171
+ console.log();
172
+ }
173
+
174
+ console.log(pc.dim(`\nUsage:`));
175
+ console.log(pc.dim(` donkeylabs docs # Sync all docs`));
176
+ console.log(pc.dim(` donkeylabs docs workflows # Sync specific doc`));
177
+ return;
178
+ }
179
+
180
+ const outputDir = options.output || DEFAULT_DOCS_DIR;
181
+ const specificDoc = args[0];
182
+
183
+ // Sync specific doc
184
+ if (specificDoc && specificDoc !== "--list" && specificDoc !== "-l") {
185
+ if (!availableDocs.includes(specificDoc)) {
186
+ console.error(pc.red(`Error: Doc "${specificDoc}" not found.`));
187
+ console.log(pc.dim(`\nAvailable docs: ${availableDocs.join(", ")}`));
188
+ console.log(pc.dim(`\nRun: donkeylabs docs --list`));
189
+ process.exit(1);
190
+ }
191
+
192
+ syncDoc(docsPath, specificDoc, outputDir);
193
+ console.log(pc.green(`✓ Synced ${specificDoc}.md to ${outputDir}/`));
194
+ return;
195
+ }
196
+
197
+ // Sync all docs
198
+ console.log(pc.bold("\nSyncing Documentation"));
199
+ console.log(pc.dim(`Version: ${version || "unknown"}`));
200
+ console.log(pc.dim(`Source: ${docsPath}`));
201
+ console.log(pc.dim(`Target: ${outputDir}/\n`));
202
+
203
+ const synced = syncAllDocs(docsPath, outputDir);
204
+
205
+ console.log(pc.green(`\n✓ Synced ${synced} documentation files to ${outputDir}/`));
206
+ console.log(pc.dim(`\nTip: Add ${outputDir}/ to your .gitignore if you don't want to commit docs.`));
207
+ }
@@ -0,0 +1,360 @@
1
+ /**
2
+ * Update Command
3
+ *
4
+ * Check for and install updates to @donkeylabs packages.
5
+ *
6
+ * Usage:
7
+ * donkeylabs update # Interactive update selection
8
+ * donkeylabs update --all # Update all packages
9
+ * donkeylabs update --check # Check for updates only (no install)
10
+ */
11
+
12
+ import { existsSync, readFileSync, writeFileSync, rmSync } from "fs";
13
+ import { join } from "path";
14
+ import { execSync } from "child_process";
15
+ import pc from "picocolors";
16
+
17
+ const DONKEYLABS_PACKAGES = [
18
+ "@donkeylabs/server",
19
+ "@donkeylabs/adapter-sveltekit",
20
+ "@donkeylabs/cli",
21
+ "@donkeylabs/mcp",
22
+ "@donkeylabs/adapter-serverless",
23
+ ];
24
+
25
+ interface PackageInfo {
26
+ name: string;
27
+ currentVersion: string | null;
28
+ latestVersion: string | null;
29
+ hasUpdate: boolean;
30
+ isDev: boolean;
31
+ }
32
+
33
+ interface UpdateOptions {
34
+ all?: boolean;
35
+ check?: boolean;
36
+ skipDocs?: boolean;
37
+ }
38
+
39
+ /**
40
+ * Get installed version from package.json
41
+ */
42
+ function getInstalledPackages(packageJsonPath: string): Map<string, { version: string; isDev: boolean }> {
43
+ const installed = new Map<string, { version: string; isDev: boolean }>();
44
+
45
+ if (!existsSync(packageJsonPath)) {
46
+ return installed;
47
+ }
48
+
49
+ const pkg = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
50
+
51
+ // Check dependencies
52
+ if (pkg.dependencies) {
53
+ for (const [name, version] of Object.entries(pkg.dependencies)) {
54
+ if (DONKEYLABS_PACKAGES.includes(name)) {
55
+ installed.set(name, { version: String(version), isDev: false });
56
+ }
57
+ }
58
+ }
59
+
60
+ // Check devDependencies
61
+ if (pkg.devDependencies) {
62
+ for (const [name, version] of Object.entries(pkg.devDependencies)) {
63
+ if (DONKEYLABS_PACKAGES.includes(name)) {
64
+ installed.set(name, { version: String(version), isDev: true });
65
+ }
66
+ }
67
+ }
68
+
69
+ return installed;
70
+ }
71
+
72
+ /**
73
+ * Get latest version from npm registry
74
+ */
75
+ async function getLatestVersion(packageName: string): Promise<string | null> {
76
+ try {
77
+ const result = execSync(`npm view ${packageName} version 2>/dev/null`, {
78
+ encoding: "utf-8",
79
+ stdio: ["pipe", "pipe", "pipe"],
80
+ });
81
+ return result.trim();
82
+ } catch {
83
+ return null;
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Parse version string (handles workspace:*, ^, ~, etc.)
89
+ */
90
+ function parseVersion(version: string): string {
91
+ if (version.startsWith("workspace:")) {
92
+ return "workspace";
93
+ }
94
+ // Remove ^, ~, >=, etc.
95
+ return version.replace(/^[\^~>=<]+/, "");
96
+ }
97
+
98
+ /**
99
+ * Compare versions (simple semver comparison)
100
+ */
101
+ function isNewerVersion(current: string, latest: string): boolean {
102
+ if (current === "workspace") return false;
103
+
104
+ const currentParts = current.split(".").map(Number);
105
+ const latestParts = latest.split(".").map(Number);
106
+
107
+ for (let i = 0; i < 3; i++) {
108
+ const c = currentParts[i] || 0;
109
+ const l = latestParts[i] || 0;
110
+ if (l > c) return true;
111
+ if (l < c) return false;
112
+ }
113
+
114
+ return false;
115
+ }
116
+
117
+ /**
118
+ * Check for updates
119
+ */
120
+ async function checkForUpdates(packageJsonPath: string): Promise<PackageInfo[]> {
121
+ const installed = getInstalledPackages(packageJsonPath);
122
+ const packages: PackageInfo[] = [];
123
+
124
+ console.log(pc.dim("\nChecking for updates...\n"));
125
+
126
+ for (const name of DONKEYLABS_PACKAGES) {
127
+ const info = installed.get(name);
128
+
129
+ if (!info) {
130
+ continue; // Not installed
131
+ }
132
+
133
+ const currentVersion = parseVersion(info.version);
134
+ const latestVersion = await getLatestVersion(name);
135
+
136
+ const hasUpdate = latestVersion
137
+ ? isNewerVersion(currentVersion, latestVersion)
138
+ : false;
139
+
140
+ packages.push({
141
+ name,
142
+ currentVersion,
143
+ latestVersion,
144
+ hasUpdate,
145
+ isDev: info.isDev,
146
+ });
147
+ }
148
+
149
+ return packages;
150
+ }
151
+
152
+ /**
153
+ * Display update status
154
+ */
155
+ function displayUpdateStatus(packages: PackageInfo[]): void {
156
+ const hasUpdates = packages.some((p) => p.hasUpdate);
157
+
158
+ console.log(pc.bold("Package Status\n"));
159
+
160
+ for (const pkg of packages) {
161
+ const statusIcon = pkg.hasUpdate
162
+ ? pc.yellow("⬆")
163
+ : pc.green("✓");
164
+
165
+ const versionInfo = pkg.hasUpdate
166
+ ? `${pc.dim(pkg.currentVersion)} → ${pc.green(pkg.latestVersion)}`
167
+ : pc.dim(pkg.currentVersion || "unknown");
168
+
169
+ const devBadge = pkg.isDev ? pc.dim(" (dev)") : "";
170
+
171
+ console.log(` ${statusIcon} ${pkg.name}${devBadge}`);
172
+ console.log(` ${versionInfo}\n`);
173
+ }
174
+
175
+ if (!hasUpdates) {
176
+ console.log(pc.green("All packages are up to date!"));
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Interactive package selection using simple prompt
182
+ */
183
+ async function selectPackagesToUpdate(packages: PackageInfo[]): Promise<PackageInfo[]> {
184
+ const updatable = packages.filter((p) => p.hasUpdate);
185
+
186
+ if (updatable.length === 0) {
187
+ return [];
188
+ }
189
+
190
+ console.log(pc.bold("\nSelect packages to update:\n"));
191
+
192
+ // Display options
193
+ console.log(` ${pc.cyan("0)")} Update all packages`);
194
+ updatable.forEach((pkg, i) => {
195
+ console.log(
196
+ ` ${pc.cyan(`${i + 1})`)} ${pkg.name} ${pc.dim(`${pkg.currentVersion} → ${pkg.latestVersion}`)}`
197
+ );
198
+ });
199
+ console.log(` ${pc.cyan("s)")} Skip update\n`);
200
+
201
+ // Read input
202
+ process.stdout.write(pc.bold("Enter selection (0-" + updatable.length + ", or s): "));
203
+
204
+ const input = await new Promise<string>((resolve) => {
205
+ let data = "";
206
+ process.stdin.setEncoding("utf8");
207
+ process.stdin.once("data", (chunk) => {
208
+ data = chunk.toString().trim();
209
+ resolve(data);
210
+ });
211
+ process.stdin.resume();
212
+ });
213
+
214
+ process.stdin.pause();
215
+
216
+ if (input.toLowerCase() === "s") {
217
+ return [];
218
+ }
219
+
220
+ if (input === "0") {
221
+ return updatable;
222
+ }
223
+
224
+ const index = parseInt(input, 10);
225
+ if (index >= 1 && index <= updatable.length) {
226
+ return [updatable[index - 1]];
227
+ }
228
+
229
+ console.log(pc.yellow("\nInvalid selection, skipping update."));
230
+ return [];
231
+ }
232
+
233
+ /**
234
+ * Update package.json with new versions
235
+ */
236
+ function updatePackageJson(
237
+ packageJsonPath: string,
238
+ packagesToUpdate: PackageInfo[]
239
+ ): void {
240
+ const pkg = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
241
+
242
+ for (const pkgInfo of packagesToUpdate) {
243
+ const versionString = `^${pkgInfo.latestVersion}`;
244
+
245
+ if (pkgInfo.isDev && pkg.devDependencies?.[pkgInfo.name]) {
246
+ pkg.devDependencies[pkgInfo.name] = versionString;
247
+ } else if (pkg.dependencies?.[pkgInfo.name]) {
248
+ pkg.dependencies[pkgInfo.name] = versionString;
249
+ }
250
+ }
251
+
252
+ writeFileSync(packageJsonPath, JSON.stringify(pkg, null, 2) + "\n");
253
+ }
254
+
255
+ /**
256
+ * Run bun install
257
+ */
258
+ function runInstall(): void {
259
+ console.log(pc.dim("\nRemoving node_modules..."));
260
+ const nodeModulesPath = join(process.cwd(), "node_modules");
261
+ if (existsSync(nodeModulesPath)) {
262
+ rmSync(nodeModulesPath, { recursive: true, force: true });
263
+ }
264
+
265
+ // Also remove bun.lock for clean install
266
+ const bunLockPath = join(process.cwd(), "bun.lock");
267
+ if (existsSync(bunLockPath)) {
268
+ rmSync(bunLockPath);
269
+ }
270
+
271
+ console.log(pc.dim("Running bun install...\n"));
272
+ execSync("bun install", { stdio: "inherit" });
273
+ }
274
+
275
+ /**
276
+ * Sync documentation
277
+ */
278
+ async function syncDocs(): Promise<void> {
279
+ console.log(pc.dim("\nSyncing documentation..."));
280
+
281
+ try {
282
+ const { docsCommand } = await import("./docs");
283
+ await docsCommand([], {});
284
+ } catch (error) {
285
+ console.log(pc.yellow("Could not sync docs (this is optional)"));
286
+ }
287
+ }
288
+
289
+ export async function updateCommand(
290
+ args: string[],
291
+ options: UpdateOptions = {}
292
+ ): Promise<void> {
293
+ const packageJsonPath = join(process.cwd(), "package.json");
294
+
295
+ if (!existsSync(packageJsonPath)) {
296
+ console.error(pc.red("Error: No package.json found in current directory."));
297
+ process.exit(1);
298
+ }
299
+
300
+ // Check for updates
301
+ const packages = await checkForUpdates(packageJsonPath);
302
+
303
+ if (packages.length === 0) {
304
+ console.log(pc.yellow("No @donkeylabs packages found in this project."));
305
+ console.log(pc.dim("\nInstall with: bun add @donkeylabs/server"));
306
+ return;
307
+ }
308
+
309
+ // Display status
310
+ displayUpdateStatus(packages);
311
+
312
+ // Check-only mode
313
+ if (options.check || args.includes("--check") || args.includes("-c")) {
314
+ return;
315
+ }
316
+
317
+ const updatable = packages.filter((p) => p.hasUpdate);
318
+
319
+ if (updatable.length === 0) {
320
+ return;
321
+ }
322
+
323
+ // Select packages to update
324
+ let packagesToUpdate: PackageInfo[];
325
+
326
+ if (options.all || args.includes("--all") || args.includes("-a")) {
327
+ packagesToUpdate = updatable;
328
+ } else {
329
+ packagesToUpdate = await selectPackagesToUpdate(packages);
330
+ }
331
+
332
+ if (packagesToUpdate.length === 0) {
333
+ console.log(pc.dim("\nNo packages selected for update."));
334
+ return;
335
+ }
336
+
337
+ // Confirm
338
+ console.log(pc.bold("\nUpdating packages:"));
339
+ for (const pkg of packagesToUpdate) {
340
+ console.log(` • ${pkg.name} → ${pc.green(pkg.latestVersion)}`);
341
+ }
342
+
343
+ // Update package.json
344
+ console.log(pc.dim("\nUpdating package.json..."));
345
+ updatePackageJson(packageJsonPath, packagesToUpdate);
346
+
347
+ // Run install
348
+ runInstall();
349
+
350
+ // Sync docs
351
+ if (!options.skipDocs && !args.includes("--skip-docs")) {
352
+ await syncDocs();
353
+ }
354
+
355
+ console.log(pc.green("\n✓ Update complete!"));
356
+ console.log(pc.dim("\nUpdated packages:"));
357
+ for (const pkg of packagesToUpdate) {
358
+ console.log(` ${pc.green("•")} ${pkg.name}@${pkg.latestVersion}`);
359
+ }
360
+ }
package/src/index.ts CHANGED
@@ -18,6 +18,11 @@ const { positionals, values } = parseArgs({
18
18
  version: { type: "boolean", short: "v" },
19
19
  type: { type: "string", short: "t" },
20
20
  local: { type: "boolean", short: "l" },
21
+ list: { type: "boolean" },
22
+ output: { type: "string", short: "o" },
23
+ all: { type: "boolean", short: "a" },
24
+ check: { type: "boolean", short: "c" },
25
+ "skip-docs": { type: "boolean" },
21
26
  },
22
27
  allowPositionals: true,
23
28
  });
@@ -37,6 +42,8 @@ ${pc.bold("Commands:")}
37
42
  ${pc.cyan("add")} Add optional plugins (images, auth, etc.)
38
43
  ${pc.cyan("generate")} Generate types (registry, context, client)
39
44
  ${pc.cyan("plugin")} Plugin management
45
+ ${pc.cyan("update")} Check and install package updates
46
+ ${pc.cyan("docs")} Sync documentation from installed package
40
47
  ${pc.cyan("deploy")} <platform> Deploy (vercel, cloudflare, aws, vps)
41
48
  ${pc.cyan("deploy history")} Show deployment history
42
49
  ${pc.cyan("deploy rollback")} Rollback to version
@@ -57,6 +64,11 @@ ${pc.bold("Options:")}
57
64
  donkeylabs init --type sveltekit # SvelteKit + adapter project
58
65
  donkeylabs generate
59
66
  donkeylabs plugin create myPlugin
67
+ donkeylabs update # Interactive package update
68
+ donkeylabs update --check # Check for updates only
69
+ donkeylabs update --all # Update all packages
70
+ donkeylabs docs # Sync all docs to ./docs/donkeylabs/
71
+ donkeylabs docs --list # List available docs
60
72
  donkeylabs deploy vercel # Deploy to Vercel
61
73
  donkeylabs config # Interactive configuration
62
74
  donkeylabs config set DATABASE_URL postgresql://...
@@ -113,6 +125,20 @@ async function main() {
113
125
  await mcpCommand(positionals.slice(1));
114
126
  break;
115
127
 
128
+ case "docs":
129
+ const { docsCommand } = await import("./commands/docs");
130
+ await docsCommand(positionals.slice(1), { list: values.list, output: values.output });
131
+ break;
132
+
133
+ case "update":
134
+ const { updateCommand } = await import("./commands/update");
135
+ await updateCommand(positionals.slice(1), {
136
+ all: values.all,
137
+ check: values.check,
138
+ skipDocs: values["skip-docs"],
139
+ });
140
+ break;
141
+
116
142
  case "deploy":
117
143
  const subcommand = positionals[1];
118
144
  if (subcommand === "history") {