okengine 0.9.1 → 0.10.0
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/AGENTS.md +1 -1
- package/manifest.v1.schema.json +9 -1
- package/package.json +3 -3
- package/site/content/docs/console/runs.mdx +10 -0
- package/site/content/docs/console/traces.mdx +4 -0
- package/site/content/docs/elements/ai.mdx +54 -32
- package/site/content/docs/elements/channel.mdx +97 -9
- package/site/content/docs/elements/clock.mdx +1 -0
- package/site/content/docs/elements/flow.mdx +42 -8
- package/site/content/docs/get-started/basic-usage.mdx +5 -1
- package/site/content/docs/get-started/installation.mdx +9 -5
- package/site/content/docs/meta.json +2 -0
- package/site/content/docs/providers/azure-redis.mdx +85 -0
- package/site/content/docs/providers/cockroachdb.mdx +86 -0
- package/site/content/docs/providers/digitalocean-caching.mdx +88 -0
- package/site/content/docs/providers/dragonfly-cloud.mdx +82 -0
- package/site/content/docs/providers/elasticache.mdx +92 -0
- package/site/content/docs/providers/index.mdx +101 -0
- package/site/content/docs/providers/memorystore.mdx +89 -0
- package/site/content/docs/providers/meta.json +20 -0
- package/site/content/docs/providers/neon.mdx +115 -0
- package/site/content/docs/providers/redis-cloud.mdx +93 -0
- package/site/content/docs/providers/supabase.mdx +117 -0
- package/site/content/docs/providers/upstash.mdx +91 -0
- package/site/content/docs/providers/yugabytedb.mdx +95 -0
- package/site/content/docs/recipes/caddy.mdx +128 -0
- package/site/content/docs/recipes/cockroachdb.mdx +127 -0
- package/site/content/docs/recipes/dragonfly.mdx +125 -0
- package/site/content/docs/recipes/index.mdx +146 -0
- package/site/content/docs/recipes/llama-cpp.mdx +153 -0
- package/site/content/docs/recipes/mailpit.mdx +136 -0
- package/site/content/docs/recipes/meilisearch.mdx +139 -0
- package/site/content/docs/recipes/meta.json +31 -0
- package/site/content/docs/recipes/ollama.mdx +142 -0
- package/site/content/docs/recipes/openbao.mdx +141 -0
- package/site/content/docs/recipes/pgdog.mdx +137 -0
- package/site/content/docs/recipes/postgres.mdx +136 -0
- package/site/content/docs/recipes/redis.mdx +130 -0
- package/site/content/docs/recipes/rustfs.mdx +148 -0
- package/site/content/docs/recipes/sglang.mdx +105 -0
- package/site/content/docs/recipes/supabase-docker.mdx +145 -0
- package/site/content/docs/recipes/timescale.mdx +129 -0
- package/site/content/docs/recipes/traefik.mdx +136 -0
- package/site/content/docs/recipes/valkey.mdx +121 -0
- package/site/content/docs/recipes/vllm.mdx +106 -0
- package/site/content/docs/recipes/yugabytedb.mdx +135 -0
- package/site/content/docs/reference/cli.md +42 -6
- package/site/content/docs/reference/configuration.mdx +5 -3
- package/site/content/docs/reference/environment-variables.mdx +12 -12
- package/site/content/docs/reference/fx.mdx +38 -4
- package/src/cli/ai-setup/ai-setup.test.ts +32 -12
- package/src/cli/ai-setup/apply.ts +65 -23
- package/src/cli/ai-setup/catalog.ts +1316 -35
- package/src/cli/ai-setup/detect-ollama.ts +47 -0
- package/src/cli/ai-setup/index.ts +44 -7
- package/src/cli/ai-setup/prompts.ts +430 -486
- package/src/cli/ai-setup/recommend.test.ts +150 -50
- package/src/cli/ai-setup/recommend.ts +118 -101
- package/src/cli/ask-seed.test.ts +68 -0
- package/src/cli/ask-seed.ts +79 -0
- package/src/cli/db-seed-redact.test.ts +25 -0
- package/src/cli/db-seed.ts +36 -7
- package/src/cli/db.test.ts +1 -1
- package/src/cli/db.ts +2 -2
- package/src/cli/dev-controls.test.ts +107 -0
- package/src/cli/dev-controls.ts +354 -0
- package/src/cli/dev-schema-sync.ts +12 -4
- package/src/cli/dev.test.ts +50 -30
- package/src/cli/dev.ts +607 -66
- package/src/cli/drizzle-env.test.ts +40 -6
- package/src/cli/drizzle-env.ts +22 -1
- package/src/cli/ensure-drizzle-config.ts +6 -4
- package/src/cli/hero-meta.test.ts +13 -1
- package/src/cli/hero-meta.ts +28 -6
- package/src/cli/index.ts +5 -0
- package/src/cli/registry.ts +24 -0
- package/src/cli/replay.test.ts +105 -0
- package/src/cli/replay.ts +206 -0
- package/src/compiler/extract.ts +3 -3
- package/src/config/index.ts +3 -3
- package/src/console/server/claim.ts +3 -1
- package/src/console/ui/dist/assets/{index-ClX0ZCe5.js → index-D-9KFce5.js} +2 -2
- package/src/console/ui/dist/assets/panel-overview-CW0CQEc2.js +1 -0
- package/src/console/ui/dist/assets/panel-runs-BxuyDI3x.js +1 -0
- package/src/console/ui/dist/assets/{panel-signals-Dsxdu_AR.js → panel-signals-BXofbj7v.js} +1 -1
- package/src/console/ui/dist/assets/{panel-store-hy7O8HOs.js → panel-store-B8mH-arU.js} +1 -1
- package/src/console/ui/dist/assets/{panel-traces-CBiAe5go.js → panel-traces-DSA-ZWnf.js} +1 -1
- package/src/console/ui/dist/index.html +1 -1
- package/src/console/ui/overview/slo.ts +31 -3
- package/src/console/ui/overview/types.ts +6 -0
- package/src/console/ui/runs/errors.test.ts +53 -0
- package/src/console/ui/runs/errors.ts +88 -0
- package/src/console/ui/runs/index.ts +8 -0
- package/src/console/ui/runs/search.ts +16 -0
- package/src/console/ui/shell/panels/runs/RunsPanel.tsx +72 -1
- package/src/docker/ai-model-status.test.ts +101 -0
- package/src/docker/ai-model-status.ts +276 -0
- package/src/docker/compose-health.test.ts +115 -0
- package/src/docker/compose-health.ts +234 -0
- package/src/docker/compose.ts +24 -8
- package/src/docker/derive.ts +35 -9
- package/src/docker/docker.test.ts +304 -12
- package/src/docker/helpers.ts +56 -0
- package/src/docker/index.ts +52 -0
- package/src/docker/ollama-pull.ts +232 -0
- package/src/docker/recipes/cockroach.ts +24 -0
- package/src/docker/recipes/index.ts +32 -0
- package/src/docker/recipes/llama-cpp.ts +273 -0
- package/src/docker/recipes/ollama.ts +19 -23
- package/src/docker/recipes/pgdog.ts +4 -1
- package/src/docker/recipes/postgres.ts +5 -18
- package/src/docker/recipes/sglang.ts +55 -0
- package/src/docker/recipes/supabase.ts +21 -0
- package/src/docker/recipes/timescale.ts +18 -0
- package/src/docker/recipes/vllm.ts +44 -0
- package/src/docker/recipes/yugabyte.ts +23 -0
- package/src/docker/stack-id.test.ts +22 -0
- package/src/docker/stack-id.ts +38 -4
- package/src/docker/types.ts +15 -0
- package/src/drivers/ai-ollama.integration.test.ts +7 -4
- package/src/drivers/signal-engine.ts +10 -0
- package/src/drivers/signal-postgres.ts +24 -2
- package/src/drivers/signal-types.ts +10 -0
- package/src/elements/channel/fallback.test.ts +91 -0
- package/src/elements/channel/hard-bounce.test.ts +82 -0
- package/src/elements/channel/injection.test.ts +153 -0
- package/src/elements/channel/locale-ar.test.ts +102 -0
- package/src/elements/channel/otp-delivery.ts +5 -2
- package/src/elements/channel/receipts-ledger.test.ts +100 -0
- package/src/elements/channel/runtime.ts +24 -1
- package/src/elements/channel/test-helpers.ts +60 -0
- package/src/elements/channel/whatsapp-compliance.test.ts +124 -0
- package/src/elements/store/cache.ts +1 -1
- package/src/elements/store/emit-drizzle.ts +43 -9
- package/src/elements/store/runtime.ts +1 -1
- package/src/elements/store/schema-decl.test.ts +2 -0
- package/src/elements/store/schema-decl.ts +15 -0
- package/src/elements/store/seed.ts +1 -1
- package/src/index.ts +1 -0
- package/src/kernel/app.ts +117 -10
- package/src/kernel/boot-bind/signal.ts +6 -2
- package/src/kernel/boot-bind/store.ts +2 -1
- package/src/kernel/boot.test.ts +6 -5
- package/src/kernel/boot.ts +8 -3
- package/src/kernel/compensate.test.ts +131 -0
- package/src/kernel/correlation.test.ts +108 -0
- package/src/kernel/effect-timing.test.ts +82 -0
- package/src/kernel/flow.ts +38 -0
- package/src/kernel/fx-runs.test.ts +49 -0
- package/src/kernel/fx.test.ts +14 -0
- package/src/kernel/fx.ts +111 -1
- package/src/manifest/types.ts +8 -2
- package/src/runs/collect.ts +3 -0
- package/src/runs/export-otlp.ts +64 -0
- package/src/runs/index.ts +16 -0
- package/src/runs/parquet.ts +9 -0
- package/src/runs/types.ts +5 -0
- package/src/runs/window.test.ts +68 -0
- package/src/runs/window.ts +148 -0
- package/src/runtime/boot-warn.test.ts +45 -0
- package/src/runtime/boot-warn.ts +17 -0
- package/src/runtime/dev-request-log.ts +5 -5
- package/src/term.test.ts +95 -10
- package/src/term.ts +369 -14
- package/src/console/ui/dist/assets/panel-overview-DONDzBd2.js +0 -1
- package/src/console/ui/dist/assets/panel-runs-D5zf-D9c.js +0 -1
package/src/cli/db-seed.ts
CHANGED
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
} from "../elements/store/load-plugin-tables.ts";
|
|
20
20
|
import { createFx, type Fx } from "../kernel/fx.ts";
|
|
21
21
|
import type { OkeApp } from "../kernel/app.ts";
|
|
22
|
-
import { resolveDrizzleKitEnv } from "./drizzle-env.ts";
|
|
22
|
+
import { applyComposeEnvToProcess, resolveDrizzleKitEnv } from "./drizzle-env.ts";
|
|
23
23
|
import { EXIT_OK, EXIT_RUNTIME, EXIT_USAGE } from "./exit.ts";
|
|
24
24
|
import { loadOkeConfig } from "./load-config.ts";
|
|
25
25
|
import { resolveDevSqlEnv } from "./resolve-dev-sql-env.ts";
|
|
@@ -38,7 +38,7 @@ export interface SeedOptions {
|
|
|
38
38
|
readonly env?: ConfigEnv;
|
|
39
39
|
/** Skip interactive confirm (`--force`). */
|
|
40
40
|
readonly force?: boolean;
|
|
41
|
-
/** Override seed module path (default `src/seed/index.ts`). */
|
|
41
|
+
/** Override seed module path (default `src/db/seed/index.ts`). */
|
|
42
42
|
readonly seedPath?: string;
|
|
43
43
|
/** Override app entry for boot. */
|
|
44
44
|
readonly entry?: string;
|
|
@@ -77,13 +77,17 @@ export function isSeedDef(value: unknown): value is SeedDef {
|
|
|
77
77
|
/**
|
|
78
78
|
* Redact password from a connection URL for display.
|
|
79
79
|
*
|
|
80
|
+
* Builds the string by hand — assigning unicode bullets to `URL.password`
|
|
81
|
+
* then calling `toString()` percent-encodes them (`%E2%80%A2`).
|
|
82
|
+
*
|
|
80
83
|
* @param raw - Connection URL or file path
|
|
81
84
|
*/
|
|
82
85
|
export function redactConnectionTarget(raw: string): string {
|
|
83
86
|
try {
|
|
84
87
|
const u = new URL(raw);
|
|
85
|
-
if (u.password) u.
|
|
86
|
-
|
|
88
|
+
if (!u.password) return u.toString();
|
|
89
|
+
const auth = u.username ? `${u.username}:••••@` : "••••@";
|
|
90
|
+
return `${u.protocol}//${auth}${u.host}${u.pathname}${u.search}${u.hash}`;
|
|
87
91
|
} catch {
|
|
88
92
|
return raw;
|
|
89
93
|
}
|
|
@@ -158,14 +162,35 @@ export async function executeSeedDef(
|
|
|
158
162
|
}
|
|
159
163
|
}
|
|
160
164
|
|
|
165
|
+
/** Default seed module (create-oke `src/db` layout). */
|
|
166
|
+
export const DEFAULT_SEED_REL = "src/db/seed/index.ts";
|
|
167
|
+
|
|
168
|
+
/** Legacy seed module path. */
|
|
169
|
+
export const LEGACY_SEED_REL = "src/seed/index.ts";
|
|
170
|
+
|
|
161
171
|
/**
|
|
162
|
-
*
|
|
172
|
+
* Resolve the seed module path (explicit → new default → legacy).
|
|
173
|
+
*
|
|
174
|
+
* @param cwd - Project root
|
|
175
|
+
* @param seedPath - Optional override
|
|
176
|
+
*/
|
|
177
|
+
export async function resolveSeedModulePath(cwd: string, seedPath?: string): Promise<string> {
|
|
178
|
+
if (seedPath) return resolve(cwd, seedPath);
|
|
179
|
+
for (const rel of [DEFAULT_SEED_REL, LEGACY_SEED_REL]) {
|
|
180
|
+
const abs = resolve(cwd, rel);
|
|
181
|
+
if (await Bun.file(abs).exists()) return abs;
|
|
182
|
+
}
|
|
183
|
+
return resolve(cwd, DEFAULT_SEED_REL);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Load `src/db/seed/index.ts` (or legacy / override) and return its SeedDef.
|
|
163
188
|
*
|
|
164
189
|
* @param cwd - Project root
|
|
165
190
|
* @param seedPath - Relative or absolute path
|
|
166
191
|
*/
|
|
167
192
|
export async function loadSeedDef(cwd: string, seedPath?: string): Promise<SeedDef> {
|
|
168
|
-
const abs =
|
|
193
|
+
const abs = await resolveSeedModulePath(cwd, seedPath);
|
|
169
194
|
if (!(await Bun.file(abs).exists())) {
|
|
170
195
|
throw new Error(`oke db seed: no seed module at ${abs}`);
|
|
171
196
|
}
|
|
@@ -193,6 +218,10 @@ export async function bootSeedFx(
|
|
|
193
218
|
entry?: string,
|
|
194
219
|
config?: OkeConfig | null,
|
|
195
220
|
): Promise<{ readonly fx: Fx; readonly stop: () => Promise<void> }> {
|
|
221
|
+
if (env === "docker") {
|
|
222
|
+
await applyComposeEnvToProcess(cwd);
|
|
223
|
+
if (process.env.OKE_DOCKER === undefined) process.env.OKE_DOCKER = "1";
|
|
224
|
+
}
|
|
196
225
|
const entryAbs = await resolveAppEntryForPluginTables(cwd, entry ?? config?.db?.entry);
|
|
197
226
|
if (!entryAbs) {
|
|
198
227
|
throw new Error(
|
|
@@ -208,7 +237,7 @@ export async function bootSeedFx(
|
|
|
208
237
|
const storeRuntime = app.elements?.store ?? app.bootResult?.store;
|
|
209
238
|
if (!storeRuntime) {
|
|
210
239
|
throw new Error(
|
|
211
|
-
"oke db seed: app booted without a store runtime — ensure stores are registered on the app (e.g.
|
|
240
|
+
"oke db seed: app booted without a store runtime — ensure stores are registered on the app (e.g. oke({ name, stores: [db] }))",
|
|
212
241
|
);
|
|
213
242
|
}
|
|
214
243
|
const fx = createFx({
|
package/src/cli/db.test.ts
CHANGED
|
@@ -242,7 +242,7 @@ export const app = oke({ name: "plugin-only" }).plug(
|
|
|
242
242
|
const emitted = await emitAbstractSchemaPrestep(dir, loaded.config, () => {}, "local");
|
|
243
243
|
expect(emitted).toBe(true);
|
|
244
244
|
|
|
245
|
-
const generated = await readFile(join(dir, "src", "schema.generated.ts"), "utf8");
|
|
245
|
+
const generated = await readFile(join(dir, "src", "db", "schema.generated.ts"), "utf8");
|
|
246
246
|
expect(generated).toContain('sqliteTable("metrics_daily"');
|
|
247
247
|
expect(generated).toContain('integer("hits")');
|
|
248
248
|
});
|
package/src/cli/db.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* plus explicit seed runs.
|
|
4
4
|
*
|
|
5
5
|
* Distinct from `oke schema generate` (core/plugin stub tables).
|
|
6
|
-
* When abstract decls exist (`src/schema.decl.ts`), emits Drizzle first —
|
|
6
|
+
* When abstract decls exist (`src/db/schema.decl.ts`), emits Drizzle first —
|
|
7
7
|
* not a third schema CLI.
|
|
8
8
|
*/
|
|
9
9
|
|
|
@@ -448,7 +448,7 @@ export async function dbCli(args: readonly string[]): Promise<number> {
|
|
|
448
448
|
console.log(`oke db push|generate|migrate|seed [--config|-c] [--env name] [--force]
|
|
449
449
|
|
|
450
450
|
Domain schema sync via drizzle-kit, plus explicit seed.
|
|
451
|
-
When src/schema.decl.ts and/or a plugged app entry exists, emits
|
|
451
|
+
When src/db/schema.decl.ts and/or a plugged app entry exists, emits
|
|
452
452
|
schema.generated.ts from store.schema.table + live plugin .table()
|
|
453
453
|
contributions — then runs drizzle-kit.
|
|
454
454
|
Hand-written src/schema.ts remains supported (emit skipped if nothing to emit).
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
controlServicesFromStack,
|
|
4
|
+
createDevControlDispatcher,
|
|
5
|
+
formatDevControlsHelp,
|
|
6
|
+
formatDevControlsHint,
|
|
7
|
+
formatDevControlsServiceList,
|
|
8
|
+
parseDevControlKey,
|
|
9
|
+
} from "./dev-controls.ts";
|
|
10
|
+
|
|
11
|
+
describe("dev-controls", () => {
|
|
12
|
+
test("parseDevControlKey maps raw keys", () => {
|
|
13
|
+
expect(parseDevControlKey("q")).toBe("q");
|
|
14
|
+
expect(parseDevControlKey("\u0003")).toBe("q");
|
|
15
|
+
expect(parseDevControlKey("?")).toBe("?");
|
|
16
|
+
expect(parseDevControlKey("h")).toBe("?");
|
|
17
|
+
expect(parseDevControlKey("c")).toBe("c");
|
|
18
|
+
expect(parseDevControlKey("l")).toBe("l");
|
|
19
|
+
expect(parseDevControlKey("u")).toBe("u");
|
|
20
|
+
expect(parseDevControlKey("x")).toBe("x");
|
|
21
|
+
expect(parseDevControlKey("r")).toBe("r");
|
|
22
|
+
expect(parseDevControlKey("3")).toBe("3");
|
|
23
|
+
expect(parseDevControlKey("\u001b")).toBe("esc");
|
|
24
|
+
expect(parseDevControlKey("z")).toBeNull();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("format hint and help are scannable", () => {
|
|
28
|
+
expect(formatDevControlsHint(false)).toContain("keys");
|
|
29
|
+
expect(formatDevControlsHint(false)).toContain("refresh");
|
|
30
|
+
expect(formatDevControlsHint(false)).toContain("quit");
|
|
31
|
+
expect(formatDevControlsHelp(false)).toContain("select service");
|
|
32
|
+
expect(formatDevControlsHelp(false)).toContain("refresh");
|
|
33
|
+
expect(formatDevControlsHelp(false)).not.toMatch(/\u001b\[/);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("controlServicesFromStack dedupes by serviceName", () => {
|
|
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 () => {
|
|
63
|
+
const calls: string[] = [];
|
|
64
|
+
let quit = false;
|
|
65
|
+
let settled = 0;
|
|
66
|
+
let refreshed = 0;
|
|
67
|
+
let panels = 0;
|
|
68
|
+
const d = createDevControlDispatcher({
|
|
69
|
+
write: () => {},
|
|
70
|
+
onQuit: () => {
|
|
71
|
+
quit = true;
|
|
72
|
+
},
|
|
73
|
+
onRefresh: () => {
|
|
74
|
+
refreshed += 1;
|
|
75
|
+
},
|
|
76
|
+
onShowPanel: () => {
|
|
77
|
+
panels += 1;
|
|
78
|
+
},
|
|
79
|
+
onComposeSettled: () => {
|
|
80
|
+
settled += 1;
|
|
81
|
+
},
|
|
82
|
+
services: () => [
|
|
83
|
+
{ label: "ai", serviceName: "ai" },
|
|
84
|
+
{ label: "postgres", serviceName: "store-sql" },
|
|
85
|
+
],
|
|
86
|
+
composeAction: async (action, names) => {
|
|
87
|
+
calls.push(`${action}:${names.join(",") || "*"}`);
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
d.handleKey("l");
|
|
91
|
+
await Bun.sleep(10);
|
|
92
|
+
d.handleKey("1");
|
|
93
|
+
d.handleKey("x");
|
|
94
|
+
await Bun.sleep(10);
|
|
95
|
+
d.handleKey("u");
|
|
96
|
+
await Bun.sleep(10);
|
|
97
|
+
d.handleKey("c");
|
|
98
|
+
await Bun.sleep(10);
|
|
99
|
+
d.handleKey("q");
|
|
100
|
+
expect(calls).toEqual(["stop:ai", "up:*"]);
|
|
101
|
+
expect(settled).toBe(2);
|
|
102
|
+
expect(refreshed).toBe(1);
|
|
103
|
+
expect(panels).toBe(1);
|
|
104
|
+
expect(quit).toBe(true);
|
|
105
|
+
d.stop();
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keyboard controls for a live `oke dev` session (TTY raw mode).
|
|
3
|
+
*
|
|
4
|
+
* Keys: `?` help · `q` quit · `c` refresh · `l` list · `u` up all ·
|
|
5
|
+
* `x` stop all · `1`–`9` select service then `u`/`x`/`r`.
|
|
6
|
+
*
|
|
7
|
+
* Compose health / model phase update the status board above Logs — not
|
|
8
|
+
* the log stream. `c` clears the TTY and reprints the latest board.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
formatStatusDot,
|
|
13
|
+
formatStatusLine,
|
|
14
|
+
termColorEnabled,
|
|
15
|
+
termStyle,
|
|
16
|
+
type DevStatus,
|
|
17
|
+
} from "../term.ts";
|
|
18
|
+
|
|
19
|
+
/** One Docker service row addressable by a digit key. */
|
|
20
|
+
export type DevControlService = {
|
|
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";
|
|
29
|
+
|
|
30
|
+
/** Options for {@link startDevControls}. */
|
|
31
|
+
export type StartDevControlsOptions = {
|
|
32
|
+
readonly write: (text: string) => void;
|
|
33
|
+
/** Called on `q` / Ctrl+C. */
|
|
34
|
+
readonly onQuit: () => void;
|
|
35
|
+
/**
|
|
36
|
+
* Clear the TTY and reprint chrome + latest status board (drops log noise).
|
|
37
|
+
*/
|
|
38
|
+
readonly onRefresh?: () => void | Promise<void>;
|
|
39
|
+
/**
|
|
40
|
+
* Show help / services in a clean pane (typically refresh chrome first so
|
|
41
|
+
* the panel is not interleaved with request logs).
|
|
42
|
+
*
|
|
43
|
+
* @param body - Formatted panel text
|
|
44
|
+
*/
|
|
45
|
+
readonly onShowPanel?: (body: string) => void | Promise<void>;
|
|
46
|
+
/**
|
|
47
|
+
* After a compose up/stop/restart — sync health into the board (no log lines).
|
|
48
|
+
*/
|
|
49
|
+
readonly onComposeSettled?: () => void | Promise<void>;
|
|
50
|
+
/** Live service catalogue (unique compose services). */
|
|
51
|
+
readonly services: () => readonly DevControlService[];
|
|
52
|
+
/**
|
|
53
|
+
* Run a compose control action.
|
|
54
|
+
*
|
|
55
|
+
* @param action - up / stop / restart
|
|
56
|
+
* @param serviceNames - Empty → whole stack
|
|
57
|
+
*/
|
|
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;
|
|
64
|
+
readonly stdin?: NodeJS.ReadStream;
|
|
65
|
+
readonly isTTY?: boolean;
|
|
66
|
+
readonly color?: boolean;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/** Handle returned by {@link startDevControls}. */
|
|
70
|
+
export type DevControlsHandle = {
|
|
71
|
+
readonly stop: () => void;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* One-line keys hint under Logs.
|
|
76
|
+
*
|
|
77
|
+
* @param color - Color on/off
|
|
78
|
+
*/
|
|
79
|
+
export function formatDevControlsHint(color: boolean = termColorEnabled()): string {
|
|
80
|
+
const s = termStyle(color);
|
|
81
|
+
return (
|
|
82
|
+
`${s.dim}│${s.reset} ${s.dim}keys${s.reset} ` +
|
|
83
|
+
`${s.cyan}?${s.reset} help · ` +
|
|
84
|
+
`${s.cyan}c${s.reset} refresh · ` +
|
|
85
|
+
`${s.cyan}q${s.reset} quit · ` +
|
|
86
|
+
`${s.cyan}l${s.reset} services · ` +
|
|
87
|
+
`${s.cyan}u${s.reset} up · ` +
|
|
88
|
+
`${s.cyan}x${s.reset} stop` +
|
|
89
|
+
`\n`
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Full help block for `?` / `h`.
|
|
95
|
+
*
|
|
96
|
+
* @param color - Color on/off
|
|
97
|
+
*/
|
|
98
|
+
export function formatDevControlsHelp(color: boolean = termColorEnabled()): string {
|
|
99
|
+
const s = termStyle(color);
|
|
100
|
+
const bar = `${s.dim}│${s.reset}`;
|
|
101
|
+
const k = (key: string, meaning: string) =>
|
|
102
|
+
`${bar} ${s.cyan}${key.padEnd(6)}${s.reset} ${s.dim}${meaning}${s.reset}`;
|
|
103
|
+
return [
|
|
104
|
+
"",
|
|
105
|
+
`${s.green}◇${s.reset} ${s.bold}Keys${s.reset}`,
|
|
106
|
+
k("?", "help"),
|
|
107
|
+
k("c", "refresh — clear logs, show latest ●"),
|
|
108
|
+
k("q", "quit oke dev"),
|
|
109
|
+
k("l", "list docker services"),
|
|
110
|
+
k("u", "compose up -d (all)"),
|
|
111
|
+
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
|
+
bar,
|
|
118
|
+
"",
|
|
119
|
+
].join("\n");
|
|
120
|
+
}
|
|
121
|
+
|
|
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
|
+
/**
|
|
165
|
+
* Parse one raw stdin chunk into a control key (tests / non-TTY inject).
|
|
166
|
+
*
|
|
167
|
+
* @param chunk - Raw bytes/string
|
|
168
|
+
*/
|
|
169
|
+
export function parseDevControlKey(chunk: string): string | null {
|
|
170
|
+
if (chunk.length === 0) return null;
|
|
171
|
+
if (chunk === "\u0003") return "q"; // Ctrl+C
|
|
172
|
+
if (chunk === "\u001b") return "esc";
|
|
173
|
+
const ch = chunk[0]!;
|
|
174
|
+
if (ch === "?" || ch === "h" || ch === "H") return "?";
|
|
175
|
+
if (ch === "c" || ch === "C") return "c";
|
|
176
|
+
if (ch === "q" || ch === "Q") return "q";
|
|
177
|
+
if (ch === "l" || ch === "L") return "l";
|
|
178
|
+
if (ch === "u" || ch === "U") return "u";
|
|
179
|
+
if (ch === "x" || ch === "X") return "x";
|
|
180
|
+
if (ch === "r" || ch === "R") return "r";
|
|
181
|
+
if (ch >= "1" && ch <= "9") return ch;
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Key dispatcher for tests and {@link startDevControls}. */
|
|
186
|
+
export type DevControlDispatcher = {
|
|
187
|
+
readonly handleKey: (key: string) => void;
|
|
188
|
+
readonly stop: () => void;
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Pure key dispatcher (no stdin) — used by tests and the TTY listener.
|
|
193
|
+
*
|
|
194
|
+
* @param opts - Quit / compose / services
|
|
195
|
+
*/
|
|
196
|
+
export function createDevControlDispatcher(opts: StartDevControlsOptions): DevControlDispatcher {
|
|
197
|
+
const color = opts.color ?? termColorEnabled();
|
|
198
|
+
let selected: number | null = null;
|
|
199
|
+
let busy = false;
|
|
200
|
+
let stopped = false;
|
|
201
|
+
|
|
202
|
+
const runAction = async (
|
|
203
|
+
action: DevComposeControlAction,
|
|
204
|
+
serviceNames: readonly string[],
|
|
205
|
+
_label: string,
|
|
206
|
+
): Promise<void> => {
|
|
207
|
+
if (busy) return;
|
|
208
|
+
busy = true;
|
|
209
|
+
try {
|
|
210
|
+
await opts.composeAction(action, serviceNames);
|
|
211
|
+
await opts.onComposeSettled?.();
|
|
212
|
+
} catch (err) {
|
|
213
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
214
|
+
opts.write(formatStatusLine(`${action} failed — ${msg}`, color, "error"));
|
|
215
|
+
} finally {
|
|
216
|
+
busy = false;
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
handleKey(key: string): void {
|
|
222
|
+
if (stopped) return;
|
|
223
|
+
if (key === "q") {
|
|
224
|
+
opts.onQuit();
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (key === "c") {
|
|
228
|
+
void Promise.resolve(opts.onRefresh?.()).catch((err: unknown) => {
|
|
229
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
230
|
+
opts.write(formatStatusLine(`refresh failed — ${msg}`, color, "error"));
|
|
231
|
+
});
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (key === "?") {
|
|
235
|
+
const body = formatDevControlsHelp(color);
|
|
236
|
+
void Promise.resolve(opts.onShowPanel?.(body) ?? opts.write(body));
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (key === "esc") {
|
|
240
|
+
selected = null;
|
|
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");
|
|
291
|
+
}
|
|
292
|
+
},
|
|
293
|
+
stop() {
|
|
294
|
+
stopped = true;
|
|
295
|
+
},
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Start raw-mode keyboard controls. No-op when stdin is not a TTY.
|
|
301
|
+
*
|
|
302
|
+
* @param opts - Quit / compose / services
|
|
303
|
+
*/
|
|
304
|
+
export function startDevControls(opts: StartDevControlsOptions): DevControlsHandle {
|
|
305
|
+
const stdin = opts.stdin ?? process.stdin;
|
|
306
|
+
const isTTY = opts.isTTY ?? stdin.isTTY === true;
|
|
307
|
+
const dispatcher = createDevControlDispatcher(opts);
|
|
308
|
+
|
|
309
|
+
if (!isTTY || typeof stdin.setRawMode !== "function") {
|
|
310
|
+
return { stop: () => dispatcher.stop() };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const onData = (buf: string | Buffer): void => {
|
|
314
|
+
const chunk = typeof buf === "string" ? buf : buf.toString("utf8");
|
|
315
|
+
const key = parseDevControlKey(chunk);
|
|
316
|
+
if (key) dispatcher.handleKey(key);
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
stdin.setRawMode(true);
|
|
320
|
+
stdin.resume();
|
|
321
|
+
stdin.setEncoding("utf8");
|
|
322
|
+
stdin.on("data", onData);
|
|
323
|
+
|
|
324
|
+
return {
|
|
325
|
+
stop() {
|
|
326
|
+
dispatcher.stop();
|
|
327
|
+
stdin.off("data", onData);
|
|
328
|
+
try {
|
|
329
|
+
if (typeof stdin.setRawMode === "function") stdin.setRawMode(false);
|
|
330
|
+
} catch {
|
|
331
|
+
// already closed
|
|
332
|
+
}
|
|
333
|
+
},
|
|
334
|
+
};
|
|
335
|
+
}
|
|
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
|
+
}
|
|
@@ -30,6 +30,11 @@ export interface DevSchemaSyncOptions {
|
|
|
30
30
|
readonly pushFn?: (cwd: string, env: ConfigEnv) => Promise<number>;
|
|
31
31
|
/** Derive docker infra without running compose (tests). */
|
|
32
32
|
readonly dryRun?: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Skip the “compose env ready” line — `oke dev -d` already brought the
|
|
35
|
+
* stack up and prints {@link import("../term.ts").formatStackSummary}.
|
|
36
|
+
*/
|
|
37
|
+
readonly quietComposeReady?: boolean;
|
|
33
38
|
}
|
|
34
39
|
|
|
35
40
|
/** Outcome of {@link syncDevSchema}. */
|
|
@@ -71,6 +76,7 @@ async function ensureDockerStack(
|
|
|
71
76
|
cwd: string,
|
|
72
77
|
config: OkeConfig,
|
|
73
78
|
write: (text: string) => void,
|
|
79
|
+
quietComposeReady = false,
|
|
74
80
|
): Promise<void> {
|
|
75
81
|
const images = resolveImages(config);
|
|
76
82
|
if (Object.keys(images).length === 0) {
|
|
@@ -95,9 +101,11 @@ async function ensureDockerStack(
|
|
|
95
101
|
host: "127.0.0.1",
|
|
96
102
|
});
|
|
97
103
|
await writeDerivedFiles(derived, resolve(cwd, DEFAULT_DOCKER_DIR), { writeStackEnv: true });
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
104
|
+
if (!quietComposeReady) {
|
|
105
|
+
write(
|
|
106
|
+
"oke: docker compose env ready under docker/.env.docker (start containers with `oke dev -d` or docker compose up)\n",
|
|
107
|
+
);
|
|
108
|
+
}
|
|
101
109
|
}
|
|
102
110
|
|
|
103
111
|
/**
|
|
@@ -126,7 +134,7 @@ export async function syncDevSchema(
|
|
|
126
134
|
if (!config) {
|
|
127
135
|
throw new Error("docker mode: oke.config.ts not found");
|
|
128
136
|
}
|
|
129
|
-
await ensureDockerStack(cwd, config, write);
|
|
137
|
+
await ensureDockerStack(cwd, config, write, options.quietComposeReady === true);
|
|
130
138
|
}
|
|
131
139
|
|
|
132
140
|
const push =
|