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
|
@@ -7,7 +7,7 @@ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
|
7
7
|
import { tmpdir } from "node:os";
|
|
8
8
|
import { join } from "node:path";
|
|
9
9
|
import type { OkeConfig } from "../config/index.ts";
|
|
10
|
-
import { resolveDrizzleKitEnv } from "./drizzle-env.ts";
|
|
10
|
+
import { applyComposeEnvToProcess, resolveDrizzleKitEnv } from "./drizzle-env.ts";
|
|
11
11
|
|
|
12
12
|
const dirs: string[] = [];
|
|
13
13
|
|
|
@@ -46,11 +46,18 @@ describe("resolveDrizzleKitEnv", () => {
|
|
|
46
46
|
join(dir, "docker", ".env.docker"),
|
|
47
47
|
"DATABASE_URL=postgres://u:p@127.0.0.1:5432/oke\n",
|
|
48
48
|
);
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
49
|
+
const prevDb = process.env["DATABASE_URL"];
|
|
50
|
+
delete process.env["DATABASE_URL"];
|
|
51
|
+
try {
|
|
52
|
+
const ctx = await resolveDrizzleKitEnv(dir, dualConfig, "docker");
|
|
53
|
+
expect(ctx.dialect).toBe("postgresql");
|
|
54
|
+
expect(ctx.overlay.OKE_DRIZZLE_DIALECT).toBe("postgresql");
|
|
55
|
+
expect(ctx.overlay.DATABASE_URL).toBe("postgres://u:p@127.0.0.1:5432/oke");
|
|
56
|
+
expect(ctx.overlay.OKE_SQLITE_URL).toBeUndefined();
|
|
57
|
+
} finally {
|
|
58
|
+
if (prevDb === undefined) delete process.env["DATABASE_URL"];
|
|
59
|
+
else process.env["DATABASE_URL"] = prevDb;
|
|
60
|
+
}
|
|
54
61
|
});
|
|
55
62
|
|
|
56
63
|
test("docker dialect is postgresql even when DATABASE_URL unset (no URL guessing)", async () => {
|
|
@@ -65,3 +72,30 @@ describe("resolveDrizzleKitEnv", () => {
|
|
|
65
72
|
await expect(resolveDrizzleKitEnv(dir, bad, "local")).rejects.toThrow(/not supported/);
|
|
66
73
|
});
|
|
67
74
|
});
|
|
75
|
+
|
|
76
|
+
describe("applyComposeEnvToProcess", () => {
|
|
77
|
+
test("fills unset keys from docker/.env.docker without overwriting", async () => {
|
|
78
|
+
const dir = await tempDir();
|
|
79
|
+
await mkdir(join(dir, "docker"), { recursive: true });
|
|
80
|
+
await writeFile(
|
|
81
|
+
join(dir, "docker", ".env.docker"),
|
|
82
|
+
"DATABASE_URL=postgres://u:p@127.0.0.1:5432/oke\nREDIS_URL=redis://127.0.0.1:6379\n",
|
|
83
|
+
);
|
|
84
|
+
const prevDb = process.env["DATABASE_URL"];
|
|
85
|
+
const prevRedis = process.env["REDIS_URL"];
|
|
86
|
+
delete process.env["DATABASE_URL"];
|
|
87
|
+
process.env["REDIS_URL"] = "redis://already-set";
|
|
88
|
+
try {
|
|
89
|
+
const applied = await applyComposeEnvToProcess(dir);
|
|
90
|
+
expect(applied).toContain("DATABASE_URL");
|
|
91
|
+
expect(applied).not.toContain("REDIS_URL");
|
|
92
|
+
expect(process.env["DATABASE_URL"]).toBe("postgres://u:p@127.0.0.1:5432/oke");
|
|
93
|
+
expect(process.env["REDIS_URL"]).toBe("redis://already-set");
|
|
94
|
+
} finally {
|
|
95
|
+
if (prevDb === undefined) delete process.env["DATABASE_URL"];
|
|
96
|
+
else process.env["DATABASE_URL"] = prevDb;
|
|
97
|
+
if (prevRedis === undefined) delete process.env["REDIS_URL"];
|
|
98
|
+
else process.env["REDIS_URL"] = prevRedis;
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
});
|
package/src/cli/drizzle-env.ts
CHANGED
|
@@ -26,7 +26,7 @@ export interface DrizzleKitEnvContext {
|
|
|
26
26
|
*
|
|
27
27
|
* @param cwd - Project root
|
|
28
28
|
*/
|
|
29
|
-
async function readComposeEnv(cwd: string): Promise<Map<string, string>> {
|
|
29
|
+
export async function readComposeEnv(cwd: string): Promise<Map<string, string>> {
|
|
30
30
|
const { path } = resolveComposeEnvPath(cwd);
|
|
31
31
|
const file = Bun.file(path);
|
|
32
32
|
if (!(await file.exists())) return new Map();
|
|
@@ -37,6 +37,27 @@ async function readComposeEnv(cwd: string): Promise<Map<string, string>> {
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Fill unset `process.env` keys from compose `.env.docker` (never overwrites).
|
|
42
|
+
*
|
|
43
|
+
* SQL / Redis binders read `process.env` directly — Vault can resolve the
|
|
44
|
+
* same file, but `oke db seed` must hydrate env before `app.boot()`.
|
|
45
|
+
*
|
|
46
|
+
* @param cwd - Project root
|
|
47
|
+
* @returns Keys that were applied
|
|
48
|
+
*/
|
|
49
|
+
export async function applyComposeEnvToProcess(cwd: string): Promise<readonly string[]> {
|
|
50
|
+
const compose = await readComposeEnv(cwd);
|
|
51
|
+
const applied: string[] = [];
|
|
52
|
+
for (const [key, value] of compose) {
|
|
53
|
+
if (process.env[key] === undefined) {
|
|
54
|
+
process.env[key] = value;
|
|
55
|
+
applied.push(key);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return applied;
|
|
59
|
+
}
|
|
60
|
+
|
|
40
61
|
/**
|
|
41
62
|
* Resolve the drizzle-kit env context for a project + env.
|
|
42
63
|
*
|
|
@@ -13,17 +13,19 @@ const DRIZZLE_CONFIG_TEMPLATE = `import { defineConfig } from "drizzle-kit";
|
|
|
13
13
|
*
|
|
14
14
|
* Dialect comes from OKE's \`store.sql\` driver map (resolved by the CLI and
|
|
15
15
|
* injected as \`OKE_DRIZZLE_DIALECT\`) — never inferred from \`DATABASE_URL\`
|
|
16
|
-
* presence. \`src/schema.generated.ts\` is emitted from \`src/schema.decl.ts\`
|
|
16
|
+
* presence. \`src/db/schema.generated.ts\` is emitted from \`src/db/schema.decl.ts\`
|
|
17
17
|
* for the active dialect as a pre-step of \`oke db\` / \`oke dev\`.
|
|
18
18
|
*
|
|
19
|
-
* Versioned prod migrations land in \`./
|
|
19
|
+
* Versioned prod migrations land in \`./src/db/migrations\` — never mix with \`.oke/\`.
|
|
20
20
|
*/
|
|
21
21
|
const dialect = (process.env.OKE_DRIZZLE_DIALECT ?? "sqlite") as "sqlite" | "postgresql";
|
|
22
22
|
|
|
23
23
|
export default defineConfig({
|
|
24
24
|
dialect,
|
|
25
|
-
schema: "./src/schema.generated.ts",
|
|
26
|
-
out: "./
|
|
25
|
+
schema: "./src/db/schema.generated.ts",
|
|
26
|
+
out: "./src/db/migrations",
|
|
27
|
+
// Core runtime tables (oke_crons, …) are created by drivers — not domain schema.
|
|
28
|
+
tablesFilter: ["!oke_*"],
|
|
27
29
|
dbCredentials: {
|
|
28
30
|
url:
|
|
29
31
|
dialect === "postgresql"
|
|
@@ -49,7 +49,8 @@ describe("hero-meta", () => {
|
|
|
49
49
|
"ai",
|
|
50
50
|
]);
|
|
51
51
|
expect(rows.find((r) => r.element === "store")?.detail).toContain("sqlite");
|
|
52
|
-
expect(rows.find((r) => r.element === "flow")?.detail).toBe("
|
|
52
|
+
expect(rows.find((r) => r.element === "flow")?.detail).toBe("");
|
|
53
|
+
expect(rows.find((r) => r.element === "flow")?.status).toBe("ready");
|
|
53
54
|
});
|
|
54
55
|
|
|
55
56
|
test("docker profile applies to every element", () => {
|
|
@@ -81,4 +82,15 @@ describe("hero-meta", () => {
|
|
|
81
82
|
expect(snap.runtimeEnv).toBe("local");
|
|
82
83
|
expect(decodeHeroSnapshot(encodeHeroSnapshot(snap))).toEqual(snap);
|
|
83
84
|
});
|
|
85
|
+
|
|
86
|
+
test("hero AI row includes configured model id", () => {
|
|
87
|
+
const snap = buildDevHeroSnapshot({
|
|
88
|
+
config: SAMPLE,
|
|
89
|
+
docker: true,
|
|
90
|
+
aiModel: "gemma4:e4b-q4_K_M",
|
|
91
|
+
nodeEnv: "development",
|
|
92
|
+
});
|
|
93
|
+
const ai = snap.elements.find((r) => r.element === "ai")?.detail ?? "";
|
|
94
|
+
expect(ai).toContain("gemma4:e4b-q4_K_M");
|
|
95
|
+
});
|
|
84
96
|
});
|
package/src/cli/hero-meta.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
type DriversConfig,
|
|
10
10
|
type OkeConfig,
|
|
11
11
|
} from "../config/index.ts";
|
|
12
|
+
import type { DevStatus } from "../term.ts";
|
|
12
13
|
|
|
13
14
|
/** How the process is running (local laptop → production). */
|
|
14
15
|
export type DevRuntimeProfile = "local" | "docker" | "test" | "production";
|
|
@@ -21,6 +22,8 @@ export type HeroElementRow = {
|
|
|
21
22
|
readonly element: string;
|
|
22
23
|
/** Driver summary, or `—` when unbound. */
|
|
23
24
|
readonly detail: string;
|
|
25
|
+
/** Status dot — ready / pending / error / idle. */
|
|
26
|
+
readonly status?: DevStatus;
|
|
24
27
|
};
|
|
25
28
|
|
|
26
29
|
/** Serializable hero payload (TTY + soft-reload env). */
|
|
@@ -148,6 +151,10 @@ export function resolveHeroElements(
|
|
|
148
151
|
readonly docker: boolean;
|
|
149
152
|
readonly sqlDriver?: string;
|
|
150
153
|
readonly kvDriver?: string;
|
|
154
|
+
/** Configured model id (`OKE_AI_MODEL`) when known. */
|
|
155
|
+
readonly aiModel?: string;
|
|
156
|
+
/** AI readiness when known (model loading / ready). */
|
|
157
|
+
readonly aiStatus?: DevStatus;
|
|
151
158
|
/** Which driver map column to read (default `local`). */
|
|
152
159
|
readonly configEnv?: ConfigEnv;
|
|
153
160
|
},
|
|
@@ -162,8 +169,13 @@ export function resolveHeroElements(
|
|
|
162
169
|
resolveDriverId(drivers?.store?.kv, configEnv) ??
|
|
163
170
|
(options.docker ? "redis" : "memory");
|
|
164
171
|
|
|
172
|
+
const aiDriver = resolveDriverId(drivers?.ai, configEnv);
|
|
173
|
+
const aiModel = options.aiModel?.trim();
|
|
174
|
+
const aiDetail = aiDriver && aiModel ? `${aiDriver} · ${aiModel}` : (aiDriver ?? "—");
|
|
175
|
+
|
|
165
176
|
const byName: Record<string, string> = {
|
|
166
|
-
flow
|
|
177
|
+
// Status ● is rendered separately — flow has no driver id.
|
|
178
|
+
flow: "",
|
|
167
179
|
signal: resolveDriverId(drivers?.signal, configEnv) ?? "—",
|
|
168
180
|
store: storeDetail(drivers?.store, configEnv, {
|
|
169
181
|
sql: options.sqlDriver,
|
|
@@ -173,13 +185,17 @@ export function resolveHeroElements(
|
|
|
173
185
|
gate: gateKv,
|
|
174
186
|
vault: resolveDriverId(drivers?.vault, configEnv) ?? "—",
|
|
175
187
|
channel: channelDetail(drivers?.channel, configEnv),
|
|
176
|
-
ai:
|
|
188
|
+
ai: aiDetail,
|
|
177
189
|
};
|
|
178
190
|
|
|
179
|
-
return EIGHT_ELEMENTS.map((element) =>
|
|
180
|
-
element
|
|
181
|
-
detail
|
|
182
|
-
|
|
191
|
+
return EIGHT_ELEMENTS.map((element) => {
|
|
192
|
+
const detail = byName[element] ?? "—";
|
|
193
|
+
const idle = detail === "—" || detail === "";
|
|
194
|
+
let status: DevStatus = idle && element !== "flow" ? "idle" : "ready";
|
|
195
|
+
if (element === "flow") status = "ready";
|
|
196
|
+
if (element === "ai" && options.aiStatus && !idle) status = options.aiStatus;
|
|
197
|
+
return { element, detail, status };
|
|
198
|
+
});
|
|
183
199
|
}
|
|
184
200
|
|
|
185
201
|
/**
|
|
@@ -192,6 +208,10 @@ export function buildDevHeroSnapshot(options: {
|
|
|
192
208
|
readonly docker: boolean;
|
|
193
209
|
readonly sqlDriver?: string;
|
|
194
210
|
readonly kvDriver?: string;
|
|
211
|
+
/** Configured model id (`OKE_AI_MODEL`) when known. */
|
|
212
|
+
readonly aiModel?: string;
|
|
213
|
+
/** AI readiness when known (model loading / ready). */
|
|
214
|
+
readonly aiStatus?: DevStatus;
|
|
195
215
|
readonly version?: string;
|
|
196
216
|
readonly nodeEnv?: string;
|
|
197
217
|
}): DevHeroSnapshot {
|
|
@@ -207,6 +227,8 @@ export function buildDevHeroSnapshot(options: {
|
|
|
207
227
|
docker: options.docker,
|
|
208
228
|
sqlDriver: options.sqlDriver,
|
|
209
229
|
kvDriver: options.kvDriver,
|
|
230
|
+
aiModel: options.aiModel,
|
|
231
|
+
aiStatus: options.aiStatus,
|
|
210
232
|
}),
|
|
211
233
|
...(options.version !== undefined ? { version: options.version } : {}),
|
|
212
234
|
};
|
package/src/cli/index.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { imagesCli } from "./images.ts";
|
|
|
19
19
|
import { modeCli } from "./mode.ts";
|
|
20
20
|
import { privacyEraseCli } from "./privacy-erase.ts";
|
|
21
21
|
import { formatOkeHelp } from "./registry.ts";
|
|
22
|
+
import { replayCli } from "./replay.ts";
|
|
22
23
|
import { schemaCli } from "./schema.ts";
|
|
23
24
|
import { stackCli } from "./stack.ts";
|
|
24
25
|
import { startCli } from "./start.ts";
|
|
@@ -88,6 +89,10 @@ if (cmd === "branch") {
|
|
|
88
89
|
process.exit(await branchCli(sub ? [sub, ...rest] : rest));
|
|
89
90
|
}
|
|
90
91
|
|
|
92
|
+
if (cmd === "replay") {
|
|
93
|
+
process.exit(await replayCli(sub ? [sub, ...rest] : rest));
|
|
94
|
+
}
|
|
95
|
+
|
|
91
96
|
if (cmd === "privacy" && sub === "erase") {
|
|
92
97
|
process.exit(await privacyEraseCli(rest));
|
|
93
98
|
}
|
package/src/cli/registry.ts
CHANGED
|
@@ -466,6 +466,30 @@ export const OKE_COMMANDS: readonly CliCommand[] = [
|
|
|
466
466
|
HELP,
|
|
467
467
|
],
|
|
468
468
|
},
|
|
469
|
+
{
|
|
470
|
+
name: "replay",
|
|
471
|
+
summary: "re-invoke a past Flow from a Runs WideEvent",
|
|
472
|
+
leaf: true,
|
|
473
|
+
flags: [
|
|
474
|
+
{
|
|
475
|
+
long: "--request-id",
|
|
476
|
+
short: "-r",
|
|
477
|
+
takesValue: true,
|
|
478
|
+
valueName: "id",
|
|
479
|
+
summary: "WideEvent / run id to replay",
|
|
480
|
+
},
|
|
481
|
+
ENTRY,
|
|
482
|
+
{
|
|
483
|
+
long: "--dry-run",
|
|
484
|
+
summary: "Stub irreversible effects (send / ask)",
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
long: "--live",
|
|
488
|
+
summary: "Allow live re-execution of send / ask effects",
|
|
489
|
+
},
|
|
490
|
+
HELP,
|
|
491
|
+
],
|
|
492
|
+
},
|
|
469
493
|
{
|
|
470
494
|
name: "privacy",
|
|
471
495
|
summary: "crypto-shred subject data",
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { createRunsRuntime } from "../runs/runtime.ts";
|
|
3
|
+
import type { WideEvent } from "../runs/types.ts";
|
|
4
|
+
import { eventHasIrreversible, findRunById, runReplay } from "./replay.ts";
|
|
5
|
+
|
|
6
|
+
function sampleEvent(overrides: Partial<WideEvent> = {}): WideEvent {
|
|
7
|
+
return {
|
|
8
|
+
id: "run-1",
|
|
9
|
+
flow: "notes.create",
|
|
10
|
+
trigger: "http",
|
|
11
|
+
plane: "user",
|
|
12
|
+
gates: [],
|
|
13
|
+
cache: "none",
|
|
14
|
+
error: null,
|
|
15
|
+
input: { title: "hello" },
|
|
16
|
+
effects: [],
|
|
17
|
+
logs: [],
|
|
18
|
+
durationMs: 12,
|
|
19
|
+
startedAt: 1,
|
|
20
|
+
endedAt: 13,
|
|
21
|
+
dimensions: {},
|
|
22
|
+
...overrides,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe("oke replay", () => {
|
|
27
|
+
test("eventHasIrreversible detects send/ask", () => {
|
|
28
|
+
expect(eventHasIrreversible(sampleEvent())).toBe(false);
|
|
29
|
+
expect(
|
|
30
|
+
eventHasIrreversible(
|
|
31
|
+
sampleEvent({
|
|
32
|
+
effects: [
|
|
33
|
+
{
|
|
34
|
+
kind: "send",
|
|
35
|
+
resource: "welcome",
|
|
36
|
+
timestamp: 1,
|
|
37
|
+
duration: 1,
|
|
38
|
+
reversibility: "irreversible",
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
}),
|
|
42
|
+
),
|
|
43
|
+
).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("findRunById + runReplay re-invokes with stored input", async () => {
|
|
47
|
+
const runs = createRunsRuntime({ driver: "memory" });
|
|
48
|
+
await runs.open();
|
|
49
|
+
const event = sampleEvent({ id: "req-42" });
|
|
50
|
+
await runs.append(event);
|
|
51
|
+
|
|
52
|
+
expect(await findRunById(runs, "req-42")).toEqual(event);
|
|
53
|
+
|
|
54
|
+
const lines: string[] = [];
|
|
55
|
+
let seenInput: unknown;
|
|
56
|
+
const code = await runReplay({
|
|
57
|
+
requestId: "req-42",
|
|
58
|
+
runs,
|
|
59
|
+
write: (t) => lines.push(t),
|
|
60
|
+
executeReplay: async (_entry, e, dryRun) => {
|
|
61
|
+
seenInput = e.input;
|
|
62
|
+
expect(dryRun).toBe(false);
|
|
63
|
+
return { output: { ok: true, title: (e.input as { title: string }).title } };
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
expect(code).toBe(0);
|
|
67
|
+
expect(seenInput).toEqual({ title: "hello" });
|
|
68
|
+
expect(lines.some((l) => l.includes("notes.create"))).toBe(true);
|
|
69
|
+
|
|
70
|
+
await runs.close();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("defaults to dry-run when ledger has send", async () => {
|
|
74
|
+
const runs = createRunsRuntime({ driver: "memory" });
|
|
75
|
+
await runs.open();
|
|
76
|
+
await runs.append(
|
|
77
|
+
sampleEvent({
|
|
78
|
+
id: "req-send",
|
|
79
|
+
effects: [
|
|
80
|
+
{
|
|
81
|
+
kind: "send",
|
|
82
|
+
resource: "mail",
|
|
83
|
+
timestamp: 1,
|
|
84
|
+
duration: 2,
|
|
85
|
+
reversibility: "irreversible",
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
}),
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
let dry: boolean | undefined;
|
|
92
|
+
const code = await runReplay({
|
|
93
|
+
requestId: "req-send",
|
|
94
|
+
runs,
|
|
95
|
+
write: () => {},
|
|
96
|
+
executeReplay: async (_e, _ev, dryRun) => {
|
|
97
|
+
dry = dryRun;
|
|
98
|
+
return { output: { ok: true } };
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
expect(code).toBe(0);
|
|
102
|
+
expect(dry).toBe(true);
|
|
103
|
+
await runs.close();
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `oke replay --request-id <id>` — re-invoke a past Flow from a WideEvent.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { resolve } from "node:path";
|
|
6
|
+
import { withDryRun } from "../kernel/dry-run.ts";
|
|
7
|
+
import type { AnyFlowDef } from "../kernel/flow.ts";
|
|
8
|
+
import type { InternalTrigger } from "../kernel/triggers.ts";
|
|
9
|
+
import { createRunsRuntime, type RunsRuntime, type WideEvent } from "../runs/index.ts";
|
|
10
|
+
import { resolveStartEntry } from "./start.ts";
|
|
11
|
+
import { flagValue, hasFlag } from "./args.ts";
|
|
12
|
+
import { EXIT_OK, EXIT_USAGE } from "./exit.ts";
|
|
13
|
+
|
|
14
|
+
/** Options for {@link runReplay}. */
|
|
15
|
+
export interface ReplayOptions {
|
|
16
|
+
readonly requestId: string;
|
|
17
|
+
readonly cwd?: string;
|
|
18
|
+
readonly entry?: string;
|
|
19
|
+
/** Force dry-run (stub irreversible effects). */
|
|
20
|
+
readonly dryRun?: boolean;
|
|
21
|
+
/** Force live replay even when the ledger has send/ask. */
|
|
22
|
+
readonly live?: boolean;
|
|
23
|
+
readonly write?: (text: string) => void;
|
|
24
|
+
/** Injected runs runtime (tests). */
|
|
25
|
+
readonly runs?: RunsRuntime;
|
|
26
|
+
/**
|
|
27
|
+
* Load and execute the app (tests).
|
|
28
|
+
*
|
|
29
|
+
* @param entry - Absolute entry path
|
|
30
|
+
* @param event - Wide event to replay
|
|
31
|
+
* @param dryRun - Whether to wrap in dry-run
|
|
32
|
+
*/
|
|
33
|
+
readonly executeReplay?: (
|
|
34
|
+
entry: string,
|
|
35
|
+
event: WideEvent,
|
|
36
|
+
dryRun: boolean,
|
|
37
|
+
) => Promise<{ readonly output: unknown; readonly failure?: unknown }>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* True when the event's effect ledger includes irreversible kinds.
|
|
42
|
+
*
|
|
43
|
+
* @param event - Wide event
|
|
44
|
+
*/
|
|
45
|
+
export function eventHasIrreversible(event: WideEvent): boolean {
|
|
46
|
+
return event.effects.some((e) => e.kind === "send" || e.kind === "ask");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Look up a WideEvent by id across the runs store.
|
|
51
|
+
*
|
|
52
|
+
* @param runs - Open runs runtime
|
|
53
|
+
* @param id - Request / run id
|
|
54
|
+
*/
|
|
55
|
+
export async function findRunById(runs: RunsRuntime, id: string): Promise<WideEvent | undefined> {
|
|
56
|
+
const all = await runs.all();
|
|
57
|
+
return all.find((e) => e.id === id);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Resolve an OkeApp export from an entry module.
|
|
62
|
+
*
|
|
63
|
+
* @param mod - Imported module
|
|
64
|
+
*/
|
|
65
|
+
export function appFromModule(mod: Record<string, unknown>): {
|
|
66
|
+
execute: (
|
|
67
|
+
flow: AnyFlowDef,
|
|
68
|
+
input: unknown,
|
|
69
|
+
trigger: InternalTrigger,
|
|
70
|
+
) => Promise<{ output: unknown; failure?: unknown }>;
|
|
71
|
+
bindings: readonly { flow: AnyFlowDef }[];
|
|
72
|
+
} | null {
|
|
73
|
+
const candidates = [mod.app, mod.default, mod.oke];
|
|
74
|
+
for (const c of candidates) {
|
|
75
|
+
if (
|
|
76
|
+
c &&
|
|
77
|
+
typeof c === "object" &&
|
|
78
|
+
"execute" in c &&
|
|
79
|
+
typeof (c as { execute: unknown }).execute === "function" &&
|
|
80
|
+
"bindings" in c
|
|
81
|
+
) {
|
|
82
|
+
return c as {
|
|
83
|
+
execute: (
|
|
84
|
+
flow: AnyFlowDef,
|
|
85
|
+
input: unknown,
|
|
86
|
+
trigger: InternalTrigger,
|
|
87
|
+
) => Promise<{ output: unknown; failure?: unknown }>;
|
|
88
|
+
bindings: readonly { flow: AnyFlowDef }[];
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function defaultExecuteReplay(
|
|
96
|
+
entry: string,
|
|
97
|
+
event: WideEvent,
|
|
98
|
+
dryRun: boolean,
|
|
99
|
+
): Promise<{ readonly output: unknown; readonly failure?: unknown }> {
|
|
100
|
+
const mod = (await import(entry)) as Record<string, unknown>;
|
|
101
|
+
const app = appFromModule(mod);
|
|
102
|
+
if (!app) {
|
|
103
|
+
throw new Error(`oke replay: ${entry} must export \`app\` (oke({…}))`);
|
|
104
|
+
}
|
|
105
|
+
const flow = app.bindings.map((b) => b.flow).find((f) => f.name === event.flow);
|
|
106
|
+
if (!flow) {
|
|
107
|
+
throw new Error(`oke replay: flow "${event.flow}" not found in app bindings`);
|
|
108
|
+
}
|
|
109
|
+
if (event.input === undefined) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
`oke replay: run "${event.id}" has no stored input — re-run after upgrading to a build that persists WideEvent.input`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
const trigger: InternalTrigger = { kind: "internal" };
|
|
115
|
+
const run = () => app.execute(flow, event.input, trigger);
|
|
116
|
+
if (dryRun) {
|
|
117
|
+
const wrapped = await withDryRun(run);
|
|
118
|
+
return wrapped.result as { output: unknown; failure?: unknown };
|
|
119
|
+
}
|
|
120
|
+
return run();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Replay a past run locally from the Runs store.
|
|
125
|
+
*
|
|
126
|
+
* @param options - Request id + entry
|
|
127
|
+
*/
|
|
128
|
+
export async function runReplay(options: ReplayOptions): Promise<number> {
|
|
129
|
+
const write = options.write ?? ((t) => process.stdout.write(t));
|
|
130
|
+
const cwd = options.cwd ?? process.cwd();
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
const runs =
|
|
134
|
+
options.runs ??
|
|
135
|
+
createRunsRuntime({
|
|
136
|
+
driver: "files",
|
|
137
|
+
localRoot: resolve(cwd, ".oke/runs"),
|
|
138
|
+
});
|
|
139
|
+
if (!runs.store) await runs.open();
|
|
140
|
+
|
|
141
|
+
const event = await findRunById(runs, options.requestId);
|
|
142
|
+
if (!event) {
|
|
143
|
+
console.error(`oke replay: run not found: ${options.requestId}`);
|
|
144
|
+
return 1;
|
|
145
|
+
}
|
|
146
|
+
if (event.input === undefined) {
|
|
147
|
+
console.error(
|
|
148
|
+
`oke replay: run "${options.requestId}" has no stored input (cannot re-invoke)`,
|
|
149
|
+
);
|
|
150
|
+
return 1;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const irreversible = eventHasIrreversible(event);
|
|
154
|
+
const dryRun = options.live === true ? false : options.dryRun === true || irreversible;
|
|
155
|
+
if (irreversible && !options.live && options.dryRun !== false) {
|
|
156
|
+
write(
|
|
157
|
+
"oke replay: ledger has send/ask — running as dry-run (pass --live to re-execute external effects)\n",
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const execute = options.executeReplay ?? defaultExecuteReplay;
|
|
162
|
+
const entry = options.executeReplay
|
|
163
|
+
? (options.entry ?? "test://entry")
|
|
164
|
+
: await resolveStartEntry(cwd, options.entry);
|
|
165
|
+
write(`oke replay: ${event.flow} ← ${event.id}${dryRun ? " (dry-run)" : ""}\n`);
|
|
166
|
+
const result = await execute(entry, event, dryRun);
|
|
167
|
+
if (result.failure) {
|
|
168
|
+
write(`oke replay: failed ${JSON.stringify(result.failure)}\n`);
|
|
169
|
+
return 1;
|
|
170
|
+
}
|
|
171
|
+
write(`oke replay: ok ${JSON.stringify(result.output ?? null)}\n`);
|
|
172
|
+
return EXIT_OK;
|
|
173
|
+
} catch (err) {
|
|
174
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
175
|
+
return 1;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* CLI entry for `oke replay`.
|
|
181
|
+
*
|
|
182
|
+
* @param args - Args after `replay`
|
|
183
|
+
*/
|
|
184
|
+
export async function replayCli(args: readonly string[]): Promise<number> {
|
|
185
|
+
if (hasFlag(args, "--help", "-h")) {
|
|
186
|
+
console.log(`oke replay --request-id <id> [--entry|-e src/app.ts] [--dry-run] [--live]
|
|
187
|
+
|
|
188
|
+
Re-invoke a past Flow locally from a WideEvent in the Runs store.
|
|
189
|
+
Defaults to dry-run when the ledger contains send/ask effects.
|
|
190
|
+
`);
|
|
191
|
+
return EXIT_OK;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const requestId = flagValue(args, "--request-id", "-r");
|
|
195
|
+
if (!requestId) {
|
|
196
|
+
console.error("Usage: oke replay --request-id <id>");
|
|
197
|
+
return EXIT_USAGE;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return runReplay({
|
|
201
|
+
requestId,
|
|
202
|
+
entry: flagValue(args, "--entry", "-e"),
|
|
203
|
+
dryRun: hasFlag(args, "--dry-run"),
|
|
204
|
+
live: hasFlag(args, "--live"),
|
|
205
|
+
});
|
|
206
|
+
}
|
package/src/compiler/extract.ts
CHANGED
|
@@ -22,6 +22,7 @@ import type {
|
|
|
22
22
|
Journey,
|
|
23
23
|
Manifest,
|
|
24
24
|
RateStrategy,
|
|
25
|
+
ResourceRef,
|
|
25
26
|
SecretContract,
|
|
26
27
|
Signal,
|
|
27
28
|
SignalDelivery,
|
|
@@ -1333,9 +1334,8 @@ function registerResourceMount(
|
|
|
1333
1334
|
const resource = baseName ? scope.resources.get(baseName) : undefined;
|
|
1334
1335
|
|
|
1335
1336
|
const unit = resource?.unit;
|
|
1336
|
-
const storeRef
|
|
1337
|
-
?
|
|
1338
|
-
: never;
|
|
1337
|
+
const storeRef: ResourceRef | undefined =
|
|
1338
|
+
resource?.storeRef !== undefined ? (resource.storeRef as ResourceRef) : undefined;
|
|
1339
1339
|
const effects: Effects | undefined = storeRef
|
|
1340
1340
|
? { reads: [storeRef], writes: [storeRef] }
|
|
1341
1341
|
: undefined;
|
package/src/config/index.ts
CHANGED
|
@@ -155,13 +155,13 @@ export interface DbConfig {
|
|
|
155
155
|
/**
|
|
156
156
|
* Abstract schema declare module (`store.schema.table` exports).
|
|
157
157
|
* When present, `oke db` emits Drizzle into {@link generated} before sync.
|
|
158
|
-
* Default `"src/schema.decl.ts"` (
|
|
158
|
+
* Default `"src/db/schema.decl.ts"` (legacy `"src/schema.decl.ts"` still resolved).
|
|
159
159
|
*/
|
|
160
160
|
readonly declare?: string;
|
|
161
161
|
/**
|
|
162
162
|
* Generated Drizzle schema path written from abstract decls.
|
|
163
|
-
* Default `"src/schema.generated.ts"
|
|
164
|
-
* at this file when using the abstract path.
|
|
163
|
+
* Default `"src/db/schema.generated.ts"` (legacy `"src/schema.generated.ts"`).
|
|
164
|
+
* Point `drizzle.config.ts` `schema` at this file when using the abstract path.
|
|
165
165
|
*/
|
|
166
166
|
readonly generated?: string;
|
|
167
167
|
/**
|
|
@@ -58,7 +58,9 @@ export function mintClaimCode(now: () => number = Date.now): ClaimCodeState {
|
|
|
58
58
|
*/
|
|
59
59
|
export function printClaimCodeOnce(
|
|
60
60
|
state: ClaimCodeState,
|
|
61
|
-
write: (line: string) => void = (line) =>
|
|
61
|
+
write: (line: string) => void = (line) => {
|
|
62
|
+
process.stdout.write(line);
|
|
63
|
+
},
|
|
62
64
|
): void {
|
|
63
65
|
if (state.printed) return;
|
|
64
66
|
state.printed = true;
|