@donkeylabs/cli 2.0.17 → 2.0.19

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.17",
3
+ "version": "2.0.19",
4
4
  "type": "module",
5
5
  "description": "CLI for @donkeylabs/server - project scaffolding and code generation",
6
6
  "main": "./src/index.ts",
@@ -23,35 +23,30 @@ interface DocsCommandOptions {
23
23
  }
24
24
 
25
25
  /**
26
- * Find the docs directory from installed @donkeylabs/server
26
+ * Find the server package directory from installed @donkeylabs/server
27
27
  */
28
- function findDocsPath(): string | null {
29
- // Try common locations
28
+ function findServerPkgDir(): string | null {
30
29
  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"),
30
+ join(process.cwd(), "node_modules", "@donkeylabs", "server"),
31
+ join(process.cwd(), "node_modules", ".bun", "@donkeylabs", "server"),
32
+ join(process.cwd(), "..", "server"),
33
+ join(process.cwd(), "..", "..", "packages", "server"),
38
34
  ];
39
35
 
40
36
  for (const path of possiblePaths) {
41
- if (existsSync(path) && statSync(path).isDirectory()) {
37
+ const docsDir = join(path, "docs");
38
+ if (existsSync(docsDir) && statSync(docsDir).isDirectory()) {
42
39
  return path;
43
40
  }
44
41
  }
45
42
 
46
- // Try to resolve from require
47
43
  try {
48
44
  const serverPkgPath = require.resolve("@donkeylabs/server/package.json", {
49
45
  paths: [process.cwd()],
50
46
  });
51
47
  const serverDir = dirname(serverPkgPath);
52
- const docsPath = join(serverDir, "docs");
53
- if (existsSync(docsPath)) {
54
- return docsPath;
48
+ if (existsSync(join(serverDir, "docs"))) {
49
+ return serverDir;
55
50
  }
56
51
  } catch {
57
52
  // Package not found
@@ -60,6 +55,30 @@ function findDocsPath(): string | null {
60
55
  return null;
61
56
  }
62
57
 
58
+ /**
59
+ * Find the docs directory from installed @donkeylabs/server
60
+ */
61
+ function findDocsPath(): string | null {
62
+ const serverDir = findServerPkgDir();
63
+ if (!serverDir) return null;
64
+ return join(serverDir, "docs");
65
+ }
66
+
67
+ /**
68
+ * Sync agents.md from the server package root to the project root
69
+ */
70
+ function syncAgentsMd(): void {
71
+ const serverDir = findServerPkgDir();
72
+ if (!serverDir) return;
73
+
74
+ const agentsMdSrc = join(serverDir, "agents.md");
75
+ if (!existsSync(agentsMdSrc)) return;
76
+
77
+ const content = readFileSync(agentsMdSrc, "utf-8");
78
+ writeFileSync(join(process.cwd(), "agents.md"), content);
79
+ console.log(pc.green("✓ Synced agents.md"));
80
+ }
81
+
63
82
  /**
64
83
  * Get list of available doc files
65
84
  */
@@ -201,6 +220,7 @@ export async function docsCommand(args: string[], options: DocsCommandOptions =
201
220
  console.log(pc.dim(`Target: ${outputDir}/\n`));
202
221
 
203
222
  const synced = syncAllDocs(docsPath, outputDir);
223
+ syncAgentsMd();
204
224
 
205
225
  console.log(pc.green(`\n✓ Synced ${synced} documentation files to ${outputDir}/`));
206
226
  console.log(pc.dim(`\nTip: Add ${outputDir}/ to your .gitignore if you don't want to commit docs.`));
@@ -374,6 +374,7 @@ function createPackageJson(projectDir: string, options: InitOptions) {
374
374
  "prepare": "bun --bun svelte-kit sync && bun run gen:types || echo ''",
375
375
  "check": "bun --bun svelte-kit sync && bun --bun svelte-check --tsconfig ./tsconfig.json",
376
376
  "gen:types": "donkeylabs generate",
377
+ "update": "donkeylabs update",
377
378
  "cli": "donkeylabs",
378
379
  "test": "bun test",
379
380
  ...(options.deployment === "docker" && {
@@ -386,6 +387,7 @@ function createPackageJson(projectDir: string, options: InitOptions) {
386
387
  "build": "bun build src/server/index.ts --outdir=dist",
387
388
  "start": "bun run dist/index.js",
388
389
  "gen:types": "bunx donkeylabs generate",
390
+ "update": "bunx donkeylabs update",
389
391
  "test": "bun test",
390
392
  "lint": "bun --bun tsc --noEmit",
391
393
  ...(options.deployment === "docker" && {
@@ -167,7 +167,7 @@ export async function initCommand(args: string[]) {
167
167
  } else {
168
168
  console.log(pc.green("\n✓ Dependencies installed\n"));
169
169
 
170
- // Copy CLAUDE.md and docs/ from @donkeylabs/server to project root
170
+ // Copy agents.md and docs/ from @donkeylabs/server to project root
171
171
  await copyDocsFromServer(targetDir);
172
172
  }
173
173
 
@@ -324,7 +324,7 @@ async function copyDirectory(src: string, dest: string): Promise<void> {
324
324
  }
325
325
 
326
326
  /**
327
- * Copy CLAUDE.md and docs/ from @donkeylabs/server to project root
327
+ * Copy agents.md and docs/ from @donkeylabs/server to project root
328
328
  * for AI-assisted development
329
329
  */
330
330
  async function copyDocsFromServer(targetDir: string): Promise<void> {
@@ -334,19 +334,19 @@ async function copyDocsFromServer(targetDir: string): Promise<void> {
334
334
  return; // Server package not installed
335
335
  }
336
336
 
337
- // Copy CLAUDE.md
338
- const claudeMdSrc = join(serverPkgPath, "CLAUDE.md");
339
- if (existsSync(claudeMdSrc)) {
340
- const claudeMdDest = join(targetDir, "CLAUDE.md");
341
- await copyFile(claudeMdSrc, claudeMdDest);
342
- console.log(pc.green(" Created:"), "CLAUDE.md (AI instructions)");
337
+ // Copy agents.md
338
+ const agentsMdSrc = join(serverPkgPath, "agents.md");
339
+ if (existsSync(agentsMdSrc)) {
340
+ const agentsMdDest = join(targetDir, "agents.md");
341
+ await copyFile(agentsMdSrc, agentsMdDest);
342
+ console.log(pc.green(" Created:"), "agents.md (AI instructions)");
343
343
  }
344
344
 
345
345
  // Copy docs/ directory
346
346
  const docsSrc = join(serverPkgPath, "docs");
347
347
  if (existsSync(docsSrc)) {
348
- const docsDest = join(targetDir, "docs");
348
+ const docsDest = join(targetDir, "docs", "donkeylabs");
349
349
  await copyDirectory(docsSrc, docsDest);
350
- console.log(pc.green(" Created:"), "docs/ (detailed documentation)");
350
+ console.log(pc.green(" Created:"), "docs/donkeylabs/ (detailed documentation)");
351
351
  }
352
352
  }
@@ -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
@@ -20,6 +20,9 @@ const { positionals, values } = parseArgs({
20
20
  local: { type: "boolean", short: "l" },
21
21
  list: { type: "boolean" },
22
22
  output: { type: "string", short: "o" },
23
+ all: { type: "boolean", short: "a" },
24
+ check: { type: "boolean", short: "c" },
25
+ "skip-docs": { type: "boolean" },
23
26
  },
24
27
  allowPositionals: true,
25
28
  });
@@ -39,6 +42,7 @@ ${pc.bold("Commands:")}
39
42
  ${pc.cyan("add")} Add optional plugins (images, auth, etc.)
40
43
  ${pc.cyan("generate")} Generate types (registry, context, client)
41
44
  ${pc.cyan("plugin")} Plugin management
45
+ ${pc.cyan("update")} Check and install package updates
42
46
  ${pc.cyan("docs")} Sync documentation from installed package
43
47
  ${pc.cyan("deploy")} <platform> Deploy (vercel, cloudflare, aws, vps)
44
48
  ${pc.cyan("deploy history")} Show deployment history
@@ -60,9 +64,11 @@ ${pc.bold("Options:")}
60
64
  donkeylabs init --type sveltekit # SvelteKit + adapter project
61
65
  donkeylabs generate
62
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
63
70
  donkeylabs docs # Sync all docs to ./docs/donkeylabs/
64
71
  donkeylabs docs --list # List available docs
65
- donkeylabs docs workflows # Sync specific doc
66
72
  donkeylabs deploy vercel # Deploy to Vercel
67
73
  donkeylabs config # Interactive configuration
68
74
  donkeylabs config set DATABASE_URL postgresql://...
@@ -124,6 +130,15 @@ async function main() {
124
130
  await docsCommand(positionals.slice(1), { list: values.list, output: values.output });
125
131
  break;
126
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
+
127
142
  case "deploy":
128
143
  const subcommand = positionals[1];
129
144
  if (subcommand === "history") {