okengine 0.10.0 → 0.10.2
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/elements/clock.mdx +2 -2
- package/site/content/docs/elements/flow.mdx +10 -10
- package/site/content/docs/elements/gate.mdx +4 -4
- package/site/content/docs/elements/signal.mdx +4 -7
- package/site/content/docs/elements/store.mdx +1 -1
- package/site/content/docs/elements/vault.mdx +1 -1
- package/site/content/docs/get-started/basic-usage.mdx +1 -3
- package/site/content/docs/get-started/introduction.mdx +1 -1
- package/site/content/docs/reference/cli.md +12 -12
- package/site/content/docs/reference/fx.mdx +3 -4
- 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/apply.ts +13 -0
- package/src/cli/build.test.ts +67 -0
- package/src/cli/build.ts +34 -0
- package/src/cli/db-seed.ts +1 -1
- package/src/cli/dev-app-runner.ts +6 -0
- package/src/cli/dev-controls.test.ts +43 -48
- package/src/cli/dev-controls.ts +23 -172
- package/src/cli/dev.test.ts +2 -4
- package/src/cli/dev.ts +63 -16
- package/src/cli/docker-cli.test.ts +15 -0
- package/src/cli/start.ts +12 -1
- package/src/client/notes-contract.test.ts +3 -4
- package/src/compiler/aot.test.ts +1 -2
- package/src/compiler/effects-infer.ts +37 -8
- package/src/compiler/extract.test.ts +186 -6
- package/src/compiler/extract.ts +31 -6
- package/src/compiler/fixtures/raw/raw-unannotated.ts +2 -4
- 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 +85 -0
- 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/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/cache.test.ts +28 -0
- 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 +80 -9
- package/src/kernel/auto-registry.test.ts +198 -0
- package/src/kernel/boot-bind/honor-config.test.ts +5 -5
- package/src/kernel/boot.test.ts +19 -15
- package/src/kernel/boot.ts +186 -8
- 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 +204 -0
- package/src/kernel/element-registries.ts +27 -0
- package/src/kernel/errors.ts +22 -0
- package/src/kernel/flow.test.ts +8 -15
- package/src/kernel/flow.ts +12 -14
- package/src/kernel/fx.ts +42 -13
- 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/manifest/sql-resource.ts +41 -0
- 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
|
@@ -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
|
-
}
|
package/src/cli/dev.test.ts
CHANGED
|
@@ -398,13 +398,11 @@ async function writePingApp(dir: string, version: string): Promise<void> {
|
|
|
398
398
|
join(dir, "src/flows/ping.ts"),
|
|
399
399
|
`import { on, flow, http, gate } from ${JSON.stringify(OKE_INDEX)};
|
|
400
400
|
|
|
401
|
-
export const ping = on(http.get("/ping").gate(gate.public), flow({
|
|
402
|
-
name: "ping",
|
|
401
|
+
export const ping = on(http.get("/ping").gate(gate.public), flow("ping", {
|
|
403
402
|
do: () => ({ version: ${JSON.stringify(version)} as const }),
|
|
404
403
|
}));
|
|
405
404
|
|
|
406
|
-
export const slow = on(http.get("/slow").gate(gate.public), flow({
|
|
407
|
-
name: "slow",
|
|
405
|
+
export const slow = on(http.get("/slow").gate(gate.public), flow("slow", {
|
|
408
406
|
do: async () => {
|
|
409
407
|
const started = ${JSON.stringify(version)};
|
|
410
408
|
await Bun.sleep(800);
|
package/src/cli/dev.ts
CHANGED
|
@@ -47,7 +47,6 @@ import { clientAdd } from "./client-add.ts";
|
|
|
47
47
|
import { resolveDriverId } from "../config/index.ts";
|
|
48
48
|
import { askDevMode, type AskDevModeFn } from "./ask-dev-mode.ts";
|
|
49
49
|
import {
|
|
50
|
-
controlServicesFromStack,
|
|
51
50
|
formatDevControlsHint,
|
|
52
51
|
startDevControls,
|
|
53
52
|
type DevComposeControlAction,
|
|
@@ -177,6 +176,17 @@ export interface DevOptions {
|
|
|
177
176
|
* @param filename - Relative path from the watcher
|
|
178
177
|
*/
|
|
179
178
|
readonly onDbAutoPush?: (filename: string) => void;
|
|
179
|
+
/**
|
|
180
|
+
* Injectable `.adopt()` barrel regeneration (tests). Default: real
|
|
181
|
+
* `generateAdoptBarrel` + write to `<cwd>/src/flows/generated.ts`. Runs
|
|
182
|
+
* once per `oke dev` session, before the entry is resolved/imported —
|
|
183
|
+
* a real file on disk, so `oke dev`'s runtime `import()` and `oke build`'s
|
|
184
|
+
* `Bun.build()` resolve it identically (unlike a virtual-module Bun
|
|
185
|
+
* plugin, which does not — see `compiler/generate-adopt.ts`).
|
|
186
|
+
*
|
|
187
|
+
* @param cwd - Project root
|
|
188
|
+
*/
|
|
189
|
+
readonly syncAdoptBarrel?: (cwd: string) => Promise<readonly string[]>;
|
|
180
190
|
/** Watch project source changes (injectable for tests). */
|
|
181
191
|
readonly watchFs?: DevWatchFn;
|
|
182
192
|
readonly images?: Readonly<Record<string, string>>;
|
|
@@ -233,6 +243,21 @@ export interface DevOptions {
|
|
|
233
243
|
readonly env: Record<string, string>;
|
|
234
244
|
readonly onUpdate?: (service: string, status: DevStatus) => void;
|
|
235
245
|
}) => Promise<Map<string, DevStatus>>;
|
|
246
|
+
/**
|
|
247
|
+
* Injectable `docker compose ps` runner for live health polls
|
|
248
|
+
* ({@link startComposeHealthWatch} / keyboard refresh). Default: real
|
|
249
|
+
* `docker`. When {@link DevOptions.composeHealth} is set and this is
|
|
250
|
+
* omitted, live polls return empty (unit tests never shell out).
|
|
251
|
+
*
|
|
252
|
+
* @param args - Args after `docker` (e.g. `compose -f … ps -a …`)
|
|
253
|
+
* @param cwd - Compose directory
|
|
254
|
+
* @param env - Stack env merged into the process env
|
|
255
|
+
*/
|
|
256
|
+
readonly composeHealthRun?: (
|
|
257
|
+
args: readonly string[],
|
|
258
|
+
cwd: string,
|
|
259
|
+
env: Record<string, string>,
|
|
260
|
+
) => Promise<string>;
|
|
236
261
|
/**
|
|
237
262
|
* Regenerate client types (injectable).
|
|
238
263
|
*
|
|
@@ -298,6 +323,14 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
|
|
|
298
323
|
/** Reassigned to {@link createAnchoredBoard} wrap after the status board paints. */
|
|
299
324
|
let write: (text: string) => void = options.write ?? ((t) => process.stdout.write(t));
|
|
300
325
|
const chromeWrite = (t: string) => write(formatCliChrome(t));
|
|
326
|
+
/**
|
|
327
|
+
* `docker compose ps` runner for live / default health polls.
|
|
328
|
+
* Explicit inject wins; otherwise a `composeHealth` inject implies tests —
|
|
329
|
+
* never shell out to a real `docker` binary.
|
|
330
|
+
*/
|
|
331
|
+
const composeHealthRun =
|
|
332
|
+
options.composeHealthRun ??
|
|
333
|
+
(options.composeHealth ? async (): Promise<string> => "[]" : undefined);
|
|
301
334
|
const previousWarn = console.warn;
|
|
302
335
|
console.warn = (...args: unknown[]) => {
|
|
303
336
|
const msg = args.map(String).join(" ");
|
|
@@ -311,6 +344,28 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
|
|
|
311
344
|
console.warn = previousWarn;
|
|
312
345
|
};
|
|
313
346
|
const cwd = options.cwd ?? process.cwd();
|
|
347
|
+
|
|
348
|
+
// One-shot `.adopt()` barrel regen for the session — real file on disk
|
|
349
|
+
// (`src/flows/generated.ts`), written before the entry is resolved/imported
|
|
350
|
+
// below so a freshly-added flows unit is adoptable without a hand edit.
|
|
351
|
+
// Best-effort like `syncDevSchema` below: a project with no `src/flows`
|
|
352
|
+
// yet (or a synthetic test tree) never blocks the dev session.
|
|
353
|
+
const syncAdoptBarrel =
|
|
354
|
+
options.syncAdoptBarrel ??
|
|
355
|
+
(async (root: string) => {
|
|
356
|
+
const { generateAdoptBarrel } = await import("../compiler/generate-adopt.ts");
|
|
357
|
+
const { mkdir, writeFile } = await import("node:fs/promises");
|
|
358
|
+
const { source, units } = await generateAdoptBarrel({ rootDir: root });
|
|
359
|
+
await mkdir(resolve(root, "src/flows"), { recursive: true });
|
|
360
|
+
await writeFile(resolve(root, "src/flows/generated.ts"), source);
|
|
361
|
+
return units;
|
|
362
|
+
});
|
|
363
|
+
try {
|
|
364
|
+
await syncAdoptBarrel(cwd);
|
|
365
|
+
} catch {
|
|
366
|
+
/* best-effort — boot-time assertAdoptBarrelFresh (rootDir opt-in) is the real gate */
|
|
367
|
+
}
|
|
368
|
+
|
|
314
369
|
const preferredApp = options.appPort ?? Number(Bun.env.PORT ?? APP_PORT);
|
|
315
370
|
const preferredConsole = options.consolePort ?? CONSOLE_PORT;
|
|
316
371
|
const preferredMcp = options.mcpPort ?? MCP_PORT;
|
|
@@ -656,6 +711,7 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
|
|
|
656
711
|
files: composeFiles,
|
|
657
712
|
cwd: dockerOut,
|
|
658
713
|
env: dockerStarted.env,
|
|
714
|
+
run: composeHealthRun,
|
|
659
715
|
timeoutMs: 20_000,
|
|
660
716
|
isDone: (map) => {
|
|
661
717
|
// Empty ps (compose just-created / injectable gap): do not spin 20s.
|
|
@@ -952,6 +1008,7 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
|
|
|
952
1008
|
files: started.files,
|
|
953
1009
|
cwd: started.cwd,
|
|
954
1010
|
env: started.env,
|
|
1011
|
+
run: composeHealthRun,
|
|
955
1012
|
intervalMs: 2_000,
|
|
956
1013
|
onChange: (map) => {
|
|
957
1014
|
liveComposeHealth = map;
|
|
@@ -1325,6 +1382,7 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
|
|
|
1325
1382
|
files: started.files,
|
|
1326
1383
|
cwd: started.cwd,
|
|
1327
1384
|
env: started.env,
|
|
1385
|
+
run: composeHealthRun,
|
|
1328
1386
|
});
|
|
1329
1387
|
if (liveComposeHealth.get("ai") === "error") heroAiStatus = "error";
|
|
1330
1388
|
repaintBoard();
|
|
@@ -1362,29 +1420,18 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
|
|
|
1362
1420
|
process.exit(0);
|
|
1363
1421
|
},
|
|
1364
1422
|
onRefresh: () => refreshChrome(),
|
|
1365
|
-
// Refresh first so help
|
|
1423
|
+
// Refresh first so help never sits in the middle of request logs.
|
|
1366
1424
|
onShowPanel: async (body) => {
|
|
1367
1425
|
await refreshChrome();
|
|
1368
1426
|
write(body);
|
|
1369
1427
|
},
|
|
1370
1428
|
onComposeSettled: () => syncComposeBoard(),
|
|
1371
|
-
|
|
1372
|
-
statusOf: (serviceName) => liveComposeHealth.get(serviceName),
|
|
1373
|
-
composeAction: async (action: DevComposeControlAction, serviceNames) => {
|
|
1429
|
+
composeAction: async (action: DevComposeControlAction) => {
|
|
1374
1430
|
const files = started.files;
|
|
1375
1431
|
const finalArgs =
|
|
1376
|
-
action === "up"
|
|
1432
|
+
action === "up"
|
|
1377
1433
|
? ["compose", ...files.flatMap((f) => ["-f", f]), "up", "-d"]
|
|
1378
|
-
:
|
|
1379
|
-
? [
|
|
1380
|
-
"compose",
|
|
1381
|
-
...files.flatMap((f) => ["-f", f]),
|
|
1382
|
-
"up",
|
|
1383
|
-
"-d",
|
|
1384
|
-
"--no-deps",
|
|
1385
|
-
...serviceNames,
|
|
1386
|
-
]
|
|
1387
|
-
: ["compose", ...files.flatMap((f) => ["-f", f]), action, ...serviceNames];
|
|
1434
|
+
: ["compose", ...files.flatMap((f) => ["-f", f]), action];
|
|
1388
1435
|
const proc = Bun.spawn(["docker", ...finalArgs], {
|
|
1389
1436
|
cwd: started.cwd,
|
|
1390
1437
|
stdout: "pipe",
|
|
@@ -207,6 +207,21 @@ describe("oke start", () => {
|
|
|
207
207
|
expect(code).toBe(0);
|
|
208
208
|
expect(ran).toBe(entry);
|
|
209
209
|
});
|
|
210
|
+
|
|
211
|
+
test("sets OKE_ROOT_DIR so boot() can lazily derive effects for undeclared flows", async () => {
|
|
212
|
+
const dir = await mkdtemp(join(tmpdir(), "oke-cli-start-rootdir-"));
|
|
213
|
+
await Bun.write(join(dir, "src/app.ts"), "export {}\n");
|
|
214
|
+
let seenRootDir: string | undefined;
|
|
215
|
+
const code = await runStart({
|
|
216
|
+
cwd: dir,
|
|
217
|
+
runEntry: async (_e, env) => {
|
|
218
|
+
seenRootDir = env.OKE_ROOT_DIR;
|
|
219
|
+
},
|
|
220
|
+
write: () => {},
|
|
221
|
+
});
|
|
222
|
+
expect(code).toBe(0);
|
|
223
|
+
expect(seenRootDir).toBe(dir);
|
|
224
|
+
});
|
|
210
225
|
});
|
|
211
226
|
|
|
212
227
|
describe("oke vault", () => {
|
package/src/cli/start.ts
CHANGED
|
@@ -55,7 +55,18 @@ export async function runStart(options: StartOptions = {}): Promise<number> {
|
|
|
55
55
|
try {
|
|
56
56
|
const entry = await resolveStartEntry(cwd, options.entry);
|
|
57
57
|
const port = String(options.port ?? Number(Bun.env.PORT ?? APP_PORT));
|
|
58
|
-
const env = {
|
|
58
|
+
const env = {
|
|
59
|
+
...process.env,
|
|
60
|
+
NODE_ENV: "production",
|
|
61
|
+
PORT: port,
|
|
62
|
+
// Signal to boot() where to lazily extract effects from when a flow
|
|
63
|
+
// has no hand-declared `effects` — kernel/boot.ts's mintCapabilities()
|
|
64
|
+
// reads this (explicit opt-in only). docker/prod hard-fail (OKE1008)
|
|
65
|
+
// rather than silently open when neither this nor an explicit
|
|
66
|
+
// `manifest` resolves anything — this just gives that resolution a
|
|
67
|
+
// real chance to succeed instead of failing every time.
|
|
68
|
+
OKE_ROOT_DIR: process.env["OKE_ROOT_DIR"] ?? cwd,
|
|
69
|
+
} as Record<string, string>;
|
|
59
70
|
write(`oke start: ${entry} (port ${port})\n`);
|
|
60
71
|
if (options.runEntry) {
|
|
61
72
|
await options.runEntry(entry, env);
|
|
@@ -37,7 +37,7 @@ const NotFound = z.object({});
|
|
|
37
37
|
|
|
38
38
|
const create = on(
|
|
39
39
|
http.post("/notes"),
|
|
40
|
-
flow({
|
|
40
|
+
flow("notes.create", {
|
|
41
41
|
in: NewNote,
|
|
42
42
|
out: NoteId,
|
|
43
43
|
do: (input) => ({ id: `n_${input.title}` }),
|
|
@@ -46,7 +46,7 @@ const create = on(
|
|
|
46
46
|
|
|
47
47
|
const get = on(
|
|
48
48
|
http.get("/notes/:id"),
|
|
49
|
-
flow({
|
|
49
|
+
flow("notes.get", {
|
|
50
50
|
in: NoteId,
|
|
51
51
|
out: Note,
|
|
52
52
|
errors: { NotFound },
|
|
@@ -250,8 +250,7 @@ describe("Notes — typeof app carries contracts", () => {
|
|
|
250
250
|
});
|
|
251
251
|
|
|
252
252
|
test("untriggered adopted flow is RPC-only on the client", async () => {
|
|
253
|
-
const stats = flow({
|
|
254
|
-
name: "notes.stats",
|
|
253
|
+
const stats = flow("notes.stats", {
|
|
255
254
|
in: NoteId,
|
|
256
255
|
out: z.object({ clicks: z.number() }),
|
|
257
256
|
do: () => ({ clicks: 7 }),
|
package/src/compiler/aot.test.ts
CHANGED
|
@@ -120,8 +120,7 @@ describe("typed error narrowing end-to-end", () => {
|
|
|
120
120
|
|
|
121
121
|
on(
|
|
122
122
|
http.post("/bookings").gate(gate.public),
|
|
123
|
-
flow({
|
|
124
|
-
name: "bookings.create",
|
|
123
|
+
flow("bookings.create", {
|
|
125
124
|
in: Booking,
|
|
126
125
|
out: z.object({ id: z.string() }),
|
|
127
126
|
errors: { FlightFull },
|