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.
- 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 -22
- package/dist/orchestration/assert.js +0 -26
- package/dist/orchestration/spin/SpinHost.d.ts +0 -109
- package/dist/orchestration/spin/SpinHost.js +0 -337
- 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 -47
- package/dist/output/log/RunLog.js +0 -142
- 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
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
const PREFIX = {
|
|
2
|
-
event: "⚡",
|
|
3
|
-
action: "▸",
|
|
4
|
-
run: "●",
|
|
5
|
-
snapshot: "📎",
|
|
6
|
-
assert: "⊨",
|
|
7
|
-
};
|
|
8
|
-
/**
|
|
9
|
-
* Levels that render as their own top-level milestone line — marker-prefixed and
|
|
10
|
-
* never grouped/indented. Everything else is action/proc output, which the
|
|
11
|
-
* renderer groups under an `▸ <source>` header and indents.
|
|
12
|
-
*/
|
|
13
|
-
const MILESTONE = {
|
|
14
|
-
run: "run",
|
|
15
|
-
event: "event",
|
|
16
|
-
snapshot: "snapshot",
|
|
17
|
-
assert: "assert",
|
|
18
|
-
};
|
|
19
|
-
/** Only runs *longer than* this collapse, so a trigger always hides a meaningful amount. */
|
|
20
|
-
const COLLAPSE_THRESHOLD = 15;
|
|
21
|
-
/**
|
|
22
|
-
* Turns raw {@link LogEntry} records into the pretty, grouped text form.
|
|
23
|
-
* Presentation lives here and nowhere else: the collector stores raw entries; the
|
|
24
|
-
* live drain and `run.log` writer each construct a renderer and feed it entries in
|
|
25
|
-
* order. Two modes over the same grouping rules — pick per instance, don't mix:
|
|
26
|
-
*
|
|
27
|
-
* - **streaming** — {@link render} returns one entry's line as it arrives (live
|
|
28
|
-
* drain, incremental `run.log`).
|
|
29
|
-
* - **batch** — {@link push}/{@link end} accumulate a same-source run and collapse
|
|
30
|
-
* it when it grows past the threshold, so one proc's burst can't drown the
|
|
31
|
-
* durable `run.log` (the caller off-loads the full run to a snapshot).
|
|
32
|
-
*
|
|
33
|
-
* Grouping is decided against the *single merged stream* (`lastSource`), not
|
|
34
|
-
* per-producer — so interleaved concurrent sources re-header correctly. A
|
|
35
|
-
* milestone line breaks the current group, so the next action line re-headers.
|
|
36
|
-
*/
|
|
37
|
-
export class LogRenderer {
|
|
38
|
-
/** Lines of a collapsed run shown inline before the pointer. */
|
|
39
|
-
static COLLAPSE_HEAD = 5;
|
|
40
|
-
/** Prefix a milestone/header line with its marker (`● run`, `▸ action`, …). */
|
|
41
|
-
static marker(kind, text) {
|
|
42
|
-
return `${PREFIX[kind]} ${text}`;
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* Renders a {@link CollapsedRun} as its `head` lines under the `▸ <source>`
|
|
46
|
-
* header, then a single indented `📎` pointer line to the off-loaded snapshot at
|
|
47
|
-
* `rel` reporting how many lines were hidden. Pure — `rel` is supplied by the
|
|
48
|
-
* caller after it writes the file.
|
|
49
|
-
*/
|
|
50
|
-
static renderCollapsed(run, { rel, head = LogRenderer.COLLAPSE_HEAD }) {
|
|
51
|
-
const shown = run.lines.slice(0, head);
|
|
52
|
-
const lines = shown.map((message, i) => i === 0 ? `${LogRenderer.marker("action", run.source)}\n\t${message}` : `\t${message}`);
|
|
53
|
-
const hidden = run.lines.length - shown.length;
|
|
54
|
-
lines.push(`\t${LogRenderer.marker("snapshot", `… +${hidden} more lines → ${rel}`)}`);
|
|
55
|
-
return lines.join("\n");
|
|
56
|
-
}
|
|
57
|
-
// Streaming grouping state: the source of the group currently open.
|
|
58
|
-
lastSource;
|
|
59
|
-
// Batch state: the same-source run currently being accumulated.
|
|
60
|
-
run;
|
|
61
|
-
threshold;
|
|
62
|
-
constructor(threshold = COLLAPSE_THRESHOLD) {
|
|
63
|
-
this.threshold = threshold;
|
|
64
|
-
}
|
|
65
|
-
/**
|
|
66
|
-
* Streaming: render one entry to its pretty line. A milestone renders as its own
|
|
67
|
-
* marker line and breaks the group; other entries are grouped under an
|
|
68
|
-
* `▸ <source>` header (first line of a group) and indented (the rest).
|
|
69
|
-
*/
|
|
70
|
-
render(entry) {
|
|
71
|
-
const milestone = MILESTONE[entry.level];
|
|
72
|
-
if (milestone) {
|
|
73
|
-
// Reset grouping: after an interjection, the next action line re-headers.
|
|
74
|
-
this.lastSource = undefined;
|
|
75
|
-
return LogRenderer.marker(milestone, entry.message);
|
|
76
|
-
}
|
|
77
|
-
if (entry.source !== this.lastSource) {
|
|
78
|
-
this.lastSource = entry.source;
|
|
79
|
-
return `${LogRenderer.marker("action", entry.source)}\n\t${entry.message}`;
|
|
80
|
-
}
|
|
81
|
-
return `\t${entry.message}`;
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* Batch: push an entry in order and get back the {@link DisplayItem}s that just
|
|
85
|
-
* became final (0, 1, or a whole flushed run). Only the current same-source run
|
|
86
|
-
* is held in memory, so segmenting a huge log stays bounded — the `run.log`
|
|
87
|
-
* writer feeds this straight from `run.jsonl` on disk. Output is byte-identical
|
|
88
|
-
* to {@link render} for non-collapsed runs. Pure — no filesystem.
|
|
89
|
-
*/
|
|
90
|
-
push(entry) {
|
|
91
|
-
const out = [];
|
|
92
|
-
const milestone = MILESTONE[entry.level];
|
|
93
|
-
if (milestone) {
|
|
94
|
-
// A milestone breaks the current run, then renders as its own line.
|
|
95
|
-
this.flush(out);
|
|
96
|
-
out.push({ kind: "line", text: LogRenderer.marker(milestone, entry.message) });
|
|
97
|
-
return out;
|
|
98
|
-
}
|
|
99
|
-
if (this.run && this.run.source === entry.source) {
|
|
100
|
-
this.run.lines.push(entry.message);
|
|
101
|
-
}
|
|
102
|
-
else {
|
|
103
|
-
this.flush(out);
|
|
104
|
-
this.run = { source: entry.source, lines: [entry.message] };
|
|
105
|
-
}
|
|
106
|
-
return out;
|
|
107
|
-
}
|
|
108
|
-
/** Batch: flush the last pending run (call once after the final {@link push}). */
|
|
109
|
-
end() {
|
|
110
|
-
const out = [];
|
|
111
|
-
this.flush(out);
|
|
112
|
-
return out;
|
|
113
|
-
}
|
|
114
|
-
// Flush the pending run into `out`: collapse it if it ran long, else emit grouped
|
|
115
|
-
// lines exactly as `render` would (header first, indent the rest).
|
|
116
|
-
flush(out) {
|
|
117
|
-
const run = this.run;
|
|
118
|
-
if (!run)
|
|
119
|
-
return;
|
|
120
|
-
if (run.lines.length > this.threshold) {
|
|
121
|
-
out.push({ kind: "collapsed", run: { source: run.source, lines: run.lines } });
|
|
122
|
-
}
|
|
123
|
-
else {
|
|
124
|
-
run.lines.forEach((message, i) => {
|
|
125
|
-
out.push({
|
|
126
|
-
kind: "line",
|
|
127
|
-
text: i === 0 ? `${LogRenderer.marker("action", run.source)}\n\t${message}` : `\t${message}`,
|
|
128
|
-
});
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
this.run = undefined;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
export interface LogEntry {
|
|
2
|
-
sequence: number;
|
|
3
|
-
timestamp: number;
|
|
4
|
-
/**
|
|
5
|
-
* Who emitted this line — an action/proc name, or `"event"`/`"run"`/`"snapshot"`.
|
|
6
|
-
* Retained so the structured log can be filtered and correlated by origin.
|
|
7
|
-
*/
|
|
8
|
-
source: string;
|
|
9
|
-
level: string;
|
|
10
|
-
message: string;
|
|
11
|
-
}
|
|
12
|
-
/** Input to a logger. `source` drives the header line and is retained on the entry. */
|
|
13
|
-
export interface LogInput {
|
|
14
|
-
source: string;
|
|
15
|
-
level: string;
|
|
16
|
-
message: string;
|
|
17
|
-
}
|
|
18
|
-
export interface SubscribeOptions {
|
|
19
|
-
/**
|
|
20
|
-
* Replay the buffered history before live entries, so a late subscriber sees
|
|
21
|
-
* everything logged so far. Default: false (live entries only).
|
|
22
|
-
*/
|
|
23
|
-
replay?: boolean;
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* What actions and the `proc` primitive log into: a raw append-only store. It
|
|
27
|
-
* keeps no presentation — grouping headers and indentation are applied at read
|
|
28
|
-
* time by the renderer (see `render.ts`), so every entry stays raw and queryable.
|
|
29
|
-
*/
|
|
30
|
-
export interface Logger {
|
|
31
|
-
/** Streams a raw text/byte source into the log, split into lines. */
|
|
32
|
-
attach(stream: AsyncIterable<unknown>, source: string, level: string): void;
|
|
33
|
-
/** Appends a single entry. */
|
|
34
|
-
append(entry: LogInput): void;
|
|
35
|
-
/** Subscribes to entries, optionally replaying history first. */
|
|
36
|
-
subscribe(options?: SubscribeOptions): AsyncIterable<LogEntry>;
|
|
37
|
-
}
|
|
38
|
-
export declare class LogsCollector implements Logger {
|
|
39
|
-
private sequence;
|
|
40
|
-
private readonly history;
|
|
41
|
-
private readonly maxHistory;
|
|
42
|
-
private readonly trimHistoryTo;
|
|
43
|
-
private readonly subscribers;
|
|
44
|
-
private ended;
|
|
45
|
-
/**
|
|
46
|
-
* Streams a raw text/byte source (e.g. a child process's stdout) into the
|
|
47
|
-
* collector: splits it into lines and appends each under the given
|
|
48
|
-
* `source`/`level`. Partial lines are buffered across chunks and a non-empty
|
|
49
|
-
* trailing fragment is flushed when the stream ends.
|
|
50
|
-
*/
|
|
51
|
-
attach(stream: AsyncIterable<unknown>, source: string, level: string): void;
|
|
52
|
-
/** Raw store: records the line verbatim, source included; presentation is the renderer's job. */
|
|
53
|
-
append(entry: LogInput): void;
|
|
54
|
-
/**
|
|
55
|
-
* End the log: every subscription returns and no new one is accepted. Called
|
|
56
|
-
* once at run finalize — *after* the last entry (e.g. `finished`) is appended —
|
|
57
|
-
* so `streamLog` captures the whole run before its stream closes. Kept separate
|
|
58
|
-
* from teardown so entries logged *during* teardown still stream/drain live.
|
|
59
|
-
*/
|
|
60
|
-
close(): void;
|
|
61
|
-
/** EOF for readers: end every live subscription and refuse to register new ones. */
|
|
62
|
-
private closeAll;
|
|
63
|
-
subscribe({ replay }?: SubscribeOptions): AsyncIterable<LogEntry>;
|
|
64
|
-
snapshot(): readonly LogEntry[];
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* A {@link Logger} decorator that prefixes every entry's `source` with a fixed path
|
|
68
|
-
* (` › `-joined), forwarding to a base logger. Handed to a **child** action (and its
|
|
69
|
-
* `proc`) so all of its output is attributed to its ancestry — `parent › child ›
|
|
70
|
-
* <source>` — while the underlying store stays the run's one log.
|
|
71
|
-
*/
|
|
72
|
-
export declare class ScopedLogger implements Logger {
|
|
73
|
-
private readonly base;
|
|
74
|
-
private readonly prefix;
|
|
75
|
-
constructor(base: Logger, path: string);
|
|
76
|
-
attach(stream: AsyncIterable<unknown>, source: string, level: string): void;
|
|
77
|
-
append(entry: LogInput): void;
|
|
78
|
-
subscribe(options?: SubscribeOptions): AsyncIterable<LogEntry>;
|
|
79
|
-
}
|
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
import { AsyncQueue } from "../../utils/AsyncQueue.js";
|
|
2
|
-
export class LogsCollector {
|
|
3
|
-
sequence = 0;
|
|
4
|
-
// Recent entries retained in memory, only to replay to late subscribers. The
|
|
5
|
-
// *complete* record is streamed to run.jsonl on disk, so this is capped — an
|
|
6
|
-
// indefinite run (dev servers, tailing big logs) must not grow without bound.
|
|
7
|
-
history = [];
|
|
8
|
-
// Cap and the size we trim back to when it's exceeded (trimming in batches keeps
|
|
9
|
-
// append amortized O(1) instead of O(n) per line once full).
|
|
10
|
-
maxHistory = 100_000;
|
|
11
|
-
trimHistoryTo = 90_000;
|
|
12
|
-
subscribers = new Set();
|
|
13
|
-
// Set once the bound signal aborts: the log has reached EOF for readers.
|
|
14
|
-
ended = false;
|
|
15
|
-
/**
|
|
16
|
-
* Streams a raw text/byte source (e.g. a child process's stdout) into the
|
|
17
|
-
* collector: splits it into lines and appends each under the given
|
|
18
|
-
* `source`/`level`. Partial lines are buffered across chunks and a non-empty
|
|
19
|
-
* trailing fragment is flushed when the stream ends.
|
|
20
|
-
*/
|
|
21
|
-
attach(stream, source, level) {
|
|
22
|
-
void splitLines(stream, (message) => this.append({ source, level, message }));
|
|
23
|
-
}
|
|
24
|
-
/** Raw store: records the line verbatim, source included; presentation is the renderer's job. */
|
|
25
|
-
append(entry) {
|
|
26
|
-
const log = {
|
|
27
|
-
source: entry.source,
|
|
28
|
-
level: entry.level,
|
|
29
|
-
message: entry.message,
|
|
30
|
-
sequence: ++this.sequence,
|
|
31
|
-
timestamp: Date.now(),
|
|
32
|
-
};
|
|
33
|
-
this.history.push(log);
|
|
34
|
-
// Cap the in-memory history — the durable copy is already on disk (run.jsonl).
|
|
35
|
-
if (this.history.length > this.maxHistory) {
|
|
36
|
-
this.history.splice(0, this.history.length - this.trimHistoryTo);
|
|
37
|
-
}
|
|
38
|
-
for (const subscriber of this.subscribers) {
|
|
39
|
-
// push the log into the queue for every subscriber to receive
|
|
40
|
-
subscriber.push(log);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* End the log: every subscription returns and no new one is accepted. Called
|
|
45
|
-
* once at run finalize — *after* the last entry (e.g. `finished`) is appended —
|
|
46
|
-
* so `streamLog` captures the whole run before its stream closes. Kept separate
|
|
47
|
-
* from teardown so entries logged *during* teardown still stream/drain live.
|
|
48
|
-
*/
|
|
49
|
-
close() {
|
|
50
|
-
this.closeAll();
|
|
51
|
-
}
|
|
52
|
-
/** EOF for readers: end every live subscription and refuse to register new ones. */
|
|
53
|
-
closeAll() {
|
|
54
|
-
this.ended = true;
|
|
55
|
-
for (const queue of this.subscribers)
|
|
56
|
-
queue.close();
|
|
57
|
-
this.subscribers.clear();
|
|
58
|
-
}
|
|
59
|
-
subscribe({ replay = false } = {}) {
|
|
60
|
-
// create a log queue for the new subscriber
|
|
61
|
-
const queue = new AsyncQueue();
|
|
62
|
-
// Optionally replay buffered history before live entries. Synchronous, so no
|
|
63
|
-
// append can interleave between the replay and registration — a late
|
|
64
|
-
// subscriber sees every entry, with no gap and no duplicate.
|
|
65
|
-
if (replay)
|
|
66
|
-
for (const entry of this.history)
|
|
67
|
-
queue.push(entry);
|
|
68
|
-
// If the run has already ended, this subscription is EOF after the replay —
|
|
69
|
-
// don't register it for entries that will never come. Otherwise, fan it in.
|
|
70
|
-
if (this.ended)
|
|
71
|
-
queue.close();
|
|
72
|
-
else
|
|
73
|
-
this.subscribers.add(queue);
|
|
74
|
-
const { subscribers } = this;
|
|
75
|
-
// logs queue for the current subscriber
|
|
76
|
-
const inner = queue[Symbol.asyncIterator]();
|
|
77
|
-
return {
|
|
78
|
-
[Symbol.asyncIterator]() {
|
|
79
|
-
return {
|
|
80
|
-
// iterates the subscriber's log queue
|
|
81
|
-
next: () => inner.next(),
|
|
82
|
-
// Unsubscribe on early exit (break/return), so a consumer that stops
|
|
83
|
-
// reading (e.g. waitForLog on a match) doesn't leave a dead queue that
|
|
84
|
-
// append keeps pushing into forever.
|
|
85
|
-
return: () => {
|
|
86
|
-
subscribers.delete(queue);
|
|
87
|
-
queue.close();
|
|
88
|
-
return Promise.resolve({ done: true, value: undefined });
|
|
89
|
-
},
|
|
90
|
-
};
|
|
91
|
-
},
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
snapshot() {
|
|
95
|
-
return this.history;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* A {@link Logger} decorator that prefixes every entry's `source` with a fixed path
|
|
100
|
-
* (` › `-joined), forwarding to a base logger. Handed to a **child** action (and its
|
|
101
|
-
* `proc`) so all of its output is attributed to its ancestry — `parent › child ›
|
|
102
|
-
* <source>` — while the underlying store stays the run's one log.
|
|
103
|
-
*/
|
|
104
|
-
export class ScopedLogger {
|
|
105
|
-
base;
|
|
106
|
-
prefix;
|
|
107
|
-
constructor(base, path) {
|
|
108
|
-
this.base = base;
|
|
109
|
-
this.prefix = `${path} › `;
|
|
110
|
-
}
|
|
111
|
-
attach(stream, source, level) {
|
|
112
|
-
this.base.attach(stream, this.prefix + source, level);
|
|
113
|
-
}
|
|
114
|
-
append(entry) {
|
|
115
|
-
this.base.append({ ...entry, source: this.prefix + entry.source });
|
|
116
|
-
}
|
|
117
|
-
subscribe(options) {
|
|
118
|
-
return this.base.subscribe(options);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
/**
|
|
122
|
-
* Splits a byte/string stream into lines, calling `onLine` for each. Partial
|
|
123
|
-
* lines are buffered across chunks and a non-empty trailing fragment is flushed
|
|
124
|
-
* when the stream ends.
|
|
125
|
-
*/
|
|
126
|
-
async function splitLines(stream, onLine) {
|
|
127
|
-
let buffer = "";
|
|
128
|
-
for await (const chunk of stream) {
|
|
129
|
-
buffer += String(chunk);
|
|
130
|
-
let newline;
|
|
131
|
-
while ((newline = buffer.indexOf("\n")) !== -1) {
|
|
132
|
-
const line = buffer.slice(0, newline).replace(/\r$/, "");
|
|
133
|
-
buffer = buffer.slice(newline + 1);
|
|
134
|
-
if (line)
|
|
135
|
-
onLine(line);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
const tail = buffer.replace(/\r$/, "");
|
|
139
|
-
if (tail)
|
|
140
|
-
onLine(tail);
|
|
141
|
-
}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import type { LogsCollector } from "./LogsCollector.ts";
|
|
2
|
-
import type { RunFolder } from "../folder/RunFolder.ts";
|
|
3
|
-
import type { SnapshotStore } from "../snapshot/SnapshotStore.ts";
|
|
4
|
-
/**
|
|
5
|
-
* Writes the run's two log files: `run.jsonl` (the complete, authoritative record,
|
|
6
|
-
* streamed live so it survives a hard kill) and `run.log` (the human timeline).
|
|
7
|
-
*
|
|
8
|
-
* Two phases: {@link stream} runs for the whole run, appending each entry to both
|
|
9
|
-
* files as it arrives; {@link write} runs once at finalize, re-rendering `run.log`
|
|
10
|
-
* from the finished `run.jsonl` in collapsed form. Depends on a {@link RunFolder}
|
|
11
|
-
* (where the files land) and a {@link SnapshotStore} (to off-load collapsed runs).
|
|
12
|
-
*/
|
|
13
|
-
export declare class RunLog {
|
|
14
|
-
private readonly folder;
|
|
15
|
-
private readonly snapshots;
|
|
16
|
-
private assertionsPath;
|
|
17
|
-
constructor(folder: RunFolder, snapshots: SnapshotStore);
|
|
18
|
-
/** The `assertions.log` path if any assertion was logged this run, else `undefined`. */
|
|
19
|
-
get assertionsLog(): string | undefined;
|
|
20
|
-
/**
|
|
21
|
-
* Streams every log entry to `run.jsonl` (the complete, authoritative record) and a
|
|
22
|
-
* live `run.log` incrementally as they arrive, so output survives a hard kill or lost
|
|
23
|
-
* SIGINT. The log is closed once at finalize, after which {@link write} re-renders
|
|
24
|
-
* `run.log` from `run.jsonl` in collapsed form.
|
|
25
|
-
*
|
|
26
|
-
* Subscribes **synchronously, before any `await`** — so a fast synchronous producer
|
|
27
|
-
* that appends (and trims the in-memory history) before the file setup finishes still
|
|
28
|
-
* has its entries buffered on the subscription queue rather than lost. `replay: true`
|
|
29
|
-
* covers anything already logged when this is called.
|
|
30
|
-
*/
|
|
31
|
-
stream(logs: LogsCollector): Promise<void>;
|
|
32
|
-
/**
|
|
33
|
-
* Renders the final, collapsed `run.log` from the authoritative `run.jsonl` (the
|
|
34
|
-
* complete record {@link stream} wrote), **streaming** it line by line so the whole
|
|
35
|
-
* run is never held in memory. Grouping/indentation is applied here so the stored
|
|
36
|
-
* `run.jsonl` stays presentation-free; large same-source runs are collapsed —
|
|
37
|
-
* off-loaded to a text snapshot and replaced inline by their head plus a pointer — so
|
|
38
|
-
* one proc's burst can't drown the timeline. `run.jsonl` is left as-is. Returns the
|
|
39
|
-
* `run.log` path (surfaced to the terminal).
|
|
40
|
-
*
|
|
41
|
-
* Call only after {@link stream} has closed (the run's log is ended), so `run.jsonl`
|
|
42
|
-
* is complete and no longer being written.
|
|
43
|
-
*/
|
|
44
|
-
write(): Promise<string>;
|
|
45
|
-
/** A compact header block for the top of run.log — legend + run identity. */
|
|
46
|
-
private header;
|
|
47
|
-
}
|
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
import { createReadStream, createWriteStream } from "node:fs";
|
|
2
|
-
import { createInterface } from "node:readline";
|
|
3
|
-
import { LogRenderer } from "./LogRenderer.js";
|
|
4
|
-
import { closeStream } from "../../system/fs/streams.js";
|
|
5
|
-
import { ymd, hms } from "../../utils/format.js";
|
|
6
|
-
/**
|
|
7
|
-
* Writes the run's two log files: `run.jsonl` (the complete, authoritative record,
|
|
8
|
-
* streamed live so it survives a hard kill) and `run.log` (the human timeline).
|
|
9
|
-
*
|
|
10
|
-
* Two phases: {@link stream} runs for the whole run, appending each entry to both
|
|
11
|
-
* files as it arrives; {@link write} runs once at finalize, re-rendering `run.log`
|
|
12
|
-
* from the finished `run.jsonl` in collapsed form. Depends on a {@link RunFolder}
|
|
13
|
-
* (where the files land) and a {@link SnapshotStore} (to off-load collapsed runs).
|
|
14
|
-
*/
|
|
15
|
-
export class RunLog {
|
|
16
|
-
folder;
|
|
17
|
-
snapshots;
|
|
18
|
-
// Path of `assertions.log`, set the first time an assertion is streamed (lazily
|
|
19
|
-
// created). Stays `undefined` for a run with no assertions, so the finalize step
|
|
20
|
-
// links it only when it exists. See {@link assertionsLog}.
|
|
21
|
-
assertionsPath;
|
|
22
|
-
constructor(folder, snapshots) {
|
|
23
|
-
this.folder = folder;
|
|
24
|
-
this.snapshots = snapshots;
|
|
25
|
-
}
|
|
26
|
-
/** The `assertions.log` path if any assertion was logged this run, else `undefined`. */
|
|
27
|
-
get assertionsLog() {
|
|
28
|
-
return this.assertionsPath;
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Streams every log entry to `run.jsonl` (the complete, authoritative record) and a
|
|
32
|
-
* live `run.log` incrementally as they arrive, so output survives a hard kill or lost
|
|
33
|
-
* SIGINT. The log is closed once at finalize, after which {@link write} re-renders
|
|
34
|
-
* `run.log` from `run.jsonl` in collapsed form.
|
|
35
|
-
*
|
|
36
|
-
* Subscribes **synchronously, before any `await`** — so a fast synchronous producer
|
|
37
|
-
* that appends (and trims the in-memory history) before the file setup finishes still
|
|
38
|
-
* has its entries buffered on the subscription queue rather than lost. `replay: true`
|
|
39
|
-
* covers anything already logged when this is called.
|
|
40
|
-
*/
|
|
41
|
-
async stream(logs) {
|
|
42
|
-
const entries = logs.subscribe({ replay: true });
|
|
43
|
-
await this.folder.ensure();
|
|
44
|
-
const jsonl = createWriteStream(this.folder.resolve("run.jsonl"));
|
|
45
|
-
const pretty = createWriteStream(this.folder.resolve("run.log"));
|
|
46
|
-
const renderer = new LogRenderer();
|
|
47
|
-
pretty.write(this.header() + "\n");
|
|
48
|
-
// The dedicated assertions file is a filtered projection of the same stream:
|
|
49
|
-
// opened lazily on the first `assert` entry (so runs with none produce no file),
|
|
50
|
-
// one `⊨ …` line each. Assertions still ride the main run.jsonl / run.log too.
|
|
51
|
-
let assertions;
|
|
52
|
-
try {
|
|
53
|
-
for await (const entry of entries) {
|
|
54
|
-
jsonl.write(JSON.stringify(entry) + "\n");
|
|
55
|
-
pretty.write(renderer.render(entry) + "\n");
|
|
56
|
-
if (entry.level === "assert") {
|
|
57
|
-
if (!assertions) {
|
|
58
|
-
this.assertionsPath = this.folder.resolve("assertions.log");
|
|
59
|
-
assertions = createWriteStream(this.assertionsPath);
|
|
60
|
-
}
|
|
61
|
-
assertions.write(LogRenderer.marker("assert", entry.message) + "\n");
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
finally {
|
|
66
|
-
// End AND wait for both streams to fully close before resolving. {@link write}
|
|
67
|
-
// re-renders run.log at finalize (from this run.jsonl); if a buffered chunk from
|
|
68
|
-
// this stream landed *after* write() truncated run.log, the OS would zero-fill the
|
|
69
|
-
// gap — a multi-MB run of NUL bytes. `run` awaits this promise before calling
|
|
70
|
-
// write(), so this writer is guaranteed done first (and run.jsonl is complete).
|
|
71
|
-
await Promise.all([
|
|
72
|
-
closeStream(jsonl),
|
|
73
|
-
closeStream(pretty),
|
|
74
|
-
...(assertions ? [closeStream(assertions)] : []),
|
|
75
|
-
]);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
/**
|
|
79
|
-
* Renders the final, collapsed `run.log` from the authoritative `run.jsonl` (the
|
|
80
|
-
* complete record {@link stream} wrote), **streaming** it line by line so the whole
|
|
81
|
-
* run is never held in memory. Grouping/indentation is applied here so the stored
|
|
82
|
-
* `run.jsonl` stays presentation-free; large same-source runs are collapsed —
|
|
83
|
-
* off-loaded to a text snapshot and replaced inline by their head plus a pointer — so
|
|
84
|
-
* one proc's burst can't drown the timeline. `run.jsonl` is left as-is. Returns the
|
|
85
|
-
* `run.log` path (surfaced to the terminal).
|
|
86
|
-
*
|
|
87
|
-
* Call only after {@link stream} has closed (the run's log is ended), so `run.jsonl`
|
|
88
|
-
* is complete and no longer being written.
|
|
89
|
-
*/
|
|
90
|
-
async write() {
|
|
91
|
-
await this.folder.ensure();
|
|
92
|
-
const jsonlFile = this.folder.resolve("run.jsonl");
|
|
93
|
-
const file = this.folder.resolve("run.log");
|
|
94
|
-
const out = createWriteStream(file);
|
|
95
|
-
out.write(this.header());
|
|
96
|
-
const renderer = new LogRenderer();
|
|
97
|
-
const writes = [];
|
|
98
|
-
const emit = (item) => {
|
|
99
|
-
if (item.kind === "line") {
|
|
100
|
-
out.write(item.text + "\n");
|
|
101
|
-
}
|
|
102
|
-
else {
|
|
103
|
-
const snap = this.snapshots.captureText(`output-${item.run.source}`, item.run.lines);
|
|
104
|
-
writes.push(snap.written);
|
|
105
|
-
out.write(LogRenderer.renderCollapsed(item.run, { rel: snap.rel }) + "\n");
|
|
106
|
-
}
|
|
107
|
-
};
|
|
108
|
-
try {
|
|
109
|
-
const rl = createInterface({ input: createReadStream(jsonlFile), crlfDelay: Infinity });
|
|
110
|
-
for await (const line of rl) {
|
|
111
|
-
if (!line)
|
|
112
|
-
continue;
|
|
113
|
-
let entry;
|
|
114
|
-
try {
|
|
115
|
-
entry = JSON.parse(line);
|
|
116
|
-
}
|
|
117
|
-
catch {
|
|
118
|
-
continue; // a torn final line (dirty exit) — skip it
|
|
119
|
-
}
|
|
120
|
-
for (const item of renderer.push(entry))
|
|
121
|
-
emit(item);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
catch {
|
|
125
|
-
// run.jsonl missing/unreadable (e.g. a very early crash) — write what we have.
|
|
126
|
-
}
|
|
127
|
-
for (const item of renderer.end())
|
|
128
|
-
emit(item);
|
|
129
|
-
await closeStream(out); // flush + close run.log before reporting it written
|
|
130
|
-
await Promise.all(writes); // ensure every off-loaded run is on disk
|
|
131
|
-
return file;
|
|
132
|
-
}
|
|
133
|
-
/** A compact header block for the top of run.log — legend + run identity. */
|
|
134
|
-
header() {
|
|
135
|
-
const now = new Date();
|
|
136
|
-
return [
|
|
137
|
-
`─── ${this.folder.name()} · ${ymd(now)} ${hms(now)} ───`,
|
|
138
|
-
`● run ⚡ event ▸ action 📎 snapshot ⇥ output ⊨ assert`,
|
|
139
|
-
"",
|
|
140
|
-
].join("\n");
|
|
141
|
-
}
|
|
142
|
-
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
-
import type { Logger } from "./LogsCollector.ts";
|
|
3
|
-
/**
|
|
4
|
-
* The path of the action whose async context is currently executing (e.g.
|
|
5
|
-
* "Build", "Dev › Server"). `SpinHost.launch` wraps each action's `start` in
|
|
6
|
-
* `actionScope.run(path, …)`, so any `console.*` call inside that action's
|
|
7
|
-
* (a)synchronous body reads its path here. No store → the call ran outside any
|
|
8
|
-
* action (the spin body, top-level, a third-party module init).
|
|
9
|
-
*/
|
|
10
|
-
export declare const actionScope: AsyncLocalStorage<string>;
|
|
11
|
-
/**
|
|
12
|
-
* Patch `console.log/info/debug/warn/error` so each call appends into `logs`
|
|
13
|
-
* instead of writing to the terminal, attributing every line to the current
|
|
14
|
-
* action via {@link actionScope}. Returns a `restore()` that reinstalls the
|
|
15
|
-
* methods that were installed when this ran — so sequential runs nest cleanly
|
|
16
|
-
* (save-current / restore-saved).
|
|
17
|
-
*
|
|
18
|
-
* The patched methods do **not** echo to the terminal: their output reaches it
|
|
19
|
-
* through the run's log renderer (the caller keeps the original `console.log`
|
|
20
|
-
* for the renderer's own writes). Args are rendered with `util.format` (exact
|
|
21
|
-
* console semantics), split on newlines, and appended line-by-line; an empty
|
|
22
|
-
* render is skipped.
|
|
23
|
-
*/
|
|
24
|
-
export declare function captureConsole(logs: Logger): () => void;
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
-
import { format } from "node:util";
|
|
3
|
-
/**
|
|
4
|
-
* The path of the action whose async context is currently executing (e.g.
|
|
5
|
-
* "Build", "Dev › Server"). `SpinHost.launch` wraps each action's `start` in
|
|
6
|
-
* `actionScope.run(path, …)`, so any `console.*` call inside that action's
|
|
7
|
-
* (a)synchronous body reads its path here. No store → the call ran outside any
|
|
8
|
-
* action (the spin body, top-level, a third-party module init).
|
|
9
|
-
*/
|
|
10
|
-
export const actionScope = new AsyncLocalStorage();
|
|
11
|
-
/** The console methods we fold into the log, each mapped to its log level. */
|
|
12
|
-
const LEVELS = {
|
|
13
|
-
log: "info",
|
|
14
|
-
info: "info",
|
|
15
|
-
debug: "debug",
|
|
16
|
-
warn: "warn",
|
|
17
|
-
error: "error",
|
|
18
|
-
};
|
|
19
|
-
/**
|
|
20
|
-
* Patch `console.log/info/debug/warn/error` so each call appends into `logs`
|
|
21
|
-
* instead of writing to the terminal, attributing every line to the current
|
|
22
|
-
* action via {@link actionScope}. Returns a `restore()` that reinstalls the
|
|
23
|
-
* methods that were installed when this ran — so sequential runs nest cleanly
|
|
24
|
-
* (save-current / restore-saved).
|
|
25
|
-
*
|
|
26
|
-
* The patched methods do **not** echo to the terminal: their output reaches it
|
|
27
|
-
* through the run's log renderer (the caller keeps the original `console.log`
|
|
28
|
-
* for the renderer's own writes). Args are rendered with `util.format` (exact
|
|
29
|
-
* console semantics), split on newlines, and appended line-by-line; an empty
|
|
30
|
-
* render is skipped.
|
|
31
|
-
*/
|
|
32
|
-
export function captureConsole(logs) {
|
|
33
|
-
const c = console;
|
|
34
|
-
const saved = {};
|
|
35
|
-
const methods = Object.keys(LEVELS);
|
|
36
|
-
for (const method of methods) {
|
|
37
|
-
saved[method] = c[method];
|
|
38
|
-
const level = LEVELS[method];
|
|
39
|
-
c[method] = (...args) => {
|
|
40
|
-
const path = actionScope.getStore();
|
|
41
|
-
const source = path ? `${path} › console` : "console";
|
|
42
|
-
const text = format(...args);
|
|
43
|
-
for (const line of text.split("\n")) {
|
|
44
|
-
if (line)
|
|
45
|
-
logs.append({ source, level, message: line });
|
|
46
|
-
}
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
return () => {
|
|
50
|
-
for (const method of methods)
|
|
51
|
-
c[method] = saved[method];
|
|
52
|
-
};
|
|
53
|
-
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import type { createWriteStream } from "node:fs";
|
|
2
|
-
/**
|
|
3
|
-
* Flush and close a write stream, resolving once its file descriptor is released.
|
|
4
|
-
*
|
|
5
|
-
* Resolves on `close` *or* `error` — a stream error still frees the file, and a
|
|
6
|
-
* caller waiting to re-read or truncate that file must not hang because the write
|
|
7
|
-
* end failed. The one shared fs primitive behind both `run.jsonl` and `run.log`
|
|
8
|
-
* writers, where "the bytes are on disk and the fd is closed" is the exact barrier
|
|
9
|
-
* the finalize step waits on.
|
|
10
|
-
*/
|
|
11
|
-
export declare const closeStream: (stream: ReturnType<typeof createWriteStream>) => Promise<void>;
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Flush and close a write stream, resolving once its file descriptor is released.
|
|
3
|
-
*
|
|
4
|
-
* Resolves on `close` *or* `error` — a stream error still frees the file, and a
|
|
5
|
-
* caller waiting to re-read or truncate that file must not hang because the write
|
|
6
|
-
* end failed. The one shared fs primitive behind both `run.jsonl` and `run.log`
|
|
7
|
-
* writers, where "the bytes are on disk and the fd is closed" is the exact barrier
|
|
8
|
-
* the finalize step waits on.
|
|
9
|
-
*/
|
|
10
|
-
export const closeStream = (stream) => new Promise((resolve) => {
|
|
11
|
-
stream.on("close", () => resolve());
|
|
12
|
-
stream.on("error", () => resolve()); // a stream error still frees the file; don't hang
|
|
13
|
-
stream.end();
|
|
14
|
-
});
|