okengine 0.10.1 → 0.10.3
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/README.md +1 -1
- package/package.json +1 -1
- package/site/content/docs/ai/mcp.mdx +0 -1
- package/site/content/docs/elements/ai.mdx +0 -6
- package/site/content/docs/elements/channel.mdx +0 -6
- package/site/content/docs/elements/clock.mdx +2 -8
- package/site/content/docs/elements/flow.mdx +10 -17
- package/site/content/docs/elements/gate.mdx +4 -10
- package/site/content/docs/elements/signal.mdx +4 -13
- package/site/content/docs/elements/store.mdx +9 -16
- package/site/content/docs/elements/vault.mdx +1 -7
- package/site/content/docs/get-started/basic-usage.mdx +1 -9
- package/site/content/docs/get-started/introduction.mdx +1 -1
- package/site/content/docs/get-started/why.mdx +1 -2
- package/site/content/docs/index.mdx +0 -1
- package/site/content/docs/meta.json +0 -1
- package/site/content/docs/reference/cli.md +12 -12
- package/site/content/docs/reference/fx.mdx +6 -7
- package/src/auth/auth.test.ts +1 -2
- package/src/auth/bindings.ts +5 -15
- package/src/auth/gate-auth.test.ts +2 -2
- package/src/cli/ai-setup/ai-setup.test.ts +3 -1
- package/src/cli/ai-setup/apply.ts +13 -0
- package/src/cli/build.test.ts +67 -0
- package/src/cli/build.ts +33 -0
- package/src/cli/db-seed.ts +1 -1
- package/src/cli/dev-controls.test.ts +43 -48
- package/src/cli/dev-controls.ts +23 -172
- package/src/cli/dev.test.ts +90 -5
- package/src/cli/dev.ts +39 -16
- package/src/cli/doctor.ts +98 -5
- package/src/cli/load-config.images.test.ts +31 -1
- package/src/cli/load-config.ts +2 -2
- package/src/cli/safe-defaults.test.ts +1 -1
- package/src/client/notes-contract.test.ts +3 -4
- package/src/compiler/aot.test.ts +1 -2
- package/src/compiler/extract.test.ts +76 -21
- package/src/compiler/extract.ts +15 -3
- package/src/compiler/fixtures/raw/raw-unannotated.ts +2 -4
- package/src/compiler/fixtures/skyport/oke.config.ts +4 -2
- package/src/compiler/fixtures/skyport/src/flows/bookings/index.ts +4 -8
- package/src/compiler/fixtures/skyport/src/flows/payments/index.ts +1 -2
- package/src/compiler/fixtures/skyport/src/flows/support/index.ts +1 -2
- package/src/compiler/fixtures/skyport.expected.json +3 -3
- package/src/compiler/fixtures/triggers/five-triggers.ts +5 -10
- package/src/compiler/generate-adopt.test.ts +85 -0
- package/src/compiler/generate-adopt.ts +111 -0
- package/src/config/driver-defaults.test.ts +122 -0
- package/src/config/driver-defaults.ts +186 -0
- package/src/config/index.ts +97 -7
- package/src/console/server/dry-run.audit.test.ts +2 -2
- package/src/console/server/flows.ts +50 -150
- package/src/docker/compose.ts +13 -3
- package/src/docker/images-config.test.ts +168 -0
- package/src/docker/recipes/llama-cpp.ts +10 -1
- package/src/docker/stack-id.ts +1 -0
- package/src/elements/channel/declare.ts +27 -1
- package/src/elements/clock/chaos-child.ts +3 -6
- package/src/elements/clock/chaos.test.ts +1 -2
- package/src/elements/clock.test.ts +2 -4
- package/src/elements/signal/declare.ts +25 -1
- package/src/elements/store/declare.ts +27 -1
- package/src/elements/store/resource.ts +6 -11
- package/src/elements/store/upsert-app.test.ts +1 -2
- package/src/elements/vault/declare.ts +29 -1
- package/src/kernel/adopt-barrel-fresh.test.ts +103 -0
- package/src/kernel/app.ts +66 -9
- package/src/kernel/auto-registry.test.ts +198 -0
- package/src/kernel/boot-bind/channel.ts +12 -4
- package/src/kernel/boot-bind/clock.ts +3 -3
- package/src/kernel/boot-bind/gate.ts +7 -4
- package/src/kernel/boot-bind/honor-config.test.ts +5 -5
- package/src/kernel/boot-bind/journal.ts +3 -3
- package/src/kernel/boot-bind/signal.ts +3 -1
- package/src/kernel/boot-bind/store.test.ts +19 -0
- package/src/kernel/boot-bind/store.ts +24 -9
- package/src/kernel/boot-bind/vault.ts +3 -3
- package/src/kernel/boot.test.ts +19 -15
- package/src/kernel/boot.ts +81 -1
- package/src/kernel/call.test.ts +5 -10
- package/src/kernel/compensate.test.ts +3 -6
- package/src/kernel/concurrency.test.ts +3 -5
- package/src/kernel/correlation.test.ts +4 -8
- package/src/kernel/edge.test.ts +1 -1
- package/src/kernel/effects-stamping.test.ts +4 -7
- package/src/kernel/element-registries.ts +27 -0
- package/src/kernel/errors.ts +11 -0
- package/src/kernel/flow.test.ts +8 -15
- package/src/kernel/flow.ts +12 -14
- package/src/kernel/hook-timing.test.ts +2 -2
- package/src/kernel/hooks.test.ts +5 -10
- package/src/kernel/horizontal-child.ts +5 -10
- package/src/kernel/journal-boot.test.ts +2 -4
- package/src/kernel/pipeline.test.ts +6 -12
- package/src/kernel/plugin/capabilities.test.ts +1 -1
- package/src/kernel/plugin/decorate.test.ts +3 -8
- package/src/kernel/plugin/scoping.test.ts +6 -16
- package/src/kernel/plugin-elements.test.ts +1 -1
- package/src/kernel/plugin-needs.test.ts +1 -1
- package/src/kernel/ready.test.ts +1 -2
- package/src/kernel/registry-isolation.test.ts +5 -5
- package/src/kernel/triggers.ts +1 -1
- package/src/plugins/anonymous.ts +1 -3
- package/src/plugins/compression.test.ts +6 -8
- package/src/plugins/config-source.test.ts +1 -1
- package/src/plugins/config-source.ts +1 -2
- package/src/plugins/cors.test.ts +10 -10
- package/src/plugins/csrf.test.ts +2 -2
- package/src/plugins/headers.test.ts +14 -15
- package/src/plugins/ip-allowlist.test.ts +9 -9
- package/src/plugins/magic-link.ts +2 -6
- package/src/plugins/maintenance-mode.test.ts +6 -6
- package/src/plugins/otp.ts +5 -15
- package/src/plugins/passkey.ts +4 -12
- package/src/plugins/two-factor.ts +3 -9
- package/src/plugins/username.ts +2 -6
- package/src/release/measure.ts +1 -1
- package/src/runs/runs.test.ts +3 -5
- package/src/runtime/serve.test.ts +2 -4
- package/src/test/create-test-app.test.ts +3 -7
- package/src/test/provisions.integration.test.ts +4 -9
- package/src/test/reset-element-registries.ts +31 -0
- package/site/content/docs/console/access.mdx +0 -29
- package/site/content/docs/console/ai.mdx +0 -35
- package/site/content/docs/console/architecture.mdx +0 -35
- package/site/content/docs/console/channels.mdx +0 -37
- package/site/content/docs/console/clock.mdx +0 -51
- package/site/content/docs/console/flows.mdx +0 -31
- package/site/content/docs/console/gates.mdx +0 -73
- package/site/content/docs/console/index.mdx +0 -54
- package/site/content/docs/console/manifest-diff.mdx +0 -34
- package/site/content/docs/console/meta.json +0 -24
- package/site/content/docs/console/overview.mdx +0 -40
- package/site/content/docs/console/plugins.mdx +0 -41
- package/site/content/docs/console/privacy.mdx +0 -32
- package/site/content/docs/console/runs.mdx +0 -50
- package/site/content/docs/console/signals.mdx +0 -40
- package/site/content/docs/console/store.mdx +0 -32
- package/site/content/docs/console/tenancy.mdx +0 -32
- package/site/content/docs/console/traces.mdx +0 -38
- package/site/content/docs/console/vault.mdx +0 -41
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { dirname, join } from "node:path";
|
|
7
7
|
import { OLLAMA_IMAGE } from "../../docker/recipes/index.ts";
|
|
8
|
+
import { DEFAULT_DOCKER_DIR } from "../../docker/types.ts";
|
|
8
9
|
|
|
9
10
|
/** Choices applied to the project. */
|
|
10
11
|
export type AiSetupApplyInput = {
|
|
@@ -91,6 +92,18 @@ export function applyAiSetup(
|
|
|
91
92
|
}
|
|
92
93
|
writeFileSync(envPath, env.endsWith("\n") ? env : `${env}\n`, "utf8");
|
|
93
94
|
|
|
95
|
+
// `docker/.env.docker` is regenerated from `.env.local` only on its first
|
|
96
|
+
// boot (existing values there are treated as durable, possibly hand-edited
|
|
97
|
+
// pins — see stack-id.ts). Once it exists, a later `oke ai setup` run must
|
|
98
|
+
// update it directly, or the docker profile keeps the old model forever.
|
|
99
|
+
if (input.chatModel) {
|
|
100
|
+
const dockerEnvPath = join(cwd, DEFAULT_DOCKER_DIR, ".env.docker");
|
|
101
|
+
if (existsSync(dockerEnvPath)) {
|
|
102
|
+
const dockerEnv = readFileSync(dockerEnvPath, "utf8");
|
|
103
|
+
writeFileSync(dockerEnvPath, upsertEnv(dockerEnv, "OKE_AI_MODEL", input.chatModel), "utf8");
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
94
107
|
const aiTsPath = join(cwd, "src", "core", "ai.ts");
|
|
95
108
|
mkdirSync(dirname(aiTsPath), { recursive: true });
|
|
96
109
|
writeFileSync(aiTsPath, renderAiTs(input), "utf8");
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `oke build` regenerates `src/flows/generated.ts` before bundling — real
|
|
3
|
+
* before/after proof, not an assumption about the wiring.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, expect, test } from "bun:test";
|
|
7
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { runBuild } from "./build.ts";
|
|
11
|
+
|
|
12
|
+
async function makeUnit(root: string, name: string): Promise<void> {
|
|
13
|
+
const dir = join(root, "src/flows", name);
|
|
14
|
+
await mkdir(dir, { recursive: true });
|
|
15
|
+
await writeFile(join(dir, "index.ts"), "export const nothing = 1;\n");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe("oke build — .adopt() barrel regeneration", () => {
|
|
19
|
+
test("real before/after: no generated.ts before build, real barrel content after", async () => {
|
|
20
|
+
const root = await mkdtemp(join(tmpdir(), "oke-build-adopt-"));
|
|
21
|
+
try {
|
|
22
|
+
await makeUnit(root, "main");
|
|
23
|
+
await makeUnit(root, "notes");
|
|
24
|
+
|
|
25
|
+
const generatedPath = join(root, "src/flows/generated.ts");
|
|
26
|
+
await expect(readFile(generatedPath, "utf8")).rejects.toThrow();
|
|
27
|
+
|
|
28
|
+
const code = await runBuild({
|
|
29
|
+
rootDir: root,
|
|
30
|
+
bundle: async () => ({ success: true, logs: "" }),
|
|
31
|
+
});
|
|
32
|
+
expect(code).toBe(0);
|
|
33
|
+
|
|
34
|
+
const after = await readFile(generatedPath, "utf8");
|
|
35
|
+
expect(after).toContain('export * as main from "./main/index.ts";');
|
|
36
|
+
expect(after).toContain('export * as notes from "./notes/index.ts";');
|
|
37
|
+
} finally {
|
|
38
|
+
await rm(root, { recursive: true, force: true });
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("regeneration runs before bundling, using the injected syncAdoptBarrel", async () => {
|
|
43
|
+
const calls: string[] = [];
|
|
44
|
+
const code = await runBuild({
|
|
45
|
+
rootDir: "/fake/root",
|
|
46
|
+
syncAdoptBarrel: async (rootDir) => {
|
|
47
|
+
calls.push(`sync:${rootDir}`);
|
|
48
|
+
return ["main", "notes"];
|
|
49
|
+
},
|
|
50
|
+
bundle: async () => {
|
|
51
|
+
calls.push("bundle");
|
|
52
|
+
return { success: true, logs: "" };
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
expect(code).toBe(0);
|
|
56
|
+
expect(calls).toEqual(["sync:/fake/root", "bundle"]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("bundle failure still returns non-zero after a successful sync", async () => {
|
|
60
|
+
const code = await runBuild({
|
|
61
|
+
rootDir: "/fake/root",
|
|
62
|
+
syncAdoptBarrel: async () => [],
|
|
63
|
+
bundle: async () => ({ success: false, logs: "boom" }),
|
|
64
|
+
});
|
|
65
|
+
expect(code).toBe(1);
|
|
66
|
+
});
|
|
67
|
+
});
|
package/src/cli/build.ts
CHANGED
|
@@ -8,12 +8,39 @@ export interface BuildOptions {
|
|
|
8
8
|
readonly entry?: string;
|
|
9
9
|
readonly outdir?: string;
|
|
10
10
|
readonly write?: (text: string) => void;
|
|
11
|
+
/** Project root for the `.adopt()` barrel sync (default `process.cwd()`). */
|
|
12
|
+
readonly rootDir?: string;
|
|
11
13
|
/** Inject bun.build (tests). */
|
|
12
14
|
readonly bundle?: (opts: {
|
|
13
15
|
entry: string;
|
|
14
16
|
outdir: string;
|
|
15
17
|
target: "bun" | "node" | "browser";
|
|
16
18
|
}) => Promise<{ success: boolean; logs: string }>;
|
|
19
|
+
/**
|
|
20
|
+
* Inject `.adopt()` barrel regeneration (tests). Default: real
|
|
21
|
+
* `generateAdoptBarrel` + atomic write to `<rootDir>/src/flows/generated.ts`
|
|
22
|
+
* (`generated.ts.tmp` → rename). Returns the unit names written, for the
|
|
23
|
+
* status line.
|
|
24
|
+
*/
|
|
25
|
+
readonly syncAdoptBarrel?: (rootDir: string) => Promise<readonly string[]>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Regenerate `src/flows/generated.ts` from every `src/flows/<unit>/index.ts`
|
|
30
|
+
* unit folder — pre-step for `oke build` (mirrors `oke db`'s `schema.generated.ts`
|
|
31
|
+
* pre-step). A real file on disk, not a virtual module: identical resolution
|
|
32
|
+
* under `oke dev`'s runtime `import()` and `oke build`'s `Bun.build()`
|
|
33
|
+
* (investigated — a virtual-module Bun plugin does not resolve consistently
|
|
34
|
+
* across those two paths). Written atomically so a concurrent reader never
|
|
35
|
+
* sees a torn file.
|
|
36
|
+
*
|
|
37
|
+
* @param rootDir - Project root
|
|
38
|
+
*/
|
|
39
|
+
async function defaultSyncAdoptBarrel(rootDir: string): Promise<readonly string[]> {
|
|
40
|
+
const { generateAdoptBarrel, writeAdoptBarrel } = await import("../compiler/generate-adopt.ts");
|
|
41
|
+
const { source, units } = await generateAdoptBarrel({ rootDir });
|
|
42
|
+
await writeAdoptBarrel(rootDir, source);
|
|
43
|
+
return units;
|
|
17
44
|
}
|
|
18
45
|
|
|
19
46
|
/**
|
|
@@ -26,6 +53,7 @@ export async function runBuild(options: BuildOptions = {}): Promise<number> {
|
|
|
26
53
|
const target = options.target ?? "bun";
|
|
27
54
|
const entry = options.entry ?? "src/app.ts";
|
|
28
55
|
const outdir = options.outdir ?? "dist";
|
|
56
|
+
const rootDir = options.rootDir ?? process.cwd();
|
|
29
57
|
const bunTarget = target === "edge" ? "browser" : target === "node" ? "node" : "bun";
|
|
30
58
|
|
|
31
59
|
const bundle =
|
|
@@ -43,8 +71,13 @@ export async function runBuild(options: BuildOptions = {}): Promise<number> {
|
|
|
43
71
|
logs: result.logs.map((l) => String(l)).join("\n"),
|
|
44
72
|
};
|
|
45
73
|
});
|
|
74
|
+
const syncAdoptBarrel = options.syncAdoptBarrel ?? defaultSyncAdoptBarrel;
|
|
46
75
|
|
|
47
76
|
try {
|
|
77
|
+
const units = await syncAdoptBarrel(rootDir);
|
|
78
|
+
write(
|
|
79
|
+
`oke build: .adopt() barrel → src/flows/generated.ts (${units.length} unit${units.length === 1 ? "" : "s"})\n`,
|
|
80
|
+
);
|
|
48
81
|
const result = await bundle({ entry, outdir, target: bunTarget });
|
|
49
82
|
if (!result.success) {
|
|
50
83
|
console.error(result.logs || "oke build: failed");
|
package/src/cli/db-seed.ts
CHANGED
|
@@ -233,7 +233,7 @@ export async function bootSeedFx(
|
|
|
233
233
|
if (!app) {
|
|
234
234
|
throw new Error(`oke db seed: no oke() app export in ${entryAbs}`);
|
|
235
235
|
}
|
|
236
|
-
await app.boot({ env, startScheduler: false, config: config ?? undefined });
|
|
236
|
+
await app.boot({ env, startScheduler: false, config: config ?? undefined, rootDir: cwd });
|
|
237
237
|
const storeRuntime = app.elements?.store ?? app.bootResult?.store;
|
|
238
238
|
if (!storeRuntime) {
|
|
239
239
|
throw new Error(
|
|
@@ -1,65 +1,42 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import {
|
|
3
|
-
controlServicesFromStack,
|
|
4
3
|
createDevControlDispatcher,
|
|
5
4
|
formatDevControlsHelp,
|
|
6
5
|
formatDevControlsHint,
|
|
7
|
-
formatDevControlsServiceList,
|
|
8
6
|
parseDevControlKey,
|
|
9
7
|
} from "./dev-controls.ts";
|
|
10
8
|
|
|
9
|
+
const CTRL_C = String.fromCharCode(3);
|
|
10
|
+
|
|
11
11
|
describe("dev-controls", () => {
|
|
12
12
|
test("parseDevControlKey maps raw keys", () => {
|
|
13
13
|
expect(parseDevControlKey("q")).toBe("q");
|
|
14
|
-
expect(parseDevControlKey("\u0003")).toBe("q");
|
|
15
14
|
expect(parseDevControlKey("?")).toBe("?");
|
|
16
15
|
expect(parseDevControlKey("h")).toBe("?");
|
|
17
|
-
expect(parseDevControlKey("
|
|
18
|
-
expect(parseDevControlKey("l")).toBe("l");
|
|
16
|
+
expect(parseDevControlKey("r")).toBe("r");
|
|
19
17
|
expect(parseDevControlKey("u")).toBe("u");
|
|
20
18
|
expect(parseDevControlKey("x")).toBe("x");
|
|
21
|
-
expect(parseDevControlKey("
|
|
22
|
-
expect(parseDevControlKey("
|
|
23
|
-
expect(parseDevControlKey("
|
|
19
|
+
expect(parseDevControlKey("c")).toBeNull();
|
|
20
|
+
expect(parseDevControlKey("l")).toBeNull();
|
|
21
|
+
expect(parseDevControlKey("3")).toBeNull();
|
|
22
|
+
expect(parseDevControlKey("")).toBeNull();
|
|
24
23
|
expect(parseDevControlKey("z")).toBeNull();
|
|
25
24
|
});
|
|
26
25
|
|
|
26
|
+
test("parseDevControlKey maps Ctrl+C to quit", () => {
|
|
27
|
+
expect(parseDevControlKey(CTRL_C)).toBe("q");
|
|
28
|
+
});
|
|
29
|
+
|
|
27
30
|
test("format hint and help are scannable", () => {
|
|
28
31
|
expect(formatDevControlsHint(false)).toContain("keys");
|
|
29
32
|
expect(formatDevControlsHint(false)).toContain("refresh");
|
|
30
33
|
expect(formatDevControlsHint(false)).toContain("quit");
|
|
31
|
-
expect(
|
|
34
|
+
expect(formatDevControlsHint(false)).not.toContain("services");
|
|
32
35
|
expect(formatDevControlsHelp(false)).toContain("refresh");
|
|
33
|
-
expect(formatDevControlsHelp(false)).not.
|
|
36
|
+
expect(formatDevControlsHelp(false)).not.toContain("select service");
|
|
34
37
|
});
|
|
35
38
|
|
|
36
|
-
test("
|
|
37
|
-
const list = controlServicesFromStack([
|
|
38
|
-
{ label: "ai", serviceName: "ai" },
|
|
39
|
-
{ label: "mail", serviceName: "channel-email" },
|
|
40
|
-
{ label: "mail-ui", serviceName: "channel-email" },
|
|
41
|
-
{ label: "postgres", serviceName: "store-sql" },
|
|
42
|
-
]);
|
|
43
|
-
expect(list.map((s) => s.serviceName)).toEqual(["ai", "channel-email", "store-sql"]);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
test("formatDevControlsServiceList numbers rows", () => {
|
|
47
|
-
const out = formatDevControlsServiceList(
|
|
48
|
-
[
|
|
49
|
-
{ label: "ai", serviceName: "ai" },
|
|
50
|
-
{ label: "postgres", serviceName: "store-sql" },
|
|
51
|
-
],
|
|
52
|
-
(name) => (name === "ai" ? "error" : "ready"),
|
|
53
|
-
false,
|
|
54
|
-
1,
|
|
55
|
-
);
|
|
56
|
-
expect(out).toContain("1");
|
|
57
|
-
expect(out).toContain("ai");
|
|
58
|
-
expect(out).toContain("postgres");
|
|
59
|
-
expect(out).toContain(">");
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
test("dispatcher select → stop → up stack → quit", async () => {
|
|
39
|
+
test("dispatcher up -> stop -> refresh -> help -> quit", async () => {
|
|
63
40
|
const calls: string[] = [];
|
|
64
41
|
let quit = false;
|
|
65
42
|
let settled = 0;
|
|
@@ -79,29 +56,47 @@ describe("dev-controls", () => {
|
|
|
79
56
|
onComposeSettled: () => {
|
|
80
57
|
settled += 1;
|
|
81
58
|
},
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
{ label: "postgres", serviceName: "store-sql" },
|
|
85
|
-
],
|
|
86
|
-
composeAction: async (action, names) => {
|
|
87
|
-
calls.push(`${action}:${names.join(",") || "*"}`);
|
|
59
|
+
composeAction: async (action) => {
|
|
60
|
+
calls.push(action);
|
|
88
61
|
},
|
|
89
62
|
});
|
|
90
|
-
d.handleKey("
|
|
63
|
+
d.handleKey("u");
|
|
91
64
|
await Bun.sleep(10);
|
|
92
|
-
d.handleKey("1");
|
|
93
65
|
d.handleKey("x");
|
|
94
66
|
await Bun.sleep(10);
|
|
95
|
-
d.handleKey("
|
|
67
|
+
d.handleKey("r");
|
|
96
68
|
await Bun.sleep(10);
|
|
97
|
-
d.handleKey("
|
|
69
|
+
d.handleKey("?");
|
|
98
70
|
await Bun.sleep(10);
|
|
99
71
|
d.handleKey("q");
|
|
100
|
-
expect(calls).toEqual(["
|
|
72
|
+
expect(calls).toEqual(["up", "stop"]);
|
|
101
73
|
expect(settled).toBe(2);
|
|
102
74
|
expect(refreshed).toBe(1);
|
|
103
75
|
expect(panels).toBe(1);
|
|
104
76
|
expect(quit).toBe(true);
|
|
105
77
|
d.stop();
|
|
106
78
|
});
|
|
79
|
+
|
|
80
|
+
test("busy compose action ignores a concurrent key press", async () => {
|
|
81
|
+
let running = 0;
|
|
82
|
+
let maxConcurrent = 0;
|
|
83
|
+
const calls: string[] = [];
|
|
84
|
+
const d = createDevControlDispatcher({
|
|
85
|
+
write: () => {},
|
|
86
|
+
onQuit: () => {},
|
|
87
|
+
composeAction: async (action) => {
|
|
88
|
+
running += 1;
|
|
89
|
+
maxConcurrent = Math.max(maxConcurrent, running);
|
|
90
|
+
await Bun.sleep(20);
|
|
91
|
+
calls.push(action);
|
|
92
|
+
running -= 1;
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
d.handleKey("u");
|
|
96
|
+
d.handleKey("x");
|
|
97
|
+
await Bun.sleep(40);
|
|
98
|
+
expect(calls).toEqual(["up"]);
|
|
99
|
+
expect(maxConcurrent).toBe(1);
|
|
100
|
+
d.stop();
|
|
101
|
+
});
|
|
107
102
|
});
|
package/src/cli/dev-controls.ts
CHANGED
|
@@ -1,31 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Keyboard controls for a live `oke dev` session (TTY raw mode).
|
|
3
3
|
*
|
|
4
|
-
* Keys: `?` help · `q` quit · `
|
|
5
|
-
* `x` stop all · `1`–`9` select service then `u`/`x`/`r`.
|
|
4
|
+
* Keys: `?` help · `q` quit · `r` refresh · `u` up all · `x` stop all.
|
|
6
5
|
*
|
|
7
|
-
* Compose health / model phase update the status board above Logs —
|
|
8
|
-
*
|
|
6
|
+
* Compose health / model phase update the status board above Logs — it
|
|
7
|
+
* already lists every service with a live ● status, so refresh alone is
|
|
8
|
+
* enough to see the stack; there is no separate services panel.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import {
|
|
12
|
-
formatStatusDot,
|
|
13
|
-
formatStatusLine,
|
|
14
|
-
termColorEnabled,
|
|
15
|
-
termStyle,
|
|
16
|
-
type DevStatus,
|
|
17
|
-
} from "../term.ts";
|
|
11
|
+
import { formatStatusLine, termColorEnabled, termStyle } from "../term.ts";
|
|
18
12
|
|
|
19
|
-
/**
|
|
20
|
-
export type
|
|
21
|
-
/** Human label (`ai`, `postgres`). */
|
|
22
|
-
readonly label: string;
|
|
23
|
-
/** Compose service name (`ai`, `store-sql`). */
|
|
24
|
-
readonly serviceName: string;
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
/** Compose action against zero or more service names. */
|
|
28
|
-
export type DevComposeControlAction = "up" | "stop" | "restart";
|
|
13
|
+
/** Compose action against the whole stack. */
|
|
14
|
+
export type DevComposeControlAction = "up" | "stop";
|
|
29
15
|
|
|
30
16
|
/** Options for {@link startDevControls}. */
|
|
31
17
|
export type StartDevControlsOptions = {
|
|
@@ -37,30 +23,22 @@ export type StartDevControlsOptions = {
|
|
|
37
23
|
*/
|
|
38
24
|
readonly onRefresh?: () => void | Promise<void>;
|
|
39
25
|
/**
|
|
40
|
-
* Show help
|
|
41
|
-
*
|
|
26
|
+
* Show help in a clean pane (refreshes chrome first so the panel is not
|
|
27
|
+
* interleaved with request logs).
|
|
42
28
|
*
|
|
43
29
|
* @param body - Formatted panel text
|
|
44
30
|
*/
|
|
45
31
|
readonly onShowPanel?: (body: string) => void | Promise<void>;
|
|
46
32
|
/**
|
|
47
|
-
* After a compose up/stop
|
|
33
|
+
* After a compose up/stop — sync health into the board (no log lines).
|
|
48
34
|
*/
|
|
49
35
|
readonly onComposeSettled?: () => void | Promise<void>;
|
|
50
|
-
/** Live service catalogue (unique compose services). */
|
|
51
|
-
readonly services: () => readonly DevControlService[];
|
|
52
36
|
/**
|
|
53
|
-
* Run a compose control action.
|
|
37
|
+
* Run a compose control action against the whole stack.
|
|
54
38
|
*
|
|
55
|
-
* @param action - up / stop
|
|
56
|
-
* @param serviceNames - Empty → whole stack
|
|
39
|
+
* @param action - up / stop
|
|
57
40
|
*/
|
|
58
|
-
readonly composeAction: (
|
|
59
|
-
action: DevComposeControlAction,
|
|
60
|
-
serviceNames: readonly string[],
|
|
61
|
-
) => Promise<void>;
|
|
62
|
-
/** Optional live ● status for list view. */
|
|
63
|
-
readonly statusOf?: (serviceName: string) => DevStatus | undefined;
|
|
41
|
+
readonly composeAction: (action: DevComposeControlAction) => Promise<void>;
|
|
64
42
|
readonly stdin?: NodeJS.ReadStream;
|
|
65
43
|
readonly isTTY?: boolean;
|
|
66
44
|
readonly color?: boolean;
|
|
@@ -81,9 +59,8 @@ export function formatDevControlsHint(color: boolean = termColorEnabled()): stri
|
|
|
81
59
|
return (
|
|
82
60
|
`${s.dim}│${s.reset} ${s.dim}keys${s.reset} ` +
|
|
83
61
|
`${s.cyan}?${s.reset} help · ` +
|
|
84
|
-
`${s.cyan}
|
|
62
|
+
`${s.cyan}r${s.reset} refresh · ` +
|
|
85
63
|
`${s.cyan}q${s.reset} quit · ` +
|
|
86
|
-
`${s.cyan}l${s.reset} services · ` +
|
|
87
64
|
`${s.cyan}u${s.reset} up · ` +
|
|
88
65
|
`${s.cyan}x${s.reset} stop` +
|
|
89
66
|
`\n`
|
|
@@ -104,63 +81,15 @@ export function formatDevControlsHelp(color: boolean = termColorEnabled()): stri
|
|
|
104
81
|
"",
|
|
105
82
|
`${s.green}◇${s.reset} ${s.bold}Keys${s.reset}`,
|
|
106
83
|
k("?", "help"),
|
|
107
|
-
k("
|
|
84
|
+
k("r", "refresh — clear logs, show latest ●"),
|
|
108
85
|
k("q", "quit oke dev"),
|
|
109
|
-
k("l", "list docker services"),
|
|
110
86
|
k("u", "compose up -d (all)"),
|
|
111
87
|
k("x", "compose stop (all)"),
|
|
112
|
-
k("1-9", "select service"),
|
|
113
|
-
k("… u", "start selected"),
|
|
114
|
-
k("… x", "stop selected"),
|
|
115
|
-
k("… r", "restart selected"),
|
|
116
|
-
k("esc", "clear selection"),
|
|
117
88
|
bar,
|
|
118
89
|
"",
|
|
119
90
|
].join("\n");
|
|
120
91
|
}
|
|
121
92
|
|
|
122
|
-
/**
|
|
123
|
-
* Format a numbered service list for `l`.
|
|
124
|
-
*
|
|
125
|
-
* @param services - Catalogue
|
|
126
|
-
* @param statusOf - Optional ● lookup
|
|
127
|
-
* @param color - Color on/off
|
|
128
|
-
* @param selected - Highlighted 1-based index
|
|
129
|
-
*/
|
|
130
|
-
export function formatDevControlsServiceList(
|
|
131
|
-
services: readonly DevControlService[],
|
|
132
|
-
statusOf?: (serviceName: string) => DevStatus | undefined,
|
|
133
|
-
color: boolean = termColorEnabled(),
|
|
134
|
-
selected?: number,
|
|
135
|
-
): string {
|
|
136
|
-
const s = termStyle(color);
|
|
137
|
-
const bar = `${s.dim}│${s.reset}`;
|
|
138
|
-
if (services.length === 0) {
|
|
139
|
-
return `${bar} ${s.dim}no docker services${s.reset}\n`;
|
|
140
|
-
}
|
|
141
|
-
const lines = [
|
|
142
|
-
"",
|
|
143
|
-
`${s.green}◇${s.reset} ${s.bold}Services${s.reset} ${s.dim}1-9 then u/x/r${s.reset}`,
|
|
144
|
-
];
|
|
145
|
-
const max = Math.min(9, services.length);
|
|
146
|
-
for (let i = 0; i < max; i++) {
|
|
147
|
-
const svc = services[i]!;
|
|
148
|
-
const st = statusOf?.(svc.serviceName) ?? "pending";
|
|
149
|
-
const dot = formatStatusDot(st, color);
|
|
150
|
-
const num = `${i + 1}`;
|
|
151
|
-
const mark = selected === i + 1 ? `${s.cyan}>${s.reset}` : " ";
|
|
152
|
-
lines.push(
|
|
153
|
-
`${bar} ${mark}${s.cyan}${num}${s.reset} ${dot} ${svc.label.padEnd(12)} ${s.dim}${svc.serviceName}${s.reset}`,
|
|
154
|
-
);
|
|
155
|
-
}
|
|
156
|
-
if (services.length > 9) {
|
|
157
|
-
lines.push(`${bar} ${s.dim}… ${services.length - 9} more (first 9 only)${s.reset}`);
|
|
158
|
-
}
|
|
159
|
-
lines.push(bar);
|
|
160
|
-
lines.push("");
|
|
161
|
-
return `${lines.join("\n")}\n`;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
93
|
/**
|
|
165
94
|
* Parse one raw stdin chunk into a control key (tests / non-TTY inject).
|
|
166
95
|
*
|
|
@@ -169,16 +98,12 @@ export function formatDevControlsServiceList(
|
|
|
169
98
|
export function parseDevControlKey(chunk: string): string | null {
|
|
170
99
|
if (chunk.length === 0) return null;
|
|
171
100
|
if (chunk === "\u0003") return "q"; // Ctrl+C
|
|
172
|
-
if (chunk === "\u001b") return "esc";
|
|
173
101
|
const ch = chunk[0]!;
|
|
174
102
|
if (ch === "?" || ch === "h" || ch === "H") return "?";
|
|
175
|
-
if (ch === "c" || ch === "C") return "c";
|
|
176
103
|
if (ch === "q" || ch === "Q") return "q";
|
|
177
|
-
if (ch === "
|
|
104
|
+
if (ch === "r" || ch === "R") return "r";
|
|
178
105
|
if (ch === "u" || ch === "U") return "u";
|
|
179
106
|
if (ch === "x" || ch === "X") return "x";
|
|
180
|
-
if (ch === "r" || ch === "R") return "r";
|
|
181
|
-
if (ch >= "1" && ch <= "9") return ch;
|
|
182
107
|
return null;
|
|
183
108
|
}
|
|
184
109
|
|
|
@@ -191,23 +116,18 @@ export type DevControlDispatcher = {
|
|
|
191
116
|
/**
|
|
192
117
|
* Pure key dispatcher (no stdin) — used by tests and the TTY listener.
|
|
193
118
|
*
|
|
194
|
-
* @param opts - Quit / compose
|
|
119
|
+
* @param opts - Quit / compose actions
|
|
195
120
|
*/
|
|
196
121
|
export function createDevControlDispatcher(opts: StartDevControlsOptions): DevControlDispatcher {
|
|
197
122
|
const color = opts.color ?? termColorEnabled();
|
|
198
|
-
let selected: number | null = null;
|
|
199
123
|
let busy = false;
|
|
200
124
|
let stopped = false;
|
|
201
125
|
|
|
202
|
-
const runAction = async (
|
|
203
|
-
action: DevComposeControlAction,
|
|
204
|
-
serviceNames: readonly string[],
|
|
205
|
-
_label: string,
|
|
206
|
-
): Promise<void> => {
|
|
126
|
+
const runAction = async (action: DevComposeControlAction): Promise<void> => {
|
|
207
127
|
if (busy) return;
|
|
208
128
|
busy = true;
|
|
209
129
|
try {
|
|
210
|
-
await opts.composeAction(action
|
|
130
|
+
await opts.composeAction(action);
|
|
211
131
|
await opts.onComposeSettled?.();
|
|
212
132
|
} catch (err) {
|
|
213
133
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -224,7 +144,7 @@ export function createDevControlDispatcher(opts: StartDevControlsOptions): DevCo
|
|
|
224
144
|
opts.onQuit();
|
|
225
145
|
return;
|
|
226
146
|
}
|
|
227
|
-
if (key === "
|
|
147
|
+
if (key === "r") {
|
|
228
148
|
void Promise.resolve(opts.onRefresh?.()).catch((err: unknown) => {
|
|
229
149
|
const msg = err instanceof Error ? err.message : String(err);
|
|
230
150
|
opts.write(formatStatusLine(`refresh failed — ${msg}`, color, "error"));
|
|
@@ -236,58 +156,8 @@ export function createDevControlDispatcher(opts: StartDevControlsOptions): DevCo
|
|
|
236
156
|
void Promise.resolve(opts.onShowPanel?.(body) ?? opts.write(body));
|
|
237
157
|
return;
|
|
238
158
|
}
|
|
239
|
-
if (key === "
|
|
240
|
-
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
243
|
-
if (key === "l") {
|
|
244
|
-
const body = formatDevControlsServiceList(
|
|
245
|
-
opts.services(),
|
|
246
|
-
opts.statusOf,
|
|
247
|
-
color,
|
|
248
|
-
selected ?? undefined,
|
|
249
|
-
);
|
|
250
|
-
void Promise.resolve(opts.onShowPanel?.(body) ?? opts.write(body));
|
|
251
|
-
return;
|
|
252
|
-
}
|
|
253
|
-
if (key >= "1" && key <= "9") {
|
|
254
|
-
const n = Number(key);
|
|
255
|
-
const list = opts.services();
|
|
256
|
-
if (n < 1 || n > list.length || n > 9) {
|
|
257
|
-
opts.write(formatStatusLine(`no service ${n} — press l to list`, color, "pending"));
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
selected = n;
|
|
261
|
-
const svc = list[n - 1]!;
|
|
262
|
-
opts.write(
|
|
263
|
-
formatStatusLine(
|
|
264
|
-
`selected ${n} ${svc.label} — u start · x stop · r restart · esc`,
|
|
265
|
-
color,
|
|
266
|
-
opts.statusOf?.(svc.serviceName) ?? "pending",
|
|
267
|
-
),
|
|
268
|
-
);
|
|
269
|
-
return;
|
|
270
|
-
}
|
|
271
|
-
if (key === "u" || key === "x" || key === "r") {
|
|
272
|
-
const action: DevComposeControlAction =
|
|
273
|
-
key === "u" ? "up" : key === "x" ? "stop" : "restart";
|
|
274
|
-
if (selected !== null) {
|
|
275
|
-
const list = opts.services();
|
|
276
|
-
const svc = list[selected - 1];
|
|
277
|
-
if (!svc) {
|
|
278
|
-
selected = null;
|
|
279
|
-
opts.write(formatStatusLine("selection expired — press l", color, "pending"));
|
|
280
|
-
return;
|
|
281
|
-
}
|
|
282
|
-
selected = null;
|
|
283
|
-
void runAction(action, [svc.serviceName], svc.label);
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
if (key === "r") {
|
|
287
|
-
opts.write(formatStatusLine("select a service first (l then 1-9)", color, "pending"));
|
|
288
|
-
return;
|
|
289
|
-
}
|
|
290
|
-
void runAction(action, [], "stack");
|
|
159
|
+
if (key === "u" || key === "x") {
|
|
160
|
+
void runAction(key === "u" ? "up" : "stop");
|
|
291
161
|
}
|
|
292
162
|
},
|
|
293
163
|
stop() {
|
|
@@ -299,7 +169,7 @@ export function createDevControlDispatcher(opts: StartDevControlsOptions): DevCo
|
|
|
299
169
|
/**
|
|
300
170
|
* Start raw-mode keyboard controls. No-op when stdin is not a TTY.
|
|
301
171
|
*
|
|
302
|
-
* @param opts - Quit / compose
|
|
172
|
+
* @param opts - Quit / compose actions
|
|
303
173
|
*/
|
|
304
174
|
export function startDevControls(opts: StartDevControlsOptions): DevControlsHandle {
|
|
305
175
|
const stdin = opts.stdin ?? process.stdin;
|
|
@@ -333,22 +203,3 @@ export function startDevControls(opts: StartDevControlsOptions): DevControlsHand
|
|
|
333
203
|
},
|
|
334
204
|
};
|
|
335
205
|
}
|
|
336
|
-
|
|
337
|
-
/**
|
|
338
|
-
* Dedupe stack summary rows into control services (first label wins).
|
|
339
|
-
*
|
|
340
|
-
* @param rows - Stack summary services with compose names
|
|
341
|
-
*/
|
|
342
|
-
export function controlServicesFromStack(
|
|
343
|
-
rows: readonly { readonly label: string; readonly serviceName?: string }[],
|
|
344
|
-
): DevControlService[] {
|
|
345
|
-
const seen = new Set<string>();
|
|
346
|
-
const out: DevControlService[] = [];
|
|
347
|
-
for (const row of rows) {
|
|
348
|
-
const name = row.serviceName?.trim();
|
|
349
|
-
if (!name || seen.has(name)) continue;
|
|
350
|
-
seen.add(name);
|
|
351
|
-
out.push({ label: row.label, serviceName: name });
|
|
352
|
-
}
|
|
353
|
-
return out;
|
|
354
|
-
}
|