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,337 +0,0 @@
|
|
|
1
|
-
import { relative } from "node:path";
|
|
2
|
-
import { producerOfRef } from "../action/action.js";
|
|
3
|
-
import { LogsCollector, ScopedLogger } from "../../output/log/LogsCollector.js";
|
|
4
|
-
import { LogRenderer } from "../../output/log/LogRenderer.js";
|
|
5
|
-
import { actionScope, captureConsole } from "../../output/log/console-capture.js";
|
|
6
|
-
import { Output } from "../../output/Output.js";
|
|
7
|
-
import { createAssert } from "../assert.js";
|
|
8
|
-
/** The entry script, relative to where node was invoked (so the log says what produced it). */
|
|
9
|
-
const scriptPath = () => {
|
|
10
|
-
const entry = process.argv[1];
|
|
11
|
-
return entry ? relative(process.cwd(), entry) : "(unknown)";
|
|
12
|
-
};
|
|
13
|
-
/** A constructed instance is an object carrying a `start` function. */
|
|
14
|
-
const isActionInstance = (value) => typeof value === "object" &&
|
|
15
|
-
value !== null &&
|
|
16
|
-
typeof value.start === "function";
|
|
17
|
-
/** An *uncalled* action: the callable `action(name).run(…)` returns before it's invoked. */
|
|
18
|
-
const isUncalledAction = (value) => typeof value === "function" && typeof value.actionName === "string";
|
|
19
|
-
/**
|
|
20
|
-
* Guard each launch argument: an action *instance* is required, not the action
|
|
21
|
-
* itself. Forgetting the trailing `()` is an easy slip (the callable has no
|
|
22
|
-
* `.start`), and silently treating it as something else would report a false
|
|
23
|
-
* green — so fail loudly with the fix.
|
|
24
|
-
*/
|
|
25
|
-
function assertInstance(value) {
|
|
26
|
-
if (isActionInstance(value))
|
|
27
|
-
return;
|
|
28
|
-
const hint = isUncalledAction(value)
|
|
29
|
-
? ` — that looks like an action that wasn't invoked; construct an instance by calling it: action("…").run(…)()`
|
|
30
|
-
: "";
|
|
31
|
-
throw new Error(`run() expects action instances; received ${typeof value}${hint}`);
|
|
32
|
-
}
|
|
33
|
-
/**
|
|
34
|
-
* A live spin over its own append-only log: constructing one launches the given
|
|
35
|
-
* actions in parallel under a single abort controller and drives them to a terminal
|
|
36
|
-
* {@link SpinResult}. Stop it with `.cancel()`, `await` its `.done` for the verdict.
|
|
37
|
-
*
|
|
38
|
-
* **A spin owns its world.** Each construction creates its *own* {@link LogsCollector}
|
|
39
|
-
* and {@link Output} — the run's log, folder, and snapshots — rather than sharing a
|
|
40
|
-
* process global, so runs don't bleed into each other and a fresh run can be created
|
|
41
|
-
* in a living process (sequential re-runs, in-process tests). Attach further actions
|
|
42
|
-
* to a running spin — including event-driven ones — via its `.run(…)` method.
|
|
43
|
-
*
|
|
44
|
-
* Each action runs against its own child logger (which headers and indents its
|
|
45
|
-
* lines into this run's log) and the spin's shared abort signal — both injected
|
|
46
|
-
* into its context, where the framework also binds a `proc` and the run's `output`
|
|
47
|
-
* so child processes stream into the right log and die on teardown. A **failing
|
|
48
|
-
* action never tears the spin down** — its `{ ok: false }` outcome is recorded in
|
|
49
|
-
* the verdict (and sets exit code 1) and the spin keeps running. Teardown to
|
|
50
|
-
* `closed` happens only on Ctrl-C, `.cancel()`, natural completion, or an uncaught
|
|
51
|
-
* fatal. The process-level hooks (SIGINT/uncaught handlers, `exitCode`) are owned by
|
|
52
|
-
* the run and cleaned at finalize, so they don't outlive it.
|
|
53
|
-
*/
|
|
54
|
-
export class SpinHost {
|
|
55
|
-
/** This run's append-only log — its own store, not a process global. */
|
|
56
|
-
logs = new LogsCollector();
|
|
57
|
-
/** This run's output subsystem (folder + snapshots + run-log writer), wired to {@link logs}. */
|
|
58
|
-
output = new Output(this.logs);
|
|
59
|
-
/** The spin's abort signal — for consumers that want to observe teardown. */
|
|
60
|
-
signal;
|
|
61
|
-
/** Settles once the spin reaches `closed`, carrying the terminal verdict. Never rejects. */
|
|
62
|
-
done;
|
|
63
|
-
/** The original `console.log`, captured before patching — the run's terminal writer. */
|
|
64
|
-
term;
|
|
65
|
-
/** Reinstalls the pre-run console methods; called once at finalize. */
|
|
66
|
-
restoreConsole;
|
|
67
|
-
controller = new AbortController();
|
|
68
|
-
// ── Lifecycle state ──────────────────────────────────────────────────────
|
|
69
|
-
// The single source of truth for "where in its life is this run". Only
|
|
70
|
-
// `beginClosing` and the finalize step below mutate it, and only forward.
|
|
71
|
-
// The log stays open through teardown and is closed once at finalize (after the
|
|
72
|
-
// last entry), so `streamLog` captures the whole run — including lines logged
|
|
73
|
-
// *during* teardown — and its run.jsonl is complete before `writeLog` reads it.
|
|
74
|
-
phase = "open";
|
|
75
|
-
/** The current lifecycle state — see {@link SpinState}. */
|
|
76
|
-
get state() {
|
|
77
|
-
return this.phase;
|
|
78
|
-
}
|
|
79
|
-
// Genuine failures (not teardown cascade). Frozen into the verdict at close.
|
|
80
|
-
failures = [];
|
|
81
|
-
// Every launched action's promise lives here until it settles; the drive loop
|
|
82
|
-
// empties this set. Daemons keep it non-empty until the controller aborts.
|
|
83
|
-
inFlight = new Set();
|
|
84
|
-
// Instances launched so far — used to verify a `.ref` dependency was launched
|
|
85
|
-
// before the action that consumes its handle.
|
|
86
|
-
launched = new WeakSet();
|
|
87
|
-
// The incremental crash-insurance stream; awaited at finalize before writeLog.
|
|
88
|
-
streaming;
|
|
89
|
-
// Ctrl-C tears the spin down gracefully through the lifecycle, like cancel().
|
|
90
|
-
onSigint = () => this.beginClosing("interrupted (SIGINT)");
|
|
91
|
-
onUncaught = this.onFatal("uncaughtException");
|
|
92
|
-
onUnhandled = this.onFatal("unhandledRejection");
|
|
93
|
-
constructor(...actions) {
|
|
94
|
-
// Validate eagerly so a bad argument throws cleanly before any work begins.
|
|
95
|
-
for (const instance of actions)
|
|
96
|
-
assertInstance(instance);
|
|
97
|
-
this.signal = this.controller.signal;
|
|
98
|
-
// Capture the ORIGINAL console.log as this run's terminal writer BEFORE patching,
|
|
99
|
-
// or `drain`/finalize would re-enter the patched method and feedback-loop. Then
|
|
100
|
-
// patch console so every console.* during the run folds into this run's log,
|
|
101
|
-
// attributed via `actionScope` (see `launch`).
|
|
102
|
-
this.term = console.log.bind(console);
|
|
103
|
-
this.restoreConsole = captureConsole(this.logs);
|
|
104
|
-
process.once("SIGINT", this.onSigint);
|
|
105
|
-
process.on("uncaughtException", this.onUncaught);
|
|
106
|
-
process.on("unhandledRejection", this.onUnhandled);
|
|
107
|
-
this.narrate(`run started · ${scriptPath()}`);
|
|
108
|
-
// Start streaming log entries to disk immediately — incremental writes survive
|
|
109
|
-
// hard kills where the final writeLog never runs. Kept so finalize can await it
|
|
110
|
-
// *before* writeLog rewrites the same files: otherwise a late flush from this
|
|
111
|
-
// stream lands past writeLog's truncation and the OS zero-fills the gap (a
|
|
112
|
-
// multi-MB NUL run in run.log).
|
|
113
|
-
this.streaming = this.output.runLog.stream(this.logs).catch(() => { });
|
|
114
|
-
// Live-render the log to the console for the whole run (replay + live). Always
|
|
115
|
-
// on — the run's terminal output, no flag or call required.
|
|
116
|
-
this.drain();
|
|
117
|
-
for (const instance of actions)
|
|
118
|
-
this.launch(instance, undefined);
|
|
119
|
-
// Launch before driving: the drive loop breaks on an empty `inFlight`, so it
|
|
120
|
-
// must not start until the initial actions have populated it.
|
|
121
|
-
this.done = this.drive();
|
|
122
|
-
// `done` resolves with the verdict and never rejects; guard anyway so a stray
|
|
123
|
-
// finalize error can't surface as an unhandled rejection.
|
|
124
|
-
this.done.catch(() => { });
|
|
125
|
-
}
|
|
126
|
-
/**
|
|
127
|
-
* Launch more actions into this same spin — same controller, same log. Chainable.
|
|
128
|
-
*
|
|
129
|
-
* Throws if the spin is no longer `open`.
|
|
130
|
-
*/
|
|
131
|
-
run(...more) {
|
|
132
|
-
for (const instance of more)
|
|
133
|
-
this.nodInto(instance, undefined);
|
|
134
|
-
return this;
|
|
135
|
-
}
|
|
136
|
-
/**
|
|
137
|
-
* Launch `instance` into the spin under an optional `parentPath` (the nodding
|
|
138
|
-
* action's path, or `undefined` for a top-level nod). The single guarded entry to
|
|
139
|
-
* launching — `run` (top-level) and an action's own scoped `nod` both route here,
|
|
140
|
-
* so the "spin is open" check lives in one place.
|
|
141
|
-
*/
|
|
142
|
-
nodInto(instance, parentPath) {
|
|
143
|
-
if (this.phase !== "open") {
|
|
144
|
-
throw new Error(`spin.run(…) rejected: the spin is "${this.phase}", not "open". Launch actions before ` +
|
|
145
|
-
`teardown, or from a lifecycle event of an action that is still running.`);
|
|
146
|
-
}
|
|
147
|
-
this.launch(instance, parentPath);
|
|
148
|
-
}
|
|
149
|
-
/** Begin teardown: abort the signal (killing procs, unblocking waits, stopping daemons). */
|
|
150
|
-
cancel() {
|
|
151
|
-
this.beginClosing("cancelled");
|
|
152
|
-
}
|
|
153
|
-
/**
|
|
154
|
-
* Live-render the log to the console. Started automatically from the constructor
|
|
155
|
-
* — every spin drains, with no flag to disable and nothing for the caller to
|
|
156
|
-
* invoke (for now). A single {@link LogRenderer} applies grouping/indentation as
|
|
157
|
-
* entries stream by; its state spans the replayed history and live entries as one
|
|
158
|
-
* continuous stream. The subscription ends itself when the run tears down (the
|
|
159
|
-
* collector closes it), so this loop simply runs to completion.
|
|
160
|
-
*/
|
|
161
|
-
drain() {
|
|
162
|
-
void (async () => {
|
|
163
|
-
const renderer = new LogRenderer();
|
|
164
|
-
for await (const entry of this.logs.subscribe({ replay: true }))
|
|
165
|
-
this.term(renderer.render(entry));
|
|
166
|
-
})().catch(() => { });
|
|
167
|
-
}
|
|
168
|
-
/** Narrate a run-lifecycle milestone into this run's log (renders as a `[run]` line). */
|
|
169
|
-
narrate(message) {
|
|
170
|
-
this.logs.append({ source: "run", level: "run", message });
|
|
171
|
-
}
|
|
172
|
-
/**
|
|
173
|
-
* Move `open → closing`: abort the signal (unwinding procs, waits, and
|
|
174
|
-
* daemons), narrate the cause once, and stop accepting new actions. Idempotent
|
|
175
|
-
* — only the first caller transitions; later triggers (a second failure, a
|
|
176
|
-
* `cancel()` during teardown) are no-ops. This is the *only* path to abort, so
|
|
177
|
-
* every teardown reason flows through one place.
|
|
178
|
-
*/
|
|
179
|
-
beginClosing(reason) {
|
|
180
|
-
if (this.phase !== "open")
|
|
181
|
-
return;
|
|
182
|
-
this.phase = "closing";
|
|
183
|
-
this.narrate(`tearing down · ${reason}`);
|
|
184
|
-
this.controller.abort();
|
|
185
|
-
}
|
|
186
|
-
/**
|
|
187
|
-
* Record a genuine fault in the verdict. It does **not** tear the spin down — a
|
|
188
|
-
* failing action is data, and the spin keeps running (only Ctrl-C / cancel /
|
|
189
|
-
* completion / an uncaught fatal close it).
|
|
190
|
-
*/
|
|
191
|
-
recordFailure(action, error) {
|
|
192
|
-
this.failures.push({ action, error });
|
|
193
|
-
}
|
|
194
|
-
/**
|
|
195
|
-
* A throw or rejection that escaped every action's own handling. Without a
|
|
196
|
-
* handler Node prints it and exits *immediately* — before the run can flush its
|
|
197
|
-
* log, which leaves a silent, empty log directory and no verdict. So catch it,
|
|
198
|
-
* log it, fold it into the verdict, and drive teardown so the log still flushes
|
|
199
|
-
* and the exit code reflects the fault — degrading a stray throw to a recorded
|
|
200
|
-
* failure instead of a hard crash. Removed at finalize, so it only governs
|
|
201
|
-
* while this run owns the process.
|
|
202
|
-
*/
|
|
203
|
-
onFatal(kind) {
|
|
204
|
-
return (error) => {
|
|
205
|
-
this.logs.append({
|
|
206
|
-
source: kind,
|
|
207
|
-
level: "error",
|
|
208
|
-
message: error instanceof Error ? (error.stack ?? error.message) : String(error),
|
|
209
|
-
});
|
|
210
|
-
// Genuine fault vs. teardown fallout: only one that surfaces *before* the
|
|
211
|
-
// signal aborts is the spin's own (mirrors `launch`'s cascade rule); a throw
|
|
212
|
-
// during unwind is logged but doesn't pollute the verdict. Either way tear
|
|
213
|
-
// down — unlike an action's own failure, an *uncaught* fatal leaves the
|
|
214
|
-
// process state undefined, so we always finish and flush.
|
|
215
|
-
if (!this.controller.signal.aborted)
|
|
216
|
-
this.failures.push({ action: kind, error });
|
|
217
|
-
this.beginClosing(kind);
|
|
218
|
-
};
|
|
219
|
-
}
|
|
220
|
-
launch(instance, parentPath) {
|
|
221
|
-
// A consumer built with another action's `.ref` (e.g. `chromePage("Explorer", driver.ref)`)
|
|
222
|
-
// must be launched *after* that producer — otherwise it awaits a handle that
|
|
223
|
-
// never arrives and the spin hangs silently. Catch it and say how to fix it.
|
|
224
|
-
for (const arg of instance.args) {
|
|
225
|
-
const producer = producerOfRef(arg);
|
|
226
|
-
if (producer && !this.launched.has(producer)) {
|
|
227
|
-
throw new Error(`${instance.name} was launched before its dependency ${producer.name}: it ` +
|
|
228
|
-
`consumes ${producer.name}'s .ref, so nod ${producer.name} first.`);
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
this.launched.add(instance);
|
|
232
|
-
// The action's path: its name for a top-level nod, or `<parent> › <name>` when a
|
|
233
|
-
// parent action nodded it. Top-level actions keep this run's logger (output
|
|
234
|
-
// unchanged); a child gets a ScopedLogger so its every line — and its `proc`'s,
|
|
235
|
-
// and any grandchildren's — is prefixed with this path. Its own `nod` carries
|
|
236
|
-
// this path down, so scoping composes automatically.
|
|
237
|
-
const path = parentPath ? `${parentPath} › ${instance.name}` : instance.name;
|
|
238
|
-
const scoped = parentPath === undefined ? this.logs : new ScopedLogger(this.logs, path);
|
|
239
|
-
const nod = (child) => {
|
|
240
|
-
this.nodInto(child, path);
|
|
241
|
-
return child;
|
|
242
|
-
};
|
|
243
|
-
// A boolean check bound to this action's path: it logs a `⊨` milestone, and a
|
|
244
|
-
// mismatch records a verdict failure the same way an action's own failure does
|
|
245
|
-
// — skipped during teardown (cascade, not a fault). Uses `this.logs` (not
|
|
246
|
-
// `scoped`) the same way `record` below does: the path is already embedded in
|
|
247
|
-
// the message via `name`, so routing through `scoped` would double-prefix the
|
|
248
|
-
// `source` field for a nodded child.
|
|
249
|
-
const assert = createAssert(path, this.logs, (error) => {
|
|
250
|
-
if (!this.controller.signal.aborted)
|
|
251
|
-
this.recordFailure(path, error);
|
|
252
|
-
});
|
|
253
|
-
this.narrate(`launch ${path}`);
|
|
254
|
-
// The action logs into this run's shared store (entries carry their `source`;
|
|
255
|
-
// grouping is applied at render time) and gets the spin's signal directly; the
|
|
256
|
-
// framework injects a `proc` and the run's `output` bound to both, so there's no
|
|
257
|
-
// ambient context. `start()` resolves with the action's Outcome — it never
|
|
258
|
-
// rejects for the action's own failure — so a failure is handled here as data.
|
|
259
|
-
const record = (error) => {
|
|
260
|
-
// Always record the fault in the action's own log, under its path.
|
|
261
|
-
this.logs.append({
|
|
262
|
-
source: path,
|
|
263
|
-
level: "error",
|
|
264
|
-
message: error instanceof Error ? (error.stack ?? error.message) : String(error),
|
|
265
|
-
});
|
|
266
|
-
// Genuine failure vs. teardown fallout: if the signal was already aborted
|
|
267
|
-
// when this surfaced, the failure is almost certainly the abort killing the
|
|
268
|
-
// action (a cascade), not its own fault — so it must not pollute the verdict.
|
|
269
|
-
if (!this.controller.signal.aborted)
|
|
270
|
-
this.recordFailure(path, error);
|
|
271
|
-
};
|
|
272
|
-
const promise = actionScope
|
|
273
|
-
.run(path, () => instance.start({
|
|
274
|
-
logs: scoped,
|
|
275
|
-
signal: this.controller.signal,
|
|
276
|
-
nod,
|
|
277
|
-
output: this.output,
|
|
278
|
-
assert,
|
|
279
|
-
}))
|
|
280
|
-
.then((outcome) => {
|
|
281
|
-
if (!outcome.ok)
|
|
282
|
-
record(outcome.error);
|
|
283
|
-
})
|
|
284
|
-
// Defensive: `start()` shouldn't reject, but if the framework itself throws,
|
|
285
|
-
// treat it as a fault rather than losing it to an unhandled rejection.
|
|
286
|
-
.catch(record);
|
|
287
|
-
this.inFlight.add(promise);
|
|
288
|
-
void promise.finally(() => this.inFlight.delete(promise));
|
|
289
|
-
}
|
|
290
|
-
/**
|
|
291
|
-
* Drives the run to `closed`. The loop re-checks `inFlight` because actions can
|
|
292
|
-
* be added (or daemons can settle on abort) while we await. A synchronous
|
|
293
|
-
* `.run(…)` from a settling action's lifecycle event lands before that action's
|
|
294
|
-
* `finally` removes it, so `inFlight` never spuriously hits 0 mid-handoff.
|
|
295
|
-
*/
|
|
296
|
-
async drive() {
|
|
297
|
-
for (;;) {
|
|
298
|
-
if (this.inFlight.size === 0) {
|
|
299
|
-
// Nothing left to run. If still open, every action finished on its own —
|
|
300
|
-
// natural completion; tear down so daemons/listeners unwind uniformly.
|
|
301
|
-
// If already closing, teardown is simply complete.
|
|
302
|
-
if (this.phase === "open")
|
|
303
|
-
this.beginClosing("completed");
|
|
304
|
-
break;
|
|
305
|
-
}
|
|
306
|
-
await Promise.allSettled([...this.inFlight]);
|
|
307
|
-
}
|
|
308
|
-
// ── Finalize (reached exactly once) ──────────────────────────────────────
|
|
309
|
-
this.phase = "closed";
|
|
310
|
-
process.off("SIGINT", this.onSigint);
|
|
311
|
-
process.off("uncaughtException", this.onUncaught);
|
|
312
|
-
process.off("unhandledRejection", this.onUnhandled);
|
|
313
|
-
this.restoreConsole();
|
|
314
|
-
this.narrate("finished");
|
|
315
|
-
// Close the log now (after the last entry) so `streamLog` drains and finishes
|
|
316
|
-
// writing the complete run.jsonl, then wait for it to fully close its files —
|
|
317
|
-
// both so its last flush can't corrupt run.log later, and so run.jsonl is done
|
|
318
|
-
// before `writeLog` streams it into the final collapsed run.log.
|
|
319
|
-
this.logs.close();
|
|
320
|
-
await this.streaming;
|
|
321
|
-
const file = await this.output.runLog.write();
|
|
322
|
-
// Direct to the terminal — the file's already written, so this line isn't in it.
|
|
323
|
-
this.term(LogRenderer.marker("run", `log → ${file}`));
|
|
324
|
-
// If this run logged any assertions, link the dedicated file beside the main one
|
|
325
|
-
// (only exists when there was at least one assertion). Terminal-only, like `log →`.
|
|
326
|
-
const assertionsFile = this.output.runLog.assertionsLog;
|
|
327
|
-
if (assertionsFile)
|
|
328
|
-
this.term(LogRenderer.marker("assert", `assertions → ${assertionsFile}`));
|
|
329
|
-
const result = { ok: this.failures.length === 0, failures: this.failures };
|
|
330
|
-
// One process = one run = one verdict; reflect pass/fail in the exit code so
|
|
331
|
-
// shells and CI see it without parsing the log. Only set it on failure, so a
|
|
332
|
-
// code a failing action's own handler may have set isn't clobbered to 0.
|
|
333
|
-
if (!result.ok)
|
|
334
|
-
process.exitCode = 1;
|
|
335
|
-
return result;
|
|
336
|
-
}
|
|
337
|
-
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import type { SpinBody } from "./types.ts";
|
|
2
|
-
/**
|
|
3
|
-
* Runs a linear orchestration. You pass a plain (async) function; omkit hosts it
|
|
4
|
-
* in a single in-flight action and hands it a {@link Nod}. Because that host
|
|
5
|
-
* stays in-flight while your body `await`s, the spin never idles shut between
|
|
6
|
-
* steps — so you write `await` / `if` control flow instead of nested callbacks.
|
|
7
|
-
* When the body returns, the daemons it nodded in keep the spin alive until
|
|
8
|
-
* teardown.
|
|
9
|
-
*
|
|
10
|
-
* A failing action **never throws into the body** — `await nod(x).done` yields an
|
|
11
|
-
* {@link import("./action.ts").Outcome} you inspect, and the spin keeps going
|
|
12
|
-
* (the failure is recorded in the verdict and sets exit code 1):
|
|
13
|
-
*
|
|
14
|
-
* spin({ drain: false }, async ({ nod }) => {
|
|
15
|
-
* const ans = await nod(prompt({ … })).done; // Outcome — never throws
|
|
16
|
-
* if (ans.ok && ans.value === "yes") await nod(test).done;
|
|
17
|
-
* nod(devServer); // fire-and-forget daemon
|
|
18
|
-
* const chrome = nod(chromedriver({ … }));
|
|
19
|
-
* nod(chromePage("Explorer", chrome.ref)); // data deps via .ref
|
|
20
|
-
* });
|
|
21
|
-
*
|
|
22
|
-
* Auto-drains by default;
|
|
23
|
-
*/
|
|
24
|
-
export declare function spin(body: SpinBody): void;
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import { action } from "../action/action.js";
|
|
2
|
-
import { SpinHost } from "./SpinHost.js";
|
|
3
|
-
/**
|
|
4
|
-
* Runs a linear orchestration. You pass a plain (async) function; omkit hosts it
|
|
5
|
-
* in a single in-flight action and hands it a {@link Nod}. Because that host
|
|
6
|
-
* stays in-flight while your body `await`s, the spin never idles shut between
|
|
7
|
-
* steps — so you write `await` / `if` control flow instead of nested callbacks.
|
|
8
|
-
* When the body returns, the daemons it nodded in keep the spin alive until
|
|
9
|
-
* teardown.
|
|
10
|
-
*
|
|
11
|
-
* A failing action **never throws into the body** — `await nod(x).done` yields an
|
|
12
|
-
* {@link import("./action.ts").Outcome} you inspect, and the spin keeps going
|
|
13
|
-
* (the failure is recorded in the verdict and sets exit code 1):
|
|
14
|
-
*
|
|
15
|
-
* spin({ drain: false }, async ({ nod }) => {
|
|
16
|
-
* const ans = await nod(prompt({ … })).done; // Outcome — never throws
|
|
17
|
-
* if (ans.ok && ans.value === "yes") await nod(test).done;
|
|
18
|
-
* nod(devServer); // fire-and-forget daemon
|
|
19
|
-
* const chrome = nod(chromedriver({ … }));
|
|
20
|
-
* nod(chromePage("Explorer", chrome.ref)); // data deps via .ref
|
|
21
|
-
* });
|
|
22
|
-
*
|
|
23
|
-
* Auto-drains by default;
|
|
24
|
-
*/
|
|
25
|
-
export function spin(body) {
|
|
26
|
-
// `run(...)` below assigns `host` synchronously, before the action's async body
|
|
27
|
-
// ever runs — so the closures here capture the spin safely.
|
|
28
|
-
let hostRun;
|
|
29
|
-
const hostAction = action("omkit:Spin").run(async (ctx) => {
|
|
30
|
-
const nod = (instance) => {
|
|
31
|
-
hostRun.run(instance);
|
|
32
|
-
return instance;
|
|
33
|
-
};
|
|
34
|
-
await body({
|
|
35
|
-
nod,
|
|
36
|
-
// TODO: maybe add a reason for cancel ???
|
|
37
|
-
cancel: () => hostRun.cancel(),
|
|
38
|
-
signal: ctx.signal,
|
|
39
|
-
// The host action runs inside the SpinHost, so its ctx already carries the
|
|
40
|
-
// run's output subsystem — surface the folder and a snapshot convenience to
|
|
41
|
-
// the body from there rather than reaching for a module singleton.
|
|
42
|
-
artifactsFolder: ctx.artifactsFolder,
|
|
43
|
-
snapshot: (name, value) => ctx.output.snapshots.snapshot(name, value),
|
|
44
|
-
assert: ctx.assert,
|
|
45
|
-
});
|
|
46
|
-
});
|
|
47
|
-
const mainSpinAction = hostAction();
|
|
48
|
-
hostRun = new SpinHost(mainSpinAction);
|
|
49
|
-
// The body is the orchestrator, not a peer action: if it throws, tear the whole
|
|
50
|
-
// spin down. Regular action failures only get recorded (they don't stop the
|
|
51
|
-
// spin), but the orchestrator crashing must end the run — otherwise any daemon
|
|
52
|
-
// it already nodded keeps the process alive forever on a pipeline bug.
|
|
53
|
-
void mainSpinAction.done.then((outcome) => {
|
|
54
|
-
if (!outcome.ok)
|
|
55
|
-
hostRun.cancel();
|
|
56
|
-
});
|
|
57
|
-
}
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
export type { Nod } from "../action/types.ts";
|
|
2
|
-
import type { Nod } from "../action/types.ts";
|
|
3
|
-
import type { Assert } from "../assert.ts";
|
|
4
|
-
/** What a {@link spin} body receives. */
|
|
5
|
-
export interface SpinContext {
|
|
6
|
-
/** Launch an action into the spin; returns the instance for awaiting/chaining. */
|
|
7
|
-
readonly nod: Nod;
|
|
8
|
-
/**
|
|
9
|
-
* Tear the spin down (abort the signal, stopping daemons). Use it to end a spin
|
|
10
|
-
* whose work is finished but which is held open by a daemon — e.g. once a
|
|
11
|
-
* one-shot build completes while a watcher is still running.
|
|
12
|
-
*/
|
|
13
|
-
readonly cancel: () => void;
|
|
14
|
-
/** The spin's abort signal — honor it to stop early. */
|
|
15
|
-
readonly signal: AbortSignal;
|
|
16
|
-
/**
|
|
17
|
-
* Absolute path to this spin's output folder (`logs/<name>/<date>/<time>/`),
|
|
18
|
-
* created up front. Hand it to actions (or use `ctx.artifactsFolder` inside
|
|
19
|
-
* one) to write artifacts beside `run.log`.
|
|
20
|
-
*/
|
|
21
|
-
readonly artifactsFolder: string;
|
|
22
|
-
/**
|
|
23
|
-
* Capture a JSON snapshot into this run's output folder and drop a `[snapshot]`
|
|
24
|
-
* line on the timeline — for run-level inputs the log should narrate (a build
|
|
25
|
-
* config, resolved options). Resolves with the snapshot's relative path. The
|
|
26
|
-
* run-scoped counterpart to the action `ctx.output.snapshots`.
|
|
27
|
-
*/
|
|
28
|
-
readonly snapshot: (name: string, value: unknown) => Promise<string>;
|
|
29
|
-
/**
|
|
30
|
-
* Assert a boolean invariant from the spin body. Same semantics as the action
|
|
31
|
-
* `ctx.assert`; body assertions are attributed to the host action path
|
|
32
|
-
* (`omkit:Spin`).
|
|
33
|
-
*/
|
|
34
|
-
readonly assert: Assert;
|
|
35
|
-
}
|
|
36
|
-
/** The body of a {@link spin}: any (async) function — it isn't named or wrapped by you. */
|
|
37
|
-
export type SpinBody = (ctx: SpinContext) => void | Promise<void>;
|
|
38
|
-
/**
|
|
39
|
-
* A single action's failure, captured for the spin's verdict. Cascade failures —
|
|
40
|
-
* actions that settled `{ ok: false }` only because teardown aborted them — are
|
|
41
|
-
* deliberately excluded (see `launch`'s outcome handling), so this records
|
|
42
|
-
* genuine faults, not fallout.
|
|
43
|
-
*/
|
|
44
|
-
export interface ActionFailure {
|
|
45
|
-
/** The failing action's name. */
|
|
46
|
-
readonly action: string;
|
|
47
|
-
/** Whatever it failed with (the Outcome's `error`). */
|
|
48
|
-
readonly error: unknown;
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* The terminal verdict of a spin: did everything that mattered succeed, and if
|
|
52
|
-
* not, what failed. {@link SpinHost.done} resolves (never rejects) with this, so a
|
|
53
|
-
* single `await` yields the outcome instead of forcing a try/catch on awaiters.
|
|
54
|
-
* A failing action is *recorded* here (so `ok` is false and the exit code is 1)
|
|
55
|
-
* but does **not** tear the spin down — see {@link SpinHost}.
|
|
56
|
-
*/
|
|
57
|
-
export interface SpinResult {
|
|
58
|
-
/** True when no genuine failure was recorded. */
|
|
59
|
-
readonly ok: boolean;
|
|
60
|
-
/** The genuine failures, in the order they surfaced. Empty when `ok`. */
|
|
61
|
-
readonly failures: readonly ActionFailure[];
|
|
62
|
-
}
|
|
63
|
-
export type SpinState = "open" | "closing" | "closed";
|
package/dist/output/Output.d.ts
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import type { Logger } from "./log/LogsCollector.ts";
|
|
2
|
-
import { RunFolder } from "./folder/RunFolder.ts";
|
|
3
|
-
import { SnapshotStore } from "./snapshot/SnapshotStore.ts";
|
|
4
|
-
import { RunLog } from "./log/RunLog.ts";
|
|
5
|
-
/**
|
|
6
|
-
* The run's output subsystem — the composition root that owns the per-run folder and
|
|
7
|
-
* the writers layered on it, and hands them to the rest of the process.
|
|
8
|
-
*
|
|
9
|
-
* Each run owns one of these: its {@link SpinHost} constructs an `Output` over the
|
|
10
|
-
* run's log and threads it to actions as `ctx.output`. Everything that was
|
|
11
|
-
* module-global in the old `output.ts` (the folder path, the mkdir latch, the
|
|
12
|
-
* snapshot sequence) now lives inside these three members, reachable as
|
|
13
|
-
* `output.folder`, `output.snapshots`, `output.runLog` — so callers reach for a named
|
|
14
|
-
* layer instead of a free function, and the shared state has one owner per run.
|
|
15
|
-
*/
|
|
16
|
-
export declare class Output {
|
|
17
|
-
/** The per-run directory and artifact drop. */
|
|
18
|
-
readonly folder: RunFolder;
|
|
19
|
-
/** Sequenced JSON/text snapshot files + `[snapshot]` timeline lines. */
|
|
20
|
-
readonly snapshots: SnapshotStore;
|
|
21
|
-
/** The `run.jsonl` / `run.log` writer. */
|
|
22
|
-
readonly runLog: RunLog;
|
|
23
|
-
constructor(logger: Logger);
|
|
24
|
-
}
|
package/dist/output/Output.js
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
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
|
-
}
|
package/dist/output/index.d.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
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";
|
package/dist/output/index.js
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
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";
|
|
@@ -1,73 +0,0 @@
|
|
|
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" | "assert";
|
|
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
|
-
}
|