tempest-express-sdk 0.11.0 → 0.20.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/README.md +9 -6
- package/dist/chunk-XGKAZZR2.js +106 -0
- package/dist/chunk-XGKAZZR2.js.map +1 -0
- package/dist/cli.cjs +158 -11
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +73 -12
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1829 -38
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1624 -10
- package/dist/index.d.ts +1624 -10
- package/dist/index.js +1750 -141
- package/dist/index.js.map +1 -1
- package/package.json +7 -3
- package/dist/chunk-IHO6X75J.js +0 -6
- package/dist/chunk-IHO6X75J.js.map +0 -1
package/README.md
CHANGED
|
@@ -27,20 +27,23 @@ npm install tempest-express-sdk tempest-db-js express zod
|
|
|
27
27
|
| --- | --- |
|
|
28
28
|
| **core** | `JSONLogger`, `configureLogging`, request-id context (`getRequestId`, `runWithRequestContext`), `defineEnum` |
|
|
29
29
|
| **exceptions** | `AppException` + `ConflictException` / `NotFoundException` / `UnauthorizedException` / `ForbiddenException` / `ValidationException` / `TooManyRequestsException` / `InvalidTokenException` / `ExpiredTokenException`, `MessageCatalog` (i18n) |
|
|
30
|
-
| **schemas** | `z` (OpenAPI-augmented), `baseResponseSchema`, `toDict`, `paginationFilterSchema` / `paginationSchema`, cursor pagination, `
|
|
31
|
-
| **settings** | `loadSettings`, `baseAppSettingsShape` (server / database / CORS) |
|
|
32
|
-
| **db** | re-exports `tempest-db-js` + `BaseModel`, `tableNameFor`, soft-delete / audit column helpers |
|
|
30
|
+
| **schemas** | `z` (OpenAPI-augmented), `baseResponseSchema`, `toDict`, `paginationFilterSchema` / `paginationSchema`, cursor + delta-sync pagination, `buildPaginationLinkHeader`, validated field types (`centsField`/`priceField`/`slugField`/…), `logEntrySchema` |
|
|
31
|
+
| **settings** | `loadSettings`, `baseAppSettingsShape` (server / database / CORS) + domain fragments (`jwtSettingsShape`, `authSettingsShape`, `emailSettingsShape`, `redisSettingsShape`, `sessionSettingsShape`, `uploadSettingsShape`, `minioSettingsShape`, …), `envBoolean` / `envList` |
|
|
32
|
+
| **db** | re-exports `tempest-db-js` + `BaseModel`, `tableNameFor`, soft-delete / audit column helpers; `TenantScopedRepository`, `BaseOutboxModel` + `OutboxRelay`, `BaseAuditLogModel` + `snapshot`/`diffSnapshots`, `BaseUserModel` / `BaseUserTokenModel` / `BaseUserRefreshTokenModel`, `wrapWithSlowQueryLog`, `backupDatabase` |
|
|
33
33
|
| **services / controllers** | `BaseService`, `BaseController` over a typed repository |
|
|
34
|
-
| **utils** | CPF/CNPJ/CEP/phone/UF + cities, `PasswordUtils`, `JWTUtils`, opaque tokens, `AttemptThrottle` |
|
|
35
|
-
| **auth** | `UserAuthService`, JWT middleware + role guards, `makeAuthRouter`; MFA (`MfaService`), email activation, password reset |
|
|
34
|
+
| **utils** | CPF/CNPJ/CEP/phone/UF + cities, `PasswordUtils`, `JWTUtils`, opaque tokens, `AttemptThrottle`, `sendFileDownload`/`sendBytesDownload` (Range), `configureFileLogging` (per-level + `500.log`) |
|
|
35
|
+
| **auth** | `UserAuthService`, JWT middleware + role guards, `makeAuthRouter`; MFA (`MfaService`), email activation, password reset; optional HTML pages (`renderAuthResultPage`, `renderPasswordResetFormPage`) |
|
|
36
36
|
| **cache / queue / tasks** | `CacheManager` (+`cached`), `BrokerManager` (memory/RabbitMQ), `TaskManager` |
|
|
37
37
|
| **sse / websockets** | `SSEBroker`/`sseResponse` (+ `RedisSSEBroker`), transport-agnostic `WebSocketHub` + `attachWebSocketHub` |
|
|
38
|
-
| **flags / storage** | `FeatureFlags` (+ guard), `UploadStorage`/`LocalUploadStorage` |
|
|
38
|
+
| **flags / storage** | `FeatureFlags` (+ guard), `UploadStorage`/`LocalUploadStorage`/`S3UploadStorage` (MinIO/S3) |
|
|
39
39
|
| **webpush / email** | `WebPushDispatcher` (VAPID), `EmailUtils` (SMTP) |
|
|
40
40
|
| **server utils** | `TOTPHelper` (MFA), `HTTPClient` (retry + circuit breaker), `MetricsUtils` (+ Prometheus, GPU), `makeMetricsRouter`, `getClientIp` |
|
|
41
41
|
| **integrations** | `MessagingProvider` contract; `WhatsAppProvider` (zap-api), `TelegramProvider` (Bot API), `TwilioSmsProvider` (SMS), `EmailProvider`, `MessagingHub` + `broadcastText`, webhook receivers |
|
|
42
42
|
| **admin** | `AdminSite` + `makeAdminRouter` — JSON admin with auto-derived CRUD + introspection |
|
|
43
43
|
| **api** | `createApp`, `runServer`, `registerExceptionHandlers`, `createOpenApiRegistry`, `generateOpenApiDocument`, `mountSwaggerUi`, `mountRedoc`, `makeHealthRouter` |
|
|
44
|
+
| **api/middlewares** | `rateLimitMiddleware` (memory/Redis stores, IP/header/JWT keys), `bodySizeLimitMiddleware`, `csrfMiddleware`, `idempotencyMiddleware` (memory/Redis), `GracefulShutdown`, `requestTracingMiddleware`, `prometheusMiddleware` / `HttpMetrics` |
|
|
45
|
+
| **api/oauth** | `GoogleOAuthClient`, `GitHubOAuthClient`, `OIDCProvider`, `generateOAuthState`; `WebhookSignatureVerifier`; `makeToolSpecRouter` |
|
|
46
|
+
| **testing** | `createTestDatabase`, `withTestDatabase` — in-memory SQLite engine with tables reflected from your models |
|
|
44
47
|
|
|
45
48
|
## Quick start
|
|
46
49
|
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
export { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
// src/schemas/base.ts
|
|
6
|
+
extendZodWithOpenApi(z);
|
|
7
|
+
function toDict(data, options = {}) {
|
|
8
|
+
const exclude = new Set(options.exclude ?? []);
|
|
9
|
+
const out = {};
|
|
10
|
+
for (const [key, value] of Object.entries(data)) {
|
|
11
|
+
if (exclude.has(key)) continue;
|
|
12
|
+
if (value === null || value === void 0) continue;
|
|
13
|
+
out[key] = value;
|
|
14
|
+
}
|
|
15
|
+
return { ...out, ...options.include ?? {} };
|
|
16
|
+
}
|
|
17
|
+
var baseResponseSchema = z.object({
|
|
18
|
+
id: z.string().uuid().openapi({ description: "The unique identifier of the record." }),
|
|
19
|
+
isActive: z.boolean().openapi({ description: "Whether the record is active (soft-delete flag)." }),
|
|
20
|
+
createdAt: z.coerce.date().openapi({ description: "The creation timestamp of the record (UTC)." }),
|
|
21
|
+
updatedAt: z.coerce.date().openapi({ description: "The last update timestamp of the record (UTC)." })
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// src/settings/base.ts
|
|
25
|
+
var serverSettingsShape = {
|
|
26
|
+
HOST: z.string().default("127.0.0.1"),
|
|
27
|
+
PORT: z.coerce.number().int().min(0).max(65535).default(8e3),
|
|
28
|
+
DEBUG: z.coerce.boolean().default(false)
|
|
29
|
+
};
|
|
30
|
+
var databaseSettingsShape = {
|
|
31
|
+
DATABASE_URL: z.string().default("sqlite://./app.db")
|
|
32
|
+
};
|
|
33
|
+
var corsSettingsShape = {
|
|
34
|
+
CORS_ORIGINS: z.string().default("*").transform(
|
|
35
|
+
(value) => value.split(",").map((origin) => origin.trim()).filter((origin) => origin.length > 0)
|
|
36
|
+
)
|
|
37
|
+
};
|
|
38
|
+
var baseAppSettingsShape = {
|
|
39
|
+
...serverSettingsShape,
|
|
40
|
+
...databaseSettingsShape,
|
|
41
|
+
...corsSettingsShape
|
|
42
|
+
};
|
|
43
|
+
var baseAppSettingsSchema = z.object(baseAppSettingsShape);
|
|
44
|
+
function loadSettings(schema, env = process.env) {
|
|
45
|
+
return Object.freeze(schema.parse(env));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// src/utils/password.ts
|
|
49
|
+
var cached = null;
|
|
50
|
+
async function loadBcrypt() {
|
|
51
|
+
if (cached) return cached;
|
|
52
|
+
try {
|
|
53
|
+
const mod = await import('bcryptjs');
|
|
54
|
+
cached = mod.default ?? mod;
|
|
55
|
+
} catch (cause) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
"PasswordUtils requires the 'bcryptjs' peer dependency. Install with `npm i bcryptjs`.",
|
|
58
|
+
{ cause }
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
return cached;
|
|
62
|
+
}
|
|
63
|
+
var PasswordUtils = class {
|
|
64
|
+
/**
|
|
65
|
+
* @param rounds - The bcrypt cost factor. Higher is slower and harder to
|
|
66
|
+
* brute-force. Defaults to 12.
|
|
67
|
+
*/
|
|
68
|
+
constructor(rounds = 12) {
|
|
69
|
+
this.rounds = rounds;
|
|
70
|
+
}
|
|
71
|
+
rounds;
|
|
72
|
+
/**
|
|
73
|
+
* Hash a plaintext password.
|
|
74
|
+
*
|
|
75
|
+
* @param plain - The plaintext password.
|
|
76
|
+
* @returns The bcrypt hash, ready to persist.
|
|
77
|
+
*/
|
|
78
|
+
async hash(plain) {
|
|
79
|
+
const bcrypt = await loadBcrypt();
|
|
80
|
+
const salt = await bcrypt.genSalt(this.rounds);
|
|
81
|
+
return bcrypt.hash(plain, salt);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Verify a plaintext password against a stored hash. Returns `false` for
|
|
85
|
+
* malformed hashes rather than throwing.
|
|
86
|
+
*
|
|
87
|
+
* @param plain - The plaintext password to verify.
|
|
88
|
+
* @param hashed - The previously stored bcrypt hash.
|
|
89
|
+
* @returns `true` when the password matches.
|
|
90
|
+
*/
|
|
91
|
+
async verify(plain, hashed) {
|
|
92
|
+
try {
|
|
93
|
+
const bcrypt = await loadBcrypt();
|
|
94
|
+
return await bcrypt.compare(plain, hashed);
|
|
95
|
+
} catch {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// src/version.ts
|
|
102
|
+
var VERSION = "0.20.1";
|
|
103
|
+
|
|
104
|
+
export { PasswordUtils, VERSION, baseAppSettingsSchema, baseAppSettingsShape, baseResponseSchema, corsSettingsShape, databaseSettingsShape, loadSettings, serverSettingsShape, toDict };
|
|
105
|
+
//# sourceMappingURL=chunk-XGKAZZR2.js.map
|
|
106
|
+
//# sourceMappingURL=chunk-XGKAZZR2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/schemas/base.ts","../src/settings/base.ts","../src/utils/password.ts","../src/version.ts"],"names":[],"mappings":"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","file":"chunk-XGKAZZR2.js","sourcesContent":["/**\n * Zod foundation shared by every DTO, mirroring `schemas.base.BaseSchema`.\n *\n * Pydantic uses class inheritance for shared config; Zod composes instead. This\n * module re-exports a `z` already augmented with `.openapi()` (so every schema\n * can carry OpenAPI metadata) and a {@link toDict} helper matching\n * `BaseSchema.to_dict` (drop nullish, exclude keys, merge extras).\n */\n\nimport { extendZodWithOpenApi } from \"@asteasolutions/zod-to-openapi\";\nimport { z } from \"zod\";\n\n// Augment the shared `z` instance with `.openapi(...)`. Idempotent — calling it\n// more than once across modules is safe.\nextendZodWithOpenApi(z);\n\nexport { z };\n\n/** Options for {@link toDict}. */\nexport interface ToDictOptions {\n /** Field names to drop from the output. */\n exclude?: string[];\n /** Extra entries merged on top (override existing keys). */\n include?: Record<string, unknown>;\n}\n\n/**\n * Serialize a validated object to a plain record, dropping `null`/`undefined`,\n * removing `exclude`d keys and merging `include` on top.\n *\n * @param data - The source object (typically a parsed schema).\n * @param options - Keys to exclude and entries to merge in.\n * @returns The cleaned record.\n */\nexport function toDict(\n data: Record<string, unknown>,\n options: ToDictOptions = {},\n): Record<string, unknown> {\n const exclude = new Set(options.exclude ?? []);\n const out: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(data)) {\n if (exclude.has(key)) continue;\n if (value === null || value === undefined) continue;\n out[key] = value;\n }\n return { ...out, ...(options.include ?? {}) };\n}\n\n/**\n * Response schema fields every ORM record carries (`id`, `isActive`,\n * `createdAt`, `updatedAt`). Mirrors `BaseResponseSchema`. Extend it with\n * `baseResponseSchema.extend({ ... })` to build concrete `*ResponseSchema`s.\n */\nexport const baseResponseSchema = z.object({\n id: z.string().uuid().openapi({ description: \"The unique identifier of the record.\" }),\n isActive: z\n .boolean()\n .openapi({ description: \"Whether the record is active (soft-delete flag).\" }),\n createdAt: z.coerce\n .date()\n .openapi({ description: \"The creation timestamp of the record (UTC).\" }),\n updatedAt: z.coerce\n .date()\n .openapi({ description: \"The last update timestamp of the record (UTC).\" }),\n});\n\n/** The inferred TS type of a {@link baseResponseSchema} payload. */\nexport type BaseResponse = z.infer<typeof baseResponseSchema>;\n","/**\n * Environment-driven settings, mirroring `settings.base.BaseAppSettings`.\n *\n * Pydantic-settings reads env vars into a frozen, validated model. Here a zod\n * schema plays the same role: {@link loadSettings} parses `process.env` (env\n * names are matched case-sensitively, like the FastAPI SDK) and returns a\n * frozen, fully-typed object. Composable fragments ({@link serverSettingsShape}\n * etc.) cover the common server/database/CORS knobs.\n */\n\nimport { z } from \"@/schemas/base\";\n\n/** Server bind/runtime settings. Defaults bind to localhost. */\nexport const serverSettingsShape = {\n HOST: z.string().default(\"127.0.0.1\"),\n PORT: z.coerce.number().int().min(0).max(65535).default(8000),\n DEBUG: z.coerce.boolean().default(false),\n} as const;\n\n/** Database connection settings. */\nexport const databaseSettingsShape = {\n DATABASE_URL: z.string().default(\"sqlite://./app.db\"),\n} as const;\n\n/** CORS settings. `CORS_ORIGINS` is a comma-separated list. */\nexport const corsSettingsShape = {\n CORS_ORIGINS: z\n .string()\n .default(\"*\")\n .transform((value) =>\n value\n .split(\",\")\n .map((origin) => origin.trim())\n .filter((origin) => origin.length > 0),\n ),\n} as const;\n\n/** The combined base settings shape (server + database + CORS). */\nexport const baseAppSettingsShape = {\n ...serverSettingsShape,\n ...databaseSettingsShape,\n ...corsSettingsShape,\n} as const;\n\n/** A zod object built from {@link baseAppSettingsShape}. */\nexport const baseAppSettingsSchema = z.object(baseAppSettingsShape);\n\n/** The parsed shape of {@link baseAppSettingsSchema}. */\nexport type BaseAppSettings = z.infer<typeof baseAppSettingsSchema>;\n\n/**\n * Parse and freeze settings from an environment source.\n *\n * Extend the base shape with project fields:\n *\n * ```ts\n * const settings = loadSettings(\n * z.object({ ...baseAppSettingsShape, JWT_SECRET: z.string() }),\n * );\n * ```\n *\n * @param schema - The settings zod schema.\n * @param env - The environment source (defaults to `process.env`).\n * @returns The validated, frozen settings object.\n * @throws {z.ZodError} When required env vars are missing or malformed.\n */\nexport function loadSettings<S extends z.ZodTypeAny>(\n schema: S,\n env: NodeJS.ProcessEnv = process.env,\n): Readonly<z.infer<S>> {\n return Object.freeze(schema.parse(env));\n}\n","/**\n * Password hashing backed by bcrypt, mirroring `utils.password.PasswordUtils`.\n *\n * `bcryptjs` is an optional peer dependency, imported lazily so that\n * `import \"tempest-express-sdk\"` keeps working when it is not installed — the\n * clear error is deferred to first use. Methods are async (the idiomatic Node\n * bcrypt surface).\n */\n\ntype BcryptModule = typeof import(\"bcryptjs\");\n\nlet cached: BcryptModule | null = null;\n\n/** Lazily load `bcryptjs`, with a clear install hint when missing. */\nasync function loadBcrypt(): Promise<BcryptModule> {\n if (cached) return cached;\n try {\n const mod = (await import(\"bcryptjs\")) as BcryptModule & { default?: BcryptModule };\n cached = mod.default ?? mod;\n } catch (cause) {\n throw new Error(\n \"PasswordUtils requires the 'bcryptjs' peer dependency. Install with `npm i bcryptjs`.\",\n { cause },\n );\n }\n return cached;\n}\n\n/** Hash and verify passwords using bcrypt. Stateless — construct once, reuse. */\nexport class PasswordUtils {\n /**\n * @param rounds - The bcrypt cost factor. Higher is slower and harder to\n * brute-force. Defaults to 12.\n */\n constructor(private readonly rounds: number = 12) {}\n\n /**\n * Hash a plaintext password.\n *\n * @param plain - The plaintext password.\n * @returns The bcrypt hash, ready to persist.\n */\n async hash(plain: string): Promise<string> {\n const bcrypt = await loadBcrypt();\n const salt = await bcrypt.genSalt(this.rounds);\n return bcrypt.hash(plain, salt);\n }\n\n /**\n * Verify a plaintext password against a stored hash. Returns `false` for\n * malformed hashes rather than throwing.\n *\n * @param plain - The plaintext password to verify.\n * @param hashed - The previously stored bcrypt hash.\n * @returns `true` when the password matches.\n */\n async verify(plain: string, hashed: string): Promise<boolean> {\n try {\n const bcrypt = await loadBcrypt();\n return await bcrypt.compare(plain, hashed);\n } catch {\n return false;\n }\n }\n}\n","/** The installed SDK version. Single source of truth for the barrel + CLI. */\nexport const VERSION = \"0.20.1\";\n"]}
|
package/dist/cli.cjs
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
+
var child_process = require('child_process');
|
|
4
5
|
var crypto = require('crypto');
|
|
6
|
+
var fs = require('fs');
|
|
5
7
|
var promises = require('fs/promises');
|
|
6
8
|
var path = require('path');
|
|
7
9
|
var util = require('util');
|
|
10
|
+
var zodToOpenapi = require('@asteasolutions/zod-to-openapi');
|
|
11
|
+
var zod = require('zod');
|
|
8
12
|
|
|
9
13
|
// src/cli/template.ts
|
|
10
|
-
var SDK_VERSION = "^0.
|
|
14
|
+
var SDK_VERSION = "^0.20.0";
|
|
11
15
|
function projectFiles(name) {
|
|
12
16
|
return {
|
|
13
17
|
"package.json": `${JSON.stringify(
|
|
@@ -24,7 +28,7 @@ function projectFiles(name) {
|
|
|
24
28
|
},
|
|
25
29
|
dependencies: {
|
|
26
30
|
express: "^5.1.0",
|
|
27
|
-
"tempest-db-js": "^0.
|
|
31
|
+
"tempest-db-js": "^0.4.0",
|
|
28
32
|
"tempest-express-sdk": SDK_VERSION,
|
|
29
33
|
zod: "^3.24.1"
|
|
30
34
|
},
|
|
@@ -406,9 +410,93 @@ volumes:
|
|
|
406
410
|
pgdata:
|
|
407
411
|
`;
|
|
408
412
|
}
|
|
413
|
+
zodToOpenapi.extendZodWithOpenApi(zod.z);
|
|
414
|
+
zod.z.object({
|
|
415
|
+
id: zod.z.string().uuid().openapi({ description: "The unique identifier of the record." }),
|
|
416
|
+
isActive: zod.z.boolean().openapi({ description: "Whether the record is active (soft-delete flag)." }),
|
|
417
|
+
createdAt: zod.z.coerce.date().openapi({ description: "The creation timestamp of the record (UTC)." }),
|
|
418
|
+
updatedAt: zod.z.coerce.date().openapi({ description: "The last update timestamp of the record (UTC)." })
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
// src/settings/base.ts
|
|
422
|
+
var serverSettingsShape = {
|
|
423
|
+
HOST: zod.z.string().default("127.0.0.1"),
|
|
424
|
+
PORT: zod.z.coerce.number().int().min(0).max(65535).default(8e3),
|
|
425
|
+
DEBUG: zod.z.coerce.boolean().default(false)
|
|
426
|
+
};
|
|
427
|
+
var databaseSettingsShape = {
|
|
428
|
+
DATABASE_URL: zod.z.string().default("sqlite://./app.db")
|
|
429
|
+
};
|
|
430
|
+
var corsSettingsShape = {
|
|
431
|
+
CORS_ORIGINS: zod.z.string().default("*").transform(
|
|
432
|
+
(value) => value.split(",").map((origin) => origin.trim()).filter((origin) => origin.length > 0)
|
|
433
|
+
)
|
|
434
|
+
};
|
|
435
|
+
var baseAppSettingsShape = {
|
|
436
|
+
...serverSettingsShape,
|
|
437
|
+
...databaseSettingsShape,
|
|
438
|
+
...corsSettingsShape
|
|
439
|
+
};
|
|
440
|
+
var baseAppSettingsSchema = zod.z.object(baseAppSettingsShape);
|
|
441
|
+
function loadSettings(schema, env = process.env) {
|
|
442
|
+
return Object.freeze(schema.parse(env));
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// src/utils/password.ts
|
|
446
|
+
var cached = null;
|
|
447
|
+
async function loadBcrypt() {
|
|
448
|
+
if (cached) return cached;
|
|
449
|
+
try {
|
|
450
|
+
const mod = await import('bcryptjs');
|
|
451
|
+
cached = mod.default ?? mod;
|
|
452
|
+
} catch (cause) {
|
|
453
|
+
throw new Error(
|
|
454
|
+
"PasswordUtils requires the 'bcryptjs' peer dependency. Install with `npm i bcryptjs`.",
|
|
455
|
+
{ cause }
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
return cached;
|
|
459
|
+
}
|
|
460
|
+
var PasswordUtils = class {
|
|
461
|
+
/**
|
|
462
|
+
* @param rounds - The bcrypt cost factor. Higher is slower and harder to
|
|
463
|
+
* brute-force. Defaults to 12.
|
|
464
|
+
*/
|
|
465
|
+
constructor(rounds = 12) {
|
|
466
|
+
this.rounds = rounds;
|
|
467
|
+
}
|
|
468
|
+
rounds;
|
|
469
|
+
/**
|
|
470
|
+
* Hash a plaintext password.
|
|
471
|
+
*
|
|
472
|
+
* @param plain - The plaintext password.
|
|
473
|
+
* @returns The bcrypt hash, ready to persist.
|
|
474
|
+
*/
|
|
475
|
+
async hash(plain) {
|
|
476
|
+
const bcrypt = await loadBcrypt();
|
|
477
|
+
const salt = await bcrypt.genSalt(this.rounds);
|
|
478
|
+
return bcrypt.hash(plain, salt);
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Verify a plaintext password against a stored hash. Returns `false` for
|
|
482
|
+
* malformed hashes rather than throwing.
|
|
483
|
+
*
|
|
484
|
+
* @param plain - The plaintext password to verify.
|
|
485
|
+
* @param hashed - The previously stored bcrypt hash.
|
|
486
|
+
* @returns `true` when the password matches.
|
|
487
|
+
*/
|
|
488
|
+
async verify(plain, hashed) {
|
|
489
|
+
try {
|
|
490
|
+
const bcrypt = await loadBcrypt();
|
|
491
|
+
return await bcrypt.compare(plain, hashed);
|
|
492
|
+
} catch {
|
|
493
|
+
return false;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
};
|
|
409
497
|
|
|
410
498
|
// src/version.ts
|
|
411
|
-
var VERSION = "0.
|
|
499
|
+
var VERSION = "0.20.1";
|
|
412
500
|
|
|
413
501
|
// src/cli/index.ts
|
|
414
502
|
var USAGE = `tempest-express \u2014 Express SDK CLI
|
|
@@ -419,6 +507,10 @@ Usage:
|
|
|
419
507
|
tempest-express secret [--bytes <n>] Print a random secret
|
|
420
508
|
tempest-express docker-compose [--dir <path>] Write a docker-compose.yml
|
|
421
509
|
tempest-express db Migration guidance
|
|
510
|
+
tempest-express lint [--dir <path>] Run Biome check in a project
|
|
511
|
+
tempest-express config [--dir <path>] Print the resolved base settings
|
|
512
|
+
tempest-express user --email <e> --password <p> [--admin]
|
|
513
|
+
Print a ready-to-insert user record
|
|
422
514
|
tempest-express --version Print the version
|
|
423
515
|
tempest-express --help Show this help
|
|
424
516
|
`;
|
|
@@ -428,6 +520,23 @@ var DB_HELP = `Migrations are handled by tempest-db-js (programmatic API):
|
|
|
428
520
|
|
|
429
521
|
See https://www.npmjs.com/package/tempest-db-js for the migration workflow.
|
|
430
522
|
`;
|
|
523
|
+
function parseDotEnv(path) {
|
|
524
|
+
const out = {};
|
|
525
|
+
if (!fs.existsSync(path)) return out;
|
|
526
|
+
for (const raw of fs.readFileSync(path, "utf8").split("\n")) {
|
|
527
|
+
const line = raw.trim();
|
|
528
|
+
if (!line || line.startsWith("#")) continue;
|
|
529
|
+
const eq = line.indexOf("=");
|
|
530
|
+
if (eq === -1) continue;
|
|
531
|
+
const key = line.slice(0, eq).trim();
|
|
532
|
+
let value = line.slice(eq + 1).trim();
|
|
533
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
534
|
+
value = value.slice(1, -1);
|
|
535
|
+
}
|
|
536
|
+
out[key] = value;
|
|
537
|
+
}
|
|
538
|
+
return out;
|
|
539
|
+
}
|
|
431
540
|
async function writeFiles(root, files) {
|
|
432
541
|
for (const [relative, contents] of Object.entries(files)) {
|
|
433
542
|
const target = path.join(root, relative);
|
|
@@ -471,7 +580,10 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
471
580
|
version: { type: "boolean", short: "v" },
|
|
472
581
|
help: { type: "boolean", short: "h" },
|
|
473
582
|
dir: { type: "string", default: "." },
|
|
474
|
-
bytes: { type: "string", default: "32" }
|
|
583
|
+
bytes: { type: "string", default: "32" },
|
|
584
|
+
email: { type: "string" },
|
|
585
|
+
password: { type: "string" },
|
|
586
|
+
admin: { type: "boolean", default: false }
|
|
475
587
|
}
|
|
476
588
|
});
|
|
477
589
|
if (values.version) {
|
|
@@ -527,19 +639,54 @@ ${USAGE}`);
|
|
|
527
639
|
process.stdout.write(DB_HELP);
|
|
528
640
|
return 0;
|
|
529
641
|
}
|
|
642
|
+
if (command === "lint") {
|
|
643
|
+
const target = path.resolve(dir);
|
|
644
|
+
const result = child_process.spawnSync("npx", ["biome", "check", target], {
|
|
645
|
+
stdio: "inherit",
|
|
646
|
+
shell: process.platform === "win32"
|
|
647
|
+
});
|
|
648
|
+
return result.status ?? 1;
|
|
649
|
+
}
|
|
650
|
+
if (command === "config") {
|
|
651
|
+
const env = { ...process.env, ...parseDotEnv(path.join(path.resolve(dir), ".env")) };
|
|
652
|
+
const settings = loadSettings(baseAppSettingsSchema, env);
|
|
653
|
+
process.stdout.write(`${JSON.stringify(settings, null, 2)}
|
|
654
|
+
`);
|
|
655
|
+
return 0;
|
|
656
|
+
}
|
|
657
|
+
if (command === "user") {
|
|
658
|
+
const { email, password } = values;
|
|
659
|
+
if (!email || !password) {
|
|
660
|
+
process.stderr.write("error: `user` requires --email and --password\n");
|
|
661
|
+
return 1;
|
|
662
|
+
}
|
|
663
|
+
const hashedPassword = await new PasswordUtils().hash(password);
|
|
664
|
+
const record = {
|
|
665
|
+
id: crypto.randomUUID(),
|
|
666
|
+
email,
|
|
667
|
+
hashedPassword,
|
|
668
|
+
isAdmin: Boolean(values.admin),
|
|
669
|
+
isActive: true
|
|
670
|
+
};
|
|
671
|
+
process.stdout.write(`${JSON.stringify(record, null, 2)}
|
|
672
|
+
`);
|
|
673
|
+
return 0;
|
|
674
|
+
}
|
|
530
675
|
process.stderr.write(`error: unknown command ${JSON.stringify(command)}
|
|
531
676
|
|
|
532
677
|
${USAGE}`);
|
|
533
678
|
return 1;
|
|
534
679
|
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
680
|
+
if (!process.env.VITEST) {
|
|
681
|
+
main().then((code) => {
|
|
682
|
+
process.exitCode = code;
|
|
683
|
+
}).catch((error) => {
|
|
684
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
685
|
+
process.stderr.write(`error: ${message}
|
|
540
686
|
`);
|
|
541
|
-
|
|
542
|
-
});
|
|
687
|
+
process.exitCode = 1;
|
|
688
|
+
});
|
|
689
|
+
}
|
|
543
690
|
|
|
544
691
|
exports.main = main;
|
|
545
692
|
//# sourceMappingURL=cli.cjs.map
|
package/dist/cli.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cli/template.ts","../src/version.ts","../src/cli/index.ts"],"names":["join","mkdir","dirname","writeFile","resolve","parseArgs","randomBytes"],"mappings":"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","file":"cli.cjs","sourcesContent":["/**\n * Project-template generator for the `new` command, mirroring `cli.new`.\n *\n * Produces a complete, runnable Express service that follows the SDK's layered\n * architecture (router → controller → service → repository → model) and is\n * pre-wired with `createApp`, native Swagger + Redoc, Zod validation, and a\n * `tempest-db-js` model. {@link projectFiles} returns a `path → contents` map\n * the CLI writes to disk.\n */\n\n/** SDK version the generated project depends on (kept in sync at release). */\nconst SDK_VERSION = \"^0.1.0\";\n\n/**\n * Build the file map for a new service named `name`.\n *\n * @param name - The project (and package) name.\n * @returns A map of relative file path to file contents.\n */\nexport function projectFiles(name: string): Record<string, string> {\n return {\n \"package.json\": `${JSON.stringify(\n {\n name,\n version: \"0.1.0\",\n private: true,\n type: \"module\",\n scripts: {\n dev: \"tsx watch main.ts\",\n start: \"tsx main.ts\",\n build: \"tsc -p tsconfig.json\",\n typecheck: \"tsc --noEmit\",\n },\n dependencies: {\n express: \"^5.1.0\",\n \"tempest-db-js\": \"^0.3.0\",\n \"tempest-express-sdk\": SDK_VERSION,\n zod: \"^3.24.1\",\n },\n devDependencies: {\n \"@types/express\": \"^5.0.0\",\n \"@types/node\": \"^22.10.0\",\n tsx: \"^4.19.2\",\n typescript: \"^5.7.0\",\n },\n },\n null,\n 2,\n )}\\n`,\n\n \"tsconfig.json\": `${JSON.stringify(\n {\n compilerOptions: {\n target: \"ES2022\",\n module: \"ESNext\",\n moduleResolution: \"Bundler\",\n lib: [\"ES2022\"],\n strict: true,\n noUncheckedIndexedAccess: true,\n verbatimModuleSyntax: true,\n esModuleInterop: true,\n skipLibCheck: true,\n forceConsistentCasingInFileNames: true,\n outDir: \"dist\",\n paths: { \"@/*\": [\"./src/*\"] },\n },\n include: [\"src\", \"main.ts\"],\n },\n null,\n 2,\n )}\\n`,\n\n \".gitignore\": \"node_modules\\ndist\\n.env\\n*.log\\n\",\n\n \".env.example\":\n \"HOST=127.0.0.1\\nPORT=8000\\nDEBUG=false\\nDATABASE_URL=sqlite://./app.db\\nCORS_ORIGINS=*\\n\",\n\n \"main.ts\": `import { run } from \"@/index\";\n\nrun();\n`,\n\n \"src/index.ts\": `import { run } from \"@/server\";\n\nexport { run };\n`,\n\n \"src/core/settings.ts\": `import { baseAppSettingsShape, loadSettings, z } from \"tempest-express-sdk\";\n\n/** Application settings — extend the SDK base shape with project fields. */\nexport const settingsSchema = z.object({\n ...baseAppSettingsShape,\n});\n\nexport const settings = loadSettings(settingsSchema);\n`,\n\n \"src/db/models/itemModel.ts\": `import { BaseModel, column, tableNameFor } from \"tempest-express-sdk\";\n\n/** A sample domain model. Replace with your own. */\nexport class ItemModel extends BaseModel {\n static tablename = tableNameFor(\"ItemModel\");\n name = column.text().notNull();\n price = column.integer().notNull();\n}\n`,\n\n \"src/schemas/item.ts\": `import { baseResponseSchema, z } from \"tempest-express-sdk\";\n\n/** Request payload to create an item. */\nexport const itemCreateSchema = z\n .object({\n name: z.string().min(1).openapi({ description: \"The item name.\" }),\n price: z.number().int().min(0).openapi({ description: \"Price in cents.\" }),\n })\n .openapi(\"ItemCreate\");\n\n/** Response payload for an item (base columns + domain fields). */\nexport const itemResponseSchema = baseResponseSchema\n .extend({\n name: z.string(),\n price: z.number().int(),\n })\n .openapi(\"Item\");\n\nexport type ItemCreate = z.infer<typeof itemCreateSchema>;\nexport type ItemResponse = z.infer<typeof itemResponseSchema>;\n`,\n\n \"src/db/repositories/itemRepository.ts\": `import { type AsyncSession, BaseRepository } from \"tempest-express-sdk\";\nimport { ItemModel } from \"@/db/models/itemModel\";\n\n/** Data-access layer for items. */\nexport class ItemRepository extends BaseRepository<typeof ItemModel> {\n constructor(session: AsyncSession) {\n super(ItemModel, session);\n }\n}\n`,\n\n \"src/services/itemService.ts\": `import { BaseService } from \"tempest-express-sdk\";\nimport type { ItemRepository } from \"@/db/repositories/itemRepository\";\nimport type { ItemModel } from \"@/db/models/itemModel\";\nimport type { ItemResponse } from \"@/schemas/item\";\n\n/** Business logic for items. Maps raw rows to the response shape. */\nexport class ItemService extends BaseService<typeof ItemModel, ItemResponse> {\n constructor(repository: ItemRepository) {\n super(repository, (row) => ({\n id: row.id,\n isActive: row.isActive,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n name: row.name,\n price: row.price,\n }));\n }\n}\n`,\n\n \"src/controllers/itemController.ts\": `import { BaseController } from \"tempest-express-sdk\";\nimport type { ItemModel } from \"@/db/models/itemModel\";\nimport type { ItemResponse } from \"@/schemas/item\";\nimport type { ItemService } from \"@/services/itemService\";\n\n/** Orchestration boundary between the router and the item service. */\nexport class ItemController extends BaseController<typeof ItemModel, ItemResponse> {\n constructor(service: ItemService) {\n super(service);\n }\n}\n`,\n\n \"src/api/routers/items.ts\": `import { Router } from \"express\";\nimport type { OpenAPIRegistry } from \"tempest-express-sdk\";\nimport { itemCreateSchema, itemResponseSchema } from \"@/schemas/item\";\n\n/**\n * Build the items router and register its OpenAPI paths.\n *\n * NOTE: wire a real controller (with a DB session) here. This stub returns\n * static data so a freshly generated project boots without a database.\n */\nexport function makeItemsRouter(registry: OpenAPIRegistry): Router {\n const router = Router();\n\n registry.registerPath({\n method: \"get\",\n path: \"/api/items\",\n summary: \"List items\",\n responses: {\n 200: {\n description: \"The items\",\n content: { \"application/json\": { schema: itemResponseSchema.array() } },\n },\n },\n });\n\n registry.registerPath({\n method: \"post\",\n path: \"/api/items\",\n summary: \"Create an item\",\n request: {\n body: { content: { \"application/json\": { schema: itemCreateSchema } } },\n },\n responses: {\n 201: {\n description: \"The created item\",\n content: { \"application/json\": { schema: itemResponseSchema } },\n },\n },\n });\n\n router.get(\"/api/items\", (_req, res) => {\n res.json([]);\n });\n\n router.post(\"/api/items\", (req, res) => {\n const data = itemCreateSchema.parse(req.body);\n res.status(201).json({\n id: \"00000000-0000-0000-0000-000000000000\",\n isActive: true,\n createdAt: new Date().toISOString(),\n updatedAt: new Date().toISOString(),\n ...data,\n });\n });\n\n return router;\n}\n`,\n\n \"src/api/app.ts\": `import type { Express } from \"express\";\nimport { createApp, createOpenApiRegistry } from \"tempest-express-sdk\";\nimport { settings } from \"@/core/settings\";\nimport { makeItemsRouter } from \"@/api/routers/items\";\n\n/** Build the configured Express application. */\nexport async function makeApp(): Promise<Express> {\n const registry = createOpenApiRegistry();\n\n return createApp({\n corsOrigins: settings.CORS_ORIGINS,\n openapi: {\n registry,\n info: { title: \"${name}\", version: \"0.1.0\", description: \"Powered by tempest-express-sdk.\" },\n },\n configure: (app) => {\n app.use(makeItemsRouter(registry));\n },\n });\n}\n`,\n\n \"src/server.ts\": `import { runServer } from \"tempest-express-sdk\";\nimport { settings } from \"@/core/settings\";\nimport { makeApp } from \"@/api/app\";\n\n/** Build the app and start listening. */\nexport async function run(): Promise<void> {\n const app = await makeApp();\n await runServer(app, { host: settings.HOST, port: settings.PORT });\n}\n`,\n\n \"docker-compose.yml\": dockerComposeFile(name),\n\n \"README.md\": `# ${name}\n\nGenerated with \\`tempest-express new\\` — an Express + Zod + tempest-db-js service.\n\n## Develop\n\n\\`\\`\\`bash\nnpm install\ncp .env.example .env\nnpm run dev\n\\`\\`\\`\n\n- API: http://127.0.0.1:8000/api/items\n- Swagger UI: http://127.0.0.1:8000/docs\n- Redoc: http://127.0.0.1:8000/redoc\n- Health: http://127.0.0.1:8000/health\n\\`\\`\\`\n`,\n };\n}\n\n/** Lowercase the first letter (PascalCase → camelCase). */\nfunction toCamel(name: string): string {\n return name.charAt(0).toLowerCase() + name.slice(1);\n}\n\n/** Convert `CamelCase` to `snake_case`. */\nfunction toSnake(name: string): string {\n return name.replace(/(?<!^)(?=[A-Z])/g, \"_\").toLowerCase();\n}\n\n/**\n * Build a full CRUD resource file map for a PascalCase resource name.\n *\n * Generates model + schema + repository + service + controller + router under\n * `src/`, mirroring the layered slice the `new` template ships with.\n *\n * @param pascal - The resource name in PascalCase (e.g. `Product`).\n * @returns A map of relative file path to file contents.\n */\nexport function resourceFiles(pascal: string): Record<string, string> {\n const camel = toCamel(pascal);\n const table = toSnake(pascal);\n\n return {\n [`src/db/models/${camel}Model.ts`]: `import { BaseModel, column, tableNameFor } from \"tempest-express-sdk\";\n\n/** The ${pascal} domain model. */\nexport class ${pascal}Model extends BaseModel {\n static tablename = tableNameFor(\"${pascal}Model\"); // \"${table}\"\n name = column.text().notNull();\n}\n`,\n\n [`src/schemas/${camel}.ts`]: `import { baseResponseSchema, z } from \"tempest-express-sdk\";\n\n/** Request payload to create a ${pascal}. */\nexport const ${camel}CreateSchema = z\n .object({ name: z.string().min(1) })\n .openapi(\"${pascal}Create\");\n\n/** Response payload for a ${pascal}. */\nexport const ${camel}ResponseSchema = baseResponseSchema\n .extend({ name: z.string() })\n .openapi(\"${pascal}\");\n\nexport type ${pascal}Create = z.infer<typeof ${camel}CreateSchema>;\nexport type ${pascal}Response = z.infer<typeof ${camel}ResponseSchema>;\n`,\n\n [`src/db/repositories/${camel}Repository.ts`]: `import { type AsyncSession, BaseRepository } from \"tempest-express-sdk\";\nimport { ${pascal}Model } from \"@/db/models/${camel}Model\";\n\n/** Data-access layer for ${pascal}. */\nexport class ${pascal}Repository extends BaseRepository<typeof ${pascal}Model> {\n constructor(session: AsyncSession) {\n super(${pascal}Model, session);\n }\n}\n`,\n\n [`src/services/${camel}Service.ts`]: `import { BaseService } from \"tempest-express-sdk\";\nimport type { ${pascal}Model } from \"@/db/models/${camel}Model\";\nimport type { ${pascal}Repository } from \"@/db/repositories/${camel}Repository\";\nimport type { ${pascal}Response } from \"@/schemas/${camel}\";\n\n/** Business logic for ${pascal}. */\nexport class ${pascal}Service extends BaseService<typeof ${pascal}Model, ${pascal}Response> {\n constructor(repository: ${pascal}Repository) {\n super(repository, (row) => ({\n id: row.id,\n isActive: row.isActive,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n name: row.name,\n }));\n }\n}\n`,\n\n [`src/controllers/${camel}Controller.ts`]: `import { BaseController } from \"tempest-express-sdk\";\nimport type { ${pascal}Model } from \"@/db/models/${camel}Model\";\nimport type { ${pascal}Response } from \"@/schemas/${camel}\";\nimport type { ${pascal}Service } from \"@/services/${camel}Service\";\n\n/** Orchestration boundary for ${pascal}. */\nexport class ${pascal}Controller extends BaseController<typeof ${pascal}Model, ${pascal}Response> {\n constructor(service: ${pascal}Service) {\n super(service);\n }\n}\n`,\n\n [`src/api/routers/${camel}s.ts`]: `import { Router } from \"express\";\nimport type { OpenAPIRegistry } from \"tempest-express-sdk\";\nimport { ${camel}CreateSchema, ${camel}ResponseSchema } from \"@/schemas/${camel}\";\n\n/** Build the ${pascal} router and register its OpenAPI paths. */\nexport function make${pascal}sRouter(registry: OpenAPIRegistry): Router {\n const router = Router();\n\n registry.registerPath({\n method: \"get\",\n path: \"/api/${camel}s\",\n summary: \"List ${camel}s\",\n responses: {\n 200: {\n description: \"OK\",\n content: { \"application/json\": { schema: ${camel}ResponseSchema.array() } },\n },\n },\n });\n registry.registerPath({\n method: \"post\",\n path: \"/api/${camel}s\",\n summary: \"Create a ${camel}\",\n request: {\n body: { content: { \"application/json\": { schema: ${camel}CreateSchema } } },\n },\n responses: {\n 201: {\n description: \"Created\",\n content: { \"application/json\": { schema: ${camel}ResponseSchema } },\n },\n },\n });\n\n router.get(\"/api/${camel}s\", (_req, res) => res.json([]));\n router.post(\"/api/${camel}s\", (req, res) => {\n const data = ${camel}CreateSchema.parse(req.body);\n res.status(201).json({\n id: \"00000000-0000-0000-0000-000000000000\",\n isActive: true,\n createdAt: new Date().toISOString(),\n updatedAt: new Date().toISOString(),\n ...data,\n });\n });\n\n return router;\n}\n`,\n };\n}\n\n/** Build a `docker-compose.yml` with Postgres + Redis for local dev. */\nexport function dockerComposeFile(name: string): string {\n return `services:\n postgres:\n image: postgres:16-alpine\n environment:\n POSTGRES_USER: ${name}\n POSTGRES_PASSWORD: ${name}\n POSTGRES_DB: ${name}\n ports:\n - \"5432:5432\"\n volumes:\n - pgdata:/var/lib/postgresql/data\n redis:\n image: redis:7-alpine\n ports:\n - \"6379:6379\"\n\nvolumes:\n pgdata:\n`;\n}\n","/** The installed SDK version. Single source of truth for the barrel + CLI. */\nexport const VERSION = \"0.11.0\";\n","#!/usr/bin/env node\n/**\n * `tempest-express` command-line interface, mirroring `cli.main`.\n *\n * Commands:\n * - `new <name>` — scaffold a runnable Express service from the SDK template.\n * - `--version` — print the CLI/SDK version.\n * - `--help` — print usage.\n */\n\nimport { randomBytes } from \"node:crypto\";\nimport { mkdir, writeFile } from \"node:fs/promises\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { parseArgs } from \"node:util\";\nimport { dockerComposeFile, projectFiles, resourceFiles } from \"@/cli/template\";\nimport { VERSION } from \"@/version\";\n\nconst USAGE = `tempest-express — Express SDK CLI\n\nUsage:\n tempest-express new <name> [--dir <path>] Scaffold a new service\n tempest-express generate <Name> [--dir <path>] Scaffold a CRUD resource\n tempest-express secret [--bytes <n>] Print a random secret\n tempest-express docker-compose [--dir <path>] Write a docker-compose.yml\n tempest-express db Migration guidance\n tempest-express --version Print the version\n tempest-express --help Show this help\n`;\n\nconst DB_HELP = `Migrations are handled by tempest-db-js (programmatic API):\n\n import { ... } from \"tempest-db-js/migrations\";\n\nSee https://www.npmjs.com/package/tempest-db-js for the migration workflow.\n`;\n\n/** Write a project file map under `root`, creating directories as needed. */\nasync function writeFiles(root: string, files: Record<string, string>): Promise<void> {\n for (const [relative, contents] of Object.entries(files)) {\n const target = join(root, relative);\n await mkdir(dirname(target), { recursive: true });\n await writeFile(target, contents, \"utf8\");\n }\n}\n\n/** Scaffold a new project named `name` into `dir/name`. */\nasync function runNew(name: string, dir: string): Promise<void> {\n if (!/^[a-z0-9][a-z0-9-_]*$/i.test(name)) {\n throw new Error(`Invalid project name: ${JSON.stringify(name)}`);\n }\n const root = resolve(dir, name);\n await writeFiles(root, projectFiles(name));\n process.stdout.write(\n `Created ${name} at ${root}\\n\\nNext steps:\\n cd ${name}\\n npm install\\n cp .env.example .env\\n npm run dev\\n`,\n );\n}\n\n/** Scaffold a CRUD resource into `dir` (PascalCase name required). */\nasync function runGenerate(name: string, dir: string): Promise<void> {\n if (!/^[A-Z][A-Za-z0-9]*$/.test(name)) {\n throw new Error(`Resource name must be PascalCase, got ${JSON.stringify(name)}`);\n }\n const files = resourceFiles(name);\n await writeFiles(resolve(dir), files);\n const written = Object.keys(files)\n .map((path) => ` ${path}`)\n .join(\"\\n\");\n process.stdout.write(`Generated ${name} resource:\\n${written}\\n`);\n}\n\n/**\n * Parse `argv` and dispatch to the matching command.\n *\n * @param argv - Arguments after `node script` (defaults to `process.argv`).\n * @returns The process exit code.\n */\nexport async function main(argv: string[] = process.argv.slice(2)): Promise<number> {\n const { values, positionals } = parseArgs({\n args: argv,\n allowPositionals: true,\n options: {\n version: { type: \"boolean\", short: \"v\" },\n help: { type: \"boolean\", short: \"h\" },\n dir: { type: \"string\", default: \".\" },\n bytes: { type: \"string\", default: \"32\" },\n },\n });\n\n if (values.version) {\n process.stdout.write(`${VERSION}\\n`);\n return 0;\n }\n\n const [command, ...rest] = positionals;\n\n if (values.help || command === undefined || command === \"help\") {\n process.stdout.write(USAGE);\n return 0;\n }\n\n const dir = values.dir ?? \".\";\n\n if (command === \"new\") {\n const name = rest[0];\n if (!name) {\n process.stderr.write(`error: \\`new\\` requires a project name\\n\\n${USAGE}`);\n return 1;\n }\n await runNew(name, dir);\n return 0;\n }\n\n if (command === \"generate\" || command === \"g\") {\n const name = rest[0];\n if (!name) {\n process.stderr.write(`error: \\`generate\\` requires a resource name\\n\\n${USAGE}`);\n return 1;\n }\n await runGenerate(name, dir);\n return 0;\n }\n\n if (command === \"secret\") {\n const nbytes = Number.parseInt(values.bytes ?? \"32\", 10);\n if (!Number.isInteger(nbytes) || nbytes < 16) {\n process.stderr.write(\"error: --bytes must be an integer >= 16\\n\");\n return 1;\n }\n process.stdout.write(`${randomBytes(nbytes).toString(\"base64url\")}\\n`);\n return 0;\n }\n\n if (command === \"docker-compose\") {\n await writeFiles(resolve(dir), { \"docker-compose.yml\": dockerComposeFile(\"app\") });\n process.stdout.write(`Wrote ${resolve(dir, \"docker-compose.yml\")}\\n`);\n return 0;\n }\n\n if (command === \"db\") {\n process.stdout.write(DB_HELP);\n return 0;\n }\n\n process.stderr.write(`error: unknown command ${JSON.stringify(command)}\\n\\n${USAGE}`);\n return 1;\n}\n\nmain()\n .then((code) => {\n process.exitCode = code;\n })\n .catch((error: unknown) => {\n const message = error instanceof Error ? error.message : String(error);\n process.stderr.write(`error: ${message}\\n`);\n process.exitCode = 1;\n });\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/cli/template.ts","../src/schemas/base.ts","../src/settings/base.ts","../src/utils/password.ts","../src/version.ts","../src/cli/index.ts"],"names":["extendZodWithOpenApi","z","existsSync","readFileSync","join","mkdir","dirname","writeFile","resolve","parseArgs","randomBytes","spawnSync","randomUUID"],"mappings":"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","file":"cli.cjs","sourcesContent":["/**\n * Project-template generator for the `new` command, mirroring `cli.new`.\n *\n * Produces a complete, runnable Express service that follows the SDK's layered\n * architecture (router → controller → service → repository → model) and is\n * pre-wired with `createApp`, native Swagger + Redoc, Zod validation, and a\n * `tempest-db-js` model. {@link projectFiles} returns a `path → contents` map\n * the CLI writes to disk.\n */\n\n/** SDK version the generated project depends on (kept in sync at release). */\nconst SDK_VERSION = \"^0.20.0\";\n\n/**\n * Build the file map for a new service named `name`.\n *\n * @param name - The project (and package) name.\n * @returns A map of relative file path to file contents.\n */\nexport function projectFiles(name: string): Record<string, string> {\n return {\n \"package.json\": `${JSON.stringify(\n {\n name,\n version: \"0.1.0\",\n private: true,\n type: \"module\",\n scripts: {\n dev: \"tsx watch main.ts\",\n start: \"tsx main.ts\",\n build: \"tsc -p tsconfig.json\",\n typecheck: \"tsc --noEmit\",\n },\n dependencies: {\n express: \"^5.1.0\",\n \"tempest-db-js\": \"^0.4.0\",\n \"tempest-express-sdk\": SDK_VERSION,\n zod: \"^3.24.1\",\n },\n devDependencies: {\n \"@types/express\": \"^5.0.0\",\n \"@types/node\": \"^22.10.0\",\n tsx: \"^4.19.2\",\n typescript: \"^5.7.0\",\n },\n },\n null,\n 2,\n )}\\n`,\n\n \"tsconfig.json\": `${JSON.stringify(\n {\n compilerOptions: {\n target: \"ES2022\",\n module: \"ESNext\",\n moduleResolution: \"Bundler\",\n lib: [\"ES2022\"],\n strict: true,\n noUncheckedIndexedAccess: true,\n verbatimModuleSyntax: true,\n esModuleInterop: true,\n skipLibCheck: true,\n forceConsistentCasingInFileNames: true,\n outDir: \"dist\",\n paths: { \"@/*\": [\"./src/*\"] },\n },\n include: [\"src\", \"main.ts\"],\n },\n null,\n 2,\n )}\\n`,\n\n \".gitignore\": \"node_modules\\ndist\\n.env\\n*.log\\n\",\n\n \".env.example\":\n \"HOST=127.0.0.1\\nPORT=8000\\nDEBUG=false\\nDATABASE_URL=sqlite://./app.db\\nCORS_ORIGINS=*\\n\",\n\n \"main.ts\": `import { run } from \"@/index\";\n\nrun();\n`,\n\n \"src/index.ts\": `import { run } from \"@/server\";\n\nexport { run };\n`,\n\n \"src/core/settings.ts\": `import { baseAppSettingsShape, loadSettings, z } from \"tempest-express-sdk\";\n\n/** Application settings — extend the SDK base shape with project fields. */\nexport const settingsSchema = z.object({\n ...baseAppSettingsShape,\n});\n\nexport const settings = loadSettings(settingsSchema);\n`,\n\n \"src/db/models/itemModel.ts\": `import { BaseModel, column, tableNameFor } from \"tempest-express-sdk\";\n\n/** A sample domain model. Replace with your own. */\nexport class ItemModel extends BaseModel {\n static tablename = tableNameFor(\"ItemModel\");\n name = column.text().notNull();\n price = column.integer().notNull();\n}\n`,\n\n \"src/schemas/item.ts\": `import { baseResponseSchema, z } from \"tempest-express-sdk\";\n\n/** Request payload to create an item. */\nexport const itemCreateSchema = z\n .object({\n name: z.string().min(1).openapi({ description: \"The item name.\" }),\n price: z.number().int().min(0).openapi({ description: \"Price in cents.\" }),\n })\n .openapi(\"ItemCreate\");\n\n/** Response payload for an item (base columns + domain fields). */\nexport const itemResponseSchema = baseResponseSchema\n .extend({\n name: z.string(),\n price: z.number().int(),\n })\n .openapi(\"Item\");\n\nexport type ItemCreate = z.infer<typeof itemCreateSchema>;\nexport type ItemResponse = z.infer<typeof itemResponseSchema>;\n`,\n\n \"src/db/repositories/itemRepository.ts\": `import { type AsyncSession, BaseRepository } from \"tempest-express-sdk\";\nimport { ItemModel } from \"@/db/models/itemModel\";\n\n/** Data-access layer for items. */\nexport class ItemRepository extends BaseRepository<typeof ItemModel> {\n constructor(session: AsyncSession) {\n super(ItemModel, session);\n }\n}\n`,\n\n \"src/services/itemService.ts\": `import { BaseService } from \"tempest-express-sdk\";\nimport type { ItemRepository } from \"@/db/repositories/itemRepository\";\nimport type { ItemModel } from \"@/db/models/itemModel\";\nimport type { ItemResponse } from \"@/schemas/item\";\n\n/** Business logic for items. Maps raw rows to the response shape. */\nexport class ItemService extends BaseService<typeof ItemModel, ItemResponse> {\n constructor(repository: ItemRepository) {\n super(repository, (row) => ({\n id: row.id,\n isActive: row.isActive,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n name: row.name,\n price: row.price,\n }));\n }\n}\n`,\n\n \"src/controllers/itemController.ts\": `import { BaseController } from \"tempest-express-sdk\";\nimport type { ItemModel } from \"@/db/models/itemModel\";\nimport type { ItemResponse } from \"@/schemas/item\";\nimport type { ItemService } from \"@/services/itemService\";\n\n/** Orchestration boundary between the router and the item service. */\nexport class ItemController extends BaseController<typeof ItemModel, ItemResponse> {\n constructor(service: ItemService) {\n super(service);\n }\n}\n`,\n\n \"src/api/routers/items.ts\": `import { Router } from \"express\";\nimport type { OpenAPIRegistry } from \"tempest-express-sdk\";\nimport { itemCreateSchema, itemResponseSchema } from \"@/schemas/item\";\n\n/**\n * Build the items router and register its OpenAPI paths.\n *\n * NOTE: wire a real controller (with a DB session) here. This stub returns\n * static data so a freshly generated project boots without a database.\n */\nexport function makeItemsRouter(registry: OpenAPIRegistry): Router {\n const router = Router();\n\n registry.registerPath({\n method: \"get\",\n path: \"/api/items\",\n summary: \"List items\",\n responses: {\n 200: {\n description: \"The items\",\n content: { \"application/json\": { schema: itemResponseSchema.array() } },\n },\n },\n });\n\n registry.registerPath({\n method: \"post\",\n path: \"/api/items\",\n summary: \"Create an item\",\n request: {\n body: { content: { \"application/json\": { schema: itemCreateSchema } } },\n },\n responses: {\n 201: {\n description: \"The created item\",\n content: { \"application/json\": { schema: itemResponseSchema } },\n },\n },\n });\n\n router.get(\"/api/items\", (_req, res) => {\n res.json([]);\n });\n\n router.post(\"/api/items\", (req, res) => {\n const data = itemCreateSchema.parse(req.body);\n res.status(201).json({\n id: \"00000000-0000-0000-0000-000000000000\",\n isActive: true,\n createdAt: new Date().toISOString(),\n updatedAt: new Date().toISOString(),\n ...data,\n });\n });\n\n return router;\n}\n`,\n\n \"src/api/app.ts\": `import type { Express } from \"express\";\nimport { createApp, createOpenApiRegistry } from \"tempest-express-sdk\";\nimport { settings } from \"@/core/settings\";\nimport { makeItemsRouter } from \"@/api/routers/items\";\n\n/** Build the configured Express application. */\nexport async function makeApp(): Promise<Express> {\n const registry = createOpenApiRegistry();\n\n return createApp({\n corsOrigins: settings.CORS_ORIGINS,\n openapi: {\n registry,\n info: { title: \"${name}\", version: \"0.1.0\", description: \"Powered by tempest-express-sdk.\" },\n },\n configure: (app) => {\n app.use(makeItemsRouter(registry));\n },\n });\n}\n`,\n\n \"src/server.ts\": `import { runServer } from \"tempest-express-sdk\";\nimport { settings } from \"@/core/settings\";\nimport { makeApp } from \"@/api/app\";\n\n/** Build the app and start listening. */\nexport async function run(): Promise<void> {\n const app = await makeApp();\n await runServer(app, { host: settings.HOST, port: settings.PORT });\n}\n`,\n\n \"docker-compose.yml\": dockerComposeFile(name),\n\n \"README.md\": `# ${name}\n\nGenerated with \\`tempest-express new\\` — an Express + Zod + tempest-db-js service.\n\n## Develop\n\n\\`\\`\\`bash\nnpm install\ncp .env.example .env\nnpm run dev\n\\`\\`\\`\n\n- API: http://127.0.0.1:8000/api/items\n- Swagger UI: http://127.0.0.1:8000/docs\n- Redoc: http://127.0.0.1:8000/redoc\n- Health: http://127.0.0.1:8000/health\n\\`\\`\\`\n`,\n };\n}\n\n/** Lowercase the first letter (PascalCase → camelCase). */\nfunction toCamel(name: string): string {\n return name.charAt(0).toLowerCase() + name.slice(1);\n}\n\n/** Convert `CamelCase` to `snake_case`. */\nfunction toSnake(name: string): string {\n return name.replace(/(?<!^)(?=[A-Z])/g, \"_\").toLowerCase();\n}\n\n/**\n * Build a full CRUD resource file map for a PascalCase resource name.\n *\n * Generates model + schema + repository + service + controller + router under\n * `src/`, mirroring the layered slice the `new` template ships with.\n *\n * @param pascal - The resource name in PascalCase (e.g. `Product`).\n * @returns A map of relative file path to file contents.\n */\nexport function resourceFiles(pascal: string): Record<string, string> {\n const camel = toCamel(pascal);\n const table = toSnake(pascal);\n\n return {\n [`src/db/models/${camel}Model.ts`]: `import { BaseModel, column, tableNameFor } from \"tempest-express-sdk\";\n\n/** The ${pascal} domain model. */\nexport class ${pascal}Model extends BaseModel {\n static tablename = tableNameFor(\"${pascal}Model\"); // \"${table}\"\n name = column.text().notNull();\n}\n`,\n\n [`src/schemas/${camel}.ts`]: `import { baseResponseSchema, z } from \"tempest-express-sdk\";\n\n/** Request payload to create a ${pascal}. */\nexport const ${camel}CreateSchema = z\n .object({ name: z.string().min(1) })\n .openapi(\"${pascal}Create\");\n\n/** Response payload for a ${pascal}. */\nexport const ${camel}ResponseSchema = baseResponseSchema\n .extend({ name: z.string() })\n .openapi(\"${pascal}\");\n\nexport type ${pascal}Create = z.infer<typeof ${camel}CreateSchema>;\nexport type ${pascal}Response = z.infer<typeof ${camel}ResponseSchema>;\n`,\n\n [`src/db/repositories/${camel}Repository.ts`]: `import { type AsyncSession, BaseRepository } from \"tempest-express-sdk\";\nimport { ${pascal}Model } from \"@/db/models/${camel}Model\";\n\n/** Data-access layer for ${pascal}. */\nexport class ${pascal}Repository extends BaseRepository<typeof ${pascal}Model> {\n constructor(session: AsyncSession) {\n super(${pascal}Model, session);\n }\n}\n`,\n\n [`src/services/${camel}Service.ts`]: `import { BaseService } from \"tempest-express-sdk\";\nimport type { ${pascal}Model } from \"@/db/models/${camel}Model\";\nimport type { ${pascal}Repository } from \"@/db/repositories/${camel}Repository\";\nimport type { ${pascal}Response } from \"@/schemas/${camel}\";\n\n/** Business logic for ${pascal}. */\nexport class ${pascal}Service extends BaseService<typeof ${pascal}Model, ${pascal}Response> {\n constructor(repository: ${pascal}Repository) {\n super(repository, (row) => ({\n id: row.id,\n isActive: row.isActive,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n name: row.name,\n }));\n }\n}\n`,\n\n [`src/controllers/${camel}Controller.ts`]: `import { BaseController } from \"tempest-express-sdk\";\nimport type { ${pascal}Model } from \"@/db/models/${camel}Model\";\nimport type { ${pascal}Response } from \"@/schemas/${camel}\";\nimport type { ${pascal}Service } from \"@/services/${camel}Service\";\n\n/** Orchestration boundary for ${pascal}. */\nexport class ${pascal}Controller extends BaseController<typeof ${pascal}Model, ${pascal}Response> {\n constructor(service: ${pascal}Service) {\n super(service);\n }\n}\n`,\n\n [`src/api/routers/${camel}s.ts`]: `import { Router } from \"express\";\nimport type { OpenAPIRegistry } from \"tempest-express-sdk\";\nimport { ${camel}CreateSchema, ${camel}ResponseSchema } from \"@/schemas/${camel}\";\n\n/** Build the ${pascal} router and register its OpenAPI paths. */\nexport function make${pascal}sRouter(registry: OpenAPIRegistry): Router {\n const router = Router();\n\n registry.registerPath({\n method: \"get\",\n path: \"/api/${camel}s\",\n summary: \"List ${camel}s\",\n responses: {\n 200: {\n description: \"OK\",\n content: { \"application/json\": { schema: ${camel}ResponseSchema.array() } },\n },\n },\n });\n registry.registerPath({\n method: \"post\",\n path: \"/api/${camel}s\",\n summary: \"Create a ${camel}\",\n request: {\n body: { content: { \"application/json\": { schema: ${camel}CreateSchema } } },\n },\n responses: {\n 201: {\n description: \"Created\",\n content: { \"application/json\": { schema: ${camel}ResponseSchema } },\n },\n },\n });\n\n router.get(\"/api/${camel}s\", (_req, res) => res.json([]));\n router.post(\"/api/${camel}s\", (req, res) => {\n const data = ${camel}CreateSchema.parse(req.body);\n res.status(201).json({\n id: \"00000000-0000-0000-0000-000000000000\",\n isActive: true,\n createdAt: new Date().toISOString(),\n updatedAt: new Date().toISOString(),\n ...data,\n });\n });\n\n return router;\n}\n`,\n };\n}\n\n/** Build a `docker-compose.yml` with Postgres + Redis for local dev. */\nexport function dockerComposeFile(name: string): string {\n return `services:\n postgres:\n image: postgres:16-alpine\n environment:\n POSTGRES_USER: ${name}\n POSTGRES_PASSWORD: ${name}\n POSTGRES_DB: ${name}\n ports:\n - \"5432:5432\"\n volumes:\n - pgdata:/var/lib/postgresql/data\n redis:\n image: redis:7-alpine\n ports:\n - \"6379:6379\"\n\nvolumes:\n pgdata:\n`;\n}\n","/**\n * Zod foundation shared by every DTO, mirroring `schemas.base.BaseSchema`.\n *\n * Pydantic uses class inheritance for shared config; Zod composes instead. This\n * module re-exports a `z` already augmented with `.openapi()` (so every schema\n * can carry OpenAPI metadata) and a {@link toDict} helper matching\n * `BaseSchema.to_dict` (drop nullish, exclude keys, merge extras).\n */\n\nimport { extendZodWithOpenApi } from \"@asteasolutions/zod-to-openapi\";\nimport { z } from \"zod\";\n\n// Augment the shared `z` instance with `.openapi(...)`. Idempotent — calling it\n// more than once across modules is safe.\nextendZodWithOpenApi(z);\n\nexport { z };\n\n/** Options for {@link toDict}. */\nexport interface ToDictOptions {\n /** Field names to drop from the output. */\n exclude?: string[];\n /** Extra entries merged on top (override existing keys). */\n include?: Record<string, unknown>;\n}\n\n/**\n * Serialize a validated object to a plain record, dropping `null`/`undefined`,\n * removing `exclude`d keys and merging `include` on top.\n *\n * @param data - The source object (typically a parsed schema).\n * @param options - Keys to exclude and entries to merge in.\n * @returns The cleaned record.\n */\nexport function toDict(\n data: Record<string, unknown>,\n options: ToDictOptions = {},\n): Record<string, unknown> {\n const exclude = new Set(options.exclude ?? []);\n const out: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(data)) {\n if (exclude.has(key)) continue;\n if (value === null || value === undefined) continue;\n out[key] = value;\n }\n return { ...out, ...(options.include ?? {}) };\n}\n\n/**\n * Response schema fields every ORM record carries (`id`, `isActive`,\n * `createdAt`, `updatedAt`). Mirrors `BaseResponseSchema`. Extend it with\n * `baseResponseSchema.extend({ ... })` to build concrete `*ResponseSchema`s.\n */\nexport const baseResponseSchema = z.object({\n id: z.string().uuid().openapi({ description: \"The unique identifier of the record.\" }),\n isActive: z\n .boolean()\n .openapi({ description: \"Whether the record is active (soft-delete flag).\" }),\n createdAt: z.coerce\n .date()\n .openapi({ description: \"The creation timestamp of the record (UTC).\" }),\n updatedAt: z.coerce\n .date()\n .openapi({ description: \"The last update timestamp of the record (UTC).\" }),\n});\n\n/** The inferred TS type of a {@link baseResponseSchema} payload. */\nexport type BaseResponse = z.infer<typeof baseResponseSchema>;\n","/**\n * Environment-driven settings, mirroring `settings.base.BaseAppSettings`.\n *\n * Pydantic-settings reads env vars into a frozen, validated model. Here a zod\n * schema plays the same role: {@link loadSettings} parses `process.env` (env\n * names are matched case-sensitively, like the FastAPI SDK) and returns a\n * frozen, fully-typed object. Composable fragments ({@link serverSettingsShape}\n * etc.) cover the common server/database/CORS knobs.\n */\n\nimport { z } from \"@/schemas/base\";\n\n/** Server bind/runtime settings. Defaults bind to localhost. */\nexport const serverSettingsShape = {\n HOST: z.string().default(\"127.0.0.1\"),\n PORT: z.coerce.number().int().min(0).max(65535).default(8000),\n DEBUG: z.coerce.boolean().default(false),\n} as const;\n\n/** Database connection settings. */\nexport const databaseSettingsShape = {\n DATABASE_URL: z.string().default(\"sqlite://./app.db\"),\n} as const;\n\n/** CORS settings. `CORS_ORIGINS` is a comma-separated list. */\nexport const corsSettingsShape = {\n CORS_ORIGINS: z\n .string()\n .default(\"*\")\n .transform((value) =>\n value\n .split(\",\")\n .map((origin) => origin.trim())\n .filter((origin) => origin.length > 0),\n ),\n} as const;\n\n/** The combined base settings shape (server + database + CORS). */\nexport const baseAppSettingsShape = {\n ...serverSettingsShape,\n ...databaseSettingsShape,\n ...corsSettingsShape,\n} as const;\n\n/** A zod object built from {@link baseAppSettingsShape}. */\nexport const baseAppSettingsSchema = z.object(baseAppSettingsShape);\n\n/** The parsed shape of {@link baseAppSettingsSchema}. */\nexport type BaseAppSettings = z.infer<typeof baseAppSettingsSchema>;\n\n/**\n * Parse and freeze settings from an environment source.\n *\n * Extend the base shape with project fields:\n *\n * ```ts\n * const settings = loadSettings(\n * z.object({ ...baseAppSettingsShape, JWT_SECRET: z.string() }),\n * );\n * ```\n *\n * @param schema - The settings zod schema.\n * @param env - The environment source (defaults to `process.env`).\n * @returns The validated, frozen settings object.\n * @throws {z.ZodError} When required env vars are missing or malformed.\n */\nexport function loadSettings<S extends z.ZodTypeAny>(\n schema: S,\n env: NodeJS.ProcessEnv = process.env,\n): Readonly<z.infer<S>> {\n return Object.freeze(schema.parse(env));\n}\n","/**\n * Password hashing backed by bcrypt, mirroring `utils.password.PasswordUtils`.\n *\n * `bcryptjs` is an optional peer dependency, imported lazily so that\n * `import \"tempest-express-sdk\"` keeps working when it is not installed — the\n * clear error is deferred to first use. Methods are async (the idiomatic Node\n * bcrypt surface).\n */\n\ntype BcryptModule = typeof import(\"bcryptjs\");\n\nlet cached: BcryptModule | null = null;\n\n/** Lazily load `bcryptjs`, with a clear install hint when missing. */\nasync function loadBcrypt(): Promise<BcryptModule> {\n if (cached) return cached;\n try {\n const mod = (await import(\"bcryptjs\")) as BcryptModule & { default?: BcryptModule };\n cached = mod.default ?? mod;\n } catch (cause) {\n throw new Error(\n \"PasswordUtils requires the 'bcryptjs' peer dependency. Install with `npm i bcryptjs`.\",\n { cause },\n );\n }\n return cached;\n}\n\n/** Hash and verify passwords using bcrypt. Stateless — construct once, reuse. */\nexport class PasswordUtils {\n /**\n * @param rounds - The bcrypt cost factor. Higher is slower and harder to\n * brute-force. Defaults to 12.\n */\n constructor(private readonly rounds: number = 12) {}\n\n /**\n * Hash a plaintext password.\n *\n * @param plain - The plaintext password.\n * @returns The bcrypt hash, ready to persist.\n */\n async hash(plain: string): Promise<string> {\n const bcrypt = await loadBcrypt();\n const salt = await bcrypt.genSalt(this.rounds);\n return bcrypt.hash(plain, salt);\n }\n\n /**\n * Verify a plaintext password against a stored hash. Returns `false` for\n * malformed hashes rather than throwing.\n *\n * @param plain - The plaintext password to verify.\n * @param hashed - The previously stored bcrypt hash.\n * @returns `true` when the password matches.\n */\n async verify(plain: string, hashed: string): Promise<boolean> {\n try {\n const bcrypt = await loadBcrypt();\n return await bcrypt.compare(plain, hashed);\n } catch {\n return false;\n }\n }\n}\n","/** The installed SDK version. Single source of truth for the barrel + CLI. */\nexport const VERSION = \"0.20.1\";\n","#!/usr/bin/env node\n/**\n * `tempest-express` command-line interface, mirroring `cli.main`.\n *\n * Commands:\n * - `new <name>` — scaffold a runnable Express service from the SDK template.\n * - `--version` — print the CLI/SDK version.\n * - `--help` — print usage.\n */\n\nimport { spawnSync } from \"node:child_process\";\nimport { randomBytes, randomUUID } from \"node:crypto\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { mkdir, writeFile } from \"node:fs/promises\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { parseArgs } from \"node:util\";\nimport { dockerComposeFile, projectFiles, resourceFiles } from \"@/cli/template\";\nimport { baseAppSettingsSchema, loadSettings } from \"@/settings/base\";\nimport { PasswordUtils } from \"@/utils/password\";\nimport { VERSION } from \"@/version\";\n\nconst USAGE = `tempest-express — Express SDK CLI\n\nUsage:\n tempest-express new <name> [--dir <path>] Scaffold a new service\n tempest-express generate <Name> [--dir <path>] Scaffold a CRUD resource\n tempest-express secret [--bytes <n>] Print a random secret\n tempest-express docker-compose [--dir <path>] Write a docker-compose.yml\n tempest-express db Migration guidance\n tempest-express lint [--dir <path>] Run Biome check in a project\n tempest-express config [--dir <path>] Print the resolved base settings\n tempest-express user --email <e> --password <p> [--admin]\n Print a ready-to-insert user record\n tempest-express --version Print the version\n tempest-express --help Show this help\n`;\n\nconst DB_HELP = `Migrations are handled by tempest-db-js (programmatic API):\n\n import { ... } from \"tempest-db-js/migrations\";\n\nSee https://www.npmjs.com/package/tempest-db-js for the migration workflow.\n`;\n\n/** Parse a `.env` file's `KEY=VALUE` lines into a record (ignores comments). */\nfunction parseDotEnv(path: string): Record<string, string> {\n const out: Record<string, string> = {};\n if (!existsSync(path)) return out;\n for (const raw of readFileSync(path, \"utf8\").split(\"\\n\")) {\n const line = raw.trim();\n if (!line || line.startsWith(\"#\")) continue;\n const eq = line.indexOf(\"=\");\n if (eq === -1) continue;\n const key = line.slice(0, eq).trim();\n let value = line.slice(eq + 1).trim();\n if (\n (value.startsWith('\"') && value.endsWith('\"')) ||\n (value.startsWith(\"'\") && value.endsWith(\"'\"))\n ) {\n value = value.slice(1, -1);\n }\n out[key] = value;\n }\n return out;\n}\n\n/** Write a project file map under `root`, creating directories as needed. */\nasync function writeFiles(root: string, files: Record<string, string>): Promise<void> {\n for (const [relative, contents] of Object.entries(files)) {\n const target = join(root, relative);\n await mkdir(dirname(target), { recursive: true });\n await writeFile(target, contents, \"utf8\");\n }\n}\n\n/** Scaffold a new project named `name` into `dir/name`. */\nasync function runNew(name: string, dir: string): Promise<void> {\n if (!/^[a-z0-9][a-z0-9-_]*$/i.test(name)) {\n throw new Error(`Invalid project name: ${JSON.stringify(name)}`);\n }\n const root = resolve(dir, name);\n await writeFiles(root, projectFiles(name));\n process.stdout.write(\n `Created ${name} at ${root}\\n\\nNext steps:\\n cd ${name}\\n npm install\\n cp .env.example .env\\n npm run dev\\n`,\n );\n}\n\n/** Scaffold a CRUD resource into `dir` (PascalCase name required). */\nasync function runGenerate(name: string, dir: string): Promise<void> {\n if (!/^[A-Z][A-Za-z0-9]*$/.test(name)) {\n throw new Error(`Resource name must be PascalCase, got ${JSON.stringify(name)}`);\n }\n const files = resourceFiles(name);\n await writeFiles(resolve(dir), files);\n const written = Object.keys(files)\n .map((path) => ` ${path}`)\n .join(\"\\n\");\n process.stdout.write(`Generated ${name} resource:\\n${written}\\n`);\n}\n\n/**\n * Parse `argv` and dispatch to the matching command.\n *\n * @param argv - Arguments after `node script` (defaults to `process.argv`).\n * @returns The process exit code.\n */\nexport async function main(argv: string[] = process.argv.slice(2)): Promise<number> {\n const { values, positionals } = parseArgs({\n args: argv,\n allowPositionals: true,\n options: {\n version: { type: \"boolean\", short: \"v\" },\n help: { type: \"boolean\", short: \"h\" },\n dir: { type: \"string\", default: \".\" },\n bytes: { type: \"string\", default: \"32\" },\n email: { type: \"string\" },\n password: { type: \"string\" },\n admin: { type: \"boolean\", default: false },\n },\n });\n\n if (values.version) {\n process.stdout.write(`${VERSION}\\n`);\n return 0;\n }\n\n const [command, ...rest] = positionals;\n\n if (values.help || command === undefined || command === \"help\") {\n process.stdout.write(USAGE);\n return 0;\n }\n\n const dir = values.dir ?? \".\";\n\n if (command === \"new\") {\n const name = rest[0];\n if (!name) {\n process.stderr.write(`error: \\`new\\` requires a project name\\n\\n${USAGE}`);\n return 1;\n }\n await runNew(name, dir);\n return 0;\n }\n\n if (command === \"generate\" || command === \"g\") {\n const name = rest[0];\n if (!name) {\n process.stderr.write(`error: \\`generate\\` requires a resource name\\n\\n${USAGE}`);\n return 1;\n }\n await runGenerate(name, dir);\n return 0;\n }\n\n if (command === \"secret\") {\n const nbytes = Number.parseInt(values.bytes ?? \"32\", 10);\n if (!Number.isInteger(nbytes) || nbytes < 16) {\n process.stderr.write(\"error: --bytes must be an integer >= 16\\n\");\n return 1;\n }\n process.stdout.write(`${randomBytes(nbytes).toString(\"base64url\")}\\n`);\n return 0;\n }\n\n if (command === \"docker-compose\") {\n await writeFiles(resolve(dir), { \"docker-compose.yml\": dockerComposeFile(\"app\") });\n process.stdout.write(`Wrote ${resolve(dir, \"docker-compose.yml\")}\\n`);\n return 0;\n }\n\n if (command === \"db\") {\n process.stdout.write(DB_HELP);\n return 0;\n }\n\n if (command === \"lint\") {\n const target = resolve(dir);\n const result = spawnSync(\"npx\", [\"biome\", \"check\", target], {\n stdio: \"inherit\",\n shell: process.platform === \"win32\",\n });\n return result.status ?? 1;\n }\n\n if (command === \"config\") {\n const env = { ...process.env, ...parseDotEnv(join(resolve(dir), \".env\")) };\n const settings = loadSettings(baseAppSettingsSchema, env);\n process.stdout.write(`${JSON.stringify(settings, null, 2)}\\n`);\n return 0;\n }\n\n if (command === \"user\") {\n const { email, password } = values;\n if (!email || !password) {\n process.stderr.write(\"error: `user` requires --email and --password\\n\");\n return 1;\n }\n const hashedPassword = await new PasswordUtils().hash(password);\n const record = {\n id: randomUUID(),\n email,\n hashedPassword,\n isAdmin: Boolean(values.admin),\n isActive: true,\n };\n process.stdout.write(`${JSON.stringify(record, null, 2)}\\n`);\n return 0;\n }\n\n process.stderr.write(`error: unknown command ${JSON.stringify(command)}\\n\\n${USAGE}`);\n return 1;\n}\n\n// Auto-run as a binary, but stay importable from tests without executing.\nif (!process.env.VITEST) {\n main()\n .then((code) => {\n process.exitCode = code;\n })\n .catch((error: unknown) => {\n const message = error instanceof Error ? error.message : String(error);\n process.stderr.write(`error: ${message}\\n`);\n process.exitCode = 1;\n });\n}\n"]}
|