omkit 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +144 -0
  3. package/dist/actions/chrome-page.d.ts +44 -0
  4. package/dist/actions/chrome-page.js +94 -0
  5. package/dist/actions/command.d.ts +26 -0
  6. package/dist/actions/command.js +32 -0
  7. package/dist/actions/healthcheck.d.ts +55 -0
  8. package/dist/actions/healthcheck.js +94 -0
  9. package/dist/actions/index.d.ts +22 -0
  10. package/dist/actions/index.js +14 -0
  11. package/dist/actions/prompt.d.ts +54 -0
  12. package/dist/actions/prompt.js +99 -0
  13. package/dist/actions/tail-log.d.ts +30 -0
  14. package/dist/actions/tail-log.js +124 -0
  15. package/dist/actions/until-log.d.ts +24 -0
  16. package/dist/actions/until-log.js +49 -0
  17. package/dist/actions/watch-dir.d.ts +35 -0
  18. package/dist/actions/watch-dir.js +159 -0
  19. package/dist/actions/watch.d.ts +33 -0
  20. package/dist/actions/watch.js +80 -0
  21. package/dist/core/action.d.ts +105 -0
  22. package/dist/core/action.js +133 -0
  23. package/dist/core/events.d.ts +23 -0
  24. package/dist/core/events.js +61 -0
  25. package/dist/core/helpers/AsyncQueue.d.ts +13 -0
  26. package/dist/core/helpers/AsyncQueue.js +50 -0
  27. package/dist/core/log-collector/LogsCollector.d.ts +63 -0
  28. package/dist/core/log-collector/LogsCollector.js +110 -0
  29. package/dist/core/log-collector/global.d.ts +7 -0
  30. package/dist/core/log-collector/global.js +7 -0
  31. package/dist/core/log-collector/render.d.ts +57 -0
  32. package/dist/core/log-collector/render.js +111 -0
  33. package/dist/core/markers.d.ts +9 -0
  34. package/dist/core/markers.js +13 -0
  35. package/dist/core/output.d.ts +55 -0
  36. package/dist/core/output.js +163 -0
  37. package/dist/core/process.d.ts +21 -0
  38. package/dist/core/process.js +73 -0
  39. package/dist/core/run.d.ts +95 -0
  40. package/dist/core/run.js +212 -0
  41. package/dist/core/spin.d.ts +58 -0
  42. package/dist/core/spin.js +29 -0
  43. package/dist/index.d.ts +25 -0
  44. package/dist/index.js +25 -0
  45. package/package.json +59 -0
@@ -0,0 +1,105 @@
1
+ import type { Logger } from "./log-collector/LogsCollector.ts";
2
+ import { type Emitter, type EventHandler } from "./events.ts";
3
+ import { type Proc } from "./process.ts";
4
+ /** An action that declares no events. */
5
+ export type NoEvents = Record<never, never>;
6
+ /** A value an action's `exec` may return: a result, or a promise of one. */
7
+ export type Awaitable<T> = T | Promise<T>;
8
+ /**
9
+ * Lifecycle events the framework emits for every action — on top of whatever it
10
+ * declares via `.emits<…>()`. They make key moments first-class signals (no
11
+ * log-scraping) and give a uniform sequencing hook.
12
+ */
13
+ export interface SystemEvents<Result> {
14
+ /** The action published its imperative handle via `ctx.attach` (fires once). */
15
+ attached: void;
16
+ /** The run resolved; payload is its result. */
17
+ done: Result;
18
+ /** The run rejected; payload is the thrown error. */
19
+ error: unknown;
20
+ }
21
+ /** The full event map a constructed instance exposes: the declared events plus the system ones. */
22
+ export type InstanceEvents<Events extends object, Result> = Events & SystemEvents<Result>;
23
+ /** System services the pipeline injects into every action when it runs. */
24
+ export interface SystemGlobal {
25
+ /** A logger bound to this action — feeds it directly or via `proc`. */
26
+ readonly logs: Logger;
27
+ /** The run's abort signal — aborted on teardown/cancel; honor it to stop early. */
28
+ readonly signal: AbortSignal;
29
+ }
30
+ /** What an action's implementation receives: the system bag, plus `emit`, `attach`, and `proc`. */
31
+ export interface ActionContext<Events extends object, Handle = void> extends SystemGlobal {
32
+ /** Emit one of this action's declared events (also pushed to the global log). */
33
+ readonly emit: Emitter<Events>["emit"];
34
+ /** Publish this action's imperative handle, resolving its instance's `.ref`. */
35
+ readonly attach: (handle: Handle) => void;
36
+ /**
37
+ * Spawn child processes bound to this action: their output streams into this
38
+ * action's log and they're killed on teardown. The built-in source; a custom
39
+ * one (e.g. CDP) follows the same pattern over `logs`/`signal`.
40
+ */
41
+ readonly proc: Proc;
42
+ }
43
+ /** A constructed-but-not-yet-run action: what calling an `Action` produces. */
44
+ export interface ActionInstance<Result = unknown, Events extends object = NoEvents, Handle = void> {
45
+ readonly name: string;
46
+ readonly args: readonly unknown[];
47
+ /** Runs the action with the captured args and the injected system services. */
48
+ readonly start: (system: SystemGlobal) => Promise<Result>;
49
+ /**
50
+ * Resolves with the handle the action attached via `ctx.attach` (set once). If
51
+ * the action settles *without* attaching, this never hangs: it resolves
52
+ * (`undefined` for the default `void` handle) on success, or rejects with the
53
+ * action's error on failure.
54
+ */
55
+ readonly ref: Promise<Handle>;
56
+ /** Subscribe to a declared or system event (`done`/`error`) with a handler. */
57
+ on<K extends keyof InstanceEvents<Events, Result>>(key: K, handler: EventHandler<InstanceEvents<Events, Result>[K]>): void;
58
+ /**
59
+ * A promise for the next occurrence of an event: resolves with its payload —
60
+ * or rejects if the action settles first (`error`, or `done` without the
61
+ * event), so an awaiter never hangs. Use {@link on} for ongoing/handler
62
+ * subscriptions; `once` is the awaitable, one-value form.
63
+ */
64
+ once<K extends keyof InstanceEvents<Events, Result>>(key: K): Promise<InstanceEvents<Events, Result>[K]>;
65
+ }
66
+ /**
67
+ * Any constructed instance, whatever its result, events, or handle. The launch
68
+ * surface ({@link ActionInstance}-takers like `run` and `Run.run`) accepts this
69
+ * so handle-bearing actions — those declaring `.ref<H>()` — can be launched: the
70
+ * bare `ActionInstance` pins the handle to `void`, so an instance whose `ref` is
71
+ * a `Promise<Page>` (not `Promise<void>`) would otherwise be rejected. `unknown`
72
+ * for result/handle and `object` for events admit every concrete instance while
73
+ * still excluding non-instances.
74
+ */
75
+ export type AnyActionInstance = ActionInstance<unknown, object, unknown>;
76
+ /** A callable produced by `action`: invoking it builds an {@link ActionInstance}. */
77
+ export interface Action<Args extends unknown[], Result, Events extends object = NoEvents, Handle = void> {
78
+ (...args: Args): ActionInstance<Result, Events, Handle>;
79
+ readonly actionName: string;
80
+ }
81
+ /** Intermediate step from `action(name)`: declare events/handle, then provide the impl. */
82
+ export interface ActionBuilder<Events extends object, Handle = void> {
83
+ /** Declare the typed event map this action emits. */
84
+ emits<E extends object>(): ActionBuilder<E, Handle>;
85
+ /** Declare the imperative handle this action exposes (resolved via `instance.ref`). */
86
+ ref<H>(): ActionBuilder<Events, H>;
87
+ /** Provide the implementation. `ctx` carries `logs`, `signal`, `emit`, and `attach`. */
88
+ run<Args extends unknown[], Result>(exec: (ctx: ActionContext<Events, Handle>, ...args: Args) => Awaitable<Result>): Action<Args, Result, Events, Handle>;
89
+ }
90
+ /**
91
+ * Defines an action. Always returns a builder; provide the implementation via
92
+ * `.run(…)`, optionally after declaring events and/or a handle:
93
+ *
94
+ * action("Build").run(async (ctx, opts) => { … }) // no declared events / handle
95
+ * action("Watch") // typed events + handle
96
+ * .emits<{ create: string }>()
97
+ * .ref<{ rescan(): void }>()
98
+ * .run((ctx, opts) => { ctx.emit(…); ctx.attach(…); … })
99
+ *
100
+ * `.run(…)` yields a callable that, when invoked, constructs an
101
+ * {@link ActionInstance} (deferred — the run executes it). Instances expose
102
+ * chainable `.on` / `.once` (declared events plus `done`/`error`); the attached
103
+ * handle is awaited via `instance.ref`.
104
+ */
105
+ export declare function action(name: string): ActionBuilder<NoEvents, void>;
@@ -0,0 +1,133 @@
1
+ import { events } from "./events.js";
2
+ import { createProc } from "./process.js";
3
+ /** A promise paired with its settlers, for the set-once handle. */
4
+ const deferred = () => {
5
+ let resolve;
6
+ let reject;
7
+ const promise = new Promise((res, rej) => {
8
+ resolve = res;
9
+ reject = rej;
10
+ });
11
+ return { promise, resolve, reject };
12
+ };
13
+ /**
14
+ * A promise for the next emit of `key`. It races the event against the
15
+ * instance's terminal events: if the action errors first it rejects with that
16
+ * error, and if it completes (`done`) without ever emitting `key` it rejects too
17
+ * — so an awaiter can never hang. A retained snapshot resolves it immediately.
18
+ */
19
+ const awaitEvent = (emitter, name, key) => new Promise((resolve, reject) => {
20
+ const offs = [];
21
+ let settled = false;
22
+ // First of [the event | error | done] to fire wins; the rest are unsubscribed.
23
+ const settle = (act) => {
24
+ if (settled)
25
+ return;
26
+ settled = true;
27
+ for (const off of offs)
28
+ off();
29
+ act();
30
+ };
31
+ // listenOnce is keyed by E; the system keys (error/done) are always present
32
+ // on an instance's event map, so reach them through a loosened view.
33
+ const listen = emitter.listenOnce;
34
+ offs.push(listen(key, (payload) => settle(() => resolve(payload))));
35
+ if (key !== "error") {
36
+ offs.push(listen("error", (err) => settle(() => reject(err))));
37
+ }
38
+ if (key !== "done") {
39
+ offs.push(listen("done", () => settle(() => reject(new Error(`${name}: settled before "${String(key)}"`)))));
40
+ }
41
+ // A retained snapshot fires a listen synchronously during registration; if
42
+ // that already settled us, drop any listeners registered afterwards.
43
+ if (settled)
44
+ for (const off of offs)
45
+ off();
46
+ });
47
+ /** Assembles an {@link Action} from a name and its implementation. */
48
+ const build = (name, exec) => {
49
+ const create = (...args) => {
50
+ // The emitter covers declared + system events; `ctx.emit` is narrowed to the
51
+ // declared ones, while the framework emits done/error through the same bus.
52
+ const emitter = events(name);
53
+ const emit = emitter.emit;
54
+ // Loosely typed for the framework's own emits (sidesteps EmitArgs for void results).
55
+ const systemEmit = emitter.emit;
56
+ // The handle lives on the instance as a set-once promise: `attach` resolves
57
+ // it, `instance.ref` awaits it. A second attach is a no-op (promise settled).
58
+ // If the action settles without ever attaching, `start` below settles it too
59
+ // (resolve on success, reject on error) so `ref` can't hang.
60
+ const handle = deferred();
61
+ // Attaching is a one-time lifecycle moment: resolve `ref` and emit `attached`
62
+ // (the *fact* the action became controllable — not the handle, which is just
63
+ // functions). Guarded so a second attach neither re-emits nor re-resolves.
64
+ let attached = false;
65
+ const attach = (value) => {
66
+ if (attached)
67
+ return;
68
+ attached = true;
69
+ handle.resolve(value);
70
+ systemEmit("attached");
71
+ };
72
+ // Most actions have a `void` handle and no one awaits `ref`; mark the promise
73
+ // handled so a settle-time rejection on such an instance doesn't surface as an
74
+ // unhandled rejection. Real awaiters of `ref` still receive the rejection.
75
+ void handle.promise.catch(() => { });
76
+ const instance = {
77
+ name,
78
+ args,
79
+ ref: handle.promise,
80
+ start: (system) =>
81
+ // `Promise.resolve().then` normalizes a sync result and routes a sync
82
+ // throw to the `error` path, so exec may be sync or async.
83
+ Promise.resolve()
84
+ .then(() => exec({ ...system, emit, attach, proc: createProc(system.logs, system.signal) }, ...args))
85
+ .then((result) => {
86
+ systemEmit("done", result);
87
+ // Settle `ref` if the action finished without attaching, so awaiters
88
+ // resolve (with `undefined` for the default void handle) rather than
89
+ // hang. Idempotent — a no-op when a handle was already attached.
90
+ handle.resolve(undefined);
91
+ return result;
92
+ }, (error) => {
93
+ systemEmit("error", error);
94
+ // The action failed, so its handle will never arrive — reject `ref`
95
+ // with the same error instead of leaving awaiters hung. No-op if a
96
+ // handle was already attached before the failure.
97
+ handle.reject(error);
98
+ throw error;
99
+ }),
100
+ on(key, handler) {
101
+ emitter.listen(key, handler);
102
+ },
103
+ once(key) {
104
+ return awaitEvent(emitter, name, key);
105
+ },
106
+ };
107
+ return instance;
108
+ };
109
+ return Object.assign(create, { actionName: name });
110
+ };
111
+ const builder = (name) => ({
112
+ emits: () => builder(name),
113
+ ref: () => builder(name),
114
+ run: (exec) => build(name, exec),
115
+ });
116
+ /**
117
+ * Defines an action. Always returns a builder; provide the implementation via
118
+ * `.run(…)`, optionally after declaring events and/or a handle:
119
+ *
120
+ * action("Build").run(async (ctx, opts) => { … }) // no declared events / handle
121
+ * action("Watch") // typed events + handle
122
+ * .emits<{ create: string }>()
123
+ * .ref<{ rescan(): void }>()
124
+ * .run((ctx, opts) => { ctx.emit(…); ctx.attach(…); … })
125
+ *
126
+ * `.run(…)` yields a callable that, when invoked, constructs an
127
+ * {@link ActionInstance} (deferred — the run executes it). Instances expose
128
+ * chainable `.on` / `.once` (declared events plus `done`/`error`); the attached
129
+ * handle is awaited via `instance.ref`.
130
+ */
131
+ export function action(name) {
132
+ return builder(name);
133
+ }
@@ -0,0 +1,23 @@
1
+ /** Passed to every handler so it can tell a replayed snapshot from a live emit. */
2
+ export interface EventMeta {
3
+ /** True when called with the retained last value on subscribe; false for a live emit. */
4
+ readonly isSnapshot: boolean;
5
+ }
6
+ export type EventHandler<P> = (payload: P, meta: EventMeta) => void;
7
+ /** No payload arg for `void` events, a required one otherwise. */
8
+ type EmitArgs<P> = [P] extends [void] ? [] : [payload: P];
9
+ /**
10
+ * A typed pub/sub keyed by an event map (`{ key: payload }`). It snapshots the
11
+ * last payload per key, so a late `listen`/`listenOnce` is replayed that value
12
+ * immediately (with `isSnapshot: true`) before receiving live emits.
13
+ */
14
+ export interface Emitter<Events extends object> {
15
+ /** Emit `key`'s payload to current listeners and retain it as the snapshot. */
16
+ emit<K extends keyof Events>(key: K, ...args: EmitArgs<Events[K]>): void;
17
+ /** Subscribe to `key`. Fires immediately with the snapshot if one exists. Returns an unsubscribe. */
18
+ listen<K extends keyof Events>(key: K, handler: EventHandler<Events[K]>): () => void;
19
+ /** Like {@link Emitter.listen} but auto-unsubscribes after the first delivery (snapshot counts). */
20
+ listenOnce<K extends keyof Events>(key: K, handler: EventHandler<Events[K]>): () => void;
21
+ }
22
+ export declare const events: <Events extends object>(namespace?: string) => Emitter<Events>;
23
+ export {};
@@ -0,0 +1,61 @@
1
+ import { log } from "./log-collector/global.js";
2
+ import { captureSnapshot } from "./output.js";
3
+ export const events = (namespace) => {
4
+ const handlers = new Map();
5
+ const snapshots = new Map();
6
+ const emit = (key, ...args) => {
7
+ const payload = args[0];
8
+ snapshots.set(key, payload);
9
+ // Every emit also lands in the global log, so events share the timeline with
10
+ // process output and end up in the drained log file. Fields are `·`-delimited
11
+ // — namespace (the emitting action) · key · payload. String payloads show
12
+ // inline; richer payloads are no longer dropped — they're written to a
13
+ // snapshot file and linked (`→ <path>`), so the durable record keeps them.
14
+ const fields = [namespace, String(key)];
15
+ if (typeof payload === "string") {
16
+ fields.push(payload);
17
+ }
18
+ else if (payload !== undefined) {
19
+ const name = `event-${[namespace, String(key)].filter(Boolean).join("-")}`;
20
+ fields.push(`→ ${captureSnapshot(name, payload).rel}`);
21
+ }
22
+ log.append({
23
+ source: "event",
24
+ level: "event",
25
+ message: fields.filter(Boolean).join(" · "),
26
+ });
27
+ const set = handlers.get(key);
28
+ if (!set)
29
+ return;
30
+ // Copy so a handler unsubscribing (or listenOnce) mid-emit doesn't disturb iteration.
31
+ for (const handler of [...set])
32
+ handler(payload, { isSnapshot: false });
33
+ };
34
+ const listen = (key, handler) => {
35
+ let set = handlers.get(key);
36
+ if (!set) {
37
+ set = new Set();
38
+ handlers.set(key, set);
39
+ }
40
+ set.add(handler);
41
+ // Snapshot on subscribe, if one has ever been emitted for this key.
42
+ if (snapshots.has(key))
43
+ handler(snapshots.get(key), { isSnapshot: true });
44
+ return () => {
45
+ set.delete(handler);
46
+ };
47
+ };
48
+ const listenOnce = (key, handler) => {
49
+ // A retained snapshot satisfies "once" immediately — deliver and we're done.
50
+ if (snapshots.has(key)) {
51
+ handler(snapshots.get(key), { isSnapshot: true });
52
+ return () => { };
53
+ }
54
+ const off = listen(key, (payload, meta) => {
55
+ off();
56
+ handler(payload, meta);
57
+ });
58
+ return off;
59
+ };
60
+ return { emit, listen, listenOnce };
61
+ };
@@ -0,0 +1,13 @@
1
+ export declare class AsyncQueue<T> implements AsyncIterable<T> {
2
+ private readonly items;
3
+ private readonly resolvers;
4
+ private closed;
5
+ push(item: T): void;
6
+ /**
7
+ * Ends the queue: any parked reader resolves `{ done: true }` immediately, and
8
+ * once already-buffered items are drained, future reads do too. Idempotent.
9
+ */
10
+ close(): void;
11
+ private next;
12
+ [Symbol.asyncIterator](): AsyncIterator<T>;
13
+ }
@@ -0,0 +1,50 @@
1
+ export class AsyncQueue {
2
+ items = [];
3
+ resolvers = [];
4
+ closed = false;
5
+ push(item) {
6
+ if (this.closed)
7
+ return;
8
+ const resolver = this.resolvers.shift();
9
+ if (resolver) {
10
+ resolver({
11
+ done: false,
12
+ value: item,
13
+ });
14
+ return;
15
+ }
16
+ this.items.push(item);
17
+ }
18
+ /**
19
+ * Ends the queue: any parked reader resolves `{ done: true }` immediately, and
20
+ * once already-buffered items are drained, future reads do too. Idempotent.
21
+ */
22
+ close() {
23
+ if (this.closed)
24
+ return;
25
+ this.closed = true;
26
+ // Unblock everyone parked — nothing more is coming.
27
+ for (const resolve of this.resolvers.splice(0))
28
+ resolve({ done: true, value: undefined });
29
+ }
30
+ next() {
31
+ const item = this.items.shift();
32
+ if (item !== undefined) {
33
+ return Promise.resolve({
34
+ done: false,
35
+ value: item,
36
+ });
37
+ }
38
+ // Drained and closed → the iteration is over.
39
+ if (this.closed)
40
+ return Promise.resolve({ done: true, value: undefined });
41
+ return new Promise((resolve) => {
42
+ this.resolvers.push(resolve);
43
+ });
44
+ }
45
+ [Symbol.asyncIterator]() {
46
+ return {
47
+ next: () => this.next(),
48
+ };
49
+ }
50
+ }
@@ -0,0 +1,63 @@
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 subscribers;
42
+ private ended;
43
+ /**
44
+ * Streams a raw text/byte source (e.g. a child process's stdout) into the
45
+ * collector: splits it into lines and appends each under the given
46
+ * `source`/`level`. Partial lines are buffered across chunks and a non-empty
47
+ * trailing fragment is flushed when the stream ends.
48
+ */
49
+ attach(stream: AsyncIterable<unknown>, source: string, level: string): void;
50
+ /** Raw store: records the line verbatim, source included; presentation is the renderer's job. */
51
+ append(entry: LogInput): void;
52
+ /**
53
+ * Bind the run's abort signal: when it aborts (teardown), every subscription
54
+ * ends — each parked or future `for await` returns — so subscribers don't each
55
+ * have to watch the signal. The collector *is* the world's log and the signal
56
+ * is the world ending; the log closes with it.
57
+ */
58
+ endOn(signal: AbortSignal): void;
59
+ /** EOF for readers: end every live subscription and refuse to register new ones. */
60
+ private closeAll;
61
+ subscribe({ replay }?: SubscribeOptions): AsyncIterable<LogEntry>;
62
+ snapshot(): readonly LogEntry[];
63
+ }
@@ -0,0 +1,110 @@
1
+ import { AsyncQueue } from "../helpers/AsyncQueue.js";
2
+ export class LogsCollector {
3
+ sequence = 0;
4
+ history = [];
5
+ subscribers = new Set();
6
+ // Set once the bound signal aborts: the log has reached EOF for readers.
7
+ ended = false;
8
+ /**
9
+ * Streams a raw text/byte source (e.g. a child process's stdout) into the
10
+ * collector: splits it into lines and appends each under the given
11
+ * `source`/`level`. Partial lines are buffered across chunks and a non-empty
12
+ * trailing fragment is flushed when the stream ends.
13
+ */
14
+ attach(stream, source, level) {
15
+ void splitLines(stream, (message) => this.append({ source, level, message }));
16
+ }
17
+ /** Raw store: records the line verbatim, source included; presentation is the renderer's job. */
18
+ append(entry) {
19
+ const log = {
20
+ source: entry.source,
21
+ level: entry.level,
22
+ message: entry.message,
23
+ sequence: ++this.sequence,
24
+ timestamp: Date.now(),
25
+ };
26
+ this.history.push(log);
27
+ for (const subscriber of this.subscribers) {
28
+ // push the log into the queue for every subscriber to receive
29
+ subscriber.push(log);
30
+ }
31
+ }
32
+ /**
33
+ * Bind the run's abort signal: when it aborts (teardown), every subscription
34
+ * ends — each parked or future `for await` returns — so subscribers don't each
35
+ * have to watch the signal. The collector *is* the world's log and the signal
36
+ * is the world ending; the log closes with it.
37
+ */
38
+ endOn(signal) {
39
+ if (signal.aborted)
40
+ this.closeAll();
41
+ else
42
+ signal.addEventListener("abort", () => this.closeAll(), { once: true });
43
+ }
44
+ /** EOF for readers: end every live subscription and refuse to register new ones. */
45
+ closeAll() {
46
+ this.ended = true;
47
+ for (const queue of this.subscribers)
48
+ queue.close();
49
+ this.subscribers.clear();
50
+ }
51
+ subscribe({ replay = false } = {}) {
52
+ // create a log queue for the new subscriber
53
+ const queue = new AsyncQueue();
54
+ // Optionally replay buffered history before live entries. Synchronous, so no
55
+ // append can interleave between the replay and registration — a late
56
+ // subscriber sees every entry, with no gap and no duplicate.
57
+ if (replay)
58
+ for (const entry of this.history)
59
+ queue.push(entry);
60
+ // If the run has already ended, this subscription is EOF after the replay —
61
+ // don't register it for entries that will never come. Otherwise, fan it in.
62
+ if (this.ended)
63
+ queue.close();
64
+ else
65
+ this.subscribers.add(queue);
66
+ const { subscribers } = this;
67
+ // logs queue for the current subscriber
68
+ const inner = queue[Symbol.asyncIterator]();
69
+ return {
70
+ [Symbol.asyncIterator]() {
71
+ return {
72
+ // iterates the subscriber's log queue
73
+ next: () => inner.next(),
74
+ // Unsubscribe on early exit (break/return), so a consumer that stops
75
+ // reading (e.g. waitForLog on a match) doesn't leave a dead queue that
76
+ // append keeps pushing into forever.
77
+ return: () => {
78
+ subscribers.delete(queue);
79
+ queue.close();
80
+ return Promise.resolve({ done: true, value: undefined });
81
+ },
82
+ };
83
+ },
84
+ };
85
+ }
86
+ snapshot() {
87
+ return this.history;
88
+ }
89
+ }
90
+ /**
91
+ * Splits a byte/string stream into lines, calling `onLine` for each. Partial
92
+ * lines are buffered across chunks and a non-empty trailing fragment is flushed
93
+ * when the stream ends.
94
+ */
95
+ async function splitLines(stream, onLine) {
96
+ let buffer = "";
97
+ for await (const chunk of stream) {
98
+ buffer += String(chunk);
99
+ let newline;
100
+ while ((newline = buffer.indexOf("\n")) !== -1) {
101
+ const line = buffer.slice(0, newline).replace(/\r$/, "");
102
+ buffer = buffer.slice(newline + 1);
103
+ if (line)
104
+ onLine(line);
105
+ }
106
+ }
107
+ const tail = buffer.replace(/\r$/, "");
108
+ if (tail)
109
+ onLine(tail);
110
+ }
@@ -0,0 +1,7 @@
1
+ import { LogsCollector } from "./LogsCollector.ts";
2
+ /**
3
+ * The process-wide log. One per process: pipelines log into it and drain it, and
4
+ * the event bus pushes an entry here on every `emit`. Created separately from any
5
+ * pipeline so code that runs before/around a pipeline (like a bus) can reach it.
6
+ */
7
+ export declare const log: LogsCollector;
@@ -0,0 +1,7 @@
1
+ import { LogsCollector } from "./LogsCollector.js";
2
+ /**
3
+ * The process-wide log. One per process: pipelines log into it and drain it, and
4
+ * the event bus pushes an entry here on every `emit`. Created separately from any
5
+ * pipeline so code that runs before/around a pipeline (like a bus) can reach it.
6
+ */
7
+ export const log = new LogsCollector();
@@ -0,0 +1,57 @@
1
+ import type { LogEntry } from "./LogsCollector.ts";
2
+ /**
3
+ * Builds a stateful renderer that turns raw {@link LogEntry} records into the
4
+ * pretty, grouped text form. Presentation lives here and nowhere else: the
5
+ * collector stores raw entries; `writeLog` and `drain` each create a renderer and
6
+ * feed it the entry stream in order.
7
+ *
8
+ * Grouping is decided against the *single merged stream* (the renderer's own
9
+ * `lastSource`), not per-producer — so interleaved concurrent sources re-header
10
+ * correctly, which the old per-child grouping got wrong. A milestone line breaks
11
+ * the current group, so the next action line re-headers.
12
+ */
13
+ export declare const createRenderer: () => ((entry: LogEntry) => string);
14
+ /** Lines of a collapsed run shown inline before the pointer. */
15
+ export declare const COLLAPSE_HEAD = 5;
16
+ /** Only runs *longer than* this collapse, so a trigger always hides a meaningful amount. */
17
+ export declare const COLLAPSE_THRESHOLD = 15;
18
+ /** A run set aside for collapsing: its source and every output line, in order. */
19
+ export interface CollapsedRun {
20
+ readonly source: string;
21
+ readonly lines: readonly string[];
22
+ }
23
+ /**
24
+ * One unit of batch-rendered output: either a ready-to-write text `line`
25
+ * (identical to {@link createRenderer}'s output for that entry) or a
26
+ * `collapsed` run the caller must off-load and render via {@link renderCollapsed}.
27
+ */
28
+ export type DisplayItem = {
29
+ kind: "line";
30
+ text: string;
31
+ } | {
32
+ kind: "collapsed";
33
+ run: CollapsedRun;
34
+ };
35
+ /** Tunes {@link segmentForFile}. Defaults to {@link COLLAPSE_HEAD}/{@link COLLAPSE_THRESHOLD}. */
36
+ export interface SegmentOptions {
37
+ head?: number;
38
+ threshold?: number;
39
+ }
40
+ /**
41
+ * Segments a full, ordered entry list into {@link DisplayItem}s. Milestones and
42
+ * runs no longer than `threshold` become `line` items byte-identical to
43
+ * {@link createRenderer}; a run longer than `threshold` becomes a single
44
+ * `collapsed` item carrying every line of the run. Pure — no filesystem, no
45
+ * snapshot writing.
46
+ */
47
+ export declare const segmentForFile: (entries: readonly LogEntry[], { threshold }?: SegmentOptions) => DisplayItem[];
48
+ /**
49
+ * Renders a {@link CollapsedRun} as its `head` lines under the `▸ <source>`
50
+ * header, then a single indented `📎` pointer line to the off-loaded snapshot at
51
+ * `rel` reporting how many lines were hidden. Pure — `rel` is supplied by the
52
+ * caller after it writes the file.
53
+ */
54
+ export declare const renderCollapsed: (run: CollapsedRun, { head, rel }: {
55
+ head: number;
56
+ rel: string;
57
+ }) => string;