omkit 0.4.4 → 0.4.6

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.
@@ -7,8 +7,8 @@ export interface Transport {
7
7
  onClose(handler: (code: number | null) => void): void;
8
8
  }
9
9
  /**
10
- * Turn a child transport into a {@link RunSession}: fan `log`/`prompt` child messages out to
11
- * handlers, resolve `result` on `settled`, and map `answer`/`cancel` back down the wire. A
12
- * close before `settled` resolves a failed verdict so `result` never hangs.
10
+ * Turn a child transport into a {@link RunSession}: fan `log`/`prompt`/`prompt-done` child
11
+ * messages out to handlers, resolve `result` on `settled`, and map `answer`/`cancel` back down
12
+ * the wire. A close before `settled` resolves a failed verdict so `result` never hangs.
13
13
  */
14
14
  export declare function createChannel(transport: Transport): RunSession;
@@ -1,12 +1,13 @@
1
1
  /**
2
- * Turn a child transport into a {@link RunSession}: fan `log`/`prompt` child messages out to
3
- * handlers, resolve `result` on `settled`, and map `answer`/`cancel` back down the wire. A
4
- * close before `settled` resolves a failed verdict so `result` never hangs.
2
+ * Turn a child transport into a {@link RunSession}: fan `log`/`prompt`/`prompt-done` child
3
+ * messages out to handlers, resolve `result` on `settled`, and map `answer`/`cancel` back down
4
+ * the wire. A close before `settled` resolves a failed verdict so `result` never hangs.
5
5
  */
6
6
  export function createChannel(transport) {
7
7
  const handlers = {
8
8
  log: [],
9
9
  prompt: [],
10
+ promptDone: [],
10
11
  settled: [],
11
12
  };
12
13
  let settled = false;
@@ -21,6 +22,10 @@ export function createChannel(transport) {
21
22
  for (const h of handlers.prompt)
22
23
  h({ id: message.id, spec: message.spec });
23
24
  }
25
+ else if (message.kind === "prompt-done") {
26
+ for (const h of handlers.promptDone)
27
+ h(message.id);
28
+ }
24
29
  else if (message.kind === "settled") {
25
30
  settled = true;
26
31
  const verdict = { ok: message.ok, folder: message.folder, summary: message.summary };
@@ -1,9 +1,12 @@
1
1
  import type { OmkitClient } from "./types.ts";
2
- export type { OmkitClient, RunSession, RunOptions, Verdict, Diagnostic, PromptRequest, RunEvents, } from "./types.ts";
2
+ export type { OmkitClient, RunSession, RunOptions, InspectOptions, Verdict, Diagnostic, PromptRequest, RunEvents, } from "./types.ts";
3
3
  export type { Registry, DiscoveredOm, DiscoveredAction } from "./registry.ts";
4
- /** Configuration for a client — the path to the project's `tsconfig.omkit.json`. */
4
+ import type { InspectOptions } from "./types.ts";
5
+ /** Configuration for a client — the project's `tsconfig.omkit.json` and optional debugging. */
5
6
  export interface OmkitConfig {
6
7
  tsconfig: string;
8
+ /** When set, every run this client launches forks with the Node inspector open. */
9
+ inspect?: InspectOptions;
7
10
  }
8
11
  /** Build the headless engine: discovery + supervised run + typecheck over one config. */
9
12
  export declare function createOmkitClient(config: OmkitConfig): OmkitClient;
@@ -5,7 +5,7 @@ import { runCheck } from "./check.js";
5
5
  export function createOmkitClient(config) {
6
6
  return {
7
7
  discover: async () => discover(config.tsconfig),
8
- run: (omFile, opts) => runOm(omFile, opts),
8
+ run: (omFile, opts) => runOm(omFile, { ...opts, inspect: config.inspect }),
9
9
  check: async () => runCheck(config.tsconfig),
10
10
  };
11
11
  }
@@ -1,4 +1,10 @@
1
- import type { RunSession, RunOptions } from "./types.ts";
1
+ import type { RunSession, RunOptions, InspectOptions } from "./types.ts";
2
+ /**
3
+ * The inspector flag to prepend to a child's `execArgv` — empty when no inspector is requested.
4
+ * Plain `--inspect` opens the port without stopping, so the child still installs its teardown
5
+ * handlers and settles normally.
6
+ */
7
+ export declare function inspectArgs(inspect: InspectOptions | undefined): string[];
2
8
  /**
3
9
  * Fork `omFile` as a supervised child: the `tsx` loader runs the TypeScript directly,
4
10
  * `OMKIT_SUPERVISED=1` flips the child into channel mode (see core/interaction.ts), and its
@@ -10,6 +16,4 @@ export declare function runOm(omFile: string, opts?: RunOptions): RunSession;
10
16
  * Spawn `omFile` **bare** — inherited stdio, no supervision. The child owns the terminal, so
11
17
  * its LiveRenderer, summary, and native `readline` prompts all work. Resolves the exit code.
12
18
  */
13
- export declare function spawnBare(omFile: string, opts?: {
14
- cwd?: string;
15
- }): Promise<number>;
19
+ export declare function spawnBare(omFile: string, opts?: RunOptions): Promise<number>;
@@ -7,6 +7,16 @@ import { createChannel } from "./channel.js";
7
7
  * a run in any directory still finds the loader shipped with omkit.
8
8
  */
9
9
  const tsxLoader = import.meta.resolve("tsx");
10
+ /**
11
+ * The inspector flag to prepend to a child's `execArgv` — empty when no inspector is requested.
12
+ * Plain `--inspect` opens the port without stopping, so the child still installs its teardown
13
+ * handlers and settles normally.
14
+ */
15
+ export function inspectArgs(inspect) {
16
+ if (!inspect)
17
+ return [];
18
+ return [`--inspect=${inspect.port}`];
19
+ }
10
20
  /**
11
21
  * Fork `omFile` as a supervised child: the `tsx` loader runs the TypeScript directly,
12
22
  * `OMKIT_SUPERVISED=1` flips the child into channel mode (see core/interaction.ts), and its
@@ -15,7 +25,7 @@ const tsxLoader = import.meta.resolve("tsx");
15
25
  */
16
26
  export function runOm(omFile, opts = {}) {
17
27
  const child = fork(omFile, [], {
18
- execArgv: ["--import", tsxLoader],
28
+ execArgv: [...inspectArgs(opts.inspect), "--import", tsxLoader],
19
29
  cwd: opts.cwd ?? path.dirname(omFile),
20
30
  env: { ...process.env, OMKIT_SUPERVISED: "1" },
21
31
  stdio: ["ignore", "pipe", "pipe", "ipc"],
@@ -50,7 +60,7 @@ export function spawnBare(omFile, opts = {}) {
50
60
  const env = { ...process.env };
51
61
  delete env.OMKIT_SUPERVISED;
52
62
  const child = fork(omFile, [], {
53
- execArgv: ["--import", tsxLoader],
63
+ execArgv: [...inspectArgs(opts.inspect), "--import", tsxLoader],
54
64
  cwd: opts.cwd ?? path.dirname(omFile),
55
65
  stdio: "inherit",
56
66
  env,
@@ -16,10 +16,17 @@ export interface Diagnostic {
16
16
  readonly line: number;
17
17
  readonly message: string;
18
18
  }
19
+ /** Node inspector wiring for a run's forked child, so a debugger can attach to the om. */
20
+ export interface InspectOptions {
21
+ /** Inspector port the child listens on. */
22
+ readonly port: number;
23
+ }
19
24
  /** Options for launching a run. */
20
25
  export interface RunOptions {
21
26
  /** Working directory for the child process (defaults to the om file's directory). */
22
27
  readonly cwd?: string;
28
+ /** When set, fork the child with the Node inspector open so a debugger can attach. */
29
+ readonly inspect?: InspectOptions;
23
30
  }
24
31
  /** A prompt the running om is waiting on — surfaced to the frontend, answered via `answer`. */
25
32
  export interface PromptRequest {
@@ -30,6 +37,8 @@ export interface PromptRequest {
30
37
  export interface RunEvents {
31
38
  log: (entry: LogEntry) => void;
32
39
  prompt: (request: PromptRequest) => void;
40
+ /** A pending prompt was withdrawn by the child (timed out or torn down) — clear it from the UI. */
41
+ promptDone: (id: string) => void;
33
42
  settled: (verdict: Verdict) => void;
34
43
  }
35
44
  /** A live handle to one supervised run. */
@@ -1,9 +1,12 @@
1
1
  #!/usr/bin/env node
2
+ import { type InspectOptions } from "./client/index.ts";
2
3
  export interface Cli {
3
4
  command: string;
4
5
  target?: string;
5
6
  json: boolean;
6
7
  tsconfig: string;
8
+ /** The Node inspector, if requested via `--inspect[=port]`. */
9
+ inspect?: InspectOptions;
7
10
  }
8
11
  /** Parse argv (without node/script) into a command, an optional target, and flags. */
9
12
  export declare function parseCli(argv: string[]): Cli;
package/dist/cli/index.js CHANGED
@@ -4,10 +4,31 @@ import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { realpathSync } from "node:fs";
6
6
  import { createOmkitClient } from "./client/index.js";
7
+ /**
8
+ * Pull the Node-style inspector flag out of argv, returning the options plus the remaining args.
9
+ * Matches Node's own syntax: `--inspect`, `--inspect=PORT` (default port 9229); an explicit port
10
+ * always wins.
11
+ */
12
+ function extractInspect(argv) {
13
+ let inspect;
14
+ const rest = [];
15
+ for (const arg of argv) {
16
+ const m = /^--inspect(?:=(\d+))?$/.exec(arg);
17
+ if (!m) {
18
+ rest.push(arg);
19
+ continue;
20
+ }
21
+ const port = m[1] ? Number(m[1]) : (inspect?.port ?? 9229);
22
+ inspect = { port };
23
+ }
24
+ return { inspect, rest };
25
+ }
7
26
  /** Parse argv (without node/script) into a command, an optional target, and flags. */
8
27
  export function parseCli(argv) {
28
+ // Inspector flags take an optional `=PORT`, which `parseArgs` can't express — pull them first.
29
+ const { inspect, rest } = extractInspect(argv);
9
30
  const { values, positionals } = parseArgs({
10
- args: argv,
31
+ args: rest,
11
32
  options: {
12
33
  json: { type: "boolean", default: false },
13
34
  tsconfig: { type: "string", default: "tsconfig.omkit.json" },
@@ -19,12 +40,24 @@ export function parseCli(argv) {
19
40
  target: positionals[1],
20
41
  json: Boolean(values.json),
21
42
  tsconfig: values.tsconfig,
43
+ inspect,
22
44
  };
23
45
  }
46
+ /**
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.
50
+ */
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).`);
55
+ }
24
56
  /** The bin entry: route to a lazily-imported command; owns stdout and the exit code. */
25
57
  async function main() {
26
58
  const cli = parseCli(process.argv.slice(2));
27
- const client = createOmkitClient({ tsconfig: cli.tsconfig });
59
+ const inspect = cli.inspect;
60
+ const client = createOmkitClient({ tsconfig: cli.tsconfig, inspect });
28
61
  if (cli.command === "init") {
29
62
  const { scaffold } = await import("./commands/init.js");
30
63
  const { created, skipped } = scaffold(process.cwd());
@@ -55,6 +88,10 @@ async function main() {
55
88
  // No target → open the interactive picker (browse/search oms, run one with live output).
56
89
  // A bare `omkit` lands here too, since `run` is the default command.
57
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);
58
95
  const { launchUi } = await import("./commands/ui.js");
59
96
  await launchUi(client, cli.tsconfig);
60
97
  return;
@@ -75,8 +112,10 @@ async function main() {
75
112
  process.exitCode = 1;
76
113
  return;
77
114
  }
115
+ if (inspect)
116
+ announceInspect(inspect, false);
78
117
  process.on("SIGINT", () => { }); // let the child tear down; don't die first
79
- process.exitCode = await spawnBare(omFile, { cwd: path.dirname(omFile) });
118
+ process.exitCode = await spawnBare(omFile, { cwd: path.dirname(omFile), inspect });
80
119
  return;
81
120
  }
82
121
  console.error(`unknown command "${cli.command}"`);
@@ -39,6 +39,10 @@ export function App({ client, onExit, oms: initialOms, }) {
39
39
  setLines((prev) => [...prev, rendered.text]);
40
40
  });
41
41
  s.on("prompt", (req) => setPrompt(req));
42
+ // The child gave up on the prompt (timed out or torn down). Withdraw its box so a stale,
43
+ // unanswerable prompt doesn't linger — but only if it's still the one on screen, so a late
44
+ // withdrawal can't clobber a fresh prompt.
45
+ s.on("promptDone", (id) => setPrompt((p) => (p?.id === id ? undefined : p)));
42
46
  s.on("settled", (v) => {
43
47
  setVerdict(v);
44
48
  setPrompt(undefined);
@@ -17,6 +17,9 @@ export type ChildMessage = {
17
17
  kind: "prompt";
18
18
  id: string;
19
19
  spec: PromptSpec;
20
+ } | {
21
+ kind: "prompt-done";
22
+ id: string;
20
23
  } | {
21
24
  kind: "log";
22
25
  entry: LogEntry;
@@ -29,6 +29,10 @@ export class Supervisor {
29
29
  return new Promise((resolve, reject) => {
30
30
  const onAbort = () => {
31
31
  this.pending.delete(id);
32
+ // The child is giving up on this prompt (timeout or teardown) and moving on. Tell the
33
+ // supervisor so it withdraws the prompt from its UI — otherwise a stale, unanswerable
34
+ // prompt box lingers on screen for the rest of the run.
35
+ this.send({ kind: "prompt-done", id });
32
36
  reject(new Error("prompt aborted"));
33
37
  };
34
38
  if (signal.aborted)
@@ -58,10 +58,11 @@ export interface OmContext {
58
58
  }
59
59
  /**
60
60
  * A launched action: the live node's awaitable/observable surface, returned by
61
- * **invoking an {@link Action}**. `.result` always resolves an {@link Outcome} (never
62
- * throws); `.ref` rejects on failure/cancel. The body is committed one microtask after
63
- * the launching call, so pre-body config (`withCache`) chained before the first `await`
64
- * still applies.
61
+ * **invoking an {@link Action}**. `.result` (like `.ref` and `once`) resolves the value on
62
+ * success and **rejects** with the action's error on failure (or `CancelledError` on cancel);
63
+ * reading it observes the activity, so a handled failure won't tear the run down. The body is
64
+ * committed one microtask after the launching call, so pre-body config (`withCache`) chained
65
+ * before the first `await` still applies.
65
66
  */
66
67
  export interface Activity<Result = unknown, Events extends object = NoEvents, Handle = void> {
67
68
  readonly id: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omkit",
3
- "version": "0.4.4",
3
+ "version": "0.4.6",
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",