create-meridian-app 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,468 @@
1
+ // src/commands/new.ts
2
+ import path2 from "path";
3
+ import { existsSync as existsSync2 } from "fs";
4
+ import ora from "ora";
5
+ import chalk from "chalk";
6
+ import prompts from "prompts";
7
+ import { execa } from "execa";
8
+
9
+ // src/templates/index.ts
10
+ function renderPackageJson(vars) {
11
+ return JSON.stringify(
12
+ {
13
+ name: vars.name,
14
+ version: "0.1.0",
15
+ private: true,
16
+ type: "module",
17
+ scripts: {
18
+ dev: "meridian dev",
19
+ build: "meridian build",
20
+ start: "node --import tsx/esm src/main.ts",
21
+ "db:migrate": "meridian db:migrate",
22
+ "db:generate": "meridian db:generate"
23
+ },
24
+ dependencies: {
25
+ "@meridianjs/framework": "latest",
26
+ "@meridianjs/framework-utils": "latest",
27
+ "@meridianjs/types": "latest",
28
+ "@meridianjs/event-bus-local": "latest",
29
+ "@meridianjs/user": "latest",
30
+ "@meridianjs/workspace": "latest",
31
+ "@meridianjs/auth": "latest",
32
+ "@meridianjs/project": "latest",
33
+ "@meridianjs/issue": "latest",
34
+ "dotenv": "^16.0.0"
35
+ },
36
+ devDependencies: {
37
+ "create-meridian-app": "latest",
38
+ typescript: "^5.4.0",
39
+ tsx: "^4.0.0",
40
+ "@types/node": "^22.0.0"
41
+ }
42
+ },
43
+ null,
44
+ 2
45
+ );
46
+ }
47
+ function renderTsConfig() {
48
+ return JSON.stringify(
49
+ {
50
+ compilerOptions: {
51
+ target: "ES2022",
52
+ module: "NodeNext",
53
+ moduleResolution: "NodeNext",
54
+ lib: ["ES2022"],
55
+ outDir: "dist",
56
+ rootDir: "src",
57
+ strict: true,
58
+ esModuleInterop: true,
59
+ skipLibCheck: true,
60
+ resolveJsonModule: true,
61
+ declaration: true,
62
+ declarationMap: true,
63
+ sourceMap: true
64
+ },
65
+ include: ["src/**/*"],
66
+ exclude: ["node_modules", "dist"]
67
+ },
68
+ null,
69
+ 2
70
+ );
71
+ }
72
+ function renderMeridianConfig(vars) {
73
+ return `import { defineConfig } from "@meridianjs/framework"
74
+ import dotenv from "dotenv"
75
+ dotenv.config()
76
+
77
+ export default defineConfig({
78
+ projectConfig: {
79
+ databaseUrl: process.env.DATABASE_URL ?? "${vars.databaseUrl}",
80
+ httpPort: Number(process.env.PORT) || ${vars.httpPort},
81
+ jwtSecret: process.env.JWT_SECRET ?? "changeme-replace-in-production",
82
+ },
83
+ modules: [
84
+ { resolve: "@meridianjs/event-bus-local" },
85
+ { resolve: "@meridianjs/user" },
86
+ { resolve: "@meridianjs/workspace" },
87
+ { resolve: "@meridianjs/auth" },
88
+ { resolve: "@meridianjs/project" },
89
+ { resolve: "@meridianjs/issue" },
90
+ ],
91
+ })
92
+ `;
93
+ }
94
+ function renderMainTs() {
95
+ return `import { bootstrap } from "@meridianjs/framework"
96
+ import { fileURLToPath } from "node:url"
97
+ import path from "node:path"
98
+
99
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
100
+ const rootDir = path.resolve(__dirname, "..")
101
+
102
+ const app = await bootstrap({ rootDir })
103
+ await app.start()
104
+
105
+ process.on("SIGTERM", async () => {
106
+ await app.stop()
107
+ process.exit(0)
108
+ })
109
+
110
+ process.on("SIGINT", async () => {
111
+ await app.stop()
112
+ process.exit(0)
113
+ })
114
+ `;
115
+ }
116
+ function renderMiddlewares() {
117
+ return `import { authenticateJWT } from "@meridianjs/auth"
118
+
119
+ export default {
120
+ routes: [
121
+ { matcher: "/admin", middlewares: [authenticateJWT] },
122
+ ],
123
+ }
124
+ `;
125
+ }
126
+ function renderHelloRoute() {
127
+ return `import type { Request, Response } from "express"
128
+
129
+ export const GET = async (_req: Request, res: Response) => {
130
+ res.json({ message: "Hello from Meridian!", timestamp: new Date().toISOString() })
131
+ }
132
+ `;
133
+ }
134
+ function renderGitIgnore() {
135
+ return `# Dependencies
136
+ node_modules/
137
+
138
+ # Build output
139
+ dist/
140
+
141
+ # Environment variables
142
+ .env
143
+ .env.local
144
+ .env.*.local
145
+
146
+ # Logs
147
+ *.log
148
+ npm-debug.log*
149
+
150
+ # Editor
151
+ .vscode/
152
+ .idea/
153
+ *.swp
154
+
155
+ # OS
156
+ .DS_Store
157
+ Thumbs.db
158
+ `;
159
+ }
160
+ function renderEnvExample(vars) {
161
+ return `# Copy this file to .env and fill in your values
162
+ DATABASE_URL=${vars.databaseUrl}
163
+ PORT=${vars.httpPort}
164
+ JWT_SECRET=changeme-replace-in-production
165
+ `;
166
+ }
167
+ function renderReadme(vars) {
168
+ return `# ${vars.name}
169
+
170
+ A Meridian application.
171
+
172
+ ## Getting started
173
+
174
+ \`\`\`bash
175
+ # Install dependencies
176
+ npm install
177
+
178
+ # Set up your database
179
+ cp .env.example .env
180
+ # Edit .env with your database URL
181
+
182
+ # Sync database schema
183
+ npm run db:migrate
184
+
185
+ # Start the development server
186
+ npm run dev
187
+ \`\`\`
188
+
189
+ ## Commands
190
+
191
+ | Command | Description |
192
+ |---|---|
193
+ | \`npm run dev\` | Start development server with hot reload |
194
+ | \`npm run build\` | Type-check the project |
195
+ | \`npm run db:migrate\` | Synchronize the database schema |
196
+ | \`npm run db:generate <name>\` | Generate a new migration file |
197
+
198
+ ## Project structure
199
+
200
+ \`\`\`
201
+ src/
202
+ main.ts Entry point
203
+ api/
204
+ middlewares.ts Route-level middleware configuration
205
+ admin/ File-based API routes
206
+ modules/ Custom domain modules
207
+ workflows/ DAG workflows with compensation
208
+ subscribers/ Event subscribers
209
+ jobs/ Scheduled background jobs
210
+ links/ Cross-module link definitions
211
+ \`\`\`
212
+
213
+ ## Extending Meridian
214
+
215
+ See the [Meridian documentation](https://github.com/meridian/meridian) for guides on:
216
+ - Creating custom modules
217
+ - Building workflows
218
+ - Writing event subscribers
219
+ - Scheduling background jobs
220
+ - Building plugins
221
+ `;
222
+ }
223
+ function renderModuleIndex(name, pascalName) {
224
+ return `import { Module } from "@meridianjs/framework-utils"
225
+ import { ${pascalName}ModuleService } from "./service.js"
226
+ import { ${pascalName} } from "./models/${name}.js"
227
+ import defaultLoader from "./loaders/default.js"
228
+
229
+ export default Module("${name}ModuleService", {
230
+ service: ${pascalName}ModuleService,
231
+ models: [${pascalName}],
232
+ loaders: [defaultLoader],
233
+ linkable: {
234
+ ${name}: { tableName: "${name}", primaryKey: "id" },
235
+ },
236
+ })
237
+ `;
238
+ }
239
+ function renderModuleLoader(name, pascalName) {
240
+ return `import { createModuleOrm, createRepository, dmlToEntitySchema } from "@meridianjs/framework-utils"
241
+ import type { LoaderOptions } from "@meridianjs/types"
242
+ import { ${pascalName} } from "../models/${name}.js"
243
+
244
+ const ${pascalName}Schema = dmlToEntitySchema(${pascalName})
245
+
246
+ export default async function defaultLoader({ container }: LoaderOptions): Promise<void> {
247
+ const config = container.resolve("config") as any
248
+ const orm = await createModuleOrm(
249
+ [${pascalName}Schema],
250
+ config.projectConfig.databaseUrl
251
+ )
252
+ const em = orm.em.fork()
253
+ container.register({
254
+ ${name}Repository: createRepository(em, "${name}"),
255
+ ${name}Orm: orm,
256
+ })
257
+ }
258
+ `;
259
+ }
260
+ function renderModuleModel(name, pascalName) {
261
+ return `import { model } from "@meridianjs/framework-utils"
262
+
263
+ export const ${pascalName} = model("${name}", {
264
+ id: model.id(),
265
+ name: model.text(),
266
+ created_at: model.dateTime(),
267
+ updated_at: model.dateTime(),
268
+ })
269
+ `;
270
+ }
271
+ function renderModuleService(name, pascalName) {
272
+ return `import { MeridianService } from "@meridianjs/framework-utils"
273
+ import type { MeridianContainer } from "@meridianjs/types"
274
+ import { ${pascalName} } from "./models/${name}.js"
275
+
276
+ export class ${pascalName}ModuleService extends MeridianService({ ${pascalName} }) {
277
+ constructor(container: MeridianContainer) {
278
+ super(container)
279
+ }
280
+
281
+ // Add custom service methods here
282
+ }
283
+ `;
284
+ }
285
+ function renderWorkflow(name, pascalName) {
286
+ const camelName = pascalName.charAt(0).toLowerCase() + pascalName.slice(1);
287
+ return `import { createStep, createWorkflow, WorkflowResponse } from "@meridianjs/workflow-engine"
288
+ import type { MeridianContainer } from "@meridianjs/types"
289
+
290
+ interface ${pascalName}WorkflowInput {
291
+ // Define your input shape here
292
+ }
293
+
294
+ const ${camelName}Step = createStep(
295
+ "${name}-step",
296
+ async (input: ${pascalName}WorkflowInput, { container }: { container: MeridianContainer }) => {
297
+ // Forward logic: implement your step here
298
+ return { result: null }
299
+ },
300
+ async (_input: ${pascalName}WorkflowInput, _context: { container: MeridianContainer }) => {
301
+ // Compensation logic: runs if a later step fails
302
+ }
303
+ )
304
+
305
+ export const ${camelName}Workflow = createWorkflow(
306
+ "${name}",
307
+ (input: ${pascalName}WorkflowInput) => {
308
+ const result = ${camelName}Step(input)
309
+ return new WorkflowResponse(result)
310
+ }
311
+ )
312
+ `;
313
+ }
314
+
315
+ // src/utils.ts
316
+ import path from "path";
317
+ import fs from "fs/promises";
318
+ import { existsSync } from "fs";
319
+ function toPascalCase(str) {
320
+ return str.replace(/[-_](.)/g, (_, c) => c.toUpperCase()).replace(/^(.)/, (_, c) => c.toUpperCase());
321
+ }
322
+ function toKebabCase(str) {
323
+ return str.replace(/([A-Z])/g, "-$1").toLowerCase().replace(/^-/, "");
324
+ }
325
+ async function writeFile(filePath, content) {
326
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
327
+ await fs.writeFile(filePath, content, "utf-8");
328
+ }
329
+ async function mkdir(dirPath) {
330
+ await fs.mkdir(dirPath, { recursive: true });
331
+ }
332
+ async function mkdirWithKeep(dirPath) {
333
+ await mkdir(dirPath);
334
+ await writeFile(path.join(dirPath, ".gitkeep"), "");
335
+ }
336
+ function findProjectRoot(startDir = process.cwd()) {
337
+ let dir = startDir;
338
+ while (true) {
339
+ if (existsSync(path.join(dir, "meridian.config.ts"))) {
340
+ return dir;
341
+ }
342
+ const parent = path.dirname(dir);
343
+ if (parent === dir) return null;
344
+ dir = parent;
345
+ }
346
+ }
347
+
348
+ // src/commands/new.ts
349
+ async function runNew(projectName) {
350
+ console.log();
351
+ console.log(chalk.bold(" Create Meridian App"));
352
+ console.log();
353
+ let name = projectName;
354
+ if (!name) {
355
+ const res = await prompts(
356
+ {
357
+ type: "text",
358
+ name: "name",
359
+ message: "Project name",
360
+ initial: "my-meridian-app",
361
+ validate: (v) => /^[a-z0-9-_]+$/.test(v) || "Use lowercase letters, numbers, hyphens, and underscores only"
362
+ },
363
+ { onCancel: () => process.exit(0) }
364
+ );
365
+ name = res.name;
366
+ }
367
+ const targetDir = path2.resolve(process.cwd(), name);
368
+ if (existsSync2(targetDir)) {
369
+ const { overwrite } = await prompts(
370
+ {
371
+ type: "confirm",
372
+ name: "overwrite",
373
+ message: `Directory "${name}" already exists. Continue anyway?`,
374
+ initial: false
375
+ },
376
+ { onCancel: () => process.exit(0) }
377
+ );
378
+ if (!overwrite) {
379
+ console.log(chalk.yellow(" Cancelled."));
380
+ process.exit(0);
381
+ }
382
+ }
383
+ const answers = await prompts(
384
+ [
385
+ {
386
+ type: "text",
387
+ name: "databaseUrl",
388
+ message: "Database URL",
389
+ initial: `postgresql://postgres:postgres@localhost:5432/${name}`
390
+ },
391
+ {
392
+ type: "number",
393
+ name: "httpPort",
394
+ message: "HTTP port",
395
+ initial: 9e3
396
+ },
397
+ {
398
+ type: "confirm",
399
+ name: "installDeps",
400
+ message: "Install dependencies now?",
401
+ initial: true
402
+ }
403
+ ],
404
+ { onCancel: () => process.exit(0) }
405
+ );
406
+ const vars = {
407
+ name,
408
+ databaseUrl: answers.databaseUrl,
409
+ httpPort: answers.httpPort
410
+ };
411
+ const spinner = ora("Scaffolding project...").start();
412
+ try {
413
+ await writeFile(path2.join(targetDir, "package.json"), renderPackageJson(vars));
414
+ await writeFile(path2.join(targetDir, "tsconfig.json"), renderTsConfig());
415
+ await writeFile(path2.join(targetDir, "meridian.config.ts"), renderMeridianConfig(vars));
416
+ await writeFile(path2.join(targetDir, ".gitignore"), renderGitIgnore());
417
+ await writeFile(path2.join(targetDir, ".env.example"), renderEnvExample(vars));
418
+ await writeFile(path2.join(targetDir, "README.md"), renderReadme(vars));
419
+ await writeFile(path2.join(targetDir, "src", "main.ts"), renderMainTs());
420
+ await writeFile(path2.join(targetDir, "src", "api", "middlewares.ts"), renderMiddlewares());
421
+ await writeFile(
422
+ path2.join(targetDir, "src", "api", "admin", "hello", "route.ts"),
423
+ renderHelloRoute()
424
+ );
425
+ await mkdirWithKeep(path2.join(targetDir, "src", "modules"));
426
+ await mkdirWithKeep(path2.join(targetDir, "src", "workflows"));
427
+ await mkdirWithKeep(path2.join(targetDir, "src", "subscribers"));
428
+ await mkdirWithKeep(path2.join(targetDir, "src", "jobs"));
429
+ await mkdirWithKeep(path2.join(targetDir, "src", "links"));
430
+ spinner.succeed("Scaffolded project files");
431
+ } catch (err) {
432
+ spinner.fail("Failed to scaffold project files");
433
+ throw err;
434
+ }
435
+ if (answers.installDeps) {
436
+ const installSpinner = ora("Installing dependencies...").start();
437
+ try {
438
+ await execa("npm", ["install"], { cwd: targetDir, stdio: "pipe" });
439
+ installSpinner.succeed("Dependencies installed");
440
+ } catch (err) {
441
+ installSpinner.warn("npm install failed \u2014 run it manually");
442
+ }
443
+ }
444
+ console.log();
445
+ console.log(chalk.green(" \u2713 Project created!"));
446
+ console.log();
447
+ console.log(` ${chalk.dim("cd")} ${chalk.cyan(name)}`);
448
+ if (!answers.installDeps) {
449
+ console.log(` ${chalk.dim("npm install")}`);
450
+ }
451
+ console.log(` ${chalk.dim("cp")} .env.example .env ${chalk.dim("# fill in your DATABASE_URL")}`);
452
+ console.log(` ${chalk.cyan("npm run db:migrate")}`);
453
+ console.log(` ${chalk.cyan("npm run dev")}`);
454
+ console.log();
455
+ }
456
+
457
+ export {
458
+ renderModuleIndex,
459
+ renderModuleLoader,
460
+ renderModuleModel,
461
+ renderModuleService,
462
+ renderWorkflow,
463
+ toPascalCase,
464
+ toKebabCase,
465
+ writeFile,
466
+ findProjectRoot,
467
+ runNew
468
+ };
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/cli.js ADDED
@@ -0,0 +1,363 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ findProjectRoot,
4
+ renderModuleIndex,
5
+ renderModuleLoader,
6
+ renderModuleModel,
7
+ renderModuleService,
8
+ renderWorkflow,
9
+ runNew,
10
+ toKebabCase,
11
+ toPascalCase,
12
+ writeFile
13
+ } from "./chunk-7IUTX6RW.js";
14
+
15
+ // src/cli.ts
16
+ import { Command } from "commander";
17
+
18
+ // src/commands/dev.ts
19
+ import path from "path";
20
+ import { existsSync } from "fs";
21
+ import chalk from "chalk";
22
+ import { execa } from "execa";
23
+ async function runDev() {
24
+ const rootDir = findProjectRoot();
25
+ if (!rootDir) {
26
+ console.error(chalk.red(" \u2716 Could not find meridian.config.ts. Are you inside a Meridian project?"));
27
+ process.exit(1);
28
+ }
29
+ const mainTs = path.join(rootDir, "src", "main.ts");
30
+ if (!existsSync(mainTs)) {
31
+ console.error(chalk.red(` \u2716 Entry point not found: ${mainTs}`));
32
+ process.exit(1);
33
+ }
34
+ console.log(chalk.dim(` \u2192 Starting dev server from ${rootDir}`));
35
+ console.log();
36
+ const result = await execa(
37
+ "node",
38
+ ["--import", "tsx/esm", mainTs],
39
+ {
40
+ cwd: rootDir,
41
+ stdio: "inherit",
42
+ env: {
43
+ ...process.env,
44
+ NODE_ENV: process.env.NODE_ENV ?? "development",
45
+ FORCE_COLOR: "1"
46
+ }
47
+ }
48
+ ).catch((err) => {
49
+ if (err.signal === "SIGINT" || err.signal === "SIGTERM") {
50
+ process.exit(0);
51
+ }
52
+ throw err;
53
+ });
54
+ process.exit(result.exitCode ?? 0);
55
+ }
56
+
57
+ // src/commands/build.ts
58
+ import chalk2 from "chalk";
59
+ import { execa as execa2 } from "execa";
60
+ async function runBuild() {
61
+ const rootDir = findProjectRoot();
62
+ if (!rootDir) {
63
+ console.error(chalk2.red(" \u2716 Could not find meridian.config.ts. Are you inside a Meridian project?"));
64
+ process.exit(1);
65
+ }
66
+ console.log(chalk2.dim(" \u2192 Type-checking project..."));
67
+ console.log();
68
+ const result = await execa2("npx", ["tsc", "--noEmit"], {
69
+ cwd: rootDir,
70
+ stdio: "inherit"
71
+ }).catch((err) => err);
72
+ if (result.exitCode !== 0) {
73
+ console.log();
74
+ console.error(chalk2.red(" \u2716 Type check failed"));
75
+ process.exit(result.exitCode ?? 1);
76
+ }
77
+ console.log();
78
+ console.log(chalk2.green(" \u2713 Type check passed"));
79
+ }
80
+
81
+ // src/commands/db-migrate.ts
82
+ import path2 from "path";
83
+ import chalk3 from "chalk";
84
+ import ora from "ora";
85
+ import { execa as execa3 } from "execa";
86
+ async function runDbMigrate() {
87
+ const rootDir = findProjectRoot();
88
+ if (!rootDir) {
89
+ console.error(chalk3.red(" \u2716 Could not find meridian.config.ts. Are you inside a Meridian project?"));
90
+ process.exit(1);
91
+ }
92
+ const spinner = ora("Synchronizing database schema...").start();
93
+ const script = `
94
+ import { bootstrap } from "@meridianjs/framework"
95
+ import { fileURLToPath } from "node:url"
96
+ import path from "node:path"
97
+
98
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
99
+
100
+ const app = await bootstrap({ rootDir: ${JSON.stringify(rootDir)} })
101
+ // Schema sync already ran during module loading above.
102
+ // Don't start the HTTP server \u2014 just close all connections.
103
+ await app.stop()
104
+ process.exit(0)
105
+ `;
106
+ const scriptPath = path2.join(rootDir, ".meridian-migrate-tmp.mjs");
107
+ const { writeFile: writeFile2, unlink } = await import("fs/promises");
108
+ await writeFile2(scriptPath, script, "utf-8");
109
+ try {
110
+ await execa3("node", ["--import", "tsx/esm", scriptPath], {
111
+ cwd: rootDir,
112
+ stdio: "pipe",
113
+ env: { ...process.env, NODE_ENV: "development" }
114
+ });
115
+ spinner.succeed("Database schema synchronized");
116
+ } catch (err) {
117
+ spinner.fail("Schema migration failed");
118
+ console.error();
119
+ if (err.stderr) console.error(chalk3.red(err.stderr));
120
+ if (err.stdout) console.error(err.stdout);
121
+ process.exit(1);
122
+ } finally {
123
+ await unlink(scriptPath).catch(() => null);
124
+ }
125
+ }
126
+
127
+ // src/commands/db-generate.ts
128
+ import path3 from "path";
129
+ import fs from "fs/promises";
130
+ import chalk4 from "chalk";
131
+ async function runDbGenerate(migrationName) {
132
+ const rootDir = findProjectRoot();
133
+ if (!rootDir) {
134
+ console.error(chalk4.red(" \u2716 Could not find meridian.config.ts. Are you inside a Meridian project?"));
135
+ process.exit(1);
136
+ }
137
+ if (!migrationName) {
138
+ console.error(chalk4.red(" \u2716 Migration name is required. Usage: meridian db:generate <name>"));
139
+ process.exit(1);
140
+ }
141
+ const safeName = migrationName.toLowerCase().replace(/[^a-z0-9_]/g, "_").replace(/_+/g, "_");
142
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:T.Z]/g, "").slice(0, 14);
143
+ const fileName = `${timestamp}_${safeName}.ts`;
144
+ const migrationsDir = path3.join(rootDir, "src", "migrations");
145
+ await fs.mkdir(migrationsDir, { recursive: true });
146
+ const content = `/**
147
+ * Migration: ${safeName}
148
+ * Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}
149
+ *
150
+ * This file is a placeholder. Meridian uses MikroORM's updateSchema() by default.
151
+ * For custom migration SQL, implement the up() and down() methods below and
152
+ * run them via a custom script calling \`em.getMigrator().up()\`.
153
+ */
154
+
155
+ export async function up(): Promise<void> {
156
+ // Write your migration SQL here
157
+ }
158
+
159
+ export async function down(): Promise<void> {
160
+ // Write your rollback SQL here
161
+ }
162
+ `;
163
+ const filePath = path3.join(migrationsDir, fileName);
164
+ await fs.writeFile(filePath, content, "utf-8");
165
+ console.log(chalk4.green(` \u2713 Created migration: src/migrations/${fileName}`));
166
+ console.log();
167
+ console.log(chalk4.dim(" Note: Meridian auto-syncs schema on startup via updateSchema()."));
168
+ console.log(chalk4.dim(" Use this file for custom DDL that requires manual control."));
169
+ }
170
+
171
+ // src/commands/generate/module.ts
172
+ import path4 from "path";
173
+ import { existsSync as existsSync2 } from "fs";
174
+ import chalk5 from "chalk";
175
+ async function generateModule(name) {
176
+ const rootDir = findProjectRoot();
177
+ if (!rootDir) {
178
+ console.error(chalk5.red(" \u2716 Could not find meridian.config.ts. Are you inside a Meridian project?"));
179
+ process.exit(1);
180
+ }
181
+ if (!name) {
182
+ console.error(chalk5.red(" \u2716 Module name is required. Usage: meridian generate module <name>"));
183
+ process.exit(1);
184
+ }
185
+ const kebab = toKebabCase(name);
186
+ const pascal = toPascalCase(kebab);
187
+ const moduleDir = path4.join(rootDir, "src", "modules", kebab);
188
+ if (existsSync2(moduleDir)) {
189
+ console.error(chalk5.red(` \u2716 Module directory already exists: src/modules/${kebab}`));
190
+ process.exit(1);
191
+ }
192
+ await writeFile(path4.join(moduleDir, "index.ts"), renderModuleIndex(kebab, pascal));
193
+ await writeFile(path4.join(moduleDir, `models/${kebab}.ts`), renderModuleModel(kebab, pascal));
194
+ await writeFile(path4.join(moduleDir, `loaders/default.ts`), renderModuleLoader(kebab, pascal));
195
+ await writeFile(path4.join(moduleDir, "service.ts"), renderModuleService(kebab, pascal));
196
+ console.log(chalk5.green(` \u2713 Generated module: src/modules/${kebab}/`));
197
+ console.log();
198
+ console.log(" Files created:");
199
+ console.log(chalk5.dim(` src/modules/${kebab}/index.ts`));
200
+ console.log(chalk5.dim(` src/modules/${kebab}/models/${kebab}.ts`));
201
+ console.log(chalk5.dim(` src/modules/${kebab}/loaders/default.ts`));
202
+ console.log(chalk5.dim(` src/modules/${kebab}/service.ts`));
203
+ console.log();
204
+ console.log(" Next steps:");
205
+ console.log(chalk5.dim(` 1. Add the module to your meridian.config.ts:`));
206
+ console.log(chalk5.dim(` { resolve: "./src/modules/${kebab}" }`));
207
+ console.log(chalk5.dim(` 2. Run \`npm run db:migrate\` to sync the schema`));
208
+ }
209
+
210
+ // src/commands/generate/workflow.ts
211
+ import path5 from "path";
212
+ import { existsSync as existsSync3 } from "fs";
213
+ import chalk6 from "chalk";
214
+ async function generateWorkflow(name) {
215
+ const rootDir = findProjectRoot();
216
+ if (!rootDir) {
217
+ console.error(chalk6.red(" \u2716 Could not find meridian.config.ts. Are you inside a Meridian project?"));
218
+ process.exit(1);
219
+ }
220
+ if (!name) {
221
+ console.error(chalk6.red(" \u2716 Workflow name is required. Usage: meridian generate workflow <name>"));
222
+ process.exit(1);
223
+ }
224
+ const kebab = toKebabCase(name);
225
+ const pascal = toPascalCase(kebab);
226
+ const filePath = path5.join(rootDir, "src", "workflows", `${kebab}.ts`);
227
+ if (existsSync3(filePath)) {
228
+ console.error(chalk6.red(` \u2716 Workflow already exists: src/workflows/${kebab}.ts`));
229
+ process.exit(1);
230
+ }
231
+ const camel = pascal.charAt(0).toLowerCase() + pascal.slice(1);
232
+ await writeFile(filePath, renderWorkflow(kebab, pascal));
233
+ console.log(chalk6.green(` \u2713 Generated workflow: src/workflows/${kebab}.ts`));
234
+ console.log();
235
+ console.log(" Usage:");
236
+ console.log(chalk6.dim(` import { ${camel}Workflow } from "./workflows/${kebab}.js"`));
237
+ console.log(chalk6.dim(` const { result } = await ${camel}Workflow(req.scope).run({ input: {...} })`));
238
+ }
239
+
240
+ // src/commands/generate/plugin.ts
241
+ import path6 from "path";
242
+ import { existsSync as existsSync4 } from "fs";
243
+ import chalk7 from "chalk";
244
+ function renderPluginIndex(_name, pascalName) {
245
+ return `import { fileURLToPath } from "node:url"
246
+ import path from "node:path"
247
+ import type { PluginRegistrationContext } from "@meridianjs/types"
248
+
249
+ /**
250
+ * The compiled directory of this plugin.
251
+ * The Meridian plugin-loader uses this to auto-scan api/, subscribers/, jobs/.
252
+ */
253
+ export const pluginRoot: string = path.dirname(fileURLToPath(import.meta.url))
254
+
255
+ /**
256
+ * Called during bootstrap before route/subscriber auto-scan.
257
+ * Use ctx.addModule() to register domain modules.
258
+ */
259
+ export default async function register(_ctx: PluginRegistrationContext): Promise<void> {
260
+ // Example: register a custom module
261
+ // await ctx.addModule({ resolve: My${pascalName}Module })
262
+ }
263
+ `;
264
+ }
265
+ function renderPluginRoute(name) {
266
+ return `import type { Response } from "express"
267
+
268
+ /**
269
+ * GET /admin/${name}
270
+ * Example plugin admin route.
271
+ */
272
+ export const GET = async (req: any, res: Response) => {
273
+ res.json({ plugin: "${name}", status: "active" })
274
+ }
275
+ `;
276
+ }
277
+ async function generatePlugin(name) {
278
+ const rootDir = findProjectRoot();
279
+ if (!rootDir) {
280
+ console.error(chalk7.red(" \u2716 Could not find meridian.config.ts. Are you inside a Meridian project?"));
281
+ process.exit(1);
282
+ }
283
+ if (!name) {
284
+ console.error(chalk7.red(" \u2716 Plugin name is required. Usage: meridian generate plugin <name>"));
285
+ process.exit(1);
286
+ }
287
+ const kebab = toKebabCase(name);
288
+ const pascal = toPascalCase(kebab);
289
+ const pluginDir = path6.join(rootDir, "src", "plugins", kebab);
290
+ if (existsSync4(pluginDir)) {
291
+ console.error(chalk7.red(` \u2716 Plugin directory already exists: src/plugins/${kebab}`));
292
+ process.exit(1);
293
+ }
294
+ await writeFile(path6.join(pluginDir, "index.ts"), renderPluginIndex(kebab, pascal));
295
+ await writeFile(
296
+ path6.join(pluginDir, "api", "admin", kebab, "route.ts"),
297
+ renderPluginRoute(kebab)
298
+ );
299
+ console.log(chalk7.green(` \u2713 Generated plugin: src/plugins/${kebab}/`));
300
+ console.log();
301
+ console.log(" Files created:");
302
+ console.log(chalk7.dim(` src/plugins/${kebab}/index.ts`));
303
+ console.log(chalk7.dim(` src/plugins/${kebab}/api/admin/${kebab}/route.ts`));
304
+ console.log();
305
+ console.log(" Next steps:");
306
+ console.log(chalk7.dim(` 1. Add the plugin to your meridian.config.ts:`));
307
+ console.log(chalk7.dim(` plugins: [{ resolve: "./src/plugins/${kebab}" }]`));
308
+ console.log(chalk7.dim(` 2. Start the dev server: \`npm run dev\``));
309
+ }
310
+
311
+ // src/cli.ts
312
+ var program = new Command();
313
+ program.name("meridian").description("Meridian project management framework CLI").version("0.1.0");
314
+ program.command("new [project-name]").description("Create a new Meridian project").action((projectName) => {
315
+ runNew(projectName).catch((err) => {
316
+ console.error(err);
317
+ process.exit(1);
318
+ });
319
+ });
320
+ program.command("dev").description("Start the development server").action(() => {
321
+ runDev().catch((err) => {
322
+ console.error(err);
323
+ process.exit(1);
324
+ });
325
+ });
326
+ program.command("build").description("Type-check the project").action(() => {
327
+ runBuild().catch((err) => {
328
+ console.error(err);
329
+ process.exit(1);
330
+ });
331
+ });
332
+ program.command("db:migrate").description("Synchronize the database schema (runs updateSchema on all modules)").action(() => {
333
+ runDbMigrate().catch((err) => {
334
+ console.error(err);
335
+ process.exit(1);
336
+ });
337
+ });
338
+ program.command("db:generate <name>").description("Generate a new migration file").action((name) => {
339
+ runDbGenerate(name).catch((err) => {
340
+ console.error(err);
341
+ process.exit(1);
342
+ });
343
+ });
344
+ var generateCmd = program.command("generate").alias("g").description("Generate boilerplate files");
345
+ generateCmd.command("module <name>").description("Scaffold a new module in src/modules/").action((name) => {
346
+ generateModule(name).catch((err) => {
347
+ console.error(err);
348
+ process.exit(1);
349
+ });
350
+ });
351
+ generateCmd.command("workflow <name>").description("Scaffold a new workflow in src/workflows/").action((name) => {
352
+ generateWorkflow(name).catch((err) => {
353
+ console.error(err);
354
+ process.exit(1);
355
+ });
356
+ });
357
+ generateCmd.command("plugin <name>").description("Scaffold a local plugin in src/plugins/").action((name) => {
358
+ generatePlugin(name).catch((err) => {
359
+ console.error(err);
360
+ process.exit(1);
361
+ });
362
+ });
363
+ program.parse(process.argv);
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ runNew
4
+ } from "./chunk-7IUTX6RW.js";
5
+
6
+ // src/index.ts
7
+ var projectName = process.argv[2];
8
+ var name = projectName && !projectName.startsWith("-") ? projectName : void 0;
9
+ runNew(name).catch((err) => {
10
+ console.error(err);
11
+ process.exit(1);
12
+ });
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "create-meridian-app",
3
+ "version": "0.1.0",
4
+ "description": "Create a new Meridian project or manage an existing one",
5
+ "main": "./dist/index.js",
6
+ "type": "module",
7
+ "exports": {
8
+ ".": "./dist/index.js"
9
+ },
10
+ "bin": {
11
+ "create-meridian-app": "./dist/index.js",
12
+ "meridian": "./dist/cli.js"
13
+ },
14
+ "scripts": {
15
+ "build": "tsup src/index.ts src/cli.ts --format esm --dts --clean",
16
+ "dev": "tsup src/index.ts src/cli.ts --format esm --dts --watch",
17
+ "typecheck": "tsc --noEmit",
18
+ "clean": "rm -rf dist",
19
+ "prepublishOnly": "npm run build"
20
+ },
21
+ "dependencies": {
22
+ "commander": "^12.1.0",
23
+ "ora": "^8.1.1",
24
+ "chalk": "^5.3.0",
25
+ "prompts": "^2.4.2",
26
+ "execa": "^9.5.2"
27
+ },
28
+ "devDependencies": {
29
+ "@types/prompts": "^2.4.9",
30
+ "tsup": "^8.3.5",
31
+ "tsx": "4.21.0",
32
+ "typescript": "*"
33
+ },
34
+ "files": [
35
+ "dist"
36
+ ],
37
+ "engines": {
38
+ "node": ">=20.0.0"
39
+ }
40
+ }