omkit 0.0.14 → 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.
Files changed (110) hide show
  1. package/README.md +110 -97
  2. package/dist/actions/chrome-page.d.ts +12 -29
  3. package/dist/actions/chrome-page.js +19 -32
  4. package/dist/actions/command.d.ts +8 -41
  5. package/dist/actions/command.js +15 -82
  6. package/dist/actions/healthcheck.d.ts +3 -11
  7. package/dist/actions/healthcheck.js +8 -22
  8. package/dist/actions/index.d.ts +6 -8
  9. package/dist/actions/index.js +4 -6
  10. package/dist/actions/prompt.d.ts +5 -19
  11. package/dist/actions/prompt.js +10 -31
  12. package/dist/actions/tail-log.d.ts +7 -21
  13. package/dist/actions/tail-log.js +13 -34
  14. package/dist/actions/until-log.d.ts +4 -7
  15. package/dist/actions/until-log.js +3 -3
  16. package/dist/actions/watch-dir.d.ts +5 -19
  17. package/dist/actions/watch-dir.js +13 -28
  18. package/dist/actions/watch.d.ts +4 -12
  19. package/dist/actions/watch.js +9 -16
  20. package/dist/core/ActionRun.d.ts +97 -0
  21. package/dist/core/ActionRun.js +266 -0
  22. package/dist/core/ExecutionTree.d.ts +88 -0
  23. package/dist/core/ExecutionTree.js +320 -0
  24. package/dist/core/action.d.ts +6 -0
  25. package/dist/core/action.js +72 -0
  26. package/dist/core/context.d.ts +9 -0
  27. package/dist/core/context.js +8 -0
  28. package/dist/core/om.d.ts +9 -0
  29. package/dist/core/om.js +25 -0
  30. package/dist/core/step.d.ts +9 -0
  31. package/dist/core/step.js +18 -0
  32. package/dist/core/types.d.ts +128 -0
  33. package/dist/foundation/ActionRef.d.ts +18 -0
  34. package/dist/foundation/ActionRef.js +5 -0
  35. package/dist/{utils → foundation}/AsyncQueue.d.ts +2 -4
  36. package/dist/{utils → foundation}/AsyncQueue.js +6 -19
  37. package/dist/foundation/CancelledError.d.ts +10 -0
  38. package/dist/foundation/CancelledError.js +13 -0
  39. package/dist/foundation/Deferred.d.ts +8 -0
  40. package/dist/foundation/Deferred.js +10 -0
  41. package/dist/foundation/LogEntry.d.ts +29 -0
  42. package/dist/{orchestration/events → foundation}/events.d.ts +2 -11
  43. package/dist/{orchestration/events → foundation}/events.js +1 -7
  44. package/dist/{utils → foundation}/format.d.ts +3 -4
  45. package/dist/{utils → foundation}/format.js +12 -4
  46. package/dist/foundation/fsSafe.d.ts +7 -0
  47. package/dist/foundation/fsSafe.js +7 -0
  48. package/dist/foundation/ids.d.ts +9 -0
  49. package/dist/foundation/ids.js +10 -0
  50. package/dist/index.d.ts +11 -22
  51. package/dist/index.js +10 -30
  52. package/dist/output/LiveRenderer.d.ts +13 -0
  53. package/dist/output/LiveRenderer.js +56 -0
  54. package/dist/output/console/ConsoleCapture.d.ts +20 -0
  55. package/dist/output/console/ConsoleCapture.js +47 -0
  56. package/dist/output/folder/RunFolder.d.ts +11 -22
  57. package/dist/output/folder/RunFolder.js +18 -36
  58. package/dist/output/log/LogStore.d.ts +22 -0
  59. package/dist/output/log/LogStore.js +56 -0
  60. package/dist/output/log/RawStream.d.ts +11 -0
  61. package/dist/output/log/RawStream.js +18 -0
  62. package/dist/output/snapshot/SnapshotStore.d.ts +6 -43
  63. package/dist/output/snapshot/SnapshotStore.js +12 -52
  64. package/dist/output/writers/NodeLogWriter.d.ts +8 -0
  65. package/dist/output/writers/NodeLogWriter.js +36 -0
  66. package/dist/output/writers/ResultWriter.d.ts +3 -0
  67. package/dist/output/writers/ResultWriter.js +5 -0
  68. package/dist/output/writers/RollupWriter.d.ts +9 -0
  69. package/dist/output/writers/RollupWriter.js +29 -0
  70. package/dist/output/writers/entryLine.d.ts +10 -0
  71. package/dist/output/writers/entryLine.js +39 -0
  72. package/dist/output/writers/views.d.ts +38 -0
  73. package/dist/output/writers/views.js +6 -0
  74. package/dist/system/fs/FolderCache.d.ts +6 -34
  75. package/dist/system/fs/FolderCache.js +7 -38
  76. package/dist/system/proc.d.ts +51 -0
  77. package/dist/system/proc.js +148 -0
  78. package/dist/system/procRegistry.d.ts +52 -0
  79. package/dist/system/procRegistry.js +117 -0
  80. package/package.json +3 -1
  81. package/dist/orchestration/action/action.d.ts +0 -23
  82. package/dist/orchestration/action/action.js +0 -256
  83. package/dist/orchestration/action/types.d.ts +0 -182
  84. package/dist/orchestration/assert.d.ts +0 -22
  85. package/dist/orchestration/assert.js +0 -26
  86. package/dist/orchestration/spin/SpinHost.d.ts +0 -109
  87. package/dist/orchestration/spin/SpinHost.js +0 -337
  88. package/dist/orchestration/spin/spin.d.ts +0 -24
  89. package/dist/orchestration/spin/spin.js +0 -57
  90. package/dist/orchestration/spin/types.d.ts +0 -63
  91. package/dist/output/Output.d.ts +0 -24
  92. package/dist/output/Output.js +0 -27
  93. package/dist/output/index.d.ts +0 -11
  94. package/dist/output/index.js +0 -11
  95. package/dist/output/log/LogRenderer.d.ts +0 -73
  96. package/dist/output/log/LogRenderer.js +0 -133
  97. package/dist/output/log/LogsCollector.d.ts +0 -79
  98. package/dist/output/log/LogsCollector.js +0 -141
  99. package/dist/output/log/RunLog.d.ts +0 -47
  100. package/dist/output/log/RunLog.js +0 -142
  101. package/dist/output/log/console-capture.d.ts +0 -24
  102. package/dist/output/log/console-capture.js +0 -53
  103. package/dist/system/fs/streams.d.ts +0 -11
  104. package/dist/system/fs/streams.js +0 -14
  105. package/dist/system/process/process.d.ts +0 -35
  106. package/dist/system/process/process.js +0 -104
  107. package/dist/utils/serialize.d.ts +0 -2
  108. package/dist/utils/serialize.js +0 -12
  109. /package/dist/{orchestration/action → core}/types.js +0 -0
  110. /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 sequenced snapshot files into the run folder and (for {@link snapshot})
14
- * drops a `[snapshot]` pointer line into the timeline.
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 readonly log;
26
- constructor(folder: RunFolder, log: Logger);
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 { pad } from "../../utils/format.js";
4
- import { serialize } from "../../utils/serialize.js";
3
+ import { fsSafe } from "../../foundation/fsSafe.js";
5
4
  /**
6
- * Writes sequenced snapshot files into the run folder and (for {@link snapshot})
7
- * drops a `[snapshot]` pointer line into the timeline.
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
- log;
19
- constructor(folder, log) {
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
- /** Shared writer: sequence + sanitize the name, write in the background, return its refs. */
53
- capture(name, ext, body) {
54
- const safe = name.replace(/[^\w.-]+/g, "-");
55
- const file = this.folder.resolve(`${safe}-${pad(++this.seq)}.${ext}`);
56
- const written = this.folder.ensure().then(() => writeFile(file, body, "utf8"));
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,3 @@
1
+ import type { RunView } from "./views.ts";
2
+ /** Serializes the whole run tree + verdict to `result.json`. */
3
+ export declare function writeResult(file: string, run: RunView): Promise<void>;
@@ -0,0 +1,5 @@
1
+ import { writeFile } from "node:fs/promises";
2
+ /** Serializes the whole run tree + verdict to `result.json`. */
3
+ export async function writeResult(file, run) {
4
+ await writeFile(file, `${JSON.stringify(run, null, 2)}\n`, "utf8");
5
+ }
@@ -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
+ }
@@ -0,0 +1,6 @@
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 {};
@@ -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 the `.withCache(...)` method on action instances.
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 the `.withCache(...)` method on action instances.
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;