@podosoft/podokit 0.14.0 → 0.16.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/README.md +20 -0
- package/dist/add.d.ts +4 -0
- package/dist/add.js +59 -1
- package/dist/create.js +8 -1
- package/dist/deploy-profile.d.ts +92 -0
- package/dist/deploy-profile.js +603 -0
- package/dist/deploy-render.d.ts +18 -0
- package/dist/deploy-render.js +651 -0
- package/dist/deploy.d.ts +65 -0
- package/dist/deploy.js +851 -0
- package/dist/dev.d.ts +1 -0
- package/dist/dev.js +29 -5
- package/dist/index.d.ts +8 -0
- package/dist/index.js +147 -1
- package/dist/lib.d.ts +6 -0
- package/dist/lib.js +18 -1
- package/dist/lockfile.js +3 -0
- package/dist/remove.js +7 -0
- package/dist/templates/fullstack-nest-svelte/AGENTS.md +1 -0
- package/dist/templates/fullstack-nest-svelte/README.md +19 -8
- package/dist/templates/fullstack-nest-svelte/apps/api/Dockerfile +2 -2
- package/dist/templates/fullstack-nest-svelte/apps/api/package.json +1 -0
- package/dist/templates/fullstack-nest-svelte/apps/api/src/config/redis.connection.spec.ts +53 -0
- package/dist/templates/fullstack-nest-svelte/apps/api/src/config/redis.connection.ts +69 -0
- package/dist/templates/fullstack-nest-svelte/apps/api/src/database/migrate.ts +16 -0
- package/dist/templates/fullstack-nest-svelte/apps/api/src/health/health.controller.ts +19 -4
- package/dist/templates/fullstack-nest-svelte/apps/api/src/health/health.module.ts +5 -1
- package/dist/templates/fullstack-nest-svelte/apps/api/src/health/readiness.service.spec.ts +22 -0
- package/dist/templates/fullstack-nest-svelte/apps/api/src/health/readiness.service.ts +33 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/Dockerfile +2 -2
- package/dist/templates/fullstack-nest-svelte/apps/web/src/app.html +2 -0
- package/dist/templates/fullstack-nest-svelte/compose.dev.yaml +3 -5
- package/dist/templates/fullstack-nest-svelte/dot-agents/skills/podokit-deploy/SKILL.md +20 -0
- package/dist/templates/fullstack-nest-svelte/dot-agents/skills/podokit-deploy/agents/openai.yaml +4 -0
- package/dist/templates/fullstack-nest-svelte/dot-agents/skills/podokit-deploy/references/profile.md +37 -0
- package/dist/templates/fullstack-nest-svelte/dot-agents/skills/podokit-deploy/references/rollback.md +18 -0
- package/dist/templates/fullstack-nest-svelte/dot-claude/skills/podokit-deploy/SKILL.md +8 -0
- package/dist/templates/fullstack-nest-svelte/dot-env.example +6 -0
- package/dist/templates/fullstack-nest-svelte/infra/k3s/api-deployment.yaml +1 -1
- package/dist/templates/fullstack-nest-svelte/infra/k3s/ingress.yaml +0 -4
- package/dist/templates/fullstack-nest-svelte/tests/playwright.config.ts +6 -4
- package/dist/templates/fullstack-nest-svelte/tests/playwright.extensions.ts +44 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/account-menu.svelte +35 -4
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/data-table.svelte +33 -4
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/site-runtime.svelte +6 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(admin)/admin/users/+page.svelte +18 -10
- package/dist/templates/modules/admin-dashboard/files/dot-claude/skills/podokit-data-table/SKILL.md +11 -6
- package/dist/templates/modules/admin-dashboard/files/tests/api/audit.api.spec.ts +18 -8
- package/dist/templates/modules/admin-dashboard/files/tests/api/users-admin.api.spec.ts +15 -9
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/accounts.ts +1 -0
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/disposable-users.ts +83 -0
- package/dist/templates/modules/admin-dashboard/files/tests/playwright.config.ts +13 -10
- package/dist/templates/modules/admin-dashboard/files/tests/seed.setup.ts +69 -3
- package/dist/templates/modules/admin-dashboard/files/tests/seed.teardown.ts +46 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/data-table.ui.spec.ts +13 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/email-admin.ui.spec.ts +3 -6
- package/dist/templates/modules/admin-dashboard/files/tests/ui/users-crud.ui.spec.ts +34 -28
- package/dist/templates/modules/admin-dashboard/files/tests/ui/users.ui.spec.ts +29 -39
- package/dist/templates/modules/admin-dashboard/module.manifest.json +1 -1
- package/dist/templates/modules/api-key-auth/files/apps/api/src/api-key/api-key-verifier.spec.ts +36 -0
- package/dist/templates/modules/api-key-auth/files/apps/api/src/api-key/api-key-verifier.ts +20 -0
- package/dist/templates/modules/api-key-auth/files/apps/api/src/api-key/api-key.guard.spec.ts +49 -0
- package/dist/templates/modules/api-key-auth/files/apps/api/src/api-key/api-key.guard.ts +3 -11
- package/dist/templates/modules/api-key-auth/files/apps/api/src/api-key/api-key.module.ts +3 -1
- package/dist/templates/modules/api-key-auth/module.manifest.json +1 -1
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth-environment.ts +32 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.runtime.spec.ts +35 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.ts +3 -2
- package/dist/templates/modules/auth/files/apps/api/src/health/health.controller.ts +19 -4
- package/dist/templates/modules/bullmq/files/apps/api/src/jobs/queue.ts +4 -5
- package/dist/templates/modules/bullmq/module.manifest.json +4 -1
- package/dist/templates/modules/job-progress/files/apps/api/src/progress/progress.bridge.spec.ts +36 -0
- package/dist/templates/modules/job-progress/files/apps/api/src/progress/progress.bridge.ts +26 -5
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/storage.service.spec.ts +35 -0
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/storage.service.ts +21 -2
- package/dist/templates/modules/object-storage-s3/module.manifest.json +3 -0
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.config.spec.ts +85 -0
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.config.ts +110 -0
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.identity.spec.ts +132 -0
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.identity.ts +100 -0
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.module.integration.spec.ts +91 -0
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.module.spec.ts +228 -0
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.module.ts +97 -38
- package/dist/templates/modules/rate-limit/files/tests/api/rate-limit.api.spec.ts +11 -6
- package/dist/templates/modules/rate-limit/module.manifest.json +23 -9
- package/dist/templates/modules/redis/files/apps/api/src/redis/redis.service.spec.ts +28 -0
- package/dist/templates/modules/redis/files/apps/api/src/redis/redis.service.ts +52 -11
- package/dist/templates/modules/redis/module.manifest.json +16 -1
- package/dist/templates/modules/sse/files/apps/api/src/events/events.controller.ts +17 -3
- package/dist/templates/modules/sse/files/apps/api/src/events/events.module.ts +9 -2
- package/dist/templates/modules/sse/files/apps/api/src/events/events.service.spec.ts +61 -0
- package/dist/templates/modules/sse/files/apps/api/src/events/events.service.ts +44 -4
- package/dist/templates/modules/sse/files/apps/api/src/events/events.transport.redis.spec.ts +42 -0
- package/dist/templates/modules/sse/files/apps/api/src/events/events.transport.spec.ts +97 -0
- package/dist/templates/modules/sse/files/apps/api/src/events/events.transport.ts +165 -0
- package/dist/templates/modules/sse/files/tests/api/events.api.spec.ts +41 -0
- package/dist/templates/modules/sse/module.manifest.json +16 -3
- package/dist/templates/todo/AGENTS.md +1 -0
- package/dist/templates/todo/README.md +12 -8
- package/dist/templates/todo/apps/api/Dockerfile +2 -2
- package/dist/templates/todo/apps/api/package.json +1 -0
- package/dist/templates/todo/apps/api/src/config/redis.connection.spec.ts +53 -0
- package/dist/templates/todo/apps/api/src/config/redis.connection.ts +69 -0
- package/dist/templates/todo/apps/api/src/database/migrate.ts +16 -0
- package/dist/templates/todo/apps/api/src/health/health.controller.ts +19 -4
- package/dist/templates/todo/apps/api/src/health/health.module.ts +5 -1
- package/dist/templates/todo/apps/api/src/health/readiness.service.spec.ts +22 -0
- package/dist/templates/todo/apps/api/src/health/readiness.service.ts +33 -0
- package/dist/templates/todo/apps/web/Dockerfile +2 -2
- package/dist/templates/todo/compose.dev.yaml +3 -5
- package/dist/templates/todo/dot-agents/skills/podokit-deploy/SKILL.md +20 -0
- package/dist/templates/todo/dot-agents/skills/podokit-deploy/agents/openai.yaml +4 -0
- package/dist/templates/todo/dot-agents/skills/podokit-deploy/references/profile.md +28 -0
- package/dist/templates/todo/dot-agents/skills/podokit-deploy/references/rollback.md +5 -0
- package/dist/templates/todo/dot-claude/skills/podokit-deploy/SKILL.md +8 -0
- package/dist/templates/todo/dot-env.example +6 -0
- package/dist/templates/todo/infra/k3s/api-deployment.yaml +1 -1
- package/dist/templates/todo/infra/k3s/ingress.yaml +0 -4
- package/dist/templates/todo/tests/playwright.config.ts +6 -4
- package/dist/templates/todo/tests/playwright.extensions.ts +44 -0
- package/dist/update.js +17 -13
- package/package.json +2 -2
package/dist/dev.d.ts
CHANGED
|
@@ -20,6 +20,7 @@ export type CommandRunner = (command: string, args: string[], options: {
|
|
|
20
20
|
}) => CommandResult;
|
|
21
21
|
export declare function readDevConfig(projectRoot: string): DevConfig;
|
|
22
22
|
export declare function resolveDevRuntime(projectRoot: string): DevRuntime;
|
|
23
|
+
export declare function installedDevProfiles(projectRoot: string): string[];
|
|
23
24
|
export declare function renderRuntimeCompose(runtime: DevRuntime): string;
|
|
24
25
|
export declare function renderRoute(runtime: DevRuntime): string;
|
|
25
26
|
export declare function runDevCommand(projectRoot: string, action: string | undefined, args: string[], runner?: CommandRunner): void;
|
package/dist/dev.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.readDevConfig = readDevConfig;
|
|
4
4
|
exports.resolveDevRuntime = resolveDevRuntime;
|
|
5
|
+
exports.installedDevProfiles = installedDevProfiles;
|
|
5
6
|
exports.renderRuntimeCompose = renderRuntimeCompose;
|
|
6
7
|
exports.renderRoute = renderRoute;
|
|
7
8
|
exports.runDevCommand = runDevCommand;
|
|
@@ -17,6 +18,11 @@ const GATEWAY_LABEL = "io.podosoft.podokit.dev-gateway";
|
|
|
17
18
|
const GATEWAY_NETWORK = "podokit-dev-gateway";
|
|
18
19
|
const RUNTIME_COMPOSE = ".podokit/runtime/dev.compose.yaml";
|
|
19
20
|
const DEV_CONFIG = ".podokit/dev.json";
|
|
21
|
+
const DEV_PROFILE_BY_MODULE = new Map([
|
|
22
|
+
["redis", "cache"],
|
|
23
|
+
["object-storage-s3", "storage"],
|
|
24
|
+
["bullmq", "queue"],
|
|
25
|
+
]);
|
|
20
26
|
function defaultRunner(command, args, options) {
|
|
21
27
|
const result = (0, node_child_process_1.spawnSync)(command, args, {
|
|
22
28
|
encoding: "utf8",
|
|
@@ -96,6 +102,16 @@ function resolveDevRuntime(projectRoot) {
|
|
|
96
102
|
runtimeComposePath: (0, node_path_1.join)(root, RUNTIME_COMPOSE),
|
|
97
103
|
};
|
|
98
104
|
}
|
|
105
|
+
function installedDevProfiles(projectRoot) {
|
|
106
|
+
const manifest = (0, lockfile_1.readManifest)(projectRoot);
|
|
107
|
+
if (!manifest)
|
|
108
|
+
return [];
|
|
109
|
+
return [
|
|
110
|
+
...new Set(manifest.modules
|
|
111
|
+
.map((module) => DEV_PROFILE_BY_MODULE.get(module.name))
|
|
112
|
+
.filter((profile) => profile !== undefined)),
|
|
113
|
+
];
|
|
114
|
+
}
|
|
99
115
|
function renderRuntimeCompose(runtime) {
|
|
100
116
|
return `# Generated by podo dev. Do not edit.\nservices:\n traefik:\n profiles: [podokit-legacy-proxy]\n web:\n networks:\n ${GATEWAY_NETWORK}:\n aliases: [${runtime.alias}]\nnetworks:\n ${GATEWAY_NETWORK}:\n external: true\n name: ${GATEWAY_NETWORK}\n`;
|
|
101
117
|
}
|
|
@@ -226,9 +242,10 @@ function runCompose(runtime, args, runner) {
|
|
|
226
242
|
throw new Error(`docker compose ${args.join(" ")} exited with status ${String(result.status)}.`);
|
|
227
243
|
}
|
|
228
244
|
}
|
|
229
|
-
function composeCommand(action, args) {
|
|
245
|
+
function composeCommand(action, args, installedProfiles) {
|
|
230
246
|
const global = [];
|
|
231
247
|
const command = [];
|
|
248
|
+
const explicitProfiles = new Set();
|
|
232
249
|
for (let index = 0; index < args.length; index += 1) {
|
|
233
250
|
const value = args[index];
|
|
234
251
|
if (value === undefined)
|
|
@@ -238,16 +255,22 @@ function composeCommand(action, args) {
|
|
|
238
255
|
if (!profile)
|
|
239
256
|
throw new Error("--profile requires a value.");
|
|
240
257
|
global.push(value, profile);
|
|
258
|
+
explicitProfiles.add(profile);
|
|
241
259
|
index += 1;
|
|
242
260
|
}
|
|
243
261
|
else if (value.startsWith("--profile=")) {
|
|
244
262
|
global.push(value);
|
|
263
|
+
explicitProfiles.add(value.slice("--profile=".length));
|
|
245
264
|
}
|
|
246
265
|
else {
|
|
247
266
|
command.push(value);
|
|
248
267
|
}
|
|
249
268
|
}
|
|
250
|
-
const activeProfiles = action === "down"
|
|
269
|
+
const activeProfiles = action === "down"
|
|
270
|
+
? ["--profile", "*"]
|
|
271
|
+
: installedProfiles
|
|
272
|
+
.filter((profile) => !explicitProfiles.has(profile))
|
|
273
|
+
.flatMap((profile) => ["--profile", profile]);
|
|
251
274
|
return [...activeProfiles, ...global, action, ...command];
|
|
252
275
|
}
|
|
253
276
|
function stopGatewayWhenUnused(runner) {
|
|
@@ -258,6 +281,7 @@ function stopGatewayWhenUnused(runner) {
|
|
|
258
281
|
}
|
|
259
282
|
function runDevCommand(projectRoot, action, args, runner = defaultRunner) {
|
|
260
283
|
const runtime = resolveDevRuntime(projectRoot);
|
|
284
|
+
const profiles = installedDevProfiles(projectRoot);
|
|
261
285
|
writeRuntimeFiles(runtime);
|
|
262
286
|
if (action === "url") {
|
|
263
287
|
process.stdout.write(`http://${runtime.config.hostname}\n`);
|
|
@@ -274,7 +298,7 @@ function runDevCommand(projectRoot, action, args, runner = defaultRunner) {
|
|
|
274
298
|
process.stdout.write(`Development URL: http://${runtime.config.hostname}\n`);
|
|
275
299
|
if (runtime.config.publicUrl)
|
|
276
300
|
process.stdout.write(`Public OAuth URL: ${runtime.config.publicUrl}\n`);
|
|
277
|
-
runCompose(runtime, composeCommand("watch", args), runner);
|
|
301
|
+
runCompose(runtime, composeCommand("watch", args, profiles), runner);
|
|
278
302
|
}
|
|
279
303
|
catch (error) {
|
|
280
304
|
unregisterRoute(runtime);
|
|
@@ -284,13 +308,13 @@ function runDevCommand(projectRoot, action, args, runner = defaultRunner) {
|
|
|
284
308
|
return;
|
|
285
309
|
}
|
|
286
310
|
if (action === "down") {
|
|
287
|
-
runCompose(runtime, composeCommand("down", args), runner);
|
|
311
|
+
runCompose(runtime, composeCommand("down", args, profiles), runner);
|
|
288
312
|
unregisterRoute(runtime);
|
|
289
313
|
stopGatewayWhenUnused(runner);
|
|
290
314
|
return;
|
|
291
315
|
}
|
|
292
316
|
if (action === "exec" || action === "logs" || action === "ps") {
|
|
293
|
-
runCompose(runtime, composeCommand(action, args), runner);
|
|
317
|
+
runCompose(runtime, composeCommand(action, args, profiles), runner);
|
|
294
318
|
return;
|
|
295
319
|
}
|
|
296
320
|
throw new Error("Usage: podo dev <watch|exec|logs|ps|down|url> [docker compose options]");
|
package/dist/index.d.ts
CHANGED
|
@@ -16,6 +16,14 @@ interface ParsedArgs {
|
|
|
16
16
|
positionals: string[];
|
|
17
17
|
localeName?: string;
|
|
18
18
|
localeDirection?: LocaleDirection;
|
|
19
|
+
profile?: string;
|
|
20
|
+
release?: string;
|
|
21
|
+
confirm?: string;
|
|
22
|
+
revision?: number;
|
|
23
|
+
context?: string;
|
|
24
|
+
clusterFingerprint?: string;
|
|
25
|
+
host?: string;
|
|
26
|
+
json: boolean;
|
|
19
27
|
}
|
|
20
28
|
export declare function parseArgs(argv: string[]): ParsedArgs;
|
|
21
29
|
export {};
|
package/dist/index.js
CHANGED
|
@@ -14,6 +14,9 @@ const update_1 = require("./update");
|
|
|
14
14
|
const eject_1 = require("./eject");
|
|
15
15
|
const dev_1 = require("./dev");
|
|
16
16
|
const locale_1 = require("./locale");
|
|
17
|
+
const deploy_1 = require("./deploy");
|
|
18
|
+
const deploy_profile_1 = require("./deploy-profile");
|
|
19
|
+
const deploy_render_1 = require("./deploy-render");
|
|
17
20
|
const HELP = `podo — PodoKit project generator
|
|
18
21
|
|
|
19
22
|
Usage:
|
|
@@ -24,6 +27,7 @@ Usage:
|
|
|
24
27
|
podo diff List PodoKit-managed files you have edited
|
|
25
28
|
podo doctor Check framework versions against supported ranges
|
|
26
29
|
podo dev <action> [...] Run the shared, portless container development gateway
|
|
30
|
+
podo deploy <action> Plan, apply, verify, or roll back a Kubernetes deployment
|
|
27
31
|
podo locale <command> Add, validate, activate, or list JSON locales
|
|
28
32
|
podo update [--apply] Preview (or apply) what a version update would change
|
|
29
33
|
podo eject <path...> Take ownership of managed files (update skips them)
|
|
@@ -34,6 +38,14 @@ Options:
|
|
|
34
38
|
--pm <name> Package manager: npm | pnpm | yarn (default: npm)
|
|
35
39
|
--name <label> Display name for a locale
|
|
36
40
|
--direction <direction> Text direction: ltr | rtl (default: ltr)
|
|
41
|
+
--profile <name> Deployment profile name
|
|
42
|
+
--release <tag> Immutable shared API/web release tag
|
|
43
|
+
--confirm <plan-hash> Exact deployment or rollback plan hash
|
|
44
|
+
--revision <number> Helm revision for rollback
|
|
45
|
+
--context <name> Explicit Kubernetes context for profile initialization
|
|
46
|
+
--cluster-fingerprint <sha256:...> Expected cluster fingerprint (auto-detected if omitted)
|
|
47
|
+
--host <hostname> Public hostname for profile initialization
|
|
48
|
+
--json Emit machine-readable JSON when supported
|
|
37
49
|
--adopt Adopt colliding paths explicitly declared managed by a module
|
|
38
50
|
--no-ai Skip AI agent guidance (AGENTS.md, CLAUDE.md, editor rules)
|
|
39
51
|
-y, --yes Skip prompts and accept defaults
|
|
@@ -47,6 +59,22 @@ Example:
|
|
|
47
59
|
npx @podosoft/podokit create my-app --template todo
|
|
48
60
|
cd my-app && npx @podosoft/podokit add auth
|
|
49
61
|
`;
|
|
62
|
+
const DEPLOY_HELP = `podo deploy — Kubernetes and Helm deployment workflow
|
|
63
|
+
|
|
64
|
+
Usage:
|
|
65
|
+
podo deploy init --profile <name> --context <context> [--host <hostname>]
|
|
66
|
+
podo deploy doctor --profile <name> [--json]
|
|
67
|
+
podo deploy render --profile <name> --release <tag> [--json]
|
|
68
|
+
podo deploy plan --profile <name> --release <tag> [--json]
|
|
69
|
+
podo deploy apply --profile <name> --release <tag> --confirm <plan-hash>
|
|
70
|
+
podo deploy status --profile <name> [--json]
|
|
71
|
+
podo deploy verify --profile <name> [--json]
|
|
72
|
+
podo deploy rollback --profile <name> --revision <number> [--confirm <plan-hash>]
|
|
73
|
+
|
|
74
|
+
The namespace, referenced Secrets, ingress controller, and storage classes must
|
|
75
|
+
already exist. Plan and rollback require access to the profile's exact cluster.
|
|
76
|
+
Apply and rollback require confirmation of the exact plan hash.
|
|
77
|
+
`;
|
|
50
78
|
function parseArgs(argv) {
|
|
51
79
|
const parsed = {
|
|
52
80
|
help: false,
|
|
@@ -54,6 +82,7 @@ function parseArgs(argv) {
|
|
|
54
82
|
apply: false,
|
|
55
83
|
adopt: false,
|
|
56
84
|
ai: true,
|
|
85
|
+
json: false,
|
|
57
86
|
positionals: [],
|
|
58
87
|
};
|
|
59
88
|
const positionals = [];
|
|
@@ -96,6 +125,34 @@ function parseArgs(argv) {
|
|
|
96
125
|
}
|
|
97
126
|
parsed.localeDirection = direction;
|
|
98
127
|
}
|
|
128
|
+
else if (arg === "--profile") {
|
|
129
|
+
parsed.profile = argv[++i];
|
|
130
|
+
}
|
|
131
|
+
else if (arg === "--release") {
|
|
132
|
+
parsed.release = argv[++i];
|
|
133
|
+
}
|
|
134
|
+
else if (arg === "--confirm") {
|
|
135
|
+
parsed.confirm = argv[++i];
|
|
136
|
+
}
|
|
137
|
+
else if (arg === "--revision") {
|
|
138
|
+
const revision = Number(argv[++i]);
|
|
139
|
+
if (!Number.isInteger(revision) || revision < 1) {
|
|
140
|
+
throw new Error("--revision must be a positive integer.");
|
|
141
|
+
}
|
|
142
|
+
parsed.revision = revision;
|
|
143
|
+
}
|
|
144
|
+
else if (arg === "--context") {
|
|
145
|
+
parsed.context = argv[++i];
|
|
146
|
+
}
|
|
147
|
+
else if (arg === "--cluster-fingerprint") {
|
|
148
|
+
parsed.clusterFingerprint = argv[++i];
|
|
149
|
+
}
|
|
150
|
+
else if (arg === "--host") {
|
|
151
|
+
parsed.host = argv[++i];
|
|
152
|
+
}
|
|
153
|
+
else if (arg === "--json") {
|
|
154
|
+
parsed.json = true;
|
|
155
|
+
}
|
|
99
156
|
else if (arg !== undefined && !arg.startsWith("-")) {
|
|
100
157
|
positionals.push(arg);
|
|
101
158
|
}
|
|
@@ -112,7 +169,7 @@ function fail(message) {
|
|
|
112
169
|
async function main(argv) {
|
|
113
170
|
const args = parseArgs(argv);
|
|
114
171
|
if (args.help || !args.command) {
|
|
115
|
-
process.stdout.write(HELP);
|
|
172
|
+
process.stdout.write(args.help && args.command === "deploy" ? DEPLOY_HELP : HELP);
|
|
116
173
|
return;
|
|
117
174
|
}
|
|
118
175
|
const modulesDir = (0, node_path_1.join)(__dirname, "templates", "modules");
|
|
@@ -236,6 +293,95 @@ async function main(argv) {
|
|
|
236
293
|
}
|
|
237
294
|
return;
|
|
238
295
|
}
|
|
296
|
+
if (args.command === "deploy") {
|
|
297
|
+
const action = args.positionals[1] ?? "status";
|
|
298
|
+
const profileName = args.profile;
|
|
299
|
+
if (!profileName) {
|
|
300
|
+
fail("Deployment commands require --profile <name>.");
|
|
301
|
+
}
|
|
302
|
+
try {
|
|
303
|
+
if (action === "init") {
|
|
304
|
+
if (!args.context) {
|
|
305
|
+
fail("Usage: podo deploy init --profile <name> --context <context> [--host <hostname>].");
|
|
306
|
+
}
|
|
307
|
+
const clusterFingerprint = args.clusterFingerprint ?? (0, deploy_1.inspectClusterFingerprint)(args.context);
|
|
308
|
+
const initialized = (0, deploy_profile_1.initializeDeploymentProfile)(process.cwd(), profileName, {
|
|
309
|
+
context: args.context,
|
|
310
|
+
clusterFingerprint,
|
|
311
|
+
host: args.host,
|
|
312
|
+
});
|
|
313
|
+
process.stdout.write(args.json
|
|
314
|
+
? `${JSON.stringify(initialized, null, 2)}\n`
|
|
315
|
+
: `Created deployment profile ${initialized.name} at ${initialized.path}.\n`);
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
if (action === "doctor") {
|
|
319
|
+
const findings = (0, deploy_1.doctorDeployment)(process.cwd(), profileName);
|
|
320
|
+
process.stdout.write(args.json
|
|
321
|
+
? `${JSON.stringify(findings, null, 2)}\n`
|
|
322
|
+
: `${findings.map((finding) => `${finding.ok ? "ok " : "FAIL"} ${finding.message}`).join("\n")}\n`);
|
|
323
|
+
if (findings.some((finding) => !finding.ok))
|
|
324
|
+
process.exitCode = 1;
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
if (action === "render") {
|
|
328
|
+
if (!args.release)
|
|
329
|
+
fail("podo deploy render requires --release <tag>.");
|
|
330
|
+
const profile = (0, deploy_profile_1.loadDeploymentProfile)(process.cwd(), profileName);
|
|
331
|
+
const plan = (0, deploy_1.planDeployment)(process.cwd(), profileName, args.release);
|
|
332
|
+
const runtime = (0, deploy_render_1.renderDeployment)(process.cwd(), profileName, profile, args.release, plan.images, plan.rolloutStateDigest);
|
|
333
|
+
process.stdout.write(args.json
|
|
334
|
+
? `${JSON.stringify({ plan, runtime }, null, 2)}\n`
|
|
335
|
+
: `Rendered deployment into ${runtime.root}.\nPlan hash: ${plan.planHash}\n`);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
if (action === "plan") {
|
|
339
|
+
if (!args.release)
|
|
340
|
+
fail("podo deploy plan requires --release <tag>.");
|
|
341
|
+
const plan = (0, deploy_1.planDeployment)(process.cwd(), profileName, args.release);
|
|
342
|
+
process.stdout.write(args.json
|
|
343
|
+
? `${JSON.stringify(plan, null, 2)}\n`
|
|
344
|
+
: `${plan.actions.map((entry) => `${entry.order}. ${entry.description}`).join("\n")}\nPlan hash: ${plan.planHash}\n`);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
if (action === "apply") {
|
|
348
|
+
if (!args.release || !args.confirm) {
|
|
349
|
+
fail("podo deploy apply requires --release <tag> --confirm <plan-hash>.");
|
|
350
|
+
}
|
|
351
|
+
const result = await (0, deploy_1.applyDeployment)(process.cwd(), profileName, args.release, args.confirm);
|
|
352
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
if (action === "status") {
|
|
356
|
+
const result = (0, deploy_1.getDeploymentStatus)(process.cwd(), profileName);
|
|
357
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
if (action === "verify") {
|
|
361
|
+
const result = await (0, deploy_1.verifyDeployment)(process.cwd(), profileName);
|
|
362
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
363
|
+
if (!result.ok)
|
|
364
|
+
process.exitCode = 1;
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
if (action === "rollback") {
|
|
368
|
+
if (!args.revision)
|
|
369
|
+
fail("podo deploy rollback requires --revision <number>.");
|
|
370
|
+
if (!args.confirm) {
|
|
371
|
+
const plan = (0, deploy_1.planRollback)(process.cwd(), profileName, args.revision);
|
|
372
|
+
process.stdout.write(`${JSON.stringify(plan, null, 2)}\nRe-run with --confirm ${plan.planHash}.\n`);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
const result = await (0, deploy_1.rollbackDeployment)(process.cwd(), profileName, args.revision, args.confirm);
|
|
376
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
fail(`Unknown deploy command "${action}". Use init, doctor, render, plan, apply, status, verify, or rollback.`);
|
|
380
|
+
}
|
|
381
|
+
catch (err) {
|
|
382
|
+
fail(err.message);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
239
385
|
if (args.command === "locale") {
|
|
240
386
|
const action = args.positionals[1] ?? "list";
|
|
241
387
|
const code = args.positionals[2];
|
package/dist/lib.d.ts
CHANGED
|
@@ -10,6 +10,12 @@ export { planUpdate, summarize } from "./update";
|
|
|
10
10
|
export type { UpdatePlan, FileChange } from "./update";
|
|
11
11
|
export { readDevConfig, renderRoute, renderRuntimeCompose, resolveDevRuntime, runDevCommand } from "./dev";
|
|
12
12
|
export type { CommandRunner, DevConfig, DevRuntime } from "./dev";
|
|
13
|
+
export { applyDeployment, doctorDeployment, getDeploymentStatus, inspectClusterFingerprint, planDeployment, planRollback, rollbackDeployment, verifyDeployment, } from "./deploy";
|
|
14
|
+
export type { DeploymentDoctorFinding, DeploymentPlan, DeploymentPlanAction, DeploymentStatus, VerificationResult, } from "./deploy";
|
|
15
|
+
export { assertReleaseTag, deploymentProfileDigest, initializeDeploymentProfile, listDeploymentProfiles, loadDeploymentProfile, } from "./deploy-profile";
|
|
16
|
+
export type { DependencyMode, DeployProfileV1, DeploymentProfileSummary, InitializeDeploymentProfileOptions, ObjectStorageDependencyProfile, RedisDependencyProfile, ResourceProfile, StatefulDependencyProfile, WorkloadProfile, } from "./deploy-profile";
|
|
17
|
+
export { renderDeployment } from "./deploy-render";
|
|
18
|
+
export type { DeploymentRuntime } from "./deploy-render";
|
|
13
19
|
/** Absolute path to the templates bundled in this installed package. */
|
|
14
20
|
export declare function builtinTemplatesDir(): string;
|
|
15
21
|
/** Absolute path to the modules bundled in this installed package. */
|
package/dist/lib.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.runDevCommand = exports.resolveDevRuntime = exports.renderRuntimeCompose = exports.renderRoute = exports.readDevConfig = exports.summarize = exports.planUpdate = exports.SUPPORTED_FRAMEWORKS = exports.doctor = exports.diff = exports.status = exports.addModule = exports.listModules = exports.isKnownTemplate = exports.TEMPLATE_NAMES = exports.DEFAULT_TEMPLATE = exports.TEMPLATES = exports.create = void 0;
|
|
3
|
+
exports.renderDeployment = exports.loadDeploymentProfile = exports.listDeploymentProfiles = exports.initializeDeploymentProfile = exports.deploymentProfileDigest = exports.assertReleaseTag = exports.verifyDeployment = exports.rollbackDeployment = exports.planRollback = exports.planDeployment = exports.inspectClusterFingerprint = exports.getDeploymentStatus = exports.doctorDeployment = exports.applyDeployment = exports.runDevCommand = exports.resolveDevRuntime = exports.renderRuntimeCompose = exports.renderRoute = exports.readDevConfig = exports.summarize = exports.planUpdate = exports.SUPPORTED_FRAMEWORKS = exports.doctor = exports.diff = exports.status = exports.addModule = exports.listModules = exports.isKnownTemplate = exports.TEMPLATE_NAMES = exports.DEFAULT_TEMPLATE = exports.TEMPLATES = exports.create = void 0;
|
|
4
4
|
exports.builtinTemplatesDir = builtinTemplatesDir;
|
|
5
5
|
exports.builtinModulesDir = builtinModulesDir;
|
|
6
6
|
// Library entry point: the reusable functions behind the `podo` CLI, for other
|
|
@@ -30,6 +30,23 @@ Object.defineProperty(exports, "renderRoute", { enumerable: true, get: function
|
|
|
30
30
|
Object.defineProperty(exports, "renderRuntimeCompose", { enumerable: true, get: function () { return dev_1.renderRuntimeCompose; } });
|
|
31
31
|
Object.defineProperty(exports, "resolveDevRuntime", { enumerable: true, get: function () { return dev_1.resolveDevRuntime; } });
|
|
32
32
|
Object.defineProperty(exports, "runDevCommand", { enumerable: true, get: function () { return dev_1.runDevCommand; } });
|
|
33
|
+
var deploy_1 = require("./deploy");
|
|
34
|
+
Object.defineProperty(exports, "applyDeployment", { enumerable: true, get: function () { return deploy_1.applyDeployment; } });
|
|
35
|
+
Object.defineProperty(exports, "doctorDeployment", { enumerable: true, get: function () { return deploy_1.doctorDeployment; } });
|
|
36
|
+
Object.defineProperty(exports, "getDeploymentStatus", { enumerable: true, get: function () { return deploy_1.getDeploymentStatus; } });
|
|
37
|
+
Object.defineProperty(exports, "inspectClusterFingerprint", { enumerable: true, get: function () { return deploy_1.inspectClusterFingerprint; } });
|
|
38
|
+
Object.defineProperty(exports, "planDeployment", { enumerable: true, get: function () { return deploy_1.planDeployment; } });
|
|
39
|
+
Object.defineProperty(exports, "planRollback", { enumerable: true, get: function () { return deploy_1.planRollback; } });
|
|
40
|
+
Object.defineProperty(exports, "rollbackDeployment", { enumerable: true, get: function () { return deploy_1.rollbackDeployment; } });
|
|
41
|
+
Object.defineProperty(exports, "verifyDeployment", { enumerable: true, get: function () { return deploy_1.verifyDeployment; } });
|
|
42
|
+
var deploy_profile_1 = require("./deploy-profile");
|
|
43
|
+
Object.defineProperty(exports, "assertReleaseTag", { enumerable: true, get: function () { return deploy_profile_1.assertReleaseTag; } });
|
|
44
|
+
Object.defineProperty(exports, "deploymentProfileDigest", { enumerable: true, get: function () { return deploy_profile_1.deploymentProfileDigest; } });
|
|
45
|
+
Object.defineProperty(exports, "initializeDeploymentProfile", { enumerable: true, get: function () { return deploy_profile_1.initializeDeploymentProfile; } });
|
|
46
|
+
Object.defineProperty(exports, "listDeploymentProfiles", { enumerable: true, get: function () { return deploy_profile_1.listDeploymentProfiles; } });
|
|
47
|
+
Object.defineProperty(exports, "loadDeploymentProfile", { enumerable: true, get: function () { return deploy_profile_1.loadDeploymentProfile; } });
|
|
48
|
+
var deploy_render_1 = require("./deploy-render");
|
|
49
|
+
Object.defineProperty(exports, "renderDeployment", { enumerable: true, get: function () { return deploy_render_1.renderDeployment; } });
|
|
33
50
|
/** Absolute path to the templates bundled in this installed package. */
|
|
34
51
|
function builtinTemplatesDir() {
|
|
35
52
|
return (0, node_path_1.join)(__dirname, "templates");
|
package/dist/lockfile.js
CHANGED
|
@@ -53,6 +53,7 @@ exports.DEFAULT_OWNED_GLOBS = [
|
|
|
53
53
|
// AI agent guidance — yours to customize; update never touches it.
|
|
54
54
|
"AGENTS.md",
|
|
55
55
|
"CLAUDE.md",
|
|
56
|
+
".agents/**",
|
|
56
57
|
".claude/**",
|
|
57
58
|
".cursor/**",
|
|
58
59
|
".github/copilot-instructions.md",
|
|
@@ -66,6 +67,8 @@ exports.DEFAULT_OWNED_GLOBS = [
|
|
|
66
67
|
".env.docker",
|
|
67
68
|
"infra/traefik/**",
|
|
68
69
|
".podokit/dev.json",
|
|
70
|
+
// Optional application-owned Playwright device/browser projects.
|
|
71
|
+
"tests/playwright.projects.cjs",
|
|
69
72
|
];
|
|
70
73
|
const TEXT_EXT = /\.(json|md|ts|tsx|js|mjs|cjs|css|html|yml|yaml|txt|env|svelte|gitignore|example)$/i;
|
|
71
74
|
function podokitDir(projectRoot) {
|
package/dist/remove.js
CHANGED
|
@@ -110,6 +110,13 @@ function removeModule(options) {
|
|
|
110
110
|
const sharedManagedOverrides = new Set(others.flatMap((other) => other.manifest.managedOverrides ?? []));
|
|
111
111
|
const targetManagedOverrides = new Set(targetManifest.managedOverrides ?? []);
|
|
112
112
|
manifest.managedOverrides = (manifest.managedOverrides ?? []).filter((glob) => !targetManagedOverrides.has(glob) || sharedManagedOverrides.has(glob));
|
|
113
|
+
const defaultOwnedGlobs = new Set(lockfile_1.DEFAULT_OWNED_GLOBS);
|
|
114
|
+
const sharedOwnedGlobs = new Set(others.flatMap((other) => other.manifest.ownedGlobs ?? []));
|
|
115
|
+
const targetOwnedGlobs = new Set(targetManifest.ownedGlobs ?? []);
|
|
116
|
+
manifest.ownedGlobs = manifest.ownedGlobs.filter((glob) => !targetOwnedGlobs.has(glob) ||
|
|
117
|
+
defaultOwnedGlobs.has(glob) ||
|
|
118
|
+
sharedOwnedGlobs.has(glob) ||
|
|
119
|
+
keptEdited.some((path) => (0, lockfile_1.matchGlob)(path, glob)));
|
|
113
120
|
(0, lockfile_1.writeManifest)(projectRoot, manifest);
|
|
114
121
|
(0, lockfile_1.writeFilesLock)(projectRoot, (0, lockfile_1.computeFilesLock)(projectRoot, manifest.ownedGlobs, manifest.managedOverrides));
|
|
115
122
|
return { module, removed, keptEdited, keptShared, unwired };
|
|
@@ -69,6 +69,7 @@ Useful commands:
|
|
|
69
69
|
- `podo remove <module>` — un-apply a module (inverse of add; refuses if another module needs it, keeps files you edited).
|
|
70
70
|
- `podo status` / `podo diff` — see your local edits vs. what PodoKit generated.
|
|
71
71
|
- `podo update` — pull in template/module improvements (3-way merges your edits; never touches owned files).
|
|
72
|
+
- `podo deploy` — plan, apply, verify, or roll back an immutable Kubernetes release. Use `.agents/skills/podokit-deploy`.
|
|
72
73
|
|
|
73
74
|
## Commit conventions
|
|
74
75
|
|
|
@@ -33,16 +33,17 @@ npx @podosoft/podokit dev exec api npm run migration:run -w {{projectName}}-api
|
|
|
33
33
|
npx @podosoft/podokit dev down
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
`dev watch` reads `.podokit/manifest.json` and automatically activates `cache`,
|
|
37
|
+
`storage`, and `queue` when installed modules require Redis, MinIO, or a worker.
|
|
38
|
+
You can still activate an additional Compose profile explicitly:
|
|
38
39
|
|
|
39
40
|
```bash
|
|
40
|
-
npx @podosoft/podokit dev watch
|
|
41
|
-
--profile cache --profile storage --profile queue
|
|
41
|
+
npx @podosoft/podokit dev watch --profile dev
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
Explicit profile flags are preserved for other lifecycle commands. `dev down`
|
|
45
|
+
activates all profiles while removing this project's stack and route. If it is
|
|
46
|
+
the final registered
|
|
46
47
|
project, it also removes the shared gateway and network. Source changes are synced
|
|
47
48
|
through Compose Watch, including Vite HMR on the same portless browser origin.
|
|
48
49
|
|
|
@@ -79,8 +80,18 @@ stable HTTPS tunnel, see the PodoKit [development guide](https://github.com/podo
|
|
|
79
80
|
|
|
80
81
|
## Deploy
|
|
81
82
|
|
|
82
|
-
Docker Compose in `infra/docker`;
|
|
83
|
-
|
|
83
|
+
Docker Compose lives in `infra/docker`; the basic k3s manifests in `infra/k3s`
|
|
84
|
+
route every public path through the web proxy. For a managed release on an
|
|
85
|
+
existing cluster, initialize an application-owned profile:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
npx @podosoft/podokit deploy init --profile production --context production --host app.example.com
|
|
89
|
+
npx @podosoft/podokit deploy doctor --profile production
|
|
90
|
+
npx @podosoft/podokit deploy plan --profile production --release v1.2.3
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Use existing Kubernetes Secrets and never commit their values. See the PodoKit
|
|
94
|
+
[deployment guide](https://github.com/podosoft-dev/podokit/blob/main/docs/deployment.md).
|
|
84
95
|
|
|
85
96
|
Build production images from the workspace root so Docker can use the committed
|
|
86
97
|
lockfile and all workspace package manifests:
|
|
@@ -16,12 +16,12 @@ RUN npm run build --workspace=apps/api
|
|
|
16
16
|
FROM node:22-alpine AS runtime
|
|
17
17
|
WORKDIR /app
|
|
18
18
|
ENV NODE_ENV=production
|
|
19
|
-
RUN addgroup -S app && adduser -S
|
|
19
|
+
RUN addgroup -S -g 101 app && adduser -S -D -H -u 100 -G app app
|
|
20
20
|
COPY --from=build /app/node_modules ./node_modules
|
|
21
21
|
COPY --from=deps /app/apps/api ./apps/api
|
|
22
22
|
COPY --from=build /app/apps/api/dist ./apps/api/dist
|
|
23
23
|
COPY --from=build /app/apps/api/scripts ./apps/api/scripts
|
|
24
24
|
WORKDIR /app/apps/api
|
|
25
|
-
USER
|
|
25
|
+
USER 100:101
|
|
26
26
|
EXPOSE 3000
|
|
27
27
|
CMD ["node", "dist/main"]
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"start": "node dist/main",
|
|
9
9
|
"lint": "tsc -p tsconfig.json --noEmit",
|
|
10
10
|
"test": "jest --passWithNoTests",
|
|
11
|
+
"migrate:all": "node dist/database/migrate",
|
|
11
12
|
"typeorm": "typeorm-ts-node-commonjs -d src/database/data-source.ts",
|
|
12
13
|
"migration:run": "npm run typeorm -- migration:run",
|
|
13
14
|
"migration:revert": "npm run typeorm -- migration:revert",
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, expect, it } from "@jest/globals";
|
|
2
|
+
import { redisConnectionOptions } from "./redis.connection";
|
|
3
|
+
|
|
4
|
+
describe("redisConnectionOptions", () => {
|
|
5
|
+
it("prefers an authenticated URL over individual settings", () => {
|
|
6
|
+
expect(
|
|
7
|
+
redisConnectionOptions({
|
|
8
|
+
REDIS_URL: "rediss://user:p%40ss@cache.example.com:6381/4",
|
|
9
|
+
REDIS_HOST: "ignored",
|
|
10
|
+
REDIS_PASSWORD: "ignored",
|
|
11
|
+
}),
|
|
12
|
+
).toEqual({
|
|
13
|
+
host: "cache.example.com",
|
|
14
|
+
port: 6381,
|
|
15
|
+
db: 4,
|
|
16
|
+
username: "user",
|
|
17
|
+
password: "p@ss",
|
|
18
|
+
tls: {},
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("uses authenticated host settings when a URL is absent", () => {
|
|
23
|
+
expect(
|
|
24
|
+
redisConnectionOptions({
|
|
25
|
+
REDIS_HOST: "cache",
|
|
26
|
+
REDIS_PORT: "6380",
|
|
27
|
+
REDIS_DB: "2",
|
|
28
|
+
REDIS_USERNAME: "app",
|
|
29
|
+
REDIS_PASSWORD: "secret",
|
|
30
|
+
REDIS_TLS: "true",
|
|
31
|
+
}),
|
|
32
|
+
).toEqual({
|
|
33
|
+
host: "cache",
|
|
34
|
+
port: 6380,
|
|
35
|
+
db: 2,
|
|
36
|
+
username: "app",
|
|
37
|
+
password: "secret",
|
|
38
|
+
tls: {},
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("does not include a malformed URL value in its error", () => {
|
|
43
|
+
const secretUrl = "not-a-url-with-a-secret";
|
|
44
|
+
expect(() => redisConnectionOptions({ REDIS_URL: secretUrl })).toThrow(
|
|
45
|
+
"REDIS_URL must be a valid redis:// or rediss:// URL",
|
|
46
|
+
);
|
|
47
|
+
try {
|
|
48
|
+
redisConnectionOptions({ REDIS_URL: secretUrl });
|
|
49
|
+
} catch (error) {
|
|
50
|
+
expect(String(error)).not.toContain(secretUrl);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export interface RedisConnectionOptions {
|
|
2
|
+
host: string;
|
|
3
|
+
port: number;
|
|
4
|
+
db: number;
|
|
5
|
+
username?: string;
|
|
6
|
+
password?: string;
|
|
7
|
+
tls?: Record<string, never>;
|
|
8
|
+
lazyConnect?: boolean;
|
|
9
|
+
enableReadyCheck?: boolean;
|
|
10
|
+
enableOfflineQueue?: boolean;
|
|
11
|
+
maxRetriesPerRequest?: number | null;
|
|
12
|
+
connectTimeout?: number;
|
|
13
|
+
commandTimeout?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function integer(name: string, value: string | undefined, fallback: number): number {
|
|
17
|
+
if (value === undefined || value === "") return fallback;
|
|
18
|
+
const parsed = Number(value);
|
|
19
|
+
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
20
|
+
throw new Error(`${name} must be a non-negative integer`);
|
|
21
|
+
}
|
|
22
|
+
return parsed;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function decoded(value: string, name: string): string {
|
|
26
|
+
try {
|
|
27
|
+
return decodeURIComponent(value);
|
|
28
|
+
} catch {
|
|
29
|
+
throw new Error(`${name} contains invalid percent encoding`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function fromUrl(value: string): RedisConnectionOptions {
|
|
34
|
+
let url: URL;
|
|
35
|
+
try {
|
|
36
|
+
url = new URL(value);
|
|
37
|
+
} catch {
|
|
38
|
+
throw new Error("REDIS_URL must be a valid redis:// or rediss:// URL");
|
|
39
|
+
}
|
|
40
|
+
if (url.protocol !== "redis:" && url.protocol !== "rediss:") {
|
|
41
|
+
throw new Error("REDIS_URL must use the redis:// or rediss:// protocol");
|
|
42
|
+
}
|
|
43
|
+
const dbPath = url.pathname.replace(/^\/+/, "");
|
|
44
|
+
return {
|
|
45
|
+
host: url.hostname || "localhost",
|
|
46
|
+
port: integer("REDIS_URL port", url.port, 6379),
|
|
47
|
+
db: integer("REDIS_URL database", dbPath, 0),
|
|
48
|
+
...(url.username ? { username: decoded(url.username, "REDIS_URL username") } : {}),
|
|
49
|
+
...(url.password ? { password: decoded(url.password, "REDIS_URL password") } : {}),
|
|
50
|
+
...(url.protocol === "rediss:" ? { tls: {} } : {}),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function redisConnectionOptions(
|
|
55
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
56
|
+
overrides: Partial<RedisConnectionOptions> = {},
|
|
57
|
+
): RedisConnectionOptions {
|
|
58
|
+
const base = env.REDIS_URL
|
|
59
|
+
? fromUrl(env.REDIS_URL)
|
|
60
|
+
: {
|
|
61
|
+
host: env.REDIS_HOST ?? "localhost",
|
|
62
|
+
port: integer("REDIS_PORT", env.REDIS_PORT, 6379),
|
|
63
|
+
db: integer("REDIS_DB", env.REDIS_DB, 0),
|
|
64
|
+
...(env.REDIS_USERNAME ? { username: env.REDIS_USERNAME } : {}),
|
|
65
|
+
...(env.REDIS_PASSWORD ? { password: env.REDIS_PASSWORD } : {}),
|
|
66
|
+
...(env.REDIS_TLS === "true" ? { tls: {} } : {}),
|
|
67
|
+
};
|
|
68
|
+
return { ...base, ...overrides };
|
|
69
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import dataSource from "./data-source";
|
|
2
|
+
|
|
3
|
+
async function migrate(): Promise<void> {
|
|
4
|
+
await dataSource.initialize();
|
|
5
|
+
try {
|
|
6
|
+
await dataSource.runMigrations({ transaction: "all" });
|
|
7
|
+
} finally {
|
|
8
|
+
await dataSource.destroy();
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
void migrate().catch((error: unknown) => {
|
|
13
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
14
|
+
process.stderr.write(`Migration failed: ${message}\n`);
|
|
15
|
+
process.exitCode = 1;
|
|
16
|
+
});
|