okengine 0.10.2 → 0.11.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/package.json +76 -10
- package/site/content/docs/ai/mcp.mdx +0 -1
- package/site/content/docs/deployment/docker-swarm.mdx +48 -111
- package/site/content/docs/deployment/docker.mdx +43 -36
- package/site/content/docs/deployment/index.mdx +9 -9
- package/site/content/docs/deployment/kubernetes.mdx +4 -4
- package/site/content/docs/elements/ai.mdx +7 -13
- package/site/content/docs/elements/channel.mdx +9 -13
- package/site/content/docs/elements/clock.mdx +8 -14
- package/site/content/docs/elements/flow.mdx +46 -54
- package/site/content/docs/elements/gate.mdx +6 -11
- package/site/content/docs/elements/signal.mdx +2 -8
- package/site/content/docs/elements/store.mdx +79 -93
- package/site/content/docs/elements/vault.mdx +89 -76
- package/site/content/docs/get-started/basic-usage.mdx +8 -14
- package/site/content/docs/get-started/installation.mdx +36 -36
- package/site/content/docs/get-started/introduction.mdx +11 -0
- package/site/content/docs/get-started/why.mdx +3 -4
- package/site/content/docs/index.mdx +0 -1
- package/site/content/docs/meta.json +0 -1
- package/site/content/docs/plugins/anonymous.mdx +1 -1
- package/site/content/docs/plugins/magic-link.mdx +11 -11
- package/site/content/docs/plugins/otp.mdx +2 -2
- package/site/content/docs/plugins/passkey.mdx +1 -1
- package/site/content/docs/plugins/two-factor.mdx +1 -1
- package/site/content/docs/plugins/username.mdx +1 -1
- package/site/content/docs/recipes/caddy.mdx +1 -1
- package/site/content/docs/recipes/cockroachdb.mdx +2 -2
- package/site/content/docs/recipes/dragonfly.mdx +1 -1
- package/site/content/docs/recipes/index.mdx +1 -2
- package/site/content/docs/recipes/llama-cpp.mdx +21 -23
- package/site/content/docs/recipes/mailpit.mdx +11 -11
- package/site/content/docs/recipes/meilisearch.mdx +10 -10
- package/site/content/docs/recipes/meta.json +0 -1
- package/site/content/docs/recipes/ollama.mdx +1 -1
- package/site/content/docs/recipes/pgdog.mdx +12 -9
- package/site/content/docs/recipes/postgres.mdx +3 -3
- package/site/content/docs/recipes/redis.mdx +2 -2
- package/site/content/docs/recipes/rustfs.mdx +10 -7
- package/site/content/docs/recipes/sglang.mdx +1 -1
- package/site/content/docs/recipes/supabase-docker.mdx +1 -1
- package/site/content/docs/recipes/timescale.mdx +2 -2
- package/site/content/docs/recipes/valkey.mdx +1 -1
- package/site/content/docs/recipes/vllm.mdx +1 -1
- package/site/content/docs/recipes/yugabytedb.mdx +2 -2
- package/site/content/docs/reference/cli.md +61 -38
- package/site/content/docs/reference/configuration.mdx +145 -70
- package/site/content/docs/reference/environment-variables.mdx +36 -28
- package/site/content/docs/reference/errors.mdx +28 -29
- package/site/content/docs/reference/fx.mdx +14 -7
- package/site/content/docs/reference/i18n.mdx +17 -6
- package/site/content/docs/reference/index.mdx +5 -0
- package/site/content/docs/reference/meta.json +1 -0
- package/site/content/docs/reference/migrating-environments.mdx +158 -0
- package/site/content/docs/reference/plugins.mdx +2 -2
- package/src/auth/gate-auth.test.ts +12 -10
- package/src/cli/ai-setup/ai-setup.test.ts +30 -16
- package/src/cli/ai-setup/apply.ts +111 -15
- package/src/cli/ai-setup/catalog.ts +3 -3
- package/src/cli/ai-setup/index.ts +1 -1
- package/src/cli/ai-setup/prompts.ts +1 -1
- package/src/cli/ai-setup/recommend.test.ts +1 -1
- package/src/cli/ask-seed.test.ts +3 -3
- package/src/cli/build.ts +8 -9
- package/src/cli/db-auto-push.test.ts +6 -3
- package/src/cli/db-auto-push.ts +7 -5
- package/src/cli/db-seed.ts +7 -8
- package/src/cli/db.test.ts +70 -71
- package/src/cli/db.ts +86 -12
- package/src/cli/dev-controller.test.ts +35 -0
- package/src/cli/dev-controller.ts +298 -0
- package/src/cli/dev-controls.ts +11 -65
- package/src/cli/dev-db-push.test.ts +47 -15
- package/src/cli/dev-schema-sync.test.ts +11 -11
- package/src/cli/dev-schema-sync.ts +10 -10
- package/src/cli/dev-session-lock.test.ts +106 -0
- package/src/cli/dev-session-lock.ts +209 -0
- package/src/cli/dev.test.ts +246 -114
- package/src/cli/dev.ts +153 -205
- package/src/cli/doc-staleness.test.ts +3 -6
- package/src/cli/docker-clean.test.ts +33 -1
- package/src/cli/docker-clean.ts +28 -33
- package/src/cli/docker-cli.test.ts +12 -11
- package/src/cli/docker.ts +95 -20
- package/src/cli/doctor.test.ts +40 -0
- package/src/cli/doctor.ts +142 -9
- package/src/cli/drizzle-env.test.ts +11 -13
- package/src/cli/drizzle-env.ts +18 -11
- package/src/cli/ensure-drizzle-config.ts +11 -14
- package/src/cli/hero-meta.test.ts +15 -15
- package/src/cli/hero-meta.ts +10 -9
- package/src/cli/index.ts +36 -6
- package/src/cli/load-config.images.test.ts +44 -10
- package/src/cli/load-config.source.test.ts +50 -0
- package/src/cli/load-config.ts +35 -29
- package/src/cli/meilisearch-local.ts +1 -2
- package/src/cli/mode.ts +7 -58
- package/src/cli/registry.ts +175 -19
- package/src/cli/resolve-dev-sql-env.test.ts +8 -16
- package/src/cli/resolve-dev-sql-env.ts +13 -15
- package/src/cli/safe-defaults.test.ts +29 -11
- package/src/cli/schema.ts +65 -16
- package/src/cli/test.ts +58 -0
- package/src/cli/tui/App.tsx +252 -0
- package/src/cli/tui/DevLive.tsx +116 -0
- package/src/cli/tui/components/ConfirmDialog.tsx +48 -0
- package/src/cli/tui/components/HeaderBar.tsx +67 -0
- package/src/cli/tui/components/LogStream.tsx +47 -0
- package/src/cli/tui/components/PanelCard.tsx +39 -0
- package/src/cli/tui/components/SelectRow.tsx +37 -0
- package/src/cli/tui/components/SlashPalette.tsx +172 -0
- package/src/cli/tui/components/Spinner.tsx +32 -0
- package/src/cli/tui/components/StatusBar.tsx +51 -0
- package/src/cli/tui/components/StatusDot.tsx +37 -0
- package/src/cli/tui/components/TabBar.tsx +49 -0
- package/src/cli/tui/components/Wordmark.tsx +43 -0
- package/src/cli/tui/format.ts +38 -0
- package/src/cli/tui/keys.test.ts +18 -0
- package/src/cli/tui/keys.ts +26 -0
- package/src/cli/tui/launch.test.ts +18 -0
- package/src/cli/tui/launch.ts +57 -0
- package/src/cli/tui/panels/Dashboard.tsx +201 -0
- package/src/cli/tui/panels/Database.tsx +198 -0
- package/src/cli/tui/panels/Dev.tsx +257 -0
- package/src/cli/tui/panels/Docker.tsx +357 -0
- package/src/cli/tui/panels/Navigator.test.ts +22 -0
- package/src/cli/tui/panels/Navigator.tsx +213 -0
- package/src/cli/tui/slash-catalog.test.ts +31 -0
- package/src/cli/tui/slash-catalog.ts +184 -0
- package/src/cli/tui/slash-run.ts +54 -0
- package/src/cli/tui/state/ports.ts +95 -0
- package/src/cli/tui/theme.ts +24 -0
- package/src/cli/vault-cmd.test.ts +402 -0
- package/src/cli/vault-cmd.ts +727 -69
- package/src/cli/vault-secure-input.test.ts +56 -0
- package/src/cli/vault-secure-input.ts +124 -0
- package/src/compiler/effects-infer.ts +7 -0
- package/src/compiler/extract.ts +12 -0
- package/src/compiler/fixtures/skyport/oke.config.ts +7 -5
- package/src/compiler/fixtures/skyport/src/flows/payments/index.ts +1 -1
- package/src/compiler/fixtures/skyport.expected.json +1 -1
- package/src/compiler/generate-adopt.ts +27 -1
- package/src/compiler-entry.ts +7 -0
- package/src/config/define-config.test.ts +111 -47
- package/src/config/driver-defaults.test.ts +115 -0
- package/src/config/driver-defaults.ts +156 -0
- package/src/config/index.ts +246 -131
- package/src/config/resolve-driver.test.ts +14 -19
- package/src/console/server/app.ts +4 -2
- package/src/console/server/console.test.ts +58 -1
- package/src/console/server/flows.ts +22 -2
- package/src/console/server/index.ts +3 -0
- package/src/console/server/operator-db.test.ts +55 -13
- package/src/console/server/operator-db.ts +305 -263
- package/src/console/server/serve.ts +16 -7
- package/src/console/server/state.ts +31 -9
- package/src/console/server/vault.test.ts +120 -5
- package/src/console/server/vault.ts +208 -12
- package/src/console/ui/dist/assets/{index-D-9KFce5.js → index--Y0Uaz-m.js} +2 -2
- package/src/console/ui/dist/assets/panel-vault-BkZdJPX8.js +1 -0
- package/src/console/ui/dist/assets/style-C3L349iZ.css +3 -0
- package/src/console/ui/dist/index.html +2 -2
- package/src/console/ui/shell/client.ts +16 -0
- package/src/console/ui/shell/panels/vault/VaultPanel.tsx +54 -1
- package/src/console/ui/vault/VaultA11yView.tsx +33 -1
- package/src/console/ui/vault/backend.test.ts +75 -0
- package/src/console/ui/vault/backend.ts +117 -0
- package/src/console/ui/vault/fixture.ts +21 -2
- package/src/console/ui/vault/index.ts +11 -1
- package/src/console/ui/vault/types.ts +26 -0
- package/src/docker/compose.ts +164 -61
- package/src/docker/derive.ts +106 -12
- package/src/docker/docker.test.ts +156 -110
- package/src/docker/helpers.ts +98 -0
- package/src/docker/images-config.test.ts +163 -0
- package/src/docker/index.ts +30 -5
- package/src/docker/recipes/index.ts +0 -3
- package/src/docker/recipes/llama-cpp.ts +4 -4
- package/src/docker/recipes/pgdog.ts +9 -2
- package/src/docker/resources.test.ts +71 -0
- package/src/docker/resources.ts +176 -0
- package/src/docker/stack-id.test.ts +11 -4
- package/src/docker/stack-id.ts +42 -32
- package/src/docker/stack.integration.test.ts +4 -15
- package/src/docker/types.ts +35 -3
- package/src/drivers/ai-ollama.integration.test.ts +2 -2
- package/src/drivers/conformance.test.ts +11 -12
- package/src/drivers/drizzle-dialect.test.ts +0 -4
- package/src/drivers/drizzle-dialect.ts +7 -13
- package/src/drivers/index.ts +24 -7
- package/src/drivers/journal-postgres.test.ts +3 -1
- package/src/drivers/journal-postgres.ts +4 -2
- package/src/drivers/meilisearch.ts +1 -1
- package/src/drivers/pglite.ts +28 -5
- package/src/drivers/types.ts +9 -11
- package/src/drivers/vault-aws-secrets-manager.test.ts +159 -0
- package/src/drivers/vault-aws-secrets-manager.ts +333 -0
- package/src/drivers/vault-builtin.test.ts +110 -0
- package/src/drivers/vault-builtin.ts +184 -0
- package/src/drivers/vault-driver-removal.test.ts +13 -0
- package/src/drivers/vault-managed.test.ts +95 -0
- package/src/drivers/vault-managed.ts +101 -25
- package/src/drivers/vault-types.ts +25 -5
- package/src/elements/ai/runtime.ts +1 -1
- package/src/elements/clock/chaos.test.ts +4 -1
- package/src/elements/clock/durable.ts +57 -4
- package/src/elements/clock.test.ts +13 -14
- package/src/elements/gate/constants.ts +18 -0
- package/src/elements/gate.test.ts +1 -1
- package/src/elements/gate.ts +2 -2
- package/src/elements/store/domain-ddl.test.ts +5 -12
- package/src/elements/store/emit-drizzle.ts +44 -19
- package/src/elements/store/index-boot.test.ts +15 -116
- package/src/elements/store/load-plugin-tables.ts +1 -1
- package/src/elements/store/resource-list-docs.fixture.ts +12 -6
- package/src/elements/store/resource-list-docs.test.ts +64 -44
- package/src/elements/store/resource.test.ts +17 -10
- package/src/elements/store/runtime.ts +9 -12
- package/src/elements/store/schema-decl.test.ts +16 -32
- package/src/elements/store/seed.test.ts +1 -2
- package/src/elements/store/seed.ts +7 -8
- package/src/elements/store/sql-condition.test.ts +2 -2
- package/src/elements/store/sql-session.test.ts +8 -10
- package/src/elements/store/sql-session.ts +10 -2
- package/src/elements/store.test.ts +13 -13
- package/src/elements/vault/audit.ts +233 -0
- package/src/elements/vault/boot-chain.test.ts +78 -0
- package/src/elements/vault/boot-chain.ts +43 -39
- package/src/elements/vault/builtin-adapter.test.ts +479 -0
- package/src/elements/vault/builtin-adapter.ts +1184 -0
- package/src/elements/vault/crypto.test.ts +237 -0
- package/src/elements/vault/crypto.ts +431 -0
- package/src/elements/vault/declare.ts +164 -3
- package/src/elements/vault/errors.ts +56 -0
- package/src/elements/vault/kms-unseal.test.ts +76 -0
- package/src/elements/vault/kms-unseal.ts +181 -0
- package/src/elements/vault/path.ts +90 -0
- package/src/elements/vault/resilience.test.ts +102 -0
- package/src/elements/vault/resilience.ts +107 -0
- package/src/elements/vault/runtime.ts +36 -4
- package/src/elements/vault/security-checklist.test.ts +130 -0
- package/src/elements/vault/storage.ts +448 -0
- package/src/elements/vault/types.ts +288 -0
- package/src/elements/vault/unseal.ts +150 -0
- package/src/elements/vault.test.ts +72 -4
- package/src/elements/vault.ts +91 -2
- package/src/full.ts +248 -0
- package/src/http.ts +41 -0
- package/src/i18n/interpolate-simple.ts +37 -0
- package/src/i18n/messages.ts +51 -4
- package/src/i18n-entry.ts +7 -0
- package/src/index.ts +35 -145
- package/src/journal-entry.ts +20 -0
- package/src/kernel/adopt-barrel-fresh.test.ts +25 -10
- package/src/kernel/app.ts +40 -42
- package/src/kernel/auto-registry.test.ts +2 -2
- package/src/kernel/boot-bind/ai.test.ts +5 -5
- package/src/kernel/boot-bind/ai.ts +2 -1
- package/src/kernel/boot-bind/channel.test.ts +6 -6
- package/src/kernel/boot-bind/channel.ts +5 -4
- package/src/kernel/boot-bind/clock.ts +5 -5
- package/src/kernel/boot-bind/gate.ts +6 -7
- package/src/kernel/boot-bind/honor-config.test.ts +42 -42
- package/src/kernel/boot-bind/journal.ts +5 -5
- package/src/kernel/boot-bind/signal.ts +3 -1
- package/src/kernel/boot-bind/store.test.ts +55 -37
- package/src/kernel/boot-bind/store.ts +24 -34
- package/src/kernel/boot-bind/vault.ts +19 -4
- package/src/kernel/boot.test.ts +12 -4
- package/src/kernel/boot.ts +31 -19
- package/src/kernel/budget-entry.ts +1 -1
- package/src/kernel/compensate.test.ts +288 -1
- package/src/kernel/compensate.ts +126 -0
- package/src/kernel/concurrency.test.ts +10 -9
- package/src/kernel/effects-stamping.test.ts +27 -2
- package/src/kernel/element-registries.ts +5 -3
- package/src/kernel/flow.ts +4 -2
- package/src/kernel/fx.test.ts +119 -3
- package/src/kernel/fx.ts +215 -40
- package/src/kernel/index.ts +14 -0
- package/src/kernel/journal.ts +130 -7
- package/src/kernel/pipeline.test.ts +20 -6
- package/src/kernel/redacted.test.ts +35 -0
- package/src/kernel/redacted.ts +24 -2
- package/src/kernel/router/create-default.ts +16 -0
- package/src/kernel/router/create-edge.ts +18 -0
- package/src/kernel/router/create.ts +23 -0
- package/src/kernel/router/index.ts +23 -0
- package/src/kernel/router/linear.ts +88 -0
- package/src/kernel/router/regexp.ts +188 -0
- package/src/kernel/router/smart.ts +120 -0
- package/src/kernel/router/trie.ts +108 -0
- package/src/kernel/router/types.ts +56 -0
- package/src/kernel/router.ts +18 -532
- package/src/kernel-entry.ts +43 -0
- package/src/manifest/fixtures/skyport.excerpt.json +1 -1
- package/src/manifest/fixtures/skyport.manifest.json +1 -1
- package/src/plugins/config-source.test.ts +7 -7
- package/src/release/build-lib.ts +58 -0
- package/src/release/exports.test.ts +4 -2
- package/src/release/exports.ts +30 -4
- package/src/release/measure.ts +101 -22
- package/src/runs/duckdb.ts +7 -2
- package/src/shared/optional-peer.ts +41 -0
- package/src/term.test.ts +3 -44
- package/src/test/create-test-app.ts +12 -0
- package/src/test/provisions.integration.test.ts +1 -1
- package/src/test/reset-element-registries.ts +2 -0
- package/src/upgrade/codemods.test.ts +7 -20
- package/src/upgrade/codemods.ts +93 -60
- package/site/content/docs/console/access.mdx +0 -29
- package/site/content/docs/console/ai.mdx +0 -35
- package/site/content/docs/console/architecture.mdx +0 -35
- package/site/content/docs/console/channels.mdx +0 -37
- package/site/content/docs/console/clock.mdx +0 -51
- package/site/content/docs/console/flows.mdx +0 -31
- package/site/content/docs/console/gates.mdx +0 -73
- package/site/content/docs/console/index.mdx +0 -54
- package/site/content/docs/console/manifest-diff.mdx +0 -34
- package/site/content/docs/console/meta.json +0 -24
- package/site/content/docs/console/overview.mdx +0 -40
- package/site/content/docs/console/plugins.mdx +0 -41
- package/site/content/docs/console/privacy.mdx +0 -32
- package/site/content/docs/console/runs.mdx +0 -50
- package/site/content/docs/console/signals.mdx +0 -40
- package/site/content/docs/console/store.mdx +0 -32
- package/site/content/docs/console/tenancy.mdx +0 -32
- package/site/content/docs/console/traces.mdx +0 -38
- package/site/content/docs/console/vault.mdx +0 -41
- package/site/content/docs/recipes/openbao.mdx +0 -141
- package/src/cli/ask-dev-mode.ts +0 -40
- package/src/cli/dev-mode.test.ts +0 -45
- package/src/cli/dev-mode.ts +0 -78
- package/src/cli/openbao-bootstrap.test.ts +0 -150
- package/src/cli/openbao-bootstrap.ts +0 -321
- package/src/cli/openbao-restart.integration.test.ts +0 -145
- package/src/console/ui/dist/assets/panel-vault-Za1GyJfM.js +0 -1
- package/src/console/ui/dist/assets/style-C8MxEWPd.css +0 -3
- package/src/docker/recipes/openbao.ts +0 -47
- package/src/drivers/libsql.ts +0 -179
- package/src/drivers/sqlite.ts +0 -44
- package/src/drivers/vault-openbao.test.ts +0 -97
- package/src/drivers/vault-openbao.ts +0 -143
|
@@ -7,8 +7,11 @@ import { mkdtemp } from "node:fs/promises";
|
|
|
7
7
|
import { tmpdir } from "node:os";
|
|
8
8
|
import { join } from "node:path";
|
|
9
9
|
import {
|
|
10
|
-
|
|
10
|
+
COMPOSE_BASE,
|
|
11
11
|
COMPOSE_OVERRIDE,
|
|
12
|
+
DOCKER_COMPOSE,
|
|
13
|
+
DOCKER_COMPOSE_OVERRIDE,
|
|
14
|
+
DOCKER_STACK,
|
|
12
15
|
assertNoCredentialsInYaml,
|
|
13
16
|
buildCaddyfile,
|
|
14
17
|
buildPgDogToml,
|
|
@@ -120,7 +123,7 @@ describe("image recipes", () => {
|
|
|
120
123
|
images: { "store.kv": image },
|
|
121
124
|
credentials: { "store.kv": fixedCreds["store.kv"] },
|
|
122
125
|
});
|
|
123
|
-
const kvYml = derived.files.find((f) => f.path ===
|
|
126
|
+
const kvYml = derived.files.find((f) => f.path === DOCKER_COMPOSE)?.content ?? "";
|
|
124
127
|
expect(kvYml).toContain(image);
|
|
125
128
|
expect(kvYml).toContain("valkey-server");
|
|
126
129
|
expect(derived.stackEnv.REDIS_URL).toContain("redis://");
|
|
@@ -160,7 +163,7 @@ describe("image recipes", () => {
|
|
|
160
163
|
images: { "store.kv": image },
|
|
161
164
|
credentials: { "store.kv": fixedCreds["store.kv"] },
|
|
162
165
|
});
|
|
163
|
-
const kvYml = derived.files.find((f) => f.path ===
|
|
166
|
+
const kvYml = derived.files.find((f) => f.path === DOCKER_COMPOSE)?.content ?? "";
|
|
164
167
|
expect(kvYml).toContain(image);
|
|
165
168
|
expect(kvYml).toContain("memlock");
|
|
166
169
|
expect(kvYml).toContain("HEALTHCHECK_PORT");
|
|
@@ -201,7 +204,7 @@ describe("image recipes", () => {
|
|
|
201
204
|
});
|
|
202
205
|
expect(derived.specs[0]!.port).toBe(26257);
|
|
203
206
|
expect(derived.specs[0]!.hostPort).toBe(5432);
|
|
204
|
-
const sqlYml = derived.files.find((f) => f.path ===
|
|
207
|
+
const sqlYml = derived.files.find((f) => f.path === DOCKER_COMPOSE)?.content ?? "";
|
|
205
208
|
expect(sqlYml).toContain("26257");
|
|
206
209
|
expect(sqlYml).toContain("COCKROACH_");
|
|
207
210
|
expect(derived.stackEnv.DATABASE_URL).toContain("sslmode=require");
|
|
@@ -237,7 +240,7 @@ describe("image recipes", () => {
|
|
|
237
240
|
credentials: { "store.sql": fixedCreds["store.sql"] },
|
|
238
241
|
});
|
|
239
242
|
expect(derived.specs[0]!.port).toBe(5433);
|
|
240
|
-
const sqlYml = derived.files.find((f) => f.path ===
|
|
243
|
+
const sqlYml = derived.files.find((f) => f.path === DOCKER_COMPOSE)?.content ?? "";
|
|
241
244
|
expect(sqlYml).toContain("5433");
|
|
242
245
|
expect(sqlYml).toContain("YSQL_");
|
|
243
246
|
});
|
|
@@ -251,7 +254,7 @@ describe("image recipes", () => {
|
|
|
251
254
|
images: { "store.sql": image },
|
|
252
255
|
credentials: { "store.sql": fixedCreds["store.sql"] },
|
|
253
256
|
});
|
|
254
|
-
const sqlYml = derived.files.find((f) => f.path ===
|
|
257
|
+
const sqlYml = derived.files.find((f) => f.path === DOCKER_COMPOSE)?.content ?? "";
|
|
255
258
|
expect(sqlYml).toContain("POSTGRES_USER");
|
|
256
259
|
expect(derived.stackEnv.DATABASE_URL).toContain("postgres://");
|
|
257
260
|
});
|
|
@@ -268,8 +271,8 @@ describe("image recipes", () => {
|
|
|
268
271
|
credentials: fixedCreds["store.sql"],
|
|
269
272
|
});
|
|
270
273
|
expect(applied.dependsOn?.["store-sql"]?.condition).toBe("service_healthy");
|
|
271
|
-
expect(applied.volumes).toContain("./pgdog.toml:/pgdog/pgdog.toml:ro");
|
|
272
|
-
expect(applied.volumes).toContain("./users.toml:/pgdog/users.toml:ro");
|
|
274
|
+
expect(applied.volumes).toContain("./pgdog/pgdog.toml:/pgdog/pgdog.toml:ro");
|
|
275
|
+
expect(applied.volumes).toContain("./pgdog/users.toml:/pgdog/users.toml:ro");
|
|
273
276
|
expect(applied.healthcheck?.test.join(" ")).toContain("pg_isready");
|
|
274
277
|
});
|
|
275
278
|
|
|
@@ -605,7 +608,7 @@ describe("image recipes", () => {
|
|
|
605
608
|
});
|
|
606
609
|
|
|
607
610
|
describe("deriveInfrastructure", () => {
|
|
608
|
-
test("emits Dockerfile +
|
|
611
|
+
test("emits Dockerfile + single docker-compose.yml by default", () => {
|
|
609
612
|
const result = deriveInfrastructure({
|
|
610
613
|
images: {
|
|
611
614
|
"store.sql": "pgvector/pgvector:pg17",
|
|
@@ -617,31 +620,24 @@ describe("deriveInfrastructure", () => {
|
|
|
617
620
|
|
|
618
621
|
const paths = result.files.map((f) => f.path);
|
|
619
622
|
expect(paths).toContain("Dockerfile");
|
|
620
|
-
expect(paths).toContain(
|
|
621
|
-
expect(paths).toContain(
|
|
622
|
-
expect(paths).toContain("compose.store.
|
|
623
|
-
expect(paths).toContain(COMPOSE_ALL);
|
|
623
|
+
expect(paths).toContain(DOCKER_COMPOSE);
|
|
624
|
+
expect(paths).not.toContain(COMPOSE_BASE);
|
|
625
|
+
expect(paths).not.toContain("compose.store.sql.yml");
|
|
624
626
|
expect(paths).not.toContain(COMPOSE_OVERRIDE);
|
|
627
|
+
expect(paths).not.toContain(DOCKER_COMPOSE_OVERRIDE);
|
|
625
628
|
|
|
626
|
-
expect(result.composeFiles).toEqual([
|
|
627
|
-
"compose.yml",
|
|
628
|
-
"compose.store.kv.yml",
|
|
629
|
-
"compose.store.sql.yml",
|
|
630
|
-
COMPOSE_OVERRIDE,
|
|
631
|
-
]);
|
|
632
|
-
expect(result.composeFiles).not.toContain(COMPOSE_ALL);
|
|
633
|
-
|
|
634
|
-
const sqlYml = result.files.find((f) => f.path === "compose.store.sql.yml")!.content;
|
|
635
|
-
expect(sqlYml).toContain("pgvector/pgvector:pg17");
|
|
636
|
-
expect(sqlYml).toContain("POSTGRES_PASSWORD");
|
|
637
|
-
expect(sqlYml).toContain("${OKE_STORE_SQL_PASSWORD}");
|
|
638
|
-
expect(sqlYml).toContain(".env.docker");
|
|
639
|
-
expect(sqlYml).not.toContain(fixedCreds["store.sql"].password);
|
|
629
|
+
expect(result.composeFiles).toEqual([DOCKER_COMPOSE, DOCKER_COMPOSE_OVERRIDE]);
|
|
640
630
|
|
|
641
|
-
const
|
|
642
|
-
expect(
|
|
643
|
-
expect(
|
|
644
|
-
expect(
|
|
631
|
+
const yml = result.files.find((f) => f.path === DOCKER_COMPOSE)!.content;
|
|
632
|
+
expect(yml).toContain("pgvector/pgvector:pg17");
|
|
633
|
+
expect(yml).toContain("valkey/valkey:8-alpine");
|
|
634
|
+
expect(yml).toContain("POSTGRES_PASSWORD");
|
|
635
|
+
expect(yml).toContain("${OKE_STORE_SQL_PASSWORD}");
|
|
636
|
+
expect(yml).toContain(".env.docker");
|
|
637
|
+
expect(yml).not.toContain(fixedCreds["store.sql"].password);
|
|
638
|
+
expect(yml).toContain('context: ".."');
|
|
639
|
+
expect(yml).toContain("app:");
|
|
640
|
+
expect(yml).toContain("oke-skyport:latest");
|
|
645
641
|
|
|
646
642
|
for (const f of result.files) {
|
|
647
643
|
assertNoCredentialsInYaml(f.content, Object.values(fixedCreds));
|
|
@@ -651,6 +647,32 @@ describe("deriveInfrastructure", () => {
|
|
|
651
647
|
expect(result.stackEnv.OKE_STORE_SQL_PASSWORD).toBe(fixedCreds["store.sql"].password);
|
|
652
648
|
});
|
|
653
649
|
|
|
650
|
+
test("compose YAML spaces services and adds role comments", () => {
|
|
651
|
+
const result = deriveInfrastructure({
|
|
652
|
+
images: {
|
|
653
|
+
ai: "ghcr.io/ggml-org/llama.cpp:server",
|
|
654
|
+
"channel.email": "axllent/mailpit:v1.22.3",
|
|
655
|
+
pgdog: "ghcr.io/pgdogdev/pgdog:v0.1.51",
|
|
656
|
+
"store.sql": "postgres:16",
|
|
657
|
+
},
|
|
658
|
+
credentials: { "store.sql": fixedCreds["store.sql"] },
|
|
659
|
+
includeApp: false,
|
|
660
|
+
app: "dev",
|
|
661
|
+
});
|
|
662
|
+
const yml = result.files.find((f) => f.path === DOCKER_COMPOSE)!.content;
|
|
663
|
+
expect(yml).toContain("# Generated by `oke docker`");
|
|
664
|
+
expect(yml).toContain("# ai — local inference");
|
|
665
|
+
expect(yml).toContain("# channel.email — Mailpit");
|
|
666
|
+
expect(yml).toContain("# pgdog — connection pooler");
|
|
667
|
+
expect(yml).toContain("# store.sql — Postgres");
|
|
668
|
+
// Blank line between adjacent service blocks (comment → key → … → blank → comment).
|
|
669
|
+
expect(yml).toMatch(/# ai —[^\n]+\n ai:\n[\s\S]+\n\n # channel\.email —/);
|
|
670
|
+
expect(yml).toMatch(/\nnetworks:\n[\s\S]+\n\nservices:\n/);
|
|
671
|
+
expect(serviceNamesFromComposeYaml(yml)).toEqual(
|
|
672
|
+
new Set(["ai", "channel-email", "pgdog", "store-sql"]),
|
|
673
|
+
);
|
|
674
|
+
});
|
|
675
|
+
|
|
654
676
|
test("includeApp false omits app service (infra-only stack)", () => {
|
|
655
677
|
const result = deriveInfrastructure({
|
|
656
678
|
images: { "store.sql": "postgres:16" },
|
|
@@ -658,30 +680,75 @@ describe("deriveInfrastructure", () => {
|
|
|
658
680
|
includeApp: false,
|
|
659
681
|
app: "dev",
|
|
660
682
|
});
|
|
661
|
-
const base = result.files.find((f) => f.path ===
|
|
683
|
+
const base = result.files.find((f) => f.path === DOCKER_COMPOSE)!.content;
|
|
662
684
|
expect(base).toContain("oke-dev");
|
|
663
685
|
expect(base).toContain("networks:");
|
|
664
686
|
expect(base).not.toContain("app:");
|
|
665
687
|
expect(base).not.toContain("build:");
|
|
666
688
|
});
|
|
667
689
|
|
|
668
|
-
test("prod
|
|
690
|
+
test("prod folds readiness + budgeted deploy into docker-compose.yml", () => {
|
|
669
691
|
const result = deriveInfrastructure({
|
|
670
692
|
images: { "store.sql": "postgres:16" },
|
|
671
693
|
credentials: { "store.sql": fixedCreds["store.sql"] },
|
|
672
694
|
prod: true,
|
|
695
|
+
serverCpus: 4,
|
|
696
|
+
serverMemoryGb: 8,
|
|
673
697
|
});
|
|
698
|
+
expect(result.files.some((f) => f.path === "compose.prod.yml")).toBe(false);
|
|
699
|
+
expect(result.composeFiles).toEqual([DOCKER_COMPOSE, DOCKER_COMPOSE_OVERRIDE]);
|
|
700
|
+
const yml = result.files.find((f) => f.path === DOCKER_COMPOSE)!.content;
|
|
701
|
+
expect(yml).toContain("replicas");
|
|
702
|
+
expect(yml).toContain("/_/ready");
|
|
703
|
+
expect(yml).toContain("update_config");
|
|
704
|
+
expect(yml).toContain("restart_policy");
|
|
705
|
+
expect(yml).toContain("stop_grace_period");
|
|
706
|
+
expect(yml).toContain("start-first");
|
|
707
|
+
expect(yml).toContain("on-failure");
|
|
708
|
+
expect(yml).toContain("cpus:");
|
|
709
|
+
expect(yml).toContain("memory:");
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
test("layout split emits per-role files + compose.prod.yml", () => {
|
|
713
|
+
const result = deriveInfrastructure({
|
|
714
|
+
images: { "store.sql": "postgres:16" },
|
|
715
|
+
credentials: { "store.sql": fixedCreds["store.sql"] },
|
|
716
|
+
prod: true,
|
|
717
|
+
layout: "split",
|
|
718
|
+
});
|
|
719
|
+
expect(result.files.some((f) => f.path === COMPOSE_BASE)).toBe(true);
|
|
720
|
+
expect(result.files.some((f) => f.path === "compose.store.sql.yml")).toBe(true);
|
|
674
721
|
expect(result.files.some((f) => f.path === "compose.prod.yml")).toBe(true);
|
|
675
|
-
expect(result.
|
|
676
|
-
expect(result.composeFiles
|
|
722
|
+
expect(result.files.some((f) => f.path === DOCKER_COMPOSE)).toBe(false);
|
|
723
|
+
expect(result.composeFiles).toEqual([
|
|
724
|
+
COMPOSE_BASE,
|
|
725
|
+
"compose.store.sql.yml",
|
|
726
|
+
"compose.prod.yml",
|
|
727
|
+
COMPOSE_OVERRIDE,
|
|
728
|
+
]);
|
|
677
729
|
const prod = result.files.find((f) => f.path === "compose.prod.yml")!.content;
|
|
678
730
|
expect(prod).toContain("replicas");
|
|
679
731
|
expect(prod).toContain("/_/ready");
|
|
680
|
-
expect(prod).toContain("
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
732
|
+
expect(prod).toContain("cpus:");
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
test("layout stack emits docker-stack.yml with overlay network", () => {
|
|
736
|
+
const result = deriveInfrastructure({
|
|
737
|
+
images: { "store.sql": "postgres:16" },
|
|
738
|
+
credentials: { "store.sql": fixedCreds["store.sql"] },
|
|
739
|
+
prod: true,
|
|
740
|
+
layout: "stack",
|
|
741
|
+
app: "skyport",
|
|
742
|
+
});
|
|
743
|
+
expect(result.files.map((f) => f.path)).toContain(DOCKER_STACK);
|
|
744
|
+
expect(result.files.some((f) => f.path === DOCKER_COMPOSE)).toBe(false);
|
|
745
|
+
expect(result.composeFiles).toEqual([DOCKER_STACK]);
|
|
746
|
+
const yml = result.files.find((f) => f.path === DOCKER_STACK)!.content;
|
|
747
|
+
expect(yml).toContain("overlay");
|
|
748
|
+
expect(yml).toContain("oke-skyport:latest");
|
|
749
|
+
expect(yml).not.toContain("build:");
|
|
750
|
+
expect(yml).toContain("/_/ready");
|
|
751
|
+
expect(yml).toContain("cpus:");
|
|
685
752
|
});
|
|
686
753
|
|
|
687
754
|
test("when postgres + pgdog are both pinned, DATABASE_URL points at PgDog", () => {
|
|
@@ -702,18 +769,18 @@ describe("deriveInfrastructure", () => {
|
|
|
702
769
|
// Direct Postgres URL stays on the role key for ops / escape hatch.
|
|
703
770
|
expect(result.stackEnv.OKE_STORE_SQL_URL).toContain(":5432/");
|
|
704
771
|
|
|
705
|
-
const pgdogYml = result.files.find((f) => f.path ===
|
|
772
|
+
const pgdogYml = result.files.find((f) => f.path === DOCKER_COMPOSE)!.content;
|
|
706
773
|
expect(pgdogYml).toContain("ghcr.io/pgdogdev/pgdog:v0.1.51");
|
|
707
774
|
expect(pgdogYml).toContain("store-sql");
|
|
708
775
|
expect(pgdogYml).toContain("service_healthy");
|
|
709
776
|
expect(pgdogYml).not.toContain(fixedCreds["store.sql"].password);
|
|
710
777
|
|
|
711
|
-
const pgdogToml = result.files.find((f) => f.path === "pgdog.toml")!.content;
|
|
778
|
+
const pgdogToml = result.files.find((f) => f.path === "pgdog/pgdog.toml")!.content;
|
|
712
779
|
expect(pgdogToml).toContain('pooler_mode = "transaction"');
|
|
713
780
|
expect(pgdogToml).toContain('host = "store-sql"');
|
|
714
781
|
expect(pgdogToml).not.toContain(fixedCreds["store.sql"].password);
|
|
715
782
|
|
|
716
|
-
const usersToml = result.files.find((f) => f.path === "users.toml")!.content;
|
|
783
|
+
const usersToml = result.files.find((f) => f.path === "pgdog/users.toml")!.content;
|
|
717
784
|
expect(usersToml).toContain('password = "s3cret-sql-password-xyz"');
|
|
718
785
|
expect(usersToml).toContain('database = "oke"');
|
|
719
786
|
});
|
|
@@ -725,7 +792,7 @@ describe("deriveInfrastructure", () => {
|
|
|
725
792
|
});
|
|
726
793
|
expect(result.stackEnv.DATABASE_URL).toContain(":5432/");
|
|
727
794
|
expect(result.stackEnv.OKE_PGDOG_URL).toBeUndefined();
|
|
728
|
-
expect(result.files.some((f) => f.path === "pgdog.toml")).toBe(false);
|
|
795
|
+
expect(result.files.some((f) => f.path === "pgdog/pgdog.toml")).toBe(false);
|
|
729
796
|
});
|
|
730
797
|
|
|
731
798
|
test("Dockerfile CMD is oke start", () => {
|
|
@@ -746,7 +813,7 @@ describe("deriveInfrastructure", () => {
|
|
|
746
813
|
});
|
|
747
814
|
expect(written.some((p) => p.endsWith(COMPOSE_OVERRIDE))).toBe(false);
|
|
748
815
|
expect(await Bun.file(join(dockerDir, ".env.docker")).exists()).toBe(true);
|
|
749
|
-
expect(await Bun.file(join(dockerDir,
|
|
816
|
+
expect(await Bun.file(join(dockerDir, DOCKER_COMPOSE)).exists()).toBe(true);
|
|
750
817
|
const envText = await Bun.file(join(dockerDir, ".env.docker")).text();
|
|
751
818
|
expect(envText).toContain("DATABASE_URL=");
|
|
752
819
|
expect(envText).toContain("# docker/.env.docker — generated by");
|
|
@@ -762,7 +829,7 @@ describe("deriveInfrastructure", () => {
|
|
|
762
829
|
},
|
|
763
830
|
app: "skyport",
|
|
764
831
|
});
|
|
765
|
-
const yml = result.files.find((f) => f.path ===
|
|
832
|
+
const yml = result.files.find((f) => f.path === DOCKER_COMPOSE)!.content;
|
|
766
833
|
expect(yml).toContain("getmeili/meilisearch:v1.37");
|
|
767
834
|
expect(yml).toContain("${OKE_STORE_INDEX_KEY}");
|
|
768
835
|
expect(yml).toContain("meili_data");
|
|
@@ -777,7 +844,7 @@ describe("deriveInfrastructure", () => {
|
|
|
777
844
|
images: { ai: OLLAMA_IMAGE },
|
|
778
845
|
app: "skyport",
|
|
779
846
|
});
|
|
780
|
-
const yml = result.files.find((f) => f.path ===
|
|
847
|
+
const yml = result.files.find((f) => f.path === DOCKER_COMPOSE)!.content;
|
|
781
848
|
expect(yml).toContain(OLLAMA_IMAGE);
|
|
782
849
|
expect(yml).toContain("OKE_AI_MODEL");
|
|
783
850
|
expect(yml).toContain("qwen3.5:9b");
|
|
@@ -795,7 +862,7 @@ describe("deriveInfrastructure", () => {
|
|
|
795
862
|
app: "skyport",
|
|
796
863
|
controls: { OKE_AI_MODEL: "gemma4:e4b-q4_K_M" },
|
|
797
864
|
});
|
|
798
|
-
const yml = result.files.find((f) => f.path ===
|
|
865
|
+
const yml = result.files.find((f) => f.path === DOCKER_COMPOSE)!.content;
|
|
799
866
|
expect(yml).toContain(LLAMA_CPP_IMAGE);
|
|
800
867
|
expect(yml).toContain("127.0.0.1:8080:8080");
|
|
801
868
|
expect(yml).toContain("llama-entrypoint.py");
|
|
@@ -814,8 +881,8 @@ describe("deriveInfrastructure", () => {
|
|
|
814
881
|
|
|
815
882
|
test("ai vllm and sglang emit loopback publish + GPU deploy", () => {
|
|
816
883
|
for (const [image, port, path] of [
|
|
817
|
-
[VLLM_IMAGE, 8000,
|
|
818
|
-
[SGLANG_IMAGE, 30000,
|
|
884
|
+
[VLLM_IMAGE, 8000, DOCKER_COMPOSE],
|
|
885
|
+
[SGLANG_IMAGE, 30000, DOCKER_COMPOSE],
|
|
819
886
|
] as const) {
|
|
820
887
|
const result = deriveInfrastructure({ images: { ai: image }, app: "skyport" });
|
|
821
888
|
const yml = result.files.find((f) => f.path === path)!.content;
|
|
@@ -947,18 +1014,18 @@ describe("deriveInfrastructure", () => {
|
|
|
947
1014
|
credentials: { "store.sql": fixedCreds["store.sql"] },
|
|
948
1015
|
app: "skyport",
|
|
949
1016
|
});
|
|
950
|
-
const
|
|
951
|
-
expect(
|
|
952
|
-
expect(
|
|
953
|
-
expect(
|
|
954
|
-
expect(
|
|
955
|
-
expect(
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
expect(
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
expect(
|
|
1017
|
+
const yml = result.files.find((f) => f.path === DOCKER_COMPOSE)!.content;
|
|
1018
|
+
expect(yml).toContain("app:");
|
|
1019
|
+
expect(yml).not.toContain("6530:6530");
|
|
1020
|
+
expect(yml).toContain("proxy:");
|
|
1021
|
+
expect(yml).toContain("store-sql");
|
|
1022
|
+
expect(yml).toContain("caddy:2-alpine");
|
|
1023
|
+
expect(yml).toContain("80:80");
|
|
1024
|
+
expect(yml).toContain("443:443");
|
|
1025
|
+
expect(yml).toContain("./Caddyfile:/etc/caddy/Caddyfile:ro");
|
|
1026
|
+
// App depends_on must not wait on the edge proxy.
|
|
1027
|
+
const appBlock = yml.split("store-sql:")[0]!;
|
|
1028
|
+
expect(appBlock).not.toMatch(/depends_on:[\s\S]*proxy/);
|
|
962
1029
|
|
|
963
1030
|
const caddyfile = result.files.find((f) => f.path === "Caddyfile")!.content;
|
|
964
1031
|
expect(caddyfile).toContain("reverse_proxy app:6530");
|
|
@@ -977,27 +1044,24 @@ describe("deriveInfrastructure", () => {
|
|
|
977
1044
|
app: "skyport",
|
|
978
1045
|
prod: true,
|
|
979
1046
|
});
|
|
980
|
-
const
|
|
981
|
-
expect(
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
expect(
|
|
985
|
-
expect(
|
|
986
|
-
expect(
|
|
987
|
-
expect(
|
|
988
|
-
expect(
|
|
989
|
-
expect(
|
|
990
|
-
expect(proxyYml).toContain("traefik.http.routers.app.rule");
|
|
991
|
-
expect(proxyYml).toContain("loadbalancer.server.port");
|
|
1047
|
+
const yml = result.files.find((f) => f.path === DOCKER_COMPOSE)!.content;
|
|
1048
|
+
expect(yml).not.toContain("6530:6530");
|
|
1049
|
+
expect(yml).toContain("traefik:v3.3");
|
|
1050
|
+
expect(yml).toContain(SOCKET_PROXY_IMAGE);
|
|
1051
|
+
expect(yml).toContain(SOCKET_PROXY_SERVICE);
|
|
1052
|
+
expect(yml).toContain("/var/run/docker.sock:/var/run/docker.sock:ro");
|
|
1053
|
+
expect(yml).toContain("tcp://socket-proxy:2375");
|
|
1054
|
+
expect(yml).toContain("traefik.enable");
|
|
1055
|
+
expect(yml).toContain("traefik.http.routers.app.rule");
|
|
1056
|
+
expect(yml).toContain("loadbalancer.server.port");
|
|
992
1057
|
// Raw socket must not be on the Traefik service itself.
|
|
993
|
-
const traefikBlock =
|
|
1058
|
+
const traefikBlock = yml.split("socket-proxy:")[0]!;
|
|
994
1059
|
expect(traefikBlock).toContain("traefik:v3.3");
|
|
995
1060
|
expect(traefikBlock).not.toContain("docker.sock");
|
|
996
1061
|
|
|
997
1062
|
expect(result.files.some((f) => f.path === "Caddyfile")).toBe(false);
|
|
998
1063
|
expect(result.stackEnv.OKE_PROXY_URL).toBe("https://127.0.0.1");
|
|
999
|
-
expect(result.composeFiles).
|
|
1000
|
-
expect(result.composeFiles).toContain("compose.prod.yml");
|
|
1064
|
+
expect(result.composeFiles).toEqual([DOCKER_COMPOSE, DOCKER_COMPOSE_OVERRIDE]);
|
|
1001
1065
|
});
|
|
1002
1066
|
|
|
1003
1067
|
test("without proxy role, app still publishes 6530 (default unchanged)", () => {
|
|
@@ -1006,13 +1070,13 @@ describe("deriveInfrastructure", () => {
|
|
|
1006
1070
|
credentials: { "store.sql": fixedCreds["store.sql"] },
|
|
1007
1071
|
app: "skyport",
|
|
1008
1072
|
});
|
|
1009
|
-
const base = result.files.find((f) => f.path ===
|
|
1073
|
+
const base = result.files.find((f) => f.path === DOCKER_COMPOSE)!.content;
|
|
1010
1074
|
expect(base).toContain("6530:6530");
|
|
1011
|
-
expect(
|
|
1075
|
+
expect(base).not.toContain("\n proxy:");
|
|
1012
1076
|
expect(result.stackEnv.OKE_PROXY_URL).toBeUndefined();
|
|
1013
1077
|
});
|
|
1014
1078
|
|
|
1015
|
-
test("compose.
|
|
1079
|
+
test("docker-compose.yml includes every role + companion service when prod", () => {
|
|
1016
1080
|
const result = deriveInfrastructure({
|
|
1017
1081
|
images: {
|
|
1018
1082
|
"store.sql": "postgres:16",
|
|
@@ -1027,35 +1091,17 @@ describe("deriveInfrastructure", () => {
|
|
|
1027
1091
|
prod: true,
|
|
1028
1092
|
});
|
|
1029
1093
|
|
|
1030
|
-
const
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
expect(expected.size).toBeGreaterThan(0);
|
|
1045
|
-
for (const name of expected) {
|
|
1046
|
-
expect(allServices.has(name)).toBe(true);
|
|
1047
|
-
}
|
|
1048
|
-
// Companion + role services survive the merge (not dropped).
|
|
1049
|
-
expect(allServices.has("app")).toBe(true);
|
|
1050
|
-
expect(allServices.has("store-sql")).toBe(true);
|
|
1051
|
-
expect(allServices.has("store-kv")).toBe(true);
|
|
1052
|
-
expect(allServices.has("proxy")).toBe(true);
|
|
1053
|
-
expect(allServices.has(SOCKET_PROXY_SERVICE)).toBe(true);
|
|
1054
|
-
// Prod overlay fields land on app in the merged document.
|
|
1055
|
-
expect(allYml).toContain("/_/ready");
|
|
1056
|
-
expect(allYml).toContain("stop_grace_period");
|
|
1057
|
-
expect(allYml).toContain("update_config");
|
|
1058
|
-
// Still not in the layered `-f` list.
|
|
1059
|
-
expect(result.composeFiles).not.toContain(COMPOSE_ALL);
|
|
1094
|
+
const yml = result.files.find((f) => f.path === DOCKER_COMPOSE)!.content;
|
|
1095
|
+
const services = serviceNamesFromComposeYaml(yml);
|
|
1096
|
+
expect(services.has("app")).toBe(true);
|
|
1097
|
+
expect(services.has("store-sql")).toBe(true);
|
|
1098
|
+
expect(services.has("store-kv")).toBe(true);
|
|
1099
|
+
expect(services.has("proxy")).toBe(true);
|
|
1100
|
+
expect(services.has(SOCKET_PROXY_SERVICE)).toBe(true);
|
|
1101
|
+
expect(yml).toContain("/_/ready");
|
|
1102
|
+
expect(yml).toContain("stop_grace_period");
|
|
1103
|
+
expect(yml).toContain("update_config");
|
|
1104
|
+
expect(yml).toContain("cpus:");
|
|
1105
|
+
expect(result.composeFiles).toEqual([DOCKER_COMPOSE, DOCKER_COMPOSE_OVERRIDE]);
|
|
1060
1106
|
});
|
|
1061
1107
|
});
|
package/src/docker/helpers.ts
CHANGED
|
@@ -160,3 +160,101 @@ export function toYaml(value: unknown, indent = 0): string {
|
|
|
160
160
|
}
|
|
161
161
|
return yamlQuote(String(value));
|
|
162
162
|
}
|
|
163
|
+
|
|
164
|
+
/** Preferred top-level key order for Compose documents. */
|
|
165
|
+
const COMPOSE_TOP_LEVEL_ORDER = [
|
|
166
|
+
"name",
|
|
167
|
+
"networks",
|
|
168
|
+
"services",
|
|
169
|
+
"volumes",
|
|
170
|
+
"secrets",
|
|
171
|
+
"configs",
|
|
172
|
+
] as const;
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Options for {@link composeToYaml}.
|
|
176
|
+
*/
|
|
177
|
+
export type ComposeYamlOptions = {
|
|
178
|
+
/**
|
|
179
|
+
* File header comments (without leading `# ` prefix — pass full `# …` lines).
|
|
180
|
+
* Pass an empty array to omit the header.
|
|
181
|
+
*/
|
|
182
|
+
readonly header?: readonly string[];
|
|
183
|
+
/**
|
|
184
|
+
* Per-service comment body (without `#`). When set, each service is preceded
|
|
185
|
+
* by ` # <comment>` and a blank line separates services.
|
|
186
|
+
*/
|
|
187
|
+
readonly serviceComment?: (serviceName: string) => string | undefined;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Render a Compose document with readable gaps: blank lines between top-level
|
|
192
|
+
* sections and between services, plus optional per-service comments.
|
|
193
|
+
*
|
|
194
|
+
* @param doc - Compose mapping (`name`, `networks`, `services`, …)
|
|
195
|
+
* @param options - Header / service comment hooks
|
|
196
|
+
*/
|
|
197
|
+
export function composeToYaml(
|
|
198
|
+
doc: Readonly<Record<string, unknown>>,
|
|
199
|
+
options: ComposeYamlOptions = {},
|
|
200
|
+
): string {
|
|
201
|
+
const header = options.header ?? [
|
|
202
|
+
"# Generated by `oke docker` / `oke dev --docker`.",
|
|
203
|
+
"# Local overrides: docker-compose.override.yml (do not commit secrets).",
|
|
204
|
+
];
|
|
205
|
+
const lines: string[] = [...header];
|
|
206
|
+
if (header.length > 0) lines.push("");
|
|
207
|
+
|
|
208
|
+
const present = Object.keys(doc).filter((k) => doc[k] !== undefined);
|
|
209
|
+
const ordered = [
|
|
210
|
+
...COMPOSE_TOP_LEVEL_ORDER.filter((k) => present.includes(k)),
|
|
211
|
+
...present.filter((k) => !(COMPOSE_TOP_LEVEL_ORDER as readonly string[]).includes(k)),
|
|
212
|
+
];
|
|
213
|
+
|
|
214
|
+
for (let i = 0; i < ordered.length; i++) {
|
|
215
|
+
const key = ordered[i]!;
|
|
216
|
+
const value = doc[key];
|
|
217
|
+
if (i > 0) lines.push("");
|
|
218
|
+
|
|
219
|
+
if (key === "services" && isPlainObject(value)) {
|
|
220
|
+
lines.push("services:");
|
|
221
|
+
const services = Object.entries(value).filter(([, v]) => v !== undefined);
|
|
222
|
+
for (let j = 0; j < services.length; j++) {
|
|
223
|
+
const [name, body] = services[j]!;
|
|
224
|
+
if (j > 0) lines.push("");
|
|
225
|
+
const comment = options.serviceComment?.(name);
|
|
226
|
+
if (comment) lines.push(` # ${comment}`);
|
|
227
|
+
if (body !== null && typeof body === "object") {
|
|
228
|
+
const nested = toYaml(body, 2);
|
|
229
|
+
if (nested === "{}" || nested === "[]") {
|
|
230
|
+
lines.push(` ${name}: ${nested}`);
|
|
231
|
+
} else {
|
|
232
|
+
lines.push(` ${name}:`);
|
|
233
|
+
lines.push(nested);
|
|
234
|
+
}
|
|
235
|
+
} else {
|
|
236
|
+
lines.push(` ${name}: ${toYaml(body, 0)}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (value !== null && typeof value === "object") {
|
|
243
|
+
const nested = toYaml(value, 1);
|
|
244
|
+
if (nested === "{}" || nested === "[]") {
|
|
245
|
+
lines.push(`${key}: ${nested}`);
|
|
246
|
+
} else {
|
|
247
|
+
lines.push(`${key}:`);
|
|
248
|
+
lines.push(nested);
|
|
249
|
+
}
|
|
250
|
+
} else {
|
|
251
|
+
lines.push(`${key}: ${toYaml(value, 0)}`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return `${lines.join("\n")}\n`;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
259
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
260
|
+
}
|