@donkeylabs/cli 1.1.1 → 1.1.3
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/add.ts +270 -0
- package/src/commands/generate.ts +4 -0
- package/src/index.ts +6 -0
package/package.json
CHANGED
|
@@ -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
|
+
}
|
package/src/commands/generate.ts
CHANGED
|
@@ -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");
|