omkit 0.4.6 → 0.4.8

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.
Files changed (45) hide show
  1. package/README.md +4 -2
  2. package/dist/actions/browser.d.ts +1 -1
  3. package/dist/actions/browser.js +1 -1
  4. package/dist/actions/chrome-page.d.ts +5 -4
  5. package/dist/actions/chrome-page.js +11 -11
  6. package/dist/actions/index.d.ts +2 -0
  7. package/dist/actions/index.js +1 -0
  8. package/dist/actions/port-free.d.ts +29 -0
  9. package/dist/actions/port-free.js +78 -0
  10. package/dist/cli/commands/help.d.ts +6 -0
  11. package/dist/cli/commands/help.js +27 -0
  12. package/dist/cli/commands/ls.d.ts +1 -1
  13. package/dist/cli/commands/run.d.ts +12 -1
  14. package/dist/cli/commands/run.js +47 -1
  15. package/dist/cli/commands/ui.d.ts +2 -2
  16. package/dist/cli/commands/ui.js +2 -2
  17. package/dist/cli/index.d.ts +3 -1
  18. package/dist/cli/index.js +29 -47
  19. package/dist/cli/ui/app.d.ts +2 -2
  20. package/dist/cli/ui/app.js +9 -5
  21. package/dist/cli/ui/views/OmList.d.ts +1 -1
  22. package/dist/cli/ui/views/RunView.d.ts +6 -4
  23. package/dist/cli/ui/views/RunView.js +24 -3
  24. package/dist/{cli/client → client}/channel.d.ts +1 -1
  25. package/dist/{cli/client → client}/index.d.ts +1 -1
  26. package/dist/client/index.js +18 -0
  27. package/dist/{cli/client → client}/types.d.ts +7 -3
  28. package/dist/output/LiveRenderer.d.ts +4 -2
  29. package/dist/output/LiveRenderer.js +8 -13
  30. package/dist/output/RunModel.d.ts +41 -0
  31. package/dist/output/RunModel.js +86 -0
  32. package/package.json +1 -1
  33. package/dist/cli/client/index.js +0 -11
  34. package/dist/cli/commands/check.d.ts +0 -11
  35. package/dist/cli/commands/check.js +0 -14
  36. /package/dist/{cli/client → client}/channel.js +0 -0
  37. /package/dist/{cli/client → client}/check.d.ts +0 -0
  38. /package/dist/{cli/client → client}/check.js +0 -0
  39. /package/dist/{cli/client → client}/discovery.d.ts +0 -0
  40. /package/dist/{cli/client → client}/discovery.js +0 -0
  41. /package/dist/{cli/client → client}/registry.d.ts +0 -0
  42. /package/dist/{cli/client → client}/registry.js +0 -0
  43. /package/dist/{cli/client → client}/runner.d.ts +0 -0
  44. /package/dist/{cli/client → client}/runner.js +0 -0
  45. /package/dist/{cli/client → client}/types.js +0 -0
package/README.md CHANGED
@@ -33,7 +33,7 @@ om("dev", async () => {
33
33
 
34
34
  // …then launch a real browser and hand its live page downstream — typed, not scraped.
35
35
  const chrome = browser({ headless: false }); // a visible Chromium, closed on teardown
36
- const page = chromePage("app", chrome.ref, { url: "http://localhost:3000" });
36
+ const page = chromePage("app", await chrome.ref, { url: "http://localhost:3000" });
37
37
  console.log(`opened ${(await page.ref).url()}`);
38
38
 
39
39
  console.log("stack is up — press Ctrl+C to stop");
@@ -223,6 +223,7 @@ Reusable actions built on the core engine. For anything beyond these, drop down
223
223
  - **`untilLog`** — gate on the first log entry matching a predicate.
224
224
  - **`tailLog`** — tail a file another process writes, folding its lines into the combined log.
225
225
  - **`healthcheck`** — poll a URL/port until the status (and optionally body) matches.
226
+ - **`portFree`** — the mirror of `healthcheck`: poll a TCP port until nothing is listening, so a restart can rebind without racing the old process.
226
227
  - **`prompt`** — ask the terminal for input or a choice, with a timeout that falls back to a default; the answer is its handle.
227
228
  - **`browser`** — launch a Chromium browser with Playwright and expose the live `Browser` as a handle (defaults to the installed Chrome); closed on teardown. Feed its `.ref` to `chromePage`.
228
229
  - **`chromePage`** — attach to Chrome over CDP, or to an existing Playwright `Page`/`Browser`/`Context` (including a `browser` handle or an Electron window), and expose the live `Page` as a handle.
@@ -236,8 +237,9 @@ Point the `omkit` bin at a project — a `tsconfig.omkit.json` that lists your `
236
237
  - **`omkit run <om>`** — run one om directly by name or file path, inheriting the terminal.
237
238
  - **`omkit ls`** — list the discovered oms and actions.
238
239
  - **`omkit check`** — typecheck the project (`tsc --noEmit`) and report diagnostics.
240
+ - **`omkit help`** — print the command overview (also `--help` / `-h`).
239
241
 
240
- Each run is supervised in its own child process and narrated to a run folder (see above), whether you launch it from the picker or with `run <om>`.
242
+ Both paths run the om in its own child process and narrate it to a run folder (see above). The picker **supervises** the child — piping its output into the live view and answering prompts for it — while `omkit run <om>` runs it **bare**, handing the om your terminal directly.
241
243
 
242
244
  ## Install
243
245
 
@@ -17,7 +17,7 @@ export interface BrowserOptions {
17
17
  }
18
18
  /**
19
19
  * Launches a Chromium browser with Playwright and publishes the live {@link Browser}
20
- * as this action's handle, so a driver — e.g. `chromePage("Explorer", browser.ref,
20
+ * as this action's handle, so a driver — e.g. `chromePage("Explorer", await browser.ref,
21
21
  * { url })` — can await `instance.ref` and open pages in it. This owns the browser it
22
22
  * launches (unlike a CDP endpoint handed to `chromePage`, which belongs to its opener),
23
23
  * so teardown closes it. Replaces the external chromedriver/WebDriver dance.
@@ -3,7 +3,7 @@ import { action } from "../core/action.js";
3
3
  const delay = (ms) => new Promise((r) => setTimeout(r, ms));
4
4
  /**
5
5
  * Launches a Chromium browser with Playwright and publishes the live {@link Browser}
6
- * as this action's handle, so a driver — e.g. `chromePage("Explorer", browser.ref,
6
+ * as this action's handle, so a driver — e.g. `chromePage("Explorer", await browser.ref,
7
7
  * { url })` — can await `instance.ref` and open pages in it. This owns the browser it
8
8
  * launches (unlike a CDP endpoint handed to `chromePage`, which belongs to its opener),
9
9
  * so teardown closes it. Replaces the external chromedriver/WebDriver dance.
@@ -22,10 +22,11 @@ export interface ChromePageOptions {
22
22
  }
23
23
  /**
24
24
  * Publishes a Chrome/Chromium {@link Page} as this action's handle, so downstream
25
- * actions can await `instance.ref` and drive the same live page. The source may be
26
- * a promise, so it wires straight from another action's `.ref` (e.g.
27
- * `chromePage("Explorer", chromedriver.ref)`). `label` tags this run so several
25
+ * actions can await `instance.ref` and drive the same live page. The source is a
26
+ * resolved handle, not a promise — chain it from another action by awaiting that
27
+ * action's `.ref` at the call site (e.g.
28
+ * `chromePage("Explorer", await chromedriver.ref)`). `label` tags this run so several
28
29
  * pages in one run stay distinguishable. Daemon: acquire + attach, stay alive
29
30
  * until teardown, then close the CDP connection only when it opened one.
30
31
  */
31
- export declare const chromePage: import("../index.ts").Action<[label: string, source: ChromePageSource | Promise<ChromePageSource>, opts?: ChromePageOptions | undefined], void, ChromePageEvents, Page>;
32
+ export declare const chromePage: import("../index.ts").Action<[label: string, source: ChromePageSource, opts?: ChromePageOptions | undefined], void, ChromePageEvents, Page>;
@@ -7,9 +7,10 @@ const isBrowser = (x) => typeof x.contexts === "function";
7
7
  const pageOf = (context, fresh) => fresh ? context.newPage() : (context.pages()[0] ?? context.newPage());
8
8
  /**
9
9
  * Publishes a Chrome/Chromium {@link Page} as this action's handle, so downstream
10
- * actions can await `instance.ref` and drive the same live page. The source may be
11
- * a promise, so it wires straight from another action's `.ref` (e.g.
12
- * `chromePage("Explorer", chromedriver.ref)`). `label` tags this run so several
10
+ * actions can await `instance.ref` and drive the same live page. The source is a
11
+ * resolved handle, not a promise — chain it from another action by awaiting that
12
+ * action's `.ref` at the call site (e.g.
13
+ * `chromePage("Explorer", await chromedriver.ref)`). `label` tags this run so several
13
14
  * pages in one run stay distinguishable. Daemon: acquire + attach, stay alive
14
15
  * until teardown, then close the CDP connection only when it opened one.
15
16
  */
@@ -19,13 +20,12 @@ export const chromePage = action("chromePage")
19
20
  .run(async ({ signal, emit, attach, tag }, label, source, opts = {}) => {
20
21
  tag(label);
21
22
  const { url } = opts;
22
- const resolved = await source;
23
23
  // Only a CDP connection we open here is ours to close; handles belong to callers.
24
24
  let owned;
25
25
  let page;
26
26
  let from;
27
- if (typeof resolved === "string") {
28
- const cdpUrl = resolved.includes("://") ? resolved : `http://${resolved}`;
27
+ if (typeof source === "string") {
28
+ const cdpUrl = source.includes("://") ? source : `http://${source}`;
29
29
  from = cdpUrl;
30
30
  try {
31
31
  owned = await chromium.connectOverCDP(cdpUrl);
@@ -36,18 +36,18 @@ export const chromePage = action("chromePage")
36
36
  const context = owned.contexts()[0] ?? (await owned.newContext());
37
37
  page = await pageOf(context, url !== undefined);
38
38
  }
39
- else if (isPage(resolved)) {
39
+ else if (isPage(source)) {
40
40
  from = "page handle";
41
- page = resolved;
41
+ page = source;
42
42
  }
43
- else if (isBrowser(resolved)) {
43
+ else if (isBrowser(source)) {
44
44
  from = "browser handle";
45
- const context = resolved.contexts()[0] ?? (await resolved.newContext());
45
+ const context = source.contexts()[0] ?? (await source.newContext());
46
46
  page = await pageOf(context, url !== undefined);
47
47
  }
48
48
  else {
49
49
  from = "context handle";
50
- page = await pageOf(resolved, url !== undefined);
50
+ page = await pageOf(source, url !== undefined);
51
51
  }
52
52
  page.on("console", (msg) => {
53
53
  console.log(`console: ${msg.text()}`);
@@ -8,6 +8,8 @@ export { untilLog } from "./until-log.ts";
8
8
  export type { LogMatcher, UntilLogOptions, UntilLogEvents } from "./until-log.ts";
9
9
  export { healthcheck } from "./healthcheck.ts";
10
10
  export type { HealthcheckOptions, HealthcheckResult, HealthcheckEvents, StatusMatcher, BodyMatcher, } from "./healthcheck.ts";
11
+ export { portFree } from "./port-free.ts";
12
+ export type { PortFreeOptions, PortFreeResult, PortFreeEvents } from "./port-free.ts";
11
13
  export { watch } from "./watch.ts";
12
14
  export type { FileEvent, FileListener, WatchOptions } from "./watch.ts";
13
15
  export { watchDir } from "./watch-dir.ts";
@@ -5,6 +5,7 @@
5
5
  export { command } from "./command.js";
6
6
  export { untilLog } from "./until-log.js";
7
7
  export { healthcheck } from "./healthcheck.js";
8
+ export { portFree } from "./port-free.js";
8
9
  export { watch } from "./watch.js";
9
10
  export { watchDir } from "./watch-dir.js";
10
11
  export { tailLog } from "./tail-log.js";
@@ -0,0 +1,29 @@
1
+ export interface PortFreeOptions {
2
+ /** Host to probe. Default "127.0.0.1". */
3
+ host?: string;
4
+ /** Delay between probes. Default 500ms. */
5
+ intervalMs?: number;
6
+ /** Abort a single connect attempt that hasn't answered within this long. Default 1000ms. */
7
+ connectTimeoutMs?: number;
8
+ /** Reject after this long overall. Default 30000ms; pass `undefined` to wait until teardown. */
9
+ timeoutMs?: number;
10
+ }
11
+ /** The probe that found the port free — this action's result and `free` event payload. */
12
+ export interface PortFreeResult {
13
+ port: number;
14
+ host: string;
15
+ attempts: number;
16
+ }
17
+ /** Events emitted by {@link portFree}. */
18
+ export interface PortFreeEvents {
19
+ /** Nothing is listening any more; payload is the passing probe. */
20
+ free: PortFreeResult;
21
+ }
22
+ /**
23
+ * Gate: polls a TCP port until nothing is listening on it, then resolves with that
24
+ * probe and emits `free`. The mirror of `healthcheck` — where that one waits
25
+ * for a service to come up, this waits for one to let go, so a restart can rebind
26
+ * without racing the previous process's socket. Bounded by the run's abort signal
27
+ * and `timeoutMs`, so teardown unblocks a pending probe instead of hanging.
28
+ */
29
+ export declare const portFree: import("../index.ts").Action<[port: number, opts?: PortFreeOptions | undefined], PortFreeResult, PortFreeEvents, void>;
@@ -0,0 +1,78 @@
1
+ import net from "node:net";
2
+ import { action } from "../core/action.js";
3
+ /**
4
+ * One TCP connect attempt. A refusal is the only answer that proves nobody is
5
+ * listening: a successful connect means the port is taken, and a hang or any other
6
+ * error (filtered, unreachable) is inconclusive, so both count as still busy rather
7
+ * than letting a firewall read as "free".
8
+ */
9
+ const probe = (port, host, connectTimeoutMs, signal) => new Promise((resolve) => {
10
+ const socket = new net.Socket();
11
+ let settled = false;
12
+ const finish = (free) => {
13
+ if (settled)
14
+ return;
15
+ settled = true;
16
+ signal.removeEventListener("abort", onAbort);
17
+ socket.destroy();
18
+ resolve(free);
19
+ };
20
+ // Teardown mid-connect: drop the socket and report "busy" — the loop's own abort
21
+ // check turns that into a clean stop on the next pass.
22
+ function onAbort() {
23
+ finish(false);
24
+ }
25
+ socket.setTimeout(connectTimeoutMs);
26
+ socket.once("connect", () => finish(false));
27
+ socket.once("timeout", () => finish(false));
28
+ socket.once("error", (error) => finish(error.code === "ECONNREFUSED"));
29
+ signal.addEventListener("abort", onAbort, { once: true });
30
+ socket.connect(port, host);
31
+ });
32
+ /**
33
+ * Gate: polls a TCP port until nothing is listening on it, then resolves with that
34
+ * probe and emits `free`. The mirror of `healthcheck` — where that one waits
35
+ * for a service to come up, this waits for one to let go, so a restart can rebind
36
+ * without racing the previous process's socket. Bounded by the run's abort signal
37
+ * and `timeoutMs`, so teardown unblocks a pending probe instead of hanging.
38
+ */
39
+ export const portFree = action("portFree")
40
+ .emits()
41
+ .run(async ({ signal, emit }, port, opts = {}) => {
42
+ const { host = "127.0.0.1", intervalMs = 500, connectTimeoutMs = 1000, timeoutMs = 30_000, } = opts;
43
+ // Abortable inter-probe delay: resolves after `ms`, or rejects on teardown.
44
+ const sleep = (ms) => new Promise((resolve, reject) => {
45
+ const onAbort = () => {
46
+ clearTimeout(timer);
47
+ reject(new Error("portFree: aborted"));
48
+ };
49
+ const timer = setTimeout(() => {
50
+ signal.removeEventListener("abort", onAbort);
51
+ resolve();
52
+ }, ms);
53
+ if (signal.aborted)
54
+ onAbort();
55
+ else
56
+ signal.addEventListener("abort", onAbort, { once: true });
57
+ });
58
+ const deadline = timeoutMs === undefined ? undefined : Date.now() + timeoutMs;
59
+ const target = `${host}:${port}`;
60
+ console.log(`waiting for ${target} to free up`);
61
+ let attempts = 0;
62
+ for (;;) {
63
+ if (signal.aborted)
64
+ throw new Error("portFree: aborted");
65
+ attempts++;
66
+ if (await probe(port, host, connectTimeoutMs, signal)) {
67
+ const result = { port, host, attempts };
68
+ const probes = `${attempts} ${attempts === 1 ? "probe" : "probes"}`;
69
+ console.log(`free ${target} (${probes})`);
70
+ emit("free", result);
71
+ return result;
72
+ }
73
+ if (deadline !== undefined && Date.now() >= deadline) {
74
+ throw new Error(`portFree: ${target} still in use after ${timeoutMs}ms (${attempts} probes)`);
75
+ }
76
+ await sleep(intervalMs);
77
+ }
78
+ });
@@ -0,0 +1,6 @@
1
+ /**
2
+ * The flat `omkit help` overview — one usage line, every command, and the global flags.
3
+ * A pure string builder so it's trivially testable and reusable: `main` prints it for an
4
+ * explicit `help`/`--help`, and again (to stderr) when an unknown command is given.
5
+ */
6
+ export declare function helpText(): string;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The flat `omkit help` overview — one usage line, every command, and the global flags.
3
+ * A pure string builder so it's trivially testable and reusable: `main` prints it for an
4
+ * explicit `help`/`--help`, and again (to stderr) when an unknown command is given.
5
+ */
6
+ export function helpText() {
7
+ return [
8
+ "omkit — run the workflows around your code",
9
+ "",
10
+ "Usage: omkit [command] [target] [flags]",
11
+ "",
12
+ "Commands:",
13
+ " run [om] run an om — interactive picker if no om is given, else by name or file path",
14
+ " ls list the discovered oms and actions",
15
+ " check typecheck the project (tsc --noEmit)",
16
+ " init scaffold a starter project (tsconfig.omkit.json + sample oms/ and actions/)",
17
+ " help show this help",
18
+ "",
19
+ "Flags:",
20
+ " --tsconfig <path> project config to scan (default: tsconfig.omkit.json)",
21
+ " --json machine-readable output (ls)",
22
+ " --inspect[=port] run the om in a forked child with the Node inspector open (default port 9229)",
23
+ " -h, --help show this help",
24
+ "",
25
+ "A bare `omkit` opens the interactive picker (run is the default command).",
26
+ ].join("\n");
27
+ }
@@ -1,4 +1,4 @@
1
- import type { Registry } from "../client/registry.ts";
1
+ import type { Registry } from "../../client/registry.ts";
2
2
  /** Render a {@link Registry} for the terminal (plain text) or as JSON. */
3
3
  export declare function formatRegistry(registry: Registry, opts?: {
4
4
  json?: boolean;
@@ -1,4 +1,5 @@
1
- import type { Registry } from "../client/registry.ts";
1
+ import type { Registry } from "../../client/registry.ts";
2
+ import type { OmkitClient, InspectOptions } from "../../client/index.ts";
2
3
  /**
3
4
  * Report an actionable "nothing to run" panel when discovery finds no oms. Names the config file
4
5
  * omkit actually read (absolute, so the tsconfig.omkit.json-vs-tsconfig.json confusion goes away),
@@ -16,3 +17,13 @@ export declare function resolveOm(arg: string, registry: {
16
17
  file: string;
17
18
  }[];
18
19
  }, cwd: string): string | undefined;
20
+ /**
21
+ * The `run` command. With no target, open the interactive picker (a bare `omkit` lands here too,
22
+ * since `run` is the default command); otherwise resolve the target om and run it bare, inheriting
23
+ * the terminal. Reaches the engine only through the {@link OmkitClient} interface. Sets
24
+ * `process.exitCode` on failure or from the child's exit code.
25
+ */
26
+ export declare function runCommand(client: OmkitClient, opts: {
27
+ target?: string;
28
+ inspect?: InspectOptions;
29
+ }): Promise<void>;
@@ -1,6 +1,6 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { renderDiagnostics } from "../ui/Report.js";
3
+ import { renderDiagnostics, withSpinner } from "../ui/Report.js";
4
4
  /**
5
5
  * Report an actionable "nothing to run" panel when discovery finds no oms. Names the config file
6
6
  * omkit actually read (absolute, so the tsconfig.omkit.json-vs-tsconfig.json confusion goes away),
@@ -27,3 +27,49 @@ export function resolveOm(arg, registry, cwd) {
27
27
  return asPath;
28
28
  return registry.oms.find((o) => o.name === arg)?.file;
29
29
  }
30
+ /**
31
+ * Tell the user where the inspector will be. The om runs in a forked child, so the inspector is on
32
+ * that child — never on the omkit process. In the interactive picker the child (and the port) only
33
+ * exists once an om is chosen; a bare run forks it immediately.
34
+ */
35
+ function announceInspect(inspect, interactive) {
36
+ const p = inspect.port;
37
+ const run = interactive ? "the om you pick runs" : "the om runs";
38
+ console.log(`omkit: ${run} in a forked child with the inspector on :${p} — attach a debugger to that child, not to omkit itself (--inspect).`);
39
+ }
40
+ /**
41
+ * The `run` command. With no target, open the interactive picker (a bare `omkit` lands here too,
42
+ * since `run` is the default command); otherwise resolve the target om and run it bare, inheriting
43
+ * the terminal. Reaches the engine only through the {@link OmkitClient} interface. Sets
44
+ * `process.exitCode` on failure or from the child's exit code.
45
+ */
46
+ export async function runCommand(client, opts) {
47
+ const { target, inspect } = opts;
48
+ if (!target) {
49
+ // Announce before Ink takes the terminal, so the notice isn't swallowed by the live UI —
50
+ // the supervised child's own inspector banner goes to a piped stream nobody sees. launchUi is
51
+ // lazy-loaded to keep the run.ts↔ui.tsx dependency out of module init (ui.tsx imports reportNoOms).
52
+ if (inspect)
53
+ announceInspect(inspect, true);
54
+ const { launchUi } = await import("./ui.js");
55
+ await launchUi(client);
56
+ return;
57
+ }
58
+ const registry = await withSpinner("discovering…", () => client.discover());
59
+ if (registry.oms.length === 0) {
60
+ await reportNoOms(client.tsconfig, registry);
61
+ process.exitCode = 1;
62
+ return;
63
+ }
64
+ const omFile = resolveOm(target, registry, process.cwd());
65
+ if (!omFile) {
66
+ const known = registry.oms.map((o) => o.name).join(", ");
67
+ console.error(`no om matches "${target}". Known oms: ${known}`);
68
+ process.exitCode = 1;
69
+ return;
70
+ }
71
+ if (inspect)
72
+ announceInspect(inspect, false);
73
+ process.on("SIGINT", () => { }); // let the child tear down; don't die first
74
+ process.exitCode = await client.runBare(omFile, { cwd: path.dirname(omFile) });
75
+ }
@@ -1,4 +1,4 @@
1
- import type { OmkitClient } from "../client/index.ts";
1
+ import type { OmkitClient } from "../../client/index.ts";
2
2
  /**
3
3
  * Render the interactive Ink app. Ink's own Ctrl+C handling is disabled so the app can tear the
4
4
  * run down gracefully first; the final verdict + run-folder path are printed here, after Ink
@@ -9,4 +9,4 @@ import type { OmkitClient } from "../client/index.ts";
9
9
  * it reports why (config + warnings) instead of dropping into a blank picker. Non-fatal warnings
10
10
  * are printed so they aren't lost behind the UI.
11
11
  */
12
- export declare function launchUi(client: OmkitClient, tsconfig: string): Promise<void>;
12
+ export declare function launchUi(client: OmkitClient): Promise<void>;
@@ -13,10 +13,10 @@ import { reportNoOms } from "./run.js";
13
13
  * it reports why (config + warnings) instead of dropping into a blank picker. Non-fatal warnings
14
14
  * are printed so they aren't lost behind the UI.
15
15
  */
16
- export async function launchUi(client, tsconfig) {
16
+ export async function launchUi(client) {
17
17
  const registry = await withSpinner("discovering…", () => client.discover());
18
18
  if (registry.oms.length === 0) {
19
- await reportNoOms(tsconfig, registry);
19
+ await reportNoOms(client.tsconfig, registry);
20
20
  return;
21
21
  }
22
22
  if (registry.warnings.length > 0) {
@@ -1,10 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import { type InspectOptions } from "./client/index.ts";
2
+ import { type InspectOptions } from "../client/index.ts";
3
3
  export interface Cli {
4
4
  command: string;
5
5
  target?: string;
6
6
  json: boolean;
7
7
  tsconfig: string;
8
+ /** Set by `--help`/`-h` (the `help` command is carried on `command` instead). */
9
+ help: boolean;
8
10
  /** The Node inspector, if requested via `--inspect[=port]`. */
9
11
  inspect?: InspectOptions;
10
12
  }
package/dist/cli/index.js CHANGED
@@ -3,7 +3,7 @@ import { parseArgs } from "node:util";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { realpathSync } from "node:fs";
6
- import { createOmkitClient } from "./client/index.js";
6
+ import { createOmkitClient } from "../client/index.js";
7
7
  /**
8
8
  * Pull the Node-style inspector flag out of argv, returning the options plus the remaining args.
9
9
  * Matches Node's own syntax: `--inspect`, `--inspect=PORT` (default port 9229); an explicit port
@@ -32,6 +32,7 @@ export function parseCli(argv) {
32
32
  options: {
33
33
  json: { type: "boolean", default: false },
34
34
  tsconfig: { type: "string", default: "tsconfig.omkit.json" },
35
+ help: { type: "boolean", short: "h", default: false },
35
36
  },
36
37
  allowPositionals: true,
37
38
  });
@@ -40,22 +41,36 @@ export function parseCli(argv) {
40
41
  target: positionals[1],
41
42
  json: Boolean(values.json),
42
43
  tsconfig: values.tsconfig,
44
+ help: Boolean(values.help),
43
45
  inspect,
44
46
  };
45
47
  }
46
48
  /**
47
- * Tell the user where the inspector will be. The om runs in a forked child, so the inspector is on
48
- * that child — never on this omkit process. In the interactive picker the child (and the port) only
49
- * exists once an om is chosen; a bare run forks it immediately.
49
+ * The `check` command: typecheck the project and render the diagnostics. The mapping is trivial
50
+ * (one item per diagnostic) and the policy is local — **any diagnostic fails the command** (exit 1),
51
+ * a clean project reports ok and exits 0.
50
52
  */
51
- function announceInspect(inspect, interactive) {
52
- const p = inspect.port;
53
- const run = interactive ? "the om you pick runs" : "the om runs";
54
- console.log(`omkit: ${run} in a forked child with the inspector on :${p} — attach a debugger to that child, not to omkit itself (--inspect).`);
53
+ async function checkCommand(client) {
54
+ const { renderDiagnostics, withSpinner } = await import("./ui/Report.js");
55
+ const diagnostics = await withSpinner("type-checking…", () => client.check());
56
+ const n = diagnostics.length;
57
+ const items = diagnostics.map((d) => ({
58
+ head: `${path.basename(d.file)}:${d.line}`,
59
+ detail: d.message,
60
+ }));
61
+ const title = n === 0 ? "no type errors" : `${n} type error${n === 1 ? "" : "s"}`;
62
+ await renderDiagnostics({ kind: n === 0 ? "ok" : "error", title, items }, n === 0 ? process.stdout : process.stderr);
63
+ process.exitCode = n === 0 ? 0 : 1;
55
64
  }
56
65
  /** The bin entry: route to a lazily-imported command; owns stdout and the exit code. */
57
66
  async function main() {
58
67
  const cli = parseCli(process.argv.slice(2));
68
+ // `--help`/`-h` or the `help` command short-circuits everything — no config or client needed.
69
+ if (cli.help || cli.command === "help") {
70
+ const { helpText } = await import("./commands/help.js");
71
+ console.log(helpText());
72
+ return;
73
+ }
59
74
  const inspect = cli.inspect;
60
75
  const client = createOmkitClient({ tsconfig: cli.tsconfig, inspect });
61
76
  if (cli.command === "init") {
@@ -75,50 +90,17 @@ async function main() {
75
90
  return;
76
91
  }
77
92
  if (cli.command === "check") {
78
- const { checkReport } = await import("./commands/check.js");
79
- const { renderDiagnostics, withSpinner } = await import("./ui/Report.js");
80
- const diagnostics = await withSpinner("type-checking…", () => client.check());
81
- const { title, items, code } = checkReport(diagnostics);
82
- // Type errors fail the command (exit 1); a clean project reports ok and exits 0.
83
- await renderDiagnostics({ kind: code === 0 ? "ok" : "error", title, items }, code === 0 ? process.stdout : process.stderr);
84
- process.exitCode = code;
93
+ await checkCommand(client);
85
94
  return;
86
95
  }
87
96
  if (cli.command === "run") {
88
- // No target → open the interactive picker (browse/search oms, run one with live output).
89
- // A bare `omkit` lands here too, since `run` is the default command.
90
- if (!cli.target) {
91
- // Announce before Ink takes the terminal, so the notice isn't swallowed by the live UI —
92
- // the supervised child's own inspector banner goes to a piped stream nobody sees.
93
- if (inspect)
94
- announceInspect(inspect, true);
95
- const { launchUi } = await import("./commands/ui.js");
96
- await launchUi(client, cli.tsconfig);
97
- return;
98
- }
99
- const { resolveOm, reportNoOms } = await import("./commands/run.js");
100
- const { spawnBare } = await import("./client/runner.js");
101
- const { withSpinner } = await import("./ui/Report.js");
102
- const registry = await withSpinner("discovering…", () => client.discover());
103
- if (registry.oms.length === 0) {
104
- await reportNoOms(cli.tsconfig, registry);
105
- process.exitCode = 1;
106
- return;
107
- }
108
- const omFile = resolveOm(cli.target, registry, process.cwd());
109
- if (!omFile) {
110
- const known = registry.oms.map((o) => o.name).join(", ");
111
- console.error(`no om matches "${cli.target}". Known oms: ${known}`);
112
- process.exitCode = 1;
113
- return;
114
- }
115
- if (inspect)
116
- announceInspect(inspect, false);
117
- process.on("SIGINT", () => { }); // let the child tear down; don't die first
118
- process.exitCode = await spawnBare(omFile, { cwd: path.dirname(omFile), inspect });
97
+ const { runCommand } = await import("./commands/run.js");
98
+ await runCommand(client, { target: cli.target, inspect: cli.inspect });
119
99
  return;
120
100
  }
121
- console.error(`unknown command "${cli.command}"`);
101
+ const { helpText } = await import("./commands/help.js");
102
+ console.error(`unknown command "${cli.command}"\n`);
103
+ console.error(helpText());
122
104
  process.exitCode = 1;
123
105
  }
124
106
  /**
@@ -1,6 +1,6 @@
1
1
  import { type ReactElement } from "react";
2
- import type { OmkitClient, Verdict } from "../client/types.ts";
3
- import type { DiscoveredOm } from "../client/registry.ts";
2
+ import type { OmkitClient, Verdict } from "../../client/types.ts";
3
+ import type { DiscoveredOm } from "../../client/registry.ts";
4
4
  /** The interactive app: discover → list/search → run → live milestones + prompts. */
5
5
  export declare function App({ client, onExit, oms: initialOms, }: {
6
6
  client: OmkitClient;
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useRef, useState } from "react";
3
3
  import { useApp, useInput } from "ink";
4
- import { format } from "../../output/milestones.js";
4
+ import { RunModel } from "../../output/RunModel.js";
5
5
  import { OmList } from "./views/OmList.js";
6
6
  import { RunView } from "./views/RunView.js";
7
7
  import { Spinner } from "./Spinner.js";
@@ -10,7 +10,10 @@ export function App({ client, onExit, oms: initialOms, }) {
10
10
  const { exit } = useApp();
11
11
  const [oms, setOms] = useState(initialOms ?? []);
12
12
  const [session, setSession] = useState(null);
13
+ // A per-run RunModel folds the log stream into a capped milestone tail plus a live node summary;
14
+ // these snapshots are what the view renders. The cap is what keeps a long run from growing forever.
13
15
  const [lines, setLines] = useState([]);
16
+ const [status, setStatus] = useState();
14
17
  const [prompt, setPrompt] = useState();
15
18
  const [verdict, setVerdict] = useState();
16
19
  const [tearing, setTearing] = useState(false);
@@ -31,12 +34,13 @@ export function App({ client, onExit, oms: initialOms, }) {
31
34
  };
32
35
  }, [client, initialOms]);
33
36
  const run = (om) => {
37
+ const model = new RunModel();
34
38
  const s = client.run(om.file);
35
39
  sessionRef.current = s;
36
40
  s.on("log", (entry) => {
37
- const rendered = format(entry);
38
- if (rendered)
39
- setLines((prev) => [...prev, rendered.text]);
41
+ model.apply(entry);
42
+ setLines(model.milestones());
43
+ setStatus(model.summary());
40
44
  });
41
45
  s.on("prompt", (req) => setPrompt(req));
42
46
  // The child gave up on the prompt (timed out or torn down). Withdraw its box so a stale,
@@ -95,5 +99,5 @@ export function App({ client, onExit, oms: initialOms, }) {
95
99
  });
96
100
  if (!session)
97
101
  return _jsx(OmList, { oms: oms, onSelect: run });
98
- return (_jsxs(_Fragment, { children: [_jsx(RunView, { lines: lines, prompt: prompt, verdict: verdict, onAnswer: answer }), !verdict && tearing ? _jsx(Spinner, { label: "tearing down\u2026 (stopping the run cleanly)" }) : null, !verdict && !tearing && !prompt ? _jsx(Spinner, { label: "running\u2026" }) : null] }));
102
+ return (_jsxs(_Fragment, { children: [_jsx(RunView, { lines: lines, status: status, prompt: prompt, verdict: verdict, onAnswer: answer }), !verdict && tearing ? _jsx(Spinner, { label: "tearing down\u2026 (stopping the run cleanly)" }) : null, !verdict && !tearing && !prompt ? _jsx(Spinner, { label: "running\u2026" }) : null] }));
99
103
  }
@@ -1,5 +1,5 @@
1
1
  import { type ReactElement } from "react";
2
- import type { DiscoveredOm } from "../../client/registry.ts";
2
+ import type { DiscoveredOm } from "../../../client/registry.ts";
3
3
  /** A searchable, arrow-navigable list of oms; Enter selects the highlighted one. */
4
4
  export declare function OmList({ oms, onSelect, }: {
5
5
  oms: DiscoveredOm[];
@@ -1,13 +1,15 @@
1
1
  import { type ReactElement } from "react";
2
- import type { PromptRequest, Verdict } from "../../client/types.ts";
2
+ import type { RunSummary } from "../../../output/RunModel.ts";
3
+ import type { PromptRequest, Verdict } from "../../../client/types.ts";
3
4
  /** Answer one pending prompt — free-text input, or searchable choices. */
4
5
  export declare function PromptView({ request, onAnswer, }: {
5
6
  request: PromptRequest;
6
7
  onAnswer: (value: string) => void;
7
8
  }): ReactElement;
8
- /** The live run surface: milestone lines, a pending prompt, and the final verdict. */
9
- export declare function RunView({ lines, prompt, verdict, onAnswer, }: {
10
- lines: string[];
9
+ /** The live run surface: milestone lines, a compact status strip, a pending prompt, and the verdict. */
10
+ export declare function RunView({ lines, status, prompt, verdict, onAnswer, }: {
11
+ lines: readonly string[];
12
+ status?: RunSummary;
11
13
  prompt?: PromptRequest;
12
14
  verdict?: Verdict;
13
15
  onAnswer: (value: string) => void;
@@ -35,7 +35,28 @@ function InputPrompt({ request, onAnswer, }) {
35
35
  export function PromptView({ request, onAnswer, }) {
36
36
  return request.spec.kind === "choice" ? (_jsx(ChoicePrompt, { request: request, onAnswer: onAnswer })) : (_jsx(InputPrompt, { request: request, onAnswer: onAnswer }));
37
37
  }
38
- /** The live run surface: milestone lines, a pending prompt, and the final verdict. */
39
- export function RunView({ lines, prompt, verdict, onAnswer, }) {
40
- return (_jsxs(Box, { flexDirection: "column", children: [lines.map((line, i) => (_jsx(Text, { children: line }, i))), prompt ? _jsx(PromptView, { request: prompt, onAnswer: onAnswer }) : null, verdict ? (_jsxs(Text, { color: verdict.ok ? "green" : "red", children: [verdict.ok ? "✓ ok" : "✗ failed", " \u00B7 ", verdict.folder] })) : null] }));
38
+ /** One-line roll-up of the live tree: what's still running and the finished tally. Empty until
39
+ * something is happening, so it renders nothing at the very start of a run. */
40
+ function statusStrip(s) {
41
+ const parts = [];
42
+ if (s.running.length) {
43
+ const shown = s.running.slice(0, 3).join(", ");
44
+ const more = s.running.length > 3 ? ` +${s.running.length - 3}` : "";
45
+ parts.push(`● running: ${shown}${more}`);
46
+ }
47
+ const tally = [];
48
+ if (s.ok)
49
+ tally.push(`${s.ok} ok`);
50
+ if (s.failed)
51
+ tally.push(`${s.failed} failed`);
52
+ if (s.cancelled)
53
+ tally.push(`${s.cancelled} cancelled`);
54
+ if (tally.length)
55
+ parts.push(tally.join(" · "));
56
+ return parts.join(" · ");
57
+ }
58
+ /** The live run surface: milestone lines, a compact status strip, a pending prompt, and the verdict. */
59
+ export function RunView({ lines, status, prompt, verdict, onAnswer, }) {
60
+ const strip = status && !verdict ? statusStrip(status) : "";
61
+ return (_jsxs(Box, { flexDirection: "column", children: [lines.map((line, i) => (_jsx(Text, { children: line }, i))), strip ? _jsx(Text, { dimColor: true, children: strip }) : null, prompt ? _jsx(PromptView, { request: prompt, onAnswer: onAnswer }) : null, verdict ? (_jsxs(Text, { color: verdict.ok ? "green" : "red", children: [verdict.ok ? "✓ ok" : "✗ failed", " \u00B7 ", verdict.folder] })) : null] }));
41
62
  }
@@ -1,4 +1,4 @@
1
- import type { ChildMessage, SupervisorMessage } from "../../core/interaction.ts";
1
+ import type { ChildMessage, SupervisorMessage } from "../core/interaction.ts";
2
2
  import type { RunSession } from "./types.ts";
3
3
  /** The minimal duplex the channel needs — satisfied by a real ChildProcess or a fake. */
4
4
  export interface Transport {
@@ -8,5 +8,5 @@ export interface OmkitConfig {
8
8
  /** When set, every run this client launches forks with the Node inspector open. */
9
9
  inspect?: InspectOptions;
10
10
  }
11
- /** Build the headless engine: discovery + supervised run + typecheck over one config. */
11
+ /** Build the headless engine: discovery + supervised/bare run + typecheck over one config. */
12
12
  export declare function createOmkitClient(config: OmkitConfig): OmkitClient;
@@ -0,0 +1,18 @@
1
+ import { discover } from "./discovery.js";
2
+ import { runOm, spawnBare } from "./runner.js";
3
+ import { runCheck } from "./check.js";
4
+ /** Build the headless engine: discovery + supervised/bare run + typecheck over one config. */
5
+ export function createOmkitClient(config) {
6
+ // A per-call inspect wins; the client's construction-time inspect is the fallback.
7
+ const withInspect = (opts) => ({
8
+ ...opts,
9
+ inspect: opts?.inspect ?? config.inspect,
10
+ });
11
+ return {
12
+ tsconfig: config.tsconfig,
13
+ discover: async () => discover(config.tsconfig),
14
+ run: (omFile, opts) => runOm(omFile, withInspect(opts)),
15
+ runBare: (omFile, opts) => spawnBare(omFile, withInspect(opts)),
16
+ check: async () => runCheck(config.tsconfig),
17
+ };
18
+ }
@@ -1,5 +1,5 @@
1
- import type { LogEntry } from "../../foundation/LogEntry.ts";
2
- import type { PromptSpec } from "../../core/interaction.ts";
1
+ import type { LogEntry } from "../foundation/LogEntry.ts";
2
+ import type { PromptSpec } from "../core/interaction.ts";
3
3
  import type { Registry } from "./registry.ts";
4
4
  /** A run's terminal verdict, resolved when the child settles. */
5
5
  export interface Verdict {
@@ -53,10 +53,14 @@ export interface RunSession {
53
53
  }
54
54
  /** The headless engine behind every omkit frontend. */
55
55
  export interface OmkitClient {
56
+ /** The `tsconfig.omkit.json` path this client scans and runs against — the single source of it. */
57
+ readonly tsconfig: string;
56
58
  /** Statically scan the project's `tsconfig.omkit.json` for oms and actions. */
57
59
  discover(): Promise<Registry>;
58
- /** Spawn an om file as a supervised child and return its live session. */
60
+ /** Spawn an om file as a supervised child and return its live session (the Ink UI drives this). */
59
61
  run(omFile: string, opts?: RunOptions): RunSession;
62
+ /** Spawn an om file bare — inherited stdio, no supervision — and resolve its exit code. */
63
+ runBare(omFile: string, opts?: RunOptions): Promise<number>;
60
64
  /** Typecheck the project (`tsc --noEmit`) and return diagnostics. */
61
65
  check(): Promise<Diagnostic[]>;
62
66
  }
@@ -9,11 +9,13 @@ export interface LiveTerminal {
9
9
  * retired: the interactive surface is the Ink app (`omkit ui`); this reporter is the plain
10
10
  * output for bare `omkit run`, CI, and pipes. Writes through the given terminal directly (for
11
11
  * `process.stdout`, `patch-console` doesn't intercept it, so it can't feed console capture).
12
+ *
13
+ * Folds the stream through a {@link RunModel} — the same reducer the interactive app uses — so the
14
+ * bare and supervised surfaces render a run identically (tags and all) rather than drifting apart.
12
15
  */
13
16
  export declare class LiveRenderer {
14
17
  private readonly term;
15
- /** Tags accumulated per node from `tag` entries, so milestones can show them by name. */
16
- private readonly tagsByNode;
18
+ private readonly model;
17
19
  constructor(term: LiveTerminal);
18
20
  run(store: LogStore): Promise<void>;
19
21
  }
@@ -1,30 +1,25 @@
1
- import { format } from "./milestones.js";
1
+ import { RunModel } from "./RunModel.js";
2
2
  /**
3
3
  * Appends a **curated** stream of milestones — action launches, completions, errors, events,
4
4
  * asserts, snapshots — one line each, as the run goes. The in-place status-line rendering is
5
5
  * retired: the interactive surface is the Ink app (`omkit ui`); this reporter is the plain
6
6
  * output for bare `omkit run`, CI, and pipes. Writes through the given terminal directly (for
7
7
  * `process.stdout`, `patch-console` doesn't intercept it, so it can't feed console capture).
8
+ *
9
+ * Folds the stream through a {@link RunModel} — the same reducer the interactive app uses — so the
10
+ * bare and supervised surfaces render a run identically (tags and all) rather than drifting apart.
8
11
  */
9
12
  export class LiveRenderer {
10
13
  term;
11
- /** Tags accumulated per node from `tag` entries, so milestones can show them by name. */
12
- tagsByNode = new Map();
14
+ model = new RunModel();
13
15
  constructor(term) {
14
16
  this.term = term;
15
17
  }
16
18
  async run(store) {
17
19
  for await (const entry of store.subscribe({ replay: true })) {
18
- if (entry.level === "tag" && entry.source === "tag") {
19
- const list = this.tagsByNode.get(entry.nodeId) ?? [];
20
- list.push(entry.message);
21
- this.tagsByNode.set(entry.nodeId, list);
22
- continue; // tags aren't their own line — they decorate the node's milestones
23
- }
24
- const rendered = format(entry, this.tagsByNode.get(entry.nodeId));
25
- if (rendered === null)
26
- continue;
27
- this.term.write(`${rendered.text}\n`);
20
+ const rendered = this.model.apply(entry);
21
+ if (rendered)
22
+ this.term.write(`${rendered.text}\n`);
28
23
  }
29
24
  }
30
25
  }
@@ -0,0 +1,41 @@
1
+ import type { LogEntry } from "../foundation/LogEntry.ts";
2
+ import { type Rendered } from "./milestones.ts";
3
+ export type RunNodeStatus = "running" | "ok" | "failed" | "cancelled";
4
+ /** One node of the live tree: where it is, how it's doing, and the tags it has picked up. */
5
+ export interface RunNode {
6
+ /** The node's execution path, e.g. `main/dev`. */
7
+ readonly path: string;
8
+ readonly status: RunNodeStatus;
9
+ readonly tags: readonly string[];
10
+ }
11
+ /** A compact roll-up of the live tree, for a one-line status strip. */
12
+ export interface RunSummary {
13
+ /** Paths of nodes still running, most-recently-active first. */
14
+ readonly running: readonly string[];
15
+ readonly ok: number;
16
+ readonly failed: number;
17
+ readonly cancelled: number;
18
+ }
19
+ /** How many milestone lines to keep. Older lines fall off the top so a long run can't grow without bound. */
20
+ export declare const MILESTONE_CAP = 500;
21
+ /**
22
+ * Folds a run's {@link LogEntry} stream into live, bounded state: a per-node status map (the tree)
23
+ * and a capped tail of milestone lines. One reducer shared by every frontend — the bare
24
+ * `LiveRenderer` and the interactive app both drive it — so "how a run looks" has one definition.
25
+ */
26
+ export declare class RunModel {
27
+ private readonly _nodes;
28
+ private readonly _milestones;
29
+ private seq;
30
+ /** Apply one entry: update node/tag state, append its milestone line (if any), and return it. */
31
+ apply(entry: LogEntry): Rendered | null;
32
+ /** The live tree: every node touched so far, with its current status and tags. */
33
+ nodes(): readonly RunNode[];
34
+ /** The capped tail of milestone lines, oldest first. */
35
+ milestones(): readonly string[];
36
+ /** A compact roll-up for a status strip: who's still running, and how many finished each way. */
37
+ summary(): RunSummary;
38
+ private touch;
39
+ private transition;
40
+ private push;
41
+ }
@@ -0,0 +1,86 @@
1
+ import { format } from "./milestones.js";
2
+ /** How many milestone lines to keep. Older lines fall off the top so a long run can't grow without bound. */
3
+ export const MILESTONE_CAP = 500;
4
+ /**
5
+ * Folds a run's {@link LogEntry} stream into live, bounded state: a per-node status map (the tree)
6
+ * and a capped tail of milestone lines. One reducer shared by every frontend — the bare
7
+ * `LiveRenderer` and the interactive app both drive it — so "how a run looks" has one definition.
8
+ */
9
+ export class RunModel {
10
+ _nodes = new Map();
11
+ _milestones = [];
12
+ seq = 0;
13
+ /** Apply one entry: update node/tag state, append its milestone line (if any), and return it. */
14
+ apply(entry) {
15
+ const node = this.touch(entry);
16
+ if (entry.level === "tag" && entry.source === "tag") {
17
+ node.tags.push(entry.message);
18
+ return null; // tags decorate a node's lines; they aren't a line of their own
19
+ }
20
+ this.transition(node, entry);
21
+ const rendered = format(entry, node.tags);
22
+ if (rendered)
23
+ this.push(rendered.text);
24
+ return rendered;
25
+ }
26
+ /** The live tree: every node touched so far, with its current status and tags. */
27
+ nodes() {
28
+ return [...this._nodes.values()].map((n) => ({
29
+ path: n.path,
30
+ status: n.status,
31
+ tags: [...n.tags],
32
+ }));
33
+ }
34
+ /** The capped tail of milestone lines, oldest first. */
35
+ milestones() {
36
+ return [...this._milestones];
37
+ }
38
+ /** A compact roll-up for a status strip: who's still running, and how many finished each way. */
39
+ summary() {
40
+ let ok = 0;
41
+ let failed = 0;
42
+ let cancelled = 0;
43
+ const running = [];
44
+ for (const n of this._nodes.values()) {
45
+ if (n.status === "ok")
46
+ ok++;
47
+ else if (n.status === "failed")
48
+ failed++;
49
+ else if (n.status === "cancelled")
50
+ cancelled++;
51
+ else
52
+ running.push(n);
53
+ }
54
+ running.sort((a, b) => b.order - a.order); // most-recently-active first
55
+ return { running: running.map((n) => n.path), ok, failed, cancelled };
56
+ }
57
+ touch(entry) {
58
+ const existing = this._nodes.get(entry.nodeId);
59
+ if (existing) {
60
+ existing.order = this.seq++; // most-recent activity wins the ordering
61
+ return existing;
62
+ }
63
+ const node = { path: entry.path, status: "running", tags: [], order: this.seq++ };
64
+ this._nodes.set(entry.nodeId, node);
65
+ return node;
66
+ }
67
+ transition(node, entry) {
68
+ if (entry.level === "event" && entry.source === "lifecycle") {
69
+ // A node reports `done · <status>` when it settles; "launched" leaves it running.
70
+ if (entry.message.startsWith("done · ")) {
71
+ node.status = statusOf(entry.message.slice("done · ".length));
72
+ }
73
+ }
74
+ else if (entry.level === "event" && entry.source === "cancel") {
75
+ node.status = "cancelled";
76
+ }
77
+ }
78
+ push(text) {
79
+ this._milestones.push(text);
80
+ if (this._milestones.length > MILESTONE_CAP)
81
+ this._milestones.shift();
82
+ }
83
+ }
84
+ function statusOf(s) {
85
+ return s === "ok" ? "ok" : s === "cancelled" ? "cancelled" : "failed";
86
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omkit",
3
- "version": "0.4.6",
3
+ "version": "0.4.8",
4
4
  "description": "A tiny runtime for developer workflows: typed actions that pass runtime capabilities and emit typed events, under one run and one shared log. The operational/runtime layer to tskb's knowledge layer.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,11 +0,0 @@
1
- import { discover } from "./discovery.js";
2
- import { runOm } from "./runner.js";
3
- import { runCheck } from "./check.js";
4
- /** Build the headless engine: discovery + supervised run + typecheck over one config. */
5
- export function createOmkitClient(config) {
6
- return {
7
- discover: async () => discover(config.tsconfig),
8
- run: (omFile, opts) => runOm(omFile, { ...opts, inspect: config.inspect }),
9
- check: async () => runCheck(config.tsconfig),
10
- };
11
- }
@@ -1,11 +0,0 @@
1
- import type { Diagnostic } from "../client/types.ts";
2
- import type { ReportItem } from "../ui/Report.tsx";
3
- /**
4
- * Turn typecheck diagnostics into a report: a headline, one item per diagnostic (location + message),
5
- * and the process exit code — **1 when there are any errors**, 0 when the project is clean.
6
- */
7
- export declare function checkReport(diagnostics: Diagnostic[]): {
8
- title: string;
9
- items: ReportItem[];
10
- code: number;
11
- };
@@ -1,14 +0,0 @@
1
- import path from "node:path";
2
- /**
3
- * Turn typecheck diagnostics into a report: a headline, one item per diagnostic (location + message),
4
- * and the process exit code — **1 when there are any errors**, 0 when the project is clean.
5
- */
6
- export function checkReport(diagnostics) {
7
- const n = diagnostics.length;
8
- const items = diagnostics.map((d) => ({
9
- head: `${path.basename(d.file)}:${d.line}`,
10
- detail: d.message,
11
- }));
12
- const title = n === 0 ? "no type errors" : `${n} type error${n === 1 ? "" : "s"}`;
13
- return { title, items, code: n === 0 ? 0 : 1 };
14
- }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes