@donkeylabs/server 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.
@@ -0,0 +1,287 @@
1
+ /**
2
+ * Init Command
3
+ *
4
+ * Initialize a new @donkeylabs/server project with proper database setup.
5
+ */
6
+
7
+ import { mkdir, writeFile, readFile, readdir } from "node:fs/promises";
8
+ import { join, resolve } from "node:path";
9
+ import { existsSync } from "node:fs";
10
+ import pc from "picocolors";
11
+ import prompts from "prompts";
12
+
13
+ interface InitOptions {
14
+ projectName: string;
15
+ useDatabase: boolean;
16
+ }
17
+
18
+ export async function initCommand(args: string[]): Promise<void> {
19
+ const projectDir = args[0] || ".";
20
+ const targetDir = resolve(process.cwd(), projectDir);
21
+
22
+ console.log(pc.bold("\nInitializing @donkeylabs/server project...\n"));
23
+
24
+ // Check for existing files
25
+ if (existsSync(targetDir)) {
26
+ const files = await readdir(targetDir);
27
+ const hasConflicts = files.some(
28
+ (f) => f === "src" || f === "donkeylabs.config.ts"
29
+ );
30
+
31
+ if (hasConflicts) {
32
+ const { overwrite } = await prompts({
33
+ type: "confirm",
34
+ name: "overwrite",
35
+ message: "Directory contains existing files. Overwrite?",
36
+ initial: false,
37
+ });
38
+
39
+ if (!overwrite) {
40
+ console.log(pc.yellow("Cancelled."));
41
+ return;
42
+ }
43
+ }
44
+ }
45
+
46
+ // Ask for project configuration
47
+ const responses = await prompts([
48
+ {
49
+ type: "text",
50
+ name: "projectName",
51
+ message: "Project name:",
52
+ initial: projectDir === "." ? "my-server" : projectDir,
53
+ },
54
+ {
55
+ type: "confirm",
56
+ name: "useDatabase",
57
+ message: "Set up SQLite database?",
58
+ initial: true,
59
+ },
60
+ ]);
61
+
62
+ if (!responses.projectName) {
63
+ console.log(pc.yellow("Cancelled."));
64
+ return;
65
+ }
66
+
67
+ const options: InitOptions = {
68
+ projectName: responses.projectName,
69
+ useDatabase: responses.useDatabase ?? true,
70
+ };
71
+
72
+ // Create directories
73
+ await mkdir(join(targetDir, "src/plugins"), { recursive: true });
74
+ await mkdir(join(targetDir, ".@donkeylabs/server"), { recursive: true });
75
+
76
+ // Create config file
77
+ await writeFile(
78
+ join(targetDir, "donkeylabs.config.ts"),
79
+ generateConfig()
80
+ );
81
+ console.log(pc.green(" Created:"), "donkeylabs.config.ts");
82
+
83
+ // Create database setup file if using SQLite
84
+ if (options.useDatabase) {
85
+ await writeFile(join(targetDir, "src/db.ts"), generateDatabaseSetup());
86
+ console.log(pc.green(" Created:"), "src/db.ts");
87
+
88
+ await writeFile(join(targetDir, ".env.example"), generateEnvExample());
89
+ console.log(pc.green(" Created:"), ".env.example");
90
+ }
91
+
92
+ // Create main index file
93
+ await writeFile(
94
+ join(targetDir, "src/index.ts"),
95
+ generateIndexFile(options.useDatabase)
96
+ );
97
+ console.log(pc.green(" Created:"), "src/index.ts");
98
+
99
+ // Update .gitignore
100
+ const gitignorePath = join(targetDir, ".gitignore");
101
+ const gitignoreContent = existsSync(gitignorePath)
102
+ ? await readFile(gitignorePath, "utf-8")
103
+ : "";
104
+
105
+ const gitignoreAdditions: string[] = [];
106
+ if (!gitignoreContent.includes(".@donkeylabs")) {
107
+ gitignoreAdditions.push("# Generated types", ".@donkeylabs/");
108
+ }
109
+ if (!gitignoreContent.includes(".env") && options.useDatabase) {
110
+ gitignoreAdditions.push("# Environment", ".env", ".env.local");
111
+ }
112
+ if (options.useDatabase && !gitignoreContent.includes("*.db")) {
113
+ gitignoreAdditions.push("# SQLite", "*.db", "*.db-journal");
114
+ }
115
+
116
+ if (gitignoreAdditions.length > 0) {
117
+ const newGitignore = gitignoreContent + "\n" + gitignoreAdditions.join("\n") + "\n";
118
+ await writeFile(gitignorePath, newGitignore);
119
+ console.log(pc.green(" Updated:"), ".gitignore");
120
+ }
121
+
122
+ // Create tsconfig.json if not exists
123
+ if (!existsSync(join(targetDir, "tsconfig.json"))) {
124
+ await writeFile(
125
+ join(targetDir, "tsconfig.json"),
126
+ generateTsConfig()
127
+ );
128
+ console.log(pc.green(" Created:"), "tsconfig.json");
129
+ }
130
+
131
+ // Update package.json
132
+ const pkgPath = join(targetDir, "package.json");
133
+ let pkg: any = { name: options.projectName, type: "module", scripts: {} };
134
+
135
+ if (existsSync(pkgPath)) {
136
+ pkg = JSON.parse(await readFile(pkgPath, "utf-8"));
137
+ pkg.scripts = pkg.scripts || {};
138
+ }
139
+
140
+ pkg.name = pkg.name || options.projectName;
141
+ pkg.type = "module";
142
+ pkg.scripts.dev = "bun --watch src/index.ts";
143
+ pkg.scripts.start = "bun src/index.ts";
144
+ pkg.scripts["gen:types"] = "donkeylabs generate";
145
+
146
+ await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
147
+ console.log(pc.green(" Updated:"), "package.json");
148
+
149
+ // Print next steps
150
+ console.log(`
151
+ ${pc.bold(pc.green("Success!"))} Project initialized.
152
+
153
+ ${pc.bold("Next steps:")}
154
+ 1. Install dependencies:
155
+ ${pc.cyan(getDependencyCommand(options.useDatabase))}
156
+ ${options.useDatabase ? `
157
+ 2. Set up your database:
158
+ ${pc.cyan(`cp .env.example .env`)}
159
+ ${pc.dim("# Edit .env with your database path")}
160
+ ` : ""}
161
+ ${options.useDatabase ? "3" : "2"}. Start development:
162
+ ${pc.cyan("bun run dev")}
163
+
164
+ ${options.useDatabase ? "4" : "3"}. Generate types after adding plugins:
165
+ ${pc.cyan("bun run gen:types")}
166
+ `);
167
+ }
168
+
169
+ function generateConfig(): string {
170
+ return `import { defineConfig } from "@donkeylabs/server";
171
+
172
+ export default defineConfig({
173
+ plugins: ["./src/plugins/**/index.ts"],
174
+ outDir: ".@donkeylabs/server",
175
+ });
176
+ `;
177
+ }
178
+
179
+ function generateDatabaseSetup(): string {
180
+ return `import { Kysely } from "kysely";
181
+ import { BunSqliteDialect } from "kysely-bun-sqlite";
182
+ import { Database } from "bun:sqlite";
183
+
184
+ const dbPath = process.env.DATABASE_URL || "app.db";
185
+
186
+ export const db = new Kysely<any>({
187
+ dialect: new BunSqliteDialect({
188
+ database: new Database(dbPath),
189
+ }),
190
+ });
191
+
192
+ export type DB = typeof db;
193
+ `;
194
+ }
195
+
196
+ function generateIndexFile(useDatabase: boolean): string {
197
+ const dbImport = useDatabase
198
+ ? `import { db } from "./db";\n`
199
+ : `import {
200
+ Kysely,
201
+ DummyDriver,
202
+ SqliteAdapter,
203
+ SqliteIntrospector,
204
+ SqliteQueryCompiler,
205
+ } from "kysely";
206
+
207
+ // No database configured - using dummy driver for type compatibility
208
+ const db = new Kysely<any>({
209
+ dialect: {
210
+ createAdapter: () => new SqliteAdapter(),
211
+ createDriver: () => new DummyDriver(),
212
+ createIntrospector: (db) => new SqliteIntrospector(db),
213
+ createQueryCompiler: () => new SqliteQueryCompiler(),
214
+ },
215
+ });
216
+ `;
217
+
218
+ return `${dbImport}import { AppServer, createRouter } from "@donkeylabs/server";
219
+ import { z } from "zod";
220
+
221
+ // Create Server
222
+ const server = new AppServer({
223
+ port: Number(process.env.PORT) || 3000,
224
+ db,
225
+ config: { env: process.env.NODE_ENV || "development" },
226
+ });
227
+
228
+ // Define Routes
229
+ const router = createRouter("api")
230
+ .route("hello").typed({
231
+ input: z.object({ name: z.string().optional() }),
232
+ output: z.object({ message: z.string() }),
233
+ handle: async (input) => {
234
+ return { message: \`Hello, \${input.name || "World"}!\` };
235
+ },
236
+ })
237
+ .route("health").raw({
238
+ handle: async () => {
239
+ return Response.json({ status: "ok", timestamp: new Date().toISOString() });
240
+ },
241
+ });
242
+
243
+ server.use(router);
244
+
245
+ // Start Server
246
+ await server.start();
247
+ `;
248
+ }
249
+
250
+ function generateEnvExample(): string {
251
+ return `# Database (SQLite file path)
252
+ DATABASE_URL=app.db
253
+
254
+ # Server
255
+ PORT=3000
256
+ NODE_ENV=development
257
+ `;
258
+ }
259
+
260
+ function generateTsConfig(): string {
261
+ return `{
262
+ "compilerOptions": {
263
+ "lib": ["ESNext"],
264
+ "target": "ESNext",
265
+ "module": "Preserve",
266
+ "moduleDetection": "force",
267
+ "jsx": "react-jsx",
268
+ "allowJs": true,
269
+ "moduleResolution": "bundler",
270
+ "allowImportingTsExtensions": true,
271
+ "verbatimModuleSyntax": true,
272
+ "noEmit": true,
273
+ "strict": true,
274
+ "skipLibCheck": true,
275
+ "noFallthroughCasesInSwitch": true,
276
+ "noUncheckedIndexedAccess": true,
277
+ "noImplicitOverride": true
278
+ },
279
+ "include": ["src/**/*", "*.ts", ".@donkeylabs/**/*"]
280
+ }
281
+ `;
282
+ }
283
+
284
+ function getDependencyCommand(useDatabase: boolean): string {
285
+ const base = "bun add @donkeylabs/server kysely zod";
286
+ return useDatabase ? `${base} kysely-bun-sqlite` : base;
287
+ }
@@ -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/server 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/server 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/server CLI\n")));
223
+ }
@@ -0,0 +1,192 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { existsSync } from "node:fs";
4
+ import pc from "picocolors";
5
+ import prompts from "prompts";
6
+
7
+ export async function pluginCommand(args: string[]): Promise<void> {
8
+ const subcommand = args[0];
9
+
10
+ switch (subcommand) {
11
+ case "create":
12
+ case "new":
13
+ await createPlugin(args[1]);
14
+ break;
15
+
16
+ case "list":
17
+ await listPlugins();
18
+ break;
19
+
20
+ default:
21
+ console.log(`
22
+ ${pc.bold("Plugin Management")}
23
+
24
+ ${pc.bold("Commands:")}
25
+ ${pc.cyan("create <name>")} Create a new plugin
26
+ ${pc.cyan("list")} List installed plugins
27
+
28
+ ${pc.bold("Examples:")}
29
+ donkeylabs plugin create auth
30
+ donkeylabs plugin list
31
+ `);
32
+ }
33
+ }
34
+
35
+ async function createPlugin(name?: string): Promise<void> {
36
+ if (!name) {
37
+ const response = await prompts({
38
+ type: "text",
39
+ name: "name",
40
+ message: "Plugin name:",
41
+ validate: (v) =>
42
+ /^[a-z][a-z0-9-]*$/.test(v) ||
43
+ "Name must be lowercase alphanumeric with dashes",
44
+ });
45
+ name = response.name;
46
+ }
47
+
48
+ if (!name) {
49
+ console.log(pc.yellow("Cancelled."));
50
+ return;
51
+ }
52
+
53
+ const { hasSchema, hasDependencies } = await prompts([
54
+ {
55
+ type: "confirm",
56
+ name: "hasSchema",
57
+ message: "Does this plugin need a database schema?",
58
+ initial: false,
59
+ },
60
+ {
61
+ type: "confirm",
62
+ name: "hasDependencies",
63
+ message: "Does this plugin depend on other plugins?",
64
+ initial: false,
65
+ },
66
+ ]);
67
+
68
+ let dependencies: string[] = [];
69
+ if (hasDependencies) {
70
+ const { deps } = await prompts({
71
+ type: "list",
72
+ name: "deps",
73
+ message: "Enter dependency names (comma-separated):",
74
+ separator: ",",
75
+ });
76
+ dependencies = deps?.filter(Boolean) || [];
77
+ }
78
+
79
+ const pluginDir = join(process.cwd(), "src/plugins", name);
80
+
81
+ if (existsSync(pluginDir)) {
82
+ console.log(pc.red(`Plugin directory already exists: ${pluginDir}`));
83
+ return;
84
+ }
85
+
86
+ await mkdir(pluginDir, { recursive: true });
87
+
88
+ const camelName = name.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
89
+ const pascalName = camelName.charAt(0).toUpperCase() + camelName.slice(1);
90
+
91
+ const schemaImport = hasSchema
92
+ ? `import type { DB as ${pascalName}Schema } from "./schema";\n`
93
+ : "";
94
+
95
+ const schemaType = hasSchema ? `<${pascalName}Schema>()` : "";
96
+
97
+ const depsLine =
98
+ dependencies.length > 0
99
+ ? ` dependencies: [${dependencies.map((d) => `"${d}"`).join(", ")}] as const,\n`
100
+ : "";
101
+
102
+ const pluginContent = `import { createPlugin } from "@donkeylabs/server";
103
+ ${schemaImport}
104
+ export interface ${pascalName}Service {
105
+ // Define your service interface here
106
+ getData(): Promise<string>;
107
+ }
108
+
109
+ export const ${camelName}Plugin = createPlugin${hasSchema ? `\n .withSchema${schemaType}` : ""}
110
+ .define({
111
+ name: "${name}",
112
+ version: "1.0.0",
113
+ ${depsLine}
114
+ service: async (ctx): Promise<${pascalName}Service> => {
115
+ console.log("[${pascalName}Plugin] Initializing...");
116
+ ${
117
+ dependencies.length > 0
118
+ ? dependencies.map((d) => ` // Access ${d} via: ctx.deps.${d}`).join("\n") + "\n"
119
+ : ""
120
+ }
121
+ return {
122
+ getData: async () => {
123
+ return "Hello from ${name} plugin!";
124
+ },
125
+ };
126
+ },
127
+ });
128
+ `;
129
+
130
+ await writeFile(join(pluginDir, "index.ts"), pluginContent);
131
+ console.log(pc.green(" Created:"), `src/plugins/${name}/index.ts`);
132
+
133
+ if (hasSchema) {
134
+ const schemaContent = `// Database schema types for ${name} plugin
135
+ // Run \`bun run gen:types\` to regenerate from database
136
+
137
+ export interface DB {
138
+ // Define your table interfaces here
139
+ // Example:
140
+ // ${name}: {
141
+ // id: Generated<number>;
142
+ // name: string;
143
+ // created_at: Generated<string>;
144
+ // };
145
+ }
146
+ `;
147
+ await writeFile(join(pluginDir, "schema.ts"), schemaContent);
148
+ console.log(pc.green(" Created:"), `src/plugins/${name}/schema.ts`);
149
+
150
+ await mkdir(join(pluginDir, "migrations"), { recursive: true });
151
+
152
+ const migrationContent = `import { Kysely } from "kysely";
153
+
154
+ export async function up(db: Kysely<any>) {
155
+ // Create your tables here
156
+ // await db.schema
157
+ // .createTable("${name}")
158
+ // .ifNotExists()
159
+ // .addColumn("id", "integer", (col) => col.primaryKey().autoIncrement())
160
+ // .addColumn("name", "text", (col) => col.notNull())
161
+ // .addColumn("created_at", "text", (col) => col.defaultTo(sql\`CURRENT_TIMESTAMP\`))
162
+ // .execute();
163
+ }
164
+
165
+ export async function down(db: Kysely<any>) {
166
+ // Drop your tables here
167
+ // await db.schema.dropTable("${name}").ifExists().execute();
168
+ }
169
+ `;
170
+ await writeFile(
171
+ join(pluginDir, "migrations", "001_initial.ts"),
172
+ migrationContent
173
+ );
174
+ console.log(
175
+ pc.green(" Created:"),
176
+ `src/plugins/${name}/migrations/001_initial.ts`
177
+ );
178
+ }
179
+
180
+ console.log(`
181
+ ${pc.bold(pc.green("Plugin created!"))}
182
+
183
+ ${pc.bold("Next steps:")}
184
+ 1. Edit your plugin at ${pc.cyan(`src/plugins/${name}/index.ts`)}
185
+ ${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")}
186
+ `);
187
+ }
188
+
189
+ async function listPlugins() {
190
+ console.log(pc.yellow("Plugin listing not yet implemented."));
191
+ console.log("Run 'donkeylabs generate' to see discovered plugins.");
192
+ }