omkit 0.0.15 → 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.
Files changed (110) hide show
  1. package/README.md +110 -97
  2. package/dist/actions/chrome-page.d.ts +12 -29
  3. package/dist/actions/chrome-page.js +19 -32
  4. package/dist/actions/command.d.ts +8 -41
  5. package/dist/actions/command.js +15 -82
  6. package/dist/actions/healthcheck.d.ts +3 -11
  7. package/dist/actions/healthcheck.js +8 -22
  8. package/dist/actions/index.d.ts +6 -8
  9. package/dist/actions/index.js +4 -6
  10. package/dist/actions/prompt.d.ts +5 -19
  11. package/dist/actions/prompt.js +10 -31
  12. package/dist/actions/tail-log.d.ts +7 -21
  13. package/dist/actions/tail-log.js +13 -34
  14. package/dist/actions/until-log.d.ts +4 -7
  15. package/dist/actions/until-log.js +3 -3
  16. package/dist/actions/watch-dir.d.ts +5 -19
  17. package/dist/actions/watch-dir.js +13 -28
  18. package/dist/actions/watch.d.ts +4 -12
  19. package/dist/actions/watch.js +9 -16
  20. package/dist/core/ActionRun.d.ts +97 -0
  21. package/dist/core/ActionRun.js +266 -0
  22. package/dist/core/ExecutionTree.d.ts +88 -0
  23. package/dist/core/ExecutionTree.js +320 -0
  24. package/dist/core/action.d.ts +6 -0
  25. package/dist/core/action.js +72 -0
  26. package/dist/core/context.d.ts +9 -0
  27. package/dist/core/context.js +8 -0
  28. package/dist/core/om.d.ts +9 -0
  29. package/dist/core/om.js +25 -0
  30. package/dist/core/step.d.ts +9 -0
  31. package/dist/core/step.js +18 -0
  32. package/dist/core/types.d.ts +128 -0
  33. package/dist/foundation/ActionRef.d.ts +18 -0
  34. package/dist/foundation/ActionRef.js +5 -0
  35. package/dist/{utils → foundation}/AsyncQueue.d.ts +2 -4
  36. package/dist/{utils → foundation}/AsyncQueue.js +6 -19
  37. package/dist/foundation/CancelledError.d.ts +10 -0
  38. package/dist/foundation/CancelledError.js +13 -0
  39. package/dist/foundation/Deferred.d.ts +8 -0
  40. package/dist/foundation/Deferred.js +10 -0
  41. package/dist/foundation/LogEntry.d.ts +29 -0
  42. package/dist/{orchestration/events → foundation}/events.d.ts +2 -11
  43. package/dist/{orchestration/events → foundation}/events.js +1 -7
  44. package/dist/{utils → foundation}/format.d.ts +3 -4
  45. package/dist/{utils → foundation}/format.js +12 -4
  46. package/dist/foundation/fsSafe.d.ts +7 -0
  47. package/dist/foundation/fsSafe.js +7 -0
  48. package/dist/foundation/ids.d.ts +9 -0
  49. package/dist/foundation/ids.js +10 -0
  50. package/dist/index.d.ts +11 -22
  51. package/dist/index.js +10 -30
  52. package/dist/output/LiveRenderer.d.ts +13 -0
  53. package/dist/output/LiveRenderer.js +56 -0
  54. package/dist/output/console/ConsoleCapture.d.ts +20 -0
  55. package/dist/output/console/ConsoleCapture.js +47 -0
  56. package/dist/output/folder/RunFolder.d.ts +11 -22
  57. package/dist/output/folder/RunFolder.js +18 -36
  58. package/dist/output/log/LogStore.d.ts +22 -0
  59. package/dist/output/log/LogStore.js +56 -0
  60. package/dist/output/log/RawStream.d.ts +11 -0
  61. package/dist/output/log/RawStream.js +18 -0
  62. package/dist/output/snapshot/SnapshotStore.d.ts +6 -43
  63. package/dist/output/snapshot/SnapshotStore.js +12 -52
  64. package/dist/output/writers/NodeLogWriter.d.ts +8 -0
  65. package/dist/output/writers/NodeLogWriter.js +36 -0
  66. package/dist/output/writers/ResultWriter.d.ts +3 -0
  67. package/dist/output/writers/ResultWriter.js +5 -0
  68. package/dist/output/writers/RollupWriter.d.ts +9 -0
  69. package/dist/output/writers/RollupWriter.js +29 -0
  70. package/dist/output/writers/entryLine.d.ts +10 -0
  71. package/dist/output/writers/entryLine.js +39 -0
  72. package/dist/output/writers/views.d.ts +38 -0
  73. package/dist/output/writers/views.js +6 -0
  74. package/dist/system/fs/FolderCache.d.ts +6 -34
  75. package/dist/system/fs/FolderCache.js +7 -38
  76. package/dist/system/proc.d.ts +51 -0
  77. package/dist/system/proc.js +148 -0
  78. package/dist/system/procRegistry.d.ts +52 -0
  79. package/dist/system/procRegistry.js +117 -0
  80. package/package.json +3 -1
  81. package/dist/orchestration/action/action.d.ts +0 -23
  82. package/dist/orchestration/action/action.js +0 -256
  83. package/dist/orchestration/action/types.d.ts +0 -182
  84. package/dist/orchestration/assert.d.ts +0 -24
  85. package/dist/orchestration/assert.js +0 -26
  86. package/dist/orchestration/spin/SpinHost.d.ts +0 -111
  87. package/dist/orchestration/spin/SpinHost.js +0 -345
  88. package/dist/orchestration/spin/spin.d.ts +0 -24
  89. package/dist/orchestration/spin/spin.js +0 -57
  90. package/dist/orchestration/spin/types.d.ts +0 -63
  91. package/dist/output/Output.d.ts +0 -24
  92. package/dist/output/Output.js +0 -27
  93. package/dist/output/index.d.ts +0 -11
  94. package/dist/output/index.js +0 -11
  95. package/dist/output/log/LogRenderer.d.ts +0 -73
  96. package/dist/output/log/LogRenderer.js +0 -133
  97. package/dist/output/log/LogsCollector.d.ts +0 -79
  98. package/dist/output/log/LogsCollector.js +0 -141
  99. package/dist/output/log/RunLog.d.ts +0 -54
  100. package/dist/output/log/RunLog.js +0 -152
  101. package/dist/output/log/console-capture.d.ts +0 -24
  102. package/dist/output/log/console-capture.js +0 -53
  103. package/dist/system/fs/streams.d.ts +0 -11
  104. package/dist/system/fs/streams.js +0 -14
  105. package/dist/system/process/process.d.ts +0 -35
  106. package/dist/system/process/process.js +0 -104
  107. package/dist/utils/serialize.d.ts +0 -2
  108. package/dist/utils/serialize.js +0 -12
  109. /package/dist/{orchestration/action → core}/types.js +0 -0
  110. /package/dist/{orchestration/spin/types.js → foundation/LogEntry.js} +0 -0
@@ -1,19 +1,14 @@
1
- import { action } from "../orchestration/action/action.js";
1
+ import { action } from "../core/action.js";
2
2
  /**
3
3
  * Gate: polls an HTTP endpoint until it answers in a healthy way (status and,
4
- * optionally, body), then resolves with that probe and emits `healthy`. The
5
- * counterpart to {@link import("./until-log.ts").untilLog} for "wait until a
6
- * service is up" — robust where log-scraping is brittle (no output format to
7
- * track, no ANSI to strip).
8
- *
9
- * Connection refusals and slow probes during startup are expected and simply
10
- * retried; each request is bounded by `requestTimeoutMs` (so a hung socket can't
11
- * stall the loop) and the whole wait by the run's abort signal and an optional
4
+ * optionally, body), then resolves with that probe and emits `healthy`. Refusals
5
+ * and slow probes during startup are retried; each request is bounded by
6
+ * `requestTimeoutMs` and the whole wait by the run's abort signal and optional
12
7
  * `timeoutMs`, so teardown unblocks a pending probe instead of hanging.
13
8
  */
14
- export const healthcheck = action("Healthcheck")
9
+ export const healthcheck = action("healthcheck")
15
10
  .emits()
16
- .run(async ({ logs, signal, emit }, opts = {}) => {
11
+ .run(async ({ signal, emit }, opts = {}) => {
17
12
  const { url: rawUrl, host = "localhost", port, path = "/", protocol = "http", status = (s) => s >= 200 && s < 300, body, intervalMs = 250, requestTimeoutMs = 5000, timeoutMs, request, } = opts;
18
13
  const url = rawUrl ?? (port !== undefined ? `${protocol}://${host}:${port}${path}` : undefined);
19
14
  if (!url)
@@ -46,7 +41,7 @@ export const healthcheck = action("Healthcheck")
46
41
  signal.addEventListener("abort", onAbort, { once: true });
47
42
  });
48
43
  const deadline = timeoutMs === undefined ? undefined : Date.now() + timeoutMs;
49
- logs.append({ source: "Healthcheck", level: "info", message: `probing ${url}` });
44
+ console.log(`probing ${url}`);
50
45
  let attempts = 0;
51
46
  for (;;) {
52
47
  if (signal.aborted)
@@ -55,23 +50,16 @@ export const healthcheck = action("Healthcheck")
55
50
  try {
56
51
  const res = await fetch(url, {
57
52
  ...request,
58
- // Per-probe timeout combined with teardown — either aborts the request.
59
53
  signal: AbortSignal.any([signal, AbortSignal.timeout(requestTimeoutMs)]),
60
54
  });
61
55
  if (statusOk(res.status)) {
62
- // Read the body only when a matcher needs it; otherwise drain it so the
63
- // connection is released rather than left dangling.
64
56
  const text = body === undefined ? "" : await res.text();
65
57
  if (body === undefined)
66
58
  await res.body?.cancel().catch(() => { });
67
59
  if (bodyOk(text)) {
68
60
  const result = { url, status: res.status, attempts };
69
61
  const probes = `${attempts} ${attempts === 1 ? "probe" : "probes"}`;
70
- logs.append({
71
- source: "Healthcheck",
72
- level: "info",
73
- message: `healthy ${url} → ${res.status} (${probes})`,
74
- });
62
+ console.log(`healthy ${url} → ${res.status} (${probes})`);
75
63
  emit("healthy", result);
76
64
  return result;
77
65
  }
@@ -81,8 +69,6 @@ export const healthcheck = action("Healthcheck")
81
69
  }
82
70
  }
83
71
  catch {
84
- // Teardown surfaces here too — stop cleanly. Otherwise it's a refusal or
85
- // probe timeout while the service starts up: expected, so keep polling.
86
72
  if (signal.aborted)
87
73
  throw new Error("healthcheck: aborted");
88
74
  }
@@ -1,22 +1,20 @@
1
1
  /**
2
- * tswm/actions — reusable actions built on the core engine.
3
- *
4
- * Each is a ready-made {@link action} for a common orchestration need; for
5
- * anything beyond these, drop down to `action(...).run(...)` from `tswm`.
2
+ * omkit/actions — reusable actions built on the core engine. For anything beyond
3
+ * these, drop down to `action(...).run(...)`.
6
4
  */
7
5
  export { command } from "./command.ts";
8
6
  export type { CommandOptions } from "./command.ts";
7
+ export { untilLog } from "./until-log.ts";
8
+ export type { LogMatcher, UntilLogOptions, UntilLogEvents } from "./until-log.ts";
9
+ export { healthcheck } from "./healthcheck.ts";
10
+ export type { HealthcheckOptions, HealthcheckResult, HealthcheckEvents, StatusMatcher, BodyMatcher, } from "./healthcheck.ts";
9
11
  export { watch } from "./watch.ts";
10
12
  export type { FileEvent, FileListener, WatchOptions } from "./watch.ts";
11
13
  export { watchDir } from "./watch-dir.ts";
12
14
  export type { WatchDirOptions, WatchDirEvents } from "./watch-dir.ts";
13
- export { untilLog } from "./until-log.ts";
14
- export type { LogMatcher, UntilLogOptions, UntilLogEvents } from "./until-log.ts";
15
15
  export { tailLog } from "./tail-log.ts";
16
16
  export type { TailLogOptions } from "./tail-log.ts";
17
17
  export { prompt } from "./prompt.ts";
18
18
  export type { PromptOptions, InputPromptOptions, ChoicePromptOptions, PromptChoice, PromptVia, PromptEvents, } from "./prompt.ts";
19
- export { healthcheck } from "./healthcheck.ts";
20
- export type { HealthcheckOptions, HealthcheckResult, HealthcheckEvents, StatusMatcher, BodyMatcher, } from "./healthcheck.ts";
21
19
  export { chromePage } from "./chrome-page.ts";
22
20
  export type { ChromePageOptions, ChromePageEvents, ChromePageSource, Page, Browser, BrowserContext, } from "./chrome-page.ts";
@@ -1,14 +1,12 @@
1
1
  /**
2
- * tswm/actions — reusable actions built on the core engine.
3
- *
4
- * Each is a ready-made {@link action} for a common orchestration need; for
5
- * anything beyond these, drop down to `action(...).run(...)` from `tswm`.
2
+ * omkit/actions — reusable actions built on the core engine. For anything beyond
3
+ * these, drop down to `action(...).run(...)`.
6
4
  */
7
5
  export { command } from "./command.js";
6
+ export { untilLog } from "./until-log.js";
7
+ export { healthcheck } from "./healthcheck.js";
8
8
  export { watch } from "./watch.js";
9
9
  export { watchDir } from "./watch-dir.js";
10
- export { untilLog } from "./until-log.js";
11
10
  export { tailLog } from "./tail-log.js";
12
11
  export { prompt } from "./prompt.js";
13
- export { healthcheck } from "./healthcheck.js";
14
12
  export { chromePage } from "./chrome-page.js";
@@ -6,11 +6,7 @@ export type PromptChoice = string | {
6
6
  interface PromptCommon {
7
7
  /** Question shown to the user. Defaults per kind ("Input:" / "Choose:"). */
8
8
  message?: string;
9
- /**
10
- * Use the default and stop waiting after this many ms. A non-finite or
11
- * non-positive value waits indefinitely (until answered or teardown).
12
- * Default 30000.
13
- */
9
+ /** Use the default and stop waiting after this many ms (≤0/non-finite = forever). Default 30000. */
14
10
  timeoutMs?: number;
15
11
  }
16
12
  export interface InputPromptOptions extends PromptCommon {
@@ -20,7 +16,6 @@ export interface InputPromptOptions extends PromptCommon {
20
16
  }
21
17
  export interface ChoicePromptOptions extends PromptCommon {
22
18
  kind: "choice";
23
- /** The options to choose from (entered by number, value, or label). */
24
19
  choices: PromptChoice[];
25
20
  /** Default selection — a choice value or a 1-based index. Default: the first choice. */
26
21
  default?: string | number;
@@ -29,26 +24,17 @@ export type PromptOptions = InputPromptOptions | ChoicePromptOptions;
29
24
  /** How a prompt's answer was decided. */
30
25
  export type PromptVia = "input" | "default" | "timeout";
31
26
  export interface PromptEvents {
32
- /** The prompt was shown; payload is the question message. */
33
27
  prompt: string;
34
- /** An answer settled; payload is the value and how it was decided. */
35
28
  answer: {
36
29
  value: string;
37
30
  via: PromptVia;
38
31
  };
39
32
  }
40
33
  /**
41
- * Asks the terminal a question — free-text **input** or a **choice** from a list —
42
- * and resolves with the answer, which it also publishes as the action's handle
43
- * (so `prompt(...).ref` feeds straight into another action). Everything is
44
- * defaulted: a bare `prompt()` is a free-text input with no default and a 30s
45
- * timeout.
46
- *
47
- * A configurable timeout (default 30s; non-finite/≤0 waits forever) falls back to
48
- * the default value, so an unattended run never blocks. The run's abort signal
49
- * also unblocks a pending prompt on teardown. Emits `prompt` when shown and
50
- * `answer` when settled (carrying whether it came from input, the default, or a
51
- * timeout).
34
+ * Asks the terminal a question — free-text **input** or a **choice** — and resolves
35
+ * with the answer, which it also attaches as the handle (`prompt(...).ref` feeds
36
+ * another action). A timeout (default 30s) falls back to the default so an
37
+ * unattended run never blocks; teardown also unblocks a pending prompt.
52
38
  */
53
39
  export declare const prompt: import("../index.ts").Action<[opts?: PromptOptions | undefined], string, PromptEvents, string>;
54
40
  export {};
@@ -1,27 +1,18 @@
1
1
  import { createInterface } from "node:readline/promises";
2
- import { action } from "../orchestration/action/action.js";
3
- /** Normalize choices to `{ label, value }`. */
2
+ import { action } from "../core/action.js";
4
3
  const asChoices = (choices) => choices.map((c) => (typeof c === "string" ? { label: c, value: c } : c));
5
4
  /**
6
- * Asks the terminal a question — free-text **input** or a **choice** from a list —
7
- * and resolves with the answer, which it also publishes as the action's handle
8
- * (so `prompt(...).ref` feeds straight into another action). Everything is
9
- * defaulted: a bare `prompt()` is a free-text input with no default and a 30s
10
- * timeout.
11
- *
12
- * A configurable timeout (default 30s; non-finite/≤0 waits forever) falls back to
13
- * the default value, so an unattended run never blocks. The run's abort signal
14
- * also unblocks a pending prompt on teardown. Emits `prompt` when shown and
15
- * `answer` when settled (carrying whether it came from input, the default, or a
16
- * timeout).
5
+ * Asks the terminal a question — free-text **input** or a **choice** — and resolves
6
+ * with the answer, which it also attaches as the handle (`prompt(...).ref` feeds
7
+ * another action). A timeout (default 30s) falls back to the default so an
8
+ * unattended run never blocks; teardown also unblocks a pending prompt.
17
9
  */
18
- export const prompt = action("Prompt")
10
+ export const prompt = action("prompt")
19
11
  .emits()
20
12
  .ref()
21
- .run(async ({ logs, signal, emit, attach }, opts = {}) => {
13
+ .run(async ({ signal, emit, attach }, opts = {}) => {
22
14
  const timeoutMs = opts.timeoutMs ?? 30_000;
23
15
  const finite = Number.isFinite(timeoutMs) && timeoutMs > 0;
24
- // Build the question, the fallback value, and a resolver from raw input.
25
16
  let message;
26
17
  let query;
27
18
  let defaultValue;
@@ -40,7 +31,6 @@ export const prompt = action("Prompt")
40
31
  .map((c, i) => ` ${i + 1}) ${c.label}${c.value === defaultValue ? " (default)" : ""}`)
41
32
  .join("\n");
42
33
  query = `${message}\n${list}\n> `;
43
- // Accept a 1-based index, an exact value, or an exact label.
44
34
  resolveRaw = (raw) => {
45
35
  const n = Number(raw);
46
36
  if (Number.isInteger(n) && n >= 1 && n <= choices.length)
@@ -52,11 +42,9 @@ export const prompt = action("Prompt")
52
42
  defaultValue = opts.default ?? "";
53
43
  message = opts.message ?? "Input:";
54
44
  query = defaultValue ? `${message} (${defaultValue}) ` : `${message} `;
55
- resolveRaw = (raw) => raw; // any non-empty input is the value
45
+ resolveRaw = (raw) => raw;
56
46
  }
57
47
  emit("prompt", message);
58
- // A dedicated controller for the timeout, so it's distinguishable from a
59
- // teardown abort; either one cancels the pending question.
60
48
  const onTimeout = new AbortController();
61
49
  const timer = finite ? setTimeout(() => onTimeout.abort(), timeoutMs) : undefined;
62
50
  const rl = createInterface({ input: process.stdin, output: process.stdout });
@@ -74,17 +62,12 @@ export const prompt = action("Prompt")
74
62
  via = "input";
75
63
  }
76
64
  else {
77
- logs.append({
78
- source: "Prompt",
79
- level: "info",
80
- message: `invalid answer "${raw}", using default`,
81
- });
65
+ console.log(`invalid answer "${raw}", using default`);
82
66
  via = "default";
83
67
  }
84
68
  }
85
69
  }
86
70
  catch {
87
- // Aborted before an answer: the timeout (use default + note it) or teardown.
88
71
  via = onTimeout.signal.aborted ? "timeout" : "default";
89
72
  }
90
73
  finally {
@@ -92,11 +75,7 @@ export const prompt = action("Prompt")
92
75
  clearTimeout(timer);
93
76
  rl.close();
94
77
  }
95
- logs.append({
96
- source: "Prompt",
97
- level: "info",
98
- message: via === "timeout" ? `timed out → ${value}` : `${value} (${via})`,
99
- });
78
+ console.log(via === "timeout" ? `timed out → ${value}` : `${value} (${via})`);
100
79
  attach(value); // resolves instance.ref → the answer
101
80
  emit("answer", { value, via });
102
81
  return value;
@@ -1,12 +1,5 @@
1
1
  export interface TailLogOptions {
2
- /** Label the lines are logged under. Default: the file's basename. */
3
- source?: string;
4
- /** Level for appended lines. Default "info". */
5
- level?: string;
6
- /**
7
- * Replay the file's existing content first. Default false — start at the
8
- * current end and only stream what's appended from now on (like `tail -f`).
9
- */
2
+ /** Replay the file's existing content first. Default false (start at the end). */
10
3
  fromStart?: boolean;
11
4
  /** Text encoding of the file. Default "utf8". */
12
5
  encoding?: BufferEncoding;
@@ -14,17 +7,10 @@ export interface TailLogOptions {
14
7
  debounceMs?: number;
15
8
  }
16
9
  /**
17
- * Tails a (possibly not-yet-existing) text/log file and streams each appended
18
- * line into the run's combined log — the way `proc` streams a child's stdout, but
19
- * for a file some *other* process writes. Useful for folding an external tool's
20
- * own log file into the one timeline.
21
- *
22
- * It tracks a byte offset and reads only the delta on each change, buffering a
23
- * partial trailing line until its newline arrives (so a line split across two
24
- * writes is logged once, whole). A shrink in size is treated as truncation or
25
- * rotation — the offset resets to 0 and reading resumes from the top. Since
26
- * `fs.watch` can't watch a missing path, it watches the parent directory and
27
- * filters by name, like {@link import("./watch.ts").watch}. Runs until the run's
28
- * signal aborts, then closes the watcher (flushing any trailing partial line).
10
+ * Tails a (possibly not-yet-existing) text file and streams each appended line
11
+ * into the run's log — like `proc` streams a child's stdout, but for a file some
12
+ * *other* process writes. Tracks a byte offset and reads only the delta, buffering
13
+ * a partial trailing line until its newline; a shrink is treated as truncation
14
+ * (offset resets). Daemon: runs until torn down, then flushes any partial line.
29
15
  */
30
- export declare const tailLog: import("../index.ts").Action<[target: string, opts?: TailLogOptions | undefined], void, import("../index.ts").NoEvents, void>;
16
+ export declare const tailLog: import("../index.ts").Action<[target: string, opts?: TailLogOptions | undefined], void, import("../core/types.ts").NoEvents, void>;
@@ -2,33 +2,22 @@ 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 "../orchestration/action/action.js";
5
+ import { action } from "../core/action.js";
6
6
  /**
7
- * Tails a (possibly not-yet-existing) text/log file and streams each appended
8
- * line into the run's combined log — the way `proc` streams a child's stdout, but
9
- * for a file some *other* process writes. Useful for folding an external tool's
10
- * own log file into the one timeline.
11
- *
12
- * It tracks a byte offset and reads only the delta on each change, buffering a
13
- * partial trailing line until its newline arrives (so a line split across two
14
- * writes is logged once, whole). A shrink in size is treated as truncation or
15
- * rotation — the offset resets to 0 and reading resumes from the top. Since
16
- * `fs.watch` can't watch a missing path, it watches the parent directory and
17
- * filters by name, like {@link import("./watch.ts").watch}. Runs until the run's
18
- * signal aborts, then closes the watcher (flushing any trailing partial line).
7
+ * Tails a (possibly not-yet-existing) text file and streams each appended line
8
+ * into the run's log — like `proc` streams a child's stdout, but for a file some
9
+ * *other* process writes. Tracks a byte offset and reads only the delta, buffering
10
+ * a partial trailing line until its newline; a shrink is treated as truncation
11
+ * (offset resets). Daemon: runs until torn down, then flushes any partial line.
19
12
  */
20
- export const tailLog = action("Tail Log").run(async ({ logs, signal }, target, opts = {}) => {
13
+ export const tailLog = action("tailLog").run(async ({ signal }, target, opts = {}) => {
21
14
  const file = resolve(target);
22
15
  const dir = dirname(file);
23
16
  const name = basename(file);
24
- const { source = name, level = "info", fromStart = false, encoding = "utf8", debounceMs = 50, } = opts;
17
+ const { fromStart = false, encoding = "utf8", debounceMs = 50 } = opts;
25
18
  let pos = 0; // bytes consumed so far
26
19
  let pending = ""; // partial trailing line, buffered until its newline
27
- // Decodes bytes to text across reads. It holds back an incomplete trailing
28
- // multi-byte char (split across a chunk or read boundary) until its remaining
29
- // bytes arrive, so a character straddling a boundary never decodes to `�`.
30
20
  let decoder = new StringDecoder(encoding);
31
- // Append every complete line in `text`, keeping any remainder for next time.
32
21
  const feed = (text) => {
33
22
  pending += text;
34
23
  let newline;
@@ -36,7 +25,7 @@ export const tailLog = action("Tail Log").run(async ({ logs, signal }, target, o
36
25
  const line = pending.slice(0, newline).replace(/\r$/, "");
37
26
  pending = pending.slice(newline + 1);
38
27
  if (line)
39
- logs.append({ source, level, message: line });
28
+ console.log(line);
40
29
  }
41
30
  };
42
31
  const readDelta = async () => {
@@ -48,8 +37,7 @@ export const tailLog = action("Tail Log").run(async ({ logs, signal }, target, o
48
37
  return; // missing/unreadable — wait for the next event
49
38
  }
50
39
  if (size < pos) {
51
- // Truncation/rotation: restart from the top and drop any buffered partial
52
- // line and half-decoded char — they belong to the old file's bytes.
40
+ // Truncation/rotation: restart from the top, drop stale buffers.
53
41
  pos = 0;
54
42
  pending = "";
55
43
  decoder = new StringDecoder(encoding);
@@ -66,8 +54,6 @@ export const tailLog = action("Tail Log").run(async ({ logs, signal }, target, o
66
54
  if (bytesRead === 0)
67
55
  break;
68
56
  pos += bytesRead;
69
- // decoder.write returns only fully-decoded chars, buffering any partial
70
- // trailing multi-byte char for the next chunk/read.
71
57
  feed(decoder.write(buffer.subarray(0, bytesRead)));
72
58
  }
73
59
  }
@@ -75,8 +61,7 @@ export const tailLog = action("Tail Log").run(async ({ logs, signal }, target, o
75
61
  await handle.close();
76
62
  }
77
63
  };
78
- // Serialize reads so a burst of events can't overlap; coalesce extra triggers
79
- // into a single follow-up pass.
64
+ // Serialize reads; coalesce extra triggers into a single follow-up pass.
80
65
  let reading = false;
81
66
  let again = false;
82
67
  const pump = () => {
@@ -103,25 +88,19 @@ export const tailLog = action("Tail Log").run(async ({ logs, signal }, target, o
103
88
  clearTimeout(timer);
104
89
  timer = setTimeout(pump, debounceMs);
105
90
  };
106
- // Seed the offset: from the top when replaying, otherwise from the current end
107
- // so existing content is skipped. A missing file starts at 0 — its content
108
- // counts as "appended" once it appears.
109
91
  try {
110
92
  pos = fromStart ? 0 : (await stat(file)).size;
111
93
  }
112
94
  catch {
113
95
  pos = 0;
114
96
  }
115
- // Announce the target up front so the combined log shows which file these
116
- // lines come from — the resolved absolute path, plus whether we're replaying.
117
- logs.append({ source, level, message: `tailing ${file}${fromStart ? " (from start)" : ""}` });
97
+ console.log(`tailing ${file}${fromStart ? " (from start)" : ""}`);
118
98
  if (fromStart)
119
99
  pump();
120
100
  const watcher = fsWatch(dir, (_event, filename) => {
121
101
  if (filename === null || filename === name)
122
102
  schedule();
123
103
  });
124
- // Daemon: stay alive until torn down, then stop and flush any partial line.
125
104
  return new Promise((resolveRun) => {
126
105
  const stop = () => {
127
106
  if (timer)
@@ -129,7 +108,7 @@ export const tailLog = action("Tail Log").run(async ({ logs, signal }, target, o
129
108
  watcher.close();
130
109
  const tail = pending.replace(/\r$/, "");
131
110
  if (tail)
132
- logs.append({ source, level, message: tail });
111
+ console.log(tail);
133
112
  resolveRun();
134
113
  };
135
114
  if (signal.aborted)
@@ -1,13 +1,10 @@
1
- import type { LogEntry } from "../output/log/LogsCollector.ts";
1
+ import type { LogEntry } from "../foundation/LogEntry.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 {
5
- /**
6
- * Reject if no entry matches within this many milliseconds. Omit to wait
7
- * indefinitely (the caller is then responsible for guaranteeing a match).
8
- */
5
+ /** Reject if no entry matches within this many ms. Omit to wait indefinitely. */
9
6
  timeoutMs?: number;
10
- /** Replay the buffered history before live entries (catch already-logged matches). */
7
+ /** Replay buffered history before live entries (catch already-logged matches). */
11
8
  replay?: boolean;
12
9
  }
13
10
  /** Events emitted by {@link untilLog}. */
@@ -17,7 +14,7 @@ export interface UntilLogEvents {
17
14
  }
18
15
  /**
19
16
  * Gate: resolves with the first log entry the `matcher` accepts (and emits
20
- * `match`). Subscribes to the injected collector and races each read against the
17
+ * `match`). Subscribes to the readable run log and races each read against the
21
18
  * run's abort signal (and an optional timeout), so teardown unblocks a pending
22
19
  * wait instead of hanging.
23
20
  */
@@ -1,11 +1,11 @@
1
- import { action } from "../orchestration/action/action.js";
1
+ import { action } from "../core/action.js";
2
2
  /**
3
3
  * Gate: resolves with the first log entry the `matcher` accepts (and emits
4
- * `match`). Subscribes to the injected collector and races each read against the
4
+ * `match`). Subscribes to the readable run log and races each read against the
5
5
  * run's abort signal (and an optional timeout), so teardown unblocks a pending
6
6
  * wait instead of hanging.
7
7
  */
8
- export const untilLog = action("Until Log")
8
+ export const untilLog = action("untilLog")
9
9
  .emits()
10
10
  .run(({ logs, signal, emit }, matcher, { timeoutMs, replay = false } = {}) => {
11
11
  const iterator = logs.subscribe({ replay })[Symbol.asyncIterator]();
@@ -2,16 +2,10 @@ export interface WatchDirOptions {
2
2
  /** Coalesce rapid events within this window. Default 50ms. */
3
3
  debounceMs?: number;
4
4
  }
5
- /**
6
- * Events emitted by {@link watchDir}. Each payload is the absolute path of the
7
- * entry — a file for create/update/delete, the folder itself for ready/removed.
8
- */
5
+ /** Events emitted by {@link watchDir}. Each payload is the absolute path of the entry. */
9
6
  export interface WatchDirEvents {
10
- /** A file appeared during the run. */
11
7
  create: string;
12
- /** A file's contents changed (its mtime moved). */
13
8
  update: string;
14
- /** A file was removed. */
15
9
  delete: string;
16
10
  /** The folder appeared (or existed at startup); fires again on recreation. */
17
11
  ready: string;
@@ -19,17 +13,9 @@ export interface WatchDirEvents {
19
13
  removed: string;
20
14
  }
21
15
  /**
22
- * Watches a directory and emits per-file events on changes, surviving the folder
23
- * being deleted and recreated (e.g. a build that `rm -rf`s it first).
24
- *
25
- * It watches the existing ancestor chain (target recursively, ancestors
26
- * non-recursively) so a stable ancestor keeps firing when the target reappears.
27
- * Per-file changes are derived by re-`stat`ing and diffing mtimes.
28
- *
29
- * The folder's own removal/recreation can't be seen by stat: a wipe+recreate is
30
- * atomic (never observed absent), and on NTFS the recreated dir even keeps the
31
- * old `ino`/`birthtime` (tunneling). So that's detected from the raw `rename`
32
- * event the *parent* fires for the target's name — when it does and the target
33
- * still exists, it was replaced (`removed` + `ready`).
16
+ * Watches a directory and emits per-file events, surviving the folder being
17
+ * deleted and recreated (e.g. a build that `rm -rf`s it first). Watches the
18
+ * existing ancestor chain so a stable ancestor keeps firing when the target
19
+ * reappears; per-file changes are derived by re-`stat`ing and diffing mtimes.
34
20
  */
35
21
  export declare const watchDir: import("../index.ts").Action<[target: string, opts?: WatchDirOptions | undefined], void, WatchDirEvents, void>;
@@ -1,22 +1,14 @@
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 "../orchestration/action/action.js";
4
+ import { action } from "../core/action.js";
5
5
  /**
6
- * Watches a directory and emits per-file events on changes, surviving the folder
7
- * being deleted and recreated (e.g. a build that `rm -rf`s it first).
8
- *
9
- * It watches the existing ancestor chain (target recursively, ancestors
10
- * non-recursively) so a stable ancestor keeps firing when the target reappears.
11
- * Per-file changes are derived by re-`stat`ing and diffing mtimes.
12
- *
13
- * The folder's own removal/recreation can't be seen by stat: a wipe+recreate is
14
- * atomic (never observed absent), and on NTFS the recreated dir even keeps the
15
- * old `ino`/`birthtime` (tunneling). So that's detected from the raw `rename`
16
- * event the *parent* fires for the target's name — when it does and the target
17
- * still exists, it was replaced (`removed` + `ready`).
6
+ * Watches a directory and emits per-file events, surviving the folder being
7
+ * deleted and recreated (e.g. a build that `rm -rf`s it first). Watches the
8
+ * existing ancestor chain so a stable ancestor keeps firing when the target
9
+ * reappears; per-file changes are derived by re-`stat`ing and diffing mtimes.
18
10
  */
19
- export const watchDir = action("Watch Dir")
11
+ export const watchDir = action("watchDir")
20
12
  .emits()
21
13
  .run(({ signal, emit }, target, opts = {}) => {
22
14
  const { debounceMs = 50 } = opts;
@@ -24,8 +16,8 @@ export const watchDir = action("Watch Dir")
24
16
  const targetName = basename(root);
25
17
  const targetParent = dirname(root);
26
18
  let seen = new Map(); // abs path -> mtimeMs
27
- let rootPresent = false; // believed existence of the folder itself
28
- let churned = false; // the parent reported a rename for the target since last tick
19
+ let rootPresent = false;
20
+ let churned = false;
29
21
  const scan = async () => {
30
22
  const next = new Map();
31
23
  let entries;
@@ -50,8 +42,6 @@ export const watchDir = action("Watch Dir")
50
42
  };
51
43
  const recheck = async (isInitial) => {
52
44
  const next = await scan();
53
- // Startup seeds the snapshot silently — so onCreate means "created during
54
- // this run", not "already there when we started" (last run's leftovers).
55
45
  if (!isInitial) {
56
46
  for (const [path, mtime] of next) {
57
47
  const prev = seen.get(path);
@@ -67,8 +57,6 @@ export const watchDir = action("Watch Dir")
67
57
  }
68
58
  seen = next;
69
59
  };
70
- // Derive the folder's own create/remove/recreate from existence + the churn
71
- // signal (a parent `rename` for the target's name).
72
60
  const detectDir = (isInitial, didChurn) => {
73
61
  const present = existsSync(root);
74
62
  if (isInitial) {
@@ -79,9 +67,9 @@ export const watchDir = action("Watch Dir")
79
67
  }
80
68
  if (didChurn) {
81
69
  if (rootPresent)
82
- emit("removed", root); // replaced or removed
70
+ emit("removed", root);
83
71
  if (present)
84
- emit("ready", root); // created or recreated
72
+ emit("ready", root);
85
73
  }
86
74
  else if (present !== rootPresent) {
87
75
  emit(present ? "ready" : "removed", root);
@@ -101,8 +89,6 @@ export const watchDir = action("Watch Dir")
101
89
  const watchers = [];
102
90
  let armed = [];
103
91
  const onEvent = (dir) => (eventType, filename) => {
104
- // A rename of the target's own name, seen from its parent = the folder
105
- // was created / removed / replaced.
106
92
  if (dir === targetParent && filename === targetName && eventType === "rename") {
107
93
  churned = true;
108
94
  }
@@ -112,7 +98,7 @@ export const watchDir = action("Watch Dir")
112
98
  const chain = existingChain();
113
99
  const same = chain.length === armed.length && chain.every((d, i) => d === armed[i]);
114
100
  if (same && !force)
115
- return; // re-arm only when structure changed (or forced after a churn)
101
+ return;
116
102
  for (const w of watchers)
117
103
  w.close();
118
104
  watchers.length = 0;
@@ -135,17 +121,16 @@ export const watchDir = action("Watch Dir")
135
121
  const tick = async (isInitial) => {
136
122
  const didChurn = churned;
137
123
  churned = false;
138
- arm(didChurn); // re-arm (force after a churn — the old target handle is dead)
124
+ arm(didChurn);
139
125
  detectDir(isInitial, didChurn);
140
126
  await recheck(isInitial);
141
127
  };
142
128
  void tick(true); // initial state + arm the chain
143
- // Daemon: stay alive until torn down, then flush once more and close.
144
129
  return new Promise((resolveRun) => {
145
130
  const stop = async () => {
146
131
  if (timer)
147
132
  clearTimeout(timer);
148
- await tick(false); // final flush — capture changes right before teardown
133
+ await tick(false); // final flush before teardown
149
134
  for (const w of watchers)
150
135
  w.close();
151
136
  watchers.length = 0;
@@ -9,25 +9,17 @@ export interface FileEvent {
9
9
  }
10
10
  export type FileListener = (event: FileEvent) => void;
11
11
  export interface WatchOptions {
12
- /** The file is absent when watching starts. */
13
12
  onMissing?: FileListener;
14
- /** The file appeared (or already existed at startup — `isInitial`). */
15
13
  onCreate?: FileListener;
16
- /** The file's contents changed (its mtime moved). */
17
14
  onUpdate?: FileListener;
18
- /** The file was removed. */
19
15
  onDelete?: FileListener;
20
16
  /** Coalesce rapid events (e.g. an editor's atomic save) within this window. Default 50ms. */
21
17
  debounceMs?: number;
22
18
  }
23
19
  /**
24
20
  * Watches a single file that may not exist yet, firing callbacks on its
25
- * transitions. Since `fs.watch` can't watch a missing path, it watches the
26
- * parent directory and filters by name; on each event it re-`stat`s and diffs
27
- * against the last known state rather than trusting the raw event type. Runs
28
- * until the run's signal aborts, then closes the watcher.
29
- *
30
- * Assumes the parent directory exists; watching when an ancestor is missing is
31
- * a later case.
21
+ * transitions. Watches the parent directory and filters by name (so a missing
22
+ * file can be awaited), re-`stat`ing on each event to diff against last state.
23
+ * Daemon: runs until the run's signal aborts, then closes the watcher.
32
24
  */
33
- export declare const watch: import("../index.ts").Action<[target: string, opts?: WatchOptions | undefined], void, import("../index.ts").NoEvents, void>;
25
+ export declare const watch: import("../index.ts").Action<[target: string, opts?: WatchOptions | undefined], void, import("../core/types.ts").NoEvents, void>;