@stratum-hq/create 0.2.2 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +419 -0
  2. package/package.json +18 -5
package/dist/index.js ADDED
@@ -0,0 +1,419 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import * as fs from "fs";
5
+ import * as path from "path";
6
+ import { execSync } from "child_process";
7
+ import { fileURLToPath } from "url";
8
+ function parseArgs(argv) {
9
+ const args = [...argv];
10
+ let projectName = null;
11
+ let template = "express";
12
+ let skipInstall = false;
13
+ let force = false;
14
+ for (let i = 0; i < args.length; i++) {
15
+ const arg = args[i];
16
+ if (arg === "--skip-install") {
17
+ skipInstall = true;
18
+ } else if (arg === "--force") {
19
+ force = true;
20
+ } else if (arg === "--template") {
21
+ const val = args[i + 1];
22
+ if (val === "express" || val === "fastify" || val === "nextjs") {
23
+ template = val;
24
+ i++;
25
+ } else {
26
+ console.error(`Unknown template: ${val}. Available: express, fastify, nextjs`);
27
+ process.exit(1);
28
+ }
29
+ } else if (arg.startsWith("--template=")) {
30
+ const val = arg.slice("--template=".length);
31
+ if (val === "express" || val === "fastify" || val === "nextjs") {
32
+ template = val;
33
+ } else {
34
+ console.error(`Unknown template: ${val}. Available: express, fastify, nextjs`);
35
+ process.exit(1);
36
+ }
37
+ } else if (!arg.startsWith("--")) {
38
+ projectName = arg;
39
+ }
40
+ }
41
+ return { projectName, template, skipInstall, force };
42
+ }
43
+ function printUsage() {
44
+ console.log("Usage: create-stratum <project-name> [options]");
45
+ console.log("");
46
+ console.log("Options:");
47
+ console.log(" --template <express|fastify|nextjs> Framework template (default: express)");
48
+ console.log(" --skip-install Skip npm install");
49
+ console.log(" --force Overwrite existing directory");
50
+ console.log("");
51
+ console.log("Examples:");
52
+ console.log(" npx @stratum-hq/create my-app");
53
+ console.log(" npx @stratum-hq/create my-app --template fastify");
54
+ console.log(" npx @stratum-hq/create my-app --template nextjs --skip-install");
55
+ }
56
+ function writeFile(filePath, content) {
57
+ const dir = path.dirname(filePath);
58
+ if (!fs.existsSync(dir)) {
59
+ fs.mkdirSync(dir, { recursive: true });
60
+ }
61
+ fs.writeFileSync(filePath, content, "utf8");
62
+ console.log(` created ${path.relative(process.cwd(), filePath)}`);
63
+ }
64
+ function generatePackageJson(projectName, template) {
65
+ const frameworkDeps = {
66
+ express: {
67
+ express: "^4.18.0",
68
+ "@types/express": "^4.17.21"
69
+ },
70
+ fastify: {
71
+ fastify: "^4.26.0"
72
+ },
73
+ 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"
79
+ }
80
+ };
81
+ const deps = {
82
+ "@stratum-hq/lib": "^0.2.0",
83
+ pg: "^8.11.0",
84
+ ...frameworkDeps[template]
85
+ };
86
+ return JSON.stringify(
87
+ {
88
+ name: projectName,
89
+ version: "0.1.0",
90
+ private: true,
91
+ type: "module",
92
+ scripts: template === "nextjs" ? { dev: "next dev", build: "next build", start: "next start" } : { dev: "node --watch src/index.js", build: "tsc", start: "node dist/index.js" },
93
+ dependencies: deps,
94
+ devDependencies: {
95
+ typescript: "^5.3.0",
96
+ "@types/node": "^20.11.0"
97
+ },
98
+ engines: {
99
+ node: ">=20.0.0"
100
+ }
101
+ },
102
+ null,
103
+ 2
104
+ );
105
+ }
106
+ function generateDockerCompose(projectName) {
107
+ const dbName = projectName.replace(/[^a-z0-9]/gi, "_").toLowerCase();
108
+ return `# Docker Compose for ${projectName}
109
+ # Start with: docker compose up -d
110
+ version: "3.8"
111
+
112
+ services:
113
+ db:
114
+ image: postgres:16-alpine
115
+ environment:
116
+ POSTGRES_DB: ${dbName}
117
+ POSTGRES_USER: ${dbName}
118
+ POSTGRES_PASSWORD: dev_password
119
+ ports:
120
+ - "5432:5432"
121
+ volumes:
122
+ - db_data:/var/lib/postgresql/data
123
+ - ./init.sql:/docker-entrypoint-initdb.d/init.sql
124
+ healthcheck:
125
+ test: ["CMD-SHELL", "pg_isready -U ${dbName}"]
126
+ interval: 5s
127
+ timeout: 5s
128
+ retries: 5
129
+
130
+ volumes:
131
+ db_data:
132
+ `;
133
+ }
134
+ function generateInitSql(projectName) {
135
+ const dbName = projectName.replace(/[^a-z0-9]/gi, "_").toLowerCase();
136
+ return `-- Initialize ${projectName} database
137
+ -- Enable required extensions for Stratum multi-tenancy
138
+ CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
139
+ CREATE EXTENSION IF NOT EXISTS "ltree";
140
+
141
+ -- The ltree extension enables hierarchical tenant trees
142
+ -- uuid-ossp provides uuid_generate_v4() for tenant IDs
143
+ COMMENT ON DATABASE ${dbName} IS 'Multi-tenant database for ${projectName}';
144
+ `;
145
+ }
146
+ function generateEnv(projectName) {
147
+ const dbName = projectName.replace(/[^a-z0-9]/gi, "_").toLowerCase();
148
+ return `# Environment variables for ${projectName}
149
+ # Copy to .env and fill in values
150
+
151
+ # Database
152
+ DATABASE_URL=postgres://${dbName}:dev_password@localhost:5432/${dbName}
153
+
154
+ # Authentication
155
+ JWT_SECRET=change-me-in-production
156
+
157
+ # Stratum Control Plane
158
+ STRATUM_URL=http://localhost:3001
159
+ STRATUM_API_KEY=sk_test_your_key_here
160
+
161
+ NODE_ENV=development
162
+ `;
163
+ }
164
+ function generateExpressServer(projectName) {
165
+ return `import express from "express";
166
+ import { Pool } from "pg";
167
+
168
+ const app = express();
169
+ const port = process.env.PORT || 3000;
170
+
171
+ const pool = new Pool({
172
+ connectionString: process.env.DATABASE_URL,
173
+ });
174
+
175
+ app.use(express.json());
176
+
177
+ app.get("/health", (_req, res) => {
178
+ res.json({ status: "ok", project: "${projectName}" });
179
+ });
180
+
181
+ app.get("/tenants", async (_req, res) => {
182
+ try {
183
+ // Example: query your tenant table
184
+ // const { rows } = await pool.query("SELECT * FROM tenants LIMIT 20");
185
+ // res.json(rows);
186
+ res.json({ message: "Replace this with your tenant queries", pool: !!pool });
187
+ } catch (err) {
188
+ res.status(500).json({ error: String(err) });
189
+ }
190
+ });
191
+
192
+ app.listen(port, () => {
193
+ console.log(\`${projectName} server running on http://localhost:\${port}\`);
194
+ });
195
+ `;
196
+ }
197
+ function generateFastifyServer(projectName) {
198
+ return `import Fastify from "fastify";
199
+ import { Pool } from "pg";
200
+
201
+ const fastify = Fastify({ logger: true });
202
+ const port = Number(process.env.PORT) || 3000;
203
+
204
+ const pool = new Pool({
205
+ connectionString: process.env.DATABASE_URL,
206
+ });
207
+
208
+ fastify.get("/health", async () => {
209
+ return { status: "ok", project: "${projectName}" };
210
+ });
211
+
212
+ fastify.get("/tenants", async (_request, reply) => {
213
+ try {
214
+ // Example: query your tenant table
215
+ // const { rows } = await pool.query("SELECT * FROM tenants LIMIT 20");
216
+ // return rows;
217
+ return { message: "Replace this with your tenant queries", pool: !!pool };
218
+ } catch (err) {
219
+ return reply.status(500).send({ error: String(err) });
220
+ }
221
+ });
222
+
223
+ fastify.listen({ port, host: "0.0.0.0" }, (err) => {
224
+ if (err) {
225
+ fastify.log.error(err);
226
+ process.exit(1);
227
+ }
228
+ });
229
+ `;
230
+ }
231
+ function generateNextjsPage(projectName) {
232
+ return `// app/page.tsx \u2014 ${projectName} root page
233
+ export default function Home() {
234
+ return (
235
+ <main style={{ padding: "2rem", fontFamily: "sans-serif" }}>
236
+ <h1>${projectName}</h1>
237
+ <p>Multi-tenant app powered by Stratum.</p>
238
+ <ul>
239
+ <li>Configure tenants via the Stratum control plane</li>
240
+ <li>Access tenant context via <code>x-tenant-id</code> header</li>
241
+ <li>Use <code>@stratum-hq/lib</code> for tenant resolution</li>
242
+ </ul>
243
+ </main>
244
+ );
245
+ }
246
+ `;
247
+ }
248
+ function generateNextjsMiddleware() {
249
+ return `// middleware.ts \u2014 tenant resolution via subdomain or header
250
+ import { NextRequest, NextResponse } from "next/server";
251
+
252
+ export function middleware(request: NextRequest) {
253
+ const hostname = request.headers.get("host") || "";
254
+ const subdomain = hostname.split(".")[0];
255
+ const headerTenantId = request.headers.get("x-tenant-id");
256
+ const pathTenantId = request.nextUrl.pathname.match(/^\\/tenant\\/([^/]+)/)?.[1];
257
+
258
+ const tenantId = headerTenantId || pathTenantId || subdomain;
259
+
260
+ const requestHeaders = new Headers(request.headers);
261
+ if (tenantId && tenantId !== "localhost" && tenantId !== "www") {
262
+ requestHeaders.set("x-tenant-id", tenantId);
263
+ }
264
+
265
+ return NextResponse.next({ request: { headers: requestHeaders } });
266
+ }
267
+
268
+ export const config = {
269
+ matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
270
+ };
271
+ `;
272
+ }
273
+ function generateReadme(projectName, template) {
274
+ const devCmd = template === "nextjs" ? "npm run dev" : "node --env-file=.env src/index.ts";
275
+ return `# ${projectName}
276
+
277
+ A multi-tenant application built with [Stratum](https://github.com/stratum-hq/Stratum).
278
+
279
+ ## Getting started
280
+
281
+ ### 1. Start the database
282
+
283
+ \`\`\`bash
284
+ docker compose up -d
285
+ \`\`\`
286
+
287
+ ### 2. Configure environment
288
+
289
+ \`\`\`bash
290
+ cp .env.example .env
291
+ # Edit .env \u2014 update DATABASE_URL, JWT_SECRET, and STRATUM_API_KEY
292
+ \`\`\`
293
+
294
+ ### 3. Install dependencies
295
+
296
+ \`\`\`bash
297
+ npm install
298
+ \`\`\`
299
+
300
+ ### 4. Run the app
301
+
302
+ \`\`\`bash
303
+ ${devCmd}
304
+ \`\`\`
305
+
306
+ ## Project structure
307
+
308
+ \`\`\`
309
+ ${projectName}/
310
+ \u251C\u2500\u2500 src/ # Application source
311
+ \u251C\u2500\u2500 docker-compose.yml
312
+ \u251C\u2500\u2500 init.sql # DB extensions (uuid-ossp, ltree)
313
+ \u251C\u2500\u2500 .env.example
314
+ \u2514\u2500\u2500 package.json
315
+ \`\`\`
316
+
317
+ ## Multi-tenancy
318
+
319
+ This project uses Stratum for hierarchical multi-tenancy:
320
+
321
+ - **Tenant resolution** \u2014 via JWT claim, subdomain, or \`x-tenant-id\` header
322
+ - **Config inheritance** \u2014 settings flow down the tenant tree with override support
323
+ - **Permission ABAC** \u2014 role-based permissions with tenant-scoped enforcement
324
+
325
+ See the [Stratum docs](https://github.com/stratum-hq/Stratum) for full reference.
326
+ `;
327
+ }
328
+ function createProject(projectName, template, targetDir, skipInstall) {
329
+ console.log(`
330
+ Creating ${projectName} with ${template} template...
331
+ `);
332
+ writeFile(
333
+ path.join(targetDir, "package.json"),
334
+ generatePackageJson(projectName, template)
335
+ );
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));
339
+ if (template === "express") {
340
+ writeFile(path.join(targetDir, "src", "index.ts"), generateExpressServer(projectName));
341
+ } else if (template === "fastify") {
342
+ writeFile(path.join(targetDir, "src", "index.ts"), generateFastifyServer(projectName));
343
+ } else if (template === "nextjs") {
344
+ writeFile(path.join(targetDir, "src", "app", "page.tsx"), generateNextjsPage(projectName));
345
+ writeFile(path.join(targetDir, "middleware.ts"), generateNextjsMiddleware());
346
+ }
347
+ writeFile(path.join(targetDir, "README.md"), generateReadme(projectName, template));
348
+ if (!skipInstall) {
349
+ console.log("\nInstalling dependencies...\n");
350
+ try {
351
+ execSync("npm install", { cwd: targetDir, stdio: "inherit" });
352
+ } catch {
353
+ console.warn("\nWarning: npm install failed. Run it manually in the project directory.");
354
+ }
355
+ }
356
+ }
357
+ var VALID_PROJECT_NAME = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
358
+ function validateProjectName(projectName) {
359
+ if (!projectName) {
360
+ return "Project name must not be empty.";
361
+ }
362
+ if (!VALID_PROJECT_NAME.test(projectName)) {
363
+ return `"${projectName}" is not a valid project name. Use only letters, numbers, dots, hyphens, and underscores. Must start with a letter or number.`;
364
+ }
365
+ const targetDir = path.resolve(process.cwd(), projectName);
366
+ if (path.dirname(targetDir) !== process.cwd()) {
367
+ return "Project name must not contain path separators.";
368
+ }
369
+ return null;
370
+ }
371
+ function main(argv) {
372
+ const { projectName, template, skipInstall, force } = parseArgs(argv);
373
+ if (!projectName) {
374
+ printUsage();
375
+ process.exit(1);
376
+ }
377
+ const validationError = validateProjectName(projectName);
378
+ if (validationError) {
379
+ console.error(`Error: ${validationError}`);
380
+ process.exit(1);
381
+ }
382
+ const targetDir = path.resolve(process.cwd(), projectName);
383
+ if (fs.existsSync(targetDir) && !force) {
384
+ console.error(`Error: Directory "${projectName}" already exists.`);
385
+ console.error(`Use --force to overwrite.`);
386
+ process.exit(1);
387
+ }
388
+ if (fs.existsSync(targetDir) && force) {
389
+ fs.rmSync(targetDir, { recursive: true, force: true });
390
+ }
391
+ fs.mkdirSync(targetDir, { recursive: true });
392
+ createProject(projectName, template, targetDir, skipInstall);
393
+ console.log(`
394
+ Success! Created ${projectName} at ${targetDir}
395
+ `);
396
+ console.log("Next steps:\n");
397
+ console.log(` cd ${projectName}`);
398
+ console.log(" docker compose up -d");
399
+ console.log(" cp .env.example .env");
400
+ if (skipInstall) {
401
+ console.log(" npm install");
402
+ }
403
+ if (template === "nextjs") {
404
+ console.log(" npm run dev");
405
+ } else {
406
+ console.log(" node --env-file=.env src/index.ts");
407
+ }
408
+ console.log("");
409
+ }
410
+ var __filename = fileURLToPath(import.meta.url);
411
+ if (process.argv[1] === __filename) {
412
+ main(process.argv.slice(2));
413
+ }
414
+ export {
415
+ createProject,
416
+ main,
417
+ parseArgs,
418
+ validateProjectName
419
+ };
package/package.json CHANGED
@@ -1,15 +1,28 @@
1
1
  {
2
2
  "name": "@stratum-hq/create",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Create a new Stratum multi-tenancy project",
5
- "bin": { "create-stratum": "./dist/index.js" },
5
+ "bin": {
6
+ "create-stratum": "./dist/index.js"
7
+ },
6
8
  "type": "module",
7
9
  "scripts": {
8
10
  "build": "esbuild src/index.ts --bundle --platform=node --format=esm --outfile=dist/index.js --external:@stratum-hq/*",
9
11
  "test": "vitest run"
10
12
  },
11
- "devDependencies": { "esbuild": "^0.20.0", "vitest": "^1.6.0", "typescript": "^5.3.0" },
12
- "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/" },
13
+ "devDependencies": {
14
+ "esbuild": "^0.20.0",
15
+ "vitest": "^1.6.0",
16
+ "typescript": "^5.3.0"
17
+ },
18
+ "publishConfig": {
19
+ "access": "public",
20
+ "registry": "https://registry.npmjs.org/"
21
+ },
13
22
  "license": "MIT",
14
- "repository": { "type": "git", "url": "https://github.com/stratum-hq/Stratum.git", "directory": "packages/create" }
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "https://github.com/stratum-hq/Stratum.git",
26
+ "directory": "packages/create"
27
+ }
15
28
  }