/** * Load `oke.config.ts` / Manifest helpers for CLI commands. */ import { resolve } from "node:path"; import { driverRefId, flattenImagesConfig, normalizeEnvDriverMap, type EnvDriverInput, type OkeConfig, } from "../config/index.ts"; import type { Manifest } from "../manifest/types.ts"; /** Default image pins when `images` is omitted but prod drivers need containers. */ const DEFAULT_SQL_IMAGE = "postgres:18-alpine"; const DEFAULT_PGVECTOR_IMAGE = "pgvector/pgvector:pg17"; const DEFAULT_PGDOG_IMAGE = "ghcr.io/pgdogdev/pgdog:v0.1.59"; const DEFAULT_KV_IMAGE = "docker.dragonflydb.io/dragonflydb/dragonfly:v2.0.0"; /** * Prefer `dev` then `prod` for a driver map (compose infra ≈ production). * * @param map - Env driver input (normalized first) */ function dockerOrProdId(map: EnvDriverInput | undefined): string | undefined { const normalized = normalizeEnvDriverMap(map); return driverRefId(normalized?.dev) ?? driverRefId(normalized?.prod); } /** * Derive default image pins from docker/prod driver protocols. * * Used when `oke.config.ts` omits `images` but declares postgres/redis (etc.) * so `oke dev -d` / `oke stack` / `oke docker` have something to run. * * @param config - Loaded config */ export function defaultImagesFromConfig(config: OkeConfig): Readonly> { const out: Record = {}; const sql = dockerOrProdId(config.drivers?.store?.sql); const index = dockerOrProdId(config.drivers?.store?.index); const kv = dockerOrProdId(config.drivers?.store?.kv); const signal = dockerOrProdId(config.drivers?.signal); const clock = dockerOrProdId(config.drivers?.clock); const journal = dockerOrProdId(config.drivers?.journal); // SQL-consuming facets pull a SQL image. Signal postgres still needs a // LISTEN/NOTIFY client (not Bun.SQL); clock + journal postgres are real // SKIP LOCKED stores (CronStore / durable-run journal). const needsSql = sql === "postgres" || sql === "pgvector" || index === "pgvector" || clock === "postgres" || journal === "postgres" || (config.drivers?.prod ?? []).some((p) => p === "postgres" || p === "pgvector"); if (needsSql) { out["store.sql"] = sql === "pgvector" || index === "pgvector" ? DEFAULT_PGVECTOR_IMAGE : DEFAULT_SQL_IMAGE; // PgDog in front of Postgres — caps real server connections under horizontal scale. out.pgdog = DEFAULT_PGDOG_IMAGE; } const needsKv = kv === "redis" || signal === "redis" || (config.drivers?.prod ?? []).includes("redis"); if (needsKv) { out["store.kv"] = DEFAULT_KV_IMAGE; } return out; } /** Result of loading project config. */ export interface LoadedConfig { readonly config: OkeConfig; readonly path: string; } /** * Reject removed env keys / drivers in `oke.config.*` source. * Migration is documented — no soft-compat rewrite. * * @param source - File contents */ export function assertOkeConfigSource(source: string): void { if (/(^|[^\w.])local\s*:/.test(source)) { throw new Error(`"local" is no longer a valid environment key in oke.config.ts.`); } if (/(["'])sqlite\1/.test(source)) { throw new Error(`"sqlite" is no longer a valid driver in oke.config.ts.`); } } /** * Load `oke.config.ts` (or `.js` / `.mts`) from a directory. * * @param cwd - Project root * @param configPath - Optional explicit path */ export async function loadOkeConfig( cwd = process.cwd(), configPath?: string, ): Promise { const candidates = configPath ? [resolve(cwd, configPath)] : [ resolve(cwd, "oke.config.ts"), resolve(cwd, "oke.config.mts"), resolve(cwd, "oke.config.js"), ]; for (const path of candidates) { const file = Bun.file(path); if (!(await file.exists())) continue; const source = await file.text(); assertOkeConfigSource(source); const mod = (await import(path)) as { default?: OkeConfig }; if (!mod.default || typeof mod.default !== "object") { throw new Error(`oke: ${path} must default-export defineConfig({...})`); } return { config: mod.default, path }; } throw new Error(`oke: no oke.config.ts found in ${cwd} — create one with defineConfig()`); } /** * Load a Manifest JSON document. * * @param path - Manifest path */ export async function loadManifest(path: string): Promise { const file = Bun.file(resolve(path)); if (!(await file.exists())) { throw new Error(`oke: manifest not found: ${path}`); } return (await file.json()) as Manifest; } /** * Resolve images map from config or Manifest. * * When neither declares `images`, derive defaults from prod driver protocols * so `oke dev -d` works on scaffolded templates without an explicit pin map. * * @param config - Optional config * @param manifest - Optional manifest */ export function resolveImages( config?: OkeConfig, manifest?: Manifest, ): Readonly> { const explicit = config?.images ? flattenImagesConfig(config.images) : manifest?.images; if (explicit && Object.keys(explicit).length > 0) return explicit; if (config) return defaultImagesFromConfig(config); return {}; } /** Compact driver mismatch for {@link formatStackSummary}. */ export type StackDriverMismatch = { /** Short label (`sql`, `kv`). */ readonly label: string; /** Active `drivers.*.docker` (→ prod) id. */ readonly using: string; /** Driver that would use the container. */ readonly container: string; }; /** * Detect when `oke dev -d` containers will not back the app because the * `docker` driver profile still points at local/memory backends. * * @param config - Loaded config * @param stackRoles - Roles being composed */ export function dockerDevDriverMismatches( config: OkeConfig, stackRoles: readonly string[], ): readonly StackDriverMismatch[] { const roles = new Set(stackRoles); const out: StackDriverMismatch[] = []; const sqlMap = normalizeEnvDriverMap(config.drivers?.store?.sql); const kvMap = normalizeEnvDriverMap(config.drivers?.store?.kv); const sqlDocker = driverRefId(sqlMap?.dev) ?? driverRefId(sqlMap?.prod); const kvDocker = driverRefId(kvMap?.dev) ?? driverRefId(kvMap?.prod); if ( roles.has("store.sql") && (sqlDocker === "memory" || sqlDocker === "pglite" || sqlDocker === undefined) ) { out.push({ label: "sql", using: sqlDocker ?? "unset", container: "postgres", }); } if (roles.has("store.kv") && (kvDocker === "memory" || kvDocker === undefined)) { out.push({ label: "kv", using: kvDocker ?? "memory", container: "redis", }); } return out; } /** * @deprecated Use {@link dockerDevDriverMismatches} — kept for older call sites. */ export function stackDevDriverMismatches( config: OkeConfig, stackRoles: readonly string[], ): readonly StackDriverMismatch[] { return dockerDevDriverMismatches(config, stackRoles); } /** * @deprecated Use {@link dockerDevDriverMismatches} — kept for older call sites. * @param config - Loaded config * @param stackRoles - Roles being composed */ export function stackDevDriverWarnings( config: OkeConfig, stackRoles: readonly string[], ): readonly string[] { return dockerDevDriverMismatches(config, stackRoles).map((m) => `${m.label}: ${m.using}`); }