tempest-express-sdk 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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mauricio Benjamin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,93 @@
1
+ # tempest-express-sdk
2
+
3
+ > Shared **Express + Zod + tempest-db-js** building blocks β€” a Node.js/TypeScript
4
+ > port of the conventions in [`tempest-fastapi-sdk`](https://pypi.org/project/tempest-fastapi-sdk/).
5
+
6
+ πŸ“– **Documentation:** [PortuguΓͺs (BR)](https://mauriciobenjamin700.github.io/tempest-express-sdk/) Β· [English (US)](https://mauriciobenjamin700.github.io/tempest-express-sdk/en/)
7
+
8
+ Strict TypeScript, `@`-alias imports (no `.js` suffixes), native **Swagger UI +
9
+ Redoc** generated straight from your Zod schemas, and a layered
10
+ router β†’ controller β†’ service β†’ repository β†’ model stack built on
11
+ [`tempest-db-js`](https://www.npmjs.com/package/tempest-db-js).
12
+
13
+ > ⚠️ **Status: pre-alpha (v0.1.0).** Foundation layer is built and tested; many
14
+ > `tempest-fastapi-sdk` feature modules are not yet ported (see Roadmap).
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ npm install tempest-express-sdk tempest-db-js express zod
20
+ ```
21
+
22
+ `tempest-db-js` is a required peer dependency.
23
+
24
+ ## What's inside
25
+
26
+ | Area | Exports |
27
+ | --- | --- |
28
+ | **core** | `JSONLogger`, `configureLogging`, request-id context (`getRequestId`, `runWithRequestContext`), `defineEnum` |
29
+ | **exceptions** | `AppException` + `ConflictException` / `NotFoundException` / `UnauthorizedException` / `ForbiddenException` / `ValidationException` / `TooManyRequestsException` / `InvalidTokenException` / `ExpiredTokenException`, `MessageCatalog` (i18n) |
30
+ | **schemas** | `z` (OpenAPI-augmented), `baseResponseSchema`, `toDict`, `paginationFilterSchema` / `paginationSchema`, cursor pagination, `encodeCursor` / `decodeCursor` |
31
+ | **settings** | `loadSettings`, `baseAppSettingsShape` (server / database / CORS) |
32
+ | **db** | re-exports `tempest-db-js` + `BaseModel`, `tableNameFor`, soft-delete / audit column helpers |
33
+ | **services / controllers** | `BaseService`, `BaseController` over a typed repository |
34
+ | **api** | `createApp`, `runServer`, `registerExceptionHandlers`, `createOpenApiRegistry`, `generateOpenApiDocument`, `mountSwaggerUi`, `mountRedoc`, `makeHealthRouter` |
35
+
36
+ ## Quick start
37
+
38
+ ```ts
39
+ import { createApp, createOpenApiRegistry, runServer, z } from "tempest-express-sdk";
40
+
41
+ const registry = createOpenApiRegistry();
42
+ const Item = registry.register("Item", z.object({ id: z.string().uuid(), name: z.string() }));
43
+
44
+ const app = await createApp({
45
+ corsOrigins: "*",
46
+ openapi: { registry, info: { title: "My API", version: "1.0.0" } },
47
+ configure: (app) => {
48
+ app.get("/api/items", (_req, res) => res.json([]));
49
+ },
50
+ });
51
+
52
+ await runServer(app, { host: "127.0.0.1", port: 8000 });
53
+ // Swagger UI β†’ /docs Β· Redoc β†’ /redoc Β· spec β†’ /openapi.json Β· health β†’ /health
54
+ ```
55
+
56
+ ## CLI
57
+
58
+ ```bash
59
+ npx tempest-express new my-service # scaffold a runnable layered service
60
+ cd my-service && npm install && npm run dev
61
+ ```
62
+
63
+ The generated project is a complete vertical slice (model β†’ repository β†’ service
64
+ β†’ controller β†’ router β†’ app) pre-wired with `createApp`, Swagger/Redoc and Zod.
65
+
66
+ ## Develop
67
+
68
+ ```bash
69
+ npm install
70
+ npm run test:types # tsc --noEmit
71
+ npm test # vitest
72
+ npm run build # tsup β†’ dual ESM + CJS + .d.ts (+ CLI bin)
73
+ npm run lint # biome
74
+ ```
75
+
76
+ ## Roadmap
77
+
78
+ Ported: core, exceptions (+ i18n), schemas, pagination, settings, `BaseModel`,
79
+ `BaseRepository`/`BaseService`/`BaseController`, `createApp` + Swagger/Redoc,
80
+ health, CLI `new`.
81
+
82
+ Also ported: BR utils (CPF/CNPJ/CEP/phone/UF + cities), `PasswordUtils`,
83
+ `JWTUtils`, opaque tokens, `AttemptThrottle`, the `auth` module (signup/login/
84
+ refresh + JWT guards), CLI `generate`/`secret`/`docker-compose`, and the
85
+ bilingual MkDocs docs site.
86
+
87
+ Not yet ported from `tempest-fastapi-sdk`: sessions, cache (Redis), queue
88
+ (RabbitMQ), tasks, webpush, websockets, feature flags, object storage, metrics,
89
+ admin, SSE, and the MFA / email / password-reset flows.
90
+
91
+ ## License
92
+
93
+ MIT
@@ -0,0 +1,6 @@
1
+ // src/version.ts
2
+ var VERSION = "0.1.0";
3
+
4
+ export { VERSION };
5
+ //# sourceMappingURL=chunk-2NB7ZA7G.js.map
6
+ //# sourceMappingURL=chunk-2NB7ZA7G.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/version.ts"],"names":[],"mappings":";AACO,IAAM,OAAA,GAAU","file":"chunk-2NB7ZA7G.js","sourcesContent":["/** The installed SDK version. Single source of truth for the barrel + CLI. */\nexport const VERSION = \"0.1.0\";\n"]}
package/dist/cli.cjs ADDED
@@ -0,0 +1,546 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ var crypto = require('crypto');
5
+ var promises = require('fs/promises');
6
+ var path = require('path');
7
+ var util = require('util');
8
+
9
+ // src/cli/template.ts
10
+ var SDK_VERSION = "^0.1.0";
11
+ function projectFiles(name) {
12
+ return {
13
+ "package.json": `${JSON.stringify(
14
+ {
15
+ name,
16
+ version: "0.1.0",
17
+ private: true,
18
+ type: "module",
19
+ scripts: {
20
+ dev: "tsx watch main.ts",
21
+ start: "tsx main.ts",
22
+ build: "tsc -p tsconfig.json",
23
+ typecheck: "tsc --noEmit"
24
+ },
25
+ dependencies: {
26
+ express: "^5.1.0",
27
+ "tempest-db-js": "^0.1.0",
28
+ "tempest-express-sdk": SDK_VERSION,
29
+ zod: "^3.24.1"
30
+ },
31
+ devDependencies: {
32
+ "@types/express": "^5.0.0",
33
+ "@types/node": "^22.10.0",
34
+ tsx: "^4.19.2",
35
+ typescript: "^5.7.0"
36
+ }
37
+ },
38
+ null,
39
+ 2
40
+ )}
41
+ `,
42
+ "tsconfig.json": `${JSON.stringify(
43
+ {
44
+ compilerOptions: {
45
+ target: "ES2022",
46
+ module: "ESNext",
47
+ moduleResolution: "Bundler",
48
+ lib: ["ES2022"],
49
+ strict: true,
50
+ noUncheckedIndexedAccess: true,
51
+ verbatimModuleSyntax: true,
52
+ esModuleInterop: true,
53
+ skipLibCheck: true,
54
+ forceConsistentCasingInFileNames: true,
55
+ outDir: "dist",
56
+ paths: { "@/*": ["./src/*"] }
57
+ },
58
+ include: ["src", "main.ts"]
59
+ },
60
+ null,
61
+ 2
62
+ )}
63
+ `,
64
+ ".gitignore": "node_modules\ndist\n.env\n*.log\n",
65
+ ".env.example": "HOST=127.0.0.1\nPORT=8000\nDEBUG=false\nDATABASE_URL=sqlite://./app.db\nCORS_ORIGINS=*\n",
66
+ "main.ts": `import { run } from "@/index";
67
+
68
+ run();
69
+ `,
70
+ "src/index.ts": `import { run } from "@/server";
71
+
72
+ export { run };
73
+ `,
74
+ "src/core/settings.ts": `import { baseAppSettingsShape, loadSettings, z } from "tempest-express-sdk";
75
+
76
+ /** Application settings \u2014 extend the SDK base shape with project fields. */
77
+ export const settingsSchema = z.object({
78
+ ...baseAppSettingsShape,
79
+ });
80
+
81
+ export const settings = loadSettings(settingsSchema);
82
+ `,
83
+ "src/db/models/itemModel.ts": `import { BaseModel, column, tableNameFor } from "tempest-express-sdk";
84
+
85
+ /** A sample domain model. Replace with your own. */
86
+ export class ItemModel extends BaseModel {
87
+ static tablename = tableNameFor("ItemModel");
88
+ name = column.text().notNull();
89
+ price = column.integer().notNull();
90
+ }
91
+ `,
92
+ "src/schemas/item.ts": `import { baseResponseSchema, z } from "tempest-express-sdk";
93
+
94
+ /** Request payload to create an item. */
95
+ export const itemCreateSchema = z
96
+ .object({
97
+ name: z.string().min(1).openapi({ description: "The item name." }),
98
+ price: z.number().int().min(0).openapi({ description: "Price in cents." }),
99
+ })
100
+ .openapi("ItemCreate");
101
+
102
+ /** Response payload for an item (base columns + domain fields). */
103
+ export const itemResponseSchema = baseResponseSchema
104
+ .extend({
105
+ name: z.string(),
106
+ price: z.number().int(),
107
+ })
108
+ .openapi("Item");
109
+
110
+ export type ItemCreate = z.infer<typeof itemCreateSchema>;
111
+ export type ItemResponse = z.infer<typeof itemResponseSchema>;
112
+ `,
113
+ "src/db/repositories/itemRepository.ts": `import { type AsyncSession, BaseRepository } from "tempest-express-sdk";
114
+ import { ItemModel } from "@/db/models/itemModel";
115
+
116
+ /** Data-access layer for items. */
117
+ export class ItemRepository extends BaseRepository<typeof ItemModel> {
118
+ constructor(session: AsyncSession) {
119
+ super(ItemModel, session);
120
+ }
121
+ }
122
+ `,
123
+ "src/services/itemService.ts": `import { BaseService } from "tempest-express-sdk";
124
+ import type { ItemRepository } from "@/db/repositories/itemRepository";
125
+ import type { ItemModel } from "@/db/models/itemModel";
126
+ import type { ItemResponse } from "@/schemas/item";
127
+
128
+ /** Business logic for items. Maps raw rows to the response shape. */
129
+ export class ItemService extends BaseService<typeof ItemModel, ItemResponse> {
130
+ constructor(repository: ItemRepository) {
131
+ super(repository, (row) => ({
132
+ id: row.id,
133
+ isActive: row.isActive,
134
+ createdAt: row.createdAt,
135
+ updatedAt: row.updatedAt,
136
+ name: row.name,
137
+ price: row.price,
138
+ }));
139
+ }
140
+ }
141
+ `,
142
+ "src/controllers/itemController.ts": `import { BaseController } from "tempest-express-sdk";
143
+ import type { ItemModel } from "@/db/models/itemModel";
144
+ import type { ItemResponse } from "@/schemas/item";
145
+ import type { ItemService } from "@/services/itemService";
146
+
147
+ /** Orchestration boundary between the router and the item service. */
148
+ export class ItemController extends BaseController<typeof ItemModel, ItemResponse> {
149
+ constructor(service: ItemService) {
150
+ super(service);
151
+ }
152
+ }
153
+ `,
154
+ "src/api/routers/items.ts": `import { Router } from "express";
155
+ import type { OpenAPIRegistry } from "tempest-express-sdk";
156
+ import { itemCreateSchema, itemResponseSchema } from "@/schemas/item";
157
+
158
+ /**
159
+ * Build the items router and register its OpenAPI paths.
160
+ *
161
+ * NOTE: wire a real controller (with a DB session) here. This stub returns
162
+ * static data so a freshly generated project boots without a database.
163
+ */
164
+ export function makeItemsRouter(registry: OpenAPIRegistry): Router {
165
+ const router = Router();
166
+
167
+ registry.registerPath({
168
+ method: "get",
169
+ path: "/api/items",
170
+ summary: "List items",
171
+ responses: {
172
+ 200: {
173
+ description: "The items",
174
+ content: { "application/json": { schema: itemResponseSchema.array() } },
175
+ },
176
+ },
177
+ });
178
+
179
+ registry.registerPath({
180
+ method: "post",
181
+ path: "/api/items",
182
+ summary: "Create an item",
183
+ request: {
184
+ body: { content: { "application/json": { schema: itemCreateSchema } } },
185
+ },
186
+ responses: {
187
+ 201: {
188
+ description: "The created item",
189
+ content: { "application/json": { schema: itemResponseSchema } },
190
+ },
191
+ },
192
+ });
193
+
194
+ router.get("/api/items", (_req, res) => {
195
+ res.json([]);
196
+ });
197
+
198
+ router.post("/api/items", (req, res) => {
199
+ const data = itemCreateSchema.parse(req.body);
200
+ res.status(201).json({
201
+ id: "00000000-0000-0000-0000-000000000000",
202
+ isActive: true,
203
+ createdAt: new Date().toISOString(),
204
+ updatedAt: new Date().toISOString(),
205
+ ...data,
206
+ });
207
+ });
208
+
209
+ return router;
210
+ }
211
+ `,
212
+ "src/api/app.ts": `import type { Express } from "express";
213
+ import { createApp, createOpenApiRegistry } from "tempest-express-sdk";
214
+ import { settings } from "@/core/settings";
215
+ import { makeItemsRouter } from "@/api/routers/items";
216
+
217
+ /** Build the configured Express application. */
218
+ export async function makeApp(): Promise<Express> {
219
+ const registry = createOpenApiRegistry();
220
+
221
+ return createApp({
222
+ corsOrigins: settings.CORS_ORIGINS,
223
+ openapi: {
224
+ registry,
225
+ info: { title: "${name}", version: "0.1.0", description: "Powered by tempest-express-sdk." },
226
+ },
227
+ configure: (app) => {
228
+ app.use(makeItemsRouter(registry));
229
+ },
230
+ });
231
+ }
232
+ `,
233
+ "src/server.ts": `import { runServer } from "tempest-express-sdk";
234
+ import { settings } from "@/core/settings";
235
+ import { makeApp } from "@/api/app";
236
+
237
+ /** Build the app and start listening. */
238
+ export async function run(): Promise<void> {
239
+ const app = await makeApp();
240
+ await runServer(app, { host: settings.HOST, port: settings.PORT });
241
+ }
242
+ `,
243
+ "docker-compose.yml": dockerComposeFile(name),
244
+ "README.md": `# ${name}
245
+
246
+ Generated with \`tempest-express new\` \u2014 an Express + Zod + tempest-db-js service.
247
+
248
+ ## Develop
249
+
250
+ \`\`\`bash
251
+ npm install
252
+ cp .env.example .env
253
+ npm run dev
254
+ \`\`\`
255
+
256
+ - API: http://127.0.0.1:8000/api/items
257
+ - Swagger UI: http://127.0.0.1:8000/docs
258
+ - Redoc: http://127.0.0.1:8000/redoc
259
+ - Health: http://127.0.0.1:8000/health
260
+ \`\`\`
261
+ `
262
+ };
263
+ }
264
+ function toCamel(name) {
265
+ return name.charAt(0).toLowerCase() + name.slice(1);
266
+ }
267
+ function toSnake(name) {
268
+ return name.replace(/(?<!^)(?=[A-Z])/g, "_").toLowerCase();
269
+ }
270
+ function resourceFiles(pascal) {
271
+ const camel = toCamel(pascal);
272
+ const table = toSnake(pascal);
273
+ return {
274
+ [`src/db/models/${camel}Model.ts`]: `import { BaseModel, column, tableNameFor } from "tempest-express-sdk";
275
+
276
+ /** The ${pascal} domain model. */
277
+ export class ${pascal}Model extends BaseModel {
278
+ static tablename = tableNameFor("${pascal}Model"); // "${table}"
279
+ name = column.text().notNull();
280
+ }
281
+ `,
282
+ [`src/schemas/${camel}.ts`]: `import { baseResponseSchema, z } from "tempest-express-sdk";
283
+
284
+ /** Request payload to create a ${pascal}. */
285
+ export const ${camel}CreateSchema = z
286
+ .object({ name: z.string().min(1) })
287
+ .openapi("${pascal}Create");
288
+
289
+ /** Response payload for a ${pascal}. */
290
+ export const ${camel}ResponseSchema = baseResponseSchema
291
+ .extend({ name: z.string() })
292
+ .openapi("${pascal}");
293
+
294
+ export type ${pascal}Create = z.infer<typeof ${camel}CreateSchema>;
295
+ export type ${pascal}Response = z.infer<typeof ${camel}ResponseSchema>;
296
+ `,
297
+ [`src/db/repositories/${camel}Repository.ts`]: `import { type AsyncSession, BaseRepository } from "tempest-express-sdk";
298
+ import { ${pascal}Model } from "@/db/models/${camel}Model";
299
+
300
+ /** Data-access layer for ${pascal}. */
301
+ export class ${pascal}Repository extends BaseRepository<typeof ${pascal}Model> {
302
+ constructor(session: AsyncSession) {
303
+ super(${pascal}Model, session);
304
+ }
305
+ }
306
+ `,
307
+ [`src/services/${camel}Service.ts`]: `import { BaseService } from "tempest-express-sdk";
308
+ import type { ${pascal}Model } from "@/db/models/${camel}Model";
309
+ import type { ${pascal}Repository } from "@/db/repositories/${camel}Repository";
310
+ import type { ${pascal}Response } from "@/schemas/${camel}";
311
+
312
+ /** Business logic for ${pascal}. */
313
+ export class ${pascal}Service extends BaseService<typeof ${pascal}Model, ${pascal}Response> {
314
+ constructor(repository: ${pascal}Repository) {
315
+ super(repository, (row) => ({
316
+ id: row.id,
317
+ isActive: row.isActive,
318
+ createdAt: row.createdAt,
319
+ updatedAt: row.updatedAt,
320
+ name: row.name,
321
+ }));
322
+ }
323
+ }
324
+ `,
325
+ [`src/controllers/${camel}Controller.ts`]: `import { BaseController } from "tempest-express-sdk";
326
+ import type { ${pascal}Model } from "@/db/models/${camel}Model";
327
+ import type { ${pascal}Response } from "@/schemas/${camel}";
328
+ import type { ${pascal}Service } from "@/services/${camel}Service";
329
+
330
+ /** Orchestration boundary for ${pascal}. */
331
+ export class ${pascal}Controller extends BaseController<typeof ${pascal}Model, ${pascal}Response> {
332
+ constructor(service: ${pascal}Service) {
333
+ super(service);
334
+ }
335
+ }
336
+ `,
337
+ [`src/api/routers/${camel}s.ts`]: `import { Router } from "express";
338
+ import type { OpenAPIRegistry } from "tempest-express-sdk";
339
+ import { ${camel}CreateSchema, ${camel}ResponseSchema } from "@/schemas/${camel}";
340
+
341
+ /** Build the ${pascal} router and register its OpenAPI paths. */
342
+ export function make${pascal}sRouter(registry: OpenAPIRegistry): Router {
343
+ const router = Router();
344
+
345
+ registry.registerPath({
346
+ method: "get",
347
+ path: "/api/${camel}s",
348
+ summary: "List ${camel}s",
349
+ responses: {
350
+ 200: {
351
+ description: "OK",
352
+ content: { "application/json": { schema: ${camel}ResponseSchema.array() } },
353
+ },
354
+ },
355
+ });
356
+ registry.registerPath({
357
+ method: "post",
358
+ path: "/api/${camel}s",
359
+ summary: "Create a ${camel}",
360
+ request: {
361
+ body: { content: { "application/json": { schema: ${camel}CreateSchema } } },
362
+ },
363
+ responses: {
364
+ 201: {
365
+ description: "Created",
366
+ content: { "application/json": { schema: ${camel}ResponseSchema } },
367
+ },
368
+ },
369
+ });
370
+
371
+ router.get("/api/${camel}s", (_req, res) => res.json([]));
372
+ router.post("/api/${camel}s", (req, res) => {
373
+ const data = ${camel}CreateSchema.parse(req.body);
374
+ res.status(201).json({
375
+ id: "00000000-0000-0000-0000-000000000000",
376
+ isActive: true,
377
+ createdAt: new Date().toISOString(),
378
+ updatedAt: new Date().toISOString(),
379
+ ...data,
380
+ });
381
+ });
382
+
383
+ return router;
384
+ }
385
+ `
386
+ };
387
+ }
388
+ function dockerComposeFile(name) {
389
+ return `services:
390
+ postgres:
391
+ image: postgres:16-alpine
392
+ environment:
393
+ POSTGRES_USER: ${name}
394
+ POSTGRES_PASSWORD: ${name}
395
+ POSTGRES_DB: ${name}
396
+ ports:
397
+ - "5432:5432"
398
+ volumes:
399
+ - pgdata:/var/lib/postgresql/data
400
+ redis:
401
+ image: redis:7-alpine
402
+ ports:
403
+ - "6379:6379"
404
+
405
+ volumes:
406
+ pgdata:
407
+ `;
408
+ }
409
+
410
+ // src/version.ts
411
+ var VERSION = "0.1.0";
412
+
413
+ // src/cli/index.ts
414
+ var USAGE = `tempest-express \u2014 Express SDK CLI
415
+
416
+ Usage:
417
+ tempest-express new <name> [--dir <path>] Scaffold a new service
418
+ tempest-express generate <Name> [--dir <path>] Scaffold a CRUD resource
419
+ tempest-express secret [--bytes <n>] Print a random secret
420
+ tempest-express docker-compose [--dir <path>] Write a docker-compose.yml
421
+ tempest-express db Migration guidance
422
+ tempest-express --version Print the version
423
+ tempest-express --help Show this help
424
+ `;
425
+ var DB_HELP = `Migrations are handled by tempest-db-js (programmatic API):
426
+
427
+ import { ... } from "tempest-db-js/migrations";
428
+
429
+ See https://www.npmjs.com/package/tempest-db-js for the migration workflow.
430
+ `;
431
+ async function writeFiles(root, files) {
432
+ for (const [relative, contents] of Object.entries(files)) {
433
+ const target = path.join(root, relative);
434
+ await promises.mkdir(path.dirname(target), { recursive: true });
435
+ await promises.writeFile(target, contents, "utf8");
436
+ }
437
+ }
438
+ async function runNew(name, dir) {
439
+ if (!/^[a-z0-9][a-z0-9-_]*$/i.test(name)) {
440
+ throw new Error(`Invalid project name: ${JSON.stringify(name)}`);
441
+ }
442
+ const root = path.resolve(dir, name);
443
+ await writeFiles(root, projectFiles(name));
444
+ process.stdout.write(
445
+ `Created ${name} at ${root}
446
+
447
+ Next steps:
448
+ cd ${name}
449
+ npm install
450
+ cp .env.example .env
451
+ npm run dev
452
+ `
453
+ );
454
+ }
455
+ async function runGenerate(name, dir) {
456
+ if (!/^[A-Z][A-Za-z0-9]*$/.test(name)) {
457
+ throw new Error(`Resource name must be PascalCase, got ${JSON.stringify(name)}`);
458
+ }
459
+ const files = resourceFiles(name);
460
+ await writeFiles(path.resolve(dir), files);
461
+ const written = Object.keys(files).map((path) => ` ${path}`).join("\n");
462
+ process.stdout.write(`Generated ${name} resource:
463
+ ${written}
464
+ `);
465
+ }
466
+ async function main(argv = process.argv.slice(2)) {
467
+ const { values, positionals } = util.parseArgs({
468
+ args: argv,
469
+ allowPositionals: true,
470
+ options: {
471
+ version: { type: "boolean", short: "v" },
472
+ help: { type: "boolean", short: "h" },
473
+ dir: { type: "string", default: "." },
474
+ bytes: { type: "string", default: "32" }
475
+ }
476
+ });
477
+ if (values.version) {
478
+ process.stdout.write(`${VERSION}
479
+ `);
480
+ return 0;
481
+ }
482
+ const [command, ...rest] = positionals;
483
+ if (values.help || command === void 0 || command === "help") {
484
+ process.stdout.write(USAGE);
485
+ return 0;
486
+ }
487
+ const dir = values.dir ?? ".";
488
+ if (command === "new") {
489
+ const name = rest[0];
490
+ if (!name) {
491
+ process.stderr.write(`error: \`new\` requires a project name
492
+
493
+ ${USAGE}`);
494
+ return 1;
495
+ }
496
+ await runNew(name, dir);
497
+ return 0;
498
+ }
499
+ if (command === "generate" || command === "g") {
500
+ const name = rest[0];
501
+ if (!name) {
502
+ process.stderr.write(`error: \`generate\` requires a resource name
503
+
504
+ ${USAGE}`);
505
+ return 1;
506
+ }
507
+ await runGenerate(name, dir);
508
+ return 0;
509
+ }
510
+ if (command === "secret") {
511
+ const nbytes = Number.parseInt(values.bytes ?? "32", 10);
512
+ if (!Number.isInteger(nbytes) || nbytes < 16) {
513
+ process.stderr.write("error: --bytes must be an integer >= 16\n");
514
+ return 1;
515
+ }
516
+ process.stdout.write(`${crypto.randomBytes(nbytes).toString("base64url")}
517
+ `);
518
+ return 0;
519
+ }
520
+ if (command === "docker-compose") {
521
+ await writeFiles(path.resolve(dir), { "docker-compose.yml": dockerComposeFile("app") });
522
+ process.stdout.write(`Wrote ${path.resolve(dir, "docker-compose.yml")}
523
+ `);
524
+ return 0;
525
+ }
526
+ if (command === "db") {
527
+ process.stdout.write(DB_HELP);
528
+ return 0;
529
+ }
530
+ process.stderr.write(`error: unknown command ${JSON.stringify(command)}
531
+
532
+ ${USAGE}`);
533
+ return 1;
534
+ }
535
+ main().then((code) => {
536
+ process.exitCode = code;
537
+ }).catch((error) => {
538
+ const message = error instanceof Error ? error.message : String(error);
539
+ process.stderr.write(`error: ${message}
540
+ `);
541
+ process.exitCode = 1;
542
+ });
543
+
544
+ exports.main = main;
545
+ //# sourceMappingURL=cli.cjs.map
546
+ //# sourceMappingURL=cli.cjs.map