omkit 0.4.0 → 0.4.2

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.
@@ -65,6 +65,14 @@ export const chromePage = action("chromePage")
65
65
  await page.goto(url);
66
66
  console.log(`navigated ${url}`);
67
67
  }
68
+ // Oms run under tsx, which forces esbuild `keepNames`: every function serialized into
69
+ // page.evaluate() carries a bare `__name(...)` wrapper that is undefined in the page and
70
+ // throws `ReferenceError: __name is not defined`. Define a no-op __name so those bodies
71
+ // run — seed the current document, and register it for any later navigation. Passed as a
72
+ // string so tsx doesn't rewrite (and re-break) the shim itself.
73
+ const nameShim = "globalThis.__name = globalThis.__name || function (f) { return f; };";
74
+ await page.evaluate(nameShim);
75
+ await page.addInitScript(nameShim);
68
76
  attach(page); // resolves instance.ref for every downstream action
69
77
  console.log(`attached ${from}`);
70
78
  // Daemon: hold the page open until teardown, then drop the CDP session if ours.
@@ -1,7 +1,8 @@
1
1
  import type { Registry } from "./registry.ts";
2
2
  /**
3
3
  * Statically scan the project described by `tsconfigPath` for runnable oms and inspectable
4
- * actions. Never throws for user-code problems — type errors become `warnings`, and whatever
5
- * parsed is still returned (so an editor-in-progress project stays useful).
4
+ * actions. Type errors in the user's oms/actions degrade to `warnings` (an editor-in-progress
5
+ * project stays useful), but a **fatal config problem** — a missing, unparseable, or invalid
6
+ * tsconfig — throws, so the CLI crashes visibly instead of silently returning an empty project.
6
7
  */
7
8
  export declare function discover(tsconfigPath: string): Registry;
@@ -2,22 +2,16 @@ import ts from "typescript";
2
2
  import path from "node:path";
3
3
  /**
4
4
  * Statically scan the project described by `tsconfigPath` for runnable oms and inspectable
5
- * actions. Never throws for user-code problems — type errors become `warnings`, and whatever
6
- * parsed is still returned (so an editor-in-progress project stays useful).
5
+ * actions. Type errors in the user's oms/actions degrade to `warnings` (an editor-in-progress
6
+ * project stays useful), but a **fatal config problem** — a missing, unparseable, or invalid
7
+ * tsconfig — throws, so the CLI crashes visibly instead of silently returning an empty project.
7
8
  */
8
9
  export function discover(tsconfigPath) {
9
10
  const oms = [];
10
11
  const actions = [];
11
12
  const warnings = [];
12
- const configFile = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
13
- if (configFile.error) {
14
- warnings.push(ts.flattenDiagnosticMessageText(configFile.error.messageText, "\n"));
15
- return { oms, actions, warnings };
16
- }
17
- // Resolve to an absolute base so `parsed.fileNames` are absolute. Otherwise root files stay
18
- // relative while imported files (e.g. an action pulled in by an om) resolve to absolute paths,
19
- // and the two never match in `fileSet`.
20
- const parsed = ts.parseJsonConfigFileContent(configFile.config, ts.sys, path.resolve(path.dirname(tsconfigPath)));
13
+ const { parsed, warnings: configWarnings } = loadProject(tsconfigPath);
14
+ warnings.push(...configWarnings);
21
15
  const program = ts.createProgram({
22
16
  rootNames: parsed.fileNames,
23
17
  options: { ...parsed.options, noEmit: true },
@@ -62,6 +56,28 @@ export function discover(tsconfigPath) {
62
56
  }
63
57
  return { oms, actions, warnings };
64
58
  }
59
+ /**
60
+ * Load and validate the tsconfig, throwing on a fatal config problem — a missing/unparseable file,
61
+ * or a config-level error (bad compilerOptions, an unresolvable `extends`). The benign "no inputs
62
+ * found" (TS18003) is not fatal: an empty-but-valid project comes back as a warning instead.
63
+ *
64
+ * Parses against an absolute base dir so `parsed.fileNames` are absolute — otherwise root files
65
+ * stay relative while imported files (an action pulled in by an om) resolve absolute, and the two
66
+ * never match in the caller's `fileSet`.
67
+ */
68
+ function loadProject(tsconfigPath) {
69
+ const flatten = (d) => ts.flattenDiagnosticMessageText(d.messageText, "\n");
70
+ const configFile = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
71
+ if (configFile.error)
72
+ throw new Error(`could not read ${tsconfigPath}: ${flatten(configFile.error)}`);
73
+ const parsed = ts.parseJsonConfigFileContent(configFile.config, ts.sys, path.resolve(path.dirname(tsconfigPath)));
74
+ const fatal = parsed.errors.filter((e) => e.category === ts.DiagnosticCategory.Error && e.code !== 18003);
75
+ if (fatal.length) {
76
+ throw new Error(`invalid ${tsconfigPath}:\n${fatal.map((e) => ` ${flatten(e)}`).join("\n")}`);
77
+ }
78
+ const warnings = parsed.errors.filter((e) => e.code === 18003).map(flatten);
79
+ return { parsed, warnings };
80
+ }
65
81
  /** Collect the local names bound to om/step/action from `import … from "omkit"`. */
66
82
  function omkitImports(sf) {
67
83
  const names = {};
@@ -1,6 +1,11 @@
1
1
  import type { Diagnostic } from "../client/types.ts";
2
- /** Format typecheck diagnostics into terminal text plus a process exit code. */
3
- export declare function formatDiagnostics(diagnostics: Diagnostic[]): {
4
- text: string;
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[];
5
10
  code: number;
6
11
  };
@@ -1,11 +1,14 @@
1
1
  import path from "node:path";
2
- /** Format typecheck diagnostics into terminal text plus a process exit code. */
3
- export function formatDiagnostics(diagnostics) {
4
- if (diagnostics.length === 0) {
5
- return { text: "no type errors", code: 0 };
6
- }
7
- const lines = diagnostics.map((d) => `${path.basename(d.file)}:${d.line} ${d.message}`);
8
- lines.push("");
9
- lines.push(`${diagnostics.length} error${diagnostics.length === 1 ? "" : "s"}`);
10
- return { text: lines.join("\n"), code: 1 };
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 };
11
14
  }
@@ -1,3 +1,11 @@
1
+ import type { Registry } from "../client/registry.ts";
2
+ /**
3
+ * Report an actionable "nothing to run" panel when discovery finds no oms. Names the config file
4
+ * omkit actually read (absolute, so the tsconfig.omkit.json-vs-tsconfig.json confusion goes away),
5
+ * lists any config warnings (e.g. TypeScript's "no inputs found" with the offending globs), and
6
+ * points at the usual cause: an `include` that doesn't reach the om files.
7
+ */
8
+ export declare function reportNoOms(tsconfig: string, registry: Registry): Promise<void>;
1
9
  /**
2
10
  * Resolve a `run` argument to an absolute om file: an existing file path (relative to `cwd`)
3
11
  * wins; otherwise match a discovered om by name. Returns undefined when nothing matches.
@@ -1,5 +1,22 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import { renderDiagnostics } from "../ui/Report.js";
4
+ /**
5
+ * Report an actionable "nothing to run" panel when discovery finds no oms. Names the config file
6
+ * omkit actually read (absolute, so the tsconfig.omkit.json-vs-tsconfig.json confusion goes away),
7
+ * lists any config warnings (e.g. TypeScript's "no inputs found" with the offending globs), and
8
+ * points at the usual cause: an `include` that doesn't reach the om files.
9
+ */
10
+ export function reportNoOms(tsconfig, registry) {
11
+ return renderDiagnostics({
12
+ kind: "warning",
13
+ title: "no oms found",
14
+ subtitle: `config: ${path.resolve(tsconfig)}`,
15
+ items: registry.warnings.map((w) => ({ head: "", detail: w })),
16
+ hint: "Check the config's `include` — it must reach your om files, and a glob only recurses " +
17
+ 'with `**` (e.g. "src/**/*.ts", not "src/*.ts").',
18
+ });
19
+ }
3
20
  /**
4
21
  * Resolve a `run` argument to an absolute om file: an existing file path (relative to `cwd`)
5
22
  * wins; otherwise match a discovered om by name. Returns undefined when nothing matches.
@@ -3,5 +3,10 @@ import type { OmkitClient } from "../client/index.ts";
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
5
5
  * unmounts (it erases its own frame), so the pointer to the on-disk record survives on screen.
6
+ *
7
+ * Discovery runs first, before Ink takes the terminal: a fatal config problem throws here and the
8
+ * bin's error boundary turns it into a clean crash rather than an empty list. With no oms to show,
9
+ * it reports why (config + warnings) instead of dropping into a blank picker. Non-fatal warnings
10
+ * are printed so they aren't lost behind the UI.
6
11
  */
7
- export declare function launchUi(client: OmkitClient): void;
12
+ export declare function launchUi(client: OmkitClient, tsconfig: string): Promise<void>;
@@ -1,14 +1,33 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { render } from "ink";
3
3
  import { App } from "../ui/app.js";
4
+ import { renderDiagnostics, withSpinner } from "../ui/Report.js";
5
+ import { reportNoOms } from "./run.js";
4
6
  /**
5
7
  * Render the interactive Ink app. Ink's own Ctrl+C handling is disabled so the app can tear the
6
8
  * run down gracefully first; the final verdict + run-folder path are printed here, after Ink
7
9
  * unmounts (it erases its own frame), so the pointer to the on-disk record survives on screen.
10
+ *
11
+ * Discovery runs first, before Ink takes the terminal: a fatal config problem throws here and the
12
+ * bin's error boundary turns it into a clean crash rather than an empty list. With no oms to show,
13
+ * it reports why (config + warnings) instead of dropping into a blank picker. Non-fatal warnings
14
+ * are printed so they aren't lost behind the UI.
8
15
  */
9
- export function launchUi(client) {
16
+ export async function launchUi(client, tsconfig) {
17
+ const registry = await withSpinner("discovering…", () => client.discover());
18
+ if (registry.oms.length === 0) {
19
+ await reportNoOms(tsconfig, registry);
20
+ return;
21
+ }
22
+ if (registry.warnings.length > 0) {
23
+ await renderDiagnostics({
24
+ kind: "warning",
25
+ title: `${registry.warnings.length} warning${registry.warnings.length === 1 ? "" : "s"}`,
26
+ items: registry.warnings.map((w) => ({ head: "", detail: w })),
27
+ });
28
+ }
10
29
  let finalVerdict;
11
- const app = render(_jsx(App, { client: client, onExit: (v) => (finalVerdict = v) }), {
30
+ const app = render(_jsx(App, { client: client, oms: registry.oms, onExit: (v) => (finalVerdict = v) }), {
12
31
  exitOnCtrlC: false,
13
32
  });
14
33
  void app.waitUntilExit().then(() => {
package/dist/cli/index.js CHANGED
@@ -36,13 +36,18 @@ async function main() {
36
36
  }
37
37
  if (cli.command === "ls") {
38
38
  const { formatRegistry } = await import("./commands/ls.js");
39
- console.log(formatRegistry(await client.discover(), { json: cli.json }));
39
+ const { withSpinner } = await import("./ui/Report.js");
40
+ const registry = await withSpinner("discovering…", () => client.discover());
41
+ console.log(formatRegistry(registry, { json: cli.json }));
40
42
  return;
41
43
  }
42
44
  if (cli.command === "check") {
43
- const { formatDiagnostics } = await import("./commands/check.js");
44
- const { text, code } = formatDiagnostics(await client.check());
45
- console.log(text);
45
+ const { checkReport } = await import("./commands/check.js");
46
+ const { renderDiagnostics, withSpinner } = await import("./ui/Report.js");
47
+ const diagnostics = await withSpinner("type-checking…", () => client.check());
48
+ const { title, items, code } = checkReport(diagnostics);
49
+ // Type errors fail the command (exit 1); a clean project reports ok and exits 0.
50
+ await renderDiagnostics({ kind: code === 0 ? "ok" : "error", title, items }, code === 0 ? process.stdout : process.stderr);
46
51
  process.exitCode = code;
47
52
  return;
48
53
  }
@@ -51,12 +56,18 @@ async function main() {
51
56
  // A bare `omkit` lands here too, since `run` is the default command.
52
57
  if (!cli.target) {
53
58
  const { launchUi } = await import("./commands/ui.js");
54
- launchUi(client);
59
+ await launchUi(client, cli.tsconfig);
55
60
  return;
56
61
  }
57
- const { resolveOm } = await import("./commands/run.js");
62
+ const { resolveOm, reportNoOms } = await import("./commands/run.js");
58
63
  const { spawnBare } = await import("./client/runner.js");
59
- const registry = await client.discover();
64
+ const { withSpinner } = await import("./ui/Report.js");
65
+ const registry = await withSpinner("discovering…", () => client.discover());
66
+ if (registry.oms.length === 0) {
67
+ await reportNoOms(cli.tsconfig, registry);
68
+ process.exitCode = 1;
69
+ return;
70
+ }
60
71
  const omFile = resolveOm(cli.target, registry, process.cwd());
61
72
  if (!omFile) {
62
73
  const known = registry.oms.map((o) => o.name).join(", ");
@@ -87,5 +98,33 @@ function isEntryPoint() {
87
98
  return false;
88
99
  }
89
100
  }
90
- if (isEntryPoint())
91
- void main();
101
+ /** Print a fatal error to stderr in one consistent shape, just before the process exits non-zero. */
102
+ function reportFatal(err) {
103
+ const detail = err instanceof Error ? (err.stack ?? err.message) : String(err);
104
+ process.stderr.write(`\nomkit: ${detail}\n`);
105
+ }
106
+ if (isEntryPoint()) {
107
+ // Nothing should fail silently. A fatal anywhere — a bad config, an uncaught throw, or a rejected
108
+ // promise in the interactive app — prints and crashes the CLI with a non-zero exit code.
109
+ //
110
+ // The controlled path (a rejection from `main`, e.g. a bad tsconfig) gets the pretty Ink panel;
111
+ // the last-resort sync handlers stay plain, since the process may be mid-crash and can't await.
112
+ process.on("uncaughtException", (err) => {
113
+ reportFatal(err);
114
+ process.exit(1);
115
+ });
116
+ process.on("unhandledRejection", (err) => {
117
+ reportFatal(err);
118
+ process.exit(1);
119
+ });
120
+ main().catch(async (err) => {
121
+ const { renderDiagnostics } = await import("./ui/Report.js");
122
+ const message = err instanceof Error ? err.message : String(err);
123
+ // Keep the panel clean by default (a bad tsconfig needs no stack); set OMKIT_DEBUG for the trace.
124
+ const stack = process.env.OMKIT_DEBUG && err instanceof Error && err.stack
125
+ ? err.stack.split("\n").slice(1).join("\n")
126
+ : undefined;
127
+ await renderDiagnostics({ kind: "error", title: message, hint: stack });
128
+ process.exit(1);
129
+ });
130
+ }
@@ -0,0 +1,40 @@
1
+ import { type ReactElement } from "react";
2
+ export type ReportKind = "error" | "warning" | "ok" | "info";
3
+ /** One row of a report: a colored `head` (e.g. a file:line location) and a dim `detail`. */
4
+ export interface ReportItem {
5
+ head: string;
6
+ detail?: string;
7
+ }
8
+ export interface ReportProps {
9
+ kind: ReportKind;
10
+ /** The bold, colored headline (e.g. "3 type errors"). */
11
+ title: string;
12
+ /** A dim line under the title — e.g. the config file a diagnostic came from. */
13
+ subtitle?: string;
14
+ /** The list of findings, shown under a separator. */
15
+ items?: ReportItem[];
16
+ /** A dim footer with next-step guidance. */
17
+ hint?: string;
18
+ }
19
+ /**
20
+ * A colorful, bordered diagnostics panel: an icon + title header, an optional subtitle, a separator,
21
+ * a list of items (colored location + dim detail), and an optional dim hint footer. Kind picks the
22
+ * accent color (error red / warning yellow / ok green / info cyan).
23
+ */
24
+ export declare function Report({ kind, title, subtitle, items, hint }: ReportProps): ReactElement;
25
+ /**
26
+ * Render a one-shot Ink element to `stream` (stderr by default) and resolve once it's painted.
27
+ * Paints a single frame then unmounts, leaving it in the scrollback — the right shape for
28
+ * non-interactive diagnostic output, unlike the live app.
29
+ */
30
+ export declare function renderReport(element: ReactElement, stream?: NodeJS.WriteStream): Promise<void>;
31
+ /** Build a {@link Report} from plain data and render it — so non-JSX call sites (the bin, run.ts)
32
+ * stay free of JSX while still using the Ink component. */
33
+ export declare function renderDiagnostics(props: ReportProps, stream?: NodeJS.WriteStream): Promise<void>;
34
+ /**
35
+ * Show a spinner labelled `label` while `work` runs, then clear it and resolve `work`'s result.
36
+ * Good for the noticeable-but-quiet waits (discovery, typecheck). Note: a CPU-bound synchronous
37
+ * `work` blocks the event loop, so the spinner shows its label but can't animate until `work`
38
+ * yields — it still tells the user something is happening instead of a silent hang.
39
+ */
40
+ export declare function withSpinner<T>(label: string, work: () => T | Promise<T>, stream?: NodeJS.WriteStream): Promise<T>;
@@ -0,0 +1,63 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import {} from "react";
3
+ import { Box, Text, render, useStdout } from "ink";
4
+ import { Spinner } from "./Spinner.js";
5
+ const THEME = {
6
+ error: { color: "red", icon: "✗" },
7
+ warning: { color: "yellow", icon: "⚠" },
8
+ ok: { color: "green", icon: "✓" },
9
+ info: { color: "cyan", icon: "ℹ" },
10
+ };
11
+ /** A horizontal rule sized to the terminal (bounded), used to separate the header from the list. */
12
+ function Divider({ color }) {
13
+ const { stdout } = useStdout();
14
+ const width = Math.max(Math.min((stdout?.columns ?? 80) - 6, 56), 12);
15
+ return (_jsx(Text, { color: color, dimColor: true, children: "─".repeat(width) }));
16
+ }
17
+ /**
18
+ * A colorful, bordered diagnostics panel: an icon + title header, an optional subtitle, a separator,
19
+ * a list of items (colored location + dim detail), and an optional dim hint footer. Kind picks the
20
+ * accent color (error red / warning yellow / ok green / info cyan).
21
+ */
22
+ export function Report({ kind, title, subtitle, items = [], hint }) {
23
+ const { color, icon } = THEME[kind];
24
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: color, paddingX: 1, children: [_jsxs(Text, { color: color, bold: true, children: [icon, " ", title] }), subtitle ? _jsx(Text, { dimColor: true, children: subtitle }) : null, items.length > 0 ? (_jsxs(_Fragment, { children: [_jsx(Divider, { color: color }), items.map((it, i) => (_jsxs(Text, { children: [it.head ? _jsx(Text, { color: color, children: it.head }) : null, it.detail ? _jsx(Text, { dimColor: true, children: (it.head ? " " : "") + it.detail }) : null] }, i)))] })) : null, hint ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: hint }) })) : null] }));
25
+ }
26
+ /**
27
+ * Render a one-shot Ink element to `stream` (stderr by default) and resolve once it's painted.
28
+ * Paints a single frame then unmounts, leaving it in the scrollback — the right shape for
29
+ * non-interactive diagnostic output, unlike the live app.
30
+ */
31
+ export async function renderReport(element, stream = process.stderr) {
32
+ const app = render(element, { stdout: stream, patchConsole: false });
33
+ await new Promise((resolve) => setImmediate(resolve)); // let the frame paint once
34
+ app.unmount();
35
+ await app.waitUntilExit();
36
+ }
37
+ /** Build a {@link Report} from plain data and render it — so non-JSX call sites (the bin, run.ts)
38
+ * stay free of JSX while still using the Ink component. */
39
+ export function renderDiagnostics(props, stream = process.stderr) {
40
+ return renderReport(_jsx(Report, { ...props }), stream);
41
+ }
42
+ /**
43
+ * Show a spinner labelled `label` while `work` runs, then clear it and resolve `work`'s result.
44
+ * Good for the noticeable-but-quiet waits (discovery, typecheck). Note: a CPU-bound synchronous
45
+ * `work` blocks the event loop, so the spinner shows its label but can't animate until `work`
46
+ * yields — it still tells the user something is happening instead of a silent hang.
47
+ */
48
+ export async function withSpinner(label, work, stream = process.stderr) {
49
+ // No TTY (piped/CI) → a spinner can't animate or erase itself; skip it and just do the work,
50
+ // so the output stays clean rather than leaving a stray "…" line above the result.
51
+ if (!stream.isTTY)
52
+ return work();
53
+ const app = render(_jsx(Spinner, { label: label }), { stdout: stream, patchConsole: false });
54
+ try {
55
+ await new Promise((resolve) => setImmediate(resolve)); // let the first frame paint
56
+ return await work();
57
+ }
58
+ finally {
59
+ app.clear(); // erase the spinner frame (unmount alone leaves the last frame on screen)
60
+ app.unmount();
61
+ await app.waitUntilExit();
62
+ }
63
+ }
@@ -1,9 +1,12 @@
1
1
  import { type ReactElement } from "react";
2
2
  import type { OmkitClient, Verdict } from "../client/types.ts";
3
+ import type { DiscoveredOm } from "../client/registry.ts";
3
4
  /** The interactive app: discover → list/search → run → live milestones + prompts. */
4
- export declare function App({ client, onExit, }: {
5
+ export declare function App({ client, onExit, oms: initialOms, }: {
5
6
  client: OmkitClient;
6
7
  /** Called with the run's verdict (if any) just before Ink unmounts, so the caller can print
7
8
  * a durable summary — Ink erases its own frame on exit. */
8
9
  onExit?: (verdict: Verdict | undefined) => void;
10
+ /** Oms discovered by the caller (pre-flight). When given, the app skips its own discovery. */
11
+ oms?: DiscoveredOm[];
9
12
  }): ReactElement;
@@ -6,9 +6,9 @@ import { OmList } from "./views/OmList.js";
6
6
  import { RunView } from "./views/RunView.js";
7
7
  import { Spinner } from "./Spinner.js";
8
8
  /** The interactive app: discover → list/search → run → live milestones + prompts. */
9
- export function App({ client, onExit, }) {
9
+ export function App({ client, onExit, oms: initialOms, }) {
10
10
  const { exit } = useApp();
11
- const [oms, setOms] = useState([]);
11
+ const [oms, setOms] = useState(initialOms ?? []);
12
12
  const [session, setSession] = useState(null);
13
13
  const [lines, setLines] = useState([]);
14
14
  const [prompt, setPrompt] = useState();
@@ -19,6 +19,8 @@ export function App({ client, onExit, }) {
19
19
  const verdictRef = useRef(undefined);
20
20
  const tearingRef = useRef(false);
21
21
  useEffect(() => {
22
+ if (initialOms)
23
+ return; // provided by the caller's pre-flight discovery
22
24
  let live = true;
23
25
  void client.discover().then((r) => {
24
26
  if (live)
@@ -27,7 +29,7 @@ export function App({ client, onExit, }) {
27
29
  return () => {
28
30
  live = false;
29
31
  };
30
- }, [client]);
32
+ }, [client, initialOms]);
31
33
  const run = (om) => {
32
34
  const s = client.run(om.file);
33
35
  sessionRef.current = s;
@@ -131,8 +131,13 @@ export class ActionRun {
131
131
  if (win())
132
132
  resolve(undefined);
133
133
  }, (error) => {
134
- if (win())
135
- key === "error" ? resolve(error) : reject(error);
134
+ if (!win())
135
+ return;
136
+ // Awaiting the `error` event hands back the error; any other gate rejects on failure.
137
+ if (key === "error")
138
+ resolve(error);
139
+ else
140
+ reject(error);
136
141
  });
137
142
  });
138
143
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omkit",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
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",