@donkeylabs/cli 1.0.1 → 1.1.2

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": "1.0.1",
3
+ "version": "1.1.2",
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,270 @@
1
+ /**
2
+ * Add Command
3
+ *
4
+ * Add optional plugins to a @donkeylabs/server project
5
+ */
6
+
7
+ import { mkdir, writeFile, readFile, readdir, copyFile, stat } from "node:fs/promises";
8
+ import { join, resolve, dirname, relative } from "node:path";
9
+ import { existsSync } from "node:fs";
10
+ import { fileURLToPath } from "node:url";
11
+ import { spawn } from "node:child_process";
12
+ import pc from "picocolors";
13
+ import prompts from "prompts";
14
+
15
+ const __filename = fileURLToPath(import.meta.url);
16
+ const __dirname = dirname(__filename);
17
+
18
+ interface PluginManifest {
19
+ name: string;
20
+ version: string;
21
+ description: string;
22
+ dependencies?: Record<string, string>;
23
+ copy: Array<{ from: string; to: string }>;
24
+ register: {
25
+ plugin: string;
26
+ importPath: string;
27
+ configRequired?: boolean;
28
+ };
29
+ envVars?: string[];
30
+ instructions?: string[];
31
+ }
32
+
33
+ // Files/directories to skip when copying
34
+ const SKIP_PATTERNS = [
35
+ "node_modules",
36
+ ".git",
37
+ ".DS_Store",
38
+ "manifest.json",
39
+ ];
40
+
41
+ export async function addCommand(args: string[]) {
42
+ const pluginName = args[0];
43
+
44
+ if (!pluginName) {
45
+ // List available plugins
46
+ const pluginsDir = resolve(__dirname, "../../plugins");
47
+
48
+ if (!existsSync(pluginsDir)) {
49
+ console.log(pc.yellow("No optional plugins available."));
50
+ return;
51
+ }
52
+
53
+ const plugins = await readdir(pluginsDir);
54
+ const availablePlugins: { name: string; description: string }[] = [];
55
+
56
+ for (const plugin of plugins) {
57
+ const manifestPath = join(pluginsDir, plugin, "manifest.json");
58
+ if (existsSync(manifestPath)) {
59
+ try {
60
+ const manifest: PluginManifest = JSON.parse(
61
+ await readFile(manifestPath, "utf-8")
62
+ );
63
+ availablePlugins.push({
64
+ name: manifest.name,
65
+ description: manifest.description,
66
+ });
67
+ } catch {
68
+ // Skip invalid manifests
69
+ }
70
+ }
71
+ }
72
+
73
+ if (availablePlugins.length === 0) {
74
+ console.log(pc.yellow("No optional plugins available."));
75
+ return;
76
+ }
77
+
78
+ console.log(`
79
+ ${pc.bold("Available Plugins")}
80
+
81
+ ${availablePlugins
82
+ .map((p) => ` ${pc.cyan(p.name.padEnd(15))} ${p.description}`)
83
+ .join("\n")}
84
+
85
+ ${pc.bold("Usage:")}
86
+ donkeylabs add <plugin-name>
87
+
88
+ ${pc.bold("Example:")}
89
+ donkeylabs add images
90
+ `);
91
+ return;
92
+ }
93
+
94
+ // Find the plugin
95
+ const pluginsDir = resolve(__dirname, "../../plugins");
96
+ const pluginDir = join(pluginsDir, pluginName);
97
+
98
+ if (!existsSync(pluginDir)) {
99
+ console.error(pc.red(`Plugin not found: ${pluginName}`));
100
+ console.log(`Run ${pc.cyan("donkeylabs add")} to see available plugins.`);
101
+ process.exit(1);
102
+ }
103
+
104
+ // Read manifest
105
+ const manifestPath = join(pluginDir, "manifest.json");
106
+ if (!existsSync(manifestPath)) {
107
+ console.error(pc.red(`Invalid plugin: missing manifest.json`));
108
+ process.exit(1);
109
+ }
110
+
111
+ const manifest: PluginManifest = JSON.parse(
112
+ await readFile(manifestPath, "utf-8")
113
+ );
114
+
115
+ console.log(`
116
+ ${pc.green("✓")} Found plugin: ${pc.bold(manifest.name)} ${pc.dim(`(${manifest.description})`)}
117
+ `);
118
+
119
+ // Check if project has donkeylabs config
120
+ const projectRoot = process.cwd();
121
+ const configExists =
122
+ existsSync(join(projectRoot, "donkeylabs.config.ts")) ||
123
+ existsSync(join(projectRoot, "donkeylabs.config.js"));
124
+
125
+ if (!configExists) {
126
+ console.error(pc.red("Not a @donkeylabs/server project."));
127
+ console.log(
128
+ `Run ${pc.cyan("donkeylabs init")} first to create a new project.`
129
+ );
130
+ process.exit(1);
131
+ }
132
+
133
+ // Show what will be added
134
+ console.log(pc.bold("Adding to your project:"));
135
+ for (const copy of manifest.copy) {
136
+ console.log(` ${pc.cyan("•")} ${copy.to}`);
137
+ }
138
+
139
+ // Confirm
140
+ const { confirm } = await prompts({
141
+ type: "confirm",
142
+ name: "confirm",
143
+ message: "Proceed?",
144
+ initial: true,
145
+ });
146
+
147
+ if (!confirm) {
148
+ console.log(pc.yellow("Cancelled."));
149
+ return;
150
+ }
151
+
152
+ console.log();
153
+
154
+ // Copy files
155
+ for (const copy of manifest.copy) {
156
+ const srcPath = join(pluginDir, copy.from);
157
+ const destPath = join(projectRoot, copy.to);
158
+
159
+ if (!existsSync(srcPath)) {
160
+ console.log(pc.yellow(` Skipping ${copy.from} (not found)`));
161
+ continue;
162
+ }
163
+
164
+ const srcStat = await stat(srcPath);
165
+
166
+ if (srcStat.isDirectory()) {
167
+ await copyDirectory(srcPath, destPath);
168
+ console.log(pc.green(" Created:"), copy.to + "/");
169
+ } else {
170
+ await mkdir(dirname(destPath), { recursive: true });
171
+ await copyFile(srcPath, destPath);
172
+ console.log(pc.green(" Created:"), copy.to);
173
+ }
174
+ }
175
+
176
+ // Install dependencies
177
+ if (manifest.dependencies && Object.keys(manifest.dependencies).length > 0) {
178
+ console.log(`
179
+ ${pc.bold("Installing dependencies:")}`);
180
+
181
+ const deps = Object.entries(manifest.dependencies)
182
+ .map(([name, version]) => `${name}@${version}`)
183
+ .join(" ");
184
+
185
+ console.log(` ${pc.cyan("•")} ${deps}`);
186
+
187
+ const success = await runCommand("bun", ["add", ...deps.split(" ")], projectRoot);
188
+
189
+ if (success) {
190
+ console.log(pc.green("\n✓ Dependencies installed"));
191
+ } else {
192
+ console.log(pc.yellow("\n⚠ Failed to install dependencies. Run manually:"));
193
+ console.log(` bun add ${deps}`);
194
+ }
195
+ }
196
+
197
+ // Show env vars needed
198
+ if (manifest.envVars && manifest.envVars.length > 0) {
199
+ console.log(`
200
+ ${pc.bold("Environment variables needed:")}`);
201
+ for (const envVar of manifest.envVars) {
202
+ console.log(` ${envVar}=`);
203
+ }
204
+ }
205
+
206
+ // Show registration instructions
207
+ if (manifest.instructions && manifest.instructions.length > 0) {
208
+ console.log(`
209
+ ${pc.bold("Setup instructions:")}
210
+ ${pc.dim("─".repeat(50))}`);
211
+ for (const line of manifest.instructions) {
212
+ console.log(line ? ` ${line}` : "");
213
+ }
214
+ console.log(pc.dim("─".repeat(50)));
215
+ }
216
+
217
+ console.log(`
218
+ ${pc.green("✓")} Plugin added! Run ${pc.cyan("donkeylabs generate")} to update types.
219
+ `);
220
+ }
221
+
222
+ /**
223
+ * Recursively copy a directory
224
+ */
225
+ async function copyDirectory(src: string, dest: string): Promise<void> {
226
+ await mkdir(dest, { recursive: true });
227
+
228
+ const entries = await readdir(src, { withFileTypes: true });
229
+
230
+ for (const entry of entries) {
231
+ const srcPath = join(src, entry.name);
232
+
233
+ // Skip certain files/directories
234
+ if (SKIP_PATTERNS.includes(entry.name)) {
235
+ continue;
236
+ }
237
+
238
+ const destPath = join(dest, entry.name);
239
+
240
+ if (entry.isDirectory()) {
241
+ await copyDirectory(srcPath, destPath);
242
+ } else {
243
+ await copyFile(srcPath, destPath);
244
+ }
245
+ }
246
+ }
247
+
248
+ /**
249
+ * Run a command and return success status
250
+ */
251
+ async function runCommand(
252
+ cmd: string,
253
+ args: string[],
254
+ cwd: string
255
+ ): Promise<boolean> {
256
+ return new Promise((resolve) => {
257
+ const child = spawn(cmd, args, {
258
+ stdio: "inherit",
259
+ cwd,
260
+ });
261
+
262
+ child.on("close", (code) => {
263
+ resolve(code === 0);
264
+ });
265
+
266
+ child.on("error", () => {
267
+ resolve(false);
268
+ });
269
+ });
270
+ }
@@ -95,6 +95,8 @@ interface RouteInfo {
95
95
  handler: "typed" | "raw" | string;
96
96
  inputSource?: string;
97
97
  outputSource?: string;
98
+ /** SSE event schemas (for sse handler) */
99
+ eventsSource?: Record<string, string>;
98
100
  }
99
101
 
100
102
  /**
@@ -383,6 +385,8 @@ async function extractRoutesFromServer(entryPath: string): Promise<RouteInfo[]>
383
385
  // Server outputs TypeScript strings directly now
384
386
  inputSource: r.inputType,
385
387
  outputSource: r.outputType,
388
+ // SSE event schemas
389
+ eventsSource: r.eventsType,
386
390
  };
387
391
  });
388
392
  resolve(routes);
package/src/index.ts CHANGED
@@ -33,6 +33,7 @@ ${pc.bold("Usage:")}
33
33
 
34
34
  ${pc.bold("Commands:")}
35
35
  ${pc.cyan("init")} Initialize a new project
36
+ ${pc.cyan("add")} Add optional plugins (images, auth, etc.)
36
37
  ${pc.cyan("generate")} Generate types (registry, context, client)
37
38
  ${pc.cyan("plugin")} Plugin management
38
39
  ${pc.cyan("mcp")} Setup MCP server for AI-assisted development
@@ -85,6 +86,11 @@ async function main() {
85
86
  await initCommand(initArgs);
86
87
  break;
87
88
 
89
+ case "add":
90
+ const { addCommand } = await import("./commands/add");
91
+ await addCommand(positionals.slice(1));
92
+ break;
93
+
88
94
  case "generate":
89
95
  case "gen":
90
96
  const { generateCommand } = await import("./commands/generate");