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,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WhatsApp Business characterization (Known gap freeze):
|
|
3
|
+
* - drivers pick template vs free-text by presence of message.template only
|
|
4
|
+
* - Channel runtime has no 24h session-window API
|
|
5
|
+
*
|
|
6
|
+
* Meta requires free-form session messages inside a customer-initiated 24h
|
|
7
|
+
* window and pre-approved templates outside it. OKE does not enforce that
|
|
8
|
+
* window today — this file freezes the current payload-shape behavior so the
|
|
9
|
+
* gap cannot silently look fixed without a real design.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, expect, test } from "bun:test";
|
|
13
|
+
import type {
|
|
14
|
+
ChannelDriver,
|
|
15
|
+
ChannelMessage,
|
|
16
|
+
ChannelSendResult,
|
|
17
|
+
} from "../../drivers/channel-types.ts";
|
|
18
|
+
import { channel, createChannelRuntime } from "../channel.ts";
|
|
19
|
+
|
|
20
|
+
type Captured =
|
|
21
|
+
| { kind: "template"; name: string; language: string; to: string }
|
|
22
|
+
| { kind: "text"; text: string; to: string };
|
|
23
|
+
|
|
24
|
+
function fakeWhatsAppDriver(captures: Captured[]): ChannelDriver {
|
|
25
|
+
const channelTransport = {
|
|
26
|
+
provider: "wa-cloud",
|
|
27
|
+
mediums: ["whatsapp" as const],
|
|
28
|
+
async send(message: ChannelMessage): Promise<ChannelSendResult> {
|
|
29
|
+
const templateName =
|
|
30
|
+
message.template ??
|
|
31
|
+
(typeof message.data?.template === "string" ? message.data.template : undefined);
|
|
32
|
+
const language =
|
|
33
|
+
message.locale ??
|
|
34
|
+
(typeof message.data?.language === "string" ? message.data.language : "en_US");
|
|
35
|
+
|
|
36
|
+
if (templateName) {
|
|
37
|
+
captures.push({
|
|
38
|
+
kind: "template",
|
|
39
|
+
name: templateName,
|
|
40
|
+
language,
|
|
41
|
+
to: message.to,
|
|
42
|
+
});
|
|
43
|
+
} else {
|
|
44
|
+
captures.push({
|
|
45
|
+
kind: "text",
|
|
46
|
+
text: message.text ?? "",
|
|
47
|
+
to: message.to,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
ok: true,
|
|
53
|
+
messageId: "wa-1",
|
|
54
|
+
driverId: "wa-cloud",
|
|
55
|
+
attempts: [{ driverId: "wa-cloud", ok: true, at: Date.now(), messageId: "wa-1" }],
|
|
56
|
+
};
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
id: "wa-cloud",
|
|
62
|
+
channel: channelTransport,
|
|
63
|
+
whatsappTransport: {
|
|
64
|
+
provider: "wa-cloud",
|
|
65
|
+
async send() {
|
|
66
|
+
return { messageId: "x", to: "", status: "sent", response: "" };
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
describe("WhatsApp session vs template (characterization)", () => {
|
|
73
|
+
test("template name on the send path selects template payload shape", async () => {
|
|
74
|
+
const captures: Captured[] = [];
|
|
75
|
+
const runtime = createChannelRuntime({
|
|
76
|
+
templates: [channel.template("order-update", { medium: "whatsapp" })],
|
|
77
|
+
drivers: [fakeWhatsAppDriver(captures)],
|
|
78
|
+
catalog: {
|
|
79
|
+
"order-update": { en: { text: "Your order shipped" } },
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
await runtime.send("order-update", {
|
|
84
|
+
to: "+966500000000",
|
|
85
|
+
locale: "en_US",
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
expect(captures).toEqual([
|
|
89
|
+
{
|
|
90
|
+
kind: "template",
|
|
91
|
+
name: "order-update",
|
|
92
|
+
language: "en_US",
|
|
93
|
+
to: "+966500000000",
|
|
94
|
+
},
|
|
95
|
+
]);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("missing template name sends free-text (no session-window gate)", async () => {
|
|
99
|
+
const captures: Captured[] = [];
|
|
100
|
+
// Medium-agnostic path that still hits channel transport without a template
|
|
101
|
+
// name on ChannelMessage — simulate driver-level choice when template unset.
|
|
102
|
+
const driver = fakeWhatsAppDriver(captures);
|
|
103
|
+
const result = await driver.channel!.send({
|
|
104
|
+
medium: "whatsapp",
|
|
105
|
+
to: "+966500000000",
|
|
106
|
+
text: "Hello from session",
|
|
107
|
+
// no template field — free-form
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
expect(result.ok).toBe(true);
|
|
111
|
+
expect(captures).toEqual([{ kind: "text", text: "Hello from session", to: "+966500000000" }]);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("ChannelRuntime has no session-window surface", () => {
|
|
115
|
+
const runtime = createChannelRuntime({});
|
|
116
|
+
const keys = Object.keys(runtime);
|
|
117
|
+
expect(keys).not.toContain("sessionWindow");
|
|
118
|
+
expect(keys).not.toContain("whatsappSession");
|
|
119
|
+
expect(keys).not.toContain("withinSessionWindow");
|
|
120
|
+
expect(typeof (runtime as { withinSessionWindow?: unknown }).withinSessionWindow).toBe(
|
|
121
|
+
"undefined",
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
@@ -53,7 +53,7 @@ export function tier1KeysForReads(
|
|
|
53
53
|
effects: Effects,
|
|
54
54
|
dimsByResource?: Readonly<Record<string, readonly string[]>>,
|
|
55
55
|
): string[] {
|
|
56
|
-
const reads = effects.reads ?? [];
|
|
56
|
+
const reads = (effects.reads ?? []).filter((r): r is ResourceRef => r !== "runs");
|
|
57
57
|
return reads.map((resource) => computedCacheKey(resource, dimsByResource?.[resource]));
|
|
58
58
|
}
|
|
59
59
|
|
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
* watch) when abstract decls exist — not a third schema CLI.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { existsSync } from "node:fs";
|
|
8
9
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
9
|
-
import { dirname, join
|
|
10
|
+
import { dirname, join } from "node:path";
|
|
10
11
|
import type { TableContribution } from "../../kernel/plugin.ts";
|
|
11
12
|
import {
|
|
12
13
|
isFieldBuilder,
|
|
@@ -30,10 +31,16 @@ export type SqlDialect = "sqlite" | "postgres";
|
|
|
30
31
|
export const GENERATED_SCHEMA_HEADER = "// generated by oke — do not edit";
|
|
31
32
|
|
|
32
33
|
/** Default output path (relative to project root). */
|
|
33
|
-
export const DEFAULT_GENERATED_SCHEMA_REL = "src/schema.generated.ts";
|
|
34
|
+
export const DEFAULT_GENERATED_SCHEMA_REL = "src/db/schema.generated.ts";
|
|
34
35
|
|
|
35
36
|
/** Default declaration module path (relative to project root). */
|
|
36
|
-
export const DEFAULT_DECLARE_SCHEMA_REL = "src/schema.decl.ts";
|
|
37
|
+
export const DEFAULT_DECLARE_SCHEMA_REL = "src/db/schema.decl.ts";
|
|
38
|
+
|
|
39
|
+
/** Legacy paths kept so pre-layout projects keep resolving. */
|
|
40
|
+
export const LEGACY_GENERATED_SCHEMA_REL = "src/schema.generated.ts";
|
|
41
|
+
|
|
42
|
+
/** @see LEGACY_GENERATED_SCHEMA_REL */
|
|
43
|
+
export const LEGACY_DECLARE_SCHEMA_REL = "src/schema.decl.ts";
|
|
37
44
|
|
|
38
45
|
/**
|
|
39
46
|
* Map a store.sql driver id to an emit dialect.
|
|
@@ -306,8 +313,16 @@ export async function maybeEmitDomainSchema(
|
|
|
306
313
|
options: EmitDomainSchemaOptions,
|
|
307
314
|
): Promise<{ readonly emitted: boolean; readonly path?: string; readonly tableCount?: number }> {
|
|
308
315
|
const cwd = options.cwd;
|
|
309
|
-
const
|
|
310
|
-
|
|
316
|
+
const resolved = resolveEmitPaths(cwd, {
|
|
317
|
+
...(options.declarePath !== undefined ? { declare: options.declarePath } : {}),
|
|
318
|
+
...(options.outPath !== undefined ? { generated: options.outPath } : {}),
|
|
319
|
+
});
|
|
320
|
+
const declareAbs = resolved.declarePath;
|
|
321
|
+
const outAbs = resolved.outPath;
|
|
322
|
+
const declareRel = declareAbs.startsWith(`${cwd}/`)
|
|
323
|
+
? declareAbs.slice(cwd.length + 1)
|
|
324
|
+
: declareAbs;
|
|
325
|
+
const outRel = outAbs.startsWith(`${cwd}/`) ? outAbs.slice(cwd.length + 1) : outAbs;
|
|
311
326
|
const declareFile = Bun.file(declareAbs);
|
|
312
327
|
|
|
313
328
|
let appTables: SchemaTableDecl[] = [];
|
|
@@ -332,8 +347,6 @@ export async function maybeEmitDomainSchema(
|
|
|
332
347
|
}
|
|
333
348
|
|
|
334
349
|
const tables = mergeSchemaTables(appTables, fromPlugins);
|
|
335
|
-
const outRel = options.outPath ?? DEFAULT_GENERATED_SCHEMA_REL;
|
|
336
|
-
const outAbs = resolve(cwd, outRel);
|
|
337
350
|
const source = emitDrizzleSource(tables, options.dialect, { relations: appRelations });
|
|
338
351
|
await mkdir(dirname(outAbs), { recursive: true });
|
|
339
352
|
await writeFile(outAbs, source, "utf8");
|
|
@@ -345,6 +358,7 @@ export async function maybeEmitDomainSchema(
|
|
|
345
358
|
|
|
346
359
|
/**
|
|
347
360
|
* Resolve declare/out paths from an optional db config slice.
|
|
361
|
+
* Prefers explicit config, then new `src/db/*` defaults, then legacy `src/*`.
|
|
348
362
|
*
|
|
349
363
|
* @param cwd - Project root
|
|
350
364
|
* @param db - Config `db` block
|
|
@@ -353,9 +367,29 @@ export function resolveEmitPaths(
|
|
|
353
367
|
cwd: string,
|
|
354
368
|
db?: { readonly declare?: string; readonly generated?: string },
|
|
355
369
|
): { readonly declarePath: string; readonly outPath: string } {
|
|
370
|
+
let declareRel = db?.declare;
|
|
371
|
+
if (!declareRel) {
|
|
372
|
+
if (existsSync(join(cwd, DEFAULT_DECLARE_SCHEMA_REL))) {
|
|
373
|
+
declareRel = DEFAULT_DECLARE_SCHEMA_REL;
|
|
374
|
+
} else if (existsSync(join(cwd, LEGACY_DECLARE_SCHEMA_REL))) {
|
|
375
|
+
declareRel = LEGACY_DECLARE_SCHEMA_REL;
|
|
376
|
+
} else {
|
|
377
|
+
declareRel = DEFAULT_DECLARE_SCHEMA_REL;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
let outRel = db?.generated;
|
|
382
|
+
if (!outRel) {
|
|
383
|
+
const preferLegacy =
|
|
384
|
+
declareRel === LEGACY_DECLARE_SCHEMA_REL ||
|
|
385
|
+
(!existsSync(join(cwd, DEFAULT_DECLARE_SCHEMA_REL)) &&
|
|
386
|
+
existsSync(join(cwd, LEGACY_GENERATED_SCHEMA_REL)));
|
|
387
|
+
outRel = preferLegacy ? LEGACY_GENERATED_SCHEMA_REL : DEFAULT_GENERATED_SCHEMA_REL;
|
|
388
|
+
}
|
|
389
|
+
|
|
356
390
|
return {
|
|
357
|
-
declarePath: join(cwd,
|
|
358
|
-
outPath: join(cwd,
|
|
391
|
+
declarePath: join(cwd, declareRel),
|
|
392
|
+
outPath: join(cwd, outRel),
|
|
359
393
|
};
|
|
360
394
|
}
|
|
361
395
|
|
|
@@ -473,7 +473,7 @@ export function createStoreRuntime(options: CreateStoreRuntimeOptions): StoreRun
|
|
|
473
473
|
},
|
|
474
474
|
putTier1(effects, value, dimsByResource) {
|
|
475
475
|
const keys = tier1KeysForReads(effects, dimsByResource);
|
|
476
|
-
const resources = effects.reads ?? [];
|
|
476
|
+
const resources = (effects.reads ?? []).filter((r): r is ResourceRef => r !== "runs");
|
|
477
477
|
for (const key of keys) {
|
|
478
478
|
cache.set({
|
|
479
479
|
tier: 1,
|
|
@@ -39,6 +39,8 @@ describe("store.schema.table + field.*", () => {
|
|
|
39
39
|
expect(notes.columns.createdAt.sqlType).toBe("integer");
|
|
40
40
|
expect(notes.columns.id.defaultFnKind).toBe("id");
|
|
41
41
|
expect(notes.columns.createdAt.defaultFnKind).toBe("now");
|
|
42
|
+
expect(typeof notes.id.getSQL).toBe("function");
|
|
43
|
+
expect(() => notes.id.getSQL()).toThrow(/type bridge/);
|
|
42
44
|
});
|
|
43
45
|
|
|
44
46
|
test("PII masks via declaration path without Drizzle metadata", async () => {
|
|
@@ -34,6 +34,10 @@ export interface ColumnReference {
|
|
|
34
34
|
/**
|
|
35
35
|
* Finalized column declaration — also satisfies {@link ColumnDef} so
|
|
36
36
|
* {@link classificationsFromTable} / runtime masking work without Drizzle.
|
|
37
|
+
*
|
|
38
|
+
* `getSQL` is a structural bridge so drizzle-orm operators (`eq`, `isNull`, …)
|
|
39
|
+
* accept abstract columns in TypeScript. OKE’s SQL compiler never calls it —
|
|
40
|
+
* it reads {@link SchemaColumnDecl.sqlName} / metadata directly.
|
|
37
41
|
*/
|
|
38
42
|
export interface SchemaColumnDecl extends ColumnDef {
|
|
39
43
|
/** JS object key (e.g. `createdAt`). */
|
|
@@ -57,6 +61,12 @@ export interface SchemaColumnDecl extends ColumnDef {
|
|
|
57
61
|
readonly references?: ColumnReference;
|
|
58
62
|
/** Optional human description for Console / docs (falls back to the JS key). */
|
|
59
63
|
readonly description?: string;
|
|
64
|
+
/**
|
|
65
|
+
* Drizzle `SQLWrapper` structural match — never invoked at runtime.
|
|
66
|
+
*
|
|
67
|
+
* @returns never (throws if called)
|
|
68
|
+
*/
|
|
69
|
+
getSQL(): never;
|
|
60
70
|
}
|
|
61
71
|
|
|
62
72
|
/** Table from {@link store.schema.table} — extends {@link TableHandle}. */
|
|
@@ -171,6 +181,11 @@ function createBuilder(state: FieldState): FieldBuilder {
|
|
|
171
181
|
...(state.classification ? { classification: state.classification } : {}),
|
|
172
182
|
...(state.description !== undefined ? { description: state.description } : {}),
|
|
173
183
|
...(state.references ? { references: state.references } : {}),
|
|
184
|
+
getSQL(): never {
|
|
185
|
+
throw new Error(
|
|
186
|
+
`SchemaColumnDecl.getSQL("${sqlName}") is a type bridge — OKE compiles columns directly`,
|
|
187
|
+
);
|
|
188
|
+
},
|
|
174
189
|
};
|
|
175
190
|
},
|
|
176
191
|
};
|
|
@@ -30,7 +30,7 @@ export interface SeedDef {
|
|
|
30
30
|
export type SeedCategory = "dev" | "prod";
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
|
-
* Freeze a seed declaration for `src/seed/index.ts` default export.
|
|
33
|
+
* Freeze a seed declaration for `src/db/seed/index.ts` default export.
|
|
34
34
|
*
|
|
35
35
|
* @param def - Essential / dev / prod function bags
|
|
36
36
|
*/
|
package/src/index.ts
CHANGED
package/src/kernel/app.ts
CHANGED
|
@@ -59,7 +59,9 @@ import {
|
|
|
59
59
|
logDevRequest,
|
|
60
60
|
shouldLogDevRequests,
|
|
61
61
|
} from "../runtime/dev-request-log.ts";
|
|
62
|
+
import { fail, type FlowFailure } from "./errors.ts";
|
|
62
63
|
import {
|
|
64
|
+
isFlowFailure,
|
|
63
65
|
mergeHooks,
|
|
64
66
|
runPipeline,
|
|
65
67
|
type HookFn,
|
|
@@ -359,6 +361,10 @@ export interface OkeApp<D extends Record<string, unknown> = {}, R extends AppRou
|
|
|
359
361
|
readonly operator?: FxOperator;
|
|
360
362
|
/** Pre-resolved principal (either plane). */
|
|
361
363
|
readonly principal?: ResolvedPrincipal;
|
|
364
|
+
/** Parent WideEvent id when this execution was caused by another. */
|
|
365
|
+
readonly parentId?: string;
|
|
366
|
+
/** Explicit run / WideEvent id (defaults to a new UUID). */
|
|
367
|
+
readonly runId?: string;
|
|
362
368
|
},
|
|
363
369
|
): Promise<ExecuteResult>;
|
|
364
370
|
/**
|
|
@@ -381,8 +387,13 @@ export interface OkeApp<D extends Record<string, unknown> = {}, R extends AppRou
|
|
|
381
387
|
*
|
|
382
388
|
* @param signal - Signal name or handle
|
|
383
389
|
* @param payload - Payload
|
|
390
|
+
* @param meta - Optional envelope from the bus (producer run id)
|
|
384
391
|
*/
|
|
385
|
-
dispatchSignal(
|
|
392
|
+
dispatchSignal(
|
|
393
|
+
signal: NamedRef,
|
|
394
|
+
payload?: unknown,
|
|
395
|
+
meta?: { readonly parentRunId?: string; readonly messageId?: string },
|
|
396
|
+
): Promise<ExecuteResult[]>;
|
|
386
397
|
/**
|
|
387
398
|
* Invoke all flows bound to an `every` interval.
|
|
388
399
|
*
|
|
@@ -637,8 +648,12 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
637
648
|
}
|
|
638
649
|
}
|
|
639
650
|
|
|
640
|
-
async function handleSignalFire(
|
|
641
|
-
|
|
651
|
+
async function handleSignalFire(
|
|
652
|
+
name: string,
|
|
653
|
+
payload: unknown,
|
|
654
|
+
meta?: { readonly parentRunId?: string; readonly messageId?: string },
|
|
655
|
+
): Promise<void> {
|
|
656
|
+
await app.dispatchSignal(name, payload, meta);
|
|
642
657
|
}
|
|
643
658
|
|
|
644
659
|
async function handleDurableResume(): Promise<void> {
|
|
@@ -918,6 +933,10 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
918
933
|
readonly originPrincipal?: FxPrincipal;
|
|
919
934
|
/** Explicit locale override for {@link Fx.t} / channel sends. */
|
|
920
935
|
readonly locale?: string;
|
|
936
|
+
/** Parent WideEvent id when this execution was caused by another. */
|
|
937
|
+
readonly parentId?: string;
|
|
938
|
+
/** Explicit run / WideEvent id (defaults to a new UUID). */
|
|
939
|
+
readonly runId?: string;
|
|
921
940
|
},
|
|
922
941
|
): Promise<ExecuteResult> {
|
|
923
942
|
registerFlow(flowDef);
|
|
@@ -957,6 +976,8 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
957
976
|
readonly principal?: ResolvedPrincipal;
|
|
958
977
|
readonly originPrincipal?: FxPrincipal;
|
|
959
978
|
readonly locale?: string;
|
|
979
|
+
readonly parentId?: string;
|
|
980
|
+
readonly runId?: string;
|
|
960
981
|
};
|
|
961
982
|
readonly resolvedLocale: string;
|
|
962
983
|
readonly defaultLocale: string;
|
|
@@ -991,6 +1012,15 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
991
1012
|
const now = options.fx?.now ?? booted?.clock?.now ?? (() => Date.now());
|
|
992
1013
|
const startedAt = now();
|
|
993
1014
|
const telemetry = createRunTelemetry();
|
|
1015
|
+
const inboundId =
|
|
1016
|
+
extras?.request?.headers.get("x-request-id")?.trim() ||
|
|
1017
|
+
extras?.request?.headers.get("x-correlation-id")?.trim() ||
|
|
1018
|
+
undefined;
|
|
1019
|
+
const runId =
|
|
1020
|
+
extras?.runId ??
|
|
1021
|
+
(inboundId && inboundId.length > 0 ? inboundId : undefined) ??
|
|
1022
|
+
crypto.randomUUID();
|
|
1023
|
+
const parentId = extras?.parentId;
|
|
994
1024
|
|
|
995
1025
|
// Element pipeline wiring only kicks in once booted — `autoBoot: false`
|
|
996
1026
|
// keeps intentional pre-boot unit tests ungated (no elements either).
|
|
@@ -1057,6 +1087,7 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1057
1087
|
const journal = createJournal({
|
|
1058
1088
|
store,
|
|
1059
1089
|
now,
|
|
1090
|
+
id: () => runId,
|
|
1060
1091
|
// Hold the run lease for the request's lifetime — a crash mid-run
|
|
1061
1092
|
// leaves an expired lease another instance can reclaim and resume.
|
|
1062
1093
|
...(hasJournalLease(store) ? { lease: { instanceId, leaseMs } } : {}),
|
|
@@ -1071,6 +1102,7 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1071
1102
|
flow: flowDef.name,
|
|
1072
1103
|
effects: flowDef.effects,
|
|
1073
1104
|
runTelemetry: telemetry,
|
|
1105
|
+
runId,
|
|
1074
1106
|
now,
|
|
1075
1107
|
i18n: {
|
|
1076
1108
|
locale: resolvedLocale,
|
|
@@ -1090,6 +1122,7 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1090
1122
|
aiRuntime: booted.ai,
|
|
1091
1123
|
}
|
|
1092
1124
|
: {}),
|
|
1125
|
+
runsRuntime: booted?.runs ?? (isRunsRuntimeLike(options.runs) ? options.runs : undefined),
|
|
1093
1126
|
...(journalSession ? { journal: journalSession, durable: true } : {}),
|
|
1094
1127
|
callHandler: async (name, callInput) => {
|
|
1095
1128
|
const target = flowsByName.get(name);
|
|
@@ -1103,6 +1136,7 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1103
1136
|
{
|
|
1104
1137
|
originPrincipal: freezePrincipal(fx.principal),
|
|
1105
1138
|
locale: resolvedLocale,
|
|
1139
|
+
parentId: runId,
|
|
1106
1140
|
},
|
|
1107
1141
|
);
|
|
1108
1142
|
if (inner.failure) return inner.failure;
|
|
@@ -1174,10 +1208,32 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1174
1208
|
input: ctx.input,
|
|
1175
1209
|
wakeAt: sleeping.wakeAt,
|
|
1176
1210
|
});
|
|
1177
|
-
} else if (!sleeping && result
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1211
|
+
} else if (!sleeping && isTerminalFailure(result)) {
|
|
1212
|
+
const terminalErr = result.failure ?? result.ctx.error;
|
|
1213
|
+
if (flowDef.compensate) {
|
|
1214
|
+
try {
|
|
1215
|
+
const completedSteps = journalSession.run.entries
|
|
1216
|
+
.filter((e) => e.kind === "step")
|
|
1217
|
+
.map((e) => e.name);
|
|
1218
|
+
await flowDef.compensate(
|
|
1219
|
+
{
|
|
1220
|
+
input: ctx.input as never,
|
|
1221
|
+
error: terminalErr,
|
|
1222
|
+
completedSteps,
|
|
1223
|
+
},
|
|
1224
|
+
fx,
|
|
1225
|
+
);
|
|
1226
|
+
} catch (compErr) {
|
|
1227
|
+
await journalSession.commit("failed", {
|
|
1228
|
+
error: `compensate:${failureCodeOf(compErr)}`,
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
if (journalSession.run.status === "running") {
|
|
1233
|
+
await journalSession.commit("failed", {
|
|
1234
|
+
error: failureCodeOf(terminalErr),
|
|
1235
|
+
});
|
|
1236
|
+
}
|
|
1181
1237
|
} else if (!sleeping) {
|
|
1182
1238
|
await journalSession.commit("completed", { output: result.output });
|
|
1183
1239
|
}
|
|
@@ -1187,6 +1243,10 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1187
1243
|
booted?.runs ?? (isRunsRuntimeLike(options.runs) ? options.runs : undefined);
|
|
1188
1244
|
if (runsRuntime) {
|
|
1189
1245
|
const archiveCleartext = archiveFromInput(ctx.input, options.archiveInputFields);
|
|
1246
|
+
const replayInput = redactArchivedFields(ctx.input, options.archiveInputFields);
|
|
1247
|
+
const recordFailure =
|
|
1248
|
+
result.failure ??
|
|
1249
|
+
(isTerminalFailure(result) ? failureFromUnknown(result.ctx.error) : undefined);
|
|
1190
1250
|
await runsRuntime.record(
|
|
1191
1251
|
{
|
|
1192
1252
|
flow: flowDef,
|
|
@@ -1196,7 +1256,10 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1196
1256
|
telemetry,
|
|
1197
1257
|
startedAt,
|
|
1198
1258
|
endedAt,
|
|
1199
|
-
failure:
|
|
1259
|
+
failure: recordFailure,
|
|
1260
|
+
id: runId,
|
|
1261
|
+
input: replayInput,
|
|
1262
|
+
...(parentId !== undefined ? { parentId } : {}),
|
|
1200
1263
|
},
|
|
1201
1264
|
archiveCleartext,
|
|
1202
1265
|
);
|
|
@@ -1480,12 +1543,16 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
1480
1543
|
|
|
1481
1544
|
return respond(encodeExecuteResult(result));
|
|
1482
1545
|
},
|
|
1483
|
-
async dispatchSignal(signal, payload) {
|
|
1546
|
+
async dispatchSignal(signal, payload, meta) {
|
|
1484
1547
|
const name = resolveName(signal);
|
|
1485
1548
|
const results: ExecuteResult[] = [];
|
|
1486
1549
|
for (const b of adopted) {
|
|
1487
1550
|
if (b.trigger.kind === "signal" && b.trigger.name === name) {
|
|
1488
|
-
results.push(
|
|
1551
|
+
results.push(
|
|
1552
|
+
await execute(b.flow, payload, b.trigger as SignalAsTrigger, {
|
|
1553
|
+
...(meta?.parentRunId !== undefined ? { parentId: meta.parentRunId } : {}),
|
|
1554
|
+
}),
|
|
1555
|
+
);
|
|
1489
1556
|
}
|
|
1490
1557
|
}
|
|
1491
1558
|
return results;
|
|
@@ -1555,6 +1622,26 @@ function compileHttpBinding(binding: Binding, aot: boolean): CompiledRoute {
|
|
|
1555
1622
|
);
|
|
1556
1623
|
}
|
|
1557
1624
|
|
|
1625
|
+
/** True when the pipeline ended in a terminal failure (not sleep park). */
|
|
1626
|
+
function isTerminalFailure(result: PipelineResult): boolean {
|
|
1627
|
+
if (result.failure) return true;
|
|
1628
|
+
return result.ctx.error !== undefined && !isJournalSuspend(result.ctx.error);
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
/** Machine-readable code for journal / Runs failure fields. */
|
|
1632
|
+
function failureCodeOf(err: unknown): string {
|
|
1633
|
+
if (isFlowFailure(err)) return err.error.code;
|
|
1634
|
+
if (err instanceof Error && err.name && err.name !== "Error") return err.name;
|
|
1635
|
+
if (err instanceof Error && err.message) return err.message.slice(0, 120);
|
|
1636
|
+
return "Error";
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
/** Coerce a thrown value into a FlowFailure for Runs wide events. */
|
|
1640
|
+
function failureFromUnknown(err: unknown): FlowFailure {
|
|
1641
|
+
if (isFlowFailure(err)) return err;
|
|
1642
|
+
return fail(failureCodeOf(err), {});
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1558
1645
|
/**
|
|
1559
1646
|
* Pull personal fields from validated input for crypto-shred archival.
|
|
1560
1647
|
*
|
|
@@ -1576,6 +1663,26 @@ function archiveFromInput(
|
|
|
1576
1663
|
return Object.keys(out).length > 0 ? out : undefined;
|
|
1577
1664
|
}
|
|
1578
1665
|
|
|
1666
|
+
/**
|
|
1667
|
+
* Clone input with archived personal fields replaced by shred markers so the
|
|
1668
|
+
* replay snapshot never retains cleartext for those keys.
|
|
1669
|
+
*
|
|
1670
|
+
* @param input - Validated flow input
|
|
1671
|
+
* @param fields - Property names that go to {@link archiveFromInput}
|
|
1672
|
+
*/
|
|
1673
|
+
function redactArchivedFields(input: unknown, fields: readonly string[] | undefined): unknown {
|
|
1674
|
+
if (input === undefined) return undefined;
|
|
1675
|
+
if (!fields || fields.length === 0) return input;
|
|
1676
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) return input;
|
|
1677
|
+
const obj = { ...(input as Record<string, unknown>) };
|
|
1678
|
+
for (const name of fields) {
|
|
1679
|
+
if (typeof obj[name] === "string" && (obj[name] as string).length > 0) {
|
|
1680
|
+
obj[name] = "[archived]";
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
return obj;
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1579
1686
|
/**
|
|
1580
1687
|
* Deep-merge channel template catalogs (later parts win per locale).
|
|
1581
1688
|
*
|
|
@@ -7,6 +7,7 @@ import { memorySignalDriver } from "../../drivers/signal-memory.ts";
|
|
|
7
7
|
import { createBunSignalRedisClient, redisSignalDriver } from "../../drivers/signal-redis.ts";
|
|
8
8
|
import type { SignalRedisClientLike } from "../../drivers/signal-types.ts";
|
|
9
9
|
import { createSignalRuntime, type SignalRuntime } from "../../elements/signal.ts";
|
|
10
|
+
import { emitBootWarn } from "../../runtime/boot-warn.ts";
|
|
10
11
|
import type { BootOptions } from "../boot.ts";
|
|
11
12
|
|
|
12
13
|
/**
|
|
@@ -35,7 +36,7 @@ let signalRedisWarned = false;
|
|
|
35
36
|
function warnSignalRedisProcessLocal(): void {
|
|
36
37
|
if (signalRedisWarned) return;
|
|
37
38
|
signalRedisWarned = true;
|
|
38
|
-
|
|
39
|
+
emitBootWarn(
|
|
39
40
|
"oke boot: drivers.signal redis — emit relays to Redis, but consume/live/drain use a " +
|
|
40
41
|
"process-local outbox (not multi-instance competing consumers). Prefer a shared durable " +
|
|
41
42
|
"outbox path for multi-process tests, or a single consumer instance, until Redis Streams " +
|
|
@@ -129,7 +130,10 @@ export async function bindSignal(
|
|
|
129
130
|
if (seen.has(name)) continue;
|
|
130
131
|
seen.add(name);
|
|
131
132
|
await bus.subscribe(name, `oke:${name}`, async (msg) => {
|
|
132
|
-
await handler(name, msg.payload
|
|
133
|
+
await handler(name, msg.payload, {
|
|
134
|
+
...(msg.parentRunId !== undefined ? { parentRunId: msg.parentRunId } : {}),
|
|
135
|
+
messageId: msg.id,
|
|
136
|
+
});
|
|
133
137
|
});
|
|
134
138
|
}
|
|
135
139
|
}
|
|
@@ -16,6 +16,7 @@ import { sqliteDriver } from "../../drivers/sqlite.ts";
|
|
|
16
16
|
import type { FilesDriver, IndexDriver, KvDriver, SqlDriver } from "../../drivers/types.ts";
|
|
17
17
|
import { createStoreRuntime, type StoreRuntime } from "../../elements/store.ts";
|
|
18
18
|
import type { StoreDecl } from "../../elements/store/declare.ts";
|
|
19
|
+
import { emitBootWarn } from "../../runtime/boot-warn.ts";
|
|
19
20
|
import type { BootOptions } from "../boot.ts"; // type-only — no cycle at runtime
|
|
20
21
|
|
|
21
22
|
/**
|
|
@@ -43,7 +44,7 @@ export function resetFilesFsWarnForTests(): void {
|
|
|
43
44
|
function warnFilesFsMultiInstance(): void {
|
|
44
45
|
if (filesFsWarned) return;
|
|
45
46
|
filesFsWarned = true;
|
|
46
|
-
|
|
47
|
+
emitBootWarn(
|
|
47
48
|
'oke boot: drivers.store.files "fs" is single-host — each instance sees its own ' +
|
|
48
49
|
"filesystem (temp root when unbound). For horizontal scale use drivers.store.files s3 " +
|
|
49
50
|
"(or an explicitly shared volume root, still single-host semantics).",
|
package/src/kernel/boot.test.ts
CHANGED
|
@@ -79,7 +79,7 @@ describe("boot — lazy element needs", () => {
|
|
|
79
79
|
expect(needs.signal).toBe(false);
|
|
80
80
|
});
|
|
81
81
|
|
|
82
|
-
test("oke() Store-only graph stays under the prior
|
|
82
|
+
test("oke() Store-only graph stays under the prior 54 kB baseline", async () => {
|
|
83
83
|
const dir = await mkdtemp(join(tmpdir(), "oke-store-only-"));
|
|
84
84
|
const entry = join(dir, "entry.ts");
|
|
85
85
|
const appPath = join(import.meta.dir, "app.ts");
|
|
@@ -117,10 +117,11 @@ describe("boot — lazy element needs", () => {
|
|
|
117
117
|
total += Bun.gzipSync(new Uint8Array(raw)).byteLength;
|
|
118
118
|
}
|
|
119
119
|
// Rebased after fx.sendOtp/verifyOtp on the shared fx surface (~50.1 kB
|
|
120
|
-
// gzip), durable-journal lease surface (~51.2 kB),
|
|
121
|
-
// unified otp()
|
|
122
|
-
// cap). Clock/channel/journal drivers
|
|
123
|
-
|
|
120
|
+
// gzip), durable-journal lease surface (~51.2 kB), fx.deliverOtp +
|
|
121
|
+
// unified otp() (~50.9 kB → 53 kB), then fx.runs / SLO window on the
|
|
122
|
+
// shared surface (~53.3 kB → 54 kB cap). Clock/channel/journal drivers
|
|
123
|
+
// stay lazy-bound; far below eager bind.
|
|
124
|
+
expect(total).toBeLessThan(54_000);
|
|
124
125
|
} finally {
|
|
125
126
|
await rm(dir, { recursive: true, force: true });
|
|
126
127
|
}
|
package/src/kernel/boot.ts
CHANGED
|
@@ -115,8 +115,13 @@ export interface BootOptions {
|
|
|
115
115
|
*
|
|
116
116
|
* @param signal - Signal name
|
|
117
117
|
* @param payload - Payload
|
|
118
|
+
* @param meta - Optional envelope (producer run id for trace chains)
|
|
118
119
|
*/
|
|
119
|
-
readonly onSignal?: (
|
|
120
|
+
readonly onSignal?: (
|
|
121
|
+
signal: string,
|
|
122
|
+
payload: unknown,
|
|
123
|
+
meta?: { readonly parentRunId?: string; readonly messageId?: string },
|
|
124
|
+
) => void | Promise<void>;
|
|
120
125
|
/**
|
|
121
126
|
* Resume due durable runs — called on every scheduler tick when any flow
|
|
122
127
|
* declares `durable: true` (claimDueSleep on the shared journal store).
|
|
@@ -257,8 +262,8 @@ async function loadBind<T>(name: string): Promise<T> {
|
|
|
257
262
|
export async function bootApplication(input: BootOptions = {}): Promise<BootResult> {
|
|
258
263
|
const docker =
|
|
259
264
|
input.docker === true || (input.docker !== false && process.env.OKE_DOCKER === "1");
|
|
260
|
-
// `-d` always selects the `docker` driver profile — not a mix of test
|
|
261
|
-
// prod store overrides
|
|
265
|
+
// `-d` always selects the `docker` driver profile — not a mix of local/test +
|
|
266
|
+
// prod store overrides when `$options.env` is unset.
|
|
262
267
|
const env: ConfigEnv = docker ? "docker" : (input.env ?? "local");
|
|
263
268
|
let config = input.config;
|
|
264
269
|
if (config === undefined) {
|