@donkeylabs/cli 0.1.0

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.
Files changed (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +141 -0
  3. package/package.json +51 -0
  4. package/src/commands/generate.ts +585 -0
  5. package/src/commands/init.ts +201 -0
  6. package/src/commands/interactive.ts +223 -0
  7. package/src/commands/plugin.ts +205 -0
  8. package/src/index.ts +108 -0
  9. package/templates/starter/.env.example +3 -0
  10. package/templates/starter/.gitignore.template +4 -0
  11. package/templates/starter/CLAUDE.md +144 -0
  12. package/templates/starter/donkeylabs.config.ts +6 -0
  13. package/templates/starter/package.json +21 -0
  14. package/templates/starter/src/client.test.ts +7 -0
  15. package/templates/starter/src/db.ts +9 -0
  16. package/templates/starter/src/index.ts +48 -0
  17. package/templates/starter/src/plugins/stats/index.ts +105 -0
  18. package/templates/starter/src/routes/health/index.ts +5 -0
  19. package/templates/starter/src/routes/health/ping/index.ts +13 -0
  20. package/templates/starter/src/routes/health/ping/models/model.ts +23 -0
  21. package/templates/starter/src/routes/health/ping/schema.ts +14 -0
  22. package/templates/starter/src/routes/health/ping/tests/integ.test.ts +20 -0
  23. package/templates/starter/src/routes/health/ping/tests/unit.test.ts +21 -0
  24. package/templates/starter/src/test-ctx.ts +24 -0
  25. package/templates/starter/tsconfig.json +27 -0
  26. package/templates/sveltekit-app/.env.example +3 -0
  27. package/templates/sveltekit-app/README.md +103 -0
  28. package/templates/sveltekit-app/donkeylabs.config.ts +10 -0
  29. package/templates/sveltekit-app/package.json +36 -0
  30. package/templates/sveltekit-app/src/app.css +40 -0
  31. package/templates/sveltekit-app/src/app.html +12 -0
  32. package/templates/sveltekit-app/src/hooks.server.ts +4 -0
  33. package/templates/sveltekit-app/src/lib/api.ts +134 -0
  34. package/templates/sveltekit-app/src/lib/components/ui/badge/badge.svelte +30 -0
  35. package/templates/sveltekit-app/src/lib/components/ui/badge/index.ts +3 -0
  36. package/templates/sveltekit-app/src/lib/components/ui/button/button.svelte +48 -0
  37. package/templates/sveltekit-app/src/lib/components/ui/button/index.ts +9 -0
  38. package/templates/sveltekit-app/src/lib/components/ui/card/card-content.svelte +18 -0
  39. package/templates/sveltekit-app/src/lib/components/ui/card/card-description.svelte +18 -0
  40. package/templates/sveltekit-app/src/lib/components/ui/card/card-footer.svelte +18 -0
  41. package/templates/sveltekit-app/src/lib/components/ui/card/card-header.svelte +18 -0
  42. package/templates/sveltekit-app/src/lib/components/ui/card/card-title.svelte +18 -0
  43. package/templates/sveltekit-app/src/lib/components/ui/card/card.svelte +21 -0
  44. package/templates/sveltekit-app/src/lib/components/ui/card/index.ts +21 -0
  45. package/templates/sveltekit-app/src/lib/components/ui/index.ts +4 -0
  46. package/templates/sveltekit-app/src/lib/components/ui/input/index.ts +2 -0
  47. package/templates/sveltekit-app/src/lib/components/ui/input/input.svelte +20 -0
  48. package/templates/sveltekit-app/src/lib/utils/index.ts +6 -0
  49. package/templates/sveltekit-app/src/routes/+layout.svelte +8 -0
  50. package/templates/sveltekit-app/src/routes/+page.server.ts +25 -0
  51. package/templates/sveltekit-app/src/routes/+page.svelte +401 -0
  52. package/templates/sveltekit-app/src/server/index.ts +263 -0
  53. package/templates/sveltekit-app/static/robots.txt +3 -0
  54. package/templates/sveltekit-app/svelte.config.js +18 -0
  55. package/templates/sveltekit-app/tsconfig.json +25 -0
  56. package/templates/sveltekit-app/vite.config.ts +7 -0
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Init Command
3
+ *
4
+ * Initialize a new @donkeylabs/server project by copying from templates
5
+ */
6
+
7
+ import { mkdir, writeFile, readFile, readdir, copyFile, stat } from "node:fs/promises";
8
+ import { join, resolve, dirname, basename } from "node:path";
9
+ import { existsSync } from "node:fs";
10
+ import { fileURLToPath } from "node:url";
11
+ import pc from "picocolors";
12
+ import prompts from "prompts";
13
+
14
+ const __filename = fileURLToPath(import.meta.url);
15
+ const __dirname = dirname(__filename);
16
+
17
+ type ProjectType = "server" | "sveltekit";
18
+
19
+ // Files/directories to skip when copying templates
20
+ const SKIP_PATTERNS = [
21
+ "node_modules",
22
+ ".git",
23
+ ".svelte-kit",
24
+ "build",
25
+ "dist",
26
+ ".DS_Store",
27
+ "bun.lock",
28
+ "package-lock.json",
29
+ "yarn.lock",
30
+ "pnpm-lock.yaml",
31
+ ];
32
+
33
+ // Files that need to be renamed (template name -> actual name)
34
+ const RENAME_MAP: Record<string, string> = {
35
+ ".gitignore.template": ".gitignore",
36
+ ".env.example": ".env",
37
+ };
38
+
39
+ export async function initCommand(args: string[]) {
40
+ // Parse --type flag if provided
41
+ let projectDir = ".";
42
+ let typeArg: string | null = null;
43
+
44
+ for (let i = 0; i < args.length; i++) {
45
+ if (args[i] === "--type" && args[i + 1]) {
46
+ typeArg = args[i + 1];
47
+ i++; // skip next arg
48
+ } else if (!args[i].startsWith("-")) {
49
+ projectDir = args[i];
50
+ }
51
+ }
52
+
53
+ const targetDir = resolve(process.cwd(), projectDir);
54
+
55
+ console.log(pc.bold("\nInitializing @donkeylabs/server project...\n"));
56
+
57
+ let projectType: ProjectType;
58
+
59
+ if (typeArg === "server" || typeArg === "sveltekit") {
60
+ projectType = typeArg;
61
+ console.log(pc.cyan(`Project type: ${projectType === "server" ? "Server Only" : "SvelteKit + Adapter"}\n`));
62
+ } else {
63
+ // Prompt for project type
64
+ const response = await prompts({
65
+ type: "select",
66
+ name: "projectType",
67
+ message: "Select project type:",
68
+ choices: [
69
+ {
70
+ title: "Server Only",
71
+ description: "Standalone API server with @donkeylabs/server",
72
+ value: "server",
73
+ },
74
+ {
75
+ title: "SvelteKit + Adapter",
76
+ description: "Full-stack app with SvelteKit and @donkeylabs/adapter-sveltekit",
77
+ value: "sveltekit",
78
+ },
79
+ ],
80
+ });
81
+
82
+ if (!response.projectType) {
83
+ console.log(pc.yellow("Cancelled."));
84
+ return;
85
+ }
86
+ projectType = response.projectType;
87
+ }
88
+
89
+ // Check if directory exists and has files
90
+ if (existsSync(targetDir)) {
91
+ const files = await readdir(targetDir);
92
+ const hasConflicts = files.some(
93
+ (f) => f === "src" || f === "donkeylabs.config.ts" || f === "svelte.config.js" || f === "package.json"
94
+ );
95
+
96
+ if (hasConflicts) {
97
+ const { overwrite } = await prompts({
98
+ type: "confirm",
99
+ name: "overwrite",
100
+ message: "Directory contains existing files. Overwrite?",
101
+ initial: false,
102
+ });
103
+
104
+ if (!overwrite) {
105
+ console.log(pc.yellow("Cancelled."));
106
+ return;
107
+ }
108
+ }
109
+ }
110
+
111
+ // Determine template directory
112
+ const templateName = projectType === "server" ? "starter" : "sveltekit-app";
113
+ const templateDir = resolve(__dirname, "../../templates", templateName);
114
+
115
+ if (!existsSync(templateDir)) {
116
+ console.error(pc.red(`Template not found: ${templateDir}`));
117
+ console.error(pc.dim("Make sure @donkeylabs/cli is installed correctly."));
118
+ process.exit(1);
119
+ }
120
+
121
+ // Copy template to target directory
122
+ await copyDirectory(templateDir, targetDir);
123
+
124
+ // Update package.json with project name
125
+ const pkgPath = join(targetDir, "package.json");
126
+ if (existsSync(pkgPath)) {
127
+ const pkg = JSON.parse(await readFile(pkgPath, "utf-8"));
128
+ const projectName = basename(targetDir) || "my-app";
129
+ pkg.name = projectName;
130
+ await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
131
+ }
132
+
133
+ // Print success message
134
+ if (projectType === "server") {
135
+ console.log(`
136
+ ${pc.bold(pc.green("Success!"))} Server project initialized.
137
+
138
+ ${pc.bold("Next steps:")}
139
+ 1. Install dependencies:
140
+ ${pc.cyan("bun install")}
141
+
142
+ 2. Start development:
143
+ ${pc.cyan("bun run dev")}
144
+
145
+ 3. Generate types after adding plugins:
146
+ ${pc.cyan("bun run gen:types")}
147
+ `);
148
+ } else {
149
+ console.log(`
150
+ ${pc.bold(pc.green("Success!"))} SvelteKit project initialized.
151
+
152
+ ${pc.bold("Next steps:")}
153
+ 1. Install dependencies:
154
+ ${pc.cyan("bun install")}
155
+
156
+ 2. Start development:
157
+ ${pc.cyan("bun run dev")}
158
+
159
+ 3. Build for production:
160
+ ${pc.cyan("bun run build")}
161
+
162
+ 4. Preview production build:
163
+ ${pc.cyan("bun run preview")}
164
+
165
+ ${pc.bold("Project structure:")}
166
+ src/server/index.ts - Your @donkeylabs/server API
167
+ src/lib/api.ts - Typed API client
168
+ src/routes/ - SvelteKit pages
169
+ src/hooks.server.ts - Server hooks for SSR
170
+ `);
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Recursively copy a directory
176
+ */
177
+ async function copyDirectory(src: string, dest: string): Promise<void> {
178
+ await mkdir(dest, { recursive: true });
179
+
180
+ const entries = await readdir(src, { withFileTypes: true });
181
+
182
+ for (const entry of entries) {
183
+ const srcPath = join(src, entry.name);
184
+
185
+ // Skip certain files/directories
186
+ if (SKIP_PATTERNS.includes(entry.name)) {
187
+ continue;
188
+ }
189
+
190
+ // Handle renames
191
+ const destName = RENAME_MAP[entry.name] || entry.name;
192
+ const destPath = join(dest, destName);
193
+
194
+ if (entry.isDirectory()) {
195
+ await copyDirectory(srcPath, destPath);
196
+ } else {
197
+ await copyFile(srcPath, destPath);
198
+ console.log(pc.green(" Created:"), destName);
199
+ }
200
+ }
201
+ }
@@ -0,0 +1,223 @@
1
+ /**
2
+ * Interactive CLI Menu
3
+ *
4
+ * Full interactive experience with context-aware menus
5
+ */
6
+
7
+ import prompts from "prompts";
8
+ import pc from "picocolors";
9
+ import { readdir, writeFile, mkdir } from "node:fs/promises";
10
+ import { join, basename } from "node:path";
11
+ import { existsSync } from "node:fs";
12
+ import { exec } from "node:child_process";
13
+ import { promisify } from "node:util";
14
+
15
+ const execAsync = promisify(exec);
16
+
17
+ export async function interactiveCommand() {
18
+ console.clear();
19
+ console.log(pc.magenta(pc.bold("\n @donkeylabs/cli\n")));
20
+
21
+ // Detect context - are we in a plugin directory?
22
+ const cwd = process.cwd();
23
+ const pathParts = cwd.split("/");
24
+ const parentDir = pathParts[pathParts.length - 2];
25
+ const currentDir = pathParts[pathParts.length - 1];
26
+
27
+ let contextPlugin: string | null = null;
28
+ if (parentDir === "plugins" && currentDir && existsSync(join(cwd, "index.ts"))) {
29
+ contextPlugin = currentDir;
30
+ }
31
+
32
+ // Run appropriate menu loop
33
+ if (contextPlugin) {
34
+ await pluginMenuLoop(contextPlugin);
35
+ } else {
36
+ await globalMenuLoop();
37
+ }
38
+ }
39
+
40
+ // ============================================
41
+ // Plugin Context Menu (Inside plugins/<name>/)
42
+ // ============================================
43
+
44
+ async function pluginMenuLoop(pluginName: string) {
45
+ while (true) {
46
+ console.log(pc.cyan(`\n Context: Plugin ${pc.bold(`'${pluginName}'`)}\n`));
47
+
48
+ const response = await prompts({
49
+ type: "select",
50
+ name: "action",
51
+ message: "What would you like to do?",
52
+ choices: [
53
+ { title: pc.yellow("1.") + " Generate Schema Types", value: "gen-types" },
54
+ { title: pc.yellow("2.") + " Create Migration", value: "migration" },
55
+ { title: pc.gray("─".repeat(35)), value: "separator", disabled: true },
56
+ { title: pc.blue("←") + " Back to Global Menu", value: "global" },
57
+ { title: pc.red("×") + " Exit", value: "exit" },
58
+ ],
59
+ });
60
+
61
+ if (!response.action || response.action === "exit") {
62
+ console.log(pc.gray("\nGoodbye!\n"));
63
+ process.exit(0);
64
+ }
65
+
66
+ if (response.action === "global") {
67
+ console.clear();
68
+ console.log(pc.magenta(pc.bold("\n @donkeylabs/cli\n")));
69
+ await globalMenuLoop();
70
+ return;
71
+ }
72
+
73
+ console.log(""); // spacing
74
+
75
+ switch (response.action) {
76
+ case "gen-types":
77
+ await runCommand(`bun scripts/generate-types.ts ${pluginName}`);
78
+ break;
79
+ case "migration":
80
+ await createMigration(pluginName);
81
+ break;
82
+ }
83
+
84
+ await pressEnterToContinue();
85
+ }
86
+ }
87
+
88
+ // ============================================
89
+ // Global Root Menu
90
+ // ============================================
91
+
92
+ async function globalMenuLoop() {
93
+ while (true) {
94
+ console.log(pc.cyan("\n Context: Project Root\n"));
95
+
96
+ const choices = [
97
+ { title: pc.yellow("1.") + " Create New Plugin", value: "new-plugin" },
98
+ { title: pc.yellow("2.") + " Initialize New Project", value: "init" },
99
+ { title: pc.gray("─".repeat(35)), value: "separator1", disabled: true },
100
+ { title: pc.yellow("3.") + " Generate Types", value: "generate" },
101
+ { title: pc.yellow("4.") + " Generate Registry", value: "gen-registry" },
102
+ { title: pc.yellow("5.") + " Generate Server Context", value: "gen-server" },
103
+ { title: pc.gray("─".repeat(35)), value: "separator2", disabled: true },
104
+ { title: pc.red("×") + " Exit", value: "exit" },
105
+ ];
106
+
107
+ const response = await prompts({
108
+ type: "select",
109
+ name: "action",
110
+ message: "Select a command:",
111
+ choices,
112
+ });
113
+
114
+ if (!response.action || response.action === "exit") {
115
+ console.log(pc.gray("\nGoodbye!\n"));
116
+ process.exit(0);
117
+ }
118
+
119
+ console.log(""); // spacing
120
+
121
+ switch (response.action) {
122
+ case "new-plugin":
123
+ const { pluginCommand } = await import("./plugin");
124
+ await pluginCommand(["create"]);
125
+ break;
126
+ case "init":
127
+ const { initCommand } = await import("./init");
128
+ await initCommand([]);
129
+ break;
130
+ case "generate":
131
+ const { generateCommand } = await import("./generate");
132
+ await generateCommand([]);
133
+ break;
134
+ case "gen-registry":
135
+ await runCommand("bun scripts/generate-registry.ts");
136
+ break;
137
+ case "gen-server":
138
+ await runCommand("bun scripts/generate-server.ts");
139
+ break;
140
+ }
141
+
142
+ await pressEnterToContinue();
143
+ }
144
+ }
145
+
146
+ // ============================================
147
+ // Commands
148
+ // ============================================
149
+
150
+ async function createMigration(pluginName: string) {
151
+ const nameRes = await prompts({
152
+ type: "text",
153
+ name: "migName",
154
+ message: "Migration name (e.g. add_comments):",
155
+ validate: (v) =>
156
+ /^[a-z0-9_]+$/.test(v) ? true : "Use lowercase letters, numbers, and underscores",
157
+ });
158
+
159
+ if (!nameRes.migName) return;
160
+
161
+ // Determine migrations directory
162
+ const cwd = process.cwd();
163
+ const isPluginDir = basename(join(cwd, "..")) === "plugins";
164
+ const migrationsDir = isPluginDir
165
+ ? join(cwd, "migrations")
166
+ : join(process.cwd(), "src/plugins", pluginName, "migrations");
167
+
168
+ // Generate sequential number
169
+ let nextNum = 1;
170
+ try {
171
+ const files = await readdir(migrationsDir);
172
+ const nums = files
173
+ .map((f) => parseInt(f.split("_")[0] || "", 10))
174
+ .filter((n) => !isNaN(n));
175
+ if (nums.length > 0) {
176
+ nextNum = Math.max(...nums) + 1;
177
+ }
178
+ } catch {}
179
+
180
+ const filename = `${String(nextNum).padStart(3, "0")}_${nameRes.migName}.ts`;
181
+ const content = `import type { Kysely } from "kysely";
182
+
183
+ export async function up(db: Kysely<any>): Promise<void> {
184
+ // await db.schema.createTable("...").execute();
185
+ }
186
+
187
+ export async function down(db: Kysely<any>): Promise<void> {
188
+ // await db.schema.dropTable("...").execute();
189
+ }
190
+ `;
191
+
192
+ if (!existsSync(migrationsDir)) {
193
+ await mkdir(migrationsDir, { recursive: true });
194
+ }
195
+
196
+ await writeFile(join(migrationsDir, filename), content);
197
+ console.log(pc.green(`Created migration: ${filename}`));
198
+ }
199
+
200
+ // ============================================
201
+ // Helpers
202
+ // ============================================
203
+
204
+ async function runCommand(cmd: string) {
205
+ console.log(pc.gray(`> ${cmd}\n`));
206
+ try {
207
+ const { stdout, stderr } = await execAsync(cmd);
208
+ if (stdout) console.log(stdout);
209
+ if (stderr) console.error(pc.yellow(stderr));
210
+ } catch (e: any) {
211
+ console.error(pc.red("Command failed:"), e.message);
212
+ }
213
+ }
214
+
215
+ async function pressEnterToContinue() {
216
+ await prompts({
217
+ type: "invisible",
218
+ name: "continue",
219
+ message: pc.gray("Press Enter to continue..."),
220
+ });
221
+ console.clear();
222
+ console.log(pc.magenta(pc.bold("\n @donkeylabs/cli\n")));
223
+ }
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Plugin Command
3
+ *
4
+ * Plugin management commands
5
+ */
6
+
7
+ import { mkdir, writeFile } from "node:fs/promises";
8
+ import { join } from "node:path";
9
+ import { existsSync } from "node:fs";
10
+ import pc from "picocolors";
11
+ import prompts from "prompts";
12
+
13
+ export async function pluginCommand(args: string[]) {
14
+ const subcommand = args[0];
15
+
16
+ switch (subcommand) {
17
+ case "create":
18
+ case "new":
19
+ await createPlugin(args[1]);
20
+ break;
21
+
22
+ case "list":
23
+ await listPlugins();
24
+ break;
25
+
26
+ default:
27
+ console.log(`
28
+ ${pc.bold("Plugin Management")}
29
+
30
+ ${pc.bold("Commands:")}
31
+ ${pc.cyan("create <name>")} Create a new plugin
32
+ ${pc.cyan("list")} List installed plugins
33
+
34
+ ${pc.bold("Examples:")}
35
+ donkeylabs plugin create auth
36
+ donkeylabs plugin list
37
+ `);
38
+ }
39
+ }
40
+
41
+ async function createPlugin(name?: string) {
42
+ // Prompt for name if not provided
43
+ if (!name) {
44
+ const response = await prompts({
45
+ type: "text",
46
+ name: "name",
47
+ message: "Plugin name:",
48
+ validate: (v) =>
49
+ /^[a-z][a-z0-9-]*$/.test(v) ||
50
+ "Name must be lowercase alphanumeric with dashes",
51
+ });
52
+ name = response.name;
53
+ }
54
+
55
+ if (!name) {
56
+ console.log(pc.yellow("Cancelled."));
57
+ return;
58
+ }
59
+
60
+ // Ask for configuration
61
+ const { hasSchema, hasDependencies } = await prompts([
62
+ {
63
+ type: "confirm",
64
+ name: "hasSchema",
65
+ message: "Does this plugin need a database schema?",
66
+ initial: false,
67
+ },
68
+ {
69
+ type: "confirm",
70
+ name: "hasDependencies",
71
+ message: "Does this plugin depend on other plugins?",
72
+ initial: false,
73
+ },
74
+ ]);
75
+
76
+ let dependencies: string[] = [];
77
+ if (hasDependencies) {
78
+ const { deps } = await prompts({
79
+ type: "list",
80
+ name: "deps",
81
+ message: "Enter dependency names (comma-separated):",
82
+ separator: ",",
83
+ });
84
+ dependencies = deps?.filter(Boolean) || [];
85
+ }
86
+
87
+ // Determine plugin directory
88
+ const pluginDir = join(process.cwd(), "src/plugins", name);
89
+
90
+ if (existsSync(pluginDir)) {
91
+ console.log(pc.red(`Plugin directory already exists: ${pluginDir}`));
92
+ return;
93
+ }
94
+
95
+ await mkdir(pluginDir, { recursive: true });
96
+
97
+ // Generate plugin code
98
+ const camelName = name.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
99
+ const pascalName = camelName.charAt(0).toUpperCase() + camelName.slice(1);
100
+
101
+ const schemaImport = hasSchema
102
+ ? `import type { DB as ${pascalName}Schema } from "./schema";\n`
103
+ : "";
104
+
105
+ const schemaType = hasSchema ? `<${pascalName}Schema>()` : "";
106
+
107
+ const depsLine =
108
+ dependencies.length > 0
109
+ ? ` dependencies: [${dependencies.map((d) => `"${d}"`).join(", ")}] as const,\n`
110
+ : "";
111
+
112
+ const pluginContent = `import { createPlugin } from "@donkeylabs/server";
113
+ ${schemaImport}
114
+ export interface ${pascalName}Service {
115
+ // Define your service interface here
116
+ getData(): Promise<string>;
117
+ }
118
+
119
+ export const ${camelName}Plugin = createPlugin${hasSchema ? `\n .withSchema${schemaType}` : ""}
120
+ .define({
121
+ name: "${name}",
122
+ version: "1.0.0",
123
+ ${depsLine}
124
+ service: async (ctx): Promise<${pascalName}Service> => {
125
+ console.log("[${pascalName}Plugin] Initializing...");
126
+ ${
127
+ dependencies.length > 0
128
+ ? dependencies.map((d) => ` // Access ${d} via: ctx.deps.${d}`).join("\n") + "\n"
129
+ : ""
130
+ }
131
+ return {
132
+ getData: async () => {
133
+ return "Hello from ${name} plugin!";
134
+ },
135
+ };
136
+ },
137
+ });
138
+ `;
139
+
140
+ await writeFile(join(pluginDir, "index.ts"), pluginContent);
141
+ console.log(pc.green(" Created:"), `src/plugins/${name}/index.ts`);
142
+
143
+ if (hasSchema) {
144
+ // Create schema.ts
145
+ const schemaContent = `// Database schema types for ${name} plugin
146
+ // Run \`bun run gen:types\` to regenerate from database
147
+
148
+ export interface DB {
149
+ // Define your table interfaces here
150
+ // Example:
151
+ // ${name}: {
152
+ // id: Generated<number>;
153
+ // name: string;
154
+ // created_at: Generated<string>;
155
+ // };
156
+ }
157
+ `;
158
+ await writeFile(join(pluginDir, "schema.ts"), schemaContent);
159
+ console.log(pc.green(" Created:"), `src/plugins/${name}/schema.ts`);
160
+
161
+ // Create migrations directory
162
+ await mkdir(join(pluginDir, "migrations"), { recursive: true });
163
+
164
+ // Create initial migration
165
+ const migrationContent = `import { Kysely } from "kysely";
166
+
167
+ export async function up(db: Kysely<any>) {
168
+ // Create your tables here
169
+ // await db.schema
170
+ // .createTable("${name}")
171
+ // .ifNotExists()
172
+ // .addColumn("id", "integer", (col) => col.primaryKey().autoIncrement())
173
+ // .addColumn("name", "text", (col) => col.notNull())
174
+ // .addColumn("created_at", "text", (col) => col.defaultTo(sql\`CURRENT_TIMESTAMP\`))
175
+ // .execute();
176
+ }
177
+
178
+ export async function down(db: Kysely<any>) {
179
+ // Drop your tables here
180
+ // await db.schema.dropTable("${name}").ifExists().execute();
181
+ }
182
+ `;
183
+ await writeFile(
184
+ join(pluginDir, "migrations", "001_initial.ts"),
185
+ migrationContent
186
+ );
187
+ console.log(
188
+ pc.green(" Created:"),
189
+ `src/plugins/${name}/migrations/001_initial.ts`
190
+ );
191
+ }
192
+
193
+ console.log(`
194
+ ${pc.bold(pc.green("Plugin created!"))}
195
+
196
+ ${pc.bold("Next steps:")}
197
+ 1. Edit your plugin at ${pc.cyan(`src/plugins/${name}/index.ts`)}
198
+ ${hasSchema ? ` 2. Define your schema in ${pc.cyan(`src/plugins/${name}/schema.ts`)}\n 3. Add migrations in ${pc.cyan(`src/plugins/${name}/migrations/`)}\n` : ""} ${hasSchema ? "4" : "2"}. Regenerate types: ${pc.cyan("donkeylabs generate")}
199
+ `);
200
+ }
201
+
202
+ async function listPlugins() {
203
+ console.log(pc.yellow("Plugin listing not yet implemented."));
204
+ console.log("Run 'donkeylabs generate' to see discovered plugins.");
205
+ }