okengine 0.2.1 → 0.2.3
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 +8 -8
- package/package.json +2 -1
- package/src/cli/dev-app-runner.ts +6 -2
- package/src/cli/dev.ts +84 -15
- package/src/cli/doctor.ts +3 -18
- package/src/cli/index.ts +4 -1
- package/src/cli/json-out.test.ts +1 -1
- package/src/cli/load-config.images.test.ts +75 -0
- package/src/cli/load-config.ts +67 -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/console/server/app.ts +2 -1
- package/src/console/server/claim.test.ts +7 -4
- package/src/console/server/claim.ts +2 -7
- package/src/console/server/flows.ts +1 -0
- package/src/console/server/index.ts +5 -0
- package/src/console/server/operator-db.test.ts +75 -0
- package/src/console/server/operator-db.ts +324 -0
- package/src/console/server/serve.ts +39 -0
- package/src/console/server/state.ts +13 -1
- package/src/console/ui/dist/assets/index-Bnf_3Hei.js +10 -0
- package/src/console/ui/dist/assets/panel-access-Dd37LU2c.js +64 -0
- package/src/console/ui/dist/assets/panel-ai-CC7LR6-J.js +1 -0
- package/src/console/ui/dist/assets/panel-architecture-B5b3iKCz.js +1 -0
- package/src/console/ui/dist/assets/panel-channels-CeNjTKXp.js +1 -0
- package/src/console/ui/dist/assets/panel-clock-DgFTLoHV.js +1 -0
- package/src/console/ui/dist/assets/panel-diff-DxehccqB.js +1 -0
- package/src/console/ui/dist/assets/panel-flows-BtrVn-Eg.js +45 -0
- package/src/console/ui/dist/assets/panel-gates-Z9MKRGdH.js +1 -0
- package/src/console/ui/dist/assets/panel-overview-Dt_AeXgd.js +1 -0
- package/src/console/ui/dist/assets/panel-plugins-DWd0TowH.js +1 -0
- package/src/console/ui/dist/assets/panel-runs-DGstFHeq.js +1 -0
- package/src/console/ui/dist/assets/panel-signals-DzEa2Fnt.js +1 -0
- package/src/console/ui/dist/assets/panel-store-BJkbNgxx.js +1 -0
- package/src/console/ui/dist/assets/panel-traces-BaRVO3gM.js +1 -0
- package/src/console/ui/dist/assets/panel-vault-BbfWdox0.js +1 -0
- package/src/console/ui/dist/assets/rolldown-runtime-CNC7AqOf.js +1 -0
- 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 +15 -0
- package/src/console/ui/shell/App.tsx +34 -2
- 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/derive.ts +3 -1
- package/src/docker/dockerfile.integration.test.ts +1 -1
- package/src/docker/stack.integration.test.ts +1 -1
- package/src/docs-origin.ts +19 -0
- package/src/kernel/errors.registry.test.ts +4 -2
- package/src/kernel/errors.ts +3 -1
- package/src/kernel/fx.test.ts +2 -2
- package/src/term.test.ts +51 -0
- package/src/term.ts +158 -0
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
[](https://opensource.org/licenses/MIT)
|
|
9
9
|
[](https://bun.sh)
|
|
10
10
|
|
|
11
|
-
**Framework:** [`okengine`](https://www.npmjs.com/package/okengine) · **CLI:** `oke` · **Scaffold:** [`create-oke`](https://www.npmjs.com/package/create-oke) · **Docs:** [
|
|
11
|
+
**Framework:** [`okengine`](https://www.npmjs.com/package/okengine) · **CLI:** `oke` · **Scaffold:** [`create-oke`](https://www.npmjs.com/package/create-oke) · **Docs:** [okengine.vercel.app](https://okengine.vercel.app) · **License:** MIT
|
|
12
12
|
|
|
13
13
|
### Install
|
|
14
14
|
|
|
@@ -35,7 +35,7 @@ Eight elements cover what a backend needs. Ten exports are the whole public voca
|
|
|
35
35
|
import { on, flow, signal, store, clock, gate, vault, channel, ai, plugin } from "okengine";
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
Batteries included for the Bun era: contract-first APIs, typed client, infrastructure primitives, Console, auth — pure TypeScript, MIT, self-hostable, no cloud lock-in. Handbook: [
|
|
38
|
+
Batteries included for the Bun era: contract-first APIs, typed client, infrastructure primitives, Console, auth — pure TypeScript, MIT, self-hostable, no cloud lock-in. Handbook: [handbook](https://okengine.vercel.app/docs). Spec depth: [`docs/spec/unified-theory.md`](https://github.com/omqkhafi/okengine/blob/main/docs/spec/unified-theory.md).
|
|
39
39
|
|
|
40
40
|
---
|
|
41
41
|
|
|
@@ -131,7 +131,7 @@ The rest of the scaffold is small and ordinary: `oke.config.ts` (drivers by prot
|
|
|
131
131
|
|
|
132
132
|
## How to learn (read in order)
|
|
133
133
|
|
|
134
|
-
Start on the site if you want the handbook path: [Introduction](https://
|
|
134
|
+
Start on the site if you want the handbook path: [Introduction](https://okengine.vercel.app/docs/get-started/introduction) → [Installation](https://okengine.vercel.app/docs/get-started/installation) → [Basic usage](https://okengine.vercel.app/docs/get-started/basic-usage).
|
|
135
135
|
|
|
136
136
|
Each teaching app adds the smallest next set of ideas. Do not skip ahead.
|
|
137
137
|
|
|
@@ -153,8 +153,8 @@ Same path in the spec: [`docs/spec/four-applications.md`](docs/spec/four-applica
|
|
|
153
153
|
|
|
154
154
|
## Documentation
|
|
155
155
|
|
|
156
|
-
- **Site** — [
|
|
157
|
-
- **Agents on the site** — [`/llms.txt`](https://
|
|
156
|
+
- **Site** — [okengine.vercel.app](https://okengine.vercel.app) (landing) · [handbook](https://okengine.vercel.app/docs) (handbook) · [changelog](https://okengine.vercel.app/changelog)
|
|
157
|
+
- **Agents on the site** — [`/llms.txt`](https://okengine.vercel.app/llms.txt) index · [`/llms-full.txt`](https://okengine.vercel.app/llms-full.txt) full text · `/llms.mdx/docs/...` per-page markdown
|
|
158
158
|
- **Docs MCP** — `:6536` · tools `oke.docs.search` / `oke.docs.get` (read-only; Host/Origin validated like the other surfaces)
|
|
159
159
|
- **App MCP** — `:6535` · live Manifest, schemas, effects, traces, Console-safe actions (Bearer + confirmation for writes)
|
|
160
160
|
- **Canonical specs in-repo** — [`docs/spec/`](docs/spec) (unified theory, four applications, Console)
|
|
@@ -204,7 +204,7 @@ flowchart TD
|
|
|
204
204
|
|
|
205
205
|
Seventeen panels on `:6533` (app `:6530`, MCP `:6535`): Overview, Flows, Signals, Store, Clock, Gates, Vault, Channels, AI, Architecture, Traces, Runs, Manifest Diff, Access, Plugins — plus Privacy and Tenancy when those plugins are plugged.
|
|
206
206
|
|
|
207
|
-
Operator and user planes stay separate. Every Console action is a real flow through `fx`, so the audit log is the trace. Spec: [`docs/spec/console.md`](docs/spec/console.md) · handbook: [
|
|
207
|
+
Operator and user planes stay separate. Every Console action is a real flow through `fx`, so the audit log is the trace. Spec: [`docs/spec/console.md`](docs/spec/console.md) · handbook: [console handbook](https://okengine.vercel.app/docs/console/overview).
|
|
208
208
|
|
|
209
209
|
---
|
|
210
210
|
|
|
@@ -348,11 +348,11 @@ Measured size and latency caps — see [`BUDGETS.md`](BUDGETS.md). Refresh with
|
|
|
348
348
|
|
|
349
349
|
## Status
|
|
350
350
|
|
|
351
|
-
Pre-1.0. Published on [npm](https://www.npmjs.com/package/okengine) and [JSR](https://jsr.io/@omqkhafi/okengine) (`okengine` + `create-oke`, lockstep). Docs at [
|
|
351
|
+
Pre-1.0. Published on [npm](https://www.npmjs.com/package/okengine) and [JSR](https://jsr.io/@omqkhafi/okengine) (`okengine` + `create-oke`, lockstep). Docs at [okengine.vercel.app](https://okengine.vercel.app). MIT. No `CONTRIBUTING` yet — issues and PRs welcome.
|
|
352
352
|
|
|
353
353
|
| Resource | Path |
|
|
354
354
|
|---|---|
|
|
355
|
-
| Docs site | [
|
|
355
|
+
| Docs site | [okengine.vercel.app](https://okengine.vercel.app) · [handbook](https://okengine.vercel.app/docs) · [changelog](https://okengine.vercel.app/changelog) |
|
|
356
356
|
| Unified theory | [`docs/spec/unified-theory.md`](https://github.com/omqkhafi/okengine/blob/main/docs/spec/unified-theory.md) |
|
|
357
357
|
| Four applications | [`docs/spec/four-applications.md`](https://github.com/omqkhafi/okengine/blob/main/docs/spec/four-applications.md) |
|
|
358
358
|
| Console | [`docs/spec/console.md`](https://github.com/omqkhafi/okengine/blob/main/docs/spec/console.md) |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "okengine",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "One law. Eight elements. Ten exports. One package. One manifest. Every backend need is derived, never added.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"test:mcp": "bun test src/mcp",
|
|
36
36
|
"test:console:smoke": "bunx playwright test tests/console/smoke.spec.ts",
|
|
37
37
|
"build:console": "bunx vite build --config src/console/ui/vite.config.ts",
|
|
38
|
+
"prepack": "bun run build:console",
|
|
38
39
|
"bench:cold-start": "bun test ./src/runtime/cold-start.bench.ts",
|
|
39
40
|
"bench:client": "bun test src/client/budget.test.ts",
|
|
40
41
|
"bench:console": "bun test src/console/budget.test.ts",
|
|
@@ -17,6 +17,7 @@ import { resolve } from "node:path";
|
|
|
17
17
|
import { pathToFileURL } from "node:url";
|
|
18
18
|
import { createBunRuntime } from "../runtime/bun.ts";
|
|
19
19
|
import { APP_PORT, type FetchApp } from "../runtime/types.ts";
|
|
20
|
+
import { formatAppReadyLine } from "../term.ts";
|
|
20
21
|
|
|
21
22
|
/** Stable Bun.serve id so `--hot` reuses the socket across soft reloads. */
|
|
22
23
|
export const DEV_APP_SERVE_ID = "oke-dev-app";
|
|
@@ -59,8 +60,11 @@ const handle = createBunRuntime().serve(mod.app, {
|
|
|
59
60
|
id: DEV_APP_SERVE_ID,
|
|
60
61
|
});
|
|
61
62
|
|
|
62
|
-
|
|
63
|
-
|
|
63
|
+
process.stdout.write(
|
|
64
|
+
formatAppReadyLine(
|
|
65
|
+
`http://${formatHostForUrl(hostname)}:${handle.port}`,
|
|
66
|
+
),
|
|
67
|
+
);
|
|
64
68
|
|
|
65
69
|
if (readyPath !== undefined && readyPath.length > 0) {
|
|
66
70
|
await Bun.write(readyPath, `${handle.port}\n`);
|
package/src/cli/dev.ts
CHANGED
|
@@ -23,9 +23,15 @@ import {
|
|
|
23
23
|
CONSOLE_PORT,
|
|
24
24
|
MCP_PORT,
|
|
25
25
|
} from "../runtime/types.ts";
|
|
26
|
+
import {
|
|
27
|
+
formatDevBanner,
|
|
28
|
+
formatServiceLine,
|
|
29
|
+
formatStatusLine,
|
|
30
|
+
} from "../term.ts";
|
|
26
31
|
import { clientAdd } from "./client-add.ts";
|
|
27
32
|
import { loadManifest, loadOkeConfig, resolveImages } from "./load-config.ts";
|
|
28
33
|
import { mcpContextFromConsole } from "./mcp-from-console.ts";
|
|
34
|
+
import { resolveDevPorts } from "./ports.ts";
|
|
29
35
|
|
|
30
36
|
/** Max wait for the `bun --hot` app child to bind a port. */
|
|
31
37
|
const APP_READY_TIMEOUT_MS = 30_000;
|
|
@@ -178,9 +184,27 @@ export interface DevResult {
|
|
|
178
184
|
export async function runDev(options: DevOptions = {}): Promise<DevResult> {
|
|
179
185
|
const write = options.write ?? ((t) => process.stdout.write(t));
|
|
180
186
|
const cwd = options.cwd ?? process.cwd();
|
|
181
|
-
const
|
|
182
|
-
const
|
|
183
|
-
const
|
|
187
|
+
const preferredApp = options.appPort ?? Number(Bun.env.PORT ?? APP_PORT);
|
|
188
|
+
const preferredConsole = options.consolePort ?? CONSOLE_PORT;
|
|
189
|
+
const preferredMcp = options.mcpPort ?? MCP_PORT;
|
|
190
|
+
// Explicit overrides (incl. `0` ephemeral) skip probing; otherwise +1 until free.
|
|
191
|
+
const ports =
|
|
192
|
+
options.appPort !== undefined ||
|
|
193
|
+
options.consolePort !== undefined ||
|
|
194
|
+
options.mcpPort !== undefined
|
|
195
|
+
? {
|
|
196
|
+
app: preferredApp,
|
|
197
|
+
console: preferredConsole,
|
|
198
|
+
mcp: preferredMcp,
|
|
199
|
+
}
|
|
200
|
+
: await resolveDevPorts({
|
|
201
|
+
app: preferredApp,
|
|
202
|
+
console: preferredConsole,
|
|
203
|
+
mcp: preferredMcp,
|
|
204
|
+
});
|
|
205
|
+
const appPort = ports.app;
|
|
206
|
+
const consolePort = ports.console;
|
|
207
|
+
const mcpPort = ports.mcp;
|
|
184
208
|
const keepAlive = options.keepAlive ?? true;
|
|
185
209
|
|
|
186
210
|
let entry = options.entry;
|
|
@@ -258,7 +282,7 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
|
|
|
258
282
|
}
|
|
259
283
|
});
|
|
260
284
|
await up(composeFiles, cwd);
|
|
261
|
-
write(`
|
|
285
|
+
write(formatStatusLine(`stack up (${stackRoles.join(", ")})`));
|
|
262
286
|
}
|
|
263
287
|
}
|
|
264
288
|
|
|
@@ -272,10 +296,7 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
|
|
|
272
296
|
stackEnv,
|
|
273
297
|
};
|
|
274
298
|
|
|
275
|
-
write(
|
|
276
|
-
`oke dev: app :${appPort} · Console :${consolePort} · MCP :${mcpPort}\n`,
|
|
277
|
-
);
|
|
278
|
-
write("oke dev: watching — client types regenerate on save\n");
|
|
299
|
+
write(formatDevBanner());
|
|
279
300
|
|
|
280
301
|
if (options.dryRun) return { code: 0, plan };
|
|
281
302
|
|
|
@@ -295,6 +316,20 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
|
|
|
295
316
|
? options.manifest
|
|
296
317
|
: await tryLoadProjectManifest(cwd);
|
|
297
318
|
|
|
319
|
+
async function refreshManifestInto(
|
|
320
|
+
state: ConsoleState | null,
|
|
321
|
+
): Promise<void> {
|
|
322
|
+
if (!state) return;
|
|
323
|
+
try {
|
|
324
|
+
const { extractManifest } = await import("../compiler/extract.ts");
|
|
325
|
+
const { feedManifest } = await import("../console/server/live.ts");
|
|
326
|
+
const next = await extractManifest({ rootDir: cwd });
|
|
327
|
+
feedManifest(state, next);
|
|
328
|
+
} catch {
|
|
329
|
+
// Source may be mid-edit — keep the last good Manifest.
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
298
333
|
const serveConsole =
|
|
299
334
|
options.serveConsole ??
|
|
300
335
|
(async (port) => {
|
|
@@ -312,7 +347,12 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
|
|
|
312
347
|
? { manifest: seedManifest }
|
|
313
348
|
: {}),
|
|
314
349
|
});
|
|
315
|
-
write(
|
|
350
|
+
write(
|
|
351
|
+
formatServiceLine(
|
|
352
|
+
"Console",
|
|
353
|
+
`http://127.0.0.1:${server.port}`,
|
|
354
|
+
),
|
|
355
|
+
);
|
|
316
356
|
return {
|
|
317
357
|
console: server.console,
|
|
318
358
|
port: server.port,
|
|
@@ -334,7 +374,7 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
|
|
|
334
374
|
context: mcpOptions.context,
|
|
335
375
|
now: mcpOptions.now,
|
|
336
376
|
});
|
|
337
|
-
write(
|
|
377
|
+
write(formatServiceLine("MCP", `http://127.0.0.1:${server.port}`));
|
|
338
378
|
return {
|
|
339
379
|
port: server.port,
|
|
340
380
|
url: server.url,
|
|
@@ -352,7 +392,7 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
|
|
|
352
392
|
url: appUrl,
|
|
353
393
|
out: resolve(cwd, "oke-client.d.ts"),
|
|
354
394
|
});
|
|
355
|
-
write("
|
|
395
|
+
write(formatStatusLine("regenerated oke-client.d.ts"));
|
|
356
396
|
} catch {
|
|
357
397
|
// App may not be ready yet — ignore until next save.
|
|
358
398
|
}
|
|
@@ -395,6 +435,10 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
|
|
|
395
435
|
{ recursive: true },
|
|
396
436
|
() => {
|
|
397
437
|
void regen(appUrl);
|
|
438
|
+
// Only live-extract when the host did not pin a Manifest (tests).
|
|
439
|
+
if (options.manifest === undefined) {
|
|
440
|
+
void refreshManifestInto(consoleState);
|
|
441
|
+
}
|
|
398
442
|
},
|
|
399
443
|
);
|
|
400
444
|
|
|
@@ -446,20 +490,45 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
|
|
|
446
490
|
}
|
|
447
491
|
|
|
448
492
|
/**
|
|
449
|
-
*
|
|
493
|
+
* True when a Manifest has no declared flows (scaffold / empty extract).
|
|
494
|
+
*
|
|
495
|
+
* @param manifest - Candidate
|
|
496
|
+
*/
|
|
497
|
+
function isSparseManifest(manifest: Manifest): boolean {
|
|
498
|
+
const flows = manifest.flows;
|
|
499
|
+
if (!flows || typeof flows !== "object") return true;
|
|
500
|
+
return Object.keys(flows).length === 0;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Load Manifest — prefer AoT extract from `src/` when it has flows;
|
|
505
|
+
* otherwise on-disk JSON snapshots; otherwise a sparse extract.
|
|
450
506
|
*
|
|
451
507
|
* @param cwd - Project root
|
|
452
508
|
*/
|
|
453
509
|
async function tryLoadProjectManifest(
|
|
454
510
|
cwd: string,
|
|
455
511
|
): Promise<Manifest | null | undefined> {
|
|
512
|
+
let extracted: Manifest | undefined;
|
|
513
|
+
try {
|
|
514
|
+
const { extractManifest } = await import("../compiler/extract.ts");
|
|
515
|
+
extracted = await extractManifest({ rootDir: cwd });
|
|
516
|
+
} catch {
|
|
517
|
+
// Fall through to JSON snapshots.
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
let fromDisk: Manifest | undefined;
|
|
456
521
|
for (const name of ["oke.manifest.json", "manifest.oke.json"]) {
|
|
457
522
|
const path = resolve(cwd, name);
|
|
458
523
|
if (await Bun.file(path).exists()) {
|
|
459
|
-
|
|
524
|
+
fromDisk = await loadManifest(path);
|
|
525
|
+
break;
|
|
460
526
|
}
|
|
461
527
|
}
|
|
462
|
-
|
|
528
|
+
|
|
529
|
+
if (extracted && !isSparseManifest(extracted)) return extracted;
|
|
530
|
+
if (fromDisk) return fromDisk;
|
|
531
|
+
return extracted;
|
|
463
532
|
}
|
|
464
533
|
|
|
465
534
|
/**
|
|
@@ -515,7 +584,7 @@ async function startAppHot(
|
|
|
515
584
|
proc,
|
|
516
585
|
APP_READY_TIMEOUT_MS,
|
|
517
586
|
);
|
|
518
|
-
// Child prints
|
|
587
|
+
// Child prints the App ready line (again on each soft reload).
|
|
519
588
|
const url = new URL(`http://${hostname}:${boundPort}/`);
|
|
520
589
|
return { stop, port: boundPort, url };
|
|
521
590
|
} catch (err) {
|
package/src/cli/doctor.ts
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
* `oke doctor` — verify secrets, ports, drivers, tenancy, schema drift.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { createServer } from "node:net";
|
|
6
5
|
import { resolve } from "node:path";
|
|
7
6
|
import type { Manifest } from "../manifest/types.ts";
|
|
8
7
|
import { APP_PORT, CONSOLE_PORT, MCP_PORT } from "../runtime/types.ts";
|
|
@@ -10,8 +9,11 @@ import { hasFlag, wantsJson } from "./args.ts";
|
|
|
10
9
|
import { checkManifestPiiAsks } from "./doctor-pii.ts";
|
|
11
10
|
import { EXIT_OK, EXIT_RUNTIME } from "./exit.ts";
|
|
12
11
|
import { loadManifest } from "./load-config.ts";
|
|
12
|
+
import { isPortInUse } from "./ports.ts";
|
|
13
13
|
import { schemaFingerprint, readSchemaFingerprint } from "./schema.ts";
|
|
14
14
|
|
|
15
|
+
export { isPortInUse } from "./ports.ts";
|
|
16
|
+
|
|
15
17
|
/** One doctor finding. */
|
|
16
18
|
export interface DoctorFinding {
|
|
17
19
|
readonly code:
|
|
@@ -204,20 +206,3 @@ Verify secrets, ports, schema drift, and PII→model egress before serving.
|
|
|
204
206
|
return code;
|
|
205
207
|
}
|
|
206
208
|
|
|
207
|
-
/**
|
|
208
|
-
* True when something is already listening on `port`.
|
|
209
|
-
*
|
|
210
|
-
* @param port - TCP port
|
|
211
|
-
*/
|
|
212
|
-
export function isPortInUse(port: number): Promise<boolean> {
|
|
213
|
-
return new Promise((resolvePromise) => {
|
|
214
|
-
const server = createServer();
|
|
215
|
-
server.once("error", (err: NodeJS.ErrnoException) => {
|
|
216
|
-
resolvePromise(err.code === "EADDRINUSE");
|
|
217
|
-
});
|
|
218
|
-
server.once("listening", () => {
|
|
219
|
-
server.close(() => resolvePromise(false));
|
|
220
|
-
});
|
|
221
|
-
server.listen(port, "127.0.0.1");
|
|
222
|
-
});
|
|
223
|
-
}
|
package/src/cli/index.ts
CHANGED
|
@@ -90,7 +90,10 @@ if (cmd === "completion") {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
if (cmd === undefined || cmd === "--help" || cmd === "-h" || cmd === "help") {
|
|
93
|
-
|
|
93
|
+
// Bare `oke` — commands only. Exit-code table only on explicit --help.
|
|
94
|
+
const help =
|
|
95
|
+
cmd === undefined ? formatOkeHelp() : `${formatOkeHelp()}${EXIT_CODE_HELP}`;
|
|
96
|
+
console.log(help);
|
|
94
97
|
process.exit(cmd ? EXIT_OK : EXIT_USAGE);
|
|
95
98
|
}
|
|
96
99
|
|
package/src/cli/json-out.test.ts
CHANGED
|
@@ -42,7 +42,7 @@ describe("oke --json stdout discipline", () => {
|
|
|
42
42
|
test("stack --json is valid JSON only on stdout", async () => {
|
|
43
43
|
let out = "";
|
|
44
44
|
const code = await runStackPreview({
|
|
45
|
-
images: { "store.sql": "postgres:
|
|
45
|
+
images: { "store.sql": "postgres:18-alpine" },
|
|
46
46
|
json: true,
|
|
47
47
|
write: (t) => {
|
|
48
48
|
out += t;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default image pins derived from prod drivers.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from "bun:test";
|
|
6
|
+
import {
|
|
7
|
+
defaultImagesFromConfig,
|
|
8
|
+
resolveImages,
|
|
9
|
+
} from "./load-config.ts";
|
|
10
|
+
|
|
11
|
+
describe("defaultImagesFromConfig", () => {
|
|
12
|
+
test("maps postgres + redis prod drivers", () => {
|
|
13
|
+
const images = defaultImagesFromConfig({
|
|
14
|
+
drivers: {
|
|
15
|
+
store: {
|
|
16
|
+
sql: { prod: "postgres" },
|
|
17
|
+
kv: { prod: "redis" },
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
expect(images).toEqual({
|
|
22
|
+
"store.sql": "postgres:18-alpine",
|
|
23
|
+
"store.kv": "redis:8-alpine",
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("prefers pgvector image when index uses pgvector", () => {
|
|
28
|
+
const images = defaultImagesFromConfig({
|
|
29
|
+
drivers: {
|
|
30
|
+
store: {
|
|
31
|
+
sql: { prod: "postgres" },
|
|
32
|
+
index: { prod: "pgvector" },
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
expect(images["store.sql"]).toBe("pgvector/pgvector:pg17");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("returns empty when no container drivers", () => {
|
|
40
|
+
expect(defaultImagesFromConfig({})).toEqual({});
|
|
41
|
+
expect(
|
|
42
|
+
defaultImagesFromConfig({
|
|
43
|
+
drivers: { store: { sql: { prod: "sqlite" } } },
|
|
44
|
+
}),
|
|
45
|
+
).toEqual({});
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe("resolveImages", () => {
|
|
50
|
+
test("explicit images win over defaults", () => {
|
|
51
|
+
const images = resolveImages({
|
|
52
|
+
drivers: {
|
|
53
|
+
store: {
|
|
54
|
+
sql: { prod: "postgres" },
|
|
55
|
+
kv: { prod: "redis" },
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
images: { "store.sql": "pgvector/pgvector:pg17" },
|
|
59
|
+
});
|
|
60
|
+
expect(images).toEqual({ "store.sql": "pgvector/pgvector:pg17" });
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("falls back to driver defaults when images omitted", () => {
|
|
64
|
+
const images = resolveImages({
|
|
65
|
+
drivers: {
|
|
66
|
+
store: {
|
|
67
|
+
sql: { prod: "postgres" },
|
|
68
|
+
kv: { prod: "redis" },
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
expect(images["store.sql"]).toBe("postgres:18-alpine");
|
|
73
|
+
expect(images["store.kv"]).toBe("redis:8-alpine");
|
|
74
|
+
});
|
|
75
|
+
});
|
package/src/cli/load-config.ts
CHANGED
|
@@ -3,9 +3,68 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { resolve } from "node:path";
|
|
6
|
-
import type { OkeConfig } from "../config/index.ts";
|
|
6
|
+
import type { DriverRef, OkeConfig } from "../config/index.ts";
|
|
7
7
|
import type { Manifest } from "../manifest/types.ts";
|
|
8
8
|
|
|
9
|
+
/** Default image pins when `images` is omitted but prod drivers need containers. */
|
|
10
|
+
const DEFAULT_SQL_IMAGE = "postgres:18-alpine";
|
|
11
|
+
const DEFAULT_PGVECTOR_IMAGE = "pgvector/pgvector:pg17";
|
|
12
|
+
const DEFAULT_KV_IMAGE = "redis:8-alpine";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Extract protocol id from a driver ref.
|
|
16
|
+
*
|
|
17
|
+
* @param ref - String or `{ driver }` object
|
|
18
|
+
*/
|
|
19
|
+
function driverId(ref: DriverRef | undefined): string | undefined {
|
|
20
|
+
if (ref === undefined) return undefined;
|
|
21
|
+
return typeof ref === "string" ? ref : ref.driver;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Derive default image pins from prod driver protocols.
|
|
26
|
+
*
|
|
27
|
+
* Used when `oke.config.ts` omits `images` but declares postgres/redis (etc.)
|
|
28
|
+
* so `oke dev -s` / `oke stack` / `oke docker` have something to run.
|
|
29
|
+
*
|
|
30
|
+
* @param config - Loaded config
|
|
31
|
+
*/
|
|
32
|
+
export function defaultImagesFromConfig(
|
|
33
|
+
config: OkeConfig,
|
|
34
|
+
): Readonly<Record<string, string>> {
|
|
35
|
+
const out: Record<string, string> = {};
|
|
36
|
+
const sql = driverId(config.drivers?.store?.sql?.prod);
|
|
37
|
+
const index = driverId(config.drivers?.store?.index?.prod);
|
|
38
|
+
const kv = driverId(config.drivers?.store?.kv?.prod);
|
|
39
|
+
const signal = driverId(config.drivers?.signal?.prod);
|
|
40
|
+
const clock = driverId(config.drivers?.clock?.prod);
|
|
41
|
+
|
|
42
|
+
const needsSql =
|
|
43
|
+
sql === "postgres" ||
|
|
44
|
+
sql === "pgvector" ||
|
|
45
|
+
index === "pgvector" ||
|
|
46
|
+
signal === "postgres" ||
|
|
47
|
+
clock === "postgres" ||
|
|
48
|
+
(config.drivers?.prod ?? []).some(
|
|
49
|
+
(p) => p === "postgres" || p === "pgvector",
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
if (needsSql) {
|
|
53
|
+
out["store.sql"] =
|
|
54
|
+
sql === "pgvector" || index === "pgvector"
|
|
55
|
+
? DEFAULT_PGVECTOR_IMAGE
|
|
56
|
+
: DEFAULT_SQL_IMAGE;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const needsKv =
|
|
60
|
+
kv === "redis" || (config.drivers?.prod ?? []).includes("redis");
|
|
61
|
+
if (needsKv) {
|
|
62
|
+
out["store.kv"] = DEFAULT_KV_IMAGE;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
|
|
9
68
|
/** Result of loading project config. */
|
|
10
69
|
export interface LoadedConfig {
|
|
11
70
|
readonly config: OkeConfig;
|
|
@@ -60,6 +119,9 @@ export async function loadManifest(path: string): Promise<Manifest> {
|
|
|
60
119
|
/**
|
|
61
120
|
* Resolve images map from config or Manifest.
|
|
62
121
|
*
|
|
122
|
+
* When neither declares `images`, derive defaults from prod driver protocols
|
|
123
|
+
* so `oke dev -s` works on scaffolded templates without an explicit pin map.
|
|
124
|
+
*
|
|
63
125
|
* @param config - Optional config
|
|
64
126
|
* @param manifest - Optional manifest
|
|
65
127
|
*/
|
|
@@ -67,5 +129,8 @@ export function resolveImages(
|
|
|
67
129
|
config?: OkeConfig,
|
|
68
130
|
manifest?: Manifest,
|
|
69
131
|
): Readonly<Record<string, string>> {
|
|
70
|
-
|
|
132
|
+
const explicit = config?.images ?? manifest?.images;
|
|
133
|
+
if (explicit && Object.keys(explicit).length > 0) return explicit;
|
|
134
|
+
if (config) return defaultImagesFromConfig(config);
|
|
135
|
+
return {};
|
|
71
136
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev port probing — prefer then +1 until free.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from "bun:test";
|
|
6
|
+
import { findFreePort, resolveDevPorts } from "./ports.ts";
|
|
7
|
+
|
|
8
|
+
describe("findFreePort", () => {
|
|
9
|
+
test("returns preferred when free", async () => {
|
|
10
|
+
const port = await findFreePort(6530, new Set(), async () => false);
|
|
11
|
+
expect(port).toBe(6530);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("increments until free", async () => {
|
|
15
|
+
const busy = new Set([6530, 6531]);
|
|
16
|
+
const port = await findFreePort(
|
|
17
|
+
6530,
|
|
18
|
+
new Set(),
|
|
19
|
+
async (p) => busy.has(p),
|
|
20
|
+
);
|
|
21
|
+
expect(port).toBe(6532);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("skips occupied set even when probe says free", async () => {
|
|
25
|
+
const port = await findFreePort(
|
|
26
|
+
6530,
|
|
27
|
+
new Set([6530, 6531]),
|
|
28
|
+
async () => false,
|
|
29
|
+
);
|
|
30
|
+
expect(port).toBe(6532);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("passes through ephemeral 0", async () => {
|
|
34
|
+
expect(await findFreePort(0)).toBe(0);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe("resolveDevPorts", () => {
|
|
39
|
+
test("keeps app · console · mcp distinct when preferred collide", async () => {
|
|
40
|
+
// Everything busy except 6531, 6534, 6536 — force increments.
|
|
41
|
+
const busy = new Set([6530, 6533, 6535]);
|
|
42
|
+
const ports = await resolveDevPorts(
|
|
43
|
+
{ app: 6530, console: 6533, mcp: 6535 },
|
|
44
|
+
async (p) => busy.has(p),
|
|
45
|
+
);
|
|
46
|
+
expect(ports.app).toBe(6531);
|
|
47
|
+
expect(ports.console).toBe(6534);
|
|
48
|
+
expect(ports.mcp).toBe(6536);
|
|
49
|
+
expect(new Set([ports.app, ports.console, ports.mcp]).size).toBe(3);
|
|
50
|
+
});
|
|
51
|
+
});
|
package/src/cli/ports.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev-time port probing — Next.js-style prefer-then-increment.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createServer } from "node:net";
|
|
6
|
+
|
|
7
|
+
/** Max upward probes from a preferred port before failing. */
|
|
8
|
+
const MAX_PORT_ATTEMPTS = 100;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* True when something is already listening on `port` at 127.0.0.1.
|
|
12
|
+
*
|
|
13
|
+
* @param port - TCP port
|
|
14
|
+
*/
|
|
15
|
+
export function isPortInUse(port: number): Promise<boolean> {
|
|
16
|
+
return new Promise((resolvePromise) => {
|
|
17
|
+
const server = createServer();
|
|
18
|
+
server.once("error", (err: NodeJS.ErrnoException) => {
|
|
19
|
+
resolvePromise(err.code === "EADDRINUSE");
|
|
20
|
+
});
|
|
21
|
+
server.once("listening", () => {
|
|
22
|
+
server.close(() => resolvePromise(false));
|
|
23
|
+
});
|
|
24
|
+
server.listen(port, "127.0.0.1");
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Find the first free port at or above `preferred`, skipping occupied set.
|
|
30
|
+
*
|
|
31
|
+
* @param preferred - Starting port (e.g. 6530)
|
|
32
|
+
* @param occupied - Ports already claimed in this session
|
|
33
|
+
* @param probe - Injectable busy check (tests)
|
|
34
|
+
*/
|
|
35
|
+
export async function findFreePort(
|
|
36
|
+
preferred: number,
|
|
37
|
+
occupied: ReadonlySet<number> = new Set(),
|
|
38
|
+
probe: (port: number) => Promise<boolean> = isPortInUse,
|
|
39
|
+
): Promise<number> {
|
|
40
|
+
if (!Number.isFinite(preferred) || preferred < 0) {
|
|
41
|
+
throw new Error(`oke: invalid preferred port ${preferred}`);
|
|
42
|
+
}
|
|
43
|
+
// Ephemeral (0) — leave to the OS; no probing.
|
|
44
|
+
if (preferred === 0) return 0;
|
|
45
|
+
|
|
46
|
+
for (let i = 0; i < MAX_PORT_ATTEMPTS; i++) {
|
|
47
|
+
const port = preferred + i;
|
|
48
|
+
if (occupied.has(port)) continue;
|
|
49
|
+
if (!(await probe(port))) return port;
|
|
50
|
+
}
|
|
51
|
+
throw new Error(
|
|
52
|
+
`oke: no free port near ${preferred} after ${MAX_PORT_ATTEMPTS} attempts`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Resolve distinct free ports for app · Console · MCP (dev only).
|
|
58
|
+
*
|
|
59
|
+
* @param preferred - Preferred ports
|
|
60
|
+
* @param probe - Injectable busy check (tests)
|
|
61
|
+
*/
|
|
62
|
+
export async function resolveDevPorts(
|
|
63
|
+
preferred: {
|
|
64
|
+
readonly app: number;
|
|
65
|
+
readonly console: number;
|
|
66
|
+
readonly mcp: number;
|
|
67
|
+
},
|
|
68
|
+
probe: (port: number) => Promise<boolean> = isPortInUse,
|
|
69
|
+
): Promise<{
|
|
70
|
+
readonly app: number;
|
|
71
|
+
readonly console: number;
|
|
72
|
+
readonly mcp: number;
|
|
73
|
+
}> {
|
|
74
|
+
const occupied = new Set<number>();
|
|
75
|
+
const app = await findFreePort(preferred.app, occupied, probe);
|
|
76
|
+
if (app !== 0) occupied.add(app);
|
|
77
|
+
const consolePort = await findFreePort(preferred.console, occupied, probe);
|
|
78
|
+
if (consolePort !== 0) occupied.add(consolePort);
|
|
79
|
+
const mcp = await findFreePort(preferred.mcp, occupied, probe);
|
|
80
|
+
return { app, console: consolePort, mcp };
|
|
81
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Top-level help — no Flags/JSON comment footers.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from "bun:test";
|
|
6
|
+
import { formatOkeHelp } from "./registry.ts";
|
|
7
|
+
|
|
8
|
+
describe("formatOkeHelp", () => {
|
|
9
|
+
test("lists commands without Flags/JSON commentary", () => {
|
|
10
|
+
const help = formatOkeHelp();
|
|
11
|
+
expect(help).toContain("oke — okengine CLI");
|
|
12
|
+
expect(help).toContain("Commands:");
|
|
13
|
+
expect(help).toContain("dev");
|
|
14
|
+
expect(help).not.toContain("Flags:");
|
|
15
|
+
expect(help).not.toContain("JSON:");
|
|
16
|
+
expect(help).not.toContain("Exit codes:");
|
|
17
|
+
});
|
|
18
|
+
});
|