okengine 0.2.3 → 0.2.5
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 +136 -17
- package/src/cli/docker-cli.test.ts +10 -8
- package/src/cli/docker.ts +18 -7
- package/src/cli/hero-meta.test.ts +85 -0
- package/src/cli/hero-meta.ts +252 -0
- package/src/cli/load-config.images.test.ts +51 -0
- package/src/cli/load-config.ts +84 -6
- 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/flows.ts +3 -1
- package/src/console/server/operator-db.test.ts +140 -2
- package/src/console/server/operator-db.ts +147 -1
- package/src/console/server/plugin.ts +20 -0
- package/src/console/server/serve.ts +8 -1
- package/src/console/server/state.ts +12 -1
- package/src/console/ui/dist/assets/{index-Bnf_3Hei.js → index-Dy4jht9P.js} +1 -1
- package/src/console/ui/dist/index.html +1 -1
- package/src/console/ui/shell/App.tsx +10 -2
- package/src/docker/compose.ts +45 -14
- package/src/docker/derive.ts +29 -9
- package/src/docker/docker.test.ts +28 -4
- package/src/docker/dockerfile.integration.test.ts +2 -0
- 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 +16 -7
- 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
|
@@ -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
|
});
|
package/src/term.ts
CHANGED
|
@@ -27,6 +27,7 @@ export type TermStyle = {
|
|
|
27
27
|
readonly magenta: string;
|
|
28
28
|
readonly yellow: string;
|
|
29
29
|
readonly white: string;
|
|
30
|
+
readonly red: string;
|
|
30
31
|
};
|
|
31
32
|
|
|
32
33
|
/**
|
|
@@ -45,6 +46,7 @@ export function termStyle(color: boolean = termColorEnabled()): TermStyle {
|
|
|
45
46
|
magenta: "",
|
|
46
47
|
yellow: "",
|
|
47
48
|
white: "",
|
|
49
|
+
red: "",
|
|
48
50
|
};
|
|
49
51
|
}
|
|
50
52
|
return {
|
|
@@ -56,22 +58,108 @@ export function termStyle(color: boolean = termColorEnabled()): TermStyle {
|
|
|
56
58
|
magenta: `${ESC}35m`,
|
|
57
59
|
yellow: `${ESC}33m`,
|
|
58
60
|
white: `${ESC}37m`,
|
|
61
|
+
red: `${ESC}31m`,
|
|
59
62
|
};
|
|
60
63
|
}
|
|
61
64
|
|
|
65
|
+
/** One eight-element row in the hero. */
|
|
66
|
+
export type DevHeroElement = {
|
|
67
|
+
readonly element: string;
|
|
68
|
+
readonly detail: string;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/** Shared options for the `oke dev` hero / banner. */
|
|
72
|
+
export type DevHeroMeta = {
|
|
73
|
+
/** `local` · `local-server` · `test` · `production` */
|
|
74
|
+
readonly profile?: string;
|
|
75
|
+
/** Data plane: `local` · `production` */
|
|
76
|
+
readonly runtimeEnv?: string;
|
|
77
|
+
/** Host OS + Bun, e.g. `darwin 25.4.0 · bun 1.3.14`. */
|
|
78
|
+
readonly system?: string;
|
|
79
|
+
/** Active drivers for the eight elements. */
|
|
80
|
+
readonly elements?: readonly DevHeroElement[];
|
|
81
|
+
readonly version?: string;
|
|
82
|
+
readonly color?: boolean;
|
|
83
|
+
readonly watching?: boolean;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Compact OKE wordmark — CRT / hacker block letters.
|
|
88
|
+
*
|
|
89
|
+
* @param color - Color on/off
|
|
90
|
+
*/
|
|
91
|
+
export function formatOkeWordmark(
|
|
92
|
+
color: boolean = termColorEnabled(),
|
|
93
|
+
): string {
|
|
94
|
+
const s = termStyle(color);
|
|
95
|
+
const ink = `${s.green}${s.bold}`;
|
|
96
|
+
const r = s.reset;
|
|
97
|
+
return [
|
|
98
|
+
`${ink} ██████╗ ██╗ ██╗███████╗${r}`,
|
|
99
|
+
`${ink} ██╔═══██╗██║ ██╔╝██╔════╝${r}`,
|
|
100
|
+
`${ink} ██║ ██║█████╔╝ █████╗ ${r}`,
|
|
101
|
+
`${ink} ██║ ██║██╔═██╗ ██╔══╝ ${r}`,
|
|
102
|
+
`${ink} ╚██████╔╝██║ ██╗███████╗${r}`,
|
|
103
|
+
`${ink} ╚═════╝ ╚═╝ ╚═╝╚══════╝${r}`,
|
|
104
|
+
].join("\n") + "\n";
|
|
105
|
+
}
|
|
106
|
+
|
|
62
107
|
/**
|
|
63
|
-
*
|
|
108
|
+
* Detail rows — profile, env, system, eight elements + drivers.
|
|
64
109
|
*
|
|
65
|
-
* @param options -
|
|
110
|
+
* @param options - Snapshot fields from {@link import("./cli/hero-meta.ts").buildDevHeroSnapshot}
|
|
66
111
|
*/
|
|
67
|
-
export function
|
|
68
|
-
options:
|
|
112
|
+
export function formatDevHeroDetails(
|
|
113
|
+
options: DevHeroMeta = {},
|
|
69
114
|
): string {
|
|
115
|
+
const s = termStyle(options.color ?? termColorEnabled());
|
|
116
|
+
const bar = `${s.dim}│${s.reset}`;
|
|
117
|
+
const label = (name: string) => `${s.dim}${name.padEnd(9)}${s.reset}`;
|
|
118
|
+
const lines: string[] = [];
|
|
119
|
+
if (options.profile) {
|
|
120
|
+
lines.push(
|
|
121
|
+
`${bar} ${label("profile")} ${s.cyan}${options.profile}${s.reset}`,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
if (options.runtimeEnv) {
|
|
125
|
+
lines.push(
|
|
126
|
+
`${bar} ${label("env")} ${s.cyan}${options.runtimeEnv}${s.reset}`,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
if (options.system) {
|
|
130
|
+
lines.push(
|
|
131
|
+
`${bar} ${label("system")} ${s.dim}${options.system}${s.reset}`,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
const elements = options.elements ?? [];
|
|
135
|
+
if (elements.length > 0) {
|
|
136
|
+
lines.push(`${bar} ${s.dim}elements${s.reset}`);
|
|
137
|
+
for (const row of elements) {
|
|
138
|
+
const idle = row.detail === "—";
|
|
139
|
+
const detail = idle
|
|
140
|
+
? `${s.dim}—${s.reset}`
|
|
141
|
+
: `${s.cyan}${row.detail}${s.reset}`;
|
|
142
|
+
lines.push(
|
|
143
|
+
`${bar} ${s.dim}${row.element.padEnd(9)}${s.reset} ${detail}`,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return lines.length > 0 ? `${lines.join("\n")}\n` : "";
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* `oke dev` intro — wordmark + details; service URLs print as each surface binds.
|
|
152
|
+
*
|
|
153
|
+
* @param options - Color / watch / ports / entry
|
|
154
|
+
*/
|
|
155
|
+
export function formatDevBanner(options: DevHeroMeta = {}): string {
|
|
70
156
|
const s = termStyle(options.color ?? termColorEnabled());
|
|
71
157
|
const bar = `${s.dim}│${s.reset}`;
|
|
72
158
|
const lines = [
|
|
73
159
|
"",
|
|
74
|
-
|
|
160
|
+
formatOkeWordmark(options.color ?? termColorEnabled()).trimEnd(),
|
|
161
|
+
`${s.cyan}${s.bold}┌${s.reset} ${s.bold}oke dev${s.reset}` +
|
|
162
|
+
(options.version ? ` ${s.dim}v${options.version}${s.reset}` : ""),
|
|
75
163
|
bar,
|
|
76
164
|
`${s.green}◇${s.reset} Starting`,
|
|
77
165
|
];
|
|
@@ -80,6 +168,7 @@ export function formatDevBanner(
|
|
|
80
168
|
`${bar} ${s.dim}watching — client types regenerate on save${s.reset}`,
|
|
81
169
|
);
|
|
82
170
|
}
|
|
171
|
+
lines.push(formatDevHeroDetails(options).trimEnd());
|
|
83
172
|
lines.push(bar);
|
|
84
173
|
return `${lines.join("\n")}\n`;
|
|
85
174
|
}
|
|
@@ -116,7 +205,60 @@ export function formatAppReadyLine(
|
|
|
116
205
|
}
|
|
117
206
|
|
|
118
207
|
/**
|
|
119
|
-
*
|
|
208
|
+
* ANSI clear screen + home (drops request logs; hero is reprinted after).
|
|
209
|
+
*/
|
|
210
|
+
export function clearTerminalScreen(): string {
|
|
211
|
+
return "\u001b[2J\u001b[3J\u001b[H";
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Compact hero reprinted on soft reload — URLs stay, request logs do not.
|
|
216
|
+
*
|
|
217
|
+
* @param options - Surface base URLs + meta
|
|
218
|
+
*/
|
|
219
|
+
export function formatDevHero(
|
|
220
|
+
options: DevHeroMeta & {
|
|
221
|
+
readonly appUrl: string;
|
|
222
|
+
readonly consoleUrl: string;
|
|
223
|
+
readonly mcpUrl: string;
|
|
224
|
+
},
|
|
225
|
+
): string {
|
|
226
|
+
const color = options.color ?? termColorEnabled();
|
|
227
|
+
const s = termStyle(color);
|
|
228
|
+
const bar = `${s.dim}│${s.reset}`;
|
|
229
|
+
return (
|
|
230
|
+
`\n${formatOkeWordmark(color)}` +
|
|
231
|
+
`${s.cyan}${s.bold}┌${s.reset} ${s.bold}oke dev${s.reset}` +
|
|
232
|
+
(options.version ? ` ${s.dim}v${options.version}${s.reset}` : "") +
|
|
233
|
+
`\n${bar}\n` +
|
|
234
|
+
formatDevHeroDetails({ ...options, color }) +
|
|
235
|
+
`${bar}\n` +
|
|
236
|
+
formatAppReadyLine(options.appUrl, color) +
|
|
237
|
+
formatServiceLine("Console", options.consoleUrl, color) +
|
|
238
|
+
formatServiceLine("MCP", options.mcpUrl, color) +
|
|
239
|
+
formatDevLogSeparator(color)
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Closes the hero block and opens the Logs section.
|
|
245
|
+
*
|
|
246
|
+
* @param color - Color on/off
|
|
247
|
+
*/
|
|
248
|
+
export function formatDevLogSeparator(
|
|
249
|
+
color: boolean = termColorEnabled(),
|
|
250
|
+
): string {
|
|
251
|
+
const s = termStyle(color);
|
|
252
|
+
return (
|
|
253
|
+
`${s.dim}│${s.reset}\n` +
|
|
254
|
+
`${s.dim}└${s.reset}\n` +
|
|
255
|
+
`\n${s.green}◇${s.reset} ${s.bold}Logs${s.reset}\n` +
|
|
256
|
+
`${s.dim}│${s.reset}\n\n`
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Quiet status line (regen, …).
|
|
120
262
|
*
|
|
121
263
|
* @param message - Status text
|
|
122
264
|
* @param color - Color on/off
|
|
@@ -129,6 +271,145 @@ export function formatStatusLine(
|
|
|
129
271
|
return `${s.dim}│${s.reset} ${s.dim}${message}${s.reset}\n`;
|
|
130
272
|
}
|
|
131
273
|
|
|
274
|
+
/** One infra service row for {@link formatStackSummary}. */
|
|
275
|
+
export type StackSummaryService = {
|
|
276
|
+
/** Human label (`postgres`, `redis`). */
|
|
277
|
+
readonly label: string;
|
|
278
|
+
/** Published host port. */
|
|
279
|
+
readonly hostPort: number;
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
/** Surfaces that emit request lines during `oke dev`. */
|
|
283
|
+
export type DevLogSurface = "App" | "Console" | "MCP";
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Local calendar date `YYYY-MM-DD` (no time).
|
|
287
|
+
*
|
|
288
|
+
* @param at - Instant (default now)
|
|
289
|
+
*/
|
|
290
|
+
export function formatDevLogDate(at: Date = new Date()): string {
|
|
291
|
+
const y = at.getFullYear();
|
|
292
|
+
const m = String(at.getMonth() + 1).padStart(2, "0");
|
|
293
|
+
const d = String(at.getDate()).padStart(2, "0");
|
|
294
|
+
return `${y}-${m}-${d}`;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Local clock time `HH:MM:SS` (no date).
|
|
299
|
+
*
|
|
300
|
+
* @param at - Instant (default now)
|
|
301
|
+
*/
|
|
302
|
+
export function formatDevLogTime(at: Date = new Date()): string {
|
|
303
|
+
const h = String(at.getHours()).padStart(2, "0");
|
|
304
|
+
const min = String(at.getMinutes()).padStart(2, "0");
|
|
305
|
+
const sec = String(at.getSeconds()).padStart(2, "0");
|
|
306
|
+
return `${h}:${min}:${sec}`;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* One HTTP/RPC request line for the `oke dev` TTY.
|
|
311
|
+
*
|
|
312
|
+
* @example
|
|
313
|
+
* `● App GET /health main.health 12ms 200 2026-07-26 03:11:42`
|
|
314
|
+
*
|
|
315
|
+
* @param options - Surface, method, path, flow, timing, status
|
|
316
|
+
*/
|
|
317
|
+
export function formatRequestLine(
|
|
318
|
+
options: {
|
|
319
|
+
readonly surface: DevLogSurface;
|
|
320
|
+
readonly method: string;
|
|
321
|
+
readonly path: string;
|
|
322
|
+
readonly flow?: string;
|
|
323
|
+
readonly status: number;
|
|
324
|
+
readonly ms: number;
|
|
325
|
+
/** Instant for date/time columns (default now). */
|
|
326
|
+
readonly at?: Date;
|
|
327
|
+
readonly color?: boolean;
|
|
328
|
+
},
|
|
329
|
+
): string {
|
|
330
|
+
const s = termStyle(options.color ?? termColorEnabled());
|
|
331
|
+
const at = options.at ?? new Date();
|
|
332
|
+
const date = formatDevLogDate(at);
|
|
333
|
+
const time = formatDevLogTime(at);
|
|
334
|
+
const surfaceColor =
|
|
335
|
+
options.surface === "App"
|
|
336
|
+
? s.green
|
|
337
|
+
: options.surface === "Console"
|
|
338
|
+
? s.magenta
|
|
339
|
+
: s.cyan;
|
|
340
|
+
const statusColor =
|
|
341
|
+
options.status >= 500
|
|
342
|
+
? s.red
|
|
343
|
+
: options.status >= 400
|
|
344
|
+
? s.yellow
|
|
345
|
+
: s.green;
|
|
346
|
+
const methodRaw = options.method.toUpperCase();
|
|
347
|
+
const methodColor =
|
|
348
|
+
methodRaw === "GET"
|
|
349
|
+
? s.green
|
|
350
|
+
: methodRaw === "POST"
|
|
351
|
+
? s.yellow
|
|
352
|
+
: methodRaw === "PUT" || methodRaw === "PATCH"
|
|
353
|
+
? s.magenta
|
|
354
|
+
: methodRaw === "DELETE"
|
|
355
|
+
? s.red
|
|
356
|
+
: s.cyan;
|
|
357
|
+
const method = methodRaw.padEnd(4);
|
|
358
|
+
const path = options.path.length > 28
|
|
359
|
+
? `${options.path.slice(0, 27)}…`
|
|
360
|
+
: options.path.padEnd(28);
|
|
361
|
+
const flow = (options.flow ?? "—").padEnd(22);
|
|
362
|
+
const ms = `${options.ms}ms`.padStart(6);
|
|
363
|
+
return (
|
|
364
|
+
`${surfaceColor}●${s.reset} ` +
|
|
365
|
+
`${surfaceColor}${options.surface.padEnd(7)}${s.reset} ` +
|
|
366
|
+
`${methodColor}${method}${s.reset} ` +
|
|
367
|
+
`${s.cyan}${path}${s.reset} ` +
|
|
368
|
+
`${s.dim}${flow}${s.reset} ` +
|
|
369
|
+
`${s.dim}${ms}${s.reset} ` +
|
|
370
|
+
`${statusColor}${options.status}${s.reset} ` +
|
|
371
|
+
`${s.dim}${date}${s.reset} ` +
|
|
372
|
+
`${s.dim}${time}${s.reset}\n`
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Compact `oke dev -s` summary — project, ports, app driver mode.
|
|
378
|
+
*
|
|
379
|
+
* @param options - Project name, services, driver labels
|
|
380
|
+
*/
|
|
381
|
+
export function formatStackSummary(
|
|
382
|
+
options: {
|
|
383
|
+
readonly project: string;
|
|
384
|
+
readonly services: readonly StackSummaryService[];
|
|
385
|
+
/** Drivers the host app will use (e.g. `postgres`, `redis`). */
|
|
386
|
+
readonly appDrivers?: readonly string[];
|
|
387
|
+
readonly color?: boolean;
|
|
388
|
+
},
|
|
389
|
+
): string {
|
|
390
|
+
const s = termStyle(options.color ?? termColorEnabled());
|
|
391
|
+
const bar = `${s.dim}│${s.reset}`;
|
|
392
|
+
const pad = (label: string) => label.padEnd(8);
|
|
393
|
+
const lines: string[] = [
|
|
394
|
+
`${s.green}◇${s.reset} ${s.bold}Stack${s.reset} ${s.cyan}${options.project}${s.reset}`,
|
|
395
|
+
];
|
|
396
|
+
for (const svc of options.services) {
|
|
397
|
+
lines.push(
|
|
398
|
+
`${bar} ${s.dim}${pad(svc.label)}${s.reset} ${s.cyan}:${svc.hostPort}${s.reset}`,
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
const drivers = options.appDrivers ?? [];
|
|
402
|
+
const appDetail =
|
|
403
|
+
drivers.length > 0
|
|
404
|
+
? `host Bun · ${drivers.join(" + ")}`
|
|
405
|
+
: "host Bun";
|
|
406
|
+
lines.push(
|
|
407
|
+
`${bar} ${s.dim}${pad("app")}${s.reset} ${s.dim}${appDetail}${s.reset}`,
|
|
408
|
+
);
|
|
409
|
+
lines.push(bar);
|
|
410
|
+
return `${lines.join("\n")}\n`;
|
|
411
|
+
}
|
|
412
|
+
|
|
132
413
|
/**
|
|
133
414
|
* First-admin claim code note (clack `note`-style box).
|
|
134
415
|
*
|