omkit 0.0.10 → 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.
- package/dist/actions/chrome-page.d.ts +7 -3
- package/dist/actions/chrome-page.js +11 -7
- package/dist/index.d.ts +1 -1
- package/dist/orchestration/action/action.js +1 -0
- package/dist/orchestration/action/types.d.ts +8 -1
- package/dist/orchestration/spin/SpinHost.d.ts +4 -0
- package/dist/orchestration/spin/SpinHost.js +17 -5
- package/dist/orchestration/spin/spin.d.ts +1 -1
- package/dist/orchestration/spin/spin.js +1 -1
- package/dist/output/log/console-capture.d.ts +24 -0
- package/dist/output/log/console-capture.js +53 -0
- package/dist/system/fs/FolderCache.js +3 -1
- package/dist/system/process/process.js +5 -1
- package/package.json +1 -1
|
@@ -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(
|
|
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(
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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";
|
|
@@ -66,6 +66,7 @@ class ActionRun {
|
|
|
66
66
|
attach: (value) => this.attach(value),
|
|
67
67
|
proc: createProc(system.logs, system.signal, system.output.snapshots),
|
|
68
68
|
artifactsFolder: system.output.folder.artifacts(),
|
|
69
|
+
snapshot: (name, value) => system.output.snapshots.snapshot(name, value),
|
|
69
70
|
}, ...this.args))
|
|
70
71
|
.then((result) => {
|
|
71
72
|
const outcome = { ok: true, value: result };
|
|
@@ -86,6 +86,13 @@ export interface ActionContext<Events extends object, Handle = void> extends Sys
|
|
|
86
86
|
* beside `run.log`; the same folder is on the `spin` context.
|
|
87
87
|
*/
|
|
88
88
|
readonly artifactsFolder: string;
|
|
89
|
+
/**
|
|
90
|
+
* Capture a JSON snapshot into this run's output folder and drop a `[snapshot]`
|
|
91
|
+
* line on the timeline (under this action's path). The ergonomic shortcut for
|
|
92
|
+
* `ctx.output.snapshots.snapshot(…)`; the same convenience is on the `spin`
|
|
93
|
+
* context. Resolves with the snapshot's relative path.
|
|
94
|
+
*/
|
|
95
|
+
readonly snapshot: (name: string, value: unknown) => Promise<string>;
|
|
89
96
|
}
|
|
90
97
|
/** A constructed-but-not-yet-run action: what calling an `Action` produces. */
|
|
91
98
|
export interface ActionInstance<Result = unknown, Events extends object = NoEvents, Handle = void> {
|
|
@@ -106,7 +113,7 @@ export interface ActionInstance<Result = unknown, Events extends object = NoEven
|
|
|
106
113
|
readonly done: Promise<Outcome<Result>>;
|
|
107
114
|
/**
|
|
108
115
|
* Resolves with the handle the action attached via `ctx.attach` (set once).
|
|
109
|
-
* 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
|
|
110
117
|
* the action settles *without* attaching, this resolves (`undefined` for the
|
|
111
118
|
* default `void` handle) on success and **rejects on failure** — so, unlike
|
|
112
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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));
|
|
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));
|
|
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")
|
|
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`, {
|
|
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.
|
|
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",
|