okengine 0.2.2 → 0.2.4
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 +30 -4
- package/docs/spec/example.md +12 -3
- package/docs/spec/four-applications.md +10 -3
- package/package.json +9 -40
- package/src/cli/dev-app-runner.ts +45 -6
- package/src/cli/dev.ts +204 -23
- package/src/cli/docker-cli.test.ts +10 -8
- package/src/cli/docker.ts +18 -7
- package/src/cli/doctor.ts +3 -18
- package/src/cli/hero-meta.test.ts +85 -0
- package/src/cli/hero-meta.ts +252 -0
- package/src/cli/index.ts +4 -1
- package/src/cli/json-out.test.ts +1 -1
- package/src/cli/load-config.images.test.ts +126 -0
- package/src/cli/load-config.ts +145 -2
- package/src/cli/ports.test.ts +51 -0
- package/src/cli/ports.ts +81 -0
- package/src/cli/registry.help.test.ts +18 -0
- package/src/cli/registry.ts +0 -4
- package/src/cli/safe-defaults.test.ts +3 -3
- package/src/config/define-config.test.ts +61 -0
- package/src/config/index.ts +89 -6
- package/src/config/resolve-driver.test.ts +30 -0
- package/src/console/server/app.ts +2 -1
- package/src/console/server/flows.ts +4 -1
- package/src/console/server/index.ts +5 -0
- package/src/console/server/operator-db.test.ts +213 -0
- package/src/console/server/operator-db.ts +470 -0
- package/src/console/server/plugin.ts +20 -0
- package/src/console/server/serve.ts +47 -1
- package/src/console/server/state.ts +25 -2
- package/src/console/ui/dist/assets/{index-B71Yl_SS.js → index-Dy4jht9P.js} +3 -3
- package/src/console/ui/dist/assets/{panel-overview-Bd48d9km.js → panel-overview-Dt_AeXgd.js} +1 -1
- package/src/console/ui/dist/assets/{panel-runs-BwsWqKeB.js → panel-runs-DGstFHeq.js} +1 -1
- package/src/console/ui/dist/assets/{panel-signals-9najbZY2.js → panel-signals-DzEa2Fnt.js} +1 -1
- package/src/console/ui/dist/assets/{panel-store-OHkP2pDp.js → panel-store-BJkbNgxx.js} +1 -1
- package/src/console/ui/dist/assets/{panel-traces-tn2JoY8U.js → panel-traces-BaRVO3gM.js} +1 -1
- package/src/console/ui/dist/assets/style-C8MxEWPd.css +3 -0
- package/src/console/ui/dist/favicon.svg +7 -0
- package/src/console/ui/dist/index.html +3 -2
- package/src/console/ui/shell/App.tsx +44 -4
- package/src/console/ui/shell/components/oke-logo.tsx +40 -0
- package/src/console/ui/shell/index.html +1 -0
- package/src/console/ui/shell/layout/Shell.tsx +2 -3
- package/src/console/ui/shell/panels/overview/OverviewPanel.tsx +8 -0
- package/src/console/ui/shell/public/favicon.svg +7 -0
- package/src/console/ui/shell/setup/Wizard.tsx +5 -2
- package/src/docker/compose.ts +45 -14
- package/src/docker/derive.ts +32 -10
- package/src/docker/docker.test.ts +28 -4
- package/src/docker/dockerfile.integration.test.ts +3 -1
- package/src/docker/index.ts +10 -0
- package/src/docker/stack-id.test.ts +86 -0
- package/src/docker/stack-id.ts +108 -0
- package/src/docker/stack.integration.test.ts +17 -8
- package/src/docker/types.ts +20 -1
- package/src/kernel/app.ts +39 -8
- package/src/kernel/boot-bind/store.test.ts +60 -0
- package/src/kernel/boot-bind/store.ts +142 -12
- package/src/kernel/boot.ts +28 -4
- package/src/mcp/server.ts +99 -57
- package/src/runtime/dev-request-log.test.ts +33 -0
- package/src/runtime/dev-request-log.ts +130 -0
- package/src/term.test.ts +98 -5
- package/src/term.ts +287 -6
- package/src/console/ui/dist/assets/style-Cnl7WLya.css +0 -3
package/src/kernel/boot.ts
CHANGED
|
@@ -76,8 +76,17 @@ export interface ElementRuntimes {
|
|
|
76
76
|
|
|
77
77
|
/** Declarations + options consumed by {@link bootApplication}. */
|
|
78
78
|
export interface BootOptions {
|
|
79
|
-
/**
|
|
79
|
+
/**
|
|
80
|
+
* Active environment (defaults to `dev`, or `stack` when
|
|
81
|
+
* {@link stack} / `OKE_STACK=1`).
|
|
82
|
+
*/
|
|
80
83
|
readonly env?: ConfigEnv;
|
|
84
|
+
/**
|
|
85
|
+
* Local-server mode (`oke dev -s` / `OKE_STACK=1`): force driver maps to the
|
|
86
|
+
* `stack` profile and prefer compose URLs (`.env.stack`).
|
|
87
|
+
* When unset, derived from `process.env.OKE_STACK`.
|
|
88
|
+
*/
|
|
89
|
+
readonly stack?: boolean;
|
|
81
90
|
/** Optional `oke.config.ts` document. */
|
|
82
91
|
readonly config?: OkeConfig;
|
|
83
92
|
/** Pre-built runtimes (skip construction when present). */
|
|
@@ -243,9 +252,24 @@ async function loadBind<T>(name: string): Promise<T> {
|
|
|
243
252
|
* @param options - Declarations, config, pre-built runtimes
|
|
244
253
|
*/
|
|
245
254
|
export async function bootApplication(
|
|
246
|
-
|
|
255
|
+
input: BootOptions = {},
|
|
247
256
|
): Promise<BootResult> {
|
|
248
|
-
const
|
|
257
|
+
const stack =
|
|
258
|
+
input.stack === true ||
|
|
259
|
+
(input.stack !== false && process.env.OKE_STACK === "1");
|
|
260
|
+
// `-s` always selects the `stack` driver profile — not a mix of test/dev +
|
|
261
|
+
// prod store overrides (templates often pin `env: "test"` for harnesses).
|
|
262
|
+
const env: ConfigEnv = stack ? "stack" : (input.env ?? "dev");
|
|
263
|
+
let config = input.config;
|
|
264
|
+
if (config === undefined) {
|
|
265
|
+
try {
|
|
266
|
+
const { loadOkeConfig } = await import("../cli/load-config.ts");
|
|
267
|
+
config = (await loadOkeConfig(process.cwd())).config;
|
|
268
|
+
} catch {
|
|
269
|
+
config = undefined;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
const options: BootOptions = { ...input, config, env, stack };
|
|
249
273
|
const pre = options.elements ?? {};
|
|
250
274
|
const now = options.now ?? (() => Date.now());
|
|
251
275
|
const needs = resolveElementNeeds(options);
|
|
@@ -340,7 +364,7 @@ export async function bootApplication(
|
|
|
340
364
|
let store = pre.store;
|
|
341
365
|
if (needs.store) {
|
|
342
366
|
if (!store) {
|
|
343
|
-
store = storeBind!.bindStore(options, env, now);
|
|
367
|
+
store = storeBind!.bindStore(options, env, now, stack);
|
|
344
368
|
} else {
|
|
345
369
|
for (const decl of options.stores ?? []) {
|
|
346
370
|
store.register?.(decl);
|
package/src/mcp/server.ts
CHANGED
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
* adapters receive structured operator ids.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import {
|
|
12
|
+
logDevRequest,
|
|
13
|
+
shouldLogDevRequests,
|
|
14
|
+
} from "../runtime/dev-request-log.ts";
|
|
11
15
|
import {
|
|
12
16
|
checkRequestSecurity,
|
|
13
17
|
forbiddenResponse,
|
|
@@ -67,28 +71,48 @@ export function createMcpServer(options: CreateMcpServerOptions): McpServer {
|
|
|
67
71
|
const transportSessions = new Set<string>();
|
|
68
72
|
|
|
69
73
|
const fetch = async (request: Request): Promise<Response> => {
|
|
74
|
+
const started = performance.now();
|
|
75
|
+
let flowLabel: string | undefined;
|
|
76
|
+
const url = new URL(request.url);
|
|
77
|
+
const method = request.method.toUpperCase();
|
|
78
|
+
|
|
79
|
+
const respond = (response: Response): Response => {
|
|
80
|
+
if (shouldLogDevRequests()) {
|
|
81
|
+
logDevRequest({
|
|
82
|
+
surface: "MCP",
|
|
83
|
+
method,
|
|
84
|
+
path: url.pathname,
|
|
85
|
+
flow: flowLabel,
|
|
86
|
+
status: response.status,
|
|
87
|
+
ms: Math.round(performance.now() - started),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return response;
|
|
91
|
+
};
|
|
92
|
+
|
|
70
93
|
const security = checkRequestSecurity(request, allowed);
|
|
71
|
-
if (!security.ok) return forbiddenResponse(security.reason);
|
|
94
|
+
if (!security.ok) return respond(forbiddenResponse(security.reason));
|
|
72
95
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
return Response.json({ ok: true, surface: "mcp" });
|
|
96
|
+
if (method === "GET" && url.pathname === "/health") {
|
|
97
|
+
return respond(Response.json({ ok: true, surface: "mcp" }));
|
|
76
98
|
}
|
|
77
99
|
|
|
78
|
-
if (
|
|
79
|
-
return new Response("Not Found", { status: 404 });
|
|
100
|
+
if (method !== "POST" || url.pathname !== "/mcp") {
|
|
101
|
+
return respond(new Response("Not Found", { status: 404 }));
|
|
80
102
|
}
|
|
81
103
|
|
|
82
104
|
const bearer = extractBearer(request.headers.get("authorization"));
|
|
83
105
|
if (bearer === null) {
|
|
84
|
-
return
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
106
|
+
return respond(
|
|
107
|
+
jsonRpcHttp(
|
|
108
|
+
rpcError(
|
|
109
|
+
null,
|
|
110
|
+
RpcErrorCode.unauthorized,
|
|
111
|
+
"Bearer token required",
|
|
112
|
+
asData({ reason: "missing-token" }, "error"),
|
|
113
|
+
),
|
|
114
|
+
401,
|
|
90
115
|
),
|
|
91
|
-
401,
|
|
92
116
|
);
|
|
93
117
|
}
|
|
94
118
|
|
|
@@ -103,14 +127,16 @@ export function createMcpServer(options: CreateMcpServerOptions): McpServer {
|
|
|
103
127
|
} catch (err) {
|
|
104
128
|
const message =
|
|
105
129
|
err instanceof SessionError ? err.message : "authentication failed";
|
|
106
|
-
return
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
130
|
+
return respond(
|
|
131
|
+
jsonRpcHttp(
|
|
132
|
+
rpcError(
|
|
133
|
+
null,
|
|
134
|
+
RpcErrorCode.unauthorized,
|
|
135
|
+
message,
|
|
136
|
+
asData({ reason: "auth-failed", message }, "error"),
|
|
137
|
+
),
|
|
138
|
+
401,
|
|
112
139
|
),
|
|
113
|
-
401,
|
|
114
140
|
);
|
|
115
141
|
}
|
|
116
142
|
|
|
@@ -122,22 +148,27 @@ export function createMcpServer(options: CreateMcpServerOptions): McpServer {
|
|
|
122
148
|
try {
|
|
123
149
|
body = await request.json();
|
|
124
150
|
} catch {
|
|
125
|
-
return
|
|
126
|
-
|
|
127
|
-
|
|
151
|
+
return respond(
|
|
152
|
+
jsonRpcHttp(
|
|
153
|
+
rpcError(null, RpcErrorCode.parse, "invalid JSON body"),
|
|
154
|
+
400,
|
|
155
|
+
),
|
|
128
156
|
);
|
|
129
157
|
}
|
|
130
158
|
|
|
131
159
|
const parsed = parseJsonRpcRequest(body);
|
|
132
160
|
if (!parsed.ok) {
|
|
133
|
-
return
|
|
134
|
-
|
|
135
|
-
|
|
161
|
+
return respond(
|
|
162
|
+
jsonRpcHttp(
|
|
163
|
+
rpcError(null, RpcErrorCode.invalidRequest, parsed.message),
|
|
164
|
+
400,
|
|
165
|
+
),
|
|
136
166
|
);
|
|
137
167
|
}
|
|
138
168
|
|
|
139
169
|
const { request: rpc } = parsed;
|
|
140
170
|
const id: JsonRpcId = rpc.id;
|
|
171
|
+
flowLabel = rpc.method;
|
|
141
172
|
|
|
142
173
|
switch (rpc.method) {
|
|
143
174
|
case "initialize": {
|
|
@@ -149,10 +180,10 @@ export function createMcpServer(options: CreateMcpServerOptions): McpServer {
|
|
|
149
180
|
serverInfo: { name: "okengine-mcp", version },
|
|
150
181
|
sessionId,
|
|
151
182
|
};
|
|
152
|
-
return jsonRpcHttp(rpcSuccess(id, result));
|
|
183
|
+
return respond(jsonRpcHttp(rpcSuccess(id, result)));
|
|
153
184
|
}
|
|
154
185
|
case "ping":
|
|
155
|
-
return jsonRpcHttp(rpcSuccess(id, { ok: true }));
|
|
186
|
+
return respond(jsonRpcHttp(rpcSuccess(id, { ok: true })));
|
|
156
187
|
case "tools/list": {
|
|
157
188
|
const listed = tools.listTools().map((t) => ({
|
|
158
189
|
name: t.name,
|
|
@@ -163,22 +194,27 @@ export function createMcpServer(options: CreateMcpServerOptions): McpServer {
|
|
|
163
194
|
destructiveHint: t.mutability === "write",
|
|
164
195
|
},
|
|
165
196
|
}));
|
|
166
|
-
return
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
197
|
+
return respond(
|
|
198
|
+
jsonRpcHttp(
|
|
199
|
+
rpcSuccess(id, {
|
|
200
|
+
tools: listed,
|
|
201
|
+
// Catalogue itself is data.
|
|
202
|
+
_oke: asData({ count: listed.length }, "catalog"),
|
|
203
|
+
}),
|
|
204
|
+
),
|
|
172
205
|
);
|
|
173
206
|
}
|
|
174
207
|
case "tools/call": {
|
|
175
208
|
const call = parseToolsCallParams(rpc.params);
|
|
176
209
|
if (!call.ok) {
|
|
177
|
-
return
|
|
178
|
-
|
|
179
|
-
|
|
210
|
+
return respond(
|
|
211
|
+
jsonRpcHttp(
|
|
212
|
+
rpcError(id, RpcErrorCode.invalidParams, call.message),
|
|
213
|
+
400,
|
|
214
|
+
),
|
|
180
215
|
);
|
|
181
216
|
}
|
|
217
|
+
flowLabel = call.name;
|
|
182
218
|
const result = await tools.callTool(
|
|
183
219
|
requester,
|
|
184
220
|
call.name,
|
|
@@ -193,34 +229,40 @@ export function createMcpServer(options: CreateMcpServerOptions): McpServer {
|
|
|
193
229
|
: result.code === "not-found"
|
|
194
230
|
? RpcErrorCode.methodNotFound
|
|
195
231
|
: RpcErrorCode.invalidParams;
|
|
196
|
-
return
|
|
197
|
-
|
|
198
|
-
|
|
232
|
+
return respond(
|
|
233
|
+
jsonRpcHttp(
|
|
234
|
+
rpcError(id, code, result.message, result.data),
|
|
235
|
+
result.code === "unauthorized" ? 401 : 403,
|
|
236
|
+
),
|
|
199
237
|
);
|
|
200
238
|
}
|
|
201
239
|
// MCP content blocks: text carries JSON of the inert envelope.
|
|
202
240
|
// Agents must treat it as data (envelope.kind === "data").
|
|
203
|
-
return
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
241
|
+
return respond(
|
|
242
|
+
jsonRpcHttp(
|
|
243
|
+
rpcSuccess(id, {
|
|
244
|
+
content: [
|
|
245
|
+
{
|
|
246
|
+
type: "text",
|
|
247
|
+
text: JSON.stringify(result.data),
|
|
248
|
+
},
|
|
249
|
+
],
|
|
250
|
+
structuredContent: result.data,
|
|
251
|
+
isError: false,
|
|
252
|
+
}),
|
|
253
|
+
),
|
|
214
254
|
);
|
|
215
255
|
}
|
|
216
256
|
default:
|
|
217
|
-
return
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
257
|
+
return respond(
|
|
258
|
+
jsonRpcHttp(
|
|
259
|
+
rpcError(
|
|
260
|
+
id,
|
|
261
|
+
RpcErrorCode.methodNotFound,
|
|
262
|
+
`method not found: ${rpc.method}`,
|
|
263
|
+
),
|
|
264
|
+
404,
|
|
222
265
|
),
|
|
223
|
-
404,
|
|
224
266
|
);
|
|
225
267
|
}
|
|
226
268
|
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev request log gating and silence rules.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
6
|
+
import {
|
|
7
|
+
isSilentDevRequest,
|
|
8
|
+
shouldLogDevRequests,
|
|
9
|
+
} from "./dev-request-log.ts";
|
|
10
|
+
|
|
11
|
+
describe("dev-request-log", () => {
|
|
12
|
+
const prev = process.env.OKE_DEV_REQUEST_LOG;
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
if (prev === undefined) delete process.env.OKE_DEV_REQUEST_LOG;
|
|
16
|
+
else process.env.OKE_DEV_REQUEST_LOG = prev;
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("shouldLogDevRequests follows OKE_DEV_REQUEST_LOG", () => {
|
|
20
|
+
process.env.OKE_DEV_REQUEST_LOG = "1";
|
|
21
|
+
expect(shouldLogDevRequests()).toBe(true);
|
|
22
|
+
process.env.OKE_DEV_REQUEST_LOG = "0";
|
|
23
|
+
expect(shouldLogDevRequests()).toBe(false);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("isSilentDevRequest skips live, health, assets, client.json", () => {
|
|
27
|
+
expect(isSilentDevRequest("GET", "/console/live")).toBe(true);
|
|
28
|
+
expect(isSilentDevRequest("GET", "/health")).toBe(true);
|
|
29
|
+
expect(isSilentDevRequest("GET", "/assets/index.js")).toBe(true);
|
|
30
|
+
expect(isSilentDevRequest("GET", "/_oke/client.json")).toBe(true);
|
|
31
|
+
expect(isSilentDevRequest("POST", "/console/flows")).toBe(false);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev-only request lines for `oke dev` (App / Console / MCP).
|
|
3
|
+
*
|
|
4
|
+
* Gated by `OKE_DEV_REQUEST_LOG=1`. Surfaces share one TTY; label + color
|
|
5
|
+
* keep streams readable without a multiplexer.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
9
|
+
import { formatRequestLine, type DevLogSurface } from "../term.ts";
|
|
10
|
+
|
|
11
|
+
const surfaceAls = new AsyncLocalStorage<DevLogSurface>();
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Whether colored request lines should print.
|
|
15
|
+
*/
|
|
16
|
+
export function shouldLogDevRequests(): boolean {
|
|
17
|
+
return process.env.OKE_DEV_REQUEST_LOG === "1";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Active surface for this async context, or `OKE_DEV_SURFACE`, or `App`.
|
|
22
|
+
*/
|
|
23
|
+
export function currentDevSurface(): DevLogSurface {
|
|
24
|
+
const fromAls = surfaceAls.getStore();
|
|
25
|
+
if (fromAls) return fromAls;
|
|
26
|
+
const fromEnv = process.env.OKE_DEV_SURFACE;
|
|
27
|
+
if (fromEnv === "App" || fromEnv === "Console" || fromEnv === "MCP") {
|
|
28
|
+
return fromEnv;
|
|
29
|
+
}
|
|
30
|
+
return "App";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Run a handler tagged with a surface (Console wrap around `app.fetch`).
|
|
35
|
+
*
|
|
36
|
+
* @param surface - App · Console · MCP
|
|
37
|
+
* @param fn - Async work
|
|
38
|
+
*/
|
|
39
|
+
export function runWithDevSurface<T>(
|
|
40
|
+
surface: DevLogSurface,
|
|
41
|
+
fn: () => T | Promise<T>,
|
|
42
|
+
): T | Promise<T> {
|
|
43
|
+
return surfaceAls.run(surface, fn);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Fields for one request line. */
|
|
47
|
+
export type DevRequestLogInput = {
|
|
48
|
+
readonly surface?: DevLogSurface;
|
|
49
|
+
readonly method: string;
|
|
50
|
+
readonly path: string;
|
|
51
|
+
/** Flow name, RPC method, or tool id. */
|
|
52
|
+
readonly flow?: string;
|
|
53
|
+
readonly status: number;
|
|
54
|
+
readonly ms: number;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Paths that would drown the TTY (live WS, health probes, static assets).
|
|
59
|
+
*
|
|
60
|
+
* @param method - HTTP method
|
|
61
|
+
* @param path - URL pathname
|
|
62
|
+
*/
|
|
63
|
+
export function isSilentDevRequest(method: string, path: string): boolean {
|
|
64
|
+
if (path === "/console/live") return true;
|
|
65
|
+
// Client-types regen — lands between hero and Logs and breaks the separator.
|
|
66
|
+
if (path === "/_oke/client.json") return true;
|
|
67
|
+
if (method === "GET" && (path === "/health" || path.endsWith("/health"))) {
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
return /\.(?:js|css|map|svg|png|ico|woff2?|ttf|webp)$/i.test(path);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Print one request line when {@link shouldLogDevRequests} is on.
|
|
75
|
+
*
|
|
76
|
+
* @param input - Request summary
|
|
77
|
+
*/
|
|
78
|
+
export function logDevRequest(input: DevRequestLogInput): void {
|
|
79
|
+
if (!shouldLogDevRequests()) return;
|
|
80
|
+
if (isSilentDevRequest(input.method.toUpperCase(), input.path)) return;
|
|
81
|
+
process.stdout.write(
|
|
82
|
+
formatRequestLine({
|
|
83
|
+
surface: input.surface ?? currentDevSurface(),
|
|
84
|
+
method: input.method,
|
|
85
|
+
path: input.path,
|
|
86
|
+
flow: input.flow,
|
|
87
|
+
status: input.status,
|
|
88
|
+
ms: input.ms,
|
|
89
|
+
}),
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Time an async fetch handler and log the outcome.
|
|
95
|
+
*
|
|
96
|
+
* @param surface - Fixed surface (MCP) or omit to use ALS/env
|
|
97
|
+
* @param request - Incoming request
|
|
98
|
+
* @param handle - Inner fetch
|
|
99
|
+
* @param resolveFlow - Optional label after the response (e.g. RPC method)
|
|
100
|
+
*/
|
|
101
|
+
export async function timedDevFetch(
|
|
102
|
+
request: Request,
|
|
103
|
+
handle: (request: Request) => Response | Promise<Response>,
|
|
104
|
+
options: {
|
|
105
|
+
readonly surface?: DevLogSurface;
|
|
106
|
+
readonly resolveFlow?: (
|
|
107
|
+
request: Request,
|
|
108
|
+
response: Response,
|
|
109
|
+
) => string | undefined;
|
|
110
|
+
/** Skip logging for this path/method. */
|
|
111
|
+
readonly silent?: (request: Request) => boolean;
|
|
112
|
+
} = {},
|
|
113
|
+
): Promise<Response> {
|
|
114
|
+
if (!shouldLogDevRequests() || options.silent?.(request)) {
|
|
115
|
+
return handle(request);
|
|
116
|
+
}
|
|
117
|
+
const started = performance.now();
|
|
118
|
+
const url = new URL(request.url);
|
|
119
|
+
const method = request.method.toUpperCase();
|
|
120
|
+
const response = await handle(request);
|
|
121
|
+
logDevRequest({
|
|
122
|
+
surface: options.surface ?? currentDevSurface(),
|
|
123
|
+
method,
|
|
124
|
+
path: url.pathname,
|
|
125
|
+
flow: options.resolveFlow?.(request, response),
|
|
126
|
+
status: response.status,
|
|
127
|
+
ms: Math.round(performance.now() - started),
|
|
128
|
+
});
|
|
129
|
+
return response;
|
|
130
|
+
}
|
package/src/term.test.ts
CHANGED
|
@@ -7,7 +7,12 @@ import {
|
|
|
7
7
|
formatAppReadyLine,
|
|
8
8
|
formatClaimNote,
|
|
9
9
|
formatDevBanner,
|
|
10
|
+
formatDevHero,
|
|
11
|
+
formatDevLogSeparator,
|
|
12
|
+
formatOkeWordmark,
|
|
10
13
|
formatServiceLine,
|
|
14
|
+
formatRequestLine,
|
|
15
|
+
formatStackSummary,
|
|
11
16
|
formatStatusLine,
|
|
12
17
|
termStyle,
|
|
13
18
|
} from "./term.ts";
|
|
@@ -19,11 +24,37 @@ describe("term", () => {
|
|
|
19
24
|
expect(s.bold).toBe("");
|
|
20
25
|
});
|
|
21
26
|
|
|
22
|
-
test("
|
|
23
|
-
const out =
|
|
24
|
-
expect(out).toContain("
|
|
25
|
-
expect(out).toContain("
|
|
26
|
-
|
|
27
|
+
test("formatOkeWordmark is block letters", () => {
|
|
28
|
+
const out = formatOkeWordmark(false);
|
|
29
|
+
expect(out).toContain("██");
|
|
30
|
+
expect(out).toContain("╗");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("formatDevBanner shows profile env system elements", () => {
|
|
34
|
+
const out = formatDevBanner({
|
|
35
|
+
color: false,
|
|
36
|
+
version: "0.2.4",
|
|
37
|
+
profile: "local-server",
|
|
38
|
+
runtimeEnv: "local",
|
|
39
|
+
system: "darwin 25.4.0 · bun 1.3.14",
|
|
40
|
+
elements: [
|
|
41
|
+
{ element: "flow", detail: "●" },
|
|
42
|
+
{ element: "store", detail: "sql postgres · kv redis" },
|
|
43
|
+
{ element: "signal", detail: "memory" },
|
|
44
|
+
],
|
|
45
|
+
});
|
|
46
|
+
expect(out).toContain("oke dev v0.2.4");
|
|
47
|
+
expect(out).toContain("profile");
|
|
48
|
+
expect(out).toContain("local-server");
|
|
49
|
+
expect(out).toContain("env");
|
|
50
|
+
expect(out).toContain("local");
|
|
51
|
+
expect(out).toContain("system");
|
|
52
|
+
expect(out).toContain("bun 1.3.14");
|
|
53
|
+
expect(out).toContain("elements");
|
|
54
|
+
expect(out).toContain("store");
|
|
55
|
+
expect(out).toContain("postgres");
|
|
56
|
+
expect(out).not.toContain("on(Trigger)");
|
|
57
|
+
expect(out).not.toContain("O·K·E");
|
|
27
58
|
expect(out).not.toMatch(/\u001b\[/);
|
|
28
59
|
});
|
|
29
60
|
|
|
@@ -36,6 +67,34 @@ describe("term", () => {
|
|
|
36
67
|
);
|
|
37
68
|
});
|
|
38
69
|
|
|
70
|
+
test("formatDevHero keeps App Console MCP URLs", () => {
|
|
71
|
+
const out = formatDevHero({
|
|
72
|
+
appUrl: "http://127.0.0.1:6530",
|
|
73
|
+
consoleUrl: "http://127.0.0.1:6533",
|
|
74
|
+
mcpUrl: "http://127.0.0.1:6535",
|
|
75
|
+
profile: "local",
|
|
76
|
+
runtimeEnv: "local",
|
|
77
|
+
system: "darwin 25.4.0 · bun 1.3.14",
|
|
78
|
+
elements: [{ element: "flow", detail: "●" }],
|
|
79
|
+
color: false,
|
|
80
|
+
});
|
|
81
|
+
expect(out).toContain("oke dev");
|
|
82
|
+
expect(out).toContain("██");
|
|
83
|
+
expect(out).toContain("App");
|
|
84
|
+
expect(out).toContain("Console");
|
|
85
|
+
expect(out).toContain("MCP");
|
|
86
|
+
expect(out).toContain("http://127.0.0.1:6530");
|
|
87
|
+
expect(out).toContain("└");
|
|
88
|
+
expect(out).toContain("Logs");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("formatDevLogSeparator closes hero and titles Logs", () => {
|
|
92
|
+
const out = formatDevLogSeparator(false);
|
|
93
|
+
expect(out).toContain("│");
|
|
94
|
+
expect(out).toContain("└");
|
|
95
|
+
expect(out).toContain("Logs");
|
|
96
|
+
});
|
|
97
|
+
|
|
39
98
|
test("formatClaimNote embeds code and ownership line", () => {
|
|
40
99
|
const code = "aabbccddeeff00112233445566778899";
|
|
41
100
|
const out = formatClaimNote(code, false);
|
|
@@ -48,4 +107,38 @@ describe("term", () => {
|
|
|
48
107
|
test("formatStatusLine keeps message", () => {
|
|
49
108
|
expect(formatStatusLine("stack up (sql)", false)).toContain("stack up");
|
|
50
109
|
});
|
|
110
|
+
|
|
111
|
+
test("formatRequestLine shows date, time, surface, flow, ms, status", () => {
|
|
112
|
+
const at = new Date(2026, 6, 26, 3, 11, 42);
|
|
113
|
+
const out = formatRequestLine({
|
|
114
|
+
surface: "App",
|
|
115
|
+
method: "GET",
|
|
116
|
+
path: "/health",
|
|
117
|
+
flow: "main.health",
|
|
118
|
+
status: 200,
|
|
119
|
+
ms: 12,
|
|
120
|
+
at,
|
|
121
|
+
color: false,
|
|
122
|
+
});
|
|
123
|
+
expect(out).toContain("2026-07-26");
|
|
124
|
+
expect(out).toContain("03:11:42");
|
|
125
|
+
expect(out).toContain("App");
|
|
126
|
+
expect(out).toContain("200");
|
|
127
|
+
expect(out).not.toMatch(/\u001b\[/);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("formatStackSummary is scannable", () => {
|
|
131
|
+
const out = formatStackSummary({
|
|
132
|
+
project: "oke-dev-a3f791",
|
|
133
|
+
services: [
|
|
134
|
+
{ label: "postgres", hostPort: 15975 },
|
|
135
|
+
{ label: "redis", hostPort: 16975 },
|
|
136
|
+
],
|
|
137
|
+
appDrivers: ["postgres", "redis"],
|
|
138
|
+
color: false,
|
|
139
|
+
});
|
|
140
|
+
expect(out).toContain("Stack");
|
|
141
|
+
expect(out).toContain(":15975");
|
|
142
|
+
expect(out).not.toMatch(/\u001b\[/);
|
|
143
|
+
});
|
|
51
144
|
});
|