okengine 0.9.1 → 0.10.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/AGENTS.md +1 -1
- package/manifest.v1.schema.json +9 -1
- package/package.json +3 -3
- package/site/content/docs/console/runs.mdx +10 -0
- package/site/content/docs/console/traces.mdx +4 -0
- package/site/content/docs/elements/ai.mdx +54 -32
- package/site/content/docs/elements/channel.mdx +97 -9
- package/site/content/docs/elements/clock.mdx +1 -0
- package/site/content/docs/elements/flow.mdx +42 -8
- package/site/content/docs/get-started/basic-usage.mdx +5 -1
- package/site/content/docs/get-started/installation.mdx +9 -5
- package/site/content/docs/meta.json +2 -0
- package/site/content/docs/providers/azure-redis.mdx +85 -0
- package/site/content/docs/providers/cockroachdb.mdx +86 -0
- package/site/content/docs/providers/digitalocean-caching.mdx +88 -0
- package/site/content/docs/providers/dragonfly-cloud.mdx +82 -0
- package/site/content/docs/providers/elasticache.mdx +92 -0
- package/site/content/docs/providers/index.mdx +101 -0
- package/site/content/docs/providers/memorystore.mdx +89 -0
- package/site/content/docs/providers/meta.json +20 -0
- package/site/content/docs/providers/neon.mdx +115 -0
- package/site/content/docs/providers/redis-cloud.mdx +93 -0
- package/site/content/docs/providers/supabase.mdx +117 -0
- package/site/content/docs/providers/upstash.mdx +91 -0
- package/site/content/docs/providers/yugabytedb.mdx +95 -0
- package/site/content/docs/recipes/caddy.mdx +128 -0
- package/site/content/docs/recipes/cockroachdb.mdx +127 -0
- package/site/content/docs/recipes/dragonfly.mdx +125 -0
- package/site/content/docs/recipes/index.mdx +146 -0
- package/site/content/docs/recipes/llama-cpp.mdx +153 -0
- package/site/content/docs/recipes/mailpit.mdx +136 -0
- package/site/content/docs/recipes/meilisearch.mdx +139 -0
- package/site/content/docs/recipes/meta.json +31 -0
- package/site/content/docs/recipes/ollama.mdx +142 -0
- package/site/content/docs/recipes/openbao.mdx +141 -0
- package/site/content/docs/recipes/pgdog.mdx +137 -0
- package/site/content/docs/recipes/postgres.mdx +136 -0
- package/site/content/docs/recipes/redis.mdx +130 -0
- package/site/content/docs/recipes/rustfs.mdx +148 -0
- package/site/content/docs/recipes/sglang.mdx +105 -0
- package/site/content/docs/recipes/supabase-docker.mdx +145 -0
- package/site/content/docs/recipes/timescale.mdx +129 -0
- package/site/content/docs/recipes/traefik.mdx +136 -0
- package/site/content/docs/recipes/valkey.mdx +121 -0
- package/site/content/docs/recipes/vllm.mdx +106 -0
- package/site/content/docs/recipes/yugabytedb.mdx +135 -0
- package/site/content/docs/reference/cli.md +42 -6
- package/site/content/docs/reference/configuration.mdx +5 -3
- package/site/content/docs/reference/environment-variables.mdx +12 -12
- package/site/content/docs/reference/fx.mdx +38 -4
- package/src/cli/ai-setup/ai-setup.test.ts +32 -12
- package/src/cli/ai-setup/apply.ts +65 -23
- package/src/cli/ai-setup/catalog.ts +1316 -35
- package/src/cli/ai-setup/detect-ollama.ts +47 -0
- package/src/cli/ai-setup/index.ts +44 -7
- package/src/cli/ai-setup/prompts.ts +430 -486
- package/src/cli/ai-setup/recommend.test.ts +150 -50
- package/src/cli/ai-setup/recommend.ts +118 -101
- package/src/cli/ask-seed.test.ts +68 -0
- package/src/cli/ask-seed.ts +79 -0
- package/src/cli/db-seed-redact.test.ts +25 -0
- package/src/cli/db-seed.ts +36 -7
- package/src/cli/db.test.ts +1 -1
- package/src/cli/db.ts +2 -2
- package/src/cli/dev-controls.test.ts +107 -0
- package/src/cli/dev-controls.ts +354 -0
- package/src/cli/dev-schema-sync.ts +12 -4
- package/src/cli/dev.test.ts +50 -30
- package/src/cli/dev.ts +607 -66
- package/src/cli/drizzle-env.test.ts +40 -6
- package/src/cli/drizzle-env.ts +22 -1
- package/src/cli/ensure-drizzle-config.ts +6 -4
- package/src/cli/hero-meta.test.ts +13 -1
- package/src/cli/hero-meta.ts +28 -6
- package/src/cli/index.ts +5 -0
- package/src/cli/registry.ts +24 -0
- package/src/cli/replay.test.ts +105 -0
- package/src/cli/replay.ts +206 -0
- package/src/compiler/extract.ts +3 -3
- package/src/config/index.ts +3 -3
- package/src/console/server/claim.ts +3 -1
- package/src/console/ui/dist/assets/{index-ClX0ZCe5.js → index-D-9KFce5.js} +2 -2
- package/src/console/ui/dist/assets/panel-overview-CW0CQEc2.js +1 -0
- package/src/console/ui/dist/assets/panel-runs-BxuyDI3x.js +1 -0
- package/src/console/ui/dist/assets/{panel-signals-Dsxdu_AR.js → panel-signals-BXofbj7v.js} +1 -1
- package/src/console/ui/dist/assets/{panel-store-hy7O8HOs.js → panel-store-B8mH-arU.js} +1 -1
- package/src/console/ui/dist/assets/{panel-traces-CBiAe5go.js → panel-traces-DSA-ZWnf.js} +1 -1
- package/src/console/ui/dist/index.html +1 -1
- package/src/console/ui/overview/slo.ts +31 -3
- package/src/console/ui/overview/types.ts +6 -0
- package/src/console/ui/runs/errors.test.ts +53 -0
- package/src/console/ui/runs/errors.ts +88 -0
- package/src/console/ui/runs/index.ts +8 -0
- package/src/console/ui/runs/search.ts +16 -0
- package/src/console/ui/shell/panels/runs/RunsPanel.tsx +72 -1
- package/src/docker/ai-model-status.test.ts +101 -0
- package/src/docker/ai-model-status.ts +276 -0
- package/src/docker/compose-health.test.ts +115 -0
- package/src/docker/compose-health.ts +234 -0
- package/src/docker/compose.ts +24 -8
- package/src/docker/derive.ts +35 -9
- package/src/docker/docker.test.ts +304 -12
- package/src/docker/helpers.ts +56 -0
- package/src/docker/index.ts +52 -0
- package/src/docker/ollama-pull.ts +232 -0
- package/src/docker/recipes/cockroach.ts +24 -0
- package/src/docker/recipes/index.ts +32 -0
- package/src/docker/recipes/llama-cpp.ts +273 -0
- package/src/docker/recipes/ollama.ts +19 -23
- package/src/docker/recipes/pgdog.ts +4 -1
- package/src/docker/recipes/postgres.ts +5 -18
- package/src/docker/recipes/sglang.ts +55 -0
- package/src/docker/recipes/supabase.ts +21 -0
- package/src/docker/recipes/timescale.ts +18 -0
- package/src/docker/recipes/vllm.ts +44 -0
- package/src/docker/recipes/yugabyte.ts +23 -0
- package/src/docker/stack-id.test.ts +22 -0
- package/src/docker/stack-id.ts +38 -4
- package/src/docker/types.ts +15 -0
- package/src/drivers/ai-ollama.integration.test.ts +7 -4
- package/src/drivers/signal-engine.ts +10 -0
- package/src/drivers/signal-postgres.ts +24 -2
- package/src/drivers/signal-types.ts +10 -0
- package/src/elements/channel/fallback.test.ts +91 -0
- package/src/elements/channel/hard-bounce.test.ts +82 -0
- package/src/elements/channel/injection.test.ts +153 -0
- package/src/elements/channel/locale-ar.test.ts +102 -0
- package/src/elements/channel/otp-delivery.ts +5 -2
- package/src/elements/channel/receipts-ledger.test.ts +100 -0
- package/src/elements/channel/runtime.ts +24 -1
- package/src/elements/channel/test-helpers.ts +60 -0
- package/src/elements/channel/whatsapp-compliance.test.ts +124 -0
- package/src/elements/store/cache.ts +1 -1
- package/src/elements/store/emit-drizzle.ts +43 -9
- package/src/elements/store/runtime.ts +1 -1
- package/src/elements/store/schema-decl.test.ts +2 -0
- package/src/elements/store/schema-decl.ts +15 -0
- package/src/elements/store/seed.ts +1 -1
- package/src/index.ts +1 -0
- package/src/kernel/app.ts +117 -10
- package/src/kernel/boot-bind/signal.ts +6 -2
- package/src/kernel/boot-bind/store.ts +2 -1
- package/src/kernel/boot.test.ts +6 -5
- package/src/kernel/boot.ts +8 -3
- package/src/kernel/compensate.test.ts +131 -0
- package/src/kernel/correlation.test.ts +108 -0
- package/src/kernel/effect-timing.test.ts +82 -0
- package/src/kernel/flow.ts +38 -0
- package/src/kernel/fx-runs.test.ts +49 -0
- package/src/kernel/fx.test.ts +14 -0
- package/src/kernel/fx.ts +111 -1
- package/src/manifest/types.ts +8 -2
- package/src/runs/collect.ts +3 -0
- package/src/runs/export-otlp.ts +64 -0
- package/src/runs/index.ts +16 -0
- package/src/runs/parquet.ts +9 -0
- package/src/runs/types.ts +5 -0
- package/src/runs/window.test.ts +68 -0
- package/src/runs/window.ts +148 -0
- package/src/runtime/boot-warn.test.ts +45 -0
- package/src/runtime/boot-warn.ts +17 -0
- package/src/runtime/dev-request-log.ts +5 -5
- package/src/term.test.ts +95 -10
- package/src/term.ts +369 -14
- package/src/console/ui/dist/assets/panel-overview-DONDzBd2.js +0 -1
- package/src/console/ui/dist/assets/panel-runs-D5zf-D9c.js +0 -1
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pull an Ollama model via the container's HTTP API on its exposed host port.
|
|
3
|
+
*
|
|
4
|
+
* Never shells out to a host `ollama` CLI — that talks to whichever server wins
|
|
5
|
+
* on the default local port (often a separately installed host daemon), so the
|
|
6
|
+
* model can land outside the recipe container.
|
|
7
|
+
*
|
|
8
|
+
* Skips `/api/pull` when the model is already listed on `/api/tags`.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** Fetch contract (injectable for tests) — call signature only, not Bun's `preconnect`. */
|
|
12
|
+
export type OllamaFetch = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
13
|
+
|
|
14
|
+
/** Options for {@link ensureOllamaModel}. */
|
|
15
|
+
export interface EnsureOllamaModelOptions {
|
|
16
|
+
/** Base URL of the container API, e.g. `http://127.0.0.1:11434`. */
|
|
17
|
+
readonly url: string;
|
|
18
|
+
/** Model tag to pull (e.g. `qwen3.5:9b`). */
|
|
19
|
+
readonly model: string;
|
|
20
|
+
readonly fetch?: OllamaFetch;
|
|
21
|
+
/** How long to wait for `/api/tags` before giving up (ms). Default 90s. */
|
|
22
|
+
readonly readyTimeoutMs?: number;
|
|
23
|
+
/** Overall pull deadline (ms). Default 20 minutes. */
|
|
24
|
+
readonly pullTimeoutMs?: number;
|
|
25
|
+
/** Progress / status lines (e.g. `oke: …`). */
|
|
26
|
+
readonly onStatus?: (line: string) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Fail-loud pull / readiness error. */
|
|
30
|
+
export class OllamaPullError extends Error {
|
|
31
|
+
constructor(message: string) {
|
|
32
|
+
super(message);
|
|
33
|
+
this.name = "OllamaPullError";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Normalize a base URL (trim trailing slash).
|
|
39
|
+
*
|
|
40
|
+
* @param url - Raw base URL
|
|
41
|
+
*/
|
|
42
|
+
export function normalizeOllamaPullUrl(url: string): string {
|
|
43
|
+
return url.replace(/\/+$/, "");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Whether an installed tag covers the requested model id.
|
|
48
|
+
*
|
|
49
|
+
* @param want - Requested id (e.g. `gemma4:e4b`)
|
|
50
|
+
* @param installed - Names from `/api/tags`
|
|
51
|
+
*/
|
|
52
|
+
export function ollamaTagsInclude(want: string, installed: readonly string[]): boolean {
|
|
53
|
+
const id = want.trim();
|
|
54
|
+
if (!id) return false;
|
|
55
|
+
const base = id.split(":")[0] ?? id;
|
|
56
|
+
return installed.some((name) => {
|
|
57
|
+
if (name === id) return true;
|
|
58
|
+
if (name.startsWith(`${id}-`)) return true;
|
|
59
|
+
if (name.startsWith(`${base}:`) && id.startsWith(`${base}:`)) {
|
|
60
|
+
const instTag = name.slice(base.length + 1);
|
|
61
|
+
const wantTag = id.slice(base.length + 1);
|
|
62
|
+
return (
|
|
63
|
+
instTag === wantTag ||
|
|
64
|
+
instTag.startsWith(`${wantTag}-`) ||
|
|
65
|
+
wantTag.startsWith(instTag) ||
|
|
66
|
+
(instTag === "latest" && wantTag === "latest")
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
// bare name matches `name:latest`
|
|
70
|
+
if (name === `${id}:latest` || id === `${name}:latest`) return true;
|
|
71
|
+
return false;
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Parse model names from an `/api/tags` JSON body.
|
|
77
|
+
*
|
|
78
|
+
* @param json - Parsed response
|
|
79
|
+
*/
|
|
80
|
+
export function parseOllamaTagsNames(json: unknown): string[] {
|
|
81
|
+
if (!json || typeof json !== "object") return [];
|
|
82
|
+
const models = (json as { models?: unknown }).models;
|
|
83
|
+
if (!Array.isArray(models)) return [];
|
|
84
|
+
return models
|
|
85
|
+
.map((m) =>
|
|
86
|
+
m && typeof m === "object" && typeof (m as { name?: unknown }).name === "string"
|
|
87
|
+
? (m as { name: string }).name
|
|
88
|
+
: null,
|
|
89
|
+
)
|
|
90
|
+
.filter((n): n is string => typeof n === "string" && n.length > 0);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Wait until `GET /api/tags` succeeds; skip pull when the model is already
|
|
95
|
+
* present; otherwise `POST /api/pull` with streaming progress.
|
|
96
|
+
*
|
|
97
|
+
* @param opts - Target URL + model (+ optional injectable fetch)
|
|
98
|
+
*/
|
|
99
|
+
export async function ensureOllamaModel(opts: EnsureOllamaModelOptions): Promise<void> {
|
|
100
|
+
const fetchFn = opts.fetch ?? globalThis.fetch;
|
|
101
|
+
const base = normalizeOllamaPullUrl(opts.url);
|
|
102
|
+
const model = opts.model.trim();
|
|
103
|
+
const status = opts.onStatus ?? (() => {});
|
|
104
|
+
if (!model) throw new OllamaPullError("ollama pull: model id is empty");
|
|
105
|
+
|
|
106
|
+
const readyDeadline = Date.now() + (opts.readyTimeoutMs ?? 90_000);
|
|
107
|
+
let tagsBody: unknown = null;
|
|
108
|
+
while (Date.now() < readyDeadline) {
|
|
109
|
+
try {
|
|
110
|
+
const res = await fetchFn(`${base}/api/tags`, { method: "GET" });
|
|
111
|
+
if (res.ok) {
|
|
112
|
+
tagsBody = await res.json().catch(() => null);
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
} catch {
|
|
116
|
+
// still starting
|
|
117
|
+
}
|
|
118
|
+
await Bun.sleep(500);
|
|
119
|
+
}
|
|
120
|
+
if (tagsBody === null) {
|
|
121
|
+
throw new OllamaPullError(`ollama pull: unreachable at ${base}/api/tags`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const installed = parseOllamaTagsNames(tagsBody);
|
|
125
|
+
if (ollamaTagsInclude(model, installed)) {
|
|
126
|
+
status(`oke: Ollama already has ${model} at ${base} — skip pull`);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
status(`oke: pulling ${model} into Ollama at ${base}…`);
|
|
131
|
+
|
|
132
|
+
const pullDeadline = Date.now() + (opts.pullTimeoutMs ?? 20 * 60_000);
|
|
133
|
+
const controller = new AbortController();
|
|
134
|
+
const timer = setTimeout(() => controller.abort(), Math.max(1, pullDeadline - Date.now()));
|
|
135
|
+
try {
|
|
136
|
+
const res = await fetchFn(`${base}/api/pull`, {
|
|
137
|
+
method: "POST",
|
|
138
|
+
headers: { "content-type": "application/json" },
|
|
139
|
+
body: JSON.stringify({ model, stream: true }),
|
|
140
|
+
signal: controller.signal,
|
|
141
|
+
});
|
|
142
|
+
if (!res.ok) {
|
|
143
|
+
const text = await res.text().catch(() => "");
|
|
144
|
+
throw new OllamaPullError(
|
|
145
|
+
`ollama pull: POST ${base}/api/pull → ${res.status}${text ? ` ${text.slice(0, 200)}` : ""}`,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
await consumeOllamaPullStream(res, status);
|
|
149
|
+
} catch (err) {
|
|
150
|
+
if (err instanceof OllamaPullError) throw err;
|
|
151
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
152
|
+
throw new OllamaPullError(`ollama pull: POST ${base}/api/pull failed — ${msg}`);
|
|
153
|
+
} finally {
|
|
154
|
+
clearTimeout(timer);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Read NDJSON pull progress; surface concise status updates.
|
|
160
|
+
*
|
|
161
|
+
* @param res - Streaming pull response
|
|
162
|
+
* @param status - Status writer
|
|
163
|
+
*/
|
|
164
|
+
async function consumeOllamaPullStream(
|
|
165
|
+
res: Response,
|
|
166
|
+
status: (line: string) => void,
|
|
167
|
+
): Promise<void> {
|
|
168
|
+
// Non-body responses (mocked tests) — treat as complete.
|
|
169
|
+
if (!res.body) {
|
|
170
|
+
await res.arrayBuffer().catch(() => undefined);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const reader = res.body.getReader();
|
|
175
|
+
const decoder = new TextDecoder();
|
|
176
|
+
let buf = "";
|
|
177
|
+
const state: PullStreamState = { lastStatus: "", lastPct: -1 };
|
|
178
|
+
|
|
179
|
+
for (;;) {
|
|
180
|
+
const { done, value } = await reader.read();
|
|
181
|
+
if (done) break;
|
|
182
|
+
buf += decoder.decode(value, { stream: true });
|
|
183
|
+
const lines = buf.split("\n");
|
|
184
|
+
buf = lines.pop() ?? "";
|
|
185
|
+
for (const line of lines) {
|
|
186
|
+
handlePullLine(line, status, state);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
buf += decoder.decode();
|
|
190
|
+
if (buf.trim()) {
|
|
191
|
+
handlePullLine(buf, status, state);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
type PullStreamState = { lastStatus: string; lastPct: number };
|
|
196
|
+
|
|
197
|
+
function handlePullLine(
|
|
198
|
+
line: string,
|
|
199
|
+
status: (line: string) => void,
|
|
200
|
+
state: PullStreamState,
|
|
201
|
+
): void {
|
|
202
|
+
const trimmed = line.trim();
|
|
203
|
+
if (!trimmed) return;
|
|
204
|
+
let row: {
|
|
205
|
+
status?: string;
|
|
206
|
+
error?: string;
|
|
207
|
+
completed?: number;
|
|
208
|
+
total?: number;
|
|
209
|
+
};
|
|
210
|
+
try {
|
|
211
|
+
row = JSON.parse(trimmed) as typeof row;
|
|
212
|
+
} catch {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (row.error) {
|
|
216
|
+
throw new OllamaPullError(`ollama pull: ${row.error}`);
|
|
217
|
+
}
|
|
218
|
+
const st = row.status?.trim() ?? "";
|
|
219
|
+
if (typeof row.completed === "number" && typeof row.total === "number" && row.total > 0) {
|
|
220
|
+
const pct = Math.min(100, Math.floor((100 * row.completed) / row.total));
|
|
221
|
+
if (pct !== state.lastPct && (pct === 100 || pct - state.lastPct >= 5)) {
|
|
222
|
+
state.lastPct = pct;
|
|
223
|
+
status(`oke: Ollama pull ${pct}%${st ? ` (${st})` : ""}`);
|
|
224
|
+
}
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (st && st !== state.lastStatus) {
|
|
228
|
+
state.lastStatus = st;
|
|
229
|
+
if (/^pulling\s+[a-f0-9]{12}/i.test(st)) return;
|
|
230
|
+
status(`oke: Ollama ${st}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CockroachDB image recipe — self-hosted single-node Postgres-wire SQL.
|
|
3
|
+
*
|
|
4
|
+
* Driver id stays `postgres`. Official image: `cockroachdb/cockroach`.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { cockroachEnv, cockroachHealth } from "../helpers.ts";
|
|
8
|
+
import type { ImageRecipe } from "../types.ts";
|
|
9
|
+
|
|
10
|
+
/** CockroachDB — SQL on 26257; DB Console on 8080. */
|
|
11
|
+
export const cockroach: ImageRecipe = {
|
|
12
|
+
id: "cockroach",
|
|
13
|
+
port: 26257,
|
|
14
|
+
match: (i) => /cockroach/i.test(i),
|
|
15
|
+
apply: (s) => ({
|
|
16
|
+
environment: cockroachEnv(s),
|
|
17
|
+
command: ["start-single-node", "--accept-sql-without-tls"],
|
|
18
|
+
healthcheck: cockroachHealth,
|
|
19
|
+
volumes: [`${s.serviceName}-data:/cockroach/cockroach-data`],
|
|
20
|
+
extraPorts: [{ host: 8080, container: 8080 }],
|
|
21
|
+
}),
|
|
22
|
+
url: (_s, c) =>
|
|
23
|
+
`postgres://${c.user}:${encodeURIComponent(c.password)}@${c.host}:${c.port}/${c.database}?sslmode=require`,
|
|
24
|
+
};
|
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
import type { ImageRecipe } from "../types.ts";
|
|
6
6
|
import { caddy } from "./caddy.ts";
|
|
7
|
+
import { cockroach } from "./cockroach.ts";
|
|
7
8
|
import { dragonfly } from "./dragonfly.ts";
|
|
9
|
+
import { llamaCpp } from "./llama-cpp.ts";
|
|
8
10
|
import { mailpit } from "./mailpit.ts";
|
|
9
11
|
import { meilisearch } from "./meilisearch.ts";
|
|
10
12
|
import { ollama } from "./ollama.ts";
|
|
@@ -13,11 +15,20 @@ import { pgdog } from "./pgdog.ts";
|
|
|
13
15
|
import { postgres } from "./postgres.ts";
|
|
14
16
|
import { redis } from "./redis.ts";
|
|
15
17
|
import { rustfs } from "./rustfs.ts";
|
|
18
|
+
import { sglang } from "./sglang.ts";
|
|
19
|
+
import { supabase } from "./supabase.ts";
|
|
20
|
+
import { timescale } from "./timescale.ts";
|
|
16
21
|
import { traefik } from "./traefik.ts";
|
|
17
22
|
import { valkey } from "./valkey.ts";
|
|
23
|
+
import { vllm } from "./vllm.ts";
|
|
24
|
+
import { yugabyte } from "./yugabyte.ts";
|
|
18
25
|
|
|
19
26
|
/** Default recipe catalogue — more specific image matches before protocol peers. */
|
|
20
27
|
export const builtinRecipes: readonly ImageRecipe[] = [
|
|
28
|
+
cockroach,
|
|
29
|
+
yugabyte,
|
|
30
|
+
timescale,
|
|
31
|
+
supabase,
|
|
21
32
|
postgres,
|
|
22
33
|
pgdog,
|
|
23
34
|
dragonfly,
|
|
@@ -27,6 +38,9 @@ export const builtinRecipes: readonly ImageRecipe[] = [
|
|
|
27
38
|
rustfs,
|
|
28
39
|
openbao,
|
|
29
40
|
meilisearch,
|
|
41
|
+
llamaCpp,
|
|
42
|
+
vllm,
|
|
43
|
+
sglang,
|
|
30
44
|
ollama,
|
|
31
45
|
caddy,
|
|
32
46
|
traefik,
|
|
@@ -34,7 +48,9 @@ export const builtinRecipes: readonly ImageRecipe[] = [
|
|
|
34
48
|
|
|
35
49
|
export {
|
|
36
50
|
caddy,
|
|
51
|
+
cockroach,
|
|
37
52
|
dragonfly,
|
|
53
|
+
llamaCpp,
|
|
38
54
|
mailpit,
|
|
39
55
|
meilisearch,
|
|
40
56
|
ollama,
|
|
@@ -43,10 +59,26 @@ export {
|
|
|
43
59
|
postgres,
|
|
44
60
|
redis,
|
|
45
61
|
rustfs,
|
|
62
|
+
sglang,
|
|
63
|
+
supabase,
|
|
64
|
+
timescale,
|
|
46
65
|
traefik,
|
|
47
66
|
valkey,
|
|
67
|
+
vllm,
|
|
68
|
+
yugabyte,
|
|
48
69
|
};
|
|
49
70
|
|
|
71
|
+
export {
|
|
72
|
+
buildLlamaCppEntrypoint,
|
|
73
|
+
LLAMA_CPP_ENTRYPOINT_FILE,
|
|
74
|
+
LLAMA_CPP_ENTRYPOINT_MOUNT,
|
|
75
|
+
LLAMA_CPP_IMAGE,
|
|
76
|
+
LLAMA_CPP_MIN_SAFE_BUILD,
|
|
77
|
+
} from "./llama-cpp.ts";
|
|
78
|
+
export { OLLAMA_IMAGE, OLLAMA_MIN_SAFE_VERSION } from "./ollama.ts";
|
|
79
|
+
export { SGLANG_IMAGE } from "./sglang.ts";
|
|
80
|
+
export { VLLM_IMAGE } from "./vllm.ts";
|
|
81
|
+
|
|
50
82
|
/**
|
|
51
83
|
* Resolve the recipe for an image reference.
|
|
52
84
|
*
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* llama.cpp image recipe — default local AI (`ghcr.io/ggml-org/llama.cpp`).
|
|
3
|
+
*
|
|
4
|
+
* OpenAI-compatible `llama-server` on 8080. Pin ≥ {@link LLAMA_CPP_MIN_SAFE_BUILD}
|
|
5
|
+
* (CVE-2026-27940 / CVE-2026-33298 floor). Host publish is loopback-only;
|
|
6
|
+
* never publish RPC. Models via Docker Hub `ai/` — do not load arbitrary
|
|
7
|
+
* untrusted GGUF files.
|
|
8
|
+
*
|
|
9
|
+
* b10290+ issues this recipe works around:
|
|
10
|
+
* 1. Bare `--docker-repo` / router `--models-preset` children re-enter empty
|
|
11
|
+
* router mode (`is_router_server` only checks `-m` / `-hf`) → forever
|
|
12
|
+
* `loading`.
|
|
13
|
+
* 2. `llama download -dr` only accepts `application/vnd.docker.ai.gguf.v3`;
|
|
14
|
+
* newer Hub models (e.g. gemma4) ship `vnd.cncf.model.weight.v1.raw`.
|
|
15
|
+
*
|
|
16
|
+
* Entrypoint: Hub-pull (native download, then CNCF-aware fallback) → serve
|
|
17
|
+
* single-model with `-m` + `--alias`.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type { ImageRecipe } from "../types.ts";
|
|
21
|
+
|
|
22
|
+
/** Minimum safe llama.cpp build (GGUF parser CVEs through CVE-2026-27940). */
|
|
23
|
+
export const LLAMA_CPP_MIN_SAFE_BUILD = 8146;
|
|
24
|
+
|
|
25
|
+
/** Pinned default image — verified ≥ {@link LLAMA_CPP_MIN_SAFE_BUILD}; never `latest`. */
|
|
26
|
+
export const LLAMA_CPP_IMAGE = "ghcr.io/ggml-org/llama.cpp:server-b10290";
|
|
27
|
+
|
|
28
|
+
/** Compose-relative entrypoint that downloads then serves one Hub model. */
|
|
29
|
+
export const LLAMA_CPP_ENTRYPOINT_FILE = "llama-entrypoint.py";
|
|
30
|
+
|
|
31
|
+
/** In-container path for {@link LLAMA_CPP_ENTRYPOINT_FILE}. */
|
|
32
|
+
export const LLAMA_CPP_ENTRYPOINT_MOUNT = "/oke/llama-entrypoint.py";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Python entrypoint: pull curated Docker Hub `ai/` GGUF into the cache volume,
|
|
36
|
+
* then `exec` `llama-server -m <gguf> --alias <id>` (single-model, not router).
|
|
37
|
+
*/
|
|
38
|
+
export function buildLlamaCppEntrypoint(): string {
|
|
39
|
+
return `#!/usr/bin/env python3
|
|
40
|
+
"""OKE llama.cpp entrypoint — Hub-pull curated ai/ model, then serve single-model."""
|
|
41
|
+
|
|
42
|
+
from __future__ import annotations
|
|
43
|
+
|
|
44
|
+
import json
|
|
45
|
+
import os
|
|
46
|
+
import subprocess
|
|
47
|
+
import sys
|
|
48
|
+
import urllib.error
|
|
49
|
+
import urllib.parse
|
|
50
|
+
import urllib.request
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def cache_dir() -> str:
|
|
54
|
+
return os.environ.get("LLAMA_CACHE", "/root/.cache/llama.cpp")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def parse_model(model: str) -> tuple[str, str]:
|
|
58
|
+
raw = model.strip() or "smollm2"
|
|
59
|
+
if "/" not in raw.split(":", 1)[0]:
|
|
60
|
+
raw = f"ai/{raw}"
|
|
61
|
+
if ":" in raw:
|
|
62
|
+
repo, tag = raw.rsplit(":", 1)
|
|
63
|
+
else:
|
|
64
|
+
repo, tag = raw, "latest"
|
|
65
|
+
return repo, tag
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def expected_gguf(repo: str, tag: str) -> str:
|
|
69
|
+
return f"{repo.replace('/', '_')}_{tag}.gguf"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def existing_gguf(path: str) -> str | None:
|
|
73
|
+
if os.path.isfile(path) and os.path.getsize(path) > 0:
|
|
74
|
+
return path
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def llama_download(model: str) -> int:
|
|
79
|
+
return subprocess.call(
|
|
80
|
+
["/app/llama", "download", "-dr", model, "--no-mmproj"],
|
|
81
|
+
stdout=sys.stdout,
|
|
82
|
+
stderr=sys.stderr,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def registry_token(repo: str) -> str:
|
|
87
|
+
scope = urllib.parse.quote(f"repository:{repo}:pull", safe="")
|
|
88
|
+
url = f"https://auth.docker.io/token?service=registry.docker.io&scope={scope}"
|
|
89
|
+
with urllib.request.urlopen(url, timeout=60) as res:
|
|
90
|
+
return json.load(res)["token"]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def fetch_manifest(repo: str, tag: str, token: str) -> dict:
|
|
94
|
+
req = urllib.request.Request(
|
|
95
|
+
f"https://registry-1.docker.io/v2/{repo}/manifests/{tag}",
|
|
96
|
+
headers={
|
|
97
|
+
"Authorization": f"Bearer {token}",
|
|
98
|
+
"Accept": (
|
|
99
|
+
"application/vnd.oci.image.manifest.v1+json,"
|
|
100
|
+
"application/vnd.docker.distribution.manifest.v2+json,"
|
|
101
|
+
"application/vnd.oci.image.index.v1+json,"
|
|
102
|
+
"application/vnd.docker.distribution.manifest.list.v2+json"
|
|
103
|
+
),
|
|
104
|
+
},
|
|
105
|
+
)
|
|
106
|
+
with urllib.request.urlopen(req, timeout=60) as res:
|
|
107
|
+
return json.load(res)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def pick_gguf_layer(manifest: dict) -> tuple[str, str]:
|
|
111
|
+
"""Return (digest, suggested_filename) for the primary GGUF weight layer."""
|
|
112
|
+
layers = manifest.get("layers") or []
|
|
113
|
+
candidates: list[tuple[str, str, int]] = []
|
|
114
|
+
for layer in layers:
|
|
115
|
+
media = str(layer.get("mediaType") or "")
|
|
116
|
+
digest = str(layer.get("digest") or "")
|
|
117
|
+
ann = layer.get("annotations") or {}
|
|
118
|
+
filepath = str(ann.get("org.cncf.model.filepath") or "")
|
|
119
|
+
name = filepath.split("/")[-1] if filepath else ""
|
|
120
|
+
is_gguf = (
|
|
121
|
+
media == "application/vnd.docker.ai.gguf.v3"
|
|
122
|
+
or "gguf" in media.lower()
|
|
123
|
+
or name.lower().endswith(".gguf")
|
|
124
|
+
)
|
|
125
|
+
if not is_gguf or not digest:
|
|
126
|
+
continue
|
|
127
|
+
if name.lower().startswith("mmproj"):
|
|
128
|
+
continue
|
|
129
|
+
size = int(layer.get("size") or 0)
|
|
130
|
+
candidates.append((digest, name or "model.gguf", size))
|
|
131
|
+
if not candidates:
|
|
132
|
+
raise RuntimeError("No GGUF weight layer in Docker / CNCF model manifest")
|
|
133
|
+
# Prefer the largest non-mmproj GGUF (main weights vs projectors).
|
|
134
|
+
candidates.sort(key=lambda c: c[2], reverse=True)
|
|
135
|
+
digest, name, _ = candidates[0]
|
|
136
|
+
return digest, name
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def download_blob(repo: str, digest: str, token: str, dest: str) -> None:
|
|
140
|
+
url = f"https://registry-1.docker.io/v2/{repo}/blobs/{digest}"
|
|
141
|
+
req = urllib.request.Request(url, headers={"Authorization": f"Bearer {token}"})
|
|
142
|
+
os.makedirs(os.path.dirname(dest), exist_ok=True)
|
|
143
|
+
partial = f"{dest}.partial"
|
|
144
|
+
print(f"oke ai: downloading {digest} → {dest}", flush=True)
|
|
145
|
+
with urllib.request.urlopen(req, timeout=600) as res, open(partial, "wb") as out:
|
|
146
|
+
total = int(res.headers.get("Content-Length") or 0)
|
|
147
|
+
done = 0
|
|
148
|
+
last_pct = -1
|
|
149
|
+
while True:
|
|
150
|
+
chunk = res.read(1024 * 1024)
|
|
151
|
+
if not chunk:
|
|
152
|
+
break
|
|
153
|
+
out.write(chunk)
|
|
154
|
+
done += len(chunk)
|
|
155
|
+
if total:
|
|
156
|
+
pct = (100 * done) // total
|
|
157
|
+
if pct >= last_pct + 5 or done == total:
|
|
158
|
+
last_pct = pct
|
|
159
|
+
print(
|
|
160
|
+
f"oke ai: download {pct}% ({done // (1024 * 1024)} / {total // (1024 * 1024)} MiB)",
|
|
161
|
+
flush=True,
|
|
162
|
+
)
|
|
163
|
+
os.replace(partial, dest)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def hub_pull(repo: str, tag: str, dest: str) -> None:
|
|
167
|
+
token = registry_token(repo)
|
|
168
|
+
manifest = fetch_manifest(repo, tag, token)
|
|
169
|
+
# Index → pick first linux manifest if present.
|
|
170
|
+
if "manifests" in manifest:
|
|
171
|
+
digests = [m.get("digest") for m in manifest["manifests"] if m.get("digest")]
|
|
172
|
+
if not digests:
|
|
173
|
+
raise RuntimeError("Empty OCI index for model")
|
|
174
|
+
req = urllib.request.Request(
|
|
175
|
+
f"https://registry-1.docker.io/v2/{repo}/manifests/{digests[0]}",
|
|
176
|
+
headers={
|
|
177
|
+
"Authorization": f"Bearer {token}",
|
|
178
|
+
"Accept": "application/vnd.oci.image.manifest.v1+json,application/vnd.docker.distribution.manifest.v2+json",
|
|
179
|
+
},
|
|
180
|
+
)
|
|
181
|
+
with urllib.request.urlopen(req, timeout=60) as res:
|
|
182
|
+
manifest = json.load(res)
|
|
183
|
+
digest, _name = pick_gguf_layer(manifest)
|
|
184
|
+
download_blob(repo, digest, token, dest)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def ensure_model(model: str) -> str:
|
|
188
|
+
repo, tag = parse_model(model)
|
|
189
|
+
# llama download -dr expects id without forced ai/ prefix when using default org.
|
|
190
|
+
dr = model.strip() or "smollm2"
|
|
191
|
+
if dr.startswith("ai/"):
|
|
192
|
+
dr = dr[3:]
|
|
193
|
+
dest = os.path.join(cache_dir(), expected_gguf(repo, tag))
|
|
194
|
+
hit = existing_gguf(dest)
|
|
195
|
+
if hit:
|
|
196
|
+
print(f"oke ai: using cached {hit}", flush=True)
|
|
197
|
+
return hit
|
|
198
|
+
|
|
199
|
+
print(f"oke ai: ensuring Docker Hub model '{dr}' is cached…", flush=True)
|
|
200
|
+
rc = llama_download(dr)
|
|
201
|
+
hit = existing_gguf(dest)
|
|
202
|
+
if hit:
|
|
203
|
+
return hit
|
|
204
|
+
# Native downloader may write the same basename; also accept exact path only.
|
|
205
|
+
print(
|
|
206
|
+
f"oke ai: native download exit {rc}; trying CNCF / Hub registry pull…",
|
|
207
|
+
flush=True,
|
|
208
|
+
)
|
|
209
|
+
try:
|
|
210
|
+
hub_pull(repo, tag, dest)
|
|
211
|
+
except (urllib.error.URLError, urllib.error.HTTPError, TimeoutError, RuntimeError, KeyError, ValueError) as err:
|
|
212
|
+
print(f"oke ai: failed to download model '{dr}': {err}", file=sys.stderr, flush=True)
|
|
213
|
+
sys.exit(1)
|
|
214
|
+
hit = existing_gguf(dest)
|
|
215
|
+
if not hit:
|
|
216
|
+
print(f"oke ai: download produced no file at {dest}", file=sys.stderr, flush=True)
|
|
217
|
+
sys.exit(1)
|
|
218
|
+
return hit
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def main() -> None:
|
|
222
|
+
model = (os.environ.get("OKE_AI_MODEL") or "smollm2").strip() or "smollm2"
|
|
223
|
+
gguf = ensure_model(model)
|
|
224
|
+
print(f"oke ai: serving {model} from {gguf}", flush=True)
|
|
225
|
+
os.execv(
|
|
226
|
+
"/app/llama-server",
|
|
227
|
+
[
|
|
228
|
+
"/app/llama-server",
|
|
229
|
+
"--host",
|
|
230
|
+
"0.0.0.0",
|
|
231
|
+
"--port",
|
|
232
|
+
"8080",
|
|
233
|
+
"--model",
|
|
234
|
+
gguf,
|
|
235
|
+
"--alias",
|
|
236
|
+
model,
|
|
237
|
+
],
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
if __name__ == "__main__":
|
|
242
|
+
main()
|
|
243
|
+
`;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** llama-server — OpenAI-compatible HTTP on 8080; loopback publish only. */
|
|
247
|
+
export const llamaCpp: ImageRecipe = {
|
|
248
|
+
id: "llama-cpp",
|
|
249
|
+
port: 8080,
|
|
250
|
+
match: (i) => /llama\.cpp|llamacpp/i.test(i),
|
|
251
|
+
apply: (s) => ({
|
|
252
|
+
// OKE_AI_MODEL comes from compose `env_file` (`.env.docker`). Do not
|
|
253
|
+
// re-declare it as `${OKE_AI_MODEL:-smollm2}` here — Compose interpolates
|
|
254
|
+
// that from the *host* shell and can override the stack file with the
|
|
255
|
+
// default. Never set LLAMA_ARG_MODELS_PRESET / LLAMA_ARG_DOCKER_REPO
|
|
256
|
+
// (router children inherit `LLAMA_ARG_*` and can OOM).
|
|
257
|
+
entrypoint: ["/usr/bin/python3", LLAMA_CPP_ENTRYPOINT_MOUNT],
|
|
258
|
+
volumes: [
|
|
259
|
+
`${s.serviceName}-models:/root/.cache`,
|
|
260
|
+
`./${LLAMA_CPP_ENTRYPOINT_FILE}:${LLAMA_CPP_ENTRYPOINT_MOUNT}:ro`,
|
|
261
|
+
],
|
|
262
|
+
publishBind: "127.0.0.1",
|
|
263
|
+
healthcheck: {
|
|
264
|
+
test: ["CMD-SHELL", "curl -fsS http://127.0.0.1:8080/health >/dev/null || exit 1"],
|
|
265
|
+
interval: "5s",
|
|
266
|
+
timeout: "5s",
|
|
267
|
+
retries: 24,
|
|
268
|
+
// First boot may download a multi‑GB Hub model (CNCF pull) into cache.
|
|
269
|
+
start_period: "900s",
|
|
270
|
+
},
|
|
271
|
+
}),
|
|
272
|
+
url: (_s, c) => `http://${c.host}:${c.port}/v1`,
|
|
273
|
+
};
|
|
@@ -1,47 +1,43 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Ollama image recipe — local model server (`ollama/ollama`).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Fully supported alternative to the default llama.cpp recipe. Pin ≥
|
|
5
|
+
* {@link OLLAMA_MIN_SAFE_VERSION} (CVE-2026-7482 floor). Host publish is
|
|
6
|
+
* loopback-only — never expose `:11434` on `0.0.0.0`. Load models only from
|
|
7
|
+
* Ollama's library; do not feed arbitrary untrusted GGUF into `/api/create`.
|
|
8
|
+
*
|
|
9
|
+
* Model pull is **not** done here: after compose is up,
|
|
10
|
+
* {@link ensureOllamaModel} POSTs `/api/pull` to this container's loopback URL.
|
|
7
11
|
*/
|
|
8
12
|
|
|
9
13
|
import type { ImageRecipe } from "../types.ts";
|
|
10
14
|
|
|
11
|
-
/**
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
*/
|
|
17
|
-
const OLLAMA_BOOT = [
|
|
18
|
-
"set -e",
|
|
19
|
-
"/bin/ollama serve &",
|
|
20
|
-
"pid=$$!",
|
|
21
|
-
'i=0; until /bin/ollama list >/dev/null 2>&1; do i=$$((i+1)); [ "$$i" -lt 90 ] || exit 1; sleep 1; done',
|
|
22
|
-
'/bin/ollama pull "$${OKE_AI_MODEL:-qwen3.5:9b}"',
|
|
23
|
-
"wait $$pid",
|
|
24
|
-
].join("; ");
|
|
15
|
+
/** Minimum safe Ollama release (CVE-2026-7482 — own Go GGUF loader). */
|
|
16
|
+
export const OLLAMA_MIN_SAFE_VERSION = "0.17.1";
|
|
17
|
+
|
|
18
|
+
/** Pinned default Ollama image — verified ≥ {@link OLLAMA_MIN_SAFE_VERSION}; never `latest`. */
|
|
19
|
+
export const OLLAMA_IMAGE = "ollama/ollama:0.32.6";
|
|
25
20
|
|
|
26
|
-
/** Ollama local model server. API on 11434. */
|
|
21
|
+
/** Ollama local model server. API on 11434; loopback publish only. */
|
|
27
22
|
export const ollama: ImageRecipe = {
|
|
28
23
|
id: "ollama",
|
|
29
24
|
port: 11434,
|
|
30
|
-
match: (i) => /ollama/i.test(i),
|
|
25
|
+
match: (i) => /(?:^|\/)ollama(?:[:@/]|$)/i.test(i),
|
|
31
26
|
apply: (s) => ({
|
|
32
27
|
environment: {
|
|
28
|
+
// Listen on all interfaces *inside* the container (Docker networking).
|
|
29
|
+
// Host publish uses publishBind — never 0.0.0.0 on the host.
|
|
33
30
|
OLLAMA_HOST: "0.0.0.0:11434",
|
|
34
31
|
OKE_AI_MODEL: "${OKE_AI_MODEL:-qwen3.5:9b}",
|
|
35
32
|
},
|
|
36
|
-
entrypoint: ["/bin/sh", "-c"],
|
|
37
|
-
command: [OLLAMA_BOOT],
|
|
38
33
|
volumes: [`${s.serviceName}-data:/root/.ollama`],
|
|
34
|
+
publishBind: "127.0.0.1",
|
|
39
35
|
healthcheck: {
|
|
40
36
|
test: ["CMD-SHELL", "/bin/ollama list >/dev/null 2>&1 || exit 1"],
|
|
41
37
|
interval: "5s",
|
|
42
38
|
timeout: "5s",
|
|
43
|
-
retries:
|
|
44
|
-
start_period: "
|
|
39
|
+
retries: 24,
|
|
40
|
+
start_period: "10s",
|
|
45
41
|
},
|
|
46
42
|
}),
|
|
47
43
|
url: (_s, c) => `http://${c.host}:${c.port}`,
|
|
@@ -20,8 +20,11 @@ export const PGDOG_BACKEND_SERVICE = "store-sql";
|
|
|
20
20
|
export function buildPgDogToml(opts: {
|
|
21
21
|
readonly database: string;
|
|
22
22
|
readonly postgresHost?: string;
|
|
23
|
+
/** Backend container port (`store-sql` recipe port; default 5432). */
|
|
24
|
+
readonly postgresPort?: number;
|
|
23
25
|
}): string {
|
|
24
26
|
const host = opts.postgresHost ?? PGDOG_BACKEND_SERVICE;
|
|
27
|
+
const port = opts.postgresPort ?? 5432;
|
|
25
28
|
const db = opts.database;
|
|
26
29
|
return [
|
|
27
30
|
"[general]",
|
|
@@ -32,7 +35,7 @@ export function buildPgDogToml(opts: {
|
|
|
32
35
|
"[[databases]]",
|
|
33
36
|
`name = ${tomlString(db)}`,
|
|
34
37
|
`host = ${tomlString(host)}`,
|
|
35
|
-
|
|
38
|
+
`port = ${port}`,
|
|
36
39
|
`database_name = ${tomlString(db)}`,
|
|
37
40
|
"",
|
|
38
41
|
].join("\n");
|