okengine 0.6.1 → 0.8.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 +2 -2
- package/README.md +73 -181
- package/package.json +3 -3
- package/site/content/docs/console/clock.mdx +25 -5
- package/site/content/docs/console/signals.mdx +13 -4
- package/site/content/docs/elements/ai.mdx +29 -2
- package/site/content/docs/elements/channel.mdx +42 -25
- package/site/content/docs/elements/clock.mdx +59 -25
- package/site/content/docs/elements/flow.mdx +8 -2
- package/site/content/docs/elements/gate.mdx +8 -1
- package/site/content/docs/elements/signal.mdx +173 -29
- package/site/content/docs/elements/store.mdx +595 -73
- package/site/content/docs/elements/vault.mdx +15 -5
- package/site/content/docs/get-started/basic-usage.mdx +13 -6
- package/site/content/docs/get-started/installation.mdx +52 -14
- package/site/content/docs/get-started/introduction.mdx +1 -1
- package/site/content/docs/plugins/magic-link.mdx +42 -0
- package/site/content/docs/plugins/phone-number.mdx +78 -17
- package/site/content/docs/plugins/two-factor.mdx +1 -0
- package/site/content/docs/reference/cli.md +32 -2
- package/site/content/docs/reference/configuration.mdx +15 -13
- package/site/content/docs/reference/environment-variables.mdx +32 -18
- package/site/content/docs/reference/errors.mdx +12 -11
- package/site/content/docs/reference/fx.mdx +15 -13
- package/site/content/docs/reference/security.md +3 -1
- package/src/cli/ai-setup/ai-setup.test.ts +144 -0
- package/src/cli/ai-setup/apply.ts +214 -0
- package/src/cli/ai-setup/catalog.ts +263 -0
- package/src/cli/ai-setup/detect-ollama.ts +166 -0
- package/src/cli/ai-setup/index.ts +228 -0
- package/src/cli/ai-setup/prompts.ts +649 -0
- package/src/cli/ai-setup/recommend.test.ts +100 -0
- package/src/cli/ai-setup/recommend.ts +203 -0
- package/src/cli/ai.ts +38 -0
- package/src/cli/db-auto-push.test.ts +6 -1
- package/src/cli/db-auto-push.ts +8 -3
- package/src/cli/db-seed.ts +359 -0
- package/src/cli/db.test.ts +341 -3
- package/src/cli/db.ts +75 -8
- package/src/cli/dev-db-push.test.ts +59 -0
- package/src/cli/dev.test.ts +40 -0
- package/src/cli/dev.ts +55 -1
- package/src/cli/docker-clean.test.ts +179 -0
- package/src/cli/docker-clean.ts +406 -0
- package/src/cli/docker.ts +16 -4
- package/src/cli/ensure-drizzle-config.ts +1 -1
- package/src/cli/hero-meta.test.ts +6 -5
- package/src/cli/hero-meta.ts +7 -1
- package/src/cli/index.ts +5 -0
- package/src/cli/load-config.images.test.ts +22 -0
- package/src/cli/load-config.ts +7 -2
- package/src/cli/openbao-bootstrap.test.ts +1 -0
- package/src/cli/openbao-bootstrap.ts +9 -2
- package/src/cli/registry.ts +116 -2
- package/src/client/live-gap.test.ts +35 -0
- package/src/client/transport.test.ts +21 -0
- package/src/client/transport.ts +27 -5
- package/src/compiler/effects-infer.ts +1 -0
- package/src/compiler/fixtures/skyport/oke.config.ts +1 -1
- package/src/config/define-config.test.ts +6 -6
- package/src/config/index.ts +5 -1
- package/src/console/server/console.test.ts +45 -0
- package/src/console/server/flows.ts +39 -9
- package/src/console/server/i18n.ts +41 -0
- package/src/console/server/vault.ts +12 -38
- package/src/console/ui/dist/assets/index-ClX0ZCe5.js +10 -0
- package/src/console/ui/dist/assets/{panel-access-BGv45snf.js → panel-access-ri1X9Otj.js} +1 -1
- package/src/console/ui/dist/assets/{panel-ai-B2S7LEii.js → panel-ai-DLHjRoJk.js} +1 -1
- package/src/console/ui/dist/assets/{panel-architecture-D7UJh91v.js → panel-architecture-DI3bQF3J.js} +1 -1
- package/src/console/ui/dist/assets/{panel-channels-9T3ybqRu.js → panel-channels-BDYWX29p.js} +1 -1
- package/src/console/ui/dist/assets/{panel-clock-Cb1UXGRQ.js → panel-clock-i-JpTjVB.js} +1 -1
- package/src/console/ui/dist/assets/{panel-diff-DmYbKWmN.js → panel-diff-CFuWxiXy.js} +1 -1
- package/src/console/ui/dist/assets/{panel-flows-PiHwT55z.js → panel-flows-B8TRhcoe.js} +1 -1
- package/src/console/ui/dist/assets/{panel-gates-BQGYXvjT.js → panel-gates-CfkBgco7.js} +1 -1
- package/src/console/ui/dist/assets/{panel-overview-BBnRO18l.js → panel-overview-DONDzBd2.js} +1 -1
- package/src/console/ui/dist/assets/{panel-plugins-D0PsmVw2.js → panel-plugins-DDJwTehL.js} +1 -1
- package/src/console/ui/dist/assets/{panel-runs-CWuRDe0r.js → panel-runs-D5zf-D9c.js} +1 -1
- package/src/console/ui/dist/assets/{panel-signals-Bbg4ewpP.js → panel-signals-Dsxdu_AR.js} +1 -1
- package/src/console/ui/dist/assets/{panel-store-CPCbsDRa.js → panel-store-hy7O8HOs.js} +1 -1
- package/src/console/ui/dist/assets/{panel-traces-DVAzuA_S.js → panel-traces-CBiAe5go.js} +1 -1
- package/src/console/ui/dist/assets/{panel-vault-D1_MvOmo.js → panel-vault-Za1GyJfM.js} +1 -1
- package/src/console/ui/dist/index.html +1 -1
- package/src/console/ui/shell/components/ui.tsx +5 -1
- package/src/console/ui/shell/setup/Wizard.tsx +26 -5
- package/src/docker/cleanup.test.ts +193 -0
- package/src/docker/cleanup.ts +355 -0
- package/src/docker/docker.test.ts +4 -0
- package/src/docker/index.ts +19 -0
- package/src/docker/recipes/ollama.ts +10 -5
- package/src/drivers/ai-ollama.ts +45 -2
- package/src/drivers/ai-providers.test.ts +31 -0
- package/src/drivers/channel-sently.test.ts +8 -0
- package/src/drivers/channel-taqnyat-mail.ts +34 -0
- package/src/drivers/channel-types.ts +71 -0
- package/src/drivers/clock-postgres.test.ts +258 -0
- package/src/drivers/clock-postgres.ts +410 -0
- package/src/drivers/index.ts +21 -0
- package/src/drivers/journal-postgres.test.ts +175 -0
- package/src/drivers/journal-postgres.ts +492 -0
- package/src/drivers/signal-engine.ts +98 -25
- package/src/drivers/signal-nats.ts +4 -4
- package/src/drivers/signal-postgres.ts +158 -64
- package/src/drivers/signal-redis.ts +4 -4
- package/src/drivers/signal-types.ts +61 -2
- package/src/drivers/vault-driver-removal.test.ts +5 -1
- package/src/elements/channel/runtime.ts +51 -0
- package/src/elements/channel.test.ts +71 -0
- package/src/elements/clock/chaos-child.ts +389 -0
- package/src/elements/clock/chaos.test.ts +204 -0
- package/src/elements/clock/durable.ts +7 -0
- package/src/elements/clock/reconcile.ts +110 -0
- package/src/elements/clock/runtime.ts +13 -0
- package/src/elements/clock.test.ts +164 -2
- package/src/elements/clock.ts +2 -1
- package/src/elements/gate/runtime.ts +6 -0
- package/src/elements/signal/chaos-child.ts +41 -4
- package/src/elements/signal/declare.ts +4 -1
- package/src/elements/signal/delivery-modes.test.ts +179 -0
- package/src/elements/signal/key-ordering.test.ts +306 -0
- package/src/elements/signal/lease-reclaim.test.ts +123 -0
- package/src/elements/signal/optional-emit.test.ts +76 -0
- package/src/elements/signal/order-lifecycle.test.ts +130 -0
- package/src/elements/signal/orphan-messages.test.ts +152 -0
- package/src/elements/signal/runtime.ts +11 -4
- package/src/elements/signal/schema-emit.test.ts +107 -0
- package/src/elements/signal.test.ts +89 -5
- package/src/elements/store/files-fx.ts +104 -0
- package/src/elements/store/files-image.test.ts +251 -0
- package/src/elements/store/files-image.ts +494 -0
- package/src/elements/store/runtime.ts +60 -0
- package/src/elements/store/seed.test.ts +27 -0
- package/src/elements/store/seed.ts +68 -0
- package/src/elements/store/sql-session.test.ts +39 -0
- package/src/elements/store/sql-session.ts +55 -0
- package/src/elements/store/upsert-app.test.ts +103 -0
- package/src/elements/store.ts +29 -0
- package/src/elements/vault/boot-chain.ts +150 -0
- package/src/elements/vault/runtime.ts +8 -0
- package/src/index.ts +15 -0
- package/src/kernel/app.ts +168 -15
- package/src/kernel/boot-bind/channel.test.ts +16 -0
- package/src/kernel/boot-bind/channel.ts +13 -0
- package/src/kernel/boot-bind/clock.ts +63 -7
- package/src/kernel/boot-bind/gate.ts +71 -3
- package/src/kernel/boot-bind/honor-config.test.ts +348 -0
- package/src/kernel/boot-bind/journal.ts +89 -0
- package/src/kernel/boot-bind/signal.ts +64 -7
- package/src/kernel/boot-bind/vault.ts +27 -8
- package/src/kernel/boot.test.ts +6 -4
- package/src/kernel/boot.ts +63 -15
- package/src/kernel/concurrency.ts +1 -1
- package/src/kernel/errors.ts +8 -2
- package/src/kernel/fx.test.ts +19 -0
- package/src/kernel/fx.ts +153 -9
- package/src/kernel/index.ts +6 -0
- package/src/kernel/journal-boot.test.ts +397 -0
- package/src/kernel/journal-suspend.ts +35 -0
- package/src/kernel/journal.test.ts +142 -0
- package/src/kernel/journal.ts +202 -27
- package/src/plugins/auth-methods.security.test.ts +10 -7
- package/src/plugins/phone-number.ts +67 -10
- package/src/plugins/taqnyat.live.test.ts +174 -0
- package/src/runtime/dev-request-log.test.ts +20 -1
- package/src/runtime/dev-request-log.ts +38 -0
- package/src/term.test.ts +15 -0
- package/src/term.ts +8 -3
- package/src/console/ui/dist/assets/index-CjxwRGVv.js +0 -10
package/src/cli/docker.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `oke docker` — Dockerfile + per-role compose files.
|
|
2
|
+
* `oke docker` — Dockerfile + per-role compose files · `clean` leftover stacks.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { resolve } from "node:path";
|
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
type DeriveOptions,
|
|
11
11
|
type DeriveResult,
|
|
12
12
|
} from "../docker/index.ts";
|
|
13
|
+
import { dockerCleanCli, dockerCleanHelp } from "./docker-clean.ts";
|
|
14
|
+
import { EXIT_OK } from "./exit.ts";
|
|
13
15
|
import { loadOkeConfig, loadManifest, resolveImages } from "./load-config.ts";
|
|
14
16
|
|
|
15
17
|
/** Options for {@link runDockerDerive}. */
|
|
@@ -112,11 +114,16 @@ export async function runDockerDerive(
|
|
|
112
114
|
}
|
|
113
115
|
|
|
114
116
|
/**
|
|
115
|
-
* CLI entry for `oke docker [
|
|
117
|
+
* CLI entry for `oke docker [clean] …` / derive flags.
|
|
116
118
|
*
|
|
117
119
|
* @param args - Args after `docker`
|
|
118
120
|
*/
|
|
119
121
|
export async function dockerCli(args: readonly string[]): Promise<number> {
|
|
122
|
+
const [head, ...rest] = args;
|
|
123
|
+
if (head === "clean") {
|
|
124
|
+
return dockerCleanCli(rest);
|
|
125
|
+
}
|
|
126
|
+
|
|
120
127
|
let prod = false;
|
|
121
128
|
let outDir: string | undefined;
|
|
122
129
|
let configPath: string | undefined;
|
|
@@ -132,8 +139,13 @@ export async function dockerCli(args: readonly string[]): Promise<number> {
|
|
|
132
139
|
|
|
133
140
|
Derive Dockerfile + compose.<role>.yml under docker/ (default).
|
|
134
141
|
--prod is opt-in (never the default). Credentials are never written into YAML.
|
|
135
|
-
|
|
136
|
-
|
|
142
|
+
|
|
143
|
+
${dockerCleanHelp()}`);
|
|
144
|
+
return EXIT_OK;
|
|
145
|
+
} else if (!a.startsWith("-")) {
|
|
146
|
+
console.error(`oke docker: unknown subcommand ${a}`);
|
|
147
|
+
console.error("Run `oke docker --help` for usage.");
|
|
148
|
+
return 1;
|
|
137
149
|
}
|
|
138
150
|
}
|
|
139
151
|
const { code } = await runDockerDerive({
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { resolve } from "node:path";
|
|
7
7
|
|
|
8
|
-
/** Mode-aware template (matches
|
|
8
|
+
/** Mode-aware template (matches create-oke templates' drizzle.config.ts). */
|
|
9
9
|
const DRIZZLE_CONFIG_TEMPLATE = `import { defineConfig } from "drizzle-kit";
|
|
10
10
|
|
|
11
11
|
/**
|
|
@@ -18,9 +18,9 @@ const SAMPLE: OkeConfig = {
|
|
|
18
18
|
sql: { local: "sqlite", docker: "postgres", prod: "postgres" },
|
|
19
19
|
kv: { local: "memory", docker: "redis", prod: "redis" },
|
|
20
20
|
},
|
|
21
|
-
signal: { local: "memory", docker: "
|
|
22
|
-
clock: { local: "memory", docker: "
|
|
23
|
-
vault: { local: "
|
|
21
|
+
signal: { local: "memory", docker: "redis", prod: "redis" },
|
|
22
|
+
clock: { local: "memory", docker: "file", prod: "file" },
|
|
23
|
+
vault: { local: "env", docker: "openbao", prod: "openbao" },
|
|
24
24
|
channel: {
|
|
25
25
|
email: { local: "console", docker: "smtp", prod: "smtp" },
|
|
26
26
|
},
|
|
@@ -62,8 +62,9 @@ describe("hero-meta", () => {
|
|
|
62
62
|
expect(store).toContain("postgres");
|
|
63
63
|
expect(store).toContain("redis");
|
|
64
64
|
expect(store).not.toContain("sqlite");
|
|
65
|
-
expect(rows.find((r) => r.element === "signal")?.detail).toBe("
|
|
66
|
-
expect(rows.find((r) => r.element === "clock")?.detail).toBe("
|
|
65
|
+
expect(rows.find((r) => r.element === "signal")?.detail).toBe("redis");
|
|
66
|
+
expect(rows.find((r) => r.element === "clock")?.detail).toBe("file");
|
|
67
|
+
expect(rows.find((r) => r.element === "gate")?.detail).toBe("redis");
|
|
67
68
|
expect(rows.find((r) => r.element === "channel")?.detail).toContain("smtp");
|
|
68
69
|
});
|
|
69
70
|
|
package/src/cli/hero-meta.ts
CHANGED
|
@@ -156,6 +156,12 @@ export function resolveHeroElements(
|
|
|
156
156
|
// `-d` reads the `docker` profile for every element (not a store-only override).
|
|
157
157
|
const configEnv: ConfigEnv = options.configEnv ?? (options.docker ? "docker" : "local");
|
|
158
158
|
|
|
159
|
+
// Gate rates share `drivers.store.kv` (no separate drivers.gate).
|
|
160
|
+
const gateKv =
|
|
161
|
+
options.kvDriver ??
|
|
162
|
+
resolveDriverId(drivers?.store?.kv, configEnv) ??
|
|
163
|
+
(options.docker ? "redis" : "memory");
|
|
164
|
+
|
|
159
165
|
const byName: Record<string, string> = {
|
|
160
166
|
flow: "●",
|
|
161
167
|
signal: resolveDriverId(drivers?.signal, configEnv) ?? "—",
|
|
@@ -164,7 +170,7 @@ export function resolveHeroElements(
|
|
|
164
170
|
kv: options.kvDriver,
|
|
165
171
|
}),
|
|
166
172
|
clock: resolveDriverId(drivers?.clock, configEnv) ?? "—",
|
|
167
|
-
gate:
|
|
173
|
+
gate: gateKv,
|
|
168
174
|
vault: resolveDriverId(drivers?.vault, configEnv) ?? "—",
|
|
169
175
|
channel: channelDetail(drivers?.channel, configEnv),
|
|
170
176
|
ai: resolveDriverId(drivers?.ai, configEnv) ?? "—",
|
package/src/cli/index.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* `oke` CLI entry.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { aiCli } from "./ai.ts";
|
|
6
7
|
import { branchCli } from "./branch.ts";
|
|
7
8
|
import { buildCli } from "./build.ts";
|
|
8
9
|
import { clientAddCli } from "./client-add.ts";
|
|
@@ -79,6 +80,10 @@ if (cmd === "eval") {
|
|
|
79
80
|
process.exit(await evalCli(sub ? [sub, ...rest] : rest));
|
|
80
81
|
}
|
|
81
82
|
|
|
83
|
+
if (cmd === "ai") {
|
|
84
|
+
process.exit(await aiCli(sub ? [sub, ...rest] : rest));
|
|
85
|
+
}
|
|
86
|
+
|
|
82
87
|
if (cmd === "branch") {
|
|
83
88
|
process.exit(await branchCli(sub ? [sub, ...rest] : rest));
|
|
84
89
|
}
|
|
@@ -37,6 +37,28 @@ describe("defaultImagesFromConfig", () => {
|
|
|
37
37
|
expect(images["store.sql"]).toBe("pgvector/pgvector:pg17");
|
|
38
38
|
});
|
|
39
39
|
|
|
40
|
+
test("clock postgres alone pulls store.sql image", () => {
|
|
41
|
+
const images = defaultImagesFromConfig({
|
|
42
|
+
drivers: {
|
|
43
|
+
clock: { prod: "postgres" },
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
expect(images).toEqual({
|
|
47
|
+
"store.sql": "postgres:18-alpine",
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("journal postgres alone pulls store.sql image", () => {
|
|
52
|
+
const images = defaultImagesFromConfig({
|
|
53
|
+
drivers: {
|
|
54
|
+
journal: { prod: "postgres" },
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
expect(images).toEqual({
|
|
58
|
+
"store.sql": "postgres:18-alpine",
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
40
62
|
test("returns empty when no container drivers", () => {
|
|
41
63
|
expect(defaultImagesFromConfig({})).toEqual({});
|
|
42
64
|
expect(
|
package/src/cli/load-config.ts
CHANGED
|
@@ -48,13 +48,17 @@ export function defaultImagesFromConfig(config: OkeConfig): Readonly<Record<stri
|
|
|
48
48
|
const kv = dockerOrProdId(config.drivers?.store?.kv);
|
|
49
49
|
const signal = dockerOrProdId(config.drivers?.signal);
|
|
50
50
|
const clock = dockerOrProdId(config.drivers?.clock);
|
|
51
|
+
const journal = dockerOrProdId(config.drivers?.journal);
|
|
51
52
|
|
|
53
|
+
// SQL-consuming facets pull a SQL image. Signal postgres still needs a
|
|
54
|
+
// LISTEN/NOTIFY client (not Bun.SQL); clock + journal postgres are real
|
|
55
|
+
// SKIP LOCKED stores (CronStore / durable-run journal).
|
|
52
56
|
const needsSql =
|
|
53
57
|
sql === "postgres" ||
|
|
54
58
|
sql === "pgvector" ||
|
|
55
59
|
index === "pgvector" ||
|
|
56
|
-
signal === "postgres" ||
|
|
57
60
|
clock === "postgres" ||
|
|
61
|
+
journal === "postgres" ||
|
|
58
62
|
(config.drivers?.prod ?? []).some((p) => p === "postgres" || p === "pgvector");
|
|
59
63
|
|
|
60
64
|
if (needsSql) {
|
|
@@ -62,7 +66,8 @@ export function defaultImagesFromConfig(config: OkeConfig): Readonly<Record<stri
|
|
|
62
66
|
sql === "pgvector" || index === "pgvector" ? DEFAULT_PGVECTOR_IMAGE : DEFAULT_SQL_IMAGE;
|
|
63
67
|
}
|
|
64
68
|
|
|
65
|
-
const needsKv =
|
|
69
|
+
const needsKv =
|
|
70
|
+
kv === "redis" || signal === "redis" || (config.drivers?.prod ?? []).includes("redis");
|
|
66
71
|
if (needsKv) {
|
|
67
72
|
out["store.kv"] = DEFAULT_KV_IMAGE;
|
|
68
73
|
}
|
|
@@ -221,8 +221,15 @@ export async function ensureOpenBao(
|
|
|
221
221
|
if (!existsSync(unsealPath) || !existsSync(rootPath)) {
|
|
222
222
|
throw new OpenBaoBootstrapError(
|
|
223
223
|
`openbao bootstrap: OpenBao is initialized but ${stateDir} is missing ` +
|
|
224
|
-
`unseal.key/root.token. Without the unseal key the data is unrecoverable
|
|
225
|
-
`
|
|
224
|
+
`unseal.key/root.token. Without the unseal key the vault data is unrecoverable.\n` +
|
|
225
|
+
`\n` +
|
|
226
|
+
`Usual cause in docker mode: a previous vault volume survived after the project\n` +
|
|
227
|
+
`(or .oke/openbao/) was deleted — Compose stop keeps volumes on purpose.\n` +
|
|
228
|
+
`\n` +
|
|
229
|
+
`Restore a backup of .oke/openbao/ if you have one.\n` +
|
|
230
|
+
`\n` +
|
|
231
|
+
`Dev reset (wipes vault secrets only, then re-init on next oke dev):\n` +
|
|
232
|
+
` cd docker && docker compose -f compose.yml -f compose.vault.yml down -v`,
|
|
226
233
|
);
|
|
227
234
|
}
|
|
228
235
|
unsealKey = readFileSync(unsealPath, "utf8").trim();
|
package/src/cli/registry.ts
CHANGED
|
@@ -202,7 +202,7 @@ export const OKE_COMMANDS: readonly CliCommand[] = [
|
|
|
202
202
|
},
|
|
203
203
|
{
|
|
204
204
|
name: "db",
|
|
205
|
-
summary: "domain schema — push · generate · migrate",
|
|
205
|
+
summary: "domain schema — push · generate · migrate · seed",
|
|
206
206
|
subcommands: [
|
|
207
207
|
{
|
|
208
208
|
name: "push",
|
|
@@ -215,6 +215,12 @@ export const OKE_COMMANDS: readonly CliCommand[] = [
|
|
|
215
215
|
valueName: "path",
|
|
216
216
|
summary: "drizzle.config.ts path",
|
|
217
217
|
},
|
|
218
|
+
{
|
|
219
|
+
long: "--env",
|
|
220
|
+
takesValue: true,
|
|
221
|
+
valueName: "name",
|
|
222
|
+
summary: "Config env (local|docker|test|prod)",
|
|
223
|
+
},
|
|
218
224
|
HELP,
|
|
219
225
|
],
|
|
220
226
|
},
|
|
@@ -229,6 +235,12 @@ export const OKE_COMMANDS: readonly CliCommand[] = [
|
|
|
229
235
|
valueName: "path",
|
|
230
236
|
summary: "drizzle.config.ts path",
|
|
231
237
|
},
|
|
238
|
+
{
|
|
239
|
+
long: "--env",
|
|
240
|
+
takesValue: true,
|
|
241
|
+
valueName: "name",
|
|
242
|
+
summary: "Config env (local|docker|test|prod)",
|
|
243
|
+
},
|
|
232
244
|
HELP,
|
|
233
245
|
],
|
|
234
246
|
},
|
|
@@ -243,6 +255,30 @@ export const OKE_COMMANDS: readonly CliCommand[] = [
|
|
|
243
255
|
valueName: "path",
|
|
244
256
|
summary: "drizzle.config.ts path",
|
|
245
257
|
},
|
|
258
|
+
{
|
|
259
|
+
long: "--env",
|
|
260
|
+
takesValue: true,
|
|
261
|
+
valueName: "name",
|
|
262
|
+
summary: "Config env (local|docker|test|prod)",
|
|
263
|
+
},
|
|
264
|
+
HELP,
|
|
265
|
+
],
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
name: "seed",
|
|
269
|
+
summary: "Run defineSeed (essential + env category); never at boot",
|
|
270
|
+
flags: [
|
|
271
|
+
{
|
|
272
|
+
long: "--env",
|
|
273
|
+
takesValue: true,
|
|
274
|
+
valueName: "name",
|
|
275
|
+
summary: "Config env (local|docker|test|prod)",
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
long: "--force",
|
|
279
|
+
summary: "Skip docker/prod confirmation prompt",
|
|
280
|
+
},
|
|
281
|
+
ENTRY,
|
|
246
282
|
HELP,
|
|
247
283
|
],
|
|
248
284
|
},
|
|
@@ -287,7 +323,7 @@ export const OKE_COMMANDS: readonly CliCommand[] = [
|
|
|
287
323
|
},
|
|
288
324
|
{
|
|
289
325
|
name: "docker",
|
|
290
|
-
summary: "
|
|
326
|
+
summary: "derive compose · clean leftover stacks",
|
|
291
327
|
leaf: true,
|
|
292
328
|
flags: [
|
|
293
329
|
{
|
|
@@ -300,6 +336,24 @@ export const OKE_COMMANDS: readonly CliCommand[] = [
|
|
|
300
336
|
MANIFEST,
|
|
301
337
|
HELP,
|
|
302
338
|
],
|
|
339
|
+
subcommands: [
|
|
340
|
+
{
|
|
341
|
+
name: "clean",
|
|
342
|
+
summary: "Remove oke-dev stacks (containers, networks, volumes)",
|
|
343
|
+
flags: [
|
|
344
|
+
{
|
|
345
|
+
long: "--yes",
|
|
346
|
+
short: "-y",
|
|
347
|
+
summary: "Non-interactive confirm",
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
long: "--all",
|
|
351
|
+
summary: "Every oke-dev-* stack (required with --yes outside a project)",
|
|
352
|
+
},
|
|
353
|
+
HELP,
|
|
354
|
+
],
|
|
355
|
+
},
|
|
356
|
+
],
|
|
303
357
|
},
|
|
304
358
|
{
|
|
305
359
|
name: "images",
|
|
@@ -346,6 +400,56 @@ export const OKE_COMMANDS: readonly CliCommand[] = [
|
|
|
346
400
|
leaf: true,
|
|
347
401
|
flags: [MANIFEST, HELP],
|
|
348
402
|
},
|
|
403
|
+
{
|
|
404
|
+
name: "ai",
|
|
405
|
+
summary: "configure AI driver + models",
|
|
406
|
+
subcommands: [
|
|
407
|
+
{
|
|
408
|
+
name: "setup",
|
|
409
|
+
summary: "Interactive / flagged AI provider + model setup",
|
|
410
|
+
flags: [
|
|
411
|
+
{
|
|
412
|
+
long: "--provider",
|
|
413
|
+
takesValue: true,
|
|
414
|
+
valueName: "id",
|
|
415
|
+
summary: "ollama · openai · anthropic · gemini · lmstudio · openrouter · custom",
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
long: "--chat",
|
|
419
|
+
takesValue: true,
|
|
420
|
+
valueName: "model",
|
|
421
|
+
summary: "Chat model id",
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
long: "--vision",
|
|
425
|
+
takesValue: true,
|
|
426
|
+
valueName: "model",
|
|
427
|
+
summary: "Vision model id",
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
long: "--embed",
|
|
431
|
+
takesValue: true,
|
|
432
|
+
valueName: "model",
|
|
433
|
+
summary: "Embedding model id",
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
long: "--pull",
|
|
437
|
+
summary: "Pull missing Ollama models (default)",
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
long: "--no-pull",
|
|
441
|
+
summary: "Skip ollama pull",
|
|
442
|
+
},
|
|
443
|
+
{
|
|
444
|
+
long: "--yes",
|
|
445
|
+
short: "-y",
|
|
446
|
+
summary: "Non-interactive (requires --provider)",
|
|
447
|
+
},
|
|
448
|
+
HELP,
|
|
449
|
+
],
|
|
450
|
+
},
|
|
451
|
+
],
|
|
452
|
+
},
|
|
349
453
|
{
|
|
350
454
|
name: "branch",
|
|
351
455
|
summary: "fork journaled state",
|
|
@@ -459,6 +563,16 @@ export function flagTokensFor(command: CliCommand, sub?: string): readonly strin
|
|
|
459
563
|
}
|
|
460
564
|
|
|
461
565
|
function formatCommandUsage(cmd: CliCommand): string {
|
|
566
|
+
// Leaf + subcommands (e.g. `oke docker` derive · `oke docker clean`).
|
|
567
|
+
if (cmd.leaf && cmd.subcommands && cmd.subcommands.length > 0) {
|
|
568
|
+
const flagHint = cmd.flags
|
|
569
|
+
?.filter((f) => f.long !== "--help")
|
|
570
|
+
.slice(0, 2)
|
|
571
|
+
.map((f) => formatFlagHint(f))
|
|
572
|
+
.join(" ");
|
|
573
|
+
const subs = cmd.subcommands.map((s) => s.name).join("|");
|
|
574
|
+
return `oke ${cmd.name}${flagHint ? ` ${flagHint}` : ""} · ${subs}`;
|
|
575
|
+
}
|
|
462
576
|
if (cmd.subcommands?.length === 1) {
|
|
463
577
|
const sub = cmd.subcommands[0]!;
|
|
464
578
|
const flagHint = sub.flags
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Evidence: createClient has no live-signal subscription today.
|
|
3
|
+
*
|
|
4
|
+
* `delivery: "live"` is consumed server-side via `bus.live()` (and
|
|
5
|
+
* `on(signal, flow)` for flow subscribers). `okengine/client` exposes only
|
|
6
|
+
* HTTP REST / RPC — no SSE, WebSocket, or `api.live(…)`.
|
|
7
|
+
*
|
|
8
|
+
* Design for `client.live(signalName)` is tracked with the Signal docs page;
|
|
9
|
+
* do not treat this file as a stub to fill — ship the real surface when ready.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, expect, test } from "bun:test";
|
|
13
|
+
|
|
14
|
+
import * as client from "./index.ts";
|
|
15
|
+
import { createClient } from "./create.ts";
|
|
16
|
+
|
|
17
|
+
describe("createClient — live signal gap", () => {
|
|
18
|
+
test("public client module exports no live / subscribe helpers", () => {
|
|
19
|
+
const keys = Object.keys(client).sort();
|
|
20
|
+
expect(keys).not.toContain("live");
|
|
21
|
+
expect(keys).not.toContain("subscribe");
|
|
22
|
+
expect(keys).toContain("createClient");
|
|
23
|
+
expect(keys).toContain("createTransport");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("createClient builds an HTTP call proxy only (no live method on the root)", async () => {
|
|
27
|
+
const api = createClient("http://app.test", {
|
|
28
|
+
fetch: async () => Response.json({ data: null, error: { code: "NotFound", data: {} } }),
|
|
29
|
+
});
|
|
30
|
+
// Root is a callable proxy for unit.flow — live is not a first-class API.
|
|
31
|
+
expect(typeof api).toBe("function");
|
|
32
|
+
expect(Object.prototype.hasOwnProperty.call(api, "live")).toBe(false);
|
|
33
|
+
expect("live" in (api as object)).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -47,6 +47,27 @@ describe("transport — retry", () => {
|
|
|
47
47
|
expect(error?.code).toBe("TransportError");
|
|
48
48
|
expect(n).toBe(2);
|
|
49
49
|
});
|
|
50
|
+
|
|
51
|
+
test("structured 5xx envelope is returned (not TransportError)", async () => {
|
|
52
|
+
const api = createClient<PingApp>("http://app.test", {
|
|
53
|
+
fetch: async () =>
|
|
54
|
+
Response.json(
|
|
55
|
+
{
|
|
56
|
+
data: null,
|
|
57
|
+
error: {
|
|
58
|
+
code: "InternalError",
|
|
59
|
+
data: {},
|
|
60
|
+
message: "password policy failed: minLength 12",
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
{ status: 500 },
|
|
64
|
+
),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const { error } = await api.sys.ping();
|
|
68
|
+
expect(error?.code).toBe("InternalError");
|
|
69
|
+
expect(error?.message).toMatch(/password policy/i);
|
|
70
|
+
});
|
|
50
71
|
});
|
|
51
72
|
|
|
52
73
|
describe("transport — timeout", () => {
|
package/src/client/transport.ts
CHANGED
|
@@ -44,6 +44,11 @@ export function createTransport(base: string, opts: ClientOptions = {}): Transpo
|
|
|
44
44
|
await opts.auth.refresh();
|
|
45
45
|
continue;
|
|
46
46
|
}
|
|
47
|
+
if (res.status >= 500) {
|
|
48
|
+
const structured = await decodeIfEnvelope(res);
|
|
49
|
+
if (structured) return structured;
|
|
50
|
+
throw new Error(`HTTP ${res.status}`);
|
|
51
|
+
}
|
|
47
52
|
return decode(res);
|
|
48
53
|
} catch (err) {
|
|
49
54
|
const transient = isTransient(err);
|
|
@@ -110,21 +115,38 @@ async function once(
|
|
|
110
115
|
ctrl && opts.timeout !== undefined ? setTimeout(() => ctrl.abort(), opts.timeout) : undefined;
|
|
111
116
|
|
|
112
117
|
try {
|
|
113
|
-
|
|
118
|
+
return await fetchFn(url, {
|
|
114
119
|
method,
|
|
115
120
|
headers,
|
|
116
121
|
body,
|
|
117
122
|
signal: ctrl?.signal,
|
|
118
123
|
});
|
|
119
|
-
if (res.status >= 500) {
|
|
120
|
-
throw new Error(`HTTP ${res.status}`);
|
|
121
|
-
}
|
|
122
|
-
return res;
|
|
123
124
|
} finally {
|
|
124
125
|
if (timer !== undefined) clearTimeout(timer);
|
|
125
126
|
}
|
|
126
127
|
}
|
|
127
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Decode a JSON `{ data, error }` envelope from a 5xx response, or `null`
|
|
131
|
+
* when the body is not a structured OKE failure (retry as transport error).
|
|
132
|
+
*
|
|
133
|
+
* @param res - HTTP response (body consumed)
|
|
134
|
+
*/
|
|
135
|
+
async function decodeIfEnvelope(res: Response): Promise<ClientResult | null> {
|
|
136
|
+
const text = await res.text();
|
|
137
|
+
if (!text) return null;
|
|
138
|
+
let json: unknown;
|
|
139
|
+
try {
|
|
140
|
+
json = JSON.parse(text);
|
|
141
|
+
} catch {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
if (json !== null && typeof json === "object" && "data" in json && "error" in json) {
|
|
145
|
+
return json as ClientResult;
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
|
|
128
150
|
function rpcRequest(
|
|
129
151
|
base: string,
|
|
130
152
|
key: string,
|
|
@@ -9,7 +9,7 @@ export default defineConfig({
|
|
|
9
9
|
files: { prod: "s3" },
|
|
10
10
|
index: { prod: "pgvector" },
|
|
11
11
|
},
|
|
12
|
-
signal: { prod: "
|
|
12
|
+
signal: { prod: "redis" },
|
|
13
13
|
vault: { prod: "openbao" },
|
|
14
14
|
channel: { email: { prod: "smtp" } },
|
|
15
15
|
ai: { prod: { driver: "anthropic" } },
|
|
@@ -26,7 +26,7 @@ describe("fillDockerFromProd / defineConfig", () => {
|
|
|
26
26
|
});
|
|
27
27
|
|
|
28
28
|
test("vault prod → docker copies the prod driver", () => {
|
|
29
|
-
expect(fillDockerFromProd({ local: "
|
|
29
|
+
expect(fillDockerFromProd({ local: "env", prod: "openbao" })?.docker).toBe("openbao");
|
|
30
30
|
});
|
|
31
31
|
|
|
32
32
|
test("defineConfig fills every element from prod", () => {
|
|
@@ -37,9 +37,9 @@ describe("fillDockerFromProd / defineConfig", () => {
|
|
|
37
37
|
kv: { local: "memory", prod: "redis" },
|
|
38
38
|
files: { local: "fs", prod: "s3" },
|
|
39
39
|
},
|
|
40
|
-
signal: { local: "memory", prod: "
|
|
41
|
-
clock: { local: "memory", prod: "
|
|
42
|
-
vault: { local: "
|
|
40
|
+
signal: { local: "memory", prod: "redis" },
|
|
41
|
+
clock: { local: "memory", prod: "file" },
|
|
42
|
+
vault: { local: "env", prod: "openbao" },
|
|
43
43
|
channel: {
|
|
44
44
|
email: { local: "console", prod: "smtp" },
|
|
45
45
|
},
|
|
@@ -49,8 +49,8 @@ describe("fillDockerFromProd / defineConfig", () => {
|
|
|
49
49
|
expect(cfg.drivers?.store?.sql?.docker).toBe("postgres");
|
|
50
50
|
expect(cfg.drivers?.store?.kv?.docker).toBe("redis");
|
|
51
51
|
expect(cfg.drivers?.store?.files?.docker).toBe("s3");
|
|
52
|
-
expect(cfg.drivers?.signal?.docker).toBe("
|
|
53
|
-
expect(cfg.drivers?.clock?.docker).toBe("
|
|
52
|
+
expect(cfg.drivers?.signal?.docker).toBe("redis");
|
|
53
|
+
expect(cfg.drivers?.clock?.docker).toBe("file");
|
|
54
54
|
expect(cfg.drivers?.vault?.docker).toBe("openbao");
|
|
55
55
|
expect(cfg.drivers?.channel?.email?.docker).toBe("smtp");
|
|
56
56
|
expect(cfg.drivers?.ai?.docker).toBe("anthropic");
|
package/src/config/index.ts
CHANGED
|
@@ -73,6 +73,8 @@ export interface DriversConfig {
|
|
|
73
73
|
readonly store?: StoreDriversConfig;
|
|
74
74
|
readonly signal?: EnvDriverMap;
|
|
75
75
|
readonly clock?: EnvDriverMap;
|
|
76
|
+
/** Durable-run journal: `memory` · `file` · `postgres`. */
|
|
77
|
+
readonly journal?: EnvDriverMap;
|
|
76
78
|
readonly vault?: EnvDriverMap;
|
|
77
79
|
readonly channel?: ChannelDriversConfig;
|
|
78
80
|
readonly ai?: EnvDriverMap;
|
|
@@ -283,6 +285,7 @@ export function normalizeDriversConfig(
|
|
|
283
285
|
...(store !== undefined ? { store } : {}),
|
|
284
286
|
signal: normalizeEnvDriverMap(drivers.signal as RawEnvDriverMap),
|
|
285
287
|
clock: normalizeEnvDriverMap(drivers.clock as RawEnvDriverMap),
|
|
288
|
+
journal: normalizeEnvDriverMap(drivers.journal as RawEnvDriverMap),
|
|
286
289
|
vault: normalizeEnvDriverMap(drivers.vault as RawEnvDriverMap),
|
|
287
290
|
...(channel !== undefined ? { channel } : {}),
|
|
288
291
|
ai: normalizeEnvDriverMap(drivers.ai as RawEnvDriverMap),
|
|
@@ -333,6 +336,7 @@ export function fillDriversDockerFromProd(
|
|
|
333
336
|
...(store !== undefined ? { store } : {}),
|
|
334
337
|
signal: fillDockerFromProd(drivers.signal),
|
|
335
338
|
clock: fillDockerFromProd(drivers.clock),
|
|
339
|
+
journal: fillDockerFromProd(drivers.journal),
|
|
336
340
|
vault: fillDockerFromProd(drivers.vault),
|
|
337
341
|
...(channel !== undefined ? { channel } : {}),
|
|
338
342
|
ai: fillDockerFromProd(drivers.ai),
|
|
@@ -344,7 +348,7 @@ export function fillDriversDockerFromProd(
|
|
|
344
348
|
* Define an `oke.config.ts` document.
|
|
345
349
|
*
|
|
346
350
|
* Missing `docker` driver pins are filled from `prod` so `oke dev -d` uses the
|
|
347
|
-
* same server protocols as production (vault →
|
|
351
|
+
* same server protocols as production (vault → `env` for `docker/.env.docker`).
|
|
348
352
|
* Legacy `dev`/`stack` keys are normalized to `local`/`docker`.
|
|
349
353
|
*
|
|
350
354
|
* @param config - Driver / tenancy / i18n / topology
|
|
@@ -53,6 +53,51 @@ describe("console kernel", () => {
|
|
|
53
53
|
}
|
|
54
54
|
});
|
|
55
55
|
|
|
56
|
+
test("claim rejects weak passwords with ClaimFailed password_policy (not 500)", async () => {
|
|
57
|
+
const handle = await startConsoleApp({
|
|
58
|
+
cwd,
|
|
59
|
+
secret: "test-secret-console",
|
|
60
|
+
silentClaim: true,
|
|
61
|
+
});
|
|
62
|
+
try {
|
|
63
|
+
const weak = await handle.app.fetch(
|
|
64
|
+
new Request("http://console.test/console/setup/claim", {
|
|
65
|
+
method: "POST",
|
|
66
|
+
headers: { "content-type": "application/json" },
|
|
67
|
+
body: JSON.stringify({
|
|
68
|
+
claimCode: handle.state.claim.code,
|
|
69
|
+
email: "ops@example.com",
|
|
70
|
+
name: "Ops",
|
|
71
|
+
password: "short1A",
|
|
72
|
+
}),
|
|
73
|
+
}),
|
|
74
|
+
);
|
|
75
|
+
// Zod min(12) rejects before createOperator — still not an opaque 500.
|
|
76
|
+
expect(weak.status).toBe(422);
|
|
77
|
+
const shortOkZod = await handle.app.fetch(
|
|
78
|
+
new Request("http://console.test/console/setup/claim", {
|
|
79
|
+
method: "POST",
|
|
80
|
+
headers: { "content-type": "application/json" },
|
|
81
|
+
body: JSON.stringify({
|
|
82
|
+
claimCode: handle.state.claim.code,
|
|
83
|
+
email: "ops@example.com",
|
|
84
|
+
name: "Ops",
|
|
85
|
+
password: "longenoughword",
|
|
86
|
+
}),
|
|
87
|
+
}),
|
|
88
|
+
);
|
|
89
|
+
expect(shortOkZod.status).toBe(400);
|
|
90
|
+
const body = (await shortOkZod.json()) as {
|
|
91
|
+
error: { code: string; message?: string; data?: { reason?: string } };
|
|
92
|
+
};
|
|
93
|
+
expect(body.error.code).toBe("ClaimFailed");
|
|
94
|
+
expect(body.error.data?.reason).toBe("password_policy");
|
|
95
|
+
expect(body.error.message).toMatch(/12 characters/i);
|
|
96
|
+
} finally {
|
|
97
|
+
await handle.app.stop();
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
56
101
|
test("claim creates first operator and closes the wizard permanently", async () => {
|
|
57
102
|
const handle = await startConsoleApp({
|
|
58
103
|
cwd,
|