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.
Files changed (72) hide show
  1. package/dist/actions/chrome-page.d.ts +1 -1
  2. package/dist/actions/chrome-page.js +1 -1
  3. package/dist/actions/command.d.ts +1 -1
  4. package/dist/actions/command.js +5 -6
  5. package/dist/actions/healthcheck.d.ts +1 -1
  6. package/dist/actions/healthcheck.js +1 -1
  7. package/dist/actions/prompt.d.ts +1 -1
  8. package/dist/actions/prompt.js +1 -1
  9. package/dist/actions/tail-log.d.ts +1 -1
  10. package/dist/actions/tail-log.js +1 -1
  11. package/dist/actions/until-log.d.ts +2 -2
  12. package/dist/actions/until-log.js +1 -1
  13. package/dist/actions/watch-dir.d.ts +1 -1
  14. package/dist/actions/watch-dir.js +1 -1
  15. package/dist/actions/watch.d.ts +1 -1
  16. package/dist/actions/watch.js +1 -1
  17. package/dist/index.d.ts +13 -17
  18. package/dist/index.js +15 -17
  19. package/dist/orchestration/action/action.d.ts +23 -0
  20. package/dist/orchestration/action/action.js +254 -0
  21. package/dist/{core/action.d.ts → orchestration/action/types.d.ts} +31 -27
  22. package/dist/orchestration/action/types.js +1 -0
  23. package/dist/{core → orchestration/events}/events.d.ts +9 -1
  24. package/dist/{core → orchestration/events}/events.js +14 -22
  25. package/dist/orchestration/spin/SpinHost.d.ts +105 -0
  26. package/dist/orchestration/spin/SpinHost.js +303 -0
  27. package/dist/orchestration/spin/spin.d.ts +24 -0
  28. package/dist/orchestration/spin/spin.js +56 -0
  29. package/dist/orchestration/spin/types.d.ts +56 -0
  30. package/dist/orchestration/spin/types.js +1 -0
  31. package/dist/output/Output.d.ts +24 -0
  32. package/dist/output/Output.js +27 -0
  33. package/dist/output/folder/RunFolder.d.ts +29 -0
  34. package/dist/output/folder/RunFolder.js +56 -0
  35. package/dist/output/index.d.ts +11 -0
  36. package/dist/output/index.js +11 -0
  37. package/dist/output/log/LogRenderer.d.ts +73 -0
  38. package/dist/output/log/LogRenderer.js +131 -0
  39. package/dist/{core/log-collector → output/log}/LogsCollector.d.ts +14 -0
  40. package/dist/{core/log-collector → output/log}/LogsCollector.js +24 -1
  41. package/dist/output/log/RunLog.d.ts +44 -0
  42. package/dist/output/log/RunLog.js +119 -0
  43. package/dist/output/snapshot/SnapshotStore.d.ts +49 -0
  44. package/dist/output/snapshot/SnapshotStore.js +61 -0
  45. package/dist/system/fs/FolderCache.d.ts +47 -0
  46. package/dist/system/fs/FolderCache.js +117 -0
  47. package/dist/system/fs/streams.d.ts +11 -0
  48. package/dist/system/fs/streams.js +14 -0
  49. package/dist/{core → system/process}/process.d.ts +9 -8
  50. package/dist/{core → system/process}/process.js +8 -9
  51. package/dist/utils/format.d.ts +9 -0
  52. package/dist/utils/format.js +9 -0
  53. package/dist/utils/serialize.d.ts +2 -0
  54. package/dist/utils/serialize.js +12 -0
  55. package/package.json +1 -1
  56. package/dist/core/action.js +0 -201
  57. package/dist/core/folder-cache.d.ts +0 -39
  58. package/dist/core/folder-cache.js +0 -109
  59. package/dist/core/log-collector/global.d.ts +0 -7
  60. package/dist/core/log-collector/global.js +0 -7
  61. package/dist/core/log-collector/render.d.ts +0 -68
  62. package/dist/core/log-collector/render.js +0 -132
  63. package/dist/core/markers.d.ts +0 -9
  64. package/dist/core/markers.js +0 -13
  65. package/dist/core/output.d.ts +0 -68
  66. package/dist/core/output.js +0 -213
  67. package/dist/core/run.d.ts +0 -88
  68. package/dist/core/run.js +0 -257
  69. package/dist/core/spin.d.ts +0 -72
  70. package/dist/core/spin.js +0 -45
  71. /package/dist/{core/helpers → utils}/AsyncQueue.d.ts +0 -0
  72. /package/dist/{core/helpers → utils}/AsyncQueue.js +0 -0
@@ -41,4 +41,4 @@ export interface ChromePageOptions {
41
41
  * teardown it closes the CDP connection *only* when it opened one — a handed-in
42
42
  * browser or window is left untouched for its owner to close.
43
43
  */
44
- export declare const chromePage: import("../core/action.ts").Action<[source: ChromePageSource | Promise<ChromePageSource>, opts?: ChromePageOptions | undefined], void, ChromePageEvents, Page>;
44
+ export declare const chromePage: import("../index.ts").Action<[source: ChromePageSource | Promise<ChromePageSource>, opts?: ChromePageOptions | undefined], void, ChromePageEvents, Page>;
@@ -1,5 +1,5 @@
1
1
  import { chromium } from "playwright-core";
2
- import { action } from "../core/action.js";
2
+ import { action } from "../orchestration/action/action.js";
3
3
  // Playwright's classes are structurally distinct by these methods; only Browser
4
4
  // has `contexts()`, only Page has `mainFrame()`, and a BrowserContext has
5
5
  // `newPage()` without either — enough to tell a handed-in handle apart.
@@ -1,4 +1,4 @@
1
- import { type AnyActionInstance } from "../core/action.ts";
1
+ import type { AnyActionInstance } from "../orchestration/action/types.ts";
2
2
  /** Tunes an action built with {@link command}. */
3
3
  export interface CommandOptions {
4
4
  /**
@@ -1,8 +1,7 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import path from "node:path";
3
- import { action } from "../core/action.js";
4
- import { killTree } from "../core/process.js";
5
- import { captureSnapshot } from "../core/output.js";
3
+ import { action } from "../orchestration/action/action.js";
4
+ import { killTree } from "../system/process/process.js";
6
5
  /**
7
6
  * Build the literal part of a zx tagged template from a command string, so it
8
7
  * runs verbatim. zx only quotes interpolated args, not the template's `pieces` —
@@ -15,7 +14,7 @@ export function command(name, cmdOrFile, argsOrOptions, maybeOptions) {
15
14
  const direct = Array.isArray(argsOrOptions);
16
15
  const options = (direct ? maybeOptions : argsOrOptions) ?? {};
17
16
  if (direct) {
18
- return action(name).run((ctx) => spawnDirect(ctx.logs, ctx.signal, name, cmdOrFile, argsOrOptions, options))();
17
+ return action(name).run((ctx) => spawnDirect(ctx.output.snapshots, ctx.logs, ctx.signal, name, cmdOrFile, argsOrOptions, options))();
19
18
  }
20
19
  return action(name).run(({ proc }) => {
21
20
  // path.resolve keeps an absolute cwd as-is and resolves a relative one
@@ -35,7 +34,7 @@ export function command(name, cmdOrFile, argsOrOptions, maybeOptions) {
35
34
  * log, and resolves when it exits (0 = ok; a non-zero exit is a genuine failure,
36
35
  * unless the run is tearing down, where the abort-kill is the reason).
37
36
  */
38
- const spawnDirect = (logs, signal, name, file, args, options) => {
37
+ const spawnDirect = (snapshots, logs, signal, name, file, args, options) => {
39
38
  const cwd = path.resolve(options.cwd ?? ".");
40
39
  const source = options.label ?? name;
41
40
  // windowsHide keeps a stray console window from flashing for the child.
@@ -43,7 +42,7 @@ const spawnDirect = (logs, signal, name, file, args, options) => {
43
42
  const child = spawn(file, args, { cwd, windowsHide: true, env: options.env });
44
43
  // Mirror proc's launch line: a filterable milestone on the timeline, with the
45
44
  // structured args snapshotted and linked (see process.ts for the rationale).
46
- const snap = captureSnapshot(`proc-${name}-start`, { cmd: file, args, cwd });
45
+ const snap = snapshots.captureJson(`proc-${name}-start`, { cmd: file, args, cwd });
47
46
  logs.append({
48
47
  source: name,
49
48
  level: "event",
@@ -52,4 +52,4 @@ export interface HealthcheckEvents {
52
52
  * stall the loop) and the whole wait by the run's abort signal and an optional
53
53
  * `timeoutMs`, so teardown unblocks a pending probe instead of hanging.
54
54
  */
55
- export declare const healthcheck: import("../core/action.ts").Action<[opts?: HealthcheckOptions | undefined], HealthcheckResult, HealthcheckEvents, void>;
55
+ export declare const healthcheck: import("../index.ts").Action<[opts?: HealthcheckOptions | undefined], HealthcheckResult, HealthcheckEvents, void>;
@@ -1,4 +1,4 @@
1
- import { action } from "../core/action.js";
1
+ import { action } from "../orchestration/action/action.js";
2
2
  /**
3
3
  * Gate: polls an HTTP endpoint until it answers in a healthy way (status and,
4
4
  * optionally, body), then resolves with that probe and emits `healthy`. The
@@ -50,5 +50,5 @@ export interface PromptEvents {
50
50
  * `answer` when settled (carrying whether it came from input, the default, or a
51
51
  * timeout).
52
52
  */
53
- export declare const prompt: import("../core/action.ts").Action<[opts?: PromptOptions | undefined], string, PromptEvents, string>;
53
+ export declare const prompt: import("../index.ts").Action<[opts?: PromptOptions | undefined], string, PromptEvents, string>;
54
54
  export {};
@@ -1,5 +1,5 @@
1
1
  import { createInterface } from "node:readline/promises";
2
- import { action } from "../core/action.js";
2
+ import { action } from "../orchestration/action/action.js";
3
3
  /** Normalize choices to `{ label, value }`. */
4
4
  const asChoices = (choices) => choices.map((c) => (typeof c === "string" ? { label: c, value: c } : c));
5
5
  /**
@@ -27,4 +27,4 @@ export interface TailLogOptions {
27
27
  * filters by name, like {@link import("./watch.ts").watch}. Runs until the run's
28
28
  * signal aborts, then closes the watcher (flushing any trailing partial line).
29
29
  */
30
- export declare const tailLog: import("../core/action.ts").Action<[target: string, opts?: TailLogOptions | undefined], void, import("../core/action.ts").NoEvents, void>;
30
+ export declare const tailLog: import("../index.ts").Action<[target: string, opts?: TailLogOptions | undefined], void, import("../index.ts").NoEvents, void>;
@@ -2,7 +2,7 @@ import { watch as fsWatch } from "node:fs";
2
2
  import { open, stat } from "node:fs/promises";
3
3
  import { basename, dirname, resolve } from "node:path";
4
4
  import { StringDecoder } from "node:string_decoder";
5
- import { action } from "../core/action.js";
5
+ import { action } from "../orchestration/action/action.js";
6
6
  /**
7
7
  * Tails a (possibly not-yet-existing) text/log file and streams each appended
8
8
  * line into the run's combined log — the way `proc` streams a child's stdout, but
@@ -1,4 +1,4 @@
1
- import type { LogEntry } from "../core/log-collector/LogsCollector.ts";
1
+ import type { LogEntry } from "../output/log/LogsCollector.ts";
2
2
  /** Predicate over a log entry — return `true` for the entry to wait for. */
3
3
  export type LogMatcher = (entry: LogEntry) => boolean;
4
4
  export interface UntilLogOptions {
@@ -21,4 +21,4 @@ export interface UntilLogEvents {
21
21
  * run's abort signal (and an optional timeout), so teardown unblocks a pending
22
22
  * wait instead of hanging.
23
23
  */
24
- export declare const untilLog: import("../core/action.ts").Action<[matcher: LogMatcher, (UntilLogOptions | undefined)?], LogEntry, UntilLogEvents, void>;
24
+ export declare const untilLog: import("../index.ts").Action<[matcher: LogMatcher, (UntilLogOptions | undefined)?], LogEntry, UntilLogEvents, void>;
@@ -1,4 +1,4 @@
1
- import { action } from "../core/action.js";
1
+ import { action } from "../orchestration/action/action.js";
2
2
  /**
3
3
  * Gate: resolves with the first log entry the `matcher` accepts (and emits
4
4
  * `match`). Subscribes to the injected collector and races each read against the
@@ -32,4 +32,4 @@ export interface WatchDirEvents {
32
32
  * event the *parent* fires for the target's name — when it does and the target
33
33
  * still exists, it was replaced (`removed` + `ready`).
34
34
  */
35
- export declare const watchDir: import("../core/action.ts").Action<[target: string, opts?: WatchDirOptions | undefined], void, WatchDirEvents, void>;
35
+ export declare const watchDir: import("../index.ts").Action<[target: string, opts?: WatchDirOptions | undefined], void, WatchDirEvents, void>;
@@ -1,7 +1,7 @@
1
1
  import { existsSync, watch as fsWatch } from "node:fs";
2
2
  import { readdir, stat } from "node:fs/promises";
3
3
  import { basename, dirname, join, resolve } from "node:path";
4
- import { action } from "../core/action.js";
4
+ import { action } from "../orchestration/action/action.js";
5
5
  /**
6
6
  * Watches a directory and emits per-file events on changes, surviving the folder
7
7
  * being deleted and recreated (e.g. a build that `rm -rf`s it first).
@@ -30,4 +30,4 @@ export interface WatchOptions {
30
30
  * Assumes the parent directory exists; watching when an ancestor is missing is
31
31
  * a later case.
32
32
  */
33
- export declare const watch: import("../core/action.ts").Action<[target: string, opts?: WatchOptions | undefined], void, import("../core/action.ts").NoEvents, void>;
33
+ export declare const watch: import("../index.ts").Action<[target: string, opts?: WatchOptions | undefined], void, import("../index.ts").NoEvents, void>;
@@ -1,7 +1,7 @@
1
1
  import { watch as fsWatch } from "node:fs";
2
2
  import { stat } from "node:fs/promises";
3
3
  import { basename, dirname, resolve } from "node:path";
4
- import { action } from "../core/action.js";
4
+ import { action } from "../orchestration/action/action.js";
5
5
  /**
6
6
  * Watches a single file that may not exist yet, firing callbacks on its
7
7
  * transitions. Since `fs.watch` can't watch a missing path, it watches the
package/dist/index.d.ts CHANGED
@@ -8,20 +8,16 @@
8
8
  * This is a curated public surface — internal helpers (the async queue, the log
9
9
  * renderer, marker prefixing) are intentionally not exported.
10
10
  */
11
- export { action } from "./core/action.ts";
12
- export type { Action, ActionBuilder, ActionInstance, AnyActionInstance, ActionContext, SystemGlobal, SystemEvents, InstanceEvents, NoEvents, Awaitable, Outcome, } from "./core/action.ts";
13
- export { run } from "./core/run.ts";
14
- export type { Spin, SpinResult, SpinState, ActionFailure } from "./core/run.ts";
15
- export { spin } from "./core/spin.ts";
16
- export type { Nod, SpinOptions, SpinContext, SpinBody } from "./core/spin.ts";
17
- export { events } from "./core/events.ts";
18
- export type { Emitter, EventHandler, EventMeta } from "./core/events.ts";
19
- export { createProc } from "./core/process.ts";
20
- export type { Proc } from "./core/process.ts";
21
- export { LogsCollector } from "./core/log-collector/LogsCollector.ts";
22
- export type { Logger, LogEntry, LogInput, SubscribeOptions, } from "./core/log-collector/LogsCollector.ts";
23
- export { log } from "./core/log-collector/global.ts";
24
- export { snapshot, captureSnapshot } from "./core/output.ts";
25
- export type { SnapshotRef } from "./core/output.ts";
26
- export { artifactsFolder } from "./core/output.ts";
27
- export { fingerprint, cacheDir } from "./core/folder-cache.ts";
11
+ export { action } from "./orchestration/action/action.ts";
12
+ export type { Action, ActionBuilder, ActionInstance, AnyActionInstance, ActionContext, SystemGlobal, SystemEvents, InstanceEvents, NoEvents, Awaitable, Outcome, } from "./orchestration/action/types.ts";
13
+ export { spin } from "./orchestration/spin/spin.ts";
14
+ export type { Nod, SpinContext, SpinBody } from "./orchestration/spin/types.ts";
15
+ export { events } from "./orchestration/events/events.ts";
16
+ export type { Emitter, EventHandler, EventMeta } from "./orchestration/events/events.ts";
17
+ export { createProc } from "./system/process/process.ts";
18
+ export type { Proc } from "./system/process/process.ts";
19
+ export { LogsCollector } from "./output/log/LogsCollector.ts";
20
+ export type { Logger, LogEntry, LogInput, SubscribeOptions, } from "./output/log/LogsCollector.ts";
21
+ export { Output } from "./output/index.ts";
22
+ export type { SnapshotRef } from "./output/index.ts";
23
+ export { FolderCache } from "./system/fs/FolderCache.ts";
package/dist/index.js CHANGED
@@ -9,26 +9,24 @@
9
9
  * renderer, marker prefixing) are intentionally not exported.
10
10
  */
11
11
  // ── Actions ────────────────────────────────────────────────────────────────
12
- export { action } from "./core/action.js";
13
- // ── Spin (the run handle) ────────────────────────────────────────────────────
14
- export { run } from "./core/run.js";
12
+ export { action } from "./orchestration/action/action.js";
15
13
  // ── Spin (linear orchestration) ──────────────────────────────────────────────
16
- export { spin } from "./core/spin.js";
14
+ export { spin } from "./orchestration/spin/spin.js";
17
15
  // ── Events ─────────────────────────────────────────────────────────────────
18
- export { events } from "./core/events.js";
16
+ export { events } from "./orchestration/events/events.js";
19
17
  // ── Child processes ────────────────────────────────────────────────────────
20
- export { createProc } from "./core/process.js";
18
+ export { createProc } from "./system/process/process.js";
21
19
  // ── The log ────────────────────────────────────────────────────────────────
22
- export { LogsCollector } from "./core/log-collector/LogsCollector.js";
23
- export { log } from "./core/log-collector/global.js";
24
- // ── Snapshots ──────────────────────────────────────────────────────────────
25
- export { snapshot, captureSnapshot } from "./core/output.js";
26
- // ── Artifacts ────────────────────────────────────────────────────────────────
27
- // This run's output folder (absolute) — also on the `spin`/action `ctx`. Import
28
- // it directly for artifact-writing helpers that aren't inside an action.
29
- export { artifactsFolder } from "./core/output.js";
20
+ export { LogsCollector } from "./output/log/LogsCollector.js";
21
+ // ── Snapshots & artifacts ────────────────────────────────────────────────────
22
+ // The output subsystem is owned per-run by the spin and reached through context —
23
+ // `ctx.output` (folder / snapshots / runLog) inside an action, `ctx.snapshot` and
24
+ // `ctx.artifactsFolder` inside a spin body. The classes are exported for typing and
25
+ // advanced use; there is no process-global singleton or free-function form.
26
+ export { Output } from "./output/index.js";
30
27
  // ── Folder cache ─────────────────────────────────────────────────────────────
31
28
  // Backs the `.withCache(...inputs)` method on instances (fingerprints an
32
- // action's input files/folders, not its outputs); exported for scripts that
33
- // want to inspect or manually invalidate the cache.
34
- export { fingerprint, cacheDir } from "./core/folder-cache.js";
29
+ // action's input files/folders, not its outputs); exposed for scripts that want
30
+ // to inspect or manually invalidate the cache — `FolderCache.fingerprint(...)`,
31
+ // `FolderCache.dir`.
32
+ export { FolderCache } from "./system/fs/FolderCache.js";
@@ -0,0 +1,23 @@
1
+ import type { ActionBuilder, AnyActionInstance, NoEvents } from "./types.ts";
2
+ /**
3
+ * If `value` is some action instance's `.ref` promise, the instance that owns it
4
+ * (the producer of that handle); otherwise `undefined`. Used by the run to verify
5
+ * a `.ref` dependency was launched before the action that consumes it.
6
+ */
7
+ export declare const producerOfRef: (value: unknown) => AnyActionInstance | undefined;
8
+ /**
9
+ * Defines an action. Always returns a builder; provide the implementation via
10
+ * `.run(…)`, optionally after declaring events and/or a handle:
11
+ *
12
+ * action("Build").run(async (ctx, opts) => { … }) // no declared events / handle
13
+ * action("Watch") // typed events + handle
14
+ * .emits<{ create: string }>()
15
+ * .ref<{ rescan(): void }>()
16
+ * .run((ctx, opts) => { ctx.emit(…); ctx.attach(…); … })
17
+ *
18
+ * `.run(…)` yields a callable that, when invoked, constructs an
19
+ * {@link ActionInstance} (deferred — the run executes it). Instances expose
20
+ * chainable `.on` / `.once` (declared events plus `done`/`error`); the attached
21
+ * handle is awaited via `instance.ref`.
22
+ */
23
+ export declare function action(name: string): ActionBuilder<NoEvents, void>;
@@ -0,0 +1,254 @@
1
+ import { events } from "../events/events.js";
2
+ import { createProc } from "../../system/process/process.js";
3
+ import { FolderCache } from "../../system/fs/FolderCache.js";
4
+ // Maps an instance's `.ref` promise back to the instance that produces it, so the
5
+ // run can catch a consumer launched before (or without) its `.ref` producer —
6
+ // which would otherwise await a handle that never arrives and hang silently.
7
+ const refOwners = new WeakMap();
8
+ /**
9
+ * If `value` is some action instance's `.ref` promise, the instance that owns it
10
+ * (the producer of that handle); otherwise `undefined`. Used by the run to verify
11
+ * a `.ref` dependency was launched before the action that consumes it.
12
+ */
13
+ export const producerOfRef = (value) => typeof value === "object" && value !== null ? refOwners.get(value) : undefined;
14
+ /**
15
+ * A constructed-but-not-yet-run action — everything a single launch needs, in one
16
+ * object: its own event bus, its set-once handle (`ref`) and Outcome (`done`), and
17
+ * the `start` that runs the impl and settles them. Self-contained: `start()`
18
+ * resolves with the {@link Outcome} and **never rejects** (a failing action is
19
+ * data, not a throw), so a launcher just awaits `.done`.
20
+ */
21
+ class ActionRun {
22
+ name;
23
+ args;
24
+ /** The attached handle, awaited via `.ref` (settled by `attach`, or by `start` on settle). */
25
+ ref;
26
+ /** The action's Outcome — resolves (never rejects) once it settles. */
27
+ done;
28
+ exec;
29
+ // The bus covers declared + system events; `ctx.emit` is narrowed to the declared
30
+ // ones, while the framework emits attached/done/error through the same bus.
31
+ emitter;
32
+ handle;
33
+ settled;
34
+ attached = false;
35
+ constructor(name, exec, args) {
36
+ this.name = name;
37
+ this.exec = exec;
38
+ this.args = args;
39
+ this.emitter = events(name);
40
+ this.handle = ActionRun.defer();
41
+ this.settled = ActionRun.defer();
42
+ this.ref = this.handle.promise;
43
+ this.done = this.settled.promise;
44
+ // Most actions have a `void` handle and no one awaits `ref`; mark the promise
45
+ // handled so a settle-time rejection on such an instance doesn't surface as an
46
+ // unhandled rejection. Real awaiters of `ref` still receive the rejection.
47
+ void this.handle.promise.catch(() => { });
48
+ // Register this instance as the producer of its `.ref`, so the run can catch a
49
+ // consumer built with this `.ref` that gets launched before this instance.
50
+ refOwners.set(this.ref, this);
51
+ }
52
+ start(system) {
53
+ // `ctx.emit` is the declared-events view of the same bus.
54
+ const emit = this.emitter.emit;
55
+ // The bus is pure pub/sub; logging each emit onto the timeline is layered here,
56
+ // through this action's *scoped* logger — so an emit's line inherits the action's
57
+ // path (`parent › child › event`) just like its other output, and the bus stays
58
+ // free of any global. Registered once per run of this instance.
59
+ this.emitter.onAny((key, payload) => ActionRun.logEmit(system, this.name, String(key), payload));
60
+ // `Promise.resolve().then` normalizes a sync result and routes a sync throw to
61
+ // the `error` path, so exec may be sync or async.
62
+ return Promise.resolve()
63
+ .then(() => this.exec({
64
+ ...system,
65
+ emit,
66
+ attach: (value) => this.attach(value),
67
+ proc: createProc(system.logs, system.signal, system.output.snapshots),
68
+ artifactsFolder: system.output.folder.artifacts(),
69
+ }, ...this.args))
70
+ .then((result) => {
71
+ const outcome = { ok: true, value: result };
72
+ this.settled.resolve(outcome);
73
+ this.systemEmit("done", outcome);
74
+ // Settle `ref` if the action finished without attaching, so awaiters
75
+ // resolve (with `undefined` for the default void handle) rather than
76
+ // hang. Idempotent — a no-op when a handle was already attached.
77
+ this.handle.resolve(undefined);
78
+ return outcome;
79
+ }, (error) => {
80
+ const exitCode = ActionRun.exitCodeOf(error);
81
+ const outcome = exitCode === undefined ? { ok: false, error } : { ok: false, error, exitCode };
82
+ // Emit `error` (for `.on("error")` handlers), then settle `done` with the
83
+ // Outcome — the canonical failure signal that `.done`/`once` read.
84
+ this.systemEmit("error", error);
85
+ this.settled.resolve(outcome);
86
+ this.systemEmit("done", outcome);
87
+ // The action failed, so its handle will never arrive — reject `ref` with
88
+ // the same error instead of leaving awaiters hung. No-op if already attached.
89
+ this.handle.reject(error);
90
+ // Resolve (never rethrow): failure is the Outcome, not a rejection.
91
+ return outcome;
92
+ });
93
+ }
94
+ on(key, handler) {
95
+ this.emitter.listen(key, handler);
96
+ }
97
+ once(key) {
98
+ // `done` always fires with the Outcome and never "fails" the await, so it reads
99
+ // straight from the settled promise; other keys race via awaitEvent (which
100
+ // resolves `undefined` if the action settles before the event).
101
+ return (key === "done" ? this.settled.promise : ActionRun.awaitEvent(this.emitter, key));
102
+ }
103
+ withCache(...paths) {
104
+ // Validate + canonicalize eagerly here (at the call site), not lazily inside the
105
+ // run — a relative path fails fast where it's written, and the resolved paths
106
+ // give a stable cache key regardless of spelling.
107
+ const targets = FolderCache.resolvePaths(paths);
108
+ const { name } = this;
109
+ // Wrap this instance in a fresh one (reusing the engine for its own
110
+ // emitter/ref/on/once) that fingerprints `paths` and either skips or
111
+ // runs+records. The wrapper keeps this action's `name`, so log lines and the
112
+ // `launch <name>` line are unchanged; it forwards its injected logs/signal
113
+ // straight into this inner instance's `start`.
114
+ return action(name).run(async (ctx) => {
115
+ const fp = await FolderCache.fingerprint(targets);
116
+ if ((await FolderCache.read(targets)) === fp) {
117
+ ctx.logs.append({ source: name, level: "info", message: "cached, skipping" });
118
+ return undefined;
119
+ }
120
+ const outcome = await this.start({
121
+ logs: ctx.logs,
122
+ signal: ctx.signal,
123
+ nod: ctx.nod,
124
+ output: ctx.output,
125
+ });
126
+ // Re-throw the inner failure so the wrapper fails too (the framework re-wraps
127
+ // it into this wrapper's own Outcome); only record on success.
128
+ if (!outcome.ok)
129
+ throw outcome.error;
130
+ await FolderCache.write(targets, fp);
131
+ return outcome.value;
132
+ })();
133
+ }
134
+ /** One-time lifecycle moment: resolve `ref` and emit `attached`. Guarded (second attach no-ops). */
135
+ attach(value) {
136
+ if (this.attached)
137
+ return;
138
+ this.attached = true;
139
+ this.handle.resolve(value);
140
+ this.systemEmit("attached");
141
+ }
142
+ /** The framework's own emits (loosely typed — sidesteps EmitArgs for void results). */
143
+ systemEmit(key, payload) {
144
+ this.emitter.emit(key, payload);
145
+ }
146
+ static defer() {
147
+ let resolve;
148
+ let reject;
149
+ const promise = new Promise((res, rej) => {
150
+ resolve = res;
151
+ reject = rej;
152
+ });
153
+ return { promise, resolve, reject };
154
+ }
155
+ /** Duck-type an exit code off a thrown error (zx's ProcessOutput, command's exit error). */
156
+ static exitCodeOf(error) {
157
+ const code = error?.exitCode;
158
+ return typeof code === "number" ? code : undefined;
159
+ }
160
+ /**
161
+ * Log one emit onto the run timeline, through the action's scoped `logs` so the
162
+ * line carries its path. Fields are `·`-delimited — action name · key · payload.
163
+ * A string payload rides inline; a richer one is written to a snapshot file and
164
+ * linked (`→ <rel>`) so the durable record keeps it without bloating the line.
165
+ * This is the sole place emits become log lines — the bus itself does no logging.
166
+ */
167
+ static logEmit(system, name, key, payload) {
168
+ const fields = [name, key];
169
+ if (typeof payload === "string") {
170
+ fields.push(payload);
171
+ }
172
+ else if (payload !== undefined) {
173
+ fields.push(`→ ${system.output.snapshots.captureJson(`event-${name}-${key}`, payload).rel}`);
174
+ }
175
+ system.logs.append({ source: "event", level: "event", message: fields.join(" · ") });
176
+ }
177
+ /**
178
+ * A promise for the next emit of `key`. It **never rejects**: it resolves with
179
+ * the event's payload, or with `undefined` if the action settles (`done` — which
180
+ * always fires) before `key` ever does. A retained snapshot resolves it
181
+ * immediately. (Not used for `key === "done"`, which reads the settled Outcome.)
182
+ */
183
+ static awaitEvent(emitter, key) {
184
+ return new Promise((resolve) => {
185
+ const offs = [];
186
+ let settled = false;
187
+ // First of [the event | done] to fire wins; the rest are unsubscribed.
188
+ const settle = (act) => {
189
+ if (settled)
190
+ return;
191
+ settled = true;
192
+ for (const off of offs)
193
+ off();
194
+ act();
195
+ };
196
+ // listenOnce is keyed by E; the system `done` key is always present on an
197
+ // instance's event map, so reach it through a loosened view.
198
+ const listen = emitter.listenOnce;
199
+ offs.push(listen(key, (payload) => settle(() => resolve(payload))));
200
+ // `done` fires once on any settle (success or failure); if it beats `key`,
201
+ // the event will never come — resolve `undefined` rather than reject.
202
+ if (key !== "done") {
203
+ offs.push(listen("done", () => settle(() => resolve(undefined))));
204
+ }
205
+ // A retained snapshot fires a listen synchronously during registration; if
206
+ // that already settled us, drop any listeners registered afterwards.
207
+ if (settled)
208
+ for (const off of offs)
209
+ off();
210
+ });
211
+ }
212
+ }
213
+ /**
214
+ * The step between `action(name)` and an {@link Action}: declare typed events
215
+ * and/or an imperative handle (phantom type transitions — each returns a builder
216
+ * carrying the same `name`), then `run(exec)` to bind the implementation.
217
+ */
218
+ class Builder {
219
+ name;
220
+ constructor(name) {
221
+ this.name = name;
222
+ }
223
+ emits() {
224
+ return new Builder(this.name);
225
+ }
226
+ ref() {
227
+ return new Builder(this.name);
228
+ }
229
+ run(exec) {
230
+ const { name } = this;
231
+ // Invoking the returned callable constructs a fresh instance (deferred — the run
232
+ // executes it). Each call is its own {@link ActionRun}.
233
+ const create = (...args) => new ActionRun(name, exec, args);
234
+ return Object.assign(create, { actionName: name });
235
+ }
236
+ }
237
+ /**
238
+ * Defines an action. Always returns a builder; provide the implementation via
239
+ * `.run(…)`, optionally after declaring events and/or a handle:
240
+ *
241
+ * action("Build").run(async (ctx, opts) => { … }) // no declared events / handle
242
+ * action("Watch") // typed events + handle
243
+ * .emits<{ create: string }>()
244
+ * .ref<{ rescan(): void }>()
245
+ * .run((ctx, opts) => { ctx.emit(…); ctx.attach(…); … })
246
+ *
247
+ * `.run(…)` yields a callable that, when invoked, constructs an
248
+ * {@link ActionInstance} (deferred — the run executes it). Instances expose
249
+ * chainable `.on` / `.once` (declared events plus `done`/`error`); the attached
250
+ * handle is awaited via `instance.ref`.
251
+ */
252
+ export function action(name) {
253
+ return new Builder(name);
254
+ }
@@ -1,6 +1,7 @@
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";
1
+ import type { Logger } from "../../output/log/LogsCollector.ts";
2
+ import type { Output } from "../../output/Output.ts";
3
+ import type { Emitter, EventHandler } from "../events/events.ts";
4
+ import type { Proc } from "../../system/process/process.ts";
4
5
  /** An action that declares no events. */
5
6
  export type NoEvents = Record<never, never>;
6
7
  /** A value an action's `exec` may return: a result, or a promise of one. */
@@ -9,7 +10,7 @@ export type Awaitable<T> = T | Promise<T>;
9
10
  * How an action settles. An action **never throws into the spin body** — its
10
11
  * failure is data, not control flow: it completes with `{ ok: false, error }`
11
12
  * instead of rejecting. On success it carries the exec's `value`. `exitCode` is
12
- * present when the failure came from a process (see {@link import("../actions/command.ts").command}).
13
+ * present when the failure came from a process (see {@link import("../../../actions/command.ts").command}).
13
14
  * Await it via `instance.done` or `once("done")`; both resolve, never reject.
14
15
  */
15
16
  export type Outcome<T> = {
@@ -35,16 +36,41 @@ export interface SystemEvents<Result> {
35
36
  }
36
37
  /** The full event map a constructed instance exposes: the declared events plus the system ones. */
37
38
  export type InstanceEvents<Events extends object, Result> = Events & SystemEvents<Result>;
39
+ /**
40
+ * Launch an action into the spin and get the same instance back, so you can
41
+ * `await` its `.done` (its {@link Outcome} — resolves with `{ ok, value } |
42
+ * { ok, error }`, **never throws**), its `.ref` (the attached handle; rejects on
43
+ * failure, so it's for data deps consumed inside other actions), or subscribe with
44
+ * `.on` / `.once`. The spin body waves an action in with a nod; an action nods
45
+ * **child** actions the same way, and their log output is prefixed with the
46
+ * parent's path (see `SpinHost`).
47
+ */
48
+ export type Nod = <I extends AnyActionInstance>(instance: I) => I;
38
49
  /** System services the pipeline injects into every action when it runs. */
39
50
  export interface SystemGlobal {
40
51
  /** A logger bound to this action — feeds it directly or via `proc`. */
41
52
  readonly logs: Logger;
42
53
  /** The run's abort signal — aborted on teardown/cancel; honor it to stop early. */
43
54
  readonly signal: AbortSignal;
55
+ /**
56
+ * Launch a **child** action into the same spin (returns it for awaiting/chaining).
57
+ * The child's log output is prefixed with this action's path (`parent › child`),
58
+ * and the child lives in the spin — it keeps the run alive until it settles,
59
+ * independent of when this action finishes. `await ctx.nod(x).done` to wait on it.
60
+ */
61
+ readonly nod: Nod;
62
+ /**
63
+ * This run's output subsystem — the folder, snapshot store, and run-log writer,
64
+ * owned by the {@link SpinHost} and threaded to every action (rather than reached
65
+ * as a module singleton). The framework uses it to place `ctx.artifactsFolder`
66
+ * and to snapshot event/proc payloads; actions can reach `output.snapshots` to
67
+ * capture their own artifacts.
68
+ */
69
+ readonly output: Output;
44
70
  }
45
71
  /** What an action's implementation receives: the system bag, plus `emit`, `attach`, and `proc`. */
46
72
  export interface ActionContext<Events extends object, Handle = void> extends SystemGlobal {
47
- /** Emit one of this action's declared events (also pushed to the global log). */
73
+ /** Emit one of this action's declared events (also logged onto the timeline under this action's path). */
48
74
  readonly emit: Emitter<Events>["emit"];
49
75
  /** Publish this action's imperative handle, resolving its instance's `.ref`. */
50
76
  readonly attach: (handle: Handle) => void;
@@ -126,12 +152,6 @@ export interface ActionInstance<Result = unknown, Events extends object = NoEven
126
152
  * still excluding non-instances.
127
153
  */
128
154
  export type AnyActionInstance = ActionInstance<unknown, object, unknown>;
129
- /**
130
- * If `value` is some action instance's `.ref` promise, the instance that owns it
131
- * (the producer of that handle); otherwise `undefined`. Used by the run to verify
132
- * a `.ref` dependency was launched before the action that consumes it.
133
- */
134
- export declare const producerOfRef: (value: unknown) => AnyActionInstance | undefined;
135
155
  /** A callable produced by `action`: invoking it builds an {@link ActionInstance}. */
136
156
  export interface Action<Args extends unknown[], Result, Events extends object = NoEvents, Handle = void> {
137
157
  (...args: Args): ActionInstance<Result, Events, Handle>;
@@ -146,19 +166,3 @@ export interface ActionBuilder<Events extends object, Handle = void> {
146
166
  /** Provide the implementation. `ctx` carries `logs`, `signal`, `emit`, and `attach`. */
147
167
  run<Args extends unknown[], Result>(exec: (ctx: ActionContext<Events, Handle>, ...args: Args) => Awaitable<Result>): Action<Args, Result, Events, Handle>;
148
168
  }
149
- /**
150
- * Defines an action. Always returns a builder; provide the implementation via
151
- * `.run(…)`, optionally after declaring events and/or a handle:
152
- *
153
- * action("Build").run(async (ctx, opts) => { … }) // no declared events / handle
154
- * action("Watch") // typed events + handle
155
- * .emits<{ create: string }>()
156
- * .ref<{ rescan(): void }>()
157
- * .run((ctx, opts) => { ctx.emit(…); ctx.attach(…); … })
158
- *
159
- * `.run(…)` yields a callable that, when invoked, constructs an
160
- * {@link ActionInstance} (deferred — the run executes it). Instances expose
161
- * chainable `.on` / `.once` (declared events plus `done`/`error`); the attached
162
- * handle is awaited via `instance.ref`.
163
- */
164
- export declare function action(name: string): ActionBuilder<NoEvents, void>;
@@ -0,0 +1 @@
1
+ export {};