okengine 0.10.1 → 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-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 +37 -16
- 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 +3 -3
- 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/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/honor-config.test.ts +5 -5
- 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
|
@@ -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>>;
|
|
@@ -334,6 +344,28 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
|
|
|
334
344
|
console.warn = previousWarn;
|
|
335
345
|
};
|
|
336
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
|
+
|
|
337
369
|
const preferredApp = options.appPort ?? Number(Bun.env.PORT ?? APP_PORT);
|
|
338
370
|
const preferredConsole = options.consolePort ?? CONSOLE_PORT;
|
|
339
371
|
const preferredMcp = options.mcpPort ?? MCP_PORT;
|
|
@@ -1388,29 +1420,18 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
|
|
|
1388
1420
|
process.exit(0);
|
|
1389
1421
|
},
|
|
1390
1422
|
onRefresh: () => refreshChrome(),
|
|
1391
|
-
// Refresh first so help
|
|
1423
|
+
// Refresh first so help never sits in the middle of request logs.
|
|
1392
1424
|
onShowPanel: async (body) => {
|
|
1393
1425
|
await refreshChrome();
|
|
1394
1426
|
write(body);
|
|
1395
1427
|
},
|
|
1396
1428
|
onComposeSettled: () => syncComposeBoard(),
|
|
1397
|
-
|
|
1398
|
-
statusOf: (serviceName) => liveComposeHealth.get(serviceName),
|
|
1399
|
-
composeAction: async (action: DevComposeControlAction, serviceNames) => {
|
|
1429
|
+
composeAction: async (action: DevComposeControlAction) => {
|
|
1400
1430
|
const files = started.files;
|
|
1401
1431
|
const finalArgs =
|
|
1402
|
-
action === "up"
|
|
1432
|
+
action === "up"
|
|
1403
1433
|
? ["compose", ...files.flatMap((f) => ["-f", f]), "up", "-d"]
|
|
1404
|
-
:
|
|
1405
|
-
? [
|
|
1406
|
-
"compose",
|
|
1407
|
-
...files.flatMap((f) => ["-f", f]),
|
|
1408
|
-
"up",
|
|
1409
|
-
"-d",
|
|
1410
|
-
"--no-deps",
|
|
1411
|
-
...serviceNames,
|
|
1412
|
-
]
|
|
1413
|
-
: ["compose", ...files.flatMap((f) => ["-f", f]), action, ...serviceNames];
|
|
1434
|
+
: ["compose", ...files.flatMap((f) => ["-f", f]), action];
|
|
1414
1435
|
const proc = Bun.spawn(["docker", ...finalArgs], {
|
|
1415
1436
|
cwd: started.cwd,
|
|
1416
1437
|
stdout: "pipe",
|
|
@@ -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 },
|