omkit 0.8.7 → 0.8.9
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/README.md +2 -2
- package/dist/cli/ui/app.js +5 -5
- package/dist/cli/ui/redrawLoop.d.ts +16 -0
- package/dist/cli/ui/redrawLoop.js +40 -0
- package/dist/client/channel.js +5 -3
- package/dist/core/action.js +19 -5
- package/dist/core/args.d.ts +2 -0
- package/dist/core/args.js +5 -4
- package/dist/core/interaction.d.ts +19 -5
- package/dist/core/interaction.js +39 -4
- package/dist/core/types.d.ts +7 -0
- package/dist/mcp/action-host.js +7 -1
- package/dist/output/writers/NodeLogWriter.js +2 -2
- package/dist/output/writers/entryLine.js +18 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -87,7 +87,7 @@ const build = action("build").run(({ proc }) => proc("tsc")`tsc -b`);
|
|
|
87
87
|
const activity = build(); // calling launches; returns the live Activity
|
|
88
88
|
```
|
|
89
89
|
|
|
90
|
-
Chain `.describe({ summary })` before `.run(...)` to attach a human-readable summary — stored on the definition, for `omkit ls` and future tooling to read; nothing reads it yet, so today it is documentation that travels with the code. Same shape as `om`'s, below. `.args(schema)` pins the type of `.run`'s second parameter to a zod schema's inferred type.
|
|
90
|
+
Chain `.describe({ summary })` before `.run(...)` to attach a human-readable summary — stored on the definition, for `omkit ls` and future tooling to read; nothing reads it yet, so today it is documentation that travels with the code. Same shape as `om`'s, below. `.args(schema)` pins the type of `.run`'s second parameter to a zod schema's inferred type. In code it's **type-level only**: launching neither resolves, prompts for, nor validates the value — an action launched from an om body is passed its arguments directly in code, so the schema types that call site rather than gating it. The exception is an action run as an MCP tool: the JSON an assistant sends is resolved through this same schema before the body runs, so `.default()`s are filled and bad values are refused — the default `list_oms` advertises is the one the body gets. (Prompting for arguments stays an `om`-level concern.)
|
|
91
91
|
|
|
92
92
|
```ts
|
|
93
93
|
import { z } from "zod";
|
|
@@ -96,7 +96,7 @@ const seed = action("seed")
|
|
|
96
96
|
.describe({ summary: "Seeds the database" })
|
|
97
97
|
.args(z.object({ rows: z.number() }))
|
|
98
98
|
.run(async (_ctx, { rows }) => {
|
|
99
|
-
/* rows: number — typed
|
|
99
|
+
/* rows: number — typed; validated only when run as an MCP tool */
|
|
100
100
|
});
|
|
101
101
|
|
|
102
102
|
seed({ rows: 500 }); // calling launches it; the shape is pinned, not checked at runtime
|
package/dist/cli/ui/app.js
CHANGED
|
@@ -5,6 +5,7 @@ import { RunModel } from "../../output/RunModel.js";
|
|
|
5
5
|
import { OmList } from "./views/OmList.js";
|
|
6
6
|
import { RunView } from "./views/RunView.js";
|
|
7
7
|
import { Spinner } from "./Spinner.js";
|
|
8
|
+
import { redrawLoop } from "./redrawLoop.js";
|
|
8
9
|
/**
|
|
9
10
|
* How often the run screen redraws while the log streams in.
|
|
10
11
|
*
|
|
@@ -13,7 +14,8 @@ import { Spinner } from "./Spinner.js";
|
|
|
13
14
|
* dev server emits. The app then falls permanently behind: `settled` is the *last* message on
|
|
14
15
|
* the channel, so it only arrives once every backlogged entry has been drawn, and the run looks
|
|
15
16
|
* hung long after the child has finished. Folding into the model is cheap; drawing is not, so
|
|
16
|
-
* the two run at different rates
|
|
17
|
+
* the two run at different rates — and the gap is kept *after* each frame (see {@link redrawLoop}),
|
|
18
|
+
* since a frame slower than this would otherwise leave no time between frames for the log at all.
|
|
17
19
|
*/
|
|
18
20
|
const REDRAW_MS = 80;
|
|
19
21
|
/** The interactive app: discover → list/search → run → live milestones + prompts. */
|
|
@@ -34,9 +36,7 @@ export function App({ client, onExit, oms: initialOms, }) {
|
|
|
34
36
|
const tearingRef = useRef(false);
|
|
35
37
|
const redrawTimer = useRef(undefined);
|
|
36
38
|
const stopRedraw = () => {
|
|
37
|
-
|
|
38
|
-
return;
|
|
39
|
-
clearInterval(redrawTimer.current);
|
|
39
|
+
redrawTimer.current?.stop();
|
|
40
40
|
redrawTimer.current = undefined;
|
|
41
41
|
};
|
|
42
42
|
// The redraw timer holds the event loop open, so it must not outlive the app — an unmount
|
|
@@ -69,7 +69,7 @@ export function App({ client, onExit, oms: initialOms, }) {
|
|
|
69
69
|
setLines(model.milestones());
|
|
70
70
|
setStatus(model.summary());
|
|
71
71
|
};
|
|
72
|
-
redrawTimer.current =
|
|
72
|
+
redrawTimer.current = redrawLoop(redraw, REDRAW_MS);
|
|
73
73
|
s.on("log", (entry) => {
|
|
74
74
|
model.apply(entry);
|
|
75
75
|
dirty = true;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Call `draw` repeatedly, leaving `everyMs` of free event-loop time **after** each frame
|
|
3
|
+
* finishes — not every `everyMs` from when the last one started, as `setInterval` does.
|
|
4
|
+
*
|
|
5
|
+
* The difference only shows when a frame is slower than the cadence, which a full Ink milestone
|
|
6
|
+
* tail is. A fixed-rate timer is then overdue the moment a frame ends, so the loop alternates one
|
|
7
|
+
* frame with one turn of I/O — and on Windows a turn of fork IPC is a single message. The
|
|
8
|
+
* interactive app then drains its log backlog a frame per entry, and a run that finished minutes
|
|
9
|
+
* ago is still "streaming" on screen, with `settled` stuck behind the backlog.
|
|
10
|
+
*
|
|
11
|
+
* The next frame is armed from a `setImmediate`, so the gap is measured from after anything the
|
|
12
|
+
* frame deferred to microtasks (React's commit, Ink's write) has run too.
|
|
13
|
+
*/
|
|
14
|
+
export declare function redrawLoop(draw: () => void, everyMs: number): {
|
|
15
|
+
stop(): void;
|
|
16
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Call `draw` repeatedly, leaving `everyMs` of free event-loop time **after** each frame
|
|
3
|
+
* finishes — not every `everyMs` from when the last one started, as `setInterval` does.
|
|
4
|
+
*
|
|
5
|
+
* The difference only shows when a frame is slower than the cadence, which a full Ink milestone
|
|
6
|
+
* tail is. A fixed-rate timer is then overdue the moment a frame ends, so the loop alternates one
|
|
7
|
+
* frame with one turn of I/O — and on Windows a turn of fork IPC is a single message. The
|
|
8
|
+
* interactive app then drains its log backlog a frame per entry, and a run that finished minutes
|
|
9
|
+
* ago is still "streaming" on screen, with `settled` stuck behind the backlog.
|
|
10
|
+
*
|
|
11
|
+
* The next frame is armed from a `setImmediate`, so the gap is measured from after anything the
|
|
12
|
+
* frame deferred to microtasks (React's commit, Ink's write) has run too.
|
|
13
|
+
*/
|
|
14
|
+
export function redrawLoop(draw, everyMs) {
|
|
15
|
+
let stopped = false;
|
|
16
|
+
let timer;
|
|
17
|
+
let immediate;
|
|
18
|
+
const arm = () => {
|
|
19
|
+
immediate = undefined;
|
|
20
|
+
if (!stopped)
|
|
21
|
+
timer = setTimeout(tick, everyMs);
|
|
22
|
+
};
|
|
23
|
+
const tick = () => {
|
|
24
|
+
timer = undefined;
|
|
25
|
+
if (stopped)
|
|
26
|
+
return;
|
|
27
|
+
draw();
|
|
28
|
+
immediate = setImmediate(arm);
|
|
29
|
+
};
|
|
30
|
+
timer = setTimeout(tick, everyMs);
|
|
31
|
+
return {
|
|
32
|
+
stop: () => {
|
|
33
|
+
stopped = true;
|
|
34
|
+
if (timer)
|
|
35
|
+
clearTimeout(timer);
|
|
36
|
+
if (immediate)
|
|
37
|
+
clearImmediate(immediate);
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
package/dist/client/channel.js
CHANGED
|
@@ -15,9 +15,11 @@ export function createChannel(transport) {
|
|
|
15
15
|
let resolveResult;
|
|
16
16
|
const result = new Promise((resolve) => (resolveResult = resolve));
|
|
17
17
|
transport.onMessage((message) => {
|
|
18
|
-
if (message.kind === "
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
if (message.kind === "logs") {
|
|
19
|
+
// Batched on the wire (see Supervisor.log); consumers still see one `log` per entry.
|
|
20
|
+
for (const entry of message.entries)
|
|
21
|
+
for (const h of handlers.log)
|
|
22
|
+
h(entry);
|
|
21
23
|
}
|
|
22
24
|
else if (message.kind === "prompt") {
|
|
23
25
|
for (const h of handlers.prompt)
|
package/dist/core/action.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ExecutionTree } from "./ExecutionTree.js";
|
|
2
2
|
import { callerSite } from "../foundation/callsite.js";
|
|
3
3
|
import { describeSchema } from "./discovery-mode.js";
|
|
4
|
+
import { formatIssues } from "./args.js";
|
|
4
5
|
/**
|
|
5
6
|
* The builder returned by `action(name)`: declare metadata (`.describe(…)`), events
|
|
6
7
|
* (`.emits<…>()`), an imperative handle (`.ref<…>()`), and/or pin the args type
|
|
@@ -45,8 +46,9 @@ class Builder {
|
|
|
45
46
|
const description = this.description;
|
|
46
47
|
const mcp = this.exposure;
|
|
47
48
|
const definedAt = callerSite(); // the `.run(...)` call site — where this action lives
|
|
48
|
-
// No `.args()` on this path, so there is nothing to describe.
|
|
49
|
+
// No `.args()` on this path, so there is nothing to describe — or to resolve against.
|
|
49
50
|
const describeArgs = () => describeSchema(undefined);
|
|
51
|
+
const parseArgs = (supplied) => supplied;
|
|
50
52
|
const exec = body;
|
|
51
53
|
const create = (...args) => {
|
|
52
54
|
const spec = { name, args, tags: [], body: exec, definedAt };
|
|
@@ -58,13 +60,16 @@ class Builder {
|
|
|
58
60
|
description,
|
|
59
61
|
mcp,
|
|
60
62
|
describeArgs,
|
|
63
|
+
parseArgs,
|
|
61
64
|
});
|
|
62
65
|
}
|
|
63
66
|
}
|
|
64
67
|
/**
|
|
65
|
-
* The builder after `.args(schema)`. The schema pins `.run`'s parameter type;
|
|
66
|
-
* validated
|
|
67
|
-
*
|
|
68
|
+
* The builder after `.args(schema)`. The schema pins `.run`'s parameter type; launching is
|
|
69
|
+
* not validated — an action invoked from an om body is passed its args directly in code,
|
|
70
|
+
* and the type already holds. The one untyped caller is the MCP action host, which hands
|
|
71
|
+
* the JSON it was sent through `parseArgs` first, so defaults and validation come from
|
|
72
|
+
* this same schema rather than from nothing.
|
|
68
73
|
*/
|
|
69
74
|
class ArgsBuilder {
|
|
70
75
|
name;
|
|
@@ -72,7 +77,7 @@ class ArgsBuilder {
|
|
|
72
77
|
description;
|
|
73
78
|
exposure;
|
|
74
79
|
constructor(name,
|
|
75
|
-
/**
|
|
80
|
+
/** `S` pins `.run`'s parameter; the value backs `describeArgs` and `parseArgs`. */
|
|
76
81
|
schema, description, exposure) {
|
|
77
82
|
this.name = name;
|
|
78
83
|
this.schema = schema;
|
|
@@ -97,6 +102,14 @@ class ArgsBuilder {
|
|
|
97
102
|
// that built it — rather than in whichever copy the discovery child is running.
|
|
98
103
|
const schema = this.schema;
|
|
99
104
|
const describeArgs = () => describeSchema(schema);
|
|
105
|
+
// `safeParse` is what applies `.default()`s, so this is where an advertised default
|
|
106
|
+
// becomes the effective one. A refusal names the action and every failing field.
|
|
107
|
+
const parseArgs = (supplied) => {
|
|
108
|
+
const parsed = schema.safeParse(supplied);
|
|
109
|
+
if (parsed.success)
|
|
110
|
+
return parsed.data;
|
|
111
|
+
throw new Error(`invalid args for ${name}: ${formatIssues(parsed.error)}`);
|
|
112
|
+
};
|
|
100
113
|
const exec = body;
|
|
101
114
|
const create = (...args) => {
|
|
102
115
|
const spec = { name, args, tags: [], body: exec, definedAt };
|
|
@@ -108,6 +121,7 @@ class ArgsBuilder {
|
|
|
108
121
|
description,
|
|
109
122
|
mcp,
|
|
110
123
|
describeArgs,
|
|
124
|
+
parseArgs,
|
|
111
125
|
});
|
|
112
126
|
}
|
|
113
127
|
}
|
package/dist/core/args.d.ts
CHANGED
|
@@ -37,6 +37,8 @@ export interface ResolveOptions {
|
|
|
37
37
|
* the second is unanswerable.
|
|
38
38
|
*/
|
|
39
39
|
export declare function resolveArgs(schema: z.ZodType, opts: ResolveOptions): Promise<unknown>;
|
|
40
|
+
/** Every issue on one line, `path — reason`, for an error message a caller can act on. */
|
|
41
|
+
export declare function formatIssues(error: z.ZodError): string;
|
|
40
42
|
/**
|
|
41
43
|
* True once a JSON-kind answer is finished. A `{`/`[` opener is a pasted blob and is done
|
|
42
44
|
* when it parses; anything else is a path to a JSON file, and one line is the whole answer.
|
package/dist/core/args.js
CHANGED
|
@@ -45,13 +45,13 @@ export async function resolveArgs(schema, opts) {
|
|
|
45
45
|
// refinement, or a schema that isn't an object at all). No question would make progress,
|
|
46
46
|
// so fail now with the real reason instead of looping silently to the attempt cap.
|
|
47
47
|
if (blocking.length === 0)
|
|
48
|
-
throw new Error(`could not resolve args: ${
|
|
48
|
+
throw new Error(`could not resolve args: ${formatIssues(parsed.error)}`);
|
|
49
49
|
if (!opts.interactive) {
|
|
50
50
|
const anySupplied = blocking.some((field) => current[field] !== undefined);
|
|
51
|
-
throw new MissingArgsError(blocking, anySupplied ?
|
|
51
|
+
throw new MissingArgsError(blocking, anySupplied ? formatIssues(parsed.error) : undefined);
|
|
52
52
|
}
|
|
53
53
|
if (attempt >= MAX_ATTEMPTS) {
|
|
54
|
-
throw new Error(`could not resolve args after ${MAX_ATTEMPTS} attempts: ${
|
|
54
|
+
throw new Error(`could not resolve args after ${MAX_ATTEMPTS} attempts: ${formatIssues(parsed.error)}`);
|
|
55
55
|
}
|
|
56
56
|
for (const field of blocking) {
|
|
57
57
|
const fieldSchema = propertySchema(root(), field);
|
|
@@ -85,7 +85,8 @@ function blockingFields(error) {
|
|
|
85
85
|
const names = error.issues.map((i) => String(i.path[0] ?? "")).filter((n) => n !== "");
|
|
86
86
|
return [...new Set(names)];
|
|
87
87
|
}
|
|
88
|
-
|
|
88
|
+
/** Every issue on one line, `path — reason`, for an error message a caller can act on. */
|
|
89
|
+
export function formatIssues(error) {
|
|
89
90
|
return error.issues.map((i) => `${i.path.join(".") || "(root)"} — ${i.message}`).join("; ");
|
|
90
91
|
}
|
|
91
92
|
function propertySchema(root, field) {
|
|
@@ -22,9 +22,11 @@ export type ChildMessage = {
|
|
|
22
22
|
} | {
|
|
23
23
|
kind: "prompt-done";
|
|
24
24
|
id: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
}
|
|
26
|
+
/** Log entries, oldest first — batched, see {@link Supervisor.log}. */
|
|
27
|
+
| {
|
|
28
|
+
kind: "logs";
|
|
29
|
+
entries: LogEntry[];
|
|
28
30
|
}
|
|
29
31
|
/**
|
|
30
32
|
* The root body has returned. For a long-lived run this is the moment its stack is up
|
|
@@ -59,13 +61,25 @@ type OnMessage = (handler: (message: SupervisorMessage) => void) => void;
|
|
|
59
61
|
* decides nothing (the child keeps its own prompt timeout; see the `prompt` action).
|
|
60
62
|
*/
|
|
61
63
|
export declare class Supervisor {
|
|
62
|
-
private readonly
|
|
64
|
+
private readonly sendRaw;
|
|
63
65
|
private seq;
|
|
64
66
|
private readonly pending;
|
|
65
67
|
private cancelHandler;
|
|
66
|
-
|
|
68
|
+
private batch;
|
|
69
|
+
private flushTimer;
|
|
70
|
+
private readonly send;
|
|
71
|
+
constructor(sendRaw: Send, onMessage: OnMessage);
|
|
67
72
|
request(spec: PromptSpec, signal: AbortSignal): Promise<AnswerMessage>;
|
|
73
|
+
/**
|
|
74
|
+
* Queue a log entry; it goes up with whatever else arrives within {@link LOG_BATCH_MS}.
|
|
75
|
+
*
|
|
76
|
+
* Batched because each IPC message costs the supervisor an event-loop turn (on Windows, Node
|
|
77
|
+
* delivers at most one per turn), and a frontend with slow frames can only take so many turns
|
|
78
|
+
* a second. A message per entry let a chatty run outpace the interactive app, which then kept
|
|
79
|
+
* replaying the log long after the run had finished, with `settled` stuck behind it.
|
|
80
|
+
*/
|
|
68
81
|
log(entry: LogEntry): void;
|
|
82
|
+
private flush;
|
|
69
83
|
up(ok: boolean, folder: string): void;
|
|
70
84
|
settled(ok: boolean, folder: string, summary: string[]): void;
|
|
71
85
|
onCancel(handler: () => void): void;
|
package/dist/core/interaction.js
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How long log entries wait to share one message. Short enough to read as live on screen; long
|
|
3
|
+
* enough that a burst — a build or test suite printing hundreds of lines — crosses as a handful
|
|
4
|
+
* of messages rather than hundreds.
|
|
5
|
+
*/
|
|
6
|
+
const LOG_BATCH_MS = 25;
|
|
1
7
|
/**
|
|
2
8
|
* The child-side handle to an out-of-process supervisor. Sends prompt requests, live log
|
|
3
9
|
* entries, and the final verdict up the channel; resolves prompts when the matching answer
|
|
@@ -5,12 +11,22 @@
|
|
|
5
11
|
* decides nothing (the child keeps its own prompt timeout; see the `prompt` action).
|
|
6
12
|
*/
|
|
7
13
|
export class Supervisor {
|
|
8
|
-
|
|
14
|
+
sendRaw;
|
|
9
15
|
seq = 0;
|
|
10
16
|
pending = new Map();
|
|
11
17
|
cancelHandler;
|
|
12
|
-
|
|
13
|
-
|
|
18
|
+
batch = [];
|
|
19
|
+
flushTimer;
|
|
20
|
+
send;
|
|
21
|
+
constructor(sendRaw, onMessage) {
|
|
22
|
+
this.sendRaw = sendRaw;
|
|
23
|
+
// Every other message leaves through here, so a pending batch always goes first: the entries
|
|
24
|
+
// that led to a prompt are the context it is read in, and a run's closing entries must land
|
|
25
|
+
// before its verdict.
|
|
26
|
+
this.send = (message) => {
|
|
27
|
+
this.flush();
|
|
28
|
+
sendRaw(message);
|
|
29
|
+
};
|
|
14
30
|
onMessage((message) => {
|
|
15
31
|
if (message.kind === "answer") {
|
|
16
32
|
const resolve = this.pending.get(message.id);
|
|
@@ -45,8 +61,27 @@ export class Supervisor {
|
|
|
45
61
|
this.send({ kind: "prompt", id, spec });
|
|
46
62
|
});
|
|
47
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Queue a log entry; it goes up with whatever else arrives within {@link LOG_BATCH_MS}.
|
|
66
|
+
*
|
|
67
|
+
* Batched because each IPC message costs the supervisor an event-loop turn (on Windows, Node
|
|
68
|
+
* delivers at most one per turn), and a frontend with slow frames can only take so many turns
|
|
69
|
+
* a second. A message per entry let a chatty run outpace the interactive app, which then kept
|
|
70
|
+
* replaying the log long after the run had finished, with `settled` stuck behind it.
|
|
71
|
+
*/
|
|
48
72
|
log(entry) {
|
|
49
|
-
this.
|
|
73
|
+
this.batch.push(entry);
|
|
74
|
+
this.flushTimer ??= setTimeout(() => this.flush(), LOG_BATCH_MS);
|
|
75
|
+
}
|
|
76
|
+
flush() {
|
|
77
|
+
if (this.flushTimer)
|
|
78
|
+
clearTimeout(this.flushTimer);
|
|
79
|
+
this.flushTimer = undefined;
|
|
80
|
+
if (this.batch.length === 0)
|
|
81
|
+
return;
|
|
82
|
+
const entries = this.batch;
|
|
83
|
+
this.batch = [];
|
|
84
|
+
this.sendRaw({ kind: "logs", entries });
|
|
50
85
|
}
|
|
51
86
|
up(ok, folder) {
|
|
52
87
|
this.send({ kind: "up", ok, folder });
|
package/dist/core/types.d.ts
CHANGED
|
@@ -203,6 +203,13 @@ export interface Action<Args extends unknown[], Result, Events extends object =
|
|
|
203
203
|
* throws — an unconvertible schema comes back as `schemaError`.
|
|
204
204
|
*/
|
|
205
205
|
readonly describeArgs: () => DescribedArgs;
|
|
206
|
+
/**
|
|
207
|
+
* Resolve args that arrived as untyped JSON — the MCP host's — against the declared
|
|
208
|
+
* schema: defaults filled, everything validated, or a throw naming the field. Identity
|
|
209
|
+
* without `.args()`. Runs in the defining copy of omkit for the same reason
|
|
210
|
+
* `describeArgs` does. In-code callers never need it: their args are typed.
|
|
211
|
+
*/
|
|
212
|
+
readonly parseArgs: (supplied: unknown) => unknown;
|
|
206
213
|
}
|
|
207
214
|
/** Intermediate step from `action(name)`: declare metadata/events/handle, then the impl. */
|
|
208
215
|
export interface ActionBuilderEvents<Events extends object, Handle = void> {
|
package/dist/mcp/action-host.js
CHANGED
|
@@ -47,6 +47,12 @@ om(hostOmName(name, file))
|
|
|
47
47
|
if (typeof launch !== "function") {
|
|
48
48
|
throw new Error(`${file} has no exported action "${exportName}"`);
|
|
49
49
|
}
|
|
50
|
+
const action = launch;
|
|
51
|
+
// The server only checked that the JSON *looked* right (`checkArgs`). The schema that
|
|
52
|
+
// list_oms advertised — defaults included — lives on the action, so the action applies
|
|
53
|
+
// it: what the caller was told is what the body receives. An om does the same through
|
|
54
|
+
// `resolveArgs` before its body runs.
|
|
55
|
+
const resolved = action.parseArgs(args);
|
|
50
56
|
// The action reaches `ExecutionTree.require()` through the copy of omkit *its* file
|
|
51
57
|
// imported, and `current` is a static — so this only works when both resolve to the
|
|
52
58
|
// same installed omkit. True for a normal install and for the fixtures; false under
|
|
@@ -55,7 +61,7 @@ om(hostOmName(name, file))
|
|
|
55
61
|
try {
|
|
56
62
|
// The action's result becomes the run's `value`: the one thing an action-backed tool
|
|
57
63
|
// call exists to hand back, read from `result.json` by the server's verdict.
|
|
58
|
-
return await
|
|
64
|
+
return await action(resolved).result;
|
|
59
65
|
}
|
|
60
66
|
catch (e) {
|
|
61
67
|
if (e instanceof Error && /no active om\(\) run/.test(e.message)) {
|
|
@@ -39,9 +39,9 @@ const legend = [
|
|
|
39
39
|
"#",
|
|
40
40
|
"# ── legend ──────────────────────────────────────────────────────────",
|
|
41
41
|
"# [n] global sequence — the merge/order key across every .log",
|
|
42
|
-
"#
|
|
42
|
+
"# launch ACTION_RUN(id) · → · ….log a child started — open that file for its full output",
|
|
43
|
+
"# indented rows a child action's milestones, bubbled up here (one level under launches)",
|
|
43
44
|
"# name [tags] the action and its tags · defined … where its code lives",
|
|
44
|
-
"# launch · → ….log a child started — open that file for its full output",
|
|
45
45
|
"# ⚡ event ✓ done ok ⊘ cancelled ✗ failed ⊨/⊭ assert pass/fail 📸 snapshot ▪ output",
|
|
46
46
|
];
|
|
47
47
|
const stamp = (ms) => (ms ? new Date(ms).toISOString() : "-");
|
|
@@ -6,10 +6,20 @@ import { label } from "../../foundation/ActionRef.js";
|
|
|
6
6
|
* the child by its {@link label} (name + tags) via `refOf`; everything else keeps its
|
|
7
7
|
* `source` (proc name, `console`, …).
|
|
8
8
|
*/
|
|
9
|
-
export const entryLine = (e, refOf) => {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
export const entryLine = (e, refOf) => `${indentOf(e)}[${e.sequence}] ${bodyOf(e, refOf)}`;
|
|
10
|
+
/**
|
|
11
|
+
* Bubbled child rows are nested in — the whole row, `[seq]` included. The launch pointer
|
|
12
|
+
* sits one level in; the child's own milestones sit one level deeper, under their launch.
|
|
13
|
+
*/
|
|
14
|
+
const indentOf = (e) => {
|
|
15
|
+
if (e.level !== "child")
|
|
16
|
+
return "";
|
|
17
|
+
return e.source === "launch" ? " " : " ";
|
|
18
|
+
};
|
|
19
|
+
/** `→ <id> · <logfile> · …` → `ACTION_RUN(<id>) · → · <logfile> · …`, so a launch stands out. */
|
|
20
|
+
const launchBody = (message) => {
|
|
21
|
+
const [head = "", ...rest] = message.replace(/^→\s*/, "").split(" · ");
|
|
22
|
+
return [`ACTION_RUN(${head})`, "→", ...rest].join(" · ");
|
|
13
23
|
};
|
|
14
24
|
const bodyOf = (e, refOf) => {
|
|
15
25
|
switch (e.level) {
|
|
@@ -28,11 +38,11 @@ const bodyOf = (e, refOf) => {
|
|
|
28
38
|
// output. (Cancellation no longer logs at this level — it's a clean stop.)
|
|
29
39
|
return e.source === "error" ? `✗ ${e.message}` : `▪ ${e.message}`;
|
|
30
40
|
case "child": {
|
|
31
|
-
// A bubbled child line (indented at the row level by `
|
|
32
|
-
// (`source === "launch"`)
|
|
33
|
-
// child by its label (name + tags) via `refOf`.
|
|
41
|
+
// A bubbled child line (indented at the row level by `indentOf`). The launch pointer
|
|
42
|
+
// (`source === "launch"`) reads `launch ACTION_RUN(id) · → · logfile`; a milestone
|
|
43
|
+
// names the child by its label (name + tags) via `refOf`.
|
|
34
44
|
if (e.source === "launch")
|
|
35
|
-
return `${e.source}
|
|
45
|
+
return `${e.source} ${launchBody(e.message)}`;
|
|
36
46
|
const ref = refOf?.(e.source);
|
|
37
47
|
return `${ref ? label(ref) : e.source} · ${e.message}`;
|
|
38
48
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omkit",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.9",
|
|
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",
|