omkit 0.0.9 → 0.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actions/chrome-page.d.ts +1 -1
- package/dist/actions/chrome-page.js +1 -1
- package/dist/actions/command.d.ts +1 -1
- package/dist/actions/command.js +5 -6
- package/dist/actions/healthcheck.d.ts +1 -1
- package/dist/actions/healthcheck.js +1 -1
- package/dist/actions/prompt.d.ts +1 -1
- package/dist/actions/prompt.js +1 -1
- package/dist/actions/tail-log.d.ts +1 -1
- package/dist/actions/tail-log.js +1 -1
- package/dist/actions/until-log.d.ts +2 -2
- package/dist/actions/until-log.js +1 -1
- package/dist/actions/watch-dir.d.ts +1 -1
- package/dist/actions/watch-dir.js +1 -1
- package/dist/actions/watch.d.ts +1 -1
- package/dist/actions/watch.js +1 -1
- package/dist/index.d.ts +13 -17
- package/dist/index.js +15 -17
- package/dist/orchestration/action/action.d.ts +23 -0
- package/dist/orchestration/action/action.js +254 -0
- package/dist/{core/action.d.ts → orchestration/action/types.d.ts} +31 -27
- package/dist/orchestration/action/types.js +1 -0
- package/dist/{core → orchestration/events}/events.d.ts +9 -1
- package/dist/{core → orchestration/events}/events.js +14 -22
- package/dist/orchestration/spin/SpinHost.d.ts +105 -0
- package/dist/orchestration/spin/SpinHost.js +303 -0
- package/dist/orchestration/spin/spin.d.ts +24 -0
- package/dist/orchestration/spin/spin.js +56 -0
- package/dist/orchestration/spin/types.d.ts +56 -0
- package/dist/orchestration/spin/types.js +1 -0
- package/dist/output/Output.d.ts +24 -0
- package/dist/output/Output.js +27 -0
- package/dist/output/folder/RunFolder.d.ts +29 -0
- package/dist/output/folder/RunFolder.js +56 -0
- package/dist/output/index.d.ts +11 -0
- package/dist/output/index.js +11 -0
- package/dist/output/log/LogRenderer.d.ts +73 -0
- package/dist/output/log/LogRenderer.js +131 -0
- package/dist/{core/log-collector → output/log}/LogsCollector.d.ts +14 -0
- package/dist/{core/log-collector → output/log}/LogsCollector.js +24 -1
- package/dist/output/log/RunLog.d.ts +44 -0
- package/dist/output/log/RunLog.js +119 -0
- package/dist/output/snapshot/SnapshotStore.d.ts +49 -0
- package/dist/output/snapshot/SnapshotStore.js +61 -0
- package/dist/system/fs/FolderCache.d.ts +47 -0
- package/dist/system/fs/FolderCache.js +117 -0
- package/dist/system/fs/streams.d.ts +11 -0
- package/dist/system/fs/streams.js +14 -0
- package/dist/{core → system/process}/process.d.ts +9 -8
- package/dist/{core → system/process}/process.js +8 -9
- package/dist/utils/format.d.ts +9 -0
- package/dist/utils/format.js +9 -0
- package/dist/utils/serialize.d.ts +2 -0
- package/dist/utils/serialize.js +12 -0
- package/package.json +1 -1
- package/dist/core/action.js +0 -201
- package/dist/core/folder-cache.d.ts +0 -39
- package/dist/core/folder-cache.js +0 -109
- package/dist/core/log-collector/global.d.ts +0 -7
- package/dist/core/log-collector/global.js +0 -7
- package/dist/core/log-collector/render.d.ts +0 -68
- package/dist/core/log-collector/render.js +0 -132
- package/dist/core/markers.d.ts +0 -9
- package/dist/core/markers.js +0 -13
- package/dist/core/output.d.ts +0 -68
- package/dist/core/output.js +0 -213
- package/dist/core/run.d.ts +0 -88
- package/dist/core/run.js +0 -257
- package/dist/core/spin.d.ts +0 -72
- package/dist/core/spin.js +0 -45
- /package/dist/{core/helpers → utils}/AsyncQueue.d.ts +0 -0
- /package/dist/{core/helpers → utils}/AsyncQueue.js +0 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { RunFolder } from "./folder/RunFolder.js";
|
|
2
|
+
import { SnapshotStore } from "./snapshot/SnapshotStore.js";
|
|
3
|
+
import { RunLog } from "./log/RunLog.js";
|
|
4
|
+
/**
|
|
5
|
+
* The run's output subsystem — the composition root that owns the per-run folder and
|
|
6
|
+
* the writers layered on it, and hands them to the rest of the process.
|
|
7
|
+
*
|
|
8
|
+
* Each run owns one of these: its {@link SpinHost} constructs an `Output` over the
|
|
9
|
+
* run's log and threads it to actions as `ctx.output`. Everything that was
|
|
10
|
+
* module-global in the old `output.ts` (the folder path, the mkdir latch, the
|
|
11
|
+
* snapshot sequence) now lives inside these three members, reachable as
|
|
12
|
+
* `output.folder`, `output.snapshots`, `output.runLog` — so callers reach for a named
|
|
13
|
+
* layer instead of a free function, and the shared state has one owner per run.
|
|
14
|
+
*/
|
|
15
|
+
export class Output {
|
|
16
|
+
/** The per-run directory and artifact drop. */
|
|
17
|
+
folder;
|
|
18
|
+
/** Sequenced JSON/text snapshot files + `[snapshot]` timeline lines. */
|
|
19
|
+
snapshots;
|
|
20
|
+
/** The `run.jsonl` / `run.log` writer. */
|
|
21
|
+
runLog;
|
|
22
|
+
constructor(logger) {
|
|
23
|
+
this.folder = new RunFolder();
|
|
24
|
+
this.snapshots = new SnapshotStore(this.folder, logger);
|
|
25
|
+
this.runLog = new RunLog(this.folder, this.snapshots);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Owns this process's single output directory — `logs/<name>/<date>/<time>/`, where
|
|
3
|
+
* `run.log`, `run.jsonl`, and snapshots all live together.
|
|
4
|
+
*
|
|
5
|
+
* One folder per run, one run per process: the path is computed **once, lazily**, so
|
|
6
|
+
* a snapshot taken mid-run lands in the same folder as the log that's flushed later.
|
|
7
|
+
* The {@link SnapshotStore} and {@link RunLog} take a shared instance rather than
|
|
8
|
+
* reaching for module globals, so "where does this run's output go" has one owner.
|
|
9
|
+
*/
|
|
10
|
+
export declare class RunFolder {
|
|
11
|
+
private dir;
|
|
12
|
+
private made;
|
|
13
|
+
private ensuredSync;
|
|
14
|
+
/** The entry script's base name (the run's name), e.g. "tskb-build". */
|
|
15
|
+
name(): string;
|
|
16
|
+
/** The run's output directory, `logs/<name>/<date>/<time>/` — computed once, lazily. */
|
|
17
|
+
path(): string;
|
|
18
|
+
/** A path inside the run folder — `join(path(), name)`. */
|
|
19
|
+
resolve(name: string): string;
|
|
20
|
+
/** Create the folder, memoized — every caller awaits the same underlying mkdir. */
|
|
21
|
+
ensure(): Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* Absolute path to the run folder, created **synchronously** so a caller can write
|
|
24
|
+
* into it in the same tick. Absolute so it's safe to hand to a child process with a
|
|
25
|
+
* different cwd. This is the artifact drop for actions that want to write beside
|
|
26
|
+
* `run.log` (one folder per run; one run per process).
|
|
27
|
+
*/
|
|
28
|
+
artifacts(): string;
|
|
29
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { mkdirSync } from "node:fs";
|
|
2
|
+
import { mkdir } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { ymd, hms } from "../../utils/format.js";
|
|
5
|
+
/**
|
|
6
|
+
* Owns this process's single output directory — `logs/<name>/<date>/<time>/`, where
|
|
7
|
+
* `run.log`, `run.jsonl`, and snapshots all live together.
|
|
8
|
+
*
|
|
9
|
+
* One folder per run, one run per process: the path is computed **once, lazily**, so
|
|
10
|
+
* a snapshot taken mid-run lands in the same folder as the log that's flushed later.
|
|
11
|
+
* The {@link SnapshotStore} and {@link RunLog} take a shared instance rather than
|
|
12
|
+
* reaching for module globals, so "where does this run's output go" has one owner.
|
|
13
|
+
*/
|
|
14
|
+
export class RunFolder {
|
|
15
|
+
// Computed once on first access and cached — every consumer sees the same folder.
|
|
16
|
+
dir;
|
|
17
|
+
// Memoized async mkdir; the promise is shared so concurrent callers await one op.
|
|
18
|
+
made;
|
|
19
|
+
// Whether the folder has been created synchronously (for `artifacts()`).
|
|
20
|
+
ensuredSync = false;
|
|
21
|
+
/** The entry script's base name (the run's name), e.g. "tskb-build". */
|
|
22
|
+
name() {
|
|
23
|
+
const entry = process.argv[1];
|
|
24
|
+
return entry ? path.basename(entry, path.extname(entry)) : "pipeline";
|
|
25
|
+
}
|
|
26
|
+
/** The run's output directory, `logs/<name>/<date>/<time>/` — computed once, lazily. */
|
|
27
|
+
path() {
|
|
28
|
+
if (this.dir)
|
|
29
|
+
return this.dir;
|
|
30
|
+
const now = new Date();
|
|
31
|
+
this.dir = path.join("logs", this.name(), ymd(now), hms(now, "-"));
|
|
32
|
+
return this.dir;
|
|
33
|
+
}
|
|
34
|
+
/** A path inside the run folder — `join(path(), name)`. */
|
|
35
|
+
resolve(name) {
|
|
36
|
+
return path.join(this.path(), name);
|
|
37
|
+
}
|
|
38
|
+
/** Create the folder, memoized — every caller awaits the same underlying mkdir. */
|
|
39
|
+
ensure() {
|
|
40
|
+
return (this.made ??= mkdir(this.path(), { recursive: true }).then(() => { }));
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Absolute path to the run folder, created **synchronously** so a caller can write
|
|
44
|
+
* into it in the same tick. Absolute so it's safe to hand to a child process with a
|
|
45
|
+
* different cwd. This is the artifact drop for actions that want to write beside
|
|
46
|
+
* `run.log` (one folder per run; one run per process).
|
|
47
|
+
*/
|
|
48
|
+
artifacts() {
|
|
49
|
+
const abs = path.resolve(this.path());
|
|
50
|
+
if (!this.ensuredSync) {
|
|
51
|
+
mkdirSync(abs, { recursive: true });
|
|
52
|
+
this.ensuredSync = true;
|
|
53
|
+
}
|
|
54
|
+
return abs;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The output layer's barrel — the composition root ({@link Output}) and its member
|
|
3
|
+
* classes. There is **no module singleton** here: each run's {@link SpinHost} owns
|
|
4
|
+
* its own `Output`, and threads it to actions as `ctx.output` (and to the spin body
|
|
5
|
+
* as `ctx.snapshot` / `ctx.artifactsFolder`), so nothing binds to process-global
|
|
6
|
+
* state and runs stay isolated.
|
|
7
|
+
*/
|
|
8
|
+
export { Output } from "./Output.ts";
|
|
9
|
+
export { RunFolder } from "./folder/RunFolder.ts";
|
|
10
|
+
export { type SnapshotRef, SnapshotStore } from "./snapshot/SnapshotStore.ts";
|
|
11
|
+
export { RunLog } from "./log/RunLog.ts";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The output layer's barrel — the composition root ({@link Output}) and its member
|
|
3
|
+
* classes. There is **no module singleton** here: each run's {@link SpinHost} owns
|
|
4
|
+
* its own `Output`, and threads it to actions as `ctx.output` (and to the spin body
|
|
5
|
+
* as `ctx.snapshot` / `ctx.artifactsFolder`), so nothing binds to process-global
|
|
6
|
+
* state and runs stay isolated.
|
|
7
|
+
*/
|
|
8
|
+
export { Output } from "./Output.js";
|
|
9
|
+
export { RunFolder } from "./folder/RunFolder.js";
|
|
10
|
+
export { SnapshotStore } from "./snapshot/SnapshotStore.js";
|
|
11
|
+
export { RunLog } from "./log/RunLog.js";
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { LogEntry } from "./LogsCollector.ts";
|
|
2
|
+
/** The special "title" log lines that get a distinguishing prefix. */
|
|
3
|
+
export type MarkerKind = "event" | "action" | "run" | "snapshot";
|
|
4
|
+
/** A run set aside for collapsing: its source and every output line, in order. */
|
|
5
|
+
export interface CollapsedRun {
|
|
6
|
+
readonly source: string;
|
|
7
|
+
readonly lines: readonly string[];
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* One unit of batch-rendered output: either a ready-to-write text `line`
|
|
11
|
+
* (identical to {@link LogRenderer.render}'s output for that entry) or a
|
|
12
|
+
* `collapsed` run the caller must off-load and render via {@link LogRenderer.renderCollapsed}.
|
|
13
|
+
*/
|
|
14
|
+
export type DisplayItem = {
|
|
15
|
+
kind: "line";
|
|
16
|
+
text: string;
|
|
17
|
+
} | {
|
|
18
|
+
kind: "collapsed";
|
|
19
|
+
run: CollapsedRun;
|
|
20
|
+
};
|
|
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 declare class LogRenderer {
|
|
38
|
+
/** Lines of a collapsed run shown inline before the pointer. */
|
|
39
|
+
static readonly COLLAPSE_HEAD = 5;
|
|
40
|
+
/** Prefix a milestone/header line with its marker (`● run`, `▸ action`, …). */
|
|
41
|
+
static marker(kind: MarkerKind, text: string): string;
|
|
42
|
+
/**
|
|
43
|
+
* Renders a {@link CollapsedRun} as its `head` lines under the `▸ <source>`
|
|
44
|
+
* header, then a single indented `📎` pointer line to the off-loaded snapshot at
|
|
45
|
+
* `rel` reporting how many lines were hidden. Pure — `rel` is supplied by the
|
|
46
|
+
* caller after it writes the file.
|
|
47
|
+
*/
|
|
48
|
+
static renderCollapsed(run: CollapsedRun, { rel, head }: {
|
|
49
|
+
rel: string;
|
|
50
|
+
head?: number;
|
|
51
|
+
}): string;
|
|
52
|
+
private lastSource;
|
|
53
|
+
private run;
|
|
54
|
+
private readonly threshold;
|
|
55
|
+
constructor(threshold?: number);
|
|
56
|
+
/**
|
|
57
|
+
* Streaming: render one entry to its pretty line. A milestone renders as its own
|
|
58
|
+
* marker line and breaks the group; other entries are grouped under an
|
|
59
|
+
* `▸ <source>` header (first line of a group) and indented (the rest).
|
|
60
|
+
*/
|
|
61
|
+
render(entry: LogEntry): string;
|
|
62
|
+
/**
|
|
63
|
+
* Batch: push an entry in order and get back the {@link DisplayItem}s that just
|
|
64
|
+
* became final (0, 1, or a whole flushed run). Only the current same-source run
|
|
65
|
+
* is held in memory, so segmenting a huge log stays bounded — the `run.log`
|
|
66
|
+
* writer feeds this straight from `run.jsonl` on disk. Output is byte-identical
|
|
67
|
+
* to {@link render} for non-collapsed runs. Pure — no filesystem.
|
|
68
|
+
*/
|
|
69
|
+
push(entry: LogEntry): DisplayItem[];
|
|
70
|
+
/** Batch: flush the last pending run (call once after the final {@link push}). */
|
|
71
|
+
end(): DisplayItem[];
|
|
72
|
+
private flush;
|
|
73
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
const PREFIX = {
|
|
2
|
+
event: "⚡",
|
|
3
|
+
action: "▸",
|
|
4
|
+
run: "●",
|
|
5
|
+
snapshot: "📎",
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Levels that render as their own top-level milestone line — marker-prefixed and
|
|
9
|
+
* never grouped/indented. Everything else is action/proc output, which the
|
|
10
|
+
* renderer groups under an `▸ <source>` header and indents.
|
|
11
|
+
*/
|
|
12
|
+
const MILESTONE = {
|
|
13
|
+
run: "run",
|
|
14
|
+
event: "event",
|
|
15
|
+
snapshot: "snapshot",
|
|
16
|
+
};
|
|
17
|
+
/** Only runs *longer than* this collapse, so a trigger always hides a meaningful amount. */
|
|
18
|
+
const COLLAPSE_THRESHOLD = 15;
|
|
19
|
+
/**
|
|
20
|
+
* Turns raw {@link LogEntry} records into the pretty, grouped text form.
|
|
21
|
+
* Presentation lives here and nowhere else: the collector stores raw entries; the
|
|
22
|
+
* live drain and `run.log` writer each construct a renderer and feed it entries in
|
|
23
|
+
* order. Two modes over the same grouping rules — pick per instance, don't mix:
|
|
24
|
+
*
|
|
25
|
+
* - **streaming** — {@link render} returns one entry's line as it arrives (live
|
|
26
|
+
* drain, incremental `run.log`).
|
|
27
|
+
* - **batch** — {@link push}/{@link end} accumulate a same-source run and collapse
|
|
28
|
+
* it when it grows past the threshold, so one proc's burst can't drown the
|
|
29
|
+
* durable `run.log` (the caller off-loads the full run to a snapshot).
|
|
30
|
+
*
|
|
31
|
+
* Grouping is decided against the *single merged stream* (`lastSource`), not
|
|
32
|
+
* per-producer — so interleaved concurrent sources re-header correctly. A
|
|
33
|
+
* milestone line breaks the current group, so the next action line re-headers.
|
|
34
|
+
*/
|
|
35
|
+
export class LogRenderer {
|
|
36
|
+
/** Lines of a collapsed run shown inline before the pointer. */
|
|
37
|
+
static COLLAPSE_HEAD = 5;
|
|
38
|
+
/** Prefix a milestone/header line with its marker (`● run`, `▸ action`, …). */
|
|
39
|
+
static marker(kind, text) {
|
|
40
|
+
return `${PREFIX[kind]} ${text}`;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Renders a {@link CollapsedRun} as its `head` lines under the `▸ <source>`
|
|
44
|
+
* header, then a single indented `📎` pointer line to the off-loaded snapshot at
|
|
45
|
+
* `rel` reporting how many lines were hidden. Pure — `rel` is supplied by the
|
|
46
|
+
* caller after it writes the file.
|
|
47
|
+
*/
|
|
48
|
+
static renderCollapsed(run, { rel, head = LogRenderer.COLLAPSE_HEAD }) {
|
|
49
|
+
const shown = run.lines.slice(0, head);
|
|
50
|
+
const lines = shown.map((message, i) => i === 0 ? `${LogRenderer.marker("action", run.source)}\n\t${message}` : `\t${message}`);
|
|
51
|
+
const hidden = run.lines.length - shown.length;
|
|
52
|
+
lines.push(`\t${LogRenderer.marker("snapshot", `… +${hidden} more lines → ${rel}`)}`);
|
|
53
|
+
return lines.join("\n");
|
|
54
|
+
}
|
|
55
|
+
// Streaming grouping state: the source of the group currently open.
|
|
56
|
+
lastSource;
|
|
57
|
+
// Batch state: the same-source run currently being accumulated.
|
|
58
|
+
run;
|
|
59
|
+
threshold;
|
|
60
|
+
constructor(threshold = COLLAPSE_THRESHOLD) {
|
|
61
|
+
this.threshold = threshold;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Streaming: render one entry to its pretty line. A milestone renders as its own
|
|
65
|
+
* marker line and breaks the group; other entries are grouped under an
|
|
66
|
+
* `▸ <source>` header (first line of a group) and indented (the rest).
|
|
67
|
+
*/
|
|
68
|
+
render(entry) {
|
|
69
|
+
const milestone = MILESTONE[entry.level];
|
|
70
|
+
if (milestone) {
|
|
71
|
+
// Reset grouping: after an interjection, the next action line re-headers.
|
|
72
|
+
this.lastSource = undefined;
|
|
73
|
+
return LogRenderer.marker(milestone, entry.message);
|
|
74
|
+
}
|
|
75
|
+
if (entry.source !== this.lastSource) {
|
|
76
|
+
this.lastSource = entry.source;
|
|
77
|
+
return `${LogRenderer.marker("action", entry.source)}\n\t${entry.message}`;
|
|
78
|
+
}
|
|
79
|
+
return `\t${entry.message}`;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Batch: push an entry in order and get back the {@link DisplayItem}s that just
|
|
83
|
+
* became final (0, 1, or a whole flushed run). Only the current same-source run
|
|
84
|
+
* is held in memory, so segmenting a huge log stays bounded — the `run.log`
|
|
85
|
+
* writer feeds this straight from `run.jsonl` on disk. Output is byte-identical
|
|
86
|
+
* to {@link render} for non-collapsed runs. Pure — no filesystem.
|
|
87
|
+
*/
|
|
88
|
+
push(entry) {
|
|
89
|
+
const out = [];
|
|
90
|
+
const milestone = MILESTONE[entry.level];
|
|
91
|
+
if (milestone) {
|
|
92
|
+
// A milestone breaks the current run, then renders as its own line.
|
|
93
|
+
this.flush(out);
|
|
94
|
+
out.push({ kind: "line", text: LogRenderer.marker(milestone, entry.message) });
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
if (this.run && this.run.source === entry.source) {
|
|
98
|
+
this.run.lines.push(entry.message);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
this.flush(out);
|
|
102
|
+
this.run = { source: entry.source, lines: [entry.message] };
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
/** Batch: flush the last pending run (call once after the final {@link push}). */
|
|
107
|
+
end() {
|
|
108
|
+
const out = [];
|
|
109
|
+
this.flush(out);
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
// Flush the pending run into `out`: collapse it if it ran long, else emit grouped
|
|
113
|
+
// lines exactly as `render` would (header first, indent the rest).
|
|
114
|
+
flush(out) {
|
|
115
|
+
const run = this.run;
|
|
116
|
+
if (!run)
|
|
117
|
+
return;
|
|
118
|
+
if (run.lines.length > this.threshold) {
|
|
119
|
+
out.push({ kind: "collapsed", run: { source: run.source, lines: run.lines } });
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
run.lines.forEach((message, i) => {
|
|
123
|
+
out.push({
|
|
124
|
+
kind: "line",
|
|
125
|
+
text: i === 0 ? `${LogRenderer.marker("action", run.source)}\n\t${message}` : `\t${message}`,
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
this.run = undefined;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -63,3 +63,17 @@ export declare class LogsCollector implements Logger {
|
|
|
63
63
|
subscribe({ replay }?: SubscribeOptions): AsyncIterable<LogEntry>;
|
|
64
64
|
snapshot(): readonly LogEntry[];
|
|
65
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,4 +1,4 @@
|
|
|
1
|
-
import { AsyncQueue } from "
|
|
1
|
+
import { AsyncQueue } from "../../utils/AsyncQueue.js";
|
|
2
2
|
export class LogsCollector {
|
|
3
3
|
sequence = 0;
|
|
4
4
|
// Recent entries retained in memory, only to replay to late subscribers. The
|
|
@@ -95,6 +95,29 @@ export class LogsCollector {
|
|
|
95
95
|
return this.history;
|
|
96
96
|
}
|
|
97
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
|
+
}
|
|
98
121
|
/**
|
|
99
122
|
* Splits a byte/string stream into lines, calling `onLine` for each. Partial
|
|
100
123
|
* lines are buffered across chunks and a non-empty trailing fragment is flushed
|
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
constructor(folder: RunFolder, snapshots: SnapshotStore);
|
|
17
|
+
/**
|
|
18
|
+
* Streams every log entry to `run.jsonl` (the complete, authoritative record) and a
|
|
19
|
+
* live `run.log` incrementally as they arrive, so output survives a hard kill or lost
|
|
20
|
+
* SIGINT. The log is closed once at finalize, after which {@link write} re-renders
|
|
21
|
+
* `run.log` from `run.jsonl` in collapsed form.
|
|
22
|
+
*
|
|
23
|
+
* Subscribes **synchronously, before any `await`** — so a fast synchronous producer
|
|
24
|
+
* that appends (and trims the in-memory history) before the file setup finishes still
|
|
25
|
+
* has its entries buffered on the subscription queue rather than lost. `replay: true`
|
|
26
|
+
* covers anything already logged when this is called.
|
|
27
|
+
*/
|
|
28
|
+
stream(logs: LogsCollector): Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Renders the final, collapsed `run.log` from the authoritative `run.jsonl` (the
|
|
31
|
+
* complete record {@link stream} wrote), **streaming** it line by line so the whole
|
|
32
|
+
* run is never held in memory. Grouping/indentation is applied here so the stored
|
|
33
|
+
* `run.jsonl` stays presentation-free; large same-source runs are collapsed —
|
|
34
|
+
* off-loaded to a text snapshot and replaced inline by their head plus a pointer — so
|
|
35
|
+
* one proc's burst can't drown the timeline. `run.jsonl` is left as-is. Returns the
|
|
36
|
+
* `run.log` path (surfaced to the terminal).
|
|
37
|
+
*
|
|
38
|
+
* Call only after {@link stream} has closed (the run's log is ended), so `run.jsonl`
|
|
39
|
+
* is complete and no longer being written.
|
|
40
|
+
*/
|
|
41
|
+
write(): Promise<string>;
|
|
42
|
+
/** A compact header block for the top of run.log — legend + run identity. */
|
|
43
|
+
private header;
|
|
44
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
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
|
+
constructor(folder, snapshots) {
|
|
19
|
+
this.folder = folder;
|
|
20
|
+
this.snapshots = snapshots;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Streams every log entry to `run.jsonl` (the complete, authoritative record) and a
|
|
24
|
+
* live `run.log` incrementally as they arrive, so output survives a hard kill or lost
|
|
25
|
+
* SIGINT. The log is closed once at finalize, after which {@link write} re-renders
|
|
26
|
+
* `run.log` from `run.jsonl` in collapsed form.
|
|
27
|
+
*
|
|
28
|
+
* Subscribes **synchronously, before any `await`** — so a fast synchronous producer
|
|
29
|
+
* that appends (and trims the in-memory history) before the file setup finishes still
|
|
30
|
+
* has its entries buffered on the subscription queue rather than lost. `replay: true`
|
|
31
|
+
* covers anything already logged when this is called.
|
|
32
|
+
*/
|
|
33
|
+
async stream(logs) {
|
|
34
|
+
const entries = logs.subscribe({ replay: true });
|
|
35
|
+
await this.folder.ensure();
|
|
36
|
+
const jsonl = createWriteStream(this.folder.resolve("run.jsonl"));
|
|
37
|
+
const pretty = createWriteStream(this.folder.resolve("run.log"));
|
|
38
|
+
const renderer = new LogRenderer();
|
|
39
|
+
pretty.write(this.header() + "\n");
|
|
40
|
+
try {
|
|
41
|
+
for await (const entry of entries) {
|
|
42
|
+
jsonl.write(JSON.stringify(entry) + "\n");
|
|
43
|
+
pretty.write(renderer.render(entry) + "\n");
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
// End AND wait for both streams to fully close before resolving. {@link write}
|
|
48
|
+
// re-renders run.log at finalize (from this run.jsonl); if a buffered chunk from
|
|
49
|
+
// this stream landed *after* write() truncated run.log, the OS would zero-fill the
|
|
50
|
+
// gap — a multi-MB run of NUL bytes. `run` awaits this promise before calling
|
|
51
|
+
// write(), so this writer is guaranteed done first (and run.jsonl is complete).
|
|
52
|
+
await Promise.all([closeStream(jsonl), closeStream(pretty)]);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Renders the final, collapsed `run.log` from the authoritative `run.jsonl` (the
|
|
57
|
+
* complete record {@link stream} wrote), **streaming** it line by line so the whole
|
|
58
|
+
* run is never held in memory. Grouping/indentation is applied here so the stored
|
|
59
|
+
* `run.jsonl` stays presentation-free; large same-source runs are collapsed —
|
|
60
|
+
* off-loaded to a text snapshot and replaced inline by their head plus a pointer — so
|
|
61
|
+
* one proc's burst can't drown the timeline. `run.jsonl` is left as-is. Returns the
|
|
62
|
+
* `run.log` path (surfaced to the terminal).
|
|
63
|
+
*
|
|
64
|
+
* Call only after {@link stream} has closed (the run's log is ended), so `run.jsonl`
|
|
65
|
+
* is complete and no longer being written.
|
|
66
|
+
*/
|
|
67
|
+
async write() {
|
|
68
|
+
await this.folder.ensure();
|
|
69
|
+
const jsonlFile = this.folder.resolve("run.jsonl");
|
|
70
|
+
const file = this.folder.resolve("run.log");
|
|
71
|
+
const out = createWriteStream(file);
|
|
72
|
+
out.write(this.header());
|
|
73
|
+
const renderer = new LogRenderer();
|
|
74
|
+
const writes = [];
|
|
75
|
+
const emit = (item) => {
|
|
76
|
+
if (item.kind === "line") {
|
|
77
|
+
out.write(item.text + "\n");
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
const snap = this.snapshots.captureText(`output-${item.run.source}`, item.run.lines);
|
|
81
|
+
writes.push(snap.written);
|
|
82
|
+
out.write(LogRenderer.renderCollapsed(item.run, { rel: snap.rel }) + "\n");
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
try {
|
|
86
|
+
const rl = createInterface({ input: createReadStream(jsonlFile), crlfDelay: Infinity });
|
|
87
|
+
for await (const line of rl) {
|
|
88
|
+
if (!line)
|
|
89
|
+
continue;
|
|
90
|
+
let entry;
|
|
91
|
+
try {
|
|
92
|
+
entry = JSON.parse(line);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
continue; // a torn final line (dirty exit) — skip it
|
|
96
|
+
}
|
|
97
|
+
for (const item of renderer.push(entry))
|
|
98
|
+
emit(item);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// run.jsonl missing/unreadable (e.g. a very early crash) — write what we have.
|
|
103
|
+
}
|
|
104
|
+
for (const item of renderer.end())
|
|
105
|
+
emit(item);
|
|
106
|
+
await closeStream(out); // flush + close run.log before reporting it written
|
|
107
|
+
await Promise.all(writes); // ensure every off-loaded run is on disk
|
|
108
|
+
return file;
|
|
109
|
+
}
|
|
110
|
+
/** A compact header block for the top of run.log — legend + run identity. */
|
|
111
|
+
header() {
|
|
112
|
+
const now = new Date();
|
|
113
|
+
return [
|
|
114
|
+
`─── ${this.folder.name()} · ${ymd(now)} ${hms(now)} ───`,
|
|
115
|
+
`● run ⚡ event ▸ action 📎 snapshot ⇥ output`,
|
|
116
|
+
"",
|
|
117
|
+
].join("\n");
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Logger } from "../log/LogsCollector.ts";
|
|
2
|
+
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
|
+
/**
|
|
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).
|
|
21
|
+
*/
|
|
22
|
+
export declare class SnapshotStore {
|
|
23
|
+
private seq;
|
|
24
|
+
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;
|
|
49
|
+
}
|