@podosoft/podokit 1.0.2 → 1.1.4
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 +74 -6
- package/dist/add.d.ts +14 -1
- package/dist/add.js +42 -5
- package/dist/create.d.ts +4 -0
- package/dist/create.js +9 -0
- package/dist/deploy-compose-profile.d.ts +3 -0
- package/dist/deploy-compose-profile.js +74 -8
- package/dist/deploy-compose-render.d.ts +1 -0
- package/dist/deploy-compose-render.js +46 -1
- package/dist/deploy-compose.js +2 -1
- package/dist/deploy-profile.js +18 -8
- package/dist/deploy.js +157 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +41 -0
- package/dist/lib.d.ts +2 -0
- package/dist/lib.js +8 -2
- package/dist/lockfile.d.ts +6 -1
- package/dist/lockfile.js +22 -4
- package/dist/provider.d.ts +24 -0
- package/dist/provider.js +142 -0
- package/dist/remove.js +6 -3
- package/dist/templates/base/AGENTS.md +9 -3
- package/dist/templates/base/README.md +12 -8
- package/dist/templates/fullstack/AGENTS.md +24 -5
- package/dist/templates/fullstack/README.md +75 -11
- package/dist/templates/fullstack/apps/api/package.json +4 -3
- package/dist/templates/fullstack/apps/api/src/config/env.validation.ts +1 -0
- package/dist/templates/fullstack/apps/api/src/config/providers.ts +9 -0
- package/dist/templates/fullstack/apps/api/src/core/services.ts +7 -2
- package/dist/templates/fullstack/apps/api/src/database/data-source.ts +30 -8
- package/dist/templates/fullstack/apps/api/src/database/database.spec.ts +58 -0
- package/dist/templates/fullstack/apps/api/src/database/database.ts +58 -2
- package/dist/templates/fullstack/apps/api/src/database/migrate.ts +24 -2
- package/dist/templates/fullstack/apps/api/src/database/sqlite-migrator.spec.ts +50 -0
- package/dist/templates/fullstack/apps/api/src/database/sqlite-migrator.ts +134 -0
- package/dist/templates/fullstack/compose.dev.yaml +2 -2
- package/dist/templates/fullstack/dot-agents/skills/podokit-deploy/references/profile.md +10 -3
- package/dist/templates/fullstack/dot-env.example +2 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin.module.ts +2 -2
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/profile-image/profile-image.service.spec.ts +10 -4
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/profile-image/profile-image.service.ts +10 -4
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/site-settings/site-settings.service.ts +10 -4
- package/dist/templates/modules/admin-dashboard/module.manifest.json +3 -5
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit-enabled.ts +2 -5
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.migration.spec.ts +33 -0
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.service.spec.ts +13 -0
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.service.ts +17 -8
- package/dist/templates/modules/audit-log/files/apps/api/src/migrations/1720300000000-InitAuditLogs.ts +5 -4
- package/dist/templates/modules/audit-log/module.manifest.json +1 -1
- package/dist/templates/modules/auth/files/apps/api/src/auth/account-issuer-migration.spec.ts +88 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/account-issuer-migration.ts +123 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth-config-store.ts +2 -2
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.module.ts +4 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.service.ts +2 -2
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.ts +4 -4
- package/dist/templates/modules/auth/files/apps/api/src/auth/db.ts +55 -10
- package/dist/templates/modules/auth/files/apps/api/src/auth/feature-gate.ts +10 -14
- package/dist/templates/modules/auth/files/apps/api/src/migrate.ts +24 -6
- package/dist/templates/modules/auth/files/apps/api/src/migrations/1720400000000-InitAppSettings.ts +2 -1
- package/dist/templates/modules/auth/files/apps/api/src/migrations/1720500000000-InitAuthConfig.ts +4 -3
- package/dist/templates/modules/auth/files/apps/api/src/settings/app-settings.migration.spec.ts +15 -4
- package/dist/templates/modules/auth/files/apps/api/src/settings/auth-config.migration.spec.ts +30 -0
- package/dist/templates/modules/auth/module.manifest.json +6 -6
- package/dist/templates/modules/bullmq/files/apps/api/src/jobs/bullmq-job.queue.spec.ts +10 -0
- package/dist/templates/modules/bullmq/files/apps/api/src/jobs/bullmq-job.queue.ts +107 -0
- package/dist/templates/modules/bullmq/files/apps/api/src/jobs/jobs.module.ts +12 -11
- package/dist/templates/modules/bullmq/files/apps/api/src/main-worker.ts +5 -0
- package/dist/templates/modules/bullmq/module.manifest.json +7 -0
- package/dist/templates/modules/cache-memory/files/apps/api/src/cache-memory/cache-memory.module.spec.ts +16 -0
- package/dist/templates/modules/cache-memory/files/apps/api/src/cache-memory/cache-memory.module.ts +12 -0
- package/dist/templates/modules/cache-memory/module.manifest.json +30 -0
- package/dist/templates/modules/events-memory/files/apps/api/src/events-provider-memory/events-memory.module.ts +14 -0
- package/dist/templates/modules/events-memory/module.manifest.json +30 -0
- package/dist/templates/modules/events-redis/files/apps/api/src/events-provider-redis/events-redis.module.ts +16 -0
- package/dist/templates/modules/events-redis/files/apps/api/src/events-provider-redis/redis-event.bus.ts +75 -0
- package/dist/templates/modules/events-redis/module.manifest.json +32 -0
- package/dist/templates/modules/file-upload/files/apps/api/src/files/files.module.ts +9 -5
- package/dist/templates/modules/file-upload/files/tests/api/files.api.spec.ts +1 -1
- package/dist/templates/modules/file-upload/module.manifest.json +4 -3
- package/dist/templates/modules/job-progress/files/apps/api/src/progress/job-progress.module.ts +36 -14
- package/dist/templates/modules/job-progress/files/apps/api/src/progress/progress.bridge.spec.ts +9 -32
- package/dist/templates/modules/job-progress/files/apps/api/src/progress/progress.processor.ts +16 -11
- package/dist/templates/modules/job-progress/module.manifest.json +7 -8
- package/dist/templates/modules/jobs-local/files/apps/api/src/jobs-local/jobs-local.module.ts +42 -0
- package/dist/templates/modules/jobs-local/files/apps/api/src/jobs-local/local-job.queue.spec.ts +45 -0
- package/dist/templates/modules/jobs-local/files/apps/api/src/jobs-local/local-job.queue.ts +212 -0
- package/dist/templates/modules/jobs-local/module.manifest.json +34 -0
- package/dist/templates/modules/mailer/files/apps/api/src/mail/mailer.ts +7 -3
- package/dist/templates/modules/mailer/module.manifest.json +2 -4
- package/dist/templates/modules/object-storage-local/files/apps/api/src/storage-local/storage-local.module.spec.ts +25 -0
- package/dist/templates/modules/object-storage-local/files/apps/api/src/storage-local/storage-local.module.ts +60 -0
- package/dist/templates/modules/object-storage-local/module.manifest.json +34 -0
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/s3-object.store.ts +50 -0
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/storage.module.ts +8 -0
- package/dist/templates/modules/object-storage-s3/files/infra/docker/minio.compose.yml +4 -4
- package/dist/templates/modules/object-storage-s3/files/tests/api/storage.api.spec.ts +20 -12
- package/dist/templates/modules/object-storage-s3/module.manifest.json +4 -2
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.module.spec.ts +14 -1
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.module.ts +18 -37
- package/dist/templates/modules/rate-limit/files/tests/api/rate-limit.api.spec.ts +12 -0
- package/dist/templates/modules/rate-limit/module.manifest.json +8 -6
- package/dist/templates/modules/redis/files/apps/api/src/redis/redis-cache.store.ts +82 -0
- package/dist/templates/modules/redis/files/apps/api/src/redis/redis.module.ts +6 -0
- package/dist/templates/modules/redis/module.manifest.json +2 -0
- package/dist/templates/modules/sse/files/apps/api/src/events/events.module.ts +5 -6
- package/dist/templates/modules/sse/files/apps/api/src/events/events.service.spec.ts +4 -4
- package/dist/templates/modules/sse/files/apps/api/src/events/events.service.ts +6 -5
- package/dist/templates/modules/sse/module.manifest.json +4 -6
- package/dist/templates/todo/AGENTS.md +25 -6
- package/dist/templates/todo/README.md +72 -14
- package/dist/templates/todo/apps/api/src/migrations/1720100000000-InitTodos.ts +11 -0
- package/dist/templates/todo/apps/api/src/todos/todo.repository.spec.ts +76 -0
- package/dist/templates/todo/apps/api/src/todos/todo.repository.ts +9 -4
- package/dist/templates/todo/tests/api/todo.api.spec.ts +32 -0
- package/dist/templates/todo/tests/ui/todo.ui.spec.ts +31 -0
- package/package.json +2 -1
- package/dist/templates/modules/job-progress/files/apps/api/src/progress/progress.bridge.ts +0 -32
- package/dist/templates/modules/mailer/files/apps/api/src/mail/db.ts +0 -13
- package/dist/templates/modules/sse/files/apps/api/src/events/events.transport.ts +0 -111
package/README.md
CHANGED
|
@@ -1,17 +1,44 @@
|
|
|
1
1
|
# @podosoft/podokit
|
|
2
2
|
|
|
3
3
|
PodoKit is an opinionated Bun 1.4.0 full-stack starter and CLI. It generates an
|
|
4
|
-
Elysia API, a SvelteKit web application, PostgreSQL integration
|
|
5
|
-
`Bun.SQL`, Tailwind CSS v4, shadcn-svelte, Docker, and k3s assets.
|
|
4
|
+
Elysia API, a SvelteKit web application, selectable PostgreSQL/SQLite integration
|
|
5
|
+
through native `Bun.SQL`, Tailwind CSS v4, shadcn-svelte, Docker, and k3s assets.
|
|
6
6
|
|
|
7
7
|
The CLI itself remains Node-compatible, so either `npx` or `bunx` can create and
|
|
8
8
|
maintain projects. Generated PodoKit v1 applications are Bun-only.
|
|
9
9
|
|
|
10
|
+
## Choose infrastructure
|
|
11
|
+
|
|
12
|
+
`fullstack` is the default template. `todo` adds a working CRUD example; `base`
|
|
13
|
+
is a minimal workspace with placeholder scripts. For a small working app, use
|
|
14
|
+
`fullstack` or `todo` with local providers rather than choosing `base` just to
|
|
15
|
+
avoid external infrastructure.
|
|
16
|
+
|
|
17
|
+
| Capability | Default | Local alternative |
|
|
18
|
+
| --- | --- | --- |
|
|
19
|
+
| Database | `postgres` — PostgreSQL | `sqlite` — database file |
|
|
20
|
+
| Cache | `redis` | `memory` — in-process cache |
|
|
21
|
+
| Object storage | `s3` — AWS S3 or S3-compatible storage | `local` — local files |
|
|
22
|
+
| Events | `redis` | `memory` — in-process events |
|
|
23
|
+
| Jobs | `bullmq` — Redis queue and separate worker | `local` — database-backed embedded worker |
|
|
24
|
+
|
|
25
|
+
A simple program, prototype, desktop app, or single-process service can use
|
|
26
|
+
**SQLite, memory cache, and local files** without PostgreSQL, Redis, or S3.
|
|
27
|
+
Providers can be mixed. Local providers require one API process.
|
|
28
|
+
|
|
29
|
+
Only the database is selected during creation with `--database postgres|sqlite`.
|
|
30
|
+
Other capabilities use `provider set` after creation; there are no `--cache` or
|
|
31
|
+
`--storage` creation flags. Choosing SQLite alone leaves the other defaults
|
|
32
|
+
unchanged. Creation records selections, while applying a provider or adding a
|
|
33
|
+
feature installs the necessary optional implementation modules.
|
|
34
|
+
|
|
10
35
|
## Quick start
|
|
11
36
|
|
|
37
|
+
### Default server setup
|
|
38
|
+
|
|
12
39
|
```bash
|
|
13
40
|
npx @podosoft/podokit create my-app
|
|
14
|
-
# or: bunx @podosoft/podokit create my-app
|
|
41
|
+
# or: bunx --bun @podosoft/podokit create my-app
|
|
15
42
|
|
|
16
43
|
cd my-app
|
|
17
44
|
bun install
|
|
@@ -24,10 +51,30 @@ gateway on loopback port 80; later projects reuse it and route by hostname. Stop
|
|
|
24
51
|
the project with `podo dev down`.
|
|
25
52
|
|
|
26
53
|
The `todo` template generates a working SvelteKit UI, Elysia CRUD API, and
|
|
27
|
-
PostgreSQL persistence:
|
|
54
|
+
PostgreSQL or SQLite persistence:
|
|
28
55
|
|
|
29
56
|

|
|
30
57
|
|
|
58
|
+
### Small app with local providers
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
bunx --bun @podosoft/podokit create podokit --template todo --database sqlite --yes
|
|
62
|
+
cd podokit
|
|
63
|
+
bunx --bun @podosoft/podokit provider set cache memory --apply
|
|
64
|
+
bunx --bun @podosoft/podokit provider set object-storage local --apply
|
|
65
|
+
bunx --bun @podosoft/podokit provider set events memory --apply
|
|
66
|
+
bunx --bun @podosoft/podokit provider set jobs local --apply
|
|
67
|
+
bun install
|
|
68
|
+
cp .env.example .env
|
|
69
|
+
bun run --cwd apps/api migration:run
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Run `bun run --cwd apps/api dev` and `bun run --cwd apps/web dev` in separate
|
|
73
|
+
terminals and open `http://localhost:5001`. This host-process setup requires no
|
|
74
|
+
Docker. SQLite and local files default to `./data/podokit.sqlite` and
|
|
75
|
+
`./data/files`, relative to the API working directory. Memory state disappears
|
|
76
|
+
on restart; files and the database-backed local jobs persist.
|
|
77
|
+
|
|
31
78
|
## Commands
|
|
32
79
|
|
|
33
80
|
| Command | Purpose |
|
|
@@ -35,6 +82,8 @@ PostgreSQL persistence:
|
|
|
35
82
|
| `podo create <name>` | Create a Bun project from `fullstack`, `todo`, or `base`. |
|
|
36
83
|
| `podo add <module>` | Add a composable feature module. |
|
|
37
84
|
| `podo remove <module>` | Remove a module conservatively. |
|
|
85
|
+
| `podo provider list` | Show active database, cache, object storage, event, and job providers. |
|
|
86
|
+
| `podo provider set <capability> <provider> [--apply]` | Preview or apply one provider change. |
|
|
38
87
|
| `podo status` | Show the PodoKit version, modules, ownership tiers, and local edits. |
|
|
39
88
|
| `podo diff` | List edited or deleted managed files. |
|
|
40
89
|
| `podo doctor` | Check Bun and framework versions. |
|
|
@@ -42,7 +91,7 @@ PostgreSQL persistence:
|
|
|
42
91
|
| `podo update [--apply]` | Preview or apply managed template and module updates. |
|
|
43
92
|
| `podo eject <path...>` | Transfer managed files to application ownership. |
|
|
44
93
|
| `podo dev <action>` | Manage the generated development containers. |
|
|
45
|
-
| `podo deploy <action>` | Plan, apply, verify, or roll back an immutable
|
|
94
|
+
| `podo deploy <action>` | Plan, apply, verify, or roll back an immutable Kubernetes or Docker Compose release. |
|
|
46
95
|
|
|
47
96
|
```text
|
|
48
97
|
podo create <name> [options]
|
|
@@ -50,6 +99,7 @@ podo create <name> [options]
|
|
|
50
99
|
Options:
|
|
51
100
|
--template <name> fullstack (default), todo, or base
|
|
52
101
|
--dir <path> Target directory (default: ./<name>)
|
|
102
|
+
--database <name> postgres (default) or sqlite
|
|
53
103
|
--no-ai Omit generated AI-agent guidance
|
|
54
104
|
-y, --yes Accept defaults without prompts
|
|
55
105
|
-h, --help Show help
|
|
@@ -80,6 +130,24 @@ remain untouched.
|
|
|
80
130
|
See the [module guide](https://github.com/podosoft-dev/podokit/blob/main/docs/modules.md)
|
|
81
131
|
and [updating guide](https://github.com/podosoft-dev/podokit/blob/main/docs/updating.md).
|
|
82
132
|
|
|
133
|
+
## Runtime providers
|
|
134
|
+
|
|
135
|
+
Inspect active selections and available alternatives with `podo provider list`.
|
|
136
|
+
`podo provider set` is a dry run by default and installs the selected
|
|
137
|
+
implementation module on `--apply`. Configure providers before adding features:
|
|
138
|
+
`file-upload` uses local storage when selected, and `job-progress` can use local
|
|
139
|
+
jobs and memory events. Changes append settings to `.env.example`; merge them
|
|
140
|
+
into an existing `.env` manually. Switching changes code and configuration only;
|
|
141
|
+
it never migrates or deletes data.
|
|
142
|
+
|
|
143
|
+
S3's development settings use Silo with `STORAGE_PROVIDER=minio`; use
|
|
144
|
+
`STORAGE_PROVIDER=aws` for AWS S3. Local files use `LOCAL_STORAGE_PATH`.
|
|
145
|
+
|
|
146
|
+
See the [runtime provider guide](https://github.com/podosoft-dev/podokit/blob/main/docs/providers.md)
|
|
147
|
+
before switching an existing project.
|
|
148
|
+
For the complete creation sequence, see
|
|
149
|
+
[Getting Started](https://github.com/podosoft-dev/podokit/blob/main/docs/getting-started.md).
|
|
150
|
+
|
|
83
151
|
## Compatibility boundary
|
|
84
152
|
|
|
85
153
|
PodoKit v1 does not convert projects generated by PodoKit 0.x. Existing projects
|
|
@@ -99,7 +167,7 @@ my-app/
|
|
|
99
167
|
│ ├── api/ # Bun + Elysia + native Bun.SQL
|
|
100
168
|
│ └── web/ # SvelteKit + Tailwind CSS v4 + shadcn-svelte
|
|
101
169
|
├── infra/
|
|
102
|
-
│ ├── docker/ #
|
|
170
|
+
│ ├── docker/ # provider-aware external services
|
|
103
171
|
│ └── k3s/ # deployments, services, ingress, and examples
|
|
104
172
|
├── tests/ # Playwright API and browser tests
|
|
105
173
|
├── .podokit/ # assembly and managed-file metadata
|
package/dist/add.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type TemplateVars } from "@podosoft/podokit-template-engine";
|
|
2
|
+
import { type ProviderCapability } from "@podosoft/podokit-runtime";
|
|
2
3
|
interface Injection {
|
|
3
4
|
file: string;
|
|
4
5
|
marker: string;
|
|
@@ -8,7 +9,13 @@ interface Injection {
|
|
|
8
9
|
}
|
|
9
10
|
/** Highest `module.manifest.json` schema version this CLI understands. A package
|
|
10
11
|
* module declaring a higher version is rejected rather than mis-applied. */
|
|
11
|
-
export declare const SUPPORTED_MANIFEST_VERSION =
|
|
12
|
+
export declare const SUPPORTED_MANIFEST_VERSION = 3;
|
|
13
|
+
export interface ModuleRuntimeConstraints {
|
|
14
|
+
/** Maximum safe API replicas for process-local providers. */
|
|
15
|
+
maxApiReplicas?: number;
|
|
16
|
+
/** Whether jobs must execute inside the API process. */
|
|
17
|
+
embeddedWorker?: boolean;
|
|
18
|
+
}
|
|
12
19
|
export interface ModulePathMove {
|
|
13
20
|
/** Project-relative file or directory path from an older module layout. */
|
|
14
21
|
from: string;
|
|
@@ -39,6 +46,12 @@ export interface ModuleManifest {
|
|
|
39
46
|
name: string;
|
|
40
47
|
description: string;
|
|
41
48
|
requires?: string[];
|
|
49
|
+
/** Runtime capabilities that must have an implementation module installed. */
|
|
50
|
+
requiresCapabilities?: ProviderCapability[];
|
|
51
|
+
/** Provider implementations supplied by this module. */
|
|
52
|
+
provides?: Partial<Record<ProviderCapability, string>>;
|
|
53
|
+
/** Deployment restrictions introduced by this implementation. */
|
|
54
|
+
runtimeConstraints?: ModuleRuntimeConstraints;
|
|
42
55
|
targetApp: string;
|
|
43
56
|
dependencies?: Record<string, string>;
|
|
44
57
|
devDependencies?: Record<string, string>;
|
package/dist/add.js
CHANGED
|
@@ -11,11 +11,11 @@ const node_fs_1 = require("node:fs");
|
|
|
11
11
|
const node_path_1 = require("node:path");
|
|
12
12
|
const node_module_1 = require("node:module");
|
|
13
13
|
const podokit_template_engine_1 = require("@podosoft/podokit-template-engine");
|
|
14
|
+
const podokit_runtime_1 = require("@podosoft/podokit-runtime");
|
|
14
15
|
const lockfile_1 = require("./lockfile");
|
|
15
|
-
const toolchain_1 = require("./toolchain");
|
|
16
16
|
/** Highest `module.manifest.json` schema version this CLI understands. A package
|
|
17
17
|
* module declaring a higher version is rejected rather than mis-applied. */
|
|
18
|
-
exports.SUPPORTED_MANIFEST_VERSION =
|
|
18
|
+
exports.SUPPORTED_MANIFEST_VERSION = 3;
|
|
19
19
|
/** Return the legacy targetApp package fields plus any additional app overlays. */
|
|
20
20
|
function modulePackageOverlays(manifest) {
|
|
21
21
|
const overlays = new Map();
|
|
@@ -88,6 +88,43 @@ function assertManifestVersion(name, manifest) {
|
|
|
88
88
|
if ((manifest.manifestVersion ?? exports.SUPPORTED_MANIFEST_VERSION) > exports.SUPPORTED_MANIFEST_VERSION) {
|
|
89
89
|
throw new Error(`Module "${name}" needs a newer PodoKit (manifest v${manifest.manifestVersion}, this CLI supports v${exports.SUPPORTED_MANIFEST_VERSION}). Update @podosoft/podokit.`);
|
|
90
90
|
}
|
|
91
|
+
for (const capability of manifest.requiresCapabilities ?? []) {
|
|
92
|
+
if (!(0, podokit_runtime_1.isProviderCapability)(capability)) {
|
|
93
|
+
throw new Error(`Module "${name}" requires unknown capability "${capability}".`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
for (const [capability, provider] of Object.entries(manifest.provides ?? {})) {
|
|
97
|
+
if (!(0, podokit_runtime_1.isProviderCapability)(capability)
|
|
98
|
+
|| typeof provider !== "string"
|
|
99
|
+
|| !(0, podokit_runtime_1.isProviderName)(capability, provider)) {
|
|
100
|
+
throw new Error(`Module "${name}" declares invalid provider ${capability}=${String(provider)}.`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const maxApiReplicas = manifest.runtimeConstraints?.maxApiReplicas;
|
|
104
|
+
if (maxApiReplicas !== undefined
|
|
105
|
+
&& (!Number.isSafeInteger(maxApiReplicas) || maxApiReplicas < 1)) {
|
|
106
|
+
throw new Error(`Module "${name}" declares an invalid maxApiReplicas constraint.`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function providerModuleName(projectRoot, modulesDir, capability) {
|
|
110
|
+
const project = (0, lockfile_1.readManifest)(projectRoot);
|
|
111
|
+
if (!project)
|
|
112
|
+
throw new Error("Provider capabilities require a PodoKit project manifest.");
|
|
113
|
+
const selected = project.providers[capability];
|
|
114
|
+
for (const available of listModules(modulesDir, projectRoot)) {
|
|
115
|
+
const resolved = resolveModule(available.name, modulesDir, projectRoot);
|
|
116
|
+
if (!resolved)
|
|
117
|
+
continue;
|
|
118
|
+
const provides = readModuleManifest(resolved.dir).provides;
|
|
119
|
+
if (provides?.[capability] === selected)
|
|
120
|
+
return available.name;
|
|
121
|
+
}
|
|
122
|
+
throw new Error(`No module provides the selected ${capability} provider "${selected}". ` +
|
|
123
|
+
`Choose another provider with podo provider set ${capability} <provider> --apply.`);
|
|
124
|
+
}
|
|
125
|
+
function requiredModules(projectRoot, modulesDir, manifest) {
|
|
126
|
+
const capabilities = (manifest.requiresCapabilities ?? []).map((capability) => providerModuleName(projectRoot, modulesDir, capability));
|
|
127
|
+
return [...new Set([...(manifest.requires ?? []), ...capabilities])];
|
|
91
128
|
}
|
|
92
129
|
function assertRequiredProjectFiles(projectRoot, name, manifest) {
|
|
93
130
|
const contents = manifest.requiredProjectContents ?? {};
|
|
@@ -136,7 +173,7 @@ function assertModuleGraphPrerequisites(projectRoot, module, modulesDir, visited
|
|
|
136
173
|
const manifest = readModuleManifest(resolved.dir);
|
|
137
174
|
assertManifestVersion(module, manifest);
|
|
138
175
|
assertRequiredProjectFiles(projectRoot, module, manifest);
|
|
139
|
-
for (const required of
|
|
176
|
+
for (const required of requiredModules(projectRoot, modulesDir, manifest)) {
|
|
140
177
|
assertModuleGraphPrerequisites(projectRoot, required, modulesDir, visited, module);
|
|
141
178
|
}
|
|
142
179
|
}
|
|
@@ -267,7 +304,7 @@ function applyModule(projectRoot, module, modulesDir, applied, adopt) {
|
|
|
267
304
|
const projectManifest = (0, lockfile_1.readManifest)(projectRoot);
|
|
268
305
|
const vars = {
|
|
269
306
|
projectName: appName,
|
|
270
|
-
...(projectManifest ? (0,
|
|
307
|
+
...(projectManifest ? (0, lockfile_1.manifestTemplateVars)(projectManifest) : {}),
|
|
271
308
|
};
|
|
272
309
|
const manifest = readModuleManifest(moduleDir, vars);
|
|
273
310
|
applied.add(module);
|
|
@@ -278,7 +315,7 @@ function applyModule(projectRoot, module, modulesDir, applied, adopt) {
|
|
|
278
315
|
const preserved = [];
|
|
279
316
|
const adopted = [];
|
|
280
317
|
const touched = [];
|
|
281
|
-
for (const required of
|
|
318
|
+
for (const required of requiredModules(projectRoot, modulesDir, manifest)) {
|
|
282
319
|
if (applied.has(required) || isApplied(projectRoot, modulesDir, required))
|
|
283
320
|
continue;
|
|
284
321
|
const result = applyModule(projectRoot, required, modulesDir, applied, adopt);
|
package/dist/create.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type DatabaseProviderName, type ProviderSelections } from "@podosoft/podokit-runtime";
|
|
1
2
|
import { DEFAULT_TEMPLATE } from "./templates";
|
|
2
3
|
import { type Runtime, type Toolchain } from "./toolchain";
|
|
3
4
|
export type PackageManager = "bun";
|
|
@@ -19,12 +20,15 @@ export interface CreateOptions {
|
|
|
19
20
|
podokitVersion?: string;
|
|
20
21
|
/** Include AI agent guidance (AGENTS.md, CLAUDE.md, editor rules). Defaults to true. */
|
|
21
22
|
ai?: boolean;
|
|
23
|
+
/** Database implementation selected for the generated project. */
|
|
24
|
+
database?: DatabaseProviderName;
|
|
22
25
|
}
|
|
23
26
|
export interface CreateResult {
|
|
24
27
|
projectDir: string;
|
|
25
28
|
packageManager: Toolchain["packageManager"];
|
|
26
29
|
toolchain: Toolchain;
|
|
27
30
|
template: string;
|
|
31
|
+
providers: ProviderSelections;
|
|
28
32
|
}
|
|
29
33
|
/** Validate a project name: no path separators, npm-friendly characters. */
|
|
30
34
|
export declare function assertValidName(name: string): void;
|
package/dist/create.js
CHANGED
|
@@ -6,6 +6,7 @@ exports.create = create;
|
|
|
6
6
|
const node_fs_1 = require("node:fs");
|
|
7
7
|
const node_path_1 = require("node:path");
|
|
8
8
|
const podokit_template_engine_1 = require("@podosoft/podokit-template-engine");
|
|
9
|
+
const podokit_runtime_1 = require("@podosoft/podokit-runtime");
|
|
9
10
|
const templates_1 = require("./templates");
|
|
10
11
|
Object.defineProperty(exports, "DEFAULT_TEMPLATE", { enumerable: true, get: function () { return templates_1.DEFAULT_TEMPLATE; } });
|
|
11
12
|
const lockfile_1 = require("./lockfile");
|
|
@@ -39,6 +40,11 @@ function create(options) {
|
|
|
39
40
|
assertValidName(name);
|
|
40
41
|
const template = options.template ?? templates_1.DEFAULT_TEMPLATE;
|
|
41
42
|
const toolchain = (0, toolchain_1.resolveToolchain)(options.runtime, options.packageManager);
|
|
43
|
+
const database = options.database ?? "postgres";
|
|
44
|
+
if (!(0, podokit_runtime_1.isProviderName)("database", database)) {
|
|
45
|
+
throw new Error('Database provider must be either "postgres" or "sqlite".');
|
|
46
|
+
}
|
|
47
|
+
const providers = (0, podokit_runtime_1.resolveProviderSelections)({ database });
|
|
42
48
|
const projectDir = options.targetDir
|
|
43
49
|
? (0, node_path_1.isAbsolute)(options.targetDir)
|
|
44
50
|
? options.targetDir
|
|
@@ -53,6 +59,7 @@ function create(options) {
|
|
|
53
59
|
}
|
|
54
60
|
const vars = {
|
|
55
61
|
projectName: name,
|
|
62
|
+
...(0, lockfile_1.providerTemplateVars)(providers),
|
|
56
63
|
...(0, toolchain_1.toolchainTemplateVars)(toolchain),
|
|
57
64
|
};
|
|
58
65
|
(0, podokit_template_engine_1.writeTree)((0, assemble_1.renderProjectTemplate)(templatesDir, template, vars), projectDir);
|
|
@@ -65,11 +72,13 @@ function create(options) {
|
|
|
65
72
|
toolchain,
|
|
66
73
|
answers: vars,
|
|
67
74
|
version: options.podokitVersion,
|
|
75
|
+
providers,
|
|
68
76
|
});
|
|
69
77
|
return {
|
|
70
78
|
projectDir,
|
|
71
79
|
packageManager: toolchain.packageManager,
|
|
72
80
|
toolchain,
|
|
73
81
|
template,
|
|
82
|
+
providers,
|
|
74
83
|
};
|
|
75
84
|
}
|
|
@@ -6,8 +6,10 @@ exports.initializeComposeProfile = initializeComposeProfile;
|
|
|
6
6
|
exports.composeProfileDigest = composeProfileDigest;
|
|
7
7
|
const node_crypto_1 = require("node:crypto");
|
|
8
8
|
const node_fs_1 = require("node:fs");
|
|
9
|
+
const node_net_1 = require("node:net");
|
|
9
10
|
const lockfile_1 = require("./lockfile");
|
|
10
11
|
const deploy_schema_1 = require("./deploy-schema");
|
|
12
|
+
const websocket_paths_1 = require("./websocket-paths");
|
|
11
13
|
/** Decimal core count, e.g. "0.25" or "2". */
|
|
12
14
|
const CPU_VALUE = /^(?:0\.[0-9]{1,3}|[1-9][0-9]{0,2}(?:\.[0-9]{1,3})?)$/;
|
|
13
15
|
/** Compose byte suffix, e.g. "512m" or "2g". */
|
|
@@ -17,6 +19,31 @@ const PROJECT_NAME = /^[a-z0-9][a-z0-9_-]*$/;
|
|
|
17
19
|
/** An absolute POSIX path on the target host, with no traversal or control bytes. */
|
|
18
20
|
const ABSOLUTE_PATH = /^\/(?:[A-Za-z0-9._-]+\/?)+$/;
|
|
19
21
|
const VOLUME_NAME = /^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/;
|
|
22
|
+
function parseTrustedProxyCidrs(value, field) {
|
|
23
|
+
if (!Array.isArray(value)) {
|
|
24
|
+
throw new Error(`Deployment profile field "${field}" must be a string array.`);
|
|
25
|
+
}
|
|
26
|
+
const parsed = value.map((entry, index) => {
|
|
27
|
+
if (typeof entry !== "string" || entry !== entry.trim()) {
|
|
28
|
+
throw new Error(`Deployment profile field "${field}[${index}]" must be an IP CIDR block.`);
|
|
29
|
+
}
|
|
30
|
+
const separator = entry.lastIndexOf("/");
|
|
31
|
+
const address = entry.slice(0, separator);
|
|
32
|
+
const prefixText = entry.slice(separator + 1);
|
|
33
|
+
const family = (0, node_net_1.isIP)(address);
|
|
34
|
+
if (separator <= 0 ||
|
|
35
|
+
family === 0 ||
|
|
36
|
+
!/^(?:0|[1-9][0-9]*)$/.test(prefixText) ||
|
|
37
|
+
Number(prefixText) > (family === 4 ? 32 : 128)) {
|
|
38
|
+
throw new Error(`Deployment profile field "${field}[${index}]" must be an IP CIDR block.`);
|
|
39
|
+
}
|
|
40
|
+
return entry;
|
|
41
|
+
});
|
|
42
|
+
if (new Set(parsed).size !== parsed.length) {
|
|
43
|
+
throw new Error(`Deployment profile field "${field}" contains duplicate CIDR blocks.`);
|
|
44
|
+
}
|
|
45
|
+
return parsed.sort();
|
|
46
|
+
}
|
|
20
47
|
function validateCpu(value, field) {
|
|
21
48
|
if (!CPU_VALUE.test(value)) {
|
|
22
49
|
throw new Error(`Deployment profile field "${field}" must be a decimal CPU count.`);
|
|
@@ -134,7 +161,15 @@ function parseComposeProfile(value) {
|
|
|
134
161
|
const migration = value.migration === undefined ? undefined : (0, deploy_schema_1.requiredRecord)(value, "migration");
|
|
135
162
|
const sync = value.sync === undefined ? undefined : (0, deploy_schema_1.requiredRecord)(value, "sync");
|
|
136
163
|
(0, deploy_schema_1.assertOnlyKeys)(target, ["context", "endpointFingerprint", "project"], "target");
|
|
137
|
-
(0, deploy_schema_1.assertOnlyKeys)(exposure, [
|
|
164
|
+
(0, deploy_schema_1.assertOnlyKeys)(exposure, [
|
|
165
|
+
"mode",
|
|
166
|
+
"host",
|
|
167
|
+
"bindAddress",
|
|
168
|
+
"port",
|
|
169
|
+
"webSocketPaths",
|
|
170
|
+
"trustedProxyCidrs",
|
|
171
|
+
"gatewayImage",
|
|
172
|
+
], "exposure");
|
|
138
173
|
(0, deploy_schema_1.assertOnlyKeys)(workloads, ["api", "web", "worker"], "workloads");
|
|
139
174
|
(0, deploy_schema_1.assertOnlyKeys)(dependencies, ["postgres", "redis", "objectStorage"], "dependencies");
|
|
140
175
|
(0, deploy_schema_1.assertOnlyKeys)(secrets, ["api", "web", "registryLogin"], "secrets");
|
|
@@ -161,6 +196,15 @@ function parseComposeProfile(value) {
|
|
|
161
196
|
const port = (0, deploy_schema_1.requiredNumber)(exposure, "port");
|
|
162
197
|
if (port > 65535)
|
|
163
198
|
throw new Error("Deployment exposure.port must be a TCP port.");
|
|
199
|
+
const webSocketPaths = (0, websocket_paths_1.parseExactWebSocketPaths)(exposure.webSocketPaths ?? [], "exposure.webSocketPaths");
|
|
200
|
+
const trustedProxyCidrs = parseTrustedProxyCidrs(exposure.trustedProxyCidrs ?? [], "exposure.trustedProxyCidrs");
|
|
201
|
+
if (trustedProxyCidrs.length > 0 && webSocketPaths.length === 0) {
|
|
202
|
+
throw new Error("Deployment exposure.trustedProxyCidrs requires at least one exposure.webSocketPaths entry.");
|
|
203
|
+
}
|
|
204
|
+
const gatewayImage = exposure.gatewayImage === undefined
|
|
205
|
+
? "caddy:2.10-alpine"
|
|
206
|
+
: (0, deploy_schema_1.requiredString)(exposure, "gatewayImage");
|
|
207
|
+
(0, deploy_schema_1.validateImage)(gatewayImage, "exposure.gatewayImage");
|
|
164
208
|
const web = parseComposeWorkload((0, deploy_schema_1.requiredRecord)(workloads, "web"), "workloads.web");
|
|
165
209
|
// Compose has no load balancer. Two replicas of the service that publishes the
|
|
166
210
|
// host port means the second one cannot bind it, and the deployment fails half
|
|
@@ -188,7 +232,15 @@ function parseComposeProfile(value) {
|
|
|
188
232
|
driver: "docker-compose",
|
|
189
233
|
target: { context, endpointFingerprint, project },
|
|
190
234
|
release: (0, deploy_schema_1.parseReleaseProfile)((0, deploy_schema_1.requiredRecord)(value, "release")),
|
|
191
|
-
exposure: {
|
|
235
|
+
exposure: {
|
|
236
|
+
mode: "publishedPort",
|
|
237
|
+
host,
|
|
238
|
+
bindAddress,
|
|
239
|
+
port,
|
|
240
|
+
webSocketPaths,
|
|
241
|
+
trustedProxyCidrs,
|
|
242
|
+
gatewayImage,
|
|
243
|
+
},
|
|
192
244
|
workloads: {
|
|
193
245
|
api: parseComposeWorkload((0, deploy_schema_1.requiredRecord)(workloads, "api"), "workloads.api"),
|
|
194
246
|
web,
|
|
@@ -249,6 +301,9 @@ function buildDefaultComposeProfile(projectName, options) {
|
|
|
249
301
|
// network before TLS exists.
|
|
250
302
|
bindAddress: "127.0.0.1",
|
|
251
303
|
port: 8080,
|
|
304
|
+
webSocketPaths: [],
|
|
305
|
+
trustedProxyCidrs: [],
|
|
306
|
+
gatewayImage: "caddy:2.10-alpine",
|
|
252
307
|
},
|
|
253
308
|
workloads: {
|
|
254
309
|
api: { replicas: 2, resources: { cpuLimit: "1", memoryLimit: "1g" } },
|
|
@@ -273,8 +328,8 @@ function buildDefaultComposeProfile(projectName, options) {
|
|
|
273
328
|
},
|
|
274
329
|
objectStorage: {
|
|
275
330
|
mode: "disabled",
|
|
276
|
-
image: "
|
|
277
|
-
clientImage: "
|
|
331
|
+
image: "pgsty/silo:RELEASE.2026-09-16T00-00-00Z",
|
|
332
|
+
clientImage: "pgsty/mc:RELEASE.2026-09-16T00-00-00Z",
|
|
278
333
|
envFile: `${secretsDirectory}/object-storage.env`,
|
|
279
334
|
requiredKeys: [
|
|
280
335
|
"MINIO_ROOT_PASSWORD",
|
|
@@ -319,12 +374,24 @@ function initializeComposeProfile(projectRoot, name, options) {
|
|
|
319
374
|
throw new Error(`Deployment profile "${name}" already exists.`);
|
|
320
375
|
const draft = buildDefaultComposeProfile(projectName, options);
|
|
321
376
|
const modules = new Set(manifest.modules.map((module) => module.name));
|
|
322
|
-
const
|
|
377
|
+
const providers = manifest.providers;
|
|
378
|
+
const usesRedis = (providers.cache === "redis"
|
|
379
|
+
&& ["redis", "rate-limit"].some((module) => modules.has(module)))
|
|
380
|
+
|| (providers.events === "redis" && modules.has("sse"))
|
|
381
|
+
|| (providers.jobs === "bullmq" && modules.has("bullmq"));
|
|
382
|
+
const usesEmbeddedProviders = providers.database === "sqlite"
|
|
383
|
+
|| providers.cache === "memory"
|
|
384
|
+
|| providers["object-storage"] === "local"
|
|
385
|
+
|| providers.events === "memory"
|
|
386
|
+
|| providers.jobs === "local";
|
|
387
|
+
draft.workloads.api.replicas = usesEmbeddedProviders ? 1 : 2;
|
|
388
|
+
draft.dependencies.postgres.mode = providers.database === "postgres" ? "managed" : "disabled";
|
|
323
389
|
draft.dependencies.redis.mode = usesRedis ? "managed" : "disabled";
|
|
324
|
-
draft.dependencies.objectStorage.mode =
|
|
390
|
+
draft.dependencies.objectStorage.mode = providers["object-storage"] === "s3"
|
|
391
|
+
&& modules.has("object-storage-s3")
|
|
325
392
|
? "managed"
|
|
326
393
|
: "disabled";
|
|
327
|
-
if (modules.has("bullmq")) {
|
|
394
|
+
if (providers.jobs === "bullmq" && modules.has("bullmq")) {
|
|
328
395
|
draft.workloads.worker = { replicas: 1, resources: { cpuLimit: "0.5", memoryLimit: "512m" } };
|
|
329
396
|
}
|
|
330
397
|
const usesAuth = modules.has("auth");
|
|
@@ -336,7 +403,6 @@ function initializeComposeProfile(projectRoot, name, options) {
|
|
|
336
403
|
draft.runtimeConfig = {
|
|
337
404
|
...draft.runtimeConfig,
|
|
338
405
|
...(usesAuth ? { BETTER_AUTH_URL: `https://${host}` } : {}),
|
|
339
|
-
...(modules.has("sse") && usesRedis ? { SSE_TRANSPORT: "redis" } : {}),
|
|
340
406
|
...(modules.has("rate-limit")
|
|
341
407
|
? {
|
|
342
408
|
RATE_LIMIT_TTL: "60",
|
|
@@ -239,8 +239,46 @@ function applicationService(profile, role, image, release, stateDigest, options)
|
|
|
239
239
|
" healthcheck:\n disable: true\n") +
|
|
240
240
|
labelsBlock(profile, release, stateDigest, " "));
|
|
241
241
|
}
|
|
242
|
+
function gatewayService(profile, image, release, stateDigest) {
|
|
243
|
+
const globalOptions = profile.exposure.trustedProxyCidrs.length
|
|
244
|
+
? `{
|
|
245
|
+
servers {
|
|
246
|
+
trusted_proxies static ${profile.exposure.trustedProxyCidrs.join(" ")}
|
|
247
|
+
trusted_proxies_strict
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
`
|
|
251
|
+
: "";
|
|
252
|
+
const caddyfile = globalOptions +
|
|
253
|
+
`:3000 {\n` +
|
|
254
|
+
` @api_websockets path ${profile.exposure.webSocketPaths.join(" ")}\n` +
|
|
255
|
+
` handle @api_websockets {\n` +
|
|
256
|
+
` reverse_proxy ${serviceName(profile, "api")}:${CONTAINER_PORT}\n` +
|
|
257
|
+
` }\n` +
|
|
258
|
+
` handle {\n` +
|
|
259
|
+
` reverse_proxy ${serviceName(profile, "web")}:${CONTAINER_PORT}\n` +
|
|
260
|
+
` }\n` +
|
|
261
|
+
`}\n`;
|
|
262
|
+
const start = "printf '%s' \"$${CADDYFILE_CONTENT}\" > /tmp/Caddyfile\n" +
|
|
263
|
+
"exec caddy run --config /tmp/Caddyfile --adapter caddyfile";
|
|
264
|
+
return (` ${serviceName(profile, "gateway")}:\n` +
|
|
265
|
+
` image: ${quote(image)}\n` +
|
|
266
|
+
` restart: unless-stopped\n` +
|
|
267
|
+
` entrypoint: ["sh", "-c", ${quote(start)}]\n` +
|
|
268
|
+
environmentBlock({ CADDYFILE_CONTENT: caddyfile }, " ") +
|
|
269
|
+
` depends_on:\n` +
|
|
270
|
+
` ${serviceName(profile, "api")}:\n condition: service_healthy\n` +
|
|
271
|
+
` ${serviceName(profile, "web")}:\n condition: service_healthy\n` +
|
|
272
|
+
` ports:\n - ${quote(`${profile.exposure.bindAddress}:${profile.exposure.port}:${CONTAINER_PORT}`)}\n` +
|
|
273
|
+
` networks: [${profile.target.project}]\n` +
|
|
274
|
+
` healthcheck:\n` +
|
|
275
|
+
` test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:2019/config/"]\n` +
|
|
276
|
+
` interval: 10s\n timeout: 5s\n retries: 6\n start_period: 10s\n` +
|
|
277
|
+
labelsBlock(profile, release, stateDigest, " "));
|
|
278
|
+
}
|
|
242
279
|
function renderComposeDocument(profile, release, images, stateDigest, workerCommand = ["bun", "dist/main-worker.js"]) {
|
|
243
280
|
const services = [];
|
|
281
|
+
const hasGateway = profile.exposure.webSocketPaths.length > 0;
|
|
244
282
|
if (profile.dependencies.postgres.mode === "managed" && images.postgres) {
|
|
245
283
|
services.push(postgresService(profile, images.postgres));
|
|
246
284
|
}
|
|
@@ -258,8 +296,14 @@ function renderComposeDocument(profile, release, images, stateDigest, workerComm
|
|
|
258
296
|
}));
|
|
259
297
|
services.push(applicationService(profile, "web", images.web, release, stateDigest, {
|
|
260
298
|
healthcheckPath: "/",
|
|
261
|
-
publish:
|
|
299
|
+
publish: !hasGateway,
|
|
262
300
|
}));
|
|
301
|
+
if (hasGateway) {
|
|
302
|
+
if (!images.gateway) {
|
|
303
|
+
throw new Error("A resolved gateway image is required when WebSocket paths are configured.");
|
|
304
|
+
}
|
|
305
|
+
services.push(gatewayService(profile, images.gateway, release, stateDigest));
|
|
306
|
+
}
|
|
263
307
|
if (profile.workloads.worker) {
|
|
264
308
|
// The worker runs the API image with a different entry point, exactly as the
|
|
265
309
|
// Kubernetes driver does.
|
|
@@ -306,6 +350,7 @@ function defaultComposeImages(profile, release) {
|
|
|
306
350
|
return {
|
|
307
351
|
api: `${profile.release.apiRepository}:${release}`,
|
|
308
352
|
web: `${profile.release.webRepository}:${release}`,
|
|
353
|
+
gateway: profile.exposure.webSocketPaths.length > 0 ? profile.exposure.gatewayImage : null,
|
|
309
354
|
postgres: profile.dependencies.postgres.mode === "managed"
|
|
310
355
|
? profile.dependencies.postgres.image
|
|
311
356
|
: null,
|
package/dist/deploy-compose.js
CHANGED
|
@@ -150,6 +150,7 @@ function resolveComposeImages(profile, release, runner) {
|
|
|
150
150
|
return {
|
|
151
151
|
api: resolveImage(defaults.api, runner),
|
|
152
152
|
web: resolveImage(defaults.web, runner),
|
|
153
|
+
gateway: defaults.gateway ? resolveImage(defaults.gateway, runner) : null,
|
|
153
154
|
postgres: defaults.postgres ? resolveImage(defaults.postgres, runner) : null,
|
|
154
155
|
redis: defaults.redis ? resolveImage(defaults.redis, runner) : null,
|
|
155
156
|
objectStorage: defaults.objectStorage ? resolveImage(defaults.objectStorage, runner) : null,
|
|
@@ -372,7 +373,7 @@ function planComposeDeployment(projectRoot, profileName, release, runner = deplo
|
|
|
372
373
|
actions.push({
|
|
373
374
|
order: order++,
|
|
374
375
|
kind: "application",
|
|
375
|
-
description: `Roll out api, web${profile.workloads.worker ? ", worker" : ""} and wait for health.`,
|
|
376
|
+
description: `Roll out api, web${profile.workloads.worker ? ", worker" : ""}${profile.exposure.webSocketPaths.length ? ", gateway" : ""} and wait for health.`,
|
|
376
377
|
});
|
|
377
378
|
actions.push({
|
|
378
379
|
order: order++,
|
package/dist/deploy-profile.js
CHANGED
|
@@ -279,8 +279,19 @@ function initializeDeploymentProfile(projectRoot, name, options) {
|
|
|
279
279
|
throw new Error("Project name cannot be converted to a Kubernetes release name.");
|
|
280
280
|
const host = options.host ?? "app.example.com";
|
|
281
281
|
const modules = new Set(manifest.modules.map((module) => module.name));
|
|
282
|
-
const
|
|
283
|
-
const
|
|
282
|
+
const providers = manifest.providers;
|
|
283
|
+
const usesPostgres = providers.database === "postgres";
|
|
284
|
+
const usesRedis = (providers.cache === "redis"
|
|
285
|
+
&& ["redis", "rate-limit"].some((module) => modules.has(module)))
|
|
286
|
+
|| (providers.events === "redis" && modules.has("sse"))
|
|
287
|
+
|| (providers.jobs === "bullmq" && modules.has("bullmq"));
|
|
288
|
+
const usesObjectStorage = providers["object-storage"] === "s3"
|
|
289
|
+
&& modules.has("object-storage-s3");
|
|
290
|
+
const usesEmbeddedProviders = providers.database === "sqlite"
|
|
291
|
+
|| providers.cache === "memory"
|
|
292
|
+
|| providers["object-storage"] === "local"
|
|
293
|
+
|| providers.events === "memory"
|
|
294
|
+
|| providers.jobs === "local";
|
|
284
295
|
const usesAuth = modules.has("auth");
|
|
285
296
|
const apiRequiredKeys = [
|
|
286
297
|
...(usesAuth ? ["BETTER_AUTH_SECRET"] : []),
|
|
@@ -311,7 +322,7 @@ function initializeDeploymentProfile(projectRoot, name, options) {
|
|
|
311
322
|
},
|
|
312
323
|
workloads: {
|
|
313
324
|
api: {
|
|
314
|
-
replicas: 2,
|
|
325
|
+
replicas: usesEmbeddedProviders ? 1 : 2,
|
|
315
326
|
resources: {
|
|
316
327
|
cpuRequest: "250m",
|
|
317
328
|
cpuLimit: "1000m",
|
|
@@ -328,7 +339,7 @@ function initializeDeploymentProfile(projectRoot, name, options) {
|
|
|
328
339
|
memoryLimit: "1Gi",
|
|
329
340
|
},
|
|
330
341
|
},
|
|
331
|
-
worker: modules.has("bullmq")
|
|
342
|
+
worker: providers.jobs === "bullmq" && modules.has("bullmq")
|
|
332
343
|
? {
|
|
333
344
|
replicas: 1,
|
|
334
345
|
resources: {
|
|
@@ -342,7 +353,7 @@ function initializeDeploymentProfile(projectRoot, name, options) {
|
|
|
342
353
|
},
|
|
343
354
|
dependencies: {
|
|
344
355
|
postgres: {
|
|
345
|
-
mode: "inCluster",
|
|
356
|
+
mode: usesPostgres ? "inCluster" : "disabled",
|
|
346
357
|
image: "postgres:16.10-alpine",
|
|
347
358
|
secretName: `${releaseName}-postgres`,
|
|
348
359
|
storageClassName: "",
|
|
@@ -357,8 +368,8 @@ function initializeDeploymentProfile(projectRoot, name, options) {
|
|
|
357
368
|
},
|
|
358
369
|
objectStorage: {
|
|
359
370
|
mode: usesObjectStorage ? "inCluster" : "disabled",
|
|
360
|
-
image: "
|
|
361
|
-
clientImage: "
|
|
371
|
+
image: "pgsty/silo:RELEASE.2026-09-16T00-00-00Z",
|
|
372
|
+
clientImage: "pgsty/mc:RELEASE.2026-09-16T00-00-00Z",
|
|
362
373
|
secretName: `${releaseName}-object-storage`,
|
|
363
374
|
storageClassName: "",
|
|
364
375
|
storageSize: "8Gi",
|
|
@@ -379,7 +390,6 @@ function initializeDeploymentProfile(projectRoot, name, options) {
|
|
|
379
390
|
ADDRESS_HEADER: "x-forwarded-for",
|
|
380
391
|
XFF_DEPTH: "1",
|
|
381
392
|
...(usesAuth ? { BETTER_AUTH_URL: `https://${host}` } : {}),
|
|
382
|
-
...(modules.has("sse") && usesRedis ? { SSE_TRANSPORT: "redis" } : {}),
|
|
383
393
|
...(modules.has("rate-limit")
|
|
384
394
|
? {
|
|
385
395
|
RATE_LIMIT_TTL: "60",
|