omkit 0.1.5 → 0.1.7
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/core/ActionRun.d.ts +11 -0
- package/dist/core/ActionRun.js +18 -0
- package/dist/core/ExecutionTree.d.ts +1 -1
- package/dist/core/ExecutionTree.js +11 -5
- package/dist/core/action.js +8 -4
- package/dist/core/om.js +2 -1
- package/dist/core/types.d.ts +4 -0
- package/dist/foundation/ActionRef.d.ts +9 -0
- package/dist/foundation/ActionRef.js +6 -0
- package/dist/foundation/callsite.d.ts +7 -0
- package/dist/foundation/callsite.js +38 -0
- package/dist/output/LiveRenderer.d.ts +23 -7
- package/dist/output/LiveRenderer.js +70 -25
- package/dist/output/writers/NodeLogWriter.js +16 -2
- package/dist/output/writers/RollupWriter.js +2 -1
- package/dist/output/writers/entryLine.d.ts +7 -3
- package/dist/output/writers/entryLine.js +22 -6
- package/dist/output/writers/views.d.ts +2 -0
- package/package.json +1 -1
package/dist/core/ActionRun.d.ts
CHANGED
|
@@ -7,6 +7,8 @@ import type { Exec, InstanceEvents, NodeStatus, Outcome, RunHandle } from "./typ
|
|
|
7
7
|
export interface NodeInit {
|
|
8
8
|
readonly store: LogStore;
|
|
9
9
|
readonly name: string;
|
|
10
|
+
/** `file:line` where the action was defined (the `.run(...)` site) — for the log header. */
|
|
11
|
+
readonly definedAt: string | undefined;
|
|
10
12
|
readonly uuid: string;
|
|
11
13
|
/** `${name}_${shortId}`, or `main` for the root. */
|
|
12
14
|
readonly id: string;
|
|
@@ -36,6 +38,7 @@ export declare class ActionRun<Result = unknown, Events extends object = object,
|
|
|
36
38
|
readonly id: string;
|
|
37
39
|
readonly uuid: string;
|
|
38
40
|
readonly name: string;
|
|
41
|
+
readonly definedAt: string | undefined;
|
|
39
42
|
readonly path: string;
|
|
40
43
|
readonly parentId: string | null;
|
|
41
44
|
readonly tags: string[];
|
|
@@ -62,6 +65,7 @@ export declare class ActionRun<Result = unknown, Events extends object = object,
|
|
|
62
65
|
private attached;
|
|
63
66
|
private observed;
|
|
64
67
|
private refObserved;
|
|
68
|
+
private cancelLogged;
|
|
65
69
|
private cascadeCancelled;
|
|
66
70
|
constructor(init: NodeInit);
|
|
67
71
|
/** A stable reference for headers/attribution (tags are the accumulated set now). */
|
|
@@ -76,6 +80,13 @@ export declare class ActionRun<Result = unknown, Events extends object = object,
|
|
|
76
80
|
tag(name: string): this;
|
|
77
81
|
handleFailure(handler: (error: unknown) => void): this;
|
|
78
82
|
cancel(): void;
|
|
83
|
+
/**
|
|
84
|
+
* Log a cancellation milestone on this node (once) and bubble it to the parent, so a
|
|
85
|
+
* cancellation is visible per-action and up the tree into `main`. Only while the node is
|
|
86
|
+
* still running — a node that already settled isn't "cancelled" by a later teardown — and
|
|
87
|
+
* never for the root, whose cancellation is the run-level `tearing down · …` narration.
|
|
88
|
+
*/
|
|
89
|
+
private noteCancelled;
|
|
79
90
|
/** Run the body inside the error boundary, settling the node. Never rejects. */
|
|
80
91
|
run(exec: Exec<Events, Handle, Result>, args: readonly unknown[]): Promise<void>;
|
|
81
92
|
private context;
|
package/dist/core/ActionRun.js
CHANGED
|
@@ -13,6 +13,7 @@ export class ActionRun {
|
|
|
13
13
|
id;
|
|
14
14
|
uuid;
|
|
15
15
|
name;
|
|
16
|
+
definedAt;
|
|
16
17
|
path;
|
|
17
18
|
parentId;
|
|
18
19
|
tags = [];
|
|
@@ -39,6 +40,7 @@ export class ActionRun {
|
|
|
39
40
|
attached = false;
|
|
40
41
|
observed = false;
|
|
41
42
|
refObserved = false;
|
|
43
|
+
cancelLogged = false;
|
|
42
44
|
// Set only by the parent-signal abort listener (an ANCESTOR cancelling this node).
|
|
43
45
|
// Distinguishes a run/ancestor teardown from this node's own subtree-abort on failure,
|
|
44
46
|
// so the unobserved-failure check below isn't fooled by our own controller.abort().
|
|
@@ -47,6 +49,7 @@ export class ActionRun {
|
|
|
47
49
|
this.id = init.id;
|
|
48
50
|
this.uuid = init.uuid;
|
|
49
51
|
this.name = init.name;
|
|
52
|
+
this.definedAt = init.definedAt;
|
|
50
53
|
this.path = init.path;
|
|
51
54
|
this.parentId = init.parentId;
|
|
52
55
|
this.store = init.store;
|
|
@@ -60,6 +63,7 @@ export class ActionRun {
|
|
|
60
63
|
const parent = init.parentSignal;
|
|
61
64
|
const onAncestorAbort = () => {
|
|
62
65
|
this.cascadeCancelled = true;
|
|
66
|
+
this.noteCancelled(); // this node is being cancelled by an ancestor's teardown
|
|
63
67
|
this.controller.abort();
|
|
64
68
|
};
|
|
65
69
|
if (parent) {
|
|
@@ -135,8 +139,22 @@ export class ActionRun {
|
|
|
135
139
|
return this;
|
|
136
140
|
}
|
|
137
141
|
cancel() {
|
|
142
|
+
this.noteCancelled(); // a direct cancel of this activity
|
|
138
143
|
this.controller.abort();
|
|
139
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* Log a cancellation milestone on this node (once) and bubble it to the parent, so a
|
|
147
|
+
* cancellation is visible per-action and up the tree into `main`. Only while the node is
|
|
148
|
+
* still running — a node that already settled isn't "cancelled" by a later teardown — and
|
|
149
|
+
* never for the root, whose cancellation is the run-level `tearing down · …` narration.
|
|
150
|
+
*/
|
|
151
|
+
noteCancelled() {
|
|
152
|
+
if (this.cancelLogged || this.parentId === null || this.status !== "running")
|
|
153
|
+
return;
|
|
154
|
+
this.cancelLogged = true;
|
|
155
|
+
this.log("event", "cancel", "cancelled");
|
|
156
|
+
this.bubble("⊘ cancelled");
|
|
157
|
+
}
|
|
140
158
|
// ── Execution ──────────────────────────────────────────────────────────────
|
|
141
159
|
/** Run the body inside the error boundary, settling the node. Never rejects. */
|
|
142
160
|
async run(exec, args) {
|
|
@@ -39,7 +39,7 @@ export declare class ExecutionTree {
|
|
|
39
39
|
private readonly onSigint;
|
|
40
40
|
private readonly onUncaught;
|
|
41
41
|
private readonly onUnhandled;
|
|
42
|
-
constructor();
|
|
42
|
+
constructor(definedAt?: string);
|
|
43
43
|
/** Launch an instance under the ambient current node (or the root). */
|
|
44
44
|
launch(spec: LaunchSpec): ActionRun;
|
|
45
45
|
private append;
|
|
@@ -62,11 +62,12 @@ export class ExecutionTree {
|
|
|
62
62
|
// Process hooks — installed at run start, removed at finalize (they must not outlive the run).
|
|
63
63
|
onUncaught = (e) => this.onFatal("uncaughtException", e);
|
|
64
64
|
onUnhandled = (e) => this.onFatal("unhandledRejection", e);
|
|
65
|
-
constructor() {
|
|
65
|
+
constructor(definedAt) {
|
|
66
66
|
this.folder.ensure();
|
|
67
67
|
this.root = new ActionRun({
|
|
68
68
|
...this.nodeDeps(),
|
|
69
69
|
name: "main",
|
|
70
|
+
definedAt, // where om() was called — the run's script
|
|
70
71
|
uuid: newUuid(),
|
|
71
72
|
id: "main",
|
|
72
73
|
path: "main",
|
|
@@ -91,6 +92,7 @@ export class ExecutionTree {
|
|
|
91
92
|
const node = new ActionRun({
|
|
92
93
|
...this.nodeDeps(),
|
|
93
94
|
name: spec.name,
|
|
95
|
+
definedAt: spec.definedAt,
|
|
94
96
|
uuid,
|
|
95
97
|
id,
|
|
96
98
|
path: nodePath,
|
|
@@ -102,8 +104,10 @@ export class ExecutionTree {
|
|
|
102
104
|
});
|
|
103
105
|
parent.children.push(node);
|
|
104
106
|
this.registry.push(node);
|
|
105
|
-
// Launch reference — the parent's pointer to the child's own (absolute) log
|
|
106
|
-
|
|
107
|
+
// Launch reference — the parent's pointer to the child's own (absolute) log, plus where
|
|
108
|
+
// the action is defined, so `main` (and any parent) shows each child's script.
|
|
109
|
+
const defined = spec.definedAt ? ` · defined ${spec.definedAt}` : "";
|
|
110
|
+
this.append(parent, "child", "launch", `→ ${id} · ${this.logFile(nodePath)}${defined}`);
|
|
107
111
|
for (const t of spec.tags)
|
|
108
112
|
node.tag(t);
|
|
109
113
|
const promise = currentNode.run(node, () => node.run(spec.body, spec.args));
|
|
@@ -185,8 +189,9 @@ export class ExecutionTree {
|
|
|
185
189
|
process.on("uncaughtException", this.onUncaught);
|
|
186
190
|
process.on("unhandledRejection", this.onUnhandled);
|
|
187
191
|
this.streaming = this.rawStream.run(this.store);
|
|
188
|
-
// Live, curated milestones to the terminal
|
|
189
|
-
|
|
192
|
+
// Live, curated milestones to the terminal — a single in-place status line on a TTY.
|
|
193
|
+
// Straight to process.stdout, which patch-console doesn't intercept (no capture feedback).
|
|
194
|
+
this.liveRendering = new LiveRenderer(process.stdout).run(this.store);
|
|
190
195
|
const rootPromise = currentNode.run(this.root, () => this.root.run(body, []));
|
|
191
196
|
this.trackNode(rootPromise);
|
|
192
197
|
await rootPromise;
|
|
@@ -272,6 +277,7 @@ export class ExecutionTree {
|
|
|
272
277
|
id: node.id,
|
|
273
278
|
uuid: node.uuid,
|
|
274
279
|
name: node.name,
|
|
280
|
+
definedAt: node.definedAt,
|
|
275
281
|
path: node.path,
|
|
276
282
|
parentId: node.parentId,
|
|
277
283
|
tags: [...node.tags],
|
package/dist/core/action.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ExecutionTree } from "./ExecutionTree.js";
|
|
2
2
|
import { FolderCache } from "../system/fs/FolderCache.js";
|
|
3
|
+
import { callerSite } from "../foundation/callsite.js";
|
|
3
4
|
/**
|
|
4
5
|
* A constructed-but-not-yet-run action. Inert: it only holds the name, body, args,
|
|
5
6
|
* and any tags buffered before launch. `exec()` hands it to the tree, which creates
|
|
@@ -9,11 +10,13 @@ class Instance {
|
|
|
9
10
|
name;
|
|
10
11
|
body;
|
|
11
12
|
args;
|
|
13
|
+
definedAt;
|
|
12
14
|
tags = [];
|
|
13
|
-
constructor(name, body, args) {
|
|
15
|
+
constructor(name, body, args, definedAt) {
|
|
14
16
|
this.name = name;
|
|
15
17
|
this.body = body;
|
|
16
18
|
this.args = args;
|
|
19
|
+
this.definedAt = definedAt;
|
|
17
20
|
}
|
|
18
21
|
tag(name) {
|
|
19
22
|
this.tags.push(name);
|
|
@@ -37,7 +40,7 @@ class Instance {
|
|
|
37
40
|
await FolderCache.write(targets, fp);
|
|
38
41
|
return result;
|
|
39
42
|
});
|
|
40
|
-
const cached = new Instance(this.name, body, innerArgs);
|
|
43
|
+
const cached = new Instance(this.name, body, innerArgs, this.definedAt);
|
|
41
44
|
cached.tags.push(...this.tags);
|
|
42
45
|
return cached;
|
|
43
46
|
}
|
|
@@ -59,8 +62,9 @@ class Builder {
|
|
|
59
62
|
}
|
|
60
63
|
run(body) {
|
|
61
64
|
const name = this.name;
|
|
62
|
-
const
|
|
63
|
-
|
|
65
|
+
const definedAt = callerSite(); // the `.run(...)` call site — where this action lives
|
|
66
|
+
const create = (...args) => new Instance(name, body, args, definedAt);
|
|
67
|
+
return Object.assign(create, { actionName: name, definedAt });
|
|
64
68
|
}
|
|
65
69
|
}
|
|
66
70
|
/**
|
package/dist/core/om.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ExecutionTree } from "./ExecutionTree.js";
|
|
2
|
+
import { callerSite } from "../foundation/callsite.js";
|
|
2
3
|
/**
|
|
3
4
|
* Runs a linear orchestration as the root of a fresh {@link ExecutionTree}. The
|
|
4
5
|
* body runs inside the root node's ambient scope, so any `instance.exec()` /
|
|
@@ -10,7 +11,7 @@ export function om(body) {
|
|
|
10
11
|
if (ExecutionTree.current) {
|
|
11
12
|
throw new Error("an om() run is already active in this process");
|
|
12
13
|
}
|
|
13
|
-
const tree = new ExecutionTree();
|
|
14
|
+
const tree = new ExecutionTree(callerSite()); // where om() was called — the run's script
|
|
14
15
|
ExecutionTree.current = tree;
|
|
15
16
|
const rootBody = (ctx) => body({
|
|
16
17
|
signal: ctx.signal,
|
package/dist/core/types.d.ts
CHANGED
|
@@ -110,6 +110,8 @@ export type AnyActionInstance = ActionInstance<unknown, object, unknown>;
|
|
|
110
110
|
export interface Action<Args extends unknown[], Result, Events extends object = NoEvents, Handle = void> {
|
|
111
111
|
(...args: Args): ActionInstance<Result, Events, Handle>;
|
|
112
112
|
readonly actionName: string;
|
|
113
|
+
/** `file:line` where this action was defined (the `.run(...)` site), for the log header. */
|
|
114
|
+
readonly definedAt: string | undefined;
|
|
113
115
|
}
|
|
114
116
|
/** Intermediate step from `action(name)`: declare events/handle, then provide the impl. */
|
|
115
117
|
export interface ActionBuilderEvents<Events extends object, Handle = void> {
|
|
@@ -125,4 +127,6 @@ export interface LaunchSpec {
|
|
|
125
127
|
readonly args: readonly unknown[];
|
|
126
128
|
readonly tags: readonly string[];
|
|
127
129
|
readonly body: Exec<object, unknown, unknown>;
|
|
130
|
+
/** `file:line` where the action was defined; carried onto the node for its log header. */
|
|
131
|
+
readonly definedAt: string | undefined;
|
|
128
132
|
}
|
|
@@ -16,3 +16,12 @@ export interface ActionRef {
|
|
|
16
16
|
}
|
|
17
17
|
/** Render an {@link ActionRef} for a header/reference line: `name · path · [a, b]`. */
|
|
18
18
|
export declare const renderRef: (ref: ActionRef) => string;
|
|
19
|
+
/**
|
|
20
|
+
* A compact inline label — the action **name with its tags**, `name [a, b]` — used
|
|
21
|
+
* wherever an action is referenced within a timeline line (a bubbled child milestone,
|
|
22
|
+
* a live status line) so its tags are always visible right next to the name.
|
|
23
|
+
*/
|
|
24
|
+
export declare const label: (ref: {
|
|
25
|
+
readonly name: string;
|
|
26
|
+
readonly tags: readonly string[];
|
|
27
|
+
}) => string;
|
|
@@ -3,3 +3,9 @@ export const renderRef = (ref) => {
|
|
|
3
3
|
const tags = ref.tags.length ? ` · [${ref.tags.join(", ")}]` : "";
|
|
4
4
|
return `${ref.name} · ${ref.path}${tags}`;
|
|
5
5
|
};
|
|
6
|
+
/**
|
|
7
|
+
* A compact inline label — the action **name with its tags**, `name [a, b]` — used
|
|
8
|
+
* wherever an action is referenced within a timeline line (a bubbled child milestone,
|
|
9
|
+
* a live status line) so its tags are always visible right next to the name.
|
|
10
|
+
*/
|
|
11
|
+
export const label = (ref) => ref.tags.length ? `${ref.name} [${ref.tags.join(", ")}]` : ref.name;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `file:line` of the site that *called* the function invoking `callerSite` — the first stack
|
|
3
|
+
* frame outside both this module and that caller's module. So `action().run(...)` records
|
|
4
|
+
* where `.run(...)` was written, and `om(...)` records where the run was launched from. At
|
|
5
|
+
* runtime that's the built file (e.g. `dist/actions/chrome-page.js`). `undefined` with no stack.
|
|
6
|
+
*/
|
|
7
|
+
export declare const callerSite: () => string | undefined;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
/** `{ file, line }` from a V8 stack frame — handles `at fn (path:line:col)` and `at path:line:col`. */
|
|
3
|
+
const parseFrame = (frame) => {
|
|
4
|
+
const paren = frame.match(/\(([^()]+)\)\s*$/);
|
|
5
|
+
const loc = (paren ? paren[1] : frame.replace(/^\s*at\s+/, "")).trim();
|
|
6
|
+
const m = loc.match(/^(.+):(\d+):\d+$/) ?? loc.match(/^(.+):(\d+)$/);
|
|
7
|
+
return m ? { file: m[1], line: m[2] } : undefined;
|
|
8
|
+
};
|
|
9
|
+
const nativePath = (file) => {
|
|
10
|
+
if (!file.startsWith("file://"))
|
|
11
|
+
return file;
|
|
12
|
+
try {
|
|
13
|
+
return fileURLToPath(file);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return file;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* `file:line` of the site that *called* the function invoking `callerSite` — the first stack
|
|
21
|
+
* frame outside both this module and that caller's module. So `action().run(...)` records
|
|
22
|
+
* where `.run(...)` was written, and `om(...)` records where the run was launched from. At
|
|
23
|
+
* runtime that's the built file (e.g. `dist/actions/chrome-page.js`). `undefined` with no stack.
|
|
24
|
+
*/
|
|
25
|
+
export const callerSite = () => {
|
|
26
|
+
const stack = new Error().stack;
|
|
27
|
+
if (!stack)
|
|
28
|
+
return undefined;
|
|
29
|
+
const frames = stack.split("\n").slice(1);
|
|
30
|
+
const self = parseFrame(frames[0] ?? "")?.file; // this module (callsite)
|
|
31
|
+
const caller = parseFrame(frames[1] ?? "")?.file; // the module that called us
|
|
32
|
+
for (const frame of frames) {
|
|
33
|
+
const at = parseFrame(frame);
|
|
34
|
+
if (at && at.file !== self && at.file !== caller)
|
|
35
|
+
return `${nativePath(at.file)}:${at.line}`;
|
|
36
|
+
}
|
|
37
|
+
return undefined;
|
|
38
|
+
};
|
|
@@ -1,13 +1,29 @@
|
|
|
1
1
|
import type { LogStore } from "./log/LogStore.ts";
|
|
2
|
+
/** The terminal surface the renderer writes to (process.stdout satisfies it). */
|
|
3
|
+
export interface LiveTerminal {
|
|
4
|
+
write(text: string): unknown;
|
|
5
|
+
readonly isTTY?: boolean;
|
|
6
|
+
readonly columns?: number;
|
|
7
|
+
}
|
|
2
8
|
/**
|
|
3
|
-
* Renders a **curated** stream of milestones to the terminal, live — action
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
9
|
+
* Renders a **curated** stream of milestones to the terminal, live — action launches,
|
|
10
|
+
* completions, and errors, plus events, asserts, and snapshots. The bulk output (console
|
|
11
|
+
* lines, proc stdout/stderr) is intentionally *not* shown live; it lives in the per-action
|
|
12
|
+
* `.log` files.
|
|
13
|
+
*
|
|
14
|
+
* On a TTY it keeps a **single status line**, rewriting it in place as milestones arrive,
|
|
15
|
+
* so the terminal stays quiet — except lines worth keeping (errors, failures, run
|
|
16
|
+
* milestones), which are *committed* (printed with a newline) so they persist above the
|
|
17
|
+
* live line. Piped/redirected output (no TTY) falls back to appending every line, so logs
|
|
18
|
+
* stay readable. Writes through `process.stdout` directly, which `patch-console` doesn't
|
|
19
|
+
* intercept, so it can't feed back into console capture.
|
|
8
20
|
*/
|
|
9
21
|
export declare class LiveRenderer {
|
|
10
|
-
private readonly
|
|
11
|
-
|
|
22
|
+
private readonly term;
|
|
23
|
+
/** True while a transient status line is on screen (needs clearing or a closing newline). */
|
|
24
|
+
private pending;
|
|
25
|
+
/** Tags accumulated per node from `tag` entries, so milestones can show them by the name. */
|
|
26
|
+
private readonly tagsByNode;
|
|
27
|
+
constructor(term: LiveTerminal);
|
|
12
28
|
run(store: LogStore): Promise<void>;
|
|
13
29
|
}
|
|
@@ -1,56 +1,101 @@
|
|
|
1
|
+
/** Return to column 0 and clear the whole line — the in-place status-line reset. */
|
|
2
|
+
const CLEAR = "\r\x1b[2K";
|
|
1
3
|
/**
|
|
2
|
-
* Renders a **curated** stream of milestones to the terminal, live — action
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Renders a **curated** stream of milestones to the terminal, live — action launches,
|
|
5
|
+
* completions, and errors, plus events, asserts, and snapshots. The bulk output (console
|
|
6
|
+
* lines, proc stdout/stderr) is intentionally *not* shown live; it lives in the per-action
|
|
7
|
+
* `.log` files.
|
|
8
|
+
*
|
|
9
|
+
* On a TTY it keeps a **single status line**, rewriting it in place as milestones arrive,
|
|
10
|
+
* so the terminal stays quiet — except lines worth keeping (errors, failures, run
|
|
11
|
+
* milestones), which are *committed* (printed with a newline) so they persist above the
|
|
12
|
+
* live line. Piped/redirected output (no TTY) falls back to appending every line, so logs
|
|
13
|
+
* stay readable. Writes through `process.stdout` directly, which `patch-console` doesn't
|
|
14
|
+
* intercept, so it can't feed back into console capture.
|
|
7
15
|
*/
|
|
8
16
|
export class LiveRenderer {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
17
|
+
term;
|
|
18
|
+
/** True while a transient status line is on screen (needs clearing or a closing newline). */
|
|
19
|
+
pending = false;
|
|
20
|
+
/** Tags accumulated per node from `tag` entries, so milestones can show them by the name. */
|
|
21
|
+
tagsByNode = new Map();
|
|
22
|
+
constructor(term) {
|
|
23
|
+
this.term = term;
|
|
12
24
|
}
|
|
13
25
|
async run(store) {
|
|
26
|
+
const tty = Boolean(this.term.isTTY);
|
|
14
27
|
for await (const entry of store.subscribe({ replay: true })) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
28
|
+
if (entry.level === "tag" && entry.source === "tag") {
|
|
29
|
+
const list = this.tagsByNode.get(entry.nodeId) ?? [];
|
|
30
|
+
list.push(entry.message);
|
|
31
|
+
this.tagsByNode.set(entry.nodeId, list);
|
|
32
|
+
continue; // tags aren't their own line — they decorate the node's milestones
|
|
33
|
+
}
|
|
34
|
+
const rendered = format(entry, this.tagsByNode.get(entry.nodeId));
|
|
35
|
+
if (rendered === null)
|
|
36
|
+
continue;
|
|
37
|
+
if (!tty) {
|
|
38
|
+
this.term.write(`${rendered.text}\n`); // no TTY: append, one line each
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (rendered.persist) {
|
|
42
|
+
// Commit a line that must survive: finish the transient line, then print permanently.
|
|
43
|
+
this.term.write(`${this.pending ? CLEAR : ""}${rendered.text}\n`);
|
|
44
|
+
this.pending = false;
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
// Transient: overwrite the current status line in place, truncated to fit.
|
|
48
|
+
this.term.write(`${CLEAR}${truncate(rendered.text, this.term.columns)}`);
|
|
49
|
+
this.pending = true;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// Keep the last status line on screen for the summary that follows.
|
|
53
|
+
if (this.pending) {
|
|
54
|
+
this.term.write("\n");
|
|
55
|
+
this.pending = false;
|
|
18
56
|
}
|
|
19
57
|
}
|
|
20
58
|
}
|
|
21
59
|
const firstLine = (message) => message.split("\n", 1)[0] ?? message;
|
|
22
60
|
/** ✓ ok · ⊘ cancelled (a clean stop) · ✗ failed. */
|
|
23
61
|
const doneIcon = (status) => status === "ok" ? "✓" : status === "cancelled" ? "⊘" : "✗";
|
|
24
|
-
/**
|
|
25
|
-
const
|
|
62
|
+
/** Truncate to the terminal width so an in-place line never wraps and breaks the `\r`. */
|
|
63
|
+
const truncate = (text, columns) => !columns || text.length < columns ? text : `${text.slice(0, Math.max(0, columns - 1))}…`;
|
|
64
|
+
/** The node's path with its accumulated tags appended: `main/probe_9f3c [ready, gate]`. */
|
|
65
|
+
const named = (path, tags) => tags && tags.length ? `${path} [${tags.join(", ")}]` : path;
|
|
66
|
+
/** Map a curated entry to a terminal line (and whether it persists), or `null` to drop it. */
|
|
67
|
+
const format = (e, tags) => {
|
|
26
68
|
// Action launch — bubbled onto the parent as `→ <childId> · <log>`.
|
|
27
69
|
if (e.level === "child" && e.source === "launch") {
|
|
28
70
|
const child = e.message.replace(/^→\s*/, "").split(" · ")[0];
|
|
29
|
-
return `▶ ${e.path}/${child}
|
|
71
|
+
return { text: `▶ ${e.path}/${child}`, persist: false };
|
|
30
72
|
}
|
|
31
|
-
// Lifecycle events: `launched` is already shown via the launch pointer above, so
|
|
32
|
-
//
|
|
73
|
+
// Lifecycle events: `launched` is already shown via the launch pointer above, so drop
|
|
74
|
+
// the duplicate; `done · <status>` renders with the status icon — a failure persists.
|
|
33
75
|
if (e.level === "event" && e.source === "lifecycle") {
|
|
34
76
|
if (e.message === "launched")
|
|
35
77
|
return null;
|
|
36
78
|
const status = e.message.replace(/^done · /, "");
|
|
37
|
-
return `${doneIcon(status)} ${e.path} · ${status}
|
|
79
|
+
return { text: `${doneIcon(status)} ${named(e.path, tags)} · ${status}`, persist: status === "failed" };
|
|
38
80
|
}
|
|
39
|
-
// Run milestones (started / tearing down / finished).
|
|
81
|
+
// Run milestones (started / tearing down / finished) — few, and worth keeping.
|
|
40
82
|
if (e.level === "run")
|
|
41
|
-
return `· ${e.message}
|
|
42
|
-
// An action's *own* failure — not proc stderr
|
|
43
|
-
// or a teardown cancellation (which is a clean stop, not an error).
|
|
83
|
+
return { text: `· ${e.message}`, persist: true };
|
|
84
|
+
// An action's *own* failure — not proc stderr, console.error, or a teardown cancellation.
|
|
44
85
|
if (e.level === "error" && e.source === "error") {
|
|
45
|
-
return `✗ ${e.path} · ${firstLine(e.message)}
|
|
86
|
+
return { text: `✗ ${named(e.path, tags)} · ${firstLine(e.message)}`, persist: true };
|
|
46
87
|
}
|
|
88
|
+
// A cancellation milestone (⊘) — persist it so it doesn't flash away during teardown.
|
|
89
|
+
if (e.level === "event" && e.source === "cancel")
|
|
90
|
+
return { text: `⊘ ${named(e.path, tags)} · ${e.message}`, persist: true };
|
|
47
91
|
// User-emitted events.
|
|
48
92
|
if (e.level === "event")
|
|
49
|
-
return `⚡ ${e.path} · ${e.message}
|
|
93
|
+
return { text: `⚡ ${named(e.path, tags)} · ${e.message}`, persist: false };
|
|
94
|
+
// Asserts: a failed one (⊭) persists, a pass is transient.
|
|
50
95
|
if (e.level === "assert")
|
|
51
|
-
return ` ${e.path} · ${e.message}
|
|
96
|
+
return { text: ` ${named(e.path, tags)} · ${e.message}`, persist: e.message.includes("⊭") };
|
|
52
97
|
if (e.level === "snapshot")
|
|
53
|
-
return `📸 ${e.path} · ${e.message}
|
|
98
|
+
return { text: `📸 ${named(e.path, tags)} · ${e.message}`, persist: false };
|
|
54
99
|
// Everything else (info / console / proc output / tag / bubbled child events) → files only.
|
|
55
100
|
return null;
|
|
56
101
|
};
|
|
@@ -9,21 +9,35 @@ import { entryLine, groupByNode } from "./entryLine.js";
|
|
|
9
9
|
*/
|
|
10
10
|
export async function writeNodeLogs(nodes, entries) {
|
|
11
11
|
const byNode = groupByNode(entries, () => true);
|
|
12
|
+
const refByNode = new Map(nodes.map((n) => [n.id, n]));
|
|
13
|
+
const refOf = (id) => refByNode.get(id);
|
|
12
14
|
await Promise.all(nodes.map(async (node) => {
|
|
13
|
-
const lines = (byNode.get(node.id) ?? []).map(entryLine);
|
|
15
|
+
const lines = (byNode.get(node.id) ?? []).map((e) => entryLine(e, refOf));
|
|
14
16
|
const body = [...header(node), "", ...lines, ""].join("\n");
|
|
15
17
|
await mkdir(path.dirname(node.logFile), { recursive: true });
|
|
16
18
|
await writeFile(node.logFile, body, "utf8");
|
|
17
19
|
}));
|
|
18
20
|
}
|
|
19
|
-
/** The per-file metadata header: ref, args, start/finish times,
|
|
21
|
+
/** The per-file metadata header: ref, defining script, args, start/finish times, status. */
|
|
20
22
|
const header = (node) => [
|
|
21
23
|
`# ${renderRef(node)}`,
|
|
22
24
|
`# log: ${node.logFile}`,
|
|
25
|
+
node.definedAt ? `# defined: ${node.definedAt}` : null,
|
|
23
26
|
`# args: ${renderArgs(node.args)}`,
|
|
24
27
|
`# started: ${stamp(node.startedAt)}`,
|
|
25
28
|
`# finished: ${stamp(node.endedAt)} (${node.duration}ms)`,
|
|
26
29
|
`# status: ${node.status}`,
|
|
30
|
+
...(node.parentId === null ? legend : []), // the run's root log (main.log) only
|
|
31
|
+
].filter((line) => line !== null);
|
|
32
|
+
/** A compact reading guide, printed once on the root log so an agent can follow the run. */
|
|
33
|
+
const legend = [
|
|
34
|
+
"#",
|
|
35
|
+
"# ── legend ──────────────────────────────────────────────────────────",
|
|
36
|
+
"# [n] global sequence — the merge/order key across every .log",
|
|
37
|
+
"# indented rows a child action's milestones, bubbled up here",
|
|
38
|
+
"# name [tags] the action and its tags · defined … where its code lives",
|
|
39
|
+
"# launch · → ….log a child started — open that file for its full output",
|
|
40
|
+
"# ⚡ event ✓ done ok ⊘ cancelled ✗ failed ⊨/⊭ assert pass/fail 📸 snapshot ▪ output",
|
|
27
41
|
];
|
|
28
42
|
const stamp = (ms) => (ms ? new Date(ms).toISOString() : "-");
|
|
29
43
|
const renderArgs = (args) => {
|
|
@@ -9,6 +9,7 @@ import { entryLine } from "./entryLine.js";
|
|
|
9
9
|
*/
|
|
10
10
|
export async function writeRollup(file, nodes, entries, match, footer) {
|
|
11
11
|
const refByNode = new Map(nodes.map((n) => [n.id, n]));
|
|
12
|
+
const refOf = (id) => refByNode.get(id);
|
|
12
13
|
const chunks = [];
|
|
13
14
|
let lastNode = null;
|
|
14
15
|
for (const e of entries) {
|
|
@@ -21,7 +22,7 @@ export async function writeRollup(file, nodes, entries, match, footer) {
|
|
|
21
22
|
chunks.push(`# ${ref ? renderRef(ref) : e.path}`);
|
|
22
23
|
lastNode = e.nodeId;
|
|
23
24
|
}
|
|
24
|
-
chunks.push(entryLine(e));
|
|
25
|
+
chunks.push(entryLine(e, refOf));
|
|
25
26
|
}
|
|
26
27
|
if (footer)
|
|
27
28
|
chunks.push("", footer);
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
+
import { type ActionRef } from "../../foundation/ActionRef.ts";
|
|
1
2
|
import type { LogEntry } from "../../foundation/LogEntry.ts";
|
|
3
|
+
/** Resolve a node's reference (name + tags) by id, for naming a bubbled child. */
|
|
4
|
+
export type RefOf = (nodeId: string) => ActionRef | undefined;
|
|
2
5
|
/**
|
|
3
6
|
* The shared human-readable row: `[<seq>] <body>`. Only the run-global sequence
|
|
4
7
|
* prefixes the line (date/path live in the file header + `raw.jsonl`). Event,
|
|
5
|
-
* snapshot, and assert lines carry a leading icon;
|
|
6
|
-
*
|
|
8
|
+
* snapshot, and assert lines carry a leading icon; a **bubbled child milestone** names
|
|
9
|
+
* the child by its {@link label} (name + tags) via `refOf`; everything else keeps its
|
|
10
|
+
* `source` (proc name, `console`, …).
|
|
7
11
|
*/
|
|
8
|
-
export declare const entryLine: (e: LogEntry) => string;
|
|
12
|
+
export declare const entryLine: (e: LogEntry, refOf?: RefOf) => string;
|
|
9
13
|
/** Group entries by `nodeId` (preserving order), keeping only those that `match`. */
|
|
10
14
|
export declare const groupByNode: (entries: readonly LogEntry[], match: (e: LogEntry) => boolean) => Map<string, LogEntry[]>;
|
|
@@ -1,14 +1,21 @@
|
|
|
1
|
+
import { label } from "../../foundation/ActionRef.js";
|
|
1
2
|
/**
|
|
2
3
|
* The shared human-readable row: `[<seq>] <body>`. Only the run-global sequence
|
|
3
4
|
* prefixes the line (date/path live in the file header + `raw.jsonl`). Event,
|
|
4
|
-
* snapshot, and assert lines carry a leading icon;
|
|
5
|
-
*
|
|
5
|
+
* snapshot, and assert lines carry a leading icon; a **bubbled child milestone** names
|
|
6
|
+
* the child by its {@link label} (name + tags) via `refOf`; everything else keeps its
|
|
7
|
+
* `source` (proc name, `console`, …).
|
|
6
8
|
*/
|
|
7
|
-
export const entryLine = (e) =>
|
|
8
|
-
|
|
9
|
+
export const entryLine = (e, refOf) => {
|
|
10
|
+
// Bubbled child rows are nested a level in — the whole row, `[seq]` included.
|
|
11
|
+
const indent = e.level === "child" ? " " : "";
|
|
12
|
+
return `${indent}[${e.sequence}] ${bodyOf(e, refOf)}`;
|
|
13
|
+
};
|
|
14
|
+
const bodyOf = (e, refOf) => {
|
|
9
15
|
switch (e.level) {
|
|
10
16
|
case "event":
|
|
11
|
-
|
|
17
|
+
// A cancellation milestone reads ⊘; other emitted events keep ⚡.
|
|
18
|
+
return e.source === "cancel" ? `⊘ ${e.message}` : `⚡ ${e.message}`;
|
|
12
19
|
case "snapshot":
|
|
13
20
|
return `📸 ${e.message}`;
|
|
14
21
|
case "assert":
|
|
@@ -20,7 +27,16 @@ const bodyOf = (e) => {
|
|
|
20
27
|
// An action's *own* failure keeps a ✗; proc stderr / console.error is plain
|
|
21
28
|
// output. (Cancellation no longer logs at this level — it's a clean stop.)
|
|
22
29
|
return e.source === "error" ? `✗ ${e.message}` : `▪ ${e.message}`;
|
|
23
|
-
|
|
30
|
+
case "child": {
|
|
31
|
+
// A bubbled child line (indented at the row level by `entryLine`). The launch pointer
|
|
32
|
+
// (`source === "launch"`) keeps its `→ id · logfile` message; a milestone names the
|
|
33
|
+
// child by its label (name + tags) via `refOf`.
|
|
34
|
+
if (e.source === "launch")
|
|
35
|
+
return `${e.source} · ${e.message}`;
|
|
36
|
+
const ref = refOf?.(e.source);
|
|
37
|
+
return `${ref ? label(ref) : e.source} · ${e.message}`;
|
|
38
|
+
}
|
|
39
|
+
// run/done milestones, tags, … keep their `source` label.
|
|
24
40
|
default:
|
|
25
41
|
return `${e.source} · ${e.message}`;
|
|
26
42
|
}
|
|
@@ -7,6 +7,8 @@ export interface NodeView {
|
|
|
7
7
|
readonly id: string;
|
|
8
8
|
readonly uuid: string;
|
|
9
9
|
readonly name: string;
|
|
10
|
+
/** `file:line` where the action was defined (the `.run(...)` site); absent for the root. */
|
|
11
|
+
readonly definedAt?: string;
|
|
10
12
|
readonly path: string;
|
|
11
13
|
readonly parentId: string | null;
|
|
12
14
|
readonly tags: readonly string[];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omkit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
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",
|