@stratum-hq/create 0.2.3 → 0.3.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.
package/src/index.ts CHANGED
@@ -2,8 +2,11 @@
2
2
 
3
3
  import * as fs from "fs";
4
4
  import * as path from "path";
5
+ import crypto from "node:crypto";
5
6
  import { execSync } from "child_process";
6
7
  import { fileURLToPath } from "url";
8
+ import { parsePresetString, isValidPreset, formatPresetString } from "./matrix.js";
9
+ import { createPresetProject } from "./preset-project.js";
7
10
 
8
11
  // ─── Types ────────────────────────────────────────────────────────────────────
9
12
 
@@ -12,6 +15,7 @@ export type Template = "express" | "fastify" | "nextjs";
12
15
  export interface ParsedArgs {
13
16
  projectName: string | null;
14
17
  template: Template;
18
+ preset: string | null;
15
19
  skipInstall: boolean;
16
20
  force: boolean;
17
21
  }
@@ -23,8 +27,10 @@ export function parseArgs(argv: string[]): ParsedArgs {
23
27
  const args = [...argv];
24
28
  let projectName: string | null = null;
25
29
  let template: Template = "express";
30
+ let preset: string | null = null;
26
31
  let skipInstall = false;
27
32
  let force = false;
33
+ let templateExplicit = false;
28
34
 
29
35
  for (let i = 0; i < args.length; i++) {
30
36
  const arg = args[i];
@@ -36,6 +42,7 @@ export function parseArgs(argv: string[]): ParsedArgs {
36
42
  const val = args[i + 1];
37
43
  if (val === "express" || val === "fastify" || val === "nextjs") {
38
44
  template = val;
45
+ templateExplicit = true;
39
46
  i++;
40
47
  } else {
41
48
  console.error(`Unknown template: ${val}. Available: express, fastify, nextjs`);
@@ -45,16 +52,27 @@ export function parseArgs(argv: string[]): ParsedArgs {
45
52
  const val = arg.slice("--template=".length);
46
53
  if (val === "express" || val === "fastify" || val === "nextjs") {
47
54
  template = val;
55
+ templateExplicit = true;
48
56
  } else {
49
57
  console.error(`Unknown template: ${val}. Available: express, fastify, nextjs`);
50
58
  process.exit(1);
51
59
  }
60
+ } else if (arg === "--preset") {
61
+ preset = args[i + 1] || null;
62
+ i++;
63
+ } else if (arg.startsWith("--preset=")) {
64
+ preset = arg.slice("--preset=".length) || null;
52
65
  } else if (!arg.startsWith("--")) {
53
66
  projectName = arg;
54
67
  }
55
68
  }
56
69
 
57
- return { projectName, template, skipInstall, force };
70
+ if (preset && templateExplicit) {
71
+ console.error("Error: --template and --preset cannot be used together. Use one or the other.");
72
+ process.exit(1);
73
+ }
74
+
75
+ return { projectName, template, preset, skipInstall, force };
58
76
  }
59
77
 
60
78
  // ─── Usage ────────────────────────────────────────────────────────────────────
@@ -64,13 +82,16 @@ function printUsage(): void {
64
82
  console.log("");
65
83
  console.log("Options:");
66
84
  console.log(" --template <express|fastify|nextjs> Framework template (default: express)");
85
+ console.log(" --preset <db-strategy-orm-framework> Full stack preset (e.g. postgres-rls-prisma-express)");
67
86
  console.log(" --skip-install Skip npm install");
68
87
  console.log(" --force Overwrite existing directory");
69
88
  console.log("");
70
89
  console.log("Examples:");
71
90
  console.log(" npx @stratum-hq/create my-app");
72
91
  console.log(" npx @stratum-hq/create my-app --template fastify");
73
- console.log(" npx @stratum-hq/create my-app --template nextjs --skip-install");
92
+ console.log(" npx @stratum-hq/create my-app --preset postgres-rls-prisma-express");
93
+ console.log(" npx @stratum-hq/create my-app --preset mongodb-database-mongoose-hono");
94
+ console.log(" npx @stratum-hq/create my-app --preset mysql-table-prefix-sequelize-nestjs");
74
95
  }
75
96
 
76
97
  // ─── File generation helpers ──────────────────────────────────────────────────
@@ -96,11 +117,11 @@ function generatePackageJson(projectName: string, template: Template): string {
96
117
  fastify: "^4.26.0",
97
118
  },
98
119
  nextjs: {
99
- next: "^14.1.0",
100
- react: "^18.2.0",
101
- "react-dom": "^18.2.0",
102
- "@types/react": "^18.2.0",
103
- "@types/react-dom": "^18.2.0",
120
+ next: "^15.5.16",
121
+ react: "^19.0.0",
122
+ "react-dom": "^19.0.0",
123
+ "@types/react": "^19.0.0",
124
+ "@types/react-dom": "^19.0.0",
104
125
  },
105
126
  };
106
127
 
@@ -178,6 +199,7 @@ COMMENT ON DATABASE ${dbName} IS 'Multi-tenant database for ${projectName}';
178
199
 
179
200
  function generateEnv(projectName: string): string {
180
201
  const dbName = projectName.replace(/[^a-z0-9]/gi, "_").toLowerCase();
202
+ const jwtSecret = crypto.randomBytes(32).toString("base64url");
181
203
  return `# Environment variables for ${projectName}
182
204
  # Copy to .env and fill in values
183
205
 
@@ -185,7 +207,7 @@ function generateEnv(projectName: string): string {
185
207
  DATABASE_URL=postgres://${dbName}:dev_password@localhost:5432/${dbName}
186
208
 
187
209
  # Authentication
188
- JWT_SECRET=change-me-in-production
210
+ JWT_SECRET=${jwtSecret}
189
211
 
190
212
  # Stratum Control Plane
191
213
  STRATUM_URL=http://localhost:3001
@@ -436,7 +458,7 @@ export function validateProjectName(projectName: string): string | null {
436
458
  // ─── Main entry point ─────────────────────────────────────────────────────────
437
459
 
438
460
  export function main(argv: string[]): void {
439
- const { projectName, template, skipInstall, force } = parseArgs(argv);
461
+ const { projectName, template, preset, skipInstall, force } = parseArgs(argv);
440
462
 
441
463
  if (!projectName) {
442
464
  printUsage();
@@ -463,7 +485,26 @@ export function main(argv: string[]): void {
463
485
 
464
486
  fs.mkdirSync(targetDir, { recursive: true });
465
487
 
466
- createProject(projectName, template, targetDir, skipInstall);
488
+ // Preset path: full stack generation
489
+ if (preset) {
490
+ const parsed = parsePresetString(preset);
491
+ if (!parsed) {
492
+ console.error(`Error: Invalid preset string "${preset}".`);
493
+ console.error("Format: {database}-{strategy}-{orm}-{framework}");
494
+ console.error("Example: postgres-rls-prisma-express, mongodb-database-mongoose-hono");
495
+ process.exit(1);
496
+ }
497
+ if (!isValidPreset(parsed)) {
498
+ console.error(`Error: Invalid preset combination "${preset}".`);
499
+ console.error(`${parsed.database} does not support ${parsed.strategy}/${parsed.orm} together.`);
500
+ console.error("Run with --help to see valid combinations.");
501
+ process.exit(1);
502
+ }
503
+ createPresetProject(projectName, parsed, targetDir, skipInstall);
504
+ } else {
505
+ // Template path: existing behavior, untouched
506
+ createProject(projectName, template, targetDir, skipInstall);
507
+ }
467
508
 
468
509
  console.log(`\nSuccess! Created ${projectName} at ${targetDir}\n`);
469
510
  console.log("Next steps:\n");
@@ -473,7 +514,16 @@ export function main(argv: string[]): void {
473
514
  if (skipInstall) {
474
515
  console.log(" npm install");
475
516
  }
476
- if (template === "nextjs") {
517
+ if (preset) {
518
+ const parsed = parsePresetString(preset)!;
519
+ if (parsed.framework === "nextjs") {
520
+ console.log(" npm run dev");
521
+ } else if (parsed.framework === "nestjs") {
522
+ console.log(" node --env-file=.env src/main.ts");
523
+ } else {
524
+ console.log(" node --env-file=.env src/index.ts");
525
+ }
526
+ } else if (template === "nextjs") {
477
527
  console.log(" npm run dev");
478
528
  } else {
479
529
  console.log(" node --env-file=.env src/index.ts");
package/src/matrix.ts ADDED
@@ -0,0 +1,186 @@
1
+ // ─── Stack Combination Matrix ────────────────────────────────────────────────
2
+ // Single source of truth for valid Stratum stack combinations.
3
+ // Imported by create CLI, interactive wizard, and web docs.
4
+
5
+ export type Database = "postgres" | "mongodb" | "mysql";
6
+ export type Strategy = "rls" | "schema" | "database" | "collection" | "table-prefix";
7
+ export type Orm = "prisma" | "drizzle" | "sequelize" | "knex" | "mongoose" | "pg";
8
+ export type Framework = "express" | "fastify" | "nextjs" | "hono" | "nestjs" | "none";
9
+
10
+ export interface StackPreset {
11
+ database: Database;
12
+ strategy: Strategy;
13
+ orm: Orm;
14
+ framework: Framework;
15
+ }
16
+
17
+ // ─── Valid combinations by database ──────────────────────────────────────────
18
+
19
+ const POSTGRES_STRATEGIES: Strategy[] = ["rls", "schema", "database"];
20
+ const POSTGRES_ORMS: Orm[] = ["prisma", "drizzle", "sequelize", "knex", "pg"];
21
+
22
+ const MONGODB_STRATEGIES: Strategy[] = ["database", "collection"];
23
+ const MONGODB_ORMS: Orm[] = ["mongoose"];
24
+
25
+ const MYSQL_STRATEGIES: Strategy[] = ["database", "table-prefix"];
26
+ const MYSQL_ORMS: Orm[] = ["sequelize", "knex", "pg"];
27
+
28
+ const ALL_FRAMEWORKS: Framework[] = ["express", "fastify", "nextjs", "hono", "nestjs", "none"];
29
+
30
+ export interface DatabaseConfig {
31
+ strategies: Strategy[];
32
+ orms: Orm[];
33
+ frameworks: Framework[];
34
+ }
35
+
36
+ export const VALID_COMBINATIONS: Record<Database, DatabaseConfig> = {
37
+ postgres: {
38
+ strategies: POSTGRES_STRATEGIES,
39
+ orms: POSTGRES_ORMS,
40
+ frameworks: ALL_FRAMEWORKS,
41
+ },
42
+ mongodb: {
43
+ strategies: MONGODB_STRATEGIES,
44
+ orms: MONGODB_ORMS,
45
+ frameworks: ALL_FRAMEWORKS,
46
+ },
47
+ mysql: {
48
+ strategies: MYSQL_STRATEGIES,
49
+ orms: MYSQL_ORMS,
50
+ frameworks: ALL_FRAMEWORKS,
51
+ },
52
+ };
53
+
54
+ // ─── All valid values (for parsing) ─────────────────────────────────────────
55
+
56
+ const ALL_DATABASES: Database[] = ["postgres", "mongodb", "mysql"];
57
+ const ALL_STRATEGIES: Strategy[] = ["rls", "schema", "database", "collection", "table-prefix"];
58
+ const ALL_ORMS: Orm[] = ["prisma", "drizzle", "sequelize", "knex", "mongoose", "pg"];
59
+
60
+ // ─── Validation ──────────────────────────────────────────────────────────────
61
+
62
+ export function isValidPreset(preset: StackPreset): boolean {
63
+ const config = VALID_COMBINATIONS[preset.database];
64
+ if (!config) return false;
65
+ if (!config.strategies.includes(preset.strategy)) return false;
66
+ if (!config.orms.includes(preset.orm)) return false;
67
+ if (!config.frameworks.includes(preset.framework)) return false;
68
+ return true;
69
+ }
70
+
71
+ // ─── Parsing ─────────────────────────────────────────────────────────────────
72
+
73
+ export function parsePresetString(s: string): StackPreset | null {
74
+ if (!s || typeof s !== "string") return null;
75
+
76
+ const parts = s.toLowerCase().split("-");
77
+
78
+ // Handle "table-prefix" which contains a hyphen -- it will split into
79
+ // 5 parts: [db, strategy1, "table", "prefix", orm, framework] or similar.
80
+ // We need to reconstruct multi-word tokens.
81
+ // Format: {database}-{strategy}-{orm}-{framework}
82
+ // Strategy "table-prefix" is the only multi-word token.
83
+
84
+ let database: string;
85
+ let strategy: string;
86
+ let orm: string;
87
+ let framework: string;
88
+
89
+ if (parts.length === 4) {
90
+ // Simple case: no hyphenated strategy
91
+ [database, strategy, orm, framework] = parts;
92
+ } else if (parts.length === 5) {
93
+ // "table-prefix" case: mysql-table-prefix-sequelize-express
94
+ database = parts[0];
95
+ strategy = `${parts[1]}-${parts[2]}`;
96
+ orm = parts[3];
97
+ framework = parts[4];
98
+ } else {
99
+ return null;
100
+ }
101
+
102
+ // Validate each part is a known value
103
+ if (!ALL_DATABASES.includes(database as Database)) return null;
104
+ if (!ALL_STRATEGIES.includes(strategy as Strategy)) return null;
105
+ if (!ALL_ORMS.includes(orm as Orm)) return null;
106
+ if (!ALL_FRAMEWORKS.includes(framework as Framework)) return null;
107
+
108
+ return {
109
+ database: database as Database,
110
+ strategy: strategy as Strategy,
111
+ orm: orm as Orm,
112
+ framework: framework as Framework,
113
+ };
114
+ }
115
+
116
+ // ─── Formatting ──────────────────────────────────────────────────────────────
117
+
118
+ export function formatPresetString(preset: StackPreset): string {
119
+ return `${preset.database}-${preset.strategy}-${preset.orm}-${preset.framework}`;
120
+ }
121
+
122
+ // ─── Wizard helper: narrow valid options given partial selections ─────────────
123
+
124
+ export interface ValidOptions {
125
+ databases: Database[];
126
+ strategies: Strategy[];
127
+ orms: Orm[];
128
+ frameworks: Framework[];
129
+ }
130
+
131
+ export function getValidOptions(partial: Partial<StackPreset>): ValidOptions {
132
+ let databases: Database[] = [...ALL_DATABASES];
133
+ let strategies: Strategy[] = [];
134
+ let orms: Orm[] = [];
135
+ let frameworks: Framework[] = [];
136
+
137
+ // If database is selected, narrow to that database's config
138
+ if (partial.database) {
139
+ databases = [partial.database];
140
+ }
141
+
142
+ // Collect valid strategies/orms/frameworks from matching databases
143
+ const strategySet = new Set<Strategy>();
144
+ const ormSet = new Set<Orm>();
145
+ const frameworkSet = new Set<Framework>();
146
+
147
+ for (const db of databases) {
148
+ const config = VALID_COMBINATIONS[db];
149
+
150
+ // If strategy is specified, only include this db if it supports it
151
+ if (partial.strategy && !config.strategies.includes(partial.strategy)) continue;
152
+ // If orm is specified, only include this db if it supports it
153
+ if (partial.orm && !config.orms.includes(partial.orm)) continue;
154
+
155
+ for (const s of config.strategies) strategySet.add(s);
156
+ for (const o of config.orms) ormSet.add(o);
157
+ for (const f of config.frameworks) frameworkSet.add(f);
158
+ }
159
+
160
+ strategies = [...strategySet];
161
+ orms = [...ormSet];
162
+ frameworks = [...frameworkSet];
163
+
164
+ // Further narrow if specific values are set
165
+ if (partial.strategy) {
166
+ strategies = strategies.filter((s) => s === partial.strategy);
167
+ }
168
+ if (partial.orm) {
169
+ orms = orms.filter((o) => o === partial.orm);
170
+ }
171
+ if (partial.framework) {
172
+ frameworks = frameworks.filter((f) => f === partial.framework);
173
+ }
174
+
175
+ // Narrow databases to only those that support all specified fields
176
+ if (partial.strategy || partial.orm) {
177
+ databases = databases.filter((db) => {
178
+ const config = VALID_COMBINATIONS[db];
179
+ if (partial.strategy && !config.strategies.includes(partial.strategy)) return false;
180
+ if (partial.orm && !config.orms.includes(partial.orm)) return false;
181
+ return true;
182
+ });
183
+ }
184
+
185
+ return { databases, strategies, orms, frameworks };
186
+ }
@@ -0,0 +1,148 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import crypto from "node:crypto";
4
+ import { execSync } from "child_process";
5
+ import type { StackPreset } from "./matrix.js";
6
+ import { generatePresetDockerCompose } from "./generators/docker-compose.js";
7
+ import { generatePresetInitSql } from "./generators/init-sql.js";
8
+ import { generateDbSetup } from "./generators/db-setup.js";
9
+ import { generateMiddleware } from "./generators/middleware.js";
10
+ import { generatePresetPackageJson } from "./generators/package-json.js";
11
+ import { generatePresetReadme } from "./generators/readme.js";
12
+
13
+ function writeFile(filePath: string, content: string): void {
14
+ const dir = path.dirname(filePath);
15
+ if (!fs.existsSync(dir)) {
16
+ fs.mkdirSync(dir, { recursive: true });
17
+ }
18
+ fs.writeFileSync(filePath, content, "utf8");
19
+ console.log(` created ${path.relative(process.cwd(), filePath)}`);
20
+ }
21
+
22
+ function generatePresetEnv(projectName: string, preset: StackPreset): string {
23
+ const dbName = projectName.replace(/[^a-z0-9]/gi, "_").toLowerCase();
24
+ const jwtSecret = crypto.randomBytes(32).toString("base64url");
25
+
26
+ let dbUrl: string;
27
+ switch (preset.database) {
28
+ case "postgres":
29
+ dbUrl = `postgres://${dbName}:dev_password@localhost:5432/${dbName}`;
30
+ break;
31
+ case "mongodb":
32
+ dbUrl = `mongodb://${dbName}:dev_password@localhost:27017/${dbName}?authSource=admin`;
33
+ break;
34
+ case "mysql":
35
+ dbUrl = `mysql://${dbName}:dev_password@localhost:3306/${dbName}`;
36
+ break;
37
+ }
38
+
39
+ const urlKey = preset.database === "mongodb" ? "MONGODB_URI" : "DATABASE_URL";
40
+
41
+ return `# Environment variables for ${projectName}
42
+ # Copy to .env and fill in values
43
+
44
+ # Database
45
+ ${urlKey}=${dbUrl}
46
+
47
+ # Authentication
48
+ JWT_SECRET=${jwtSecret}
49
+
50
+ # Stratum Control Plane
51
+ STRATUM_URL=http://localhost:3001
52
+ STRATUM_API_KEY=sk_test_your_key_here
53
+
54
+ NODE_ENV=development
55
+ `;
56
+ }
57
+
58
+ export function createPresetProject(
59
+ projectName: string,
60
+ preset: StackPreset,
61
+ targetDir: string,
62
+ skipInstall: boolean,
63
+ ): void {
64
+ const presetLabel = `${preset.database}/${preset.strategy}/${preset.orm}/${preset.framework}`;
65
+ console.log(`\nCreating ${projectName} with preset ${presetLabel}...\n`);
66
+
67
+ // package.json
68
+ writeFile(
69
+ path.join(targetDir, "package.json"),
70
+ generatePresetPackageJson(projectName, preset),
71
+ );
72
+
73
+ // docker-compose.yml
74
+ writeFile(
75
+ path.join(targetDir, "docker-compose.yml"),
76
+ generatePresetDockerCompose(projectName, preset),
77
+ );
78
+
79
+ // init.sql (if applicable)
80
+ const initSql = generatePresetInitSql(projectName, preset);
81
+ if (initSql !== null) {
82
+ writeFile(path.join(targetDir, "init.sql"), initSql);
83
+ }
84
+
85
+ // .env.example
86
+ writeFile(
87
+ path.join(targetDir, ".env.example"),
88
+ generatePresetEnv(projectName, preset),
89
+ );
90
+
91
+ // Database setup files (ORM config, tenant helpers)
92
+ const dbFiles = generateDbSetup(preset);
93
+ for (const file of dbFiles) {
94
+ writeFile(path.join(targetDir, file.filename), file.content);
95
+ }
96
+
97
+ // Framework middleware / server entry point
98
+ const middlewareFiles = generateMiddleware(projectName, preset);
99
+ for (const file of middlewareFiles) {
100
+ writeFile(path.join(targetDir, file.filename), file.content);
101
+ }
102
+
103
+ // README
104
+ writeFile(
105
+ path.join(targetDir, "README.md"),
106
+ generatePresetReadme(projectName, preset),
107
+ );
108
+
109
+ // tsconfig.json
110
+ writeFile(
111
+ path.join(targetDir, "tsconfig.json"),
112
+ JSON.stringify(
113
+ {
114
+ compilerOptions: {
115
+ target: "ESNext",
116
+ module: "NodeNext",
117
+ moduleResolution: "NodeNext",
118
+ strict: true,
119
+ outDir: "dist",
120
+ rootDir: "src",
121
+ declaration: true,
122
+ skipLibCheck: true,
123
+ esModuleInterop: true,
124
+ ...(preset.framework === "nestjs"
125
+ ? { experimentalDecorators: true, emitDecoratorMetadata: true }
126
+ : {}),
127
+ ...(preset.framework === "nextjs"
128
+ ? { jsx: "preserve", plugins: [{ name: "next" }] }
129
+ : {}),
130
+ },
131
+ include: ["src"],
132
+ exclude: ["node_modules", "dist"],
133
+ },
134
+ null,
135
+ 2,
136
+ ),
137
+ );
138
+
139
+ // npm install
140
+ if (!skipInstall) {
141
+ console.log("\nInstalling dependencies...\n");
142
+ try {
143
+ execSync("npm install", { cwd: targetDir, stdio: "inherit" });
144
+ } catch {
145
+ console.warn("\nWarning: npm install failed. Run it manually in the project directory.");
146
+ }
147
+ }
148
+ }