@stratum-hq/create 0.2.3 → 0.3.1

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/dist/index.js CHANGED
@@ -1,16 +1,1183 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
+ import * as fs2 from "fs";
5
+ import * as path2 from "path";
6
+ import crypto2 from "node:crypto";
7
+ import { execSync as execSync2 } from "child_process";
8
+ import { fileURLToPath } from "url";
9
+
10
+ // src/matrix.ts
11
+ var POSTGRES_STRATEGIES = ["rls", "schema", "database"];
12
+ var POSTGRES_ORMS = ["prisma", "drizzle", "sequelize", "knex", "pg"];
13
+ var MONGODB_STRATEGIES = ["database", "collection"];
14
+ var MONGODB_ORMS = ["mongoose"];
15
+ var MYSQL_STRATEGIES = ["database", "table-prefix"];
16
+ var MYSQL_ORMS = ["sequelize", "knex", "pg"];
17
+ var ALL_FRAMEWORKS = ["express", "fastify", "nextjs", "hono", "nestjs", "none"];
18
+ var VALID_COMBINATIONS = {
19
+ postgres: {
20
+ strategies: POSTGRES_STRATEGIES,
21
+ orms: POSTGRES_ORMS,
22
+ frameworks: ALL_FRAMEWORKS
23
+ },
24
+ mongodb: {
25
+ strategies: MONGODB_STRATEGIES,
26
+ orms: MONGODB_ORMS,
27
+ frameworks: ALL_FRAMEWORKS
28
+ },
29
+ mysql: {
30
+ strategies: MYSQL_STRATEGIES,
31
+ orms: MYSQL_ORMS,
32
+ frameworks: ALL_FRAMEWORKS
33
+ }
34
+ };
35
+ var ALL_DATABASES = ["postgres", "mongodb", "mysql"];
36
+ var ALL_STRATEGIES = ["rls", "schema", "database", "collection", "table-prefix"];
37
+ var ALL_ORMS = ["prisma", "drizzle", "sequelize", "knex", "mongoose", "pg"];
38
+ function isValidPreset(preset) {
39
+ const config = VALID_COMBINATIONS[preset.database];
40
+ if (!config)
41
+ return false;
42
+ if (!config.strategies.includes(preset.strategy))
43
+ return false;
44
+ if (!config.orms.includes(preset.orm))
45
+ return false;
46
+ if (!config.frameworks.includes(preset.framework))
47
+ return false;
48
+ return true;
49
+ }
50
+ function parsePresetString(s) {
51
+ if (!s || typeof s !== "string")
52
+ return null;
53
+ const parts = s.toLowerCase().split("-");
54
+ let database;
55
+ let strategy;
56
+ let orm;
57
+ let framework;
58
+ if (parts.length === 4) {
59
+ [database, strategy, orm, framework] = parts;
60
+ } else if (parts.length === 5) {
61
+ database = parts[0];
62
+ strategy = `${parts[1]}-${parts[2]}`;
63
+ orm = parts[3];
64
+ framework = parts[4];
65
+ } else {
66
+ return null;
67
+ }
68
+ if (!ALL_DATABASES.includes(database))
69
+ return null;
70
+ if (!ALL_STRATEGIES.includes(strategy))
71
+ return null;
72
+ if (!ALL_ORMS.includes(orm))
73
+ return null;
74
+ if (!ALL_FRAMEWORKS.includes(framework))
75
+ return null;
76
+ return {
77
+ database,
78
+ strategy,
79
+ orm,
80
+ framework
81
+ };
82
+ }
83
+
84
+ // src/preset-project.ts
4
85
  import * as fs from "fs";
5
86
  import * as path from "path";
87
+ import crypto from "node:crypto";
6
88
  import { execSync } from "child_process";
7
- import { fileURLToPath } from "url";
89
+
90
+ // src/generators/docker-compose.ts
91
+ function generatePresetDockerCompose(projectName, preset) {
92
+ const dbName = projectName.replace(/[^a-z0-9]/gi, "_").toLowerCase();
93
+ switch (preset.database) {
94
+ case "postgres":
95
+ return generatePostgresCompose(projectName, dbName);
96
+ case "mongodb":
97
+ return generateMongoCompose(projectName, dbName);
98
+ case "mysql":
99
+ return generateMysqlCompose(projectName, dbName);
100
+ }
101
+ }
102
+ function generatePostgresCompose(projectName, dbName) {
103
+ return `# Docker Compose for ${projectName}
104
+ # Start with: docker compose up -d
105
+ version: "3.8"
106
+
107
+ services:
108
+ db:
109
+ image: postgres:16-alpine
110
+ environment:
111
+ POSTGRES_DB: ${dbName}
112
+ POSTGRES_USER: ${dbName}
113
+ POSTGRES_PASSWORD: dev_password
114
+ ports:
115
+ - "5432:5432"
116
+ volumes:
117
+ - db_data:/var/lib/postgresql/data
118
+ - ./init.sql:/docker-entrypoint-initdb.d/init.sql
119
+ healthcheck:
120
+ test: ["CMD-SHELL", "pg_isready -U ${dbName}"]
121
+ interval: 5s
122
+ timeout: 5s
123
+ retries: 5
124
+
125
+ volumes:
126
+ db_data:
127
+ `;
128
+ }
129
+ function generateMongoCompose(projectName, dbName) {
130
+ return `# Docker Compose for ${projectName}
131
+ # Start with: docker compose up -d
132
+ version: "3.8"
133
+
134
+ services:
135
+ db:
136
+ image: mongo:7
137
+ environment:
138
+ MONGO_INITDB_ROOT_USERNAME: ${dbName}
139
+ MONGO_INITDB_ROOT_PASSWORD: dev_password
140
+ MONGO_INITDB_DATABASE: ${dbName}
141
+ ports:
142
+ - "27017:27017"
143
+ volumes:
144
+ - db_data:/data/db
145
+ healthcheck:
146
+ test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"]
147
+ interval: 5s
148
+ timeout: 5s
149
+ retries: 5
150
+
151
+ volumes:
152
+ db_data:
153
+ `;
154
+ }
155
+ function generateMysqlCompose(projectName, dbName) {
156
+ return `# Docker Compose for ${projectName}
157
+ # Start with: docker compose up -d
158
+ version: "3.8"
159
+
160
+ services:
161
+ db:
162
+ image: mysql:8
163
+ environment:
164
+ MYSQL_DATABASE: ${dbName}
165
+ MYSQL_USER: ${dbName}
166
+ MYSQL_PASSWORD: dev_password
167
+ MYSQL_ROOT_PASSWORD: dev_root_password
168
+ ports:
169
+ - "3306:3306"
170
+ volumes:
171
+ - db_data:/var/lib/mysql
172
+ - ./init.sql:/docker-entrypoint-initdb.d/init.sql
173
+ healthcheck:
174
+ test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
175
+ interval: 5s
176
+ timeout: 5s
177
+ retries: 5
178
+
179
+ volumes:
180
+ db_data:
181
+ `;
182
+ }
183
+
184
+ // src/generators/init-sql.ts
185
+ function generatePresetInitSql(projectName, preset) {
186
+ const dbName = projectName.replace(/[^a-z0-9]/gi, "_").toLowerCase();
187
+ switch (preset.database) {
188
+ case "postgres":
189
+ return generatePostgresInit(projectName, dbName, preset.strategy);
190
+ case "mongodb":
191
+ return null;
192
+ case "mysql":
193
+ return generateMysqlInit(projectName, dbName);
194
+ }
195
+ }
196
+ function generatePostgresInit(projectName, dbName, strategy) {
197
+ let rlsBlock = "";
198
+ if (strategy === "rls") {
199
+ rlsBlock = `
200
+ -- Enable Row-Level Security for tenant isolation
201
+ -- Add RLS policies to each tenant-scoped table:
202
+ --
203
+ -- ALTER TABLE your_table ENABLE ROW LEVEL SECURITY;
204
+ -- CREATE POLICY tenant_isolation ON your_table
205
+ -- USING (tenant_id = current_setting('app.current_tenant')::uuid);
206
+ --
207
+ -- The Stratum db-adapters package sets app.current_tenant automatically.
208
+ `;
209
+ }
210
+ return `-- Initialize ${projectName} database
211
+ -- Enable required extensions for Stratum multi-tenancy
212
+ CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
213
+ CREATE EXTENSION IF NOT EXISTS "ltree";
214
+
215
+ -- The ltree extension enables hierarchical tenant trees
216
+ -- uuid-ossp provides uuid_generate_v4() for tenant IDs
217
+ COMMENT ON DATABASE ${dbName} IS 'Multi-tenant database for ${projectName}';
218
+ ${rlsBlock}`;
219
+ }
220
+ function generateMysqlInit(projectName, dbName) {
221
+ return `-- Initialize ${projectName} database
222
+ -- MySQL setup for Stratum multi-tenancy
223
+
224
+ -- Ensure utf8mb4 for the database
225
+ ALTER DATABASE ${dbName} CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
226
+
227
+ -- Tenant metadata table
228
+ CREATE TABLE IF NOT EXISTS _stratum_tenants (
229
+ id VARCHAR(36) PRIMARY KEY,
230
+ name VARCHAR(255) NOT NULL,
231
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
232
+ );
233
+ `;
234
+ }
235
+
236
+ // src/generators/db-setup.ts
237
+ function generateDbSetup(preset) {
238
+ switch (preset.orm) {
239
+ case "prisma":
240
+ return generatePrismaSetup(preset);
241
+ case "drizzle":
242
+ return generateDrizzleSetup(preset);
243
+ case "sequelize":
244
+ return generateSequelizeSetup(preset);
245
+ case "knex":
246
+ return generateKnexSetup(preset);
247
+ case "mongoose":
248
+ return generateMongooseSetup(preset);
249
+ case "pg":
250
+ return generatePgSetup(preset);
251
+ }
252
+ }
253
+ function generatePrismaSetup(preset) {
254
+ const provider = preset.database === "mysql" ? "mysql" : "postgresql";
255
+ const envUrl = preset.database === "mysql" ? "mysql://user:password@localhost:3306/dbname" : "postgres://user:password@localhost:5432/dbname";
256
+ return [
257
+ {
258
+ filename: "prisma/schema.prisma",
259
+ content: `// Prisma schema for Stratum multi-tenancy
260
+ generator client {
261
+ provider = "prisma-client-js"
262
+ }
263
+
264
+ datasource db {
265
+ provider = "${provider}"
266
+ url = env("DATABASE_URL")
267
+ }
268
+
269
+ model Tenant {
270
+ id String @id @default(uuid())
271
+ name String
272
+ createdAt DateTime @default(now()) @map("created_at")
273
+
274
+ @@map("tenants")
275
+ }
276
+
277
+ // Add your tenant-scoped models here.
278
+ // Each model that needs tenant isolation should have a tenantId field.
279
+ `
280
+ },
281
+ {
282
+ filename: "src/stratum-prisma.ts",
283
+ content: `// Prisma client with Stratum tenant-scoped queries
284
+ import { PrismaClient } from "@prisma/client";
285
+ import { Pool } from "pg";
286
+ import { prismaWithTenant } from "@stratum-hq/db-adapters";
287
+
288
+ const prisma = new PrismaClient();
289
+ const pool = new Pool({
290
+ connectionString: process.env.DATABASE_URL,
291
+ });
292
+
293
+ // Create a tenant-scoped Prisma client.
294
+ // All queries through this client are automatically filtered by RLS.
295
+ export function createTenantPrisma(getTenantId: () => string) {
296
+ return prismaWithTenant(prisma, getTenantId, pool);
297
+ }
298
+
299
+ // Usage:
300
+ // const tenantPrisma = createTenantPrisma(() => currentTenantId);
301
+ // const orders = await tenantPrisma.order.findMany();
302
+
303
+ export { prisma, pool };
304
+ `
305
+ }
306
+ ];
307
+ }
308
+ function generateDrizzleSetup(preset) {
309
+ const files = [];
310
+ if (preset.database === "mysql") {
311
+ files.push({
312
+ filename: "src/stratum-drizzle.ts",
313
+ content: `// Drizzle ORM with Stratum tenant-scoped queries (MySQL)
314
+ import { drizzle } from "drizzle-orm/mysql2";
315
+ import mysql from "mysql2/promise";
316
+
317
+ const connection = await mysql.createConnection({
318
+ uri: process.env.DATABASE_URL,
319
+ });
320
+
321
+ export const db = drizzle(connection);
322
+
323
+ // For tenant scoping, prefix table access with the tenant database name
324
+ // or use the @stratum-hq/mysql adapter for automatic routing.
325
+ `
326
+ });
327
+ } else {
328
+ files.push({
329
+ filename: "src/stratum-drizzle.ts",
330
+ content: `// Drizzle ORM with Stratum tenant-scoped queries
331
+ import { drizzle } from "drizzle-orm/node-postgres";
332
+ import { Pool } from "pg";
333
+ import { drizzleWithTenant } from "@stratum-hq/db-adapters";
334
+
335
+ const pool = new Pool({
336
+ connectionString: process.env.DATABASE_URL,
337
+ });
338
+
339
+ export const db = drizzle(pool);
340
+
341
+ // Create a tenant-scoped Drizzle instance.
342
+ // Queries are automatically filtered by RLS policy.
343
+ export function createTenantDb(getTenantId: () => string) {
344
+ return drizzleWithTenant(db, getTenantId, pool);
345
+ }
346
+
347
+ // Usage:
348
+ // const tenantDb = createTenantDb(() => currentTenantId);
349
+ // const rows = await tenantDb.select().from(orders);
350
+
351
+ export { pool };
352
+ `
353
+ });
354
+ }
355
+ files.push({
356
+ filename: "drizzle.config.ts",
357
+ content: `import type { Config } from "drizzle-kit";
358
+
359
+ export default {
360
+ schema: "./src/schema.ts",
361
+ out: "./drizzle",
362
+ ${preset.database === "mysql" ? 'dialect: "mysql",' : 'dialect: "postgresql",'}
363
+ dbCredentials: {
364
+ url: process.env.DATABASE_URL!,
365
+ },
366
+ } satisfies Config;
367
+ `
368
+ });
369
+ return files;
370
+ }
371
+ function generateSequelizeSetup(preset) {
372
+ const dialect = preset.database === "mysql" ? "mysql" : "postgres";
373
+ return [
374
+ {
375
+ filename: "src/stratum-sequelize.ts",
376
+ content: `// Sequelize with Stratum tenant-scoped queries
377
+ import { Sequelize, DataTypes, Model } from "sequelize";
378
+
379
+ const sequelize = new Sequelize(process.env.DATABASE_URL!, {
380
+ dialect: "${dialect}",
381
+ logging: false,
382
+ });
383
+
384
+ // Define a tenant-scoped model example
385
+ class Tenant extends Model {}
386
+ Tenant.init(
387
+ {
388
+ id: { type: DataTypes.UUID, primaryKey: true, defaultValue: DataTypes.UUIDV4 },
389
+ name: { type: DataTypes.STRING, allowNull: false },
390
+ },
391
+ { sequelize, tableName: "tenants", underscored: true },
392
+ );
393
+
394
+ // For tenant isolation, add a tenantId scope to each model:
395
+ // MyModel.addScope("tenant", (tenantId) => ({ where: { tenant_id: tenantId } }));
396
+ // const rows = await MyModel.scope({ method: ["tenant", currentTenantId] }).findAll();
397
+
398
+ export { sequelize, Tenant };
399
+ `
400
+ }
401
+ ];
402
+ }
403
+ function generateKnexSetup(preset) {
404
+ const client = preset.database === "mysql" ? "mysql2" : "pg";
405
+ return [
406
+ {
407
+ filename: "knexfile.ts",
408
+ content: `// Knex configuration for Stratum
409
+ import type { Knex } from "knex";
410
+
411
+ const config: Knex.Config = {
412
+ client: "${client}",
413
+ connection: process.env.DATABASE_URL,
414
+ migrations: {
415
+ directory: "./migrations",
416
+ extension: "ts",
417
+ },
418
+ };
419
+
420
+ export default config;
421
+ `
422
+ },
423
+ {
424
+ filename: "src/stratum-knex.ts",
425
+ content: `// Knex with Stratum tenant-scoped queries
426
+ import Knex from "knex";
427
+ import config from "../knexfile.js";
428
+
429
+ const knex = Knex(config);
430
+
431
+ // Create a tenant-scoped query builder.
432
+ // For RLS strategy, set the session variable before queries.
433
+ export async function withTenantScope(tenantId: string, fn: (db: typeof knex) => Promise<void>) {
434
+ ${preset.database === "postgres" ? ` await knex.raw("SET app.current_tenant = ?", [tenantId]);` : ` // For ${preset.database}, scope queries by tenant_id column`}
435
+ try {
436
+ await fn(knex);
437
+ } finally {
438
+ ${preset.database === "postgres" ? ` await knex.raw("RESET app.current_tenant");` : ` // Scope cleanup not needed for column-based isolation`}
439
+ }
440
+ }
441
+
442
+ export { knex };
443
+ `
444
+ }
445
+ ];
446
+ }
447
+ function generateMongooseSetup(_preset) {
448
+ return [
449
+ {
450
+ filename: "src/stratum-mongoose.ts",
451
+ content: `// Mongoose with Stratum multi-tenant support
452
+ import mongoose from "mongoose";
453
+ import { createTenantConnection } from "@stratum-hq/mongodb";
454
+
455
+ // Main connection (used for tenant metadata)
456
+ const mainConnection = mongoose.createConnection(
457
+ process.env.MONGODB_URI || "mongodb://localhost:27017/main",
458
+ );
459
+
460
+ // Create a tenant-scoped connection.
461
+ // Each tenant gets its own database (database-per-tenant strategy)
462
+ // or collection prefix (collection strategy).
463
+ export function getTenantConnection(tenantId: string) {
464
+ return createTenantConnection(mainConnection, tenantId);
465
+ }
466
+
467
+ // Define schemas that work across tenant connections
468
+ export const TenantSchema = new mongoose.Schema({
469
+ name: { type: String, required: true },
470
+ createdAt: { type: Date, default: Date.now },
471
+ });
472
+
473
+ // Usage:
474
+ // const conn = getTenantConnection("tenant-abc");
475
+ // const Order = conn.model("Order", OrderSchema);
476
+ // const orders = await Order.find();
477
+
478
+ export { mainConnection };
479
+ `
480
+ }
481
+ ];
482
+ }
483
+ function generatePgSetup(preset) {
484
+ if (preset.database === "mysql") {
485
+ return [
486
+ {
487
+ filename: "src/stratum-db.ts",
488
+ content: `// Raw MySQL client with Stratum tenant-scoped queries
489
+ import mysql from "mysql2/promise";
490
+
491
+ const pool = mysql.createPool({
492
+ uri: process.env.DATABASE_URL,
493
+ });
494
+
495
+ // Execute a query scoped to a tenant by filtering on tenant_id
496
+ export async function tenantQuery(tenantId: string, sql: string, params: unknown[] = []) {
497
+ const [rows] = await pool.execute(sql, [...params, tenantId]);
498
+ return rows;
499
+ }
500
+
501
+ // Usage:
502
+ // const orders = await tenantQuery("tenant-abc", "SELECT * FROM orders WHERE tenant_id = ?");
503
+
504
+ export { pool };
505
+ `
506
+ }
507
+ ];
508
+ }
509
+ return [
510
+ {
511
+ filename: "src/stratum-db.ts",
512
+ content: `// PostgreSQL client with Stratum tenant-scoped queries
513
+ import { Pool } from "pg";
514
+ import { createTenantPool } from "@stratum-hq/db-adapters";
515
+
516
+ const pool = new Pool({
517
+ connectionString: process.env.DATABASE_URL,
518
+ });
519
+
520
+ // Create a tenant-scoped pool.
521
+ // Sets app.current_tenant on each connection for RLS enforcement.
522
+ export function getTenantPool(tenantId: string) {
523
+ return createTenantPool(pool, tenantId);
524
+ }
525
+
526
+ // Usage:
527
+ // const tenantPool = getTenantPool("tenant-abc");
528
+ // const { rows } = await tenantPool.query("SELECT * FROM orders");
529
+
530
+ export { pool };
531
+ `
532
+ }
533
+ ];
534
+ }
535
+
536
+ // src/generators/middleware.ts
537
+ function generateMiddleware(projectName, preset) {
538
+ switch (preset.framework) {
539
+ case "express":
540
+ return generateExpressMiddleware(projectName);
541
+ case "fastify":
542
+ return generateFastifyMiddleware(projectName);
543
+ case "nextjs":
544
+ return generateNextjsMiddleware(projectName);
545
+ case "hono":
546
+ return generateHonoMiddleware(projectName);
547
+ case "nestjs":
548
+ return generateNestjsMiddleware(projectName);
549
+ case "none":
550
+ return generateNoFramework(projectName);
551
+ }
552
+ }
553
+ function generateExpressMiddleware(projectName) {
554
+ return [
555
+ {
556
+ filename: "src/index.ts",
557
+ content: `import express from "express";
558
+
559
+ const app = express();
560
+ const port = Number(process.env.PORT) || 3000;
561
+
562
+ app.use(express.json());
563
+
564
+ // Tenant extraction middleware
565
+ app.use((req, _res, next) => {
566
+ const tenantId =
567
+ req.headers["x-tenant-id"] as string ||
568
+ req.hostname.split(".")[0];
569
+ (req as any).tenantId = tenantId;
570
+ next();
571
+ });
572
+
573
+ app.get("/health", (_req, res) => {
574
+ res.json({ status: "ok", project: "${projectName}" });
575
+ });
576
+
577
+ app.get("/tenants", async (req, res) => {
578
+ const tenantId = (req as any).tenantId;
579
+ res.json({ tenantId, message: "Replace with your tenant queries" });
580
+ });
581
+
582
+ app.listen(port, () => {
583
+ console.log(\`${projectName} running on http://localhost:\${port}\`);
584
+ });
585
+ `
586
+ }
587
+ ];
588
+ }
589
+ function generateFastifyMiddleware(projectName) {
590
+ return [
591
+ {
592
+ filename: "src/index.ts",
593
+ content: `import Fastify from "fastify";
594
+
595
+ const fastify = Fastify({ logger: true });
596
+ const port = Number(process.env.PORT) || 3000;
597
+
598
+ // Tenant extraction plugin
599
+ fastify.decorateRequest("tenantId", "");
600
+ fastify.addHook("onRequest", async (request) => {
601
+ const tenantId =
602
+ (request.headers["x-tenant-id"] as string) ||
603
+ (request.hostname?.split(".")[0] ?? "");
604
+ (request as any).tenantId = tenantId;
605
+ });
606
+
607
+ fastify.get("/health", async () => {
608
+ return { status: "ok", project: "${projectName}" };
609
+ });
610
+
611
+ fastify.get("/tenants", async (request) => {
612
+ const tenantId = (request as any).tenantId;
613
+ return { tenantId, message: "Replace with your tenant queries" };
614
+ });
615
+
616
+ fastify.listen({ port, host: "0.0.0.0" }, (err) => {
617
+ if (err) {
618
+ fastify.log.error(err);
619
+ process.exit(1);
620
+ }
621
+ });
622
+ `
623
+ }
624
+ ];
625
+ }
626
+ function generateNextjsMiddleware(projectName) {
627
+ return [
628
+ {
629
+ filename: "middleware.ts",
630
+ content: `// Next.js edge middleware for tenant resolution
631
+ import { NextRequest, NextResponse } from "next/server";
632
+
633
+ export function middleware(request: NextRequest) {
634
+ const hostname = request.headers.get("host") || "";
635
+ const subdomain = hostname.split(".")[0];
636
+ const headerTenantId = request.headers.get("x-tenant-id");
637
+ const pathTenantId = request.nextUrl.pathname.match(/^\\/tenant\\/([^/]+)/)?.[1];
638
+
639
+ const tenantId = headerTenantId || pathTenantId || subdomain;
640
+
641
+ const requestHeaders = new Headers(request.headers);
642
+ if (tenantId && tenantId !== "localhost" && tenantId !== "www") {
643
+ requestHeaders.set("x-tenant-id", tenantId);
644
+ }
645
+
646
+ return NextResponse.next({ request: { headers: requestHeaders } });
647
+ }
648
+
649
+ export const config = {
650
+ matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
651
+ };
652
+ `
653
+ },
654
+ {
655
+ filename: "src/app/page.tsx",
656
+ content: `// app/page.tsx - ${projectName} root page
657
+ export default function Home() {
658
+ return (
659
+ <main style={{ padding: "2rem", fontFamily: "sans-serif" }}>
660
+ <h1>${projectName}</h1>
661
+ <p>Multi-tenant app powered by Stratum.</p>
662
+ <ul>
663
+ <li>Configure tenants via the Stratum control plane</li>
664
+ <li>Access tenant context via <code>x-tenant-id</code> header</li>
665
+ <li>Use <code>@stratum-hq/lib</code> for tenant resolution</li>
666
+ </ul>
667
+ </main>
668
+ );
669
+ }
670
+ `
671
+ }
672
+ ];
673
+ }
674
+ function generateHonoMiddleware(projectName) {
675
+ return [
676
+ {
677
+ filename: "src/index.ts",
678
+ content: `import { Hono } from "hono";
679
+ import { serve } from "@hono/node-server";
680
+
681
+ const app = new Hono();
682
+
683
+ // Tenant extraction middleware
684
+ app.use("*", async (c, next) => {
685
+ const tenantId =
686
+ c.req.header("x-tenant-id") ||
687
+ new URL(c.req.url).hostname.split(".")[0];
688
+ c.set("tenantId", tenantId);
689
+ await next();
690
+ });
691
+
692
+ app.get("/health", (c) => {
693
+ return c.json({ status: "ok", project: "${projectName}" });
694
+ });
695
+
696
+ app.get("/tenants", (c) => {
697
+ const tenantId = c.get("tenantId");
698
+ return c.json({ tenantId, message: "Replace with your tenant queries" });
699
+ });
700
+
701
+ const port = Number(process.env.PORT) || 3000;
702
+ console.log(\`${projectName} running on http://localhost:\${port}\`);
703
+ serve({ fetch: app.fetch, port });
704
+ `
705
+ }
706
+ ];
707
+ }
708
+ function generateNestjsMiddleware(projectName) {
709
+ return [
710
+ {
711
+ filename: "src/main.ts",
712
+ content: `import { NestFactory } from "@nestjs/core";
713
+ import { AppModule } from "./app.module.js";
714
+
715
+ async function bootstrap() {
716
+ const app = await NestFactory.create(AppModule);
717
+ const port = Number(process.env.PORT) || 3000;
718
+ await app.listen(port);
719
+ console.log(\`${projectName} running on http://localhost:\${port}\`);
720
+ }
721
+ bootstrap();
722
+ `
723
+ },
724
+ {
725
+ filename: "src/app.module.ts",
726
+ content: `import { Module } from "@nestjs/common";
727
+ import { AppController } from "./app.controller.js";
728
+ import { TenantGuard } from "./tenant.guard.js";
729
+ import { APP_GUARD } from "@nestjs/core";
730
+
731
+ @Module({
732
+ controllers: [AppController],
733
+ providers: [
734
+ { provide: APP_GUARD, useClass: TenantGuard },
735
+ ],
736
+ })
737
+ export class AppModule {}
738
+ `
739
+ },
740
+ {
741
+ filename: "src/app.controller.ts",
742
+ content: `import { Controller, Get, Req } from "@nestjs/common";
743
+
744
+ @Controller()
745
+ export class AppController {
746
+ @Get("health")
747
+ health() {
748
+ return { status: "ok", project: "${projectName}" };
749
+ }
750
+
751
+ @Get("tenants")
752
+ tenants(@Req() req: any) {
753
+ return { tenantId: req.tenantId, message: "Replace with your tenant queries" };
754
+ }
755
+ }
756
+ `
757
+ },
758
+ {
759
+ filename: "src/tenant.guard.ts",
760
+ content: `import { Injectable, CanActivate, ExecutionContext } from "@nestjs/common";
761
+
762
+ @Injectable()
763
+ export class TenantGuard implements CanActivate {
764
+ canActivate(context: ExecutionContext): boolean {
765
+ const request = context.switchToHttp().getRequest();
766
+ const tenantId =
767
+ request.headers["x-tenant-id"] ||
768
+ request.hostname?.split(".")[0];
769
+ request.tenantId = tenantId || null;
770
+ return true;
771
+ }
772
+ }
773
+ `
774
+ }
775
+ ];
776
+ }
777
+ function generateNoFramework(projectName) {
778
+ return [
779
+ {
780
+ filename: "src/index.ts",
781
+ content: `// ${projectName} - Stratum multi-tenant setup (no framework)
782
+ // Use this as a starting point and integrate with your framework of choice.
783
+
784
+ console.log("${projectName} initialized");
785
+ console.log("Import your database setup from ./stratum-db or equivalent");
786
+ console.log("Add tenant resolution logic for your use case");
787
+ `
788
+ }
789
+ ];
790
+ }
791
+
792
+ // src/generators/package-json.ts
793
+ function generatePresetPackageJson(projectName, preset) {
794
+ const deps = {
795
+ "@stratum-hq/lib": "^0.2.0"
796
+ };
797
+ const devDeps = {
798
+ typescript: "^5.3.0",
799
+ "@types/node": "^20.11.0"
800
+ };
801
+ addDatabaseDeps(deps, preset);
802
+ addOrmDeps(deps, devDeps, preset);
803
+ addFrameworkDeps(deps, devDeps, preset);
804
+ addStratumDeps(deps, preset);
805
+ const scripts = getScripts(preset);
806
+ return JSON.stringify(
807
+ {
808
+ name: projectName,
809
+ version: "0.1.0",
810
+ private: true,
811
+ type: "module",
812
+ scripts,
813
+ dependencies: sortKeys(deps),
814
+ devDependencies: sortKeys(devDeps),
815
+ engines: {
816
+ node: ">=20.0.0"
817
+ }
818
+ },
819
+ null,
820
+ 2
821
+ );
822
+ }
823
+ function addDatabaseDeps(deps, preset) {
824
+ switch (preset.database) {
825
+ case "postgres":
826
+ if (preset.orm !== "prisma" && preset.orm !== "drizzle" && preset.orm !== "sequelize") {
827
+ deps["pg"] = "^8.11.0";
828
+ }
829
+ if (preset.orm === "drizzle") {
830
+ deps["pg"] = "^8.11.0";
831
+ }
832
+ if (preset.orm === "pg") {
833
+ deps["pg"] = "^8.11.0";
834
+ }
835
+ if (preset.orm === "knex") {
836
+ deps["pg"] = "^8.11.0";
837
+ }
838
+ break;
839
+ case "mongodb":
840
+ break;
841
+ case "mysql":
842
+ if (preset.orm === "drizzle" || preset.orm === "pg") {
843
+ deps["mysql2"] = "^3.9.0";
844
+ }
845
+ break;
846
+ }
847
+ }
848
+ function addOrmDeps(deps, devDeps, preset) {
849
+ switch (preset.orm) {
850
+ case "prisma":
851
+ deps["@prisma/client"] = "^5.10.0";
852
+ devDeps["prisma"] = "^5.10.0";
853
+ if (preset.database === "postgres") {
854
+ deps["pg"] = "^8.11.0";
855
+ }
856
+ break;
857
+ case "drizzle":
858
+ deps["drizzle-orm"] = "^0.30.0";
859
+ devDeps["drizzle-kit"] = "^0.21.0";
860
+ if (preset.database === "postgres") {
861
+ deps["pg"] = "^8.11.0";
862
+ }
863
+ break;
864
+ case "sequelize":
865
+ deps["sequelize"] = "^6.37.0";
866
+ if (preset.database === "mysql") {
867
+ deps["mysql2"] = "^3.9.0";
868
+ } else {
869
+ deps["pg"] = "^8.11.0";
870
+ }
871
+ break;
872
+ case "knex":
873
+ deps["knex"] = "^3.1.0";
874
+ break;
875
+ case "mongoose":
876
+ deps["mongoose"] = "^8.2.0";
877
+ break;
878
+ case "pg":
879
+ break;
880
+ }
881
+ }
882
+ function addFrameworkDeps(deps, devDeps, preset) {
883
+ switch (preset.framework) {
884
+ case "express":
885
+ deps["express"] = "^4.18.0";
886
+ devDeps["@types/express"] = "^4.17.21";
887
+ break;
888
+ case "fastify":
889
+ deps["fastify"] = "^4.26.0";
890
+ break;
891
+ case "nextjs":
892
+ deps["next"] = "^15.5.16";
893
+ deps["react"] = "^19.0.0";
894
+ deps["react-dom"] = "^19.0.0";
895
+ devDeps["@types/react"] = "^19.0.0";
896
+ devDeps["@types/react-dom"] = "^19.0.0";
897
+ break;
898
+ case "hono":
899
+ deps["hono"] = "^4.1.0";
900
+ deps["@hono/node-server"] = "^1.8.0";
901
+ break;
902
+ case "nestjs":
903
+ deps["@nestjs/core"] = "^11.0.0";
904
+ deps["@nestjs/common"] = "^11.0.0";
905
+ deps["@nestjs/platform-express"] = "^11.0.0";
906
+ deps["reflect-metadata"] = "^0.2.0";
907
+ deps["rxjs"] = "^7.8.0";
908
+ break;
909
+ case "none":
910
+ break;
911
+ }
912
+ }
913
+ function addStratumDeps(deps, preset) {
914
+ if (preset.database === "postgres" && preset.orm !== "mongoose") {
915
+ deps["@stratum-hq/db-adapters"] = "^0.2.0";
916
+ }
917
+ if (preset.database === "mongodb") {
918
+ deps["@stratum-hq/mongodb"] = "^0.2.0";
919
+ }
920
+ if (preset.database === "mysql") {
921
+ deps["@stratum-hq/mysql"] = "^0.2.0";
922
+ }
923
+ if (preset.framework === "hono") {
924
+ deps["@stratum-hq/hono"] = "^0.2.0";
925
+ }
926
+ if (preset.framework === "nestjs") {
927
+ deps["@stratum-hq/nestjs"] = "^0.2.0";
928
+ }
929
+ }
930
+ function getScripts(preset) {
931
+ if (preset.framework === "nextjs") {
932
+ return { dev: "next dev", build: "next build", start: "next start" };
933
+ }
934
+ if (preset.framework === "nestjs") {
935
+ return { dev: "node --watch src/main.ts", build: "tsc", start: "node dist/main.js" };
936
+ }
937
+ return { dev: "node --watch src/index.js", build: "tsc", start: "node dist/index.js" };
938
+ }
939
+ function sortKeys(obj) {
940
+ const sorted = {};
941
+ for (const key of Object.keys(obj).sort()) {
942
+ sorted[key] = obj[key];
943
+ }
944
+ return sorted;
945
+ }
946
+
947
+ // src/generators/readme.ts
948
+ function generatePresetReadme(projectName, preset) {
949
+ const devCmd = getDevCommand(preset);
950
+ const dbStartCmd = getDbStartInfo(preset);
951
+ const dbSetupNote = getDbSetupNote(preset);
952
+ return `# ${projectName}
953
+
954
+ A multi-tenant application built with [Stratum](https://github.com/stratum-hq/Stratum).
955
+
956
+ **Stack:** ${preset.database} + ${preset.strategy} strategy + ${preset.orm} + ${preset.framework === "none" ? "no framework" : preset.framework}
957
+
958
+ ## Getting started
959
+
960
+ ### 1. Start the database
961
+
962
+ \`\`\`bash
963
+ docker compose up -d
964
+ \`\`\`
965
+ ${dbStartCmd}
966
+
967
+ ### 2. Configure environment
968
+
969
+ \`\`\`bash
970
+ cp .env.example .env
971
+ # Edit .env with your database credentials and Stratum API key
972
+ \`\`\`
973
+
974
+ ### 3. Install dependencies
975
+
976
+ \`\`\`bash
977
+ npm install
978
+ \`\`\`
979
+ ${dbSetupNote}
980
+
981
+ ### 4. Run the app
982
+
983
+ \`\`\`bash
984
+ ${devCmd}
985
+ \`\`\`
986
+
987
+ ## Multi-tenancy
988
+
989
+ This project uses Stratum for hierarchical multi-tenancy with the **${preset.strategy}** isolation strategy:
990
+
991
+ ${getStrategyDescription(preset.strategy)}
992
+
993
+ See the [Stratum docs](https://github.com/stratum-hq/Stratum) for full reference.
994
+ `;
995
+ }
996
+ function getDevCommand(preset) {
997
+ if (preset.framework === "nextjs")
998
+ return "npm run dev";
999
+ if (preset.framework === "nestjs")
1000
+ return "node --env-file=.env src/main.ts";
1001
+ if (preset.framework === "none")
1002
+ return "node --env-file=.env src/index.ts";
1003
+ return "node --env-file=.env src/index.ts";
1004
+ }
1005
+ function getDbStartInfo(preset) {
1006
+ switch (preset.database) {
1007
+ case "postgres":
1008
+ return "\nThis starts PostgreSQL 16 on port 5432.\n";
1009
+ case "mongodb":
1010
+ return "\nThis starts MongoDB 7 on port 27017.\n";
1011
+ case "mysql":
1012
+ return "\nThis starts MySQL 8 on port 3306.\n";
1013
+ }
1014
+ }
1015
+ function getDbSetupNote(preset) {
1016
+ if (preset.orm === "prisma") {
1017
+ return `
1018
+ ### 3b. Generate Prisma client
1019
+
1020
+ \`\`\`bash
1021
+ npx prisma generate
1022
+ npx prisma db push
1023
+ \`\`\`
1024
+ `;
1025
+ }
1026
+ if (preset.orm === "drizzle") {
1027
+ return `
1028
+ ### 3b. Run Drizzle migrations
1029
+
1030
+ \`\`\`bash
1031
+ npx drizzle-kit push
1032
+ \`\`\`
1033
+ `;
1034
+ }
1035
+ return "";
1036
+ }
1037
+ function getStrategyDescription(strategy) {
1038
+ switch (strategy) {
1039
+ case "rls":
1040
+ return `- **Row-Level Security** -- PostgreSQL RLS policies filter rows by tenant automatically
1041
+ - Each query sets \`app.current_tenant\` and RLS enforces isolation
1042
+ - All tenants share one database and schema`;
1043
+ case "schema":
1044
+ return `- **Schema-per-tenant** -- each tenant gets a dedicated PostgreSQL schema
1045
+ - Queries are routed to the correct schema via search_path
1046
+ - Shared database, isolated schemas`;
1047
+ case "database":
1048
+ return `- **Database-per-tenant** -- each tenant gets a fully isolated database
1049
+ - Connection routing directs queries to the correct database
1050
+ - Maximum isolation at the cost of more resource usage`;
1051
+ case "collection":
1052
+ return `- **Collection-per-tenant** -- each tenant gets dedicated MongoDB collections
1053
+ - Collection names are prefixed or namespaced by tenant ID
1054
+ - Shared database, isolated collections`;
1055
+ case "table-prefix":
1056
+ return `- **Table-prefix** -- tenant-specific tables with a naming prefix
1057
+ - Tables are prefixed with the tenant identifier
1058
+ - Shared database, prefixed table names`;
1059
+ default:
1060
+ return "";
1061
+ }
1062
+ }
1063
+
1064
+ // src/preset-project.ts
1065
+ function writeFile(filePath, content) {
1066
+ const dir = path.dirname(filePath);
1067
+ if (!fs.existsSync(dir)) {
1068
+ fs.mkdirSync(dir, { recursive: true });
1069
+ }
1070
+ fs.writeFileSync(filePath, content, "utf8");
1071
+ console.log(` created ${path.relative(process.cwd(), filePath)}`);
1072
+ }
1073
+ function generatePresetEnv(projectName, preset) {
1074
+ const dbName = projectName.replace(/[^a-z0-9]/gi, "_").toLowerCase();
1075
+ const jwtSecret = crypto.randomBytes(32).toString("base64url");
1076
+ let dbUrl;
1077
+ switch (preset.database) {
1078
+ case "postgres":
1079
+ dbUrl = `postgres://${dbName}:dev_password@localhost:5432/${dbName}`;
1080
+ break;
1081
+ case "mongodb":
1082
+ dbUrl = `mongodb://${dbName}:dev_password@localhost:27017/${dbName}?authSource=admin`;
1083
+ break;
1084
+ case "mysql":
1085
+ dbUrl = `mysql://${dbName}:dev_password@localhost:3306/${dbName}`;
1086
+ break;
1087
+ }
1088
+ const urlKey = preset.database === "mongodb" ? "MONGODB_URI" : "DATABASE_URL";
1089
+ return `# Environment variables for ${projectName}
1090
+ # Copy to .env and fill in values
1091
+
1092
+ # Database
1093
+ ${urlKey}=${dbUrl}
1094
+
1095
+ # Authentication
1096
+ JWT_SECRET=${jwtSecret}
1097
+
1098
+ # Stratum Control Plane
1099
+ STRATUM_URL=http://localhost:3001
1100
+ STRATUM_API_KEY=sk_test_your_key_here
1101
+
1102
+ NODE_ENV=development
1103
+ `;
1104
+ }
1105
+ function createPresetProject(projectName, preset, targetDir, skipInstall) {
1106
+ const presetLabel = `${preset.database}/${preset.strategy}/${preset.orm}/${preset.framework}`;
1107
+ console.log(`
1108
+ Creating ${projectName} with preset ${presetLabel}...
1109
+ `);
1110
+ writeFile(
1111
+ path.join(targetDir, "package.json"),
1112
+ generatePresetPackageJson(projectName, preset)
1113
+ );
1114
+ writeFile(
1115
+ path.join(targetDir, "docker-compose.yml"),
1116
+ generatePresetDockerCompose(projectName, preset)
1117
+ );
1118
+ const initSql = generatePresetInitSql(projectName, preset);
1119
+ if (initSql !== null) {
1120
+ writeFile(path.join(targetDir, "init.sql"), initSql);
1121
+ }
1122
+ writeFile(
1123
+ path.join(targetDir, ".env.example"),
1124
+ generatePresetEnv(projectName, preset)
1125
+ );
1126
+ const dbFiles = generateDbSetup(preset);
1127
+ for (const file of dbFiles) {
1128
+ writeFile(path.join(targetDir, file.filename), file.content);
1129
+ }
1130
+ const middlewareFiles = generateMiddleware(projectName, preset);
1131
+ for (const file of middlewareFiles) {
1132
+ writeFile(path.join(targetDir, file.filename), file.content);
1133
+ }
1134
+ writeFile(
1135
+ path.join(targetDir, "README.md"),
1136
+ generatePresetReadme(projectName, preset)
1137
+ );
1138
+ writeFile(
1139
+ path.join(targetDir, "tsconfig.json"),
1140
+ JSON.stringify(
1141
+ {
1142
+ compilerOptions: {
1143
+ target: "ESNext",
1144
+ module: "NodeNext",
1145
+ moduleResolution: "NodeNext",
1146
+ strict: true,
1147
+ outDir: "dist",
1148
+ rootDir: "src",
1149
+ declaration: true,
1150
+ skipLibCheck: true,
1151
+ esModuleInterop: true,
1152
+ ...preset.framework === "nestjs" ? { experimentalDecorators: true, emitDecoratorMetadata: true } : {},
1153
+ ...preset.framework === "nextjs" ? { jsx: "preserve", plugins: [{ name: "next" }] } : {}
1154
+ },
1155
+ include: ["src"],
1156
+ exclude: ["node_modules", "dist"]
1157
+ },
1158
+ null,
1159
+ 2
1160
+ )
1161
+ );
1162
+ if (!skipInstall) {
1163
+ console.log("\nInstalling dependencies...\n");
1164
+ try {
1165
+ execSync("npm install", { cwd: targetDir, stdio: "inherit" });
1166
+ } catch {
1167
+ console.warn("\nWarning: npm install failed. Run it manually in the project directory.");
1168
+ }
1169
+ }
1170
+ }
1171
+
1172
+ // src/index.ts
8
1173
  function parseArgs(argv) {
9
1174
  const args = [...argv];
10
1175
  let projectName = null;
11
1176
  let template = "express";
1177
+ let preset = null;
12
1178
  let skipInstall = false;
13
1179
  let force = false;
1180
+ let templateExplicit = false;
14
1181
  for (let i = 0; i < args.length; i++) {
15
1182
  const arg = args[i];
16
1183
  if (arg === "--skip-install") {
@@ -21,6 +1188,7 @@ function parseArgs(argv) {
21
1188
  const val = args[i + 1];
22
1189
  if (val === "express" || val === "fastify" || val === "nextjs") {
23
1190
  template = val;
1191
+ templateExplicit = true;
24
1192
  i++;
25
1193
  } else {
26
1194
  console.error(`Unknown template: ${val}. Available: express, fastify, nextjs`);
@@ -30,36 +1198,49 @@ function parseArgs(argv) {
30
1198
  const val = arg.slice("--template=".length);
31
1199
  if (val === "express" || val === "fastify" || val === "nextjs") {
32
1200
  template = val;
1201
+ templateExplicit = true;
33
1202
  } else {
34
1203
  console.error(`Unknown template: ${val}. Available: express, fastify, nextjs`);
35
1204
  process.exit(1);
36
1205
  }
1206
+ } else if (arg === "--preset") {
1207
+ preset = args[i + 1] || null;
1208
+ i++;
1209
+ } else if (arg.startsWith("--preset=")) {
1210
+ preset = arg.slice("--preset=".length) || null;
37
1211
  } else if (!arg.startsWith("--")) {
38
1212
  projectName = arg;
39
1213
  }
40
1214
  }
41
- return { projectName, template, skipInstall, force };
1215
+ if (preset && templateExplicit) {
1216
+ console.error("Error: --template and --preset cannot be used together. Use one or the other.");
1217
+ process.exit(1);
1218
+ }
1219
+ return { projectName, template, preset, skipInstall, force };
42
1220
  }
43
1221
  function printUsage() {
44
1222
  console.log("Usage: create-stratum <project-name> [options]");
45
1223
  console.log("");
46
1224
  console.log("Options:");
47
1225
  console.log(" --template <express|fastify|nextjs> Framework template (default: express)");
1226
+ console.log(" --preset <db-strategy-orm-framework> Full stack preset (e.g. postgres-rls-prisma-express)");
48
1227
  console.log(" --skip-install Skip npm install");
49
1228
  console.log(" --force Overwrite existing directory");
50
1229
  console.log("");
51
1230
  console.log("Examples:");
52
1231
  console.log(" npx @stratum-hq/create my-app");
53
1232
  console.log(" npx @stratum-hq/create my-app --template fastify");
54
- console.log(" npx @stratum-hq/create my-app --template nextjs --skip-install");
1233
+ console.log(" npx @stratum-hq/create my-app --preset postgres-rls-prisma-express");
1234
+ console.log(" npx @stratum-hq/create my-app --preset mongodb-database-mongoose-hono");
1235
+ console.log(" npx @stratum-hq/create my-app --preset mysql-table-prefix-sequelize-nestjs");
55
1236
  }
56
- function writeFile(filePath, content) {
57
- const dir = path.dirname(filePath);
58
- if (!fs.existsSync(dir)) {
59
- fs.mkdirSync(dir, { recursive: true });
1237
+ function writeFile2(filePath, content) {
1238
+ const dir = path2.dirname(filePath);
1239
+ if (!fs2.existsSync(dir)) {
1240
+ fs2.mkdirSync(dir, { recursive: true });
60
1241
  }
61
- fs.writeFileSync(filePath, content, "utf8");
62
- console.log(` created ${path.relative(process.cwd(), filePath)}`);
1242
+ fs2.writeFileSync(filePath, content, "utf8");
1243
+ console.log(` created ${path2.relative(process.cwd(), filePath)}`);
63
1244
  }
64
1245
  function generatePackageJson(projectName, template) {
65
1246
  const frameworkDeps = {
@@ -71,11 +1252,11 @@ function generatePackageJson(projectName, template) {
71
1252
  fastify: "^4.26.0"
72
1253
  },
73
1254
  nextjs: {
74
- next: "^14.1.0",
75
- react: "^18.2.0",
76
- "react-dom": "^18.2.0",
77
- "@types/react": "^18.2.0",
78
- "@types/react-dom": "^18.2.0"
1255
+ next: "^15.5.16",
1256
+ react: "^19.0.0",
1257
+ "react-dom": "^19.0.0",
1258
+ "@types/react": "^19.0.0",
1259
+ "@types/react-dom": "^19.0.0"
79
1260
  }
80
1261
  };
81
1262
  const deps = {
@@ -145,6 +1326,7 @@ COMMENT ON DATABASE ${dbName} IS 'Multi-tenant database for ${projectName}';
145
1326
  }
146
1327
  function generateEnv(projectName) {
147
1328
  const dbName = projectName.replace(/[^a-z0-9]/gi, "_").toLowerCase();
1329
+ const jwtSecret = crypto2.randomBytes(32).toString("base64url");
148
1330
  return `# Environment variables for ${projectName}
149
1331
  # Copy to .env and fill in values
150
1332
 
@@ -152,7 +1334,7 @@ function generateEnv(projectName) {
152
1334
  DATABASE_URL=postgres://${dbName}:dev_password@localhost:5432/${dbName}
153
1335
 
154
1336
  # Authentication
155
- JWT_SECRET=change-me-in-production
1337
+ JWT_SECRET=${jwtSecret}
156
1338
 
157
1339
  # Stratum Control Plane
158
1340
  STRATUM_URL=http://localhost:3001
@@ -245,7 +1427,7 @@ export default function Home() {
245
1427
  }
246
1428
  `;
247
1429
  }
248
- function generateNextjsMiddleware() {
1430
+ function generateNextjsMiddleware2() {
249
1431
  return `// middleware.ts \u2014 tenant resolution via subdomain or header
250
1432
  import { NextRequest, NextResponse } from "next/server";
251
1433
 
@@ -329,26 +1511,26 @@ function createProject(projectName, template, targetDir, skipInstall) {
329
1511
  console.log(`
330
1512
  Creating ${projectName} with ${template} template...
331
1513
  `);
332
- writeFile(
333
- path.join(targetDir, "package.json"),
1514
+ writeFile2(
1515
+ path2.join(targetDir, "package.json"),
334
1516
  generatePackageJson(projectName, template)
335
1517
  );
336
- writeFile(path.join(targetDir, "docker-compose.yml"), generateDockerCompose(projectName));
337
- writeFile(path.join(targetDir, "init.sql"), generateInitSql(projectName));
338
- writeFile(path.join(targetDir, ".env.example"), generateEnv(projectName));
1518
+ writeFile2(path2.join(targetDir, "docker-compose.yml"), generateDockerCompose(projectName));
1519
+ writeFile2(path2.join(targetDir, "init.sql"), generateInitSql(projectName));
1520
+ writeFile2(path2.join(targetDir, ".env.example"), generateEnv(projectName));
339
1521
  if (template === "express") {
340
- writeFile(path.join(targetDir, "src", "index.ts"), generateExpressServer(projectName));
1522
+ writeFile2(path2.join(targetDir, "src", "index.ts"), generateExpressServer(projectName));
341
1523
  } else if (template === "fastify") {
342
- writeFile(path.join(targetDir, "src", "index.ts"), generateFastifyServer(projectName));
1524
+ writeFile2(path2.join(targetDir, "src", "index.ts"), generateFastifyServer(projectName));
343
1525
  } else if (template === "nextjs") {
344
- writeFile(path.join(targetDir, "src", "app", "page.tsx"), generateNextjsPage(projectName));
345
- writeFile(path.join(targetDir, "middleware.ts"), generateNextjsMiddleware());
1526
+ writeFile2(path2.join(targetDir, "src", "app", "page.tsx"), generateNextjsPage(projectName));
1527
+ writeFile2(path2.join(targetDir, "middleware.ts"), generateNextjsMiddleware2());
346
1528
  }
347
- writeFile(path.join(targetDir, "README.md"), generateReadme(projectName, template));
1529
+ writeFile2(path2.join(targetDir, "README.md"), generateReadme(projectName, template));
348
1530
  if (!skipInstall) {
349
1531
  console.log("\nInstalling dependencies...\n");
350
1532
  try {
351
- execSync("npm install", { cwd: targetDir, stdio: "inherit" });
1533
+ execSync2("npm install", { cwd: targetDir, stdio: "inherit" });
352
1534
  } catch {
353
1535
  console.warn("\nWarning: npm install failed. Run it manually in the project directory.");
354
1536
  }
@@ -362,14 +1544,14 @@ function validateProjectName(projectName) {
362
1544
  if (!VALID_PROJECT_NAME.test(projectName)) {
363
1545
  return `"${projectName}" is not a valid project name. Use only letters, numbers, dots, hyphens, and underscores. Must start with a letter or number.`;
364
1546
  }
365
- const targetDir = path.resolve(process.cwd(), projectName);
366
- if (path.dirname(targetDir) !== process.cwd()) {
1547
+ const targetDir = path2.resolve(process.cwd(), projectName);
1548
+ if (path2.dirname(targetDir) !== process.cwd()) {
367
1549
  return "Project name must not contain path separators.";
368
1550
  }
369
1551
  return null;
370
1552
  }
371
1553
  function main(argv) {
372
- const { projectName, template, skipInstall, force } = parseArgs(argv);
1554
+ const { projectName, template, preset, skipInstall, force } = parseArgs(argv);
373
1555
  if (!projectName) {
374
1556
  printUsage();
375
1557
  process.exit(1);
@@ -379,17 +1561,34 @@ function main(argv) {
379
1561
  console.error(`Error: ${validationError}`);
380
1562
  process.exit(1);
381
1563
  }
382
- const targetDir = path.resolve(process.cwd(), projectName);
383
- if (fs.existsSync(targetDir) && !force) {
1564
+ const targetDir = path2.resolve(process.cwd(), projectName);
1565
+ if (fs2.existsSync(targetDir) && !force) {
384
1566
  console.error(`Error: Directory "${projectName}" already exists.`);
385
1567
  console.error(`Use --force to overwrite.`);
386
1568
  process.exit(1);
387
1569
  }
388
- if (fs.existsSync(targetDir) && force) {
389
- fs.rmSync(targetDir, { recursive: true, force: true });
1570
+ if (fs2.existsSync(targetDir) && force) {
1571
+ fs2.rmSync(targetDir, { recursive: true, force: true });
1572
+ }
1573
+ fs2.mkdirSync(targetDir, { recursive: true });
1574
+ if (preset) {
1575
+ const parsed = parsePresetString(preset);
1576
+ if (!parsed) {
1577
+ console.error(`Error: Invalid preset string "${preset}".`);
1578
+ console.error("Format: {database}-{strategy}-{orm}-{framework}");
1579
+ console.error("Example: postgres-rls-prisma-express, mongodb-database-mongoose-hono");
1580
+ process.exit(1);
1581
+ }
1582
+ if (!isValidPreset(parsed)) {
1583
+ console.error(`Error: Invalid preset combination "${preset}".`);
1584
+ console.error(`${parsed.database} does not support ${parsed.strategy}/${parsed.orm} together.`);
1585
+ console.error("Run with --help to see valid combinations.");
1586
+ process.exit(1);
1587
+ }
1588
+ createPresetProject(projectName, parsed, targetDir, skipInstall);
1589
+ } else {
1590
+ createProject(projectName, template, targetDir, skipInstall);
390
1591
  }
391
- fs.mkdirSync(targetDir, { recursive: true });
392
- createProject(projectName, template, targetDir, skipInstall);
393
1592
  console.log(`
394
1593
  Success! Created ${projectName} at ${targetDir}
395
1594
  `);
@@ -400,7 +1599,16 @@ Success! Created ${projectName} at ${targetDir}
400
1599
  if (skipInstall) {
401
1600
  console.log(" npm install");
402
1601
  }
403
- if (template === "nextjs") {
1602
+ if (preset) {
1603
+ const parsed = parsePresetString(preset);
1604
+ if (parsed.framework === "nextjs") {
1605
+ console.log(" npm run dev");
1606
+ } else if (parsed.framework === "nestjs") {
1607
+ console.log(" node --env-file=.env src/main.ts");
1608
+ } else {
1609
+ console.log(" node --env-file=.env src/index.ts");
1610
+ }
1611
+ } else if (template === "nextjs") {
404
1612
  console.log(" npm run dev");
405
1613
  } else {
406
1614
  console.log(" node --env-file=.env src/index.ts");