omkit 0.0.15 → 0.1.0
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 +110 -97
- package/dist/actions/chrome-page.d.ts +12 -29
- package/dist/actions/chrome-page.js +19 -32
- package/dist/actions/command.d.ts +8 -41
- package/dist/actions/command.js +15 -82
- package/dist/actions/healthcheck.d.ts +3 -11
- package/dist/actions/healthcheck.js +8 -22
- package/dist/actions/index.d.ts +6 -8
- package/dist/actions/index.js +4 -6
- package/dist/actions/prompt.d.ts +5 -19
- package/dist/actions/prompt.js +10 -31
- package/dist/actions/tail-log.d.ts +7 -21
- package/dist/actions/tail-log.js +13 -34
- package/dist/actions/until-log.d.ts +4 -7
- package/dist/actions/until-log.js +3 -3
- package/dist/actions/watch-dir.d.ts +5 -19
- package/dist/actions/watch-dir.js +13 -28
- package/dist/actions/watch.d.ts +4 -12
- package/dist/actions/watch.js +9 -16
- package/dist/core/ActionRun.d.ts +97 -0
- package/dist/core/ActionRun.js +266 -0
- package/dist/core/ExecutionTree.d.ts +88 -0
- package/dist/core/ExecutionTree.js +320 -0
- package/dist/core/action.d.ts +6 -0
- package/dist/core/action.js +72 -0
- package/dist/core/context.d.ts +9 -0
- package/dist/core/context.js +8 -0
- package/dist/core/om.d.ts +9 -0
- package/dist/core/om.js +25 -0
- package/dist/core/step.d.ts +9 -0
- package/dist/core/step.js +18 -0
- package/dist/core/types.d.ts +128 -0
- package/dist/foundation/ActionRef.d.ts +18 -0
- package/dist/foundation/ActionRef.js +5 -0
- package/dist/{utils → foundation}/AsyncQueue.d.ts +2 -4
- package/dist/{utils → foundation}/AsyncQueue.js +6 -19
- package/dist/foundation/CancelledError.d.ts +10 -0
- package/dist/foundation/CancelledError.js +13 -0
- package/dist/foundation/Deferred.d.ts +8 -0
- package/dist/foundation/Deferred.js +10 -0
- package/dist/foundation/LogEntry.d.ts +29 -0
- package/dist/{orchestration/events → foundation}/events.d.ts +2 -11
- package/dist/{orchestration/events → foundation}/events.js +1 -7
- package/dist/{utils → foundation}/format.d.ts +3 -4
- package/dist/{utils → foundation}/format.js +12 -4
- package/dist/foundation/fsSafe.d.ts +7 -0
- package/dist/foundation/fsSafe.js +7 -0
- package/dist/foundation/ids.d.ts +9 -0
- package/dist/foundation/ids.js +10 -0
- package/dist/index.d.ts +11 -22
- package/dist/index.js +10 -30
- package/dist/output/LiveRenderer.d.ts +13 -0
- package/dist/output/LiveRenderer.js +56 -0
- package/dist/output/console/ConsoleCapture.d.ts +20 -0
- package/dist/output/console/ConsoleCapture.js +47 -0
- package/dist/output/folder/RunFolder.d.ts +11 -22
- package/dist/output/folder/RunFolder.js +18 -36
- package/dist/output/log/LogStore.d.ts +22 -0
- package/dist/output/log/LogStore.js +56 -0
- package/dist/output/log/RawStream.d.ts +11 -0
- package/dist/output/log/RawStream.js +18 -0
- package/dist/output/snapshot/SnapshotStore.d.ts +6 -43
- package/dist/output/snapshot/SnapshotStore.js +12 -52
- package/dist/output/writers/NodeLogWriter.d.ts +8 -0
- package/dist/output/writers/NodeLogWriter.js +36 -0
- package/dist/output/writers/ResultWriter.d.ts +3 -0
- package/dist/output/writers/ResultWriter.js +5 -0
- package/dist/output/writers/RollupWriter.d.ts +9 -0
- package/dist/output/writers/RollupWriter.js +29 -0
- package/dist/output/writers/entryLine.d.ts +10 -0
- package/dist/output/writers/entryLine.js +39 -0
- package/dist/output/writers/views.d.ts +38 -0
- package/dist/output/writers/views.js +6 -0
- package/dist/system/fs/FolderCache.d.ts +6 -34
- package/dist/system/fs/FolderCache.js +7 -38
- package/dist/system/proc.d.ts +51 -0
- package/dist/system/proc.js +148 -0
- package/dist/system/procRegistry.d.ts +52 -0
- package/dist/system/procRegistry.js +117 -0
- package/package.json +3 -1
- package/dist/orchestration/action/action.d.ts +0 -23
- package/dist/orchestration/action/action.js +0 -256
- package/dist/orchestration/action/types.d.ts +0 -182
- package/dist/orchestration/assert.d.ts +0 -24
- package/dist/orchestration/assert.js +0 -26
- package/dist/orchestration/spin/SpinHost.d.ts +0 -111
- package/dist/orchestration/spin/SpinHost.js +0 -345
- package/dist/orchestration/spin/spin.d.ts +0 -24
- package/dist/orchestration/spin/spin.js +0 -57
- package/dist/orchestration/spin/types.d.ts +0 -63
- package/dist/output/Output.d.ts +0 -24
- package/dist/output/Output.js +0 -27
- package/dist/output/index.d.ts +0 -11
- package/dist/output/index.js +0 -11
- package/dist/output/log/LogRenderer.d.ts +0 -73
- package/dist/output/log/LogRenderer.js +0 -133
- package/dist/output/log/LogsCollector.d.ts +0 -79
- package/dist/output/log/LogsCollector.js +0 -141
- package/dist/output/log/RunLog.d.ts +0 -54
- package/dist/output/log/RunLog.js +0 -152
- package/dist/output/log/console-capture.d.ts +0 -24
- package/dist/output/log/console-capture.js +0 -53
- package/dist/system/fs/streams.d.ts +0 -11
- package/dist/system/fs/streams.js +0 -14
- package/dist/system/process/process.d.ts +0 -35
- package/dist/system/process/process.js +0 -104
- package/dist/utils/serialize.d.ts +0 -2
- package/dist/utils/serialize.js +0 -12
- /package/dist/{orchestration/action → core}/types.js +0 -0
- /package/dist/{orchestration/spin/types.js → foundation/LogEntry.js} +0 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { AsyncQueue } from "../../foundation/AsyncQueue.js";
|
|
2
|
+
/**
|
|
3
|
+
* The run's single append-only timeline. Owns the run-global `sequence`, keeps the
|
|
4
|
+
* full history (for replay + finalize projections), and fans live entries out to
|
|
5
|
+
* subscribers. It holds no presentation — headers/prefixes are applied by writers
|
|
6
|
+
* at read time. Actions receive it only as {@link ReadableLog}.
|
|
7
|
+
*/
|
|
8
|
+
export class LogStore {
|
|
9
|
+
seq = 0;
|
|
10
|
+
history = [];
|
|
11
|
+
subscribers = new Set();
|
|
12
|
+
ended = false;
|
|
13
|
+
/** Append one entry; stamps `sequence`/`ts`, retains it, fans it out. */
|
|
14
|
+
append(input) {
|
|
15
|
+
const entry = { ...input, sequence: ++this.seq, ts: Date.now() };
|
|
16
|
+
this.history.push(entry);
|
|
17
|
+
for (const subscriber of this.subscribers)
|
|
18
|
+
subscriber.push(entry);
|
|
19
|
+
return entry;
|
|
20
|
+
}
|
|
21
|
+
subscribe({ replay = false } = {}) {
|
|
22
|
+
const queue = new AsyncQueue();
|
|
23
|
+
if (replay)
|
|
24
|
+
for (const entry of this.history)
|
|
25
|
+
queue.push(entry);
|
|
26
|
+
if (this.ended)
|
|
27
|
+
queue.close();
|
|
28
|
+
else
|
|
29
|
+
this.subscribers.add(queue);
|
|
30
|
+
const subscribers = this.subscribers;
|
|
31
|
+
const inner = queue[Symbol.asyncIterator]();
|
|
32
|
+
return {
|
|
33
|
+
[Symbol.asyncIterator]() {
|
|
34
|
+
return {
|
|
35
|
+
next: () => inner.next(),
|
|
36
|
+
return: () => {
|
|
37
|
+
subscribers.delete(queue);
|
|
38
|
+
queue.close();
|
|
39
|
+
return Promise.resolve({ done: true, value: undefined });
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/** EOF for readers: end every live subscription and refuse new ones. Called once at finalize. */
|
|
46
|
+
close() {
|
|
47
|
+
this.ended = true;
|
|
48
|
+
for (const queue of this.subscribers)
|
|
49
|
+
queue.close();
|
|
50
|
+
this.subscribers.clear();
|
|
51
|
+
}
|
|
52
|
+
/** The complete history — the source for finalize-time projections (per-node logs, rollups). */
|
|
53
|
+
entries() {
|
|
54
|
+
return this.history;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { LogStore } from "./LogStore.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Streams every entry to `raw.jsonl` as one JSON object per line, live, for the
|
|
4
|
+
* whole run — the crash-insurance / machine-merge record. `run(store)` subscribes
|
|
5
|
+
* (replay + live) and resolves once the store closes and the file is flushed.
|
|
6
|
+
*/
|
|
7
|
+
export declare class RawStream {
|
|
8
|
+
private readonly file;
|
|
9
|
+
constructor(absPath: string);
|
|
10
|
+
run(store: LogStore): Promise<void>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createWriteStream } from "node:fs";
|
|
2
|
+
/**
|
|
3
|
+
* Streams every entry to `raw.jsonl` as one JSON object per line, live, for the
|
|
4
|
+
* whole run — the crash-insurance / machine-merge record. `run(store)` subscribes
|
|
5
|
+
* (replay + live) and resolves once the store closes and the file is flushed.
|
|
6
|
+
*/
|
|
7
|
+
export class RawStream {
|
|
8
|
+
file;
|
|
9
|
+
constructor(absPath) {
|
|
10
|
+
this.file = createWriteStream(absPath, { flags: "w" });
|
|
11
|
+
}
|
|
12
|
+
async run(store) {
|
|
13
|
+
for await (const entry of store.subscribe({ replay: true })) {
|
|
14
|
+
this.file.write(`${JSON.stringify(entry)}\n`);
|
|
15
|
+
}
|
|
16
|
+
await new Promise((resolve) => this.file.end(resolve));
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -1,49 +1,12 @@
|
|
|
1
|
-
import type { Logger } from "../log/LogsCollector.ts";
|
|
2
1
|
import type { RunFolder } from "../folder/RunFolder.ts";
|
|
3
|
-
/** A written (or in-flight) snapshot file and where to find it. */
|
|
4
|
-
export interface SnapshotRef {
|
|
5
|
-
/** Absolute path to the snapshot file. */
|
|
6
|
-
readonly file: string;
|
|
7
|
-
/** Path relative to cwd — what a log line references. */
|
|
8
|
-
readonly rel: string;
|
|
9
|
-
/** Resolves once the file is flushed to disk. */
|
|
10
|
-
readonly written: Promise<void>;
|
|
11
|
-
}
|
|
12
2
|
/**
|
|
13
|
-
* Writes
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* Sequence numbers make repeated snapshots of the same name a history rather than an
|
|
17
|
-
* overwrite. Paths are computed synchronously so a caller can reference the file in
|
|
18
|
-
* the same tick while the write proceeds in the background. Depends on a
|
|
19
|
-
* {@link RunFolder} (where the files land) and a {@link Logger} (only `snapshot`
|
|
20
|
-
* writes a timeline line).
|
|
3
|
+
* Writes JSON snapshot payloads under the run folder's `snapshots/` dir and returns
|
|
4
|
+
* each file's **absolute** path (for an absolute link in the log). Sequenced so
|
|
5
|
+
* names are unique and ordered; attribution (the timeline line) is the caller's job.
|
|
21
6
|
*/
|
|
22
7
|
export declare class SnapshotStore {
|
|
23
|
-
private seq;
|
|
24
8
|
private readonly folder;
|
|
25
|
-
private
|
|
26
|
-
constructor(folder: RunFolder
|
|
27
|
-
|
|
28
|
-
* Writes `value` as a sequenced JSON snapshot file (`<name>-NN.json`) and returns its
|
|
29
|
-
* path — **without** logging a pointer line. The path is computed synchronously while
|
|
30
|
-
* the write proceeds in the background. Use {@link snapshot} when you also want a
|
|
31
|
-
* `[snapshot]` line in the timeline; the event bus uses this directly to link a
|
|
32
|
-
* non-string payload from its own line.
|
|
33
|
-
*/
|
|
34
|
-
captureJson(name: string, value: unknown): SnapshotRef;
|
|
35
|
-
/**
|
|
36
|
-
* Writes raw text `lines` as a sequenced `.log` snapshot file — the text sibling of
|
|
37
|
-
* {@link captureJson}. {@link RunLog.write} uses it to off-load a collapsed output run
|
|
38
|
-
* from `run.log`, keeping the burst's full text one click away.
|
|
39
|
-
*/
|
|
40
|
-
captureText(name: string, lines: readonly string[]): SnapshotRef;
|
|
41
|
-
/**
|
|
42
|
-
* Captures a value as a JSON snapshot (via {@link captureJson}) and records a
|
|
43
|
-
* `[snapshot] <name>: <path>` line in the timeline at the point it's taken. Callable
|
|
44
|
-
* any time, anywhere in the run; resolves with the file path once written.
|
|
45
|
-
*/
|
|
46
|
-
snapshot(name: string, value: unknown): Promise<string>;
|
|
47
|
-
/** Shared writer: sequence + sanitize the name, write in the background, return its refs. */
|
|
48
|
-
private capture;
|
|
9
|
+
private seq;
|
|
10
|
+
constructor(folder: RunFolder);
|
|
11
|
+
write(name: string, value: unknown, nodeId: string): Promise<string>;
|
|
49
12
|
}
|
|
@@ -1,61 +1,21 @@
|
|
|
1
|
-
import { writeFile } from "node:fs/promises";
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
import { serialize } from "../../utils/serialize.js";
|
|
3
|
+
import { fsSafe } from "../../foundation/fsSafe.js";
|
|
5
4
|
/**
|
|
6
|
-
* Writes
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* Sequence numbers make repeated snapshots of the same name a history rather than an
|
|
10
|
-
* overwrite. Paths are computed synchronously so a caller can reference the file in
|
|
11
|
-
* the same tick while the write proceeds in the background. Depends on a
|
|
12
|
-
* {@link RunFolder} (where the files land) and a {@link Logger} (only `snapshot`
|
|
13
|
-
* writes a timeline line).
|
|
5
|
+
* Writes JSON snapshot payloads under the run folder's `snapshots/` dir and returns
|
|
6
|
+
* each file's **absolute** path (for an absolute link in the log). Sequenced so
|
|
7
|
+
* names are unique and ordered; attribution (the timeline line) is the caller's job.
|
|
14
8
|
*/
|
|
15
9
|
export class SnapshotStore {
|
|
16
|
-
seq = 0;
|
|
17
10
|
folder;
|
|
18
|
-
|
|
19
|
-
constructor(folder
|
|
11
|
+
seq = 0;
|
|
12
|
+
constructor(folder) {
|
|
20
13
|
this.folder = folder;
|
|
21
|
-
this.log = log;
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* Writes `value` as a sequenced JSON snapshot file (`<name>-NN.json`) and returns its
|
|
25
|
-
* path — **without** logging a pointer line. The path is computed synchronously while
|
|
26
|
-
* the write proceeds in the background. Use {@link snapshot} when you also want a
|
|
27
|
-
* `[snapshot]` line in the timeline; the event bus uses this directly to link a
|
|
28
|
-
* non-string payload from its own line.
|
|
29
|
-
*/
|
|
30
|
-
captureJson(name, value) {
|
|
31
|
-
return this.capture(name, "json", `${serialize(value)}\n`);
|
|
32
|
-
}
|
|
33
|
-
/**
|
|
34
|
-
* Writes raw text `lines` as a sequenced `.log` snapshot file — the text sibling of
|
|
35
|
-
* {@link captureJson}. {@link RunLog.write} uses it to off-load a collapsed output run
|
|
36
|
-
* from `run.log`, keeping the burst's full text one click away.
|
|
37
|
-
*/
|
|
38
|
-
captureText(name, lines) {
|
|
39
|
-
return this.capture(name, "log", lines.length ? `${lines.join("\n")}\n` : "");
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* Captures a value as a JSON snapshot (via {@link captureJson}) and records a
|
|
43
|
-
* `[snapshot] <name>: <path>` line in the timeline at the point it's taken. Callable
|
|
44
|
-
* any time, anywhere in the run; resolves with the file path once written.
|
|
45
|
-
*/
|
|
46
|
-
snapshot(name, value) {
|
|
47
|
-
const { file, rel, written } = this.captureJson(name, value);
|
|
48
|
-
// Record the pointer synchronously, so it sits at the right spot in the timeline.
|
|
49
|
-
this.log.append({ source: "snapshot", level: "snapshot", message: `${name}: ${rel}` });
|
|
50
|
-
return written.then(() => file);
|
|
51
14
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
// Normalize to forward slashes so the reference is portable in the log artifact.
|
|
58
|
-
const rel = path.relative(process.cwd(), file).replaceAll("\\", "/");
|
|
59
|
-
return { file, rel, written };
|
|
15
|
+
async write(name, value, nodeId) {
|
|
16
|
+
const file = this.folder.file(path.join("snapshots", `${++this.seq}-${fsSafe(nodeId)}-${fsSafe(name)}.json`));
|
|
17
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
18
|
+
await writeFile(file, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
19
|
+
return file;
|
|
60
20
|
}
|
|
61
21
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { LogEntry } from "../../foundation/LogEntry.ts";
|
|
2
|
+
import type { NodeView } from "./views.ts";
|
|
3
|
+
/**
|
|
4
|
+
* Writes each node's own human `.log`: a one-time `ActionRef` header, then that
|
|
5
|
+
* node's entries (by `nodeId`) prefixed `[seq iso-ts path]`. Rows never repeat
|
|
6
|
+
* name/tags — the header carries them.
|
|
7
|
+
*/
|
|
8
|
+
export declare function writeNodeLogs(nodes: readonly NodeView[], entries: readonly LogEntry[]): Promise<void>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { renderRef } from "../../foundation/ActionRef.js";
|
|
4
|
+
import { entryLine, groupByNode } from "./entryLine.js";
|
|
5
|
+
/**
|
|
6
|
+
* Writes each node's own human `.log`: a one-time `ActionRef` header, then that
|
|
7
|
+
* node's entries (by `nodeId`) prefixed `[seq iso-ts path]`. Rows never repeat
|
|
8
|
+
* name/tags — the header carries them.
|
|
9
|
+
*/
|
|
10
|
+
export async function writeNodeLogs(nodes, entries) {
|
|
11
|
+
const byNode = groupByNode(entries, () => true);
|
|
12
|
+
await Promise.all(nodes.map(async (node) => {
|
|
13
|
+
const lines = (byNode.get(node.id) ?? []).map(entryLine);
|
|
14
|
+
const body = [...header(node), "", ...lines, ""].join("\n");
|
|
15
|
+
await mkdir(path.dirname(node.logFile), { recursive: true });
|
|
16
|
+
await writeFile(node.logFile, body, "utf8");
|
|
17
|
+
}));
|
|
18
|
+
}
|
|
19
|
+
/** The per-file metadata header: ref, args, start/finish times, duration, status. */
|
|
20
|
+
const header = (node) => [
|
|
21
|
+
`# ${renderRef(node)}`,
|
|
22
|
+
`# log: ${node.logFile}`,
|
|
23
|
+
`# args: ${renderArgs(node.args)}`,
|
|
24
|
+
`# started: ${stamp(node.startedAt)}`,
|
|
25
|
+
`# finished: ${stamp(node.endedAt)} (${node.duration}ms)`,
|
|
26
|
+
`# status: ${node.status}`,
|
|
27
|
+
];
|
|
28
|
+
const stamp = (ms) => (ms ? new Date(ms).toISOString() : "-");
|
|
29
|
+
const renderArgs = (args) => {
|
|
30
|
+
try {
|
|
31
|
+
return JSON.stringify(args);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return String(args);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { LogEntry } from "../../foundation/LogEntry.ts";
|
|
2
|
+
import type { NodeView } from "./views.ts";
|
|
3
|
+
/**
|
|
4
|
+
* A cross-cutting rollup: entries matching one facet (e.g. `level === "event"`), in
|
|
5
|
+
* **global sequence order**. A little action header is inserted whenever the
|
|
6
|
+
* emitting action changes, so the file reads as a chronological timeline of that
|
|
7
|
+
* facet across the run — not grouped per action. `footer`, if given, is appended.
|
|
8
|
+
*/
|
|
9
|
+
export declare function writeRollup(file: string, nodes: readonly NodeView[], entries: readonly LogEntry[], match: (e: LogEntry) => boolean, footer?: string): Promise<void>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { writeFile } from "node:fs/promises";
|
|
2
|
+
import { renderRef } from "../../foundation/ActionRef.js";
|
|
3
|
+
import { entryLine } from "./entryLine.js";
|
|
4
|
+
/**
|
|
5
|
+
* A cross-cutting rollup: entries matching one facet (e.g. `level === "event"`), in
|
|
6
|
+
* **global sequence order**. A little action header is inserted whenever the
|
|
7
|
+
* emitting action changes, so the file reads as a chronological timeline of that
|
|
8
|
+
* facet across the run — not grouped per action. `footer`, if given, is appended.
|
|
9
|
+
*/
|
|
10
|
+
export async function writeRollup(file, nodes, entries, match, footer) {
|
|
11
|
+
const refByNode = new Map(nodes.map((n) => [n.id, n]));
|
|
12
|
+
const chunks = [];
|
|
13
|
+
let lastNode = null;
|
|
14
|
+
for (const e of entries) {
|
|
15
|
+
if (!match(e))
|
|
16
|
+
continue;
|
|
17
|
+
if (e.nodeId !== lastNode) {
|
|
18
|
+
if (chunks.length)
|
|
19
|
+
chunks.push(""); // blank line before the next action's header
|
|
20
|
+
const ref = refByNode.get(e.nodeId);
|
|
21
|
+
chunks.push(`# ${ref ? renderRef(ref) : e.path}`);
|
|
22
|
+
lastNode = e.nodeId;
|
|
23
|
+
}
|
|
24
|
+
chunks.push(entryLine(e));
|
|
25
|
+
}
|
|
26
|
+
if (footer)
|
|
27
|
+
chunks.push("", footer);
|
|
28
|
+
await writeFile(file, `${chunks.join("\n")}\n`, "utf8");
|
|
29
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { LogEntry } from "../../foundation/LogEntry.ts";
|
|
2
|
+
/**
|
|
3
|
+
* The shared human-readable row: `[<seq>] <body>`. Only the run-global sequence
|
|
4
|
+
* prefixes the line (date/path live in the file header + `raw.jsonl`). Event,
|
|
5
|
+
* snapshot, and assert lines carry a leading icon; everything else keeps its
|
|
6
|
+
* `source` (proc name, child id, `console`, …).
|
|
7
|
+
*/
|
|
8
|
+
export declare const entryLine: (e: LogEntry) => string;
|
|
9
|
+
/** Group entries by `nodeId` (preserving order), keeping only those that `match`. */
|
|
10
|
+
export declare const groupByNode: (entries: readonly LogEntry[], match: (e: LogEntry) => boolean) => Map<string, LogEntry[]>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared human-readable row: `[<seq>] <body>`. Only the run-global sequence
|
|
3
|
+
* prefixes the line (date/path live in the file header + `raw.jsonl`). Event,
|
|
4
|
+
* snapshot, and assert lines carry a leading icon; everything else keeps its
|
|
5
|
+
* `source` (proc name, child id, `console`, …).
|
|
6
|
+
*/
|
|
7
|
+
export const entryLine = (e) => `[${e.sequence}] ${bodyOf(e)}`;
|
|
8
|
+
const bodyOf = (e) => {
|
|
9
|
+
switch (e.level) {
|
|
10
|
+
case "event":
|
|
11
|
+
return `⚡ ${e.message}`;
|
|
12
|
+
case "snapshot":
|
|
13
|
+
return `📸 ${e.message}`;
|
|
14
|
+
case "assert":
|
|
15
|
+
return e.message; // already ⊨/⊭-prefixed
|
|
16
|
+
// Plain output (console.log, proc stdout) — no action-name prefix, just a quiet marker.
|
|
17
|
+
case "info":
|
|
18
|
+
return `▪ ${e.message}`;
|
|
19
|
+
case "error":
|
|
20
|
+
// An action's *own* failure keeps a ✗; proc stderr / console.error is plain
|
|
21
|
+
// output. (Cancellation no longer logs at this level — it's a clean stop.)
|
|
22
|
+
return e.source === "error" ? `✗ ${e.message}` : `▪ ${e.message}`;
|
|
23
|
+
// run/done milestones, tags, and bubbled child lines keep their `source` label.
|
|
24
|
+
default:
|
|
25
|
+
return `${e.source} · ${e.message}`;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
/** Group entries by `nodeId` (preserving order), keeping only those that `match`. */
|
|
29
|
+
export const groupByNode = (entries, match) => {
|
|
30
|
+
const byNode = new Map();
|
|
31
|
+
for (const e of entries) {
|
|
32
|
+
if (!match(e))
|
|
33
|
+
continue;
|
|
34
|
+
const list = byNode.get(e.nodeId) ?? [];
|
|
35
|
+
list.push(e);
|
|
36
|
+
byNode.set(e.nodeId, list);
|
|
37
|
+
}
|
|
38
|
+
return byNode;
|
|
39
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plain, serializable projections the `core` layer builds from the tree and hands
|
|
3
|
+
* to the writers. Kept here (output) as data-only shapes so the writers never
|
|
4
|
+
* import `core`.
|
|
5
|
+
*/
|
|
6
|
+
export interface NodeView {
|
|
7
|
+
readonly id: string;
|
|
8
|
+
readonly uuid: string;
|
|
9
|
+
readonly name: string;
|
|
10
|
+
readonly path: string;
|
|
11
|
+
readonly parentId: string | null;
|
|
12
|
+
readonly tags: readonly string[];
|
|
13
|
+
readonly args: readonly unknown[];
|
|
14
|
+
readonly status: string;
|
|
15
|
+
readonly startedAt: number;
|
|
16
|
+
readonly endedAt: number;
|
|
17
|
+
readonly duration: number;
|
|
18
|
+
/** Absolute path to this node's own `.log`. */
|
|
19
|
+
readonly logFile: string;
|
|
20
|
+
readonly children: readonly NodeView[];
|
|
21
|
+
}
|
|
22
|
+
export interface RunView {
|
|
23
|
+
readonly ok: boolean;
|
|
24
|
+
readonly failures: ReadonlyArray<{
|
|
25
|
+
action: string;
|
|
26
|
+
error: string;
|
|
27
|
+
}>;
|
|
28
|
+
readonly assertions: {
|
|
29
|
+
readonly passed: number;
|
|
30
|
+
readonly failed: number;
|
|
31
|
+
};
|
|
32
|
+
readonly startedAt: number;
|
|
33
|
+
readonly endedAt: number;
|
|
34
|
+
readonly duration: number;
|
|
35
|
+
/** Absolute path to `raw.jsonl`. */
|
|
36
|
+
readonly rawStream: string;
|
|
37
|
+
readonly root: NodeView;
|
|
38
|
+
}
|
|
@@ -2,46 +2,18 @@
|
|
|
2
2
|
* An input-fingerprint cache: hash the mtime+size of one or more **input**
|
|
3
3
|
* targets (the files/folders an action reads — *not* what it produces), so an
|
|
4
4
|
* expensive action can be skipped when its inputs are unchanged since its last
|
|
5
|
-
* successful run. Backs
|
|
6
|
-
*
|
|
7
|
-
* Entries live under `node_modules/.cache/omkit/` (conventional, gitignored) —
|
|
8
|
-
* one JSON file per set of inputs, named by a hash of their absolute paths.
|
|
9
|
-
*
|
|
10
|
-
* Stateless — the cache is the filesystem, so the methods are `static`. Internal
|
|
11
|
-
* cross-references go through the class name (not `this`), so a caller may pull a
|
|
12
|
-
* method off the class (`const fp = FolderCache.fingerprint`) without losing it.
|
|
5
|
+
* successful run. Backs `.withCache(...)` on action instances. Entries live under
|
|
6
|
+
* `node_modules/.cache/omkit/`. Stateless — the cache is the filesystem.
|
|
13
7
|
*/
|
|
14
8
|
export declare class FolderCache {
|
|
15
|
-
/** Where cache entries are written — `node_modules/.cache/omkit/` under the cwd. */
|
|
16
9
|
static readonly dir: string;
|
|
17
|
-
/**
|
|
18
|
-
* Require every path to be **absolute** (a relative path is a developer error —
|
|
19
|
-
* throws, naming it) and return them canonicalized with `resolve`, so different
|
|
20
|
-
* spellings of the same target (`.`/`..`/trailing slash) collapse to one stable
|
|
21
|
-
* fingerprint prefix and cache key. `resolve` on an already-absolute path only
|
|
22
|
-
* normalizes — it never falls back to the cwd. Each target may be a file or a
|
|
23
|
-
* folder (see {@link FolderCache.fingerprint}).
|
|
24
|
-
*/
|
|
10
|
+
/** Require absolute paths (throws otherwise) and canonicalize them for a stable key. */
|
|
25
11
|
static resolvePaths(paths: string[]): string[];
|
|
26
|
-
/**
|
|
27
|
-
* Fingerprints one or more **input** targets by mtime+size. Each target may be
|
|
28
|
-
* a **file** (fingerprinted directly) or a **folder** (walked recursively, its
|
|
29
|
-
* files fingerprinted). Every path must be absolute (throws otherwise). A
|
|
30
|
-
* missing target contributes nothing — so an input that doesn't exist yet is
|
|
31
|
-
* naturally a miss, not an error. The entry list is sorted for determinism
|
|
32
|
-
* before hashing.
|
|
33
|
-
*/
|
|
12
|
+
/** Fingerprint input targets by mtime+size (files directly, folders walked). Missing = no entries. */
|
|
34
13
|
static fingerprint(paths: string[]): Promise<string>;
|
|
35
|
-
/**
|
|
36
|
-
* Reads the fingerprint stored for these targets, or `undefined` when there's
|
|
37
|
-
* no cache (absent, unreadable, or malformed — all treated as a miss).
|
|
38
|
-
*/
|
|
14
|
+
/** The stored fingerprint for these targets, or `undefined` on any miss. */
|
|
39
15
|
static read(paths: string[]): Promise<string | undefined>;
|
|
40
|
-
/**
|
|
41
|
-
* Records the fingerprint for these targets after a successful run. Writes
|
|
42
|
-
* atomically (temp file + rename) so a concurrent read never sees a half-file.
|
|
43
|
-
*/
|
|
16
|
+
/** Record the fingerprint after a successful run (atomic temp+rename). */
|
|
44
17
|
static write(paths: string[], fp: string): Promise<void>;
|
|
45
|
-
/** The on-disk filename for a set of targets — a hash of their absolute paths. */
|
|
46
18
|
private static entryPath;
|
|
47
19
|
}
|
|
@@ -5,42 +5,20 @@ import { isAbsolute, join, resolve } from "node:path";
|
|
|
5
5
|
* An input-fingerprint cache: hash the mtime+size of one or more **input**
|
|
6
6
|
* targets (the files/folders an action reads — *not* what it produces), so an
|
|
7
7
|
* expensive action can be skipped when its inputs are unchanged since its last
|
|
8
|
-
* successful run. Backs
|
|
9
|
-
*
|
|
10
|
-
* Entries live under `node_modules/.cache/omkit/` (conventional, gitignored) —
|
|
11
|
-
* one JSON file per set of inputs, named by a hash of their absolute paths.
|
|
12
|
-
*
|
|
13
|
-
* Stateless — the cache is the filesystem, so the methods are `static`. Internal
|
|
14
|
-
* cross-references go through the class name (not `this`), so a caller may pull a
|
|
15
|
-
* method off the class (`const fp = FolderCache.fingerprint`) without losing it.
|
|
8
|
+
* successful run. Backs `.withCache(...)` on action instances. Entries live under
|
|
9
|
+
* `node_modules/.cache/omkit/`. Stateless — the cache is the filesystem.
|
|
16
10
|
*/
|
|
17
11
|
export class FolderCache {
|
|
18
|
-
/** Where cache entries are written — `node_modules/.cache/omkit/` under the cwd. */
|
|
19
12
|
static dir = join(process.cwd(), "node_modules", ".cache", "omkit");
|
|
20
|
-
/**
|
|
21
|
-
* Require every path to be **absolute** (a relative path is a developer error —
|
|
22
|
-
* throws, naming it) and return them canonicalized with `resolve`, so different
|
|
23
|
-
* spellings of the same target (`.`/`..`/trailing slash) collapse to one stable
|
|
24
|
-
* fingerprint prefix and cache key. `resolve` on an already-absolute path only
|
|
25
|
-
* normalizes — it never falls back to the cwd. Each target may be a file or a
|
|
26
|
-
* folder (see {@link FolderCache.fingerprint}).
|
|
27
|
-
*/
|
|
13
|
+
/** Require absolute paths (throws otherwise) and canonicalize them for a stable key. */
|
|
28
14
|
static resolvePaths(paths) {
|
|
29
15
|
return paths.map((p) => {
|
|
30
|
-
if (!isAbsolute(p))
|
|
16
|
+
if (!isAbsolute(p))
|
|
31
17
|
throw new Error(`withCache: path must be absolute, got "${p}"`);
|
|
32
|
-
}
|
|
33
18
|
return resolve(p);
|
|
34
19
|
});
|
|
35
20
|
}
|
|
36
|
-
/**
|
|
37
|
-
* Fingerprints one or more **input** targets by mtime+size. Each target may be
|
|
38
|
-
* a **file** (fingerprinted directly) or a **folder** (walked recursively, its
|
|
39
|
-
* files fingerprinted). Every path must be absolute (throws otherwise). A
|
|
40
|
-
* missing target contributes nothing — so an input that doesn't exist yet is
|
|
41
|
-
* naturally a miss, not an error. The entry list is sorted for determinism
|
|
42
|
-
* before hashing.
|
|
43
|
-
*/
|
|
21
|
+
/** Fingerprint input targets by mtime+size (files directly, folders walked). Missing = no entries. */
|
|
44
22
|
static async fingerprint(paths) {
|
|
45
23
|
const targets = FolderCache.resolvePaths(paths);
|
|
46
24
|
const entries = [];
|
|
@@ -55,12 +33,10 @@ export class FolderCache {
|
|
|
55
33
|
catch {
|
|
56
34
|
continue; // missing/unreadable — contributes no entries
|
|
57
35
|
}
|
|
58
|
-
// A file target fingerprints itself (empty rel — there's no walk).
|
|
59
36
|
if (root.isFile()) {
|
|
60
37
|
record(target, "", root.mtimeMs, root.size);
|
|
61
38
|
continue;
|
|
62
39
|
}
|
|
63
|
-
// A folder target: walk it and fingerprint every file inside.
|
|
64
40
|
let rels;
|
|
65
41
|
try {
|
|
66
42
|
rels = await readdir(target, { recursive: true });
|
|
@@ -83,10 +59,7 @@ export class FolderCache {
|
|
|
83
59
|
entries.sort();
|
|
84
60
|
return createHash("sha1").update(entries.join("\n")).digest("hex");
|
|
85
61
|
}
|
|
86
|
-
/**
|
|
87
|
-
* Reads the fingerprint stored for these targets, or `undefined` when there's
|
|
88
|
-
* no cache (absent, unreadable, or malformed — all treated as a miss).
|
|
89
|
-
*/
|
|
62
|
+
/** The stored fingerprint for these targets, or `undefined` on any miss. */
|
|
90
63
|
static async read(paths) {
|
|
91
64
|
try {
|
|
92
65
|
const raw = await readFile(FolderCache.entryPath(paths), "utf8");
|
|
@@ -97,10 +70,7 @@ export class FolderCache {
|
|
|
97
70
|
return undefined;
|
|
98
71
|
}
|
|
99
72
|
}
|
|
100
|
-
/**
|
|
101
|
-
* Records the fingerprint for these targets after a successful run. Writes
|
|
102
|
-
* atomically (temp file + rename) so a concurrent read never sees a half-file.
|
|
103
|
-
*/
|
|
73
|
+
/** Record the fingerprint after a successful run (atomic temp+rename). */
|
|
104
74
|
static async write(paths, fp) {
|
|
105
75
|
await mkdir(FolderCache.dir, { recursive: true });
|
|
106
76
|
const target = FolderCache.entryPath(paths);
|
|
@@ -109,7 +79,6 @@ export class FolderCache {
|
|
|
109
79
|
await writeFile(tmp, JSON.stringify(entry, null, 2));
|
|
110
80
|
await rename(tmp, target);
|
|
111
81
|
}
|
|
112
|
-
/** The on-disk filename for a set of targets — a hash of their absolute paths. */
|
|
113
82
|
static entryPath(paths) {
|
|
114
83
|
const key = createHash("sha1")
|
|
115
84
|
.update(FolderCache.resolvePaths(paths).sort().join("\0"))
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { type Options, type ProcessPromise } from "zx";
|
|
2
|
+
import { ProcRegistry } from "./procRegistry.ts";
|
|
3
|
+
/**
|
|
4
|
+
* Force-kill a process **and its whole tree, synchronously**.
|
|
5
|
+
*
|
|
6
|
+
* On POSIX the child is spawned as its own process-group leader (see createProc), so
|
|
7
|
+
* `kill(-pid)` reaps the whole group in one call — every descendant, re-parented or not.
|
|
8
|
+
*
|
|
9
|
+
* On Windows there are no process groups and we can't detach (a console-less child breaks
|
|
10
|
+
* npm/vite — see createProc), so a mashed Ctrl+C sent to the shared console can make an
|
|
11
|
+
* intermediate shell (powershell/npm/npx) exit and orphan its real leaf (vite,
|
|
12
|
+
* chromedriver.exe) before teardown reaps it. `taskkill /F /T` force-kills the live tree
|
|
13
|
+
* by PID, but a leaf whose intermediate already exited can't be reached via `/T` — that
|
|
14
|
+
* mash-Ctrl+C orphan is a known Windows edge, mitigated by a port/PID sweep on teardown
|
|
15
|
+
* rather than by detaching. Best-effort: an already-dead PID is ignored.
|
|
16
|
+
*/
|
|
17
|
+
export declare const killTree: (pid: number) => void;
|
|
18
|
+
/**
|
|
19
|
+
* A copy of `env` with the orchestrator's debugger stripped, so a spawned child runs
|
|
20
|
+
* clean instead of inheriting our inspector. Removes `--inspect*` flags from
|
|
21
|
+
* `NODE_OPTIONS`, drops VS Code auto-attach's injected bootloader `--require` and its
|
|
22
|
+
* `VSCODE_INSPECTOR_OPTIONS` handshake, and deletes `NODE_OPTIONS` entirely if nothing
|
|
23
|
+
* else remains. A legitimate non-debug `--require` and any other options are preserved.
|
|
24
|
+
* Does not mutate the input.
|
|
25
|
+
*/
|
|
26
|
+
export declare const debuggerFreeEnv: (env: NodeJS.ProcessEnv) => NodeJS.ProcessEnv;
|
|
27
|
+
/** Where a proc writes its lines — bound by the framework to the owning node's log. */
|
|
28
|
+
export type ProcSink = (source: string, level: string, message: string) => void;
|
|
29
|
+
/** zx spawn options plus omkit's own knobs. */
|
|
30
|
+
export type ProcOptions = Partial<Options> & {
|
|
31
|
+
/**
|
|
32
|
+
* Let this child inherit the orchestrator's debugger instead of running clean
|
|
33
|
+
* (default false). Also enabled for every child by the `OMKIT_INHERIT_DEBUGGER`
|
|
34
|
+
* env var. See {@link debuggerFreeEnv}.
|
|
35
|
+
*/
|
|
36
|
+
inheritDebugger?: boolean;
|
|
37
|
+
};
|
|
38
|
+
/** Spawn a shell command (zx tagged-template), streaming its output into the log. */
|
|
39
|
+
export type Proc = (name: string, opts?: ProcOptions) => (pieces: TemplateStringsArray, ...args: unknown[]) => ProcessPromise;
|
|
40
|
+
/**
|
|
41
|
+
* The run-wide orphan reaper. `createProc` registers each shell it spawns (Windows only);
|
|
42
|
+
* the ExecutionTree calls `procRegistry.sweep()` at teardown to reap any that a mashed
|
|
43
|
+
* Ctrl+C orphaned. A no-op in practice off Windows (nothing is tracked there).
|
|
44
|
+
*/
|
|
45
|
+
export declare const procRegistry: ProcRegistry;
|
|
46
|
+
/**
|
|
47
|
+
* Build a {@link Proc} bound to a log sink and abort signal. The framework injects
|
|
48
|
+
* one per action as `ctx.proc`, wired to that action's log and the run's signal, so
|
|
49
|
+
* actions just describe the command — output streams in, the child dies on teardown.
|
|
50
|
+
*/
|
|
51
|
+
export declare const createProc: (sink: ProcSink, signal: AbortSignal) => Proc;
|