omkit 0.0.11 → 0.0.12

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.
@@ -33,12 +33,16 @@ export interface ChromePageOptions {
33
33
  * from whatever {@link ChromePageSource} is given — a CDP endpoint to connect to,
34
34
  * or an already-owned Playwright handle (an Electron `BrowserWindow` page, a
35
35
  * `BrowserContext`, or a `Browser`). The source may be a promise, so it wires
36
- * straight from another action's `.ref` (e.g. `chromePage(chromedriver.ref)` or
37
- * `chromePage(electronApp.ref)`).
36
+ * straight from another action's `.ref` (e.g. `chromePage("Explorer",
37
+ * chromedriver.ref)` or `chromePage("App", electronApp.ref)`).
38
38
  *
39
39
  * Daemon-shaped like the watch actions: acquire the page, attach it, then stay
40
40
  * alive on a promise that only resolves when the run's `signal` aborts. On
41
41
  * teardown it closes the CDP connection *only* when it opened one — a handed-in
42
42
  * browser or window is left untouched for its owner to close.
43
+ *
44
+ * `label` names this page in the log (its `source`), so several `chromePage`
45
+ * instances in one run stay distinguishable — e.g. `chromePage("Explorer",
46
+ * chrome.ref)` logs its lines under `Explorer` rather than a shared default.
43
47
  */
44
- export declare const chromePage: import("../index.ts").Action<[source: ChromePageSource | Promise<ChromePageSource>, opts?: ChromePageOptions | undefined], void, ChromePageEvents, Page>;
48
+ export declare const chromePage: import("../index.ts").Action<[label: string, source: ChromePageSource | Promise<ChromePageSource>, opts?: ChromePageOptions | undefined], void, ChromePageEvents, Page>;
@@ -13,18 +13,22 @@ const pageOf = (context, fresh) => fresh ? context.newPage() : (context.pages()[
13
13
  * from whatever {@link ChromePageSource} is given — a CDP endpoint to connect to,
14
14
  * or an already-owned Playwright handle (an Electron `BrowserWindow` page, a
15
15
  * `BrowserContext`, or a `Browser`). The source may be a promise, so it wires
16
- * straight from another action's `.ref` (e.g. `chromePage(chromedriver.ref)` or
17
- * `chromePage(electronApp.ref)`).
16
+ * straight from another action's `.ref` (e.g. `chromePage("Explorer",
17
+ * chromedriver.ref)` or `chromePage("App", electronApp.ref)`).
18
18
  *
19
19
  * Daemon-shaped like the watch actions: acquire the page, attach it, then stay
20
20
  * alive on a promise that only resolves when the run's `signal` aborts. On
21
21
  * teardown it closes the CDP connection *only* when it opened one — a handed-in
22
22
  * browser or window is left untouched for its owner to close.
23
+ *
24
+ * `label` names this page in the log (its `source`), so several `chromePage`
25
+ * instances in one run stay distinguishable — e.g. `chromePage("Explorer",
26
+ * chrome.ref)` logs its lines under `Explorer` rather than a shared default.
23
27
  */
24
28
  export const chromePage = action("Chrome Page")
25
29
  .emits()
26
30
  .ref()
27
- .run(async ({ logs, signal, emit, attach }, source, opts = {}) => {
31
+ .run(async ({ logs, signal, emit, attach }, label, source, opts = {}) => {
28
32
  const { url } = opts;
29
33
  const resolved = await source;
30
34
  // Resolve the source to a Page, recording what (if anything) we own. Only a
@@ -60,11 +64,11 @@ export const chromePage = action("Chrome Page")
60
64
  page = await pageOf(resolved, url !== undefined);
61
65
  }
62
66
  page.on("console", (msg) => {
63
- logs.append({ source: "Chrome Page", level: "info", message: `console: ${msg.text()}` });
67
+ logs.append({ source: label, level: "info", message: `console: ${msg.text()}` });
64
68
  emit("console", msg.text());
65
69
  });
66
70
  page.on("pageerror", (err) => {
67
- logs.append({ source: "Chrome Page", level: "error", message: err.message });
71
+ logs.append({ source: label, level: "error", message: err.message });
68
72
  emit("pageerror", err.message);
69
73
  });
70
74
  page.on("framenavigated", (frame) => {
@@ -73,10 +77,10 @@ export const chromePage = action("Chrome Page")
73
77
  });
74
78
  if (url) {
75
79
  await page.goto(url);
76
- logs.append({ source: "Chrome Page", level: "info", message: `navigated ${url}` });
80
+ logs.append({ source: label, level: "info", message: `navigated ${url}` });
77
81
  }
78
82
  attach(page); // resolves instance.ref for every downstream action
79
- logs.append({ source: "Chrome Page", level: "info", message: `attached ${from}` });
83
+ logs.append({ source: label, level: "info", message: `attached ${from}` });
80
84
  // Daemon: hold the page open until teardown, then drop the CDP session if it
81
85
  // was ours; never close a handed-in browser/window.
82
86
  return new Promise((resolveRun) => {
package/dist/index.d.ts CHANGED
@@ -17,7 +17,7 @@ export type { Emitter, EventHandler, EventMeta } from "./orchestration/events/ev
17
17
  export { createProc } from "./system/process/process.ts";
18
18
  export type { Proc } from "./system/process/process.ts";
19
19
  export { LogsCollector } from "./output/log/LogsCollector.ts";
20
- export type { Logger, LogEntry, LogInput, SubscribeOptions, } from "./output/log/LogsCollector.ts";
20
+ export type { Logger, LogEntry, LogInput, SubscribeOptions } from "./output/log/LogsCollector.ts";
21
21
  export { Output } from "./output/index.ts";
22
22
  export type { SnapshotRef } from "./output/index.ts";
23
23
  export { FolderCache } from "./system/fs/FolderCache.ts";
@@ -113,7 +113,7 @@ export interface ActionInstance<Result = unknown, Events extends object = NoEven
113
113
  readonly done: Promise<Outcome<Result>>;
114
114
  /**
115
115
  * Resolves with the handle the action attached via `ctx.attach` (set once).
116
- * Used to thread a handle into another action (`chromePage(driver.ref)`). If
116
+ * Used to thread a handle into another action (`chromePage("Explorer", driver.ref)`). If
117
117
  * the action settles *without* attaching, this resolves (`undefined` for the
118
118
  * default `void` handle) on success and **rejects on failure** — so, unlike
119
119
  * `done`, awaiting `ref` in a body can throw. It's meant to be consumed as a
@@ -32,6 +32,10 @@ export declare class SpinHost {
32
32
  readonly signal: AbortSignal;
33
33
  /** Settles once the spin reaches `closed`, carrying the terminal verdict. Never rejects. */
34
34
  readonly done: Promise<SpinResult>;
35
+ /** The original `console.log`, captured before patching — the run's terminal writer. */
36
+ private readonly term;
37
+ /** Reinstalls the pre-run console methods; called once at finalize. */
38
+ private readonly restoreConsole;
35
39
  private readonly controller;
36
40
  private phase;
37
41
  /** The current lifecycle state — see {@link SpinState}. */
@@ -2,6 +2,7 @@ import { relative } from "node:path";
2
2
  import { producerOfRef } from "../action/action.js";
3
3
  import { LogsCollector, ScopedLogger } from "../../output/log/LogsCollector.js";
4
4
  import { LogRenderer } from "../../output/log/LogRenderer.js";
5
+ import { actionScope, captureConsole } from "../../output/log/console-capture.js";
5
6
  import { Output } from "../../output/Output.js";
6
7
  /** The entry script, relative to where node was invoked (so the log says what produced it). */
7
8
  const scriptPath = () => {
@@ -58,6 +59,10 @@ export class SpinHost {
58
59
  signal;
59
60
  /** Settles once the spin reaches `closed`, carrying the terminal verdict. Never rejects. */
60
61
  done;
62
+ /** The original `console.log`, captured before patching — the run's terminal writer. */
63
+ term;
64
+ /** Reinstalls the pre-run console methods; called once at finalize. */
65
+ restoreConsole;
61
66
  controller = new AbortController();
62
67
  // ── Lifecycle state ──────────────────────────────────────────────────────
63
68
  // The single source of truth for "where in its life is this run". Only
@@ -89,6 +94,12 @@ export class SpinHost {
89
94
  for (const instance of actions)
90
95
  assertInstance(instance);
91
96
  this.signal = this.controller.signal;
97
+ // Capture the ORIGINAL console.log as this run's terminal writer BEFORE patching,
98
+ // or `drain`/finalize would re-enter the patched method and feedback-loop. Then
99
+ // patch console so every console.* during the run folds into this run's log,
100
+ // attributed via `actionScope` (see `launch`).
101
+ this.term = console.log.bind(console);
102
+ this.restoreConsole = captureConsole(this.logs);
92
103
  process.once("SIGINT", this.onSigint);
93
104
  process.on("uncaughtException", this.onUncaught);
94
105
  process.on("unhandledRejection", this.onUnhandled);
@@ -150,7 +161,7 @@ export class SpinHost {
150
161
  void (async () => {
151
162
  const renderer = new LogRenderer();
152
163
  for await (const entry of this.logs.subscribe({ replay: true }))
153
- console.log(renderer.render(entry));
164
+ this.term(renderer.render(entry));
154
165
  })().catch(() => { });
155
166
  }
156
167
  /** Narrate a run-lifecycle milestone into this run's log (renders as a `[run]` line). */
@@ -206,7 +217,7 @@ export class SpinHost {
206
217
  };
207
218
  }
208
219
  launch(instance, parentPath) {
209
- // A consumer built with another action's `.ref` (e.g. `chromePage(driver.ref)`)
220
+ // A consumer built with another action's `.ref` (e.g. `chromePage("Explorer", driver.ref)`)
210
221
  // must be launched *after* that producer — otherwise it awaits a handle that
211
222
  // never arrives and the spin hangs silently. Catch it and say how to fix it.
212
223
  for (const arg of instance.args) {
@@ -247,8 +258,8 @@ export class SpinHost {
247
258
  if (!this.controller.signal.aborted)
248
259
  this.recordFailure(path, error);
249
260
  };
250
- const promise = instance
251
- .start({ logs: scoped, signal: this.controller.signal, nod, output: this.output })
261
+ const promise = actionScope
262
+ .run(path, () => instance.start({ logs: scoped, signal: this.controller.signal, nod, output: this.output }))
252
263
  .then((outcome) => {
253
264
  if (!outcome.ok)
254
265
  record(outcome.error);
@@ -282,6 +293,7 @@ export class SpinHost {
282
293
  process.off("SIGINT", this.onSigint);
283
294
  process.off("uncaughtException", this.onUncaught);
284
295
  process.off("unhandledRejection", this.onUnhandled);
296
+ this.restoreConsole();
285
297
  this.narrate("finished");
286
298
  // Close the log now (after the last entry) so `streamLog` drains and finishes
287
299
  // writing the complete run.jsonl, then wait for it to fully close its files —
@@ -291,7 +303,7 @@ export class SpinHost {
291
303
  await this.streaming;
292
304
  const file = await this.output.runLog.write();
293
305
  // Direct to the terminal — the file's already written, so this line isn't in it.
294
- console.log(LogRenderer.marker("run", `log → ${file}`));
306
+ this.term(LogRenderer.marker("run", `log → ${file}`));
295
307
  const result = { ok: this.failures.length === 0, failures: this.failures };
296
308
  // One process = one run = one verdict; reflect pass/fail in the exit code so
297
309
  // shells and CI see it without parsing the log. Only set it on failure, so a
@@ -16,7 +16,7 @@ import type { SpinBody } from "./types.ts";
16
16
  * if (ans.ok && ans.value === "yes") await nod(test).done;
17
17
  * nod(devServer); // fire-and-forget daemon
18
18
  * const chrome = nod(chromedriver({ … }));
19
- * nod(chromePage(chrome.ref)); // data deps via .ref
19
+ * nod(chromePage("Explorer", chrome.ref)); // data deps via .ref
20
20
  * });
21
21
  *
22
22
  * Auto-drains by default;
@@ -17,7 +17,7 @@ import { SpinHost } from "./SpinHost.js";
17
17
  * if (ans.ok && ans.value === "yes") await nod(test).done;
18
18
  * nod(devServer); // fire-and-forget daemon
19
19
  * const chrome = nod(chromedriver({ … }));
20
- * nod(chromePage(chrome.ref)); // data deps via .ref
20
+ * nod(chromePage("Explorer", chrome.ref)); // data deps via .ref
21
21
  * });
22
22
  *
23
23
  * Auto-drains by default;
@@ -0,0 +1,24 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import type { Logger } from "./LogsCollector.ts";
3
+ /**
4
+ * The path of the action whose async context is currently executing (e.g.
5
+ * "Build", "Dev › Server"). `SpinHost.launch` wraps each action's `start` in
6
+ * `actionScope.run(path, …)`, so any `console.*` call inside that action's
7
+ * (a)synchronous body reads its path here. No store → the call ran outside any
8
+ * action (the spin body, top-level, a third-party module init).
9
+ */
10
+ export declare const actionScope: AsyncLocalStorage<string>;
11
+ /**
12
+ * Patch `console.log/info/debug/warn/error` so each call appends into `logs`
13
+ * instead of writing to the terminal, attributing every line to the current
14
+ * action via {@link actionScope}. Returns a `restore()` that reinstalls the
15
+ * methods that were installed when this ran — so sequential runs nest cleanly
16
+ * (save-current / restore-saved).
17
+ *
18
+ * The patched methods do **not** echo to the terminal: their output reaches it
19
+ * through the run's log renderer (the caller keeps the original `console.log`
20
+ * for the renderer's own writes). Args are rendered with `util.format` (exact
21
+ * console semantics), split on newlines, and appended line-by-line; an empty
22
+ * render is skipped.
23
+ */
24
+ export declare function captureConsole(logs: Logger): () => void;
@@ -0,0 +1,53 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import { format } from "node:util";
3
+ /**
4
+ * The path of the action whose async context is currently executing (e.g.
5
+ * "Build", "Dev › Server"). `SpinHost.launch` wraps each action's `start` in
6
+ * `actionScope.run(path, …)`, so any `console.*` call inside that action's
7
+ * (a)synchronous body reads its path here. No store → the call ran outside any
8
+ * action (the spin body, top-level, a third-party module init).
9
+ */
10
+ export const actionScope = new AsyncLocalStorage();
11
+ /** The console methods we fold into the log, each mapped to its log level. */
12
+ const LEVELS = {
13
+ log: "info",
14
+ info: "info",
15
+ debug: "debug",
16
+ warn: "warn",
17
+ error: "error",
18
+ };
19
+ /**
20
+ * Patch `console.log/info/debug/warn/error` so each call appends into `logs`
21
+ * instead of writing to the terminal, attributing every line to the current
22
+ * action via {@link actionScope}. Returns a `restore()` that reinstalls the
23
+ * methods that were installed when this ran — so sequential runs nest cleanly
24
+ * (save-current / restore-saved).
25
+ *
26
+ * The patched methods do **not** echo to the terminal: their output reaches it
27
+ * through the run's log renderer (the caller keeps the original `console.log`
28
+ * for the renderer's own writes). Args are rendered with `util.format` (exact
29
+ * console semantics), split on newlines, and appended line-by-line; an empty
30
+ * render is skipped.
31
+ */
32
+ export function captureConsole(logs) {
33
+ const c = console;
34
+ const saved = {};
35
+ const methods = Object.keys(LEVELS);
36
+ for (const method of methods) {
37
+ saved[method] = c[method];
38
+ const level = LEVELS[method];
39
+ c[method] = (...args) => {
40
+ const path = actionScope.getStore();
41
+ const source = path ? `${path} › console` : "console";
42
+ const text = format(...args);
43
+ for (const line of text.split("\n")) {
44
+ if (line)
45
+ logs.append({ source, level, message: line });
46
+ }
47
+ };
48
+ }
49
+ return () => {
50
+ for (const method of methods)
51
+ c[method] = saved[method];
52
+ };
53
+ }
@@ -111,7 +111,9 @@ export class FolderCache {
111
111
  }
112
112
  /** The on-disk filename for a set of targets — a hash of their absolute paths. */
113
113
  static entryPath(paths) {
114
- const key = createHash("sha1").update(FolderCache.resolvePaths(paths).sort().join("\0")).digest("hex");
114
+ const key = createHash("sha1")
115
+ .update(FolderCache.resolvePaths(paths).sort().join("\0"))
116
+ .digest("hex");
115
117
  return join(FolderCache.dir, `${key}.json`);
116
118
  }
117
119
  }
@@ -63,7 +63,11 @@ export const createProc = (logs, signal, snapshots) => (name, opts) => {
63
63
  // spread sits before `cwd` so this resolved value wins even when opts omits
64
64
  // it). This is the "where" that makes a launch line reproducible.
65
65
  const cwd = opts?.cwd ?? process.cwd();
66
- const snap = snapshots.captureJson(`proc-${name}-start`, { cmd: child.cmd, ...safeOpts, cwd });
66
+ const snap = snapshots.captureJson(`proc-${name}-start`, {
67
+ cmd: child.cmd,
68
+ ...safeOpts,
69
+ cwd,
70
+ });
67
71
  logs.append({
68
72
  source: name,
69
73
  level: "event",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omkit",
3
- "version": "0.0.11",
3
+ "version": "0.0.12",
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",