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.
- package/README.md +110 -97
- package/dist/actions/chrome-page.d.ts +12 -29
- package/dist/actions/chrome-page.js +19 -32
- package/dist/actions/command.d.ts +8 -41
- package/dist/actions/command.js +15 -82
- package/dist/actions/healthcheck.d.ts +3 -11
- package/dist/actions/healthcheck.js +8 -22
- package/dist/actions/index.d.ts +6 -8
- package/dist/actions/index.js +4 -6
- package/dist/actions/prompt.d.ts +5 -19
- package/dist/actions/prompt.js +10 -31
- package/dist/actions/tail-log.d.ts +7 -21
- package/dist/actions/tail-log.js +13 -34
- package/dist/actions/until-log.d.ts +4 -7
- package/dist/actions/until-log.js +3 -3
- package/dist/actions/watch-dir.d.ts +5 -19
- package/dist/actions/watch-dir.js +13 -28
- package/dist/actions/watch.d.ts +4 -12
- package/dist/actions/watch.js +9 -16
- package/dist/core/ActionRun.d.ts +97 -0
- package/dist/core/ActionRun.js +266 -0
- package/dist/core/ExecutionTree.d.ts +88 -0
- package/dist/core/ExecutionTree.js +320 -0
- package/dist/core/action.d.ts +6 -0
- package/dist/core/action.js +72 -0
- package/dist/core/context.d.ts +9 -0
- package/dist/core/context.js +8 -0
- package/dist/core/om.d.ts +9 -0
- package/dist/core/om.js +25 -0
- package/dist/core/step.d.ts +9 -0
- package/dist/core/step.js +18 -0
- package/dist/core/types.d.ts +128 -0
- package/dist/foundation/ActionRef.d.ts +18 -0
- package/dist/foundation/ActionRef.js +5 -0
- package/dist/{utils → foundation}/AsyncQueue.d.ts +2 -4
- package/dist/{utils → foundation}/AsyncQueue.js +6 -19
- package/dist/foundation/CancelledError.d.ts +10 -0
- package/dist/foundation/CancelledError.js +13 -0
- package/dist/foundation/Deferred.d.ts +8 -0
- package/dist/foundation/Deferred.js +10 -0
- package/dist/foundation/LogEntry.d.ts +29 -0
- package/dist/{orchestration/events → foundation}/events.d.ts +2 -11
- package/dist/{orchestration/events → foundation}/events.js +1 -7
- package/dist/{utils → foundation}/format.d.ts +3 -4
- package/dist/{utils → foundation}/format.js +12 -4
- package/dist/foundation/fsSafe.d.ts +7 -0
- package/dist/foundation/fsSafe.js +7 -0
- package/dist/foundation/ids.d.ts +9 -0
- package/dist/foundation/ids.js +10 -0
- package/dist/index.d.ts +11 -22
- package/dist/index.js +10 -30
- package/dist/output/LiveRenderer.d.ts +13 -0
- package/dist/output/LiveRenderer.js +56 -0
- package/dist/output/console/ConsoleCapture.d.ts +20 -0
- package/dist/output/console/ConsoleCapture.js +47 -0
- package/dist/output/folder/RunFolder.d.ts +11 -22
- package/dist/output/folder/RunFolder.js +18 -36
- package/dist/output/log/LogStore.d.ts +22 -0
- package/dist/output/log/LogStore.js +56 -0
- package/dist/output/log/RawStream.d.ts +11 -0
- package/dist/output/log/RawStream.js +18 -0
- package/dist/output/snapshot/SnapshotStore.d.ts +6 -43
- package/dist/output/snapshot/SnapshotStore.js +12 -52
- package/dist/output/writers/NodeLogWriter.d.ts +8 -0
- package/dist/output/writers/NodeLogWriter.js +36 -0
- package/dist/output/writers/ResultWriter.d.ts +3 -0
- package/dist/output/writers/ResultWriter.js +5 -0
- package/dist/output/writers/RollupWriter.d.ts +9 -0
- package/dist/output/writers/RollupWriter.js +29 -0
- package/dist/output/writers/entryLine.d.ts +10 -0
- package/dist/output/writers/entryLine.js +39 -0
- package/dist/output/writers/views.d.ts +38 -0
- package/dist/output/writers/views.js +6 -0
- package/dist/system/fs/FolderCache.d.ts +6 -34
- package/dist/system/fs/FolderCache.js +7 -38
- package/dist/system/proc.d.ts +51 -0
- package/dist/system/proc.js +148 -0
- package/dist/system/procRegistry.d.ts +52 -0
- package/dist/system/procRegistry.js +117 -0
- package/package.json +3 -1
- package/dist/orchestration/action/action.d.ts +0 -23
- package/dist/orchestration/action/action.js +0 -256
- package/dist/orchestration/action/types.d.ts +0 -182
- package/dist/orchestration/assert.d.ts +0 -24
- package/dist/orchestration/assert.js +0 -26
- package/dist/orchestration/spin/SpinHost.d.ts +0 -111
- package/dist/orchestration/spin/SpinHost.js +0 -345
- package/dist/orchestration/spin/spin.d.ts +0 -24
- package/dist/orchestration/spin/spin.js +0 -57
- package/dist/orchestration/spin/types.d.ts +0 -63
- package/dist/output/Output.d.ts +0 -24
- package/dist/output/Output.js +0 -27
- package/dist/output/index.d.ts +0 -11
- package/dist/output/index.js +0 -11
- package/dist/output/log/LogRenderer.d.ts +0 -73
- package/dist/output/log/LogRenderer.js +0 -133
- package/dist/output/log/LogsCollector.d.ts +0 -79
- package/dist/output/log/LogsCollector.js +0 -141
- package/dist/output/log/RunLog.d.ts +0 -54
- package/dist/output/log/RunLog.js +0 -152
- package/dist/output/log/console-capture.d.ts +0 -24
- package/dist/output/log/console-capture.js +0 -53
- package/dist/system/fs/streams.d.ts +0 -11
- package/dist/system/fs/streams.js +0 -14
- package/dist/system/process/process.d.ts +0 -35
- package/dist/system/process/process.js +0 -104
- package/dist/utils/serialize.d.ts +0 -2
- package/dist/utils/serialize.js +0 -12
- /package/dist/{orchestration/action → core}/types.js +0 -0
- /package/dist/{orchestration/spin/types.js → foundation/LogEntry.js} +0 -0
|
@@ -1,19 +1,14 @@
|
|
|
1
|
-
import { action } from "../
|
|
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`.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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("
|
|
9
|
+
export const healthcheck = action("healthcheck")
|
|
15
10
|
.emits()
|
|
16
|
-
.run(async ({
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/actions/index.d.ts
CHANGED
|
@@ -1,22 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
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";
|
package/dist/actions/index.js
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
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";
|
package/dist/actions/prompt.d.ts
CHANGED
|
@@ -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**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
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 {};
|
package/dist/actions/prompt.js
CHANGED
|
@@ -1,27 +1,18 @@
|
|
|
1
1
|
import { createInterface } from "node:readline/promises";
|
|
2
|
-
import { action } from "../
|
|
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**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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("
|
|
10
|
+
export const prompt = action("prompt")
|
|
19
11
|
.emits()
|
|
20
12
|
.ref()
|
|
21
|
-
.run(async ({
|
|
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;
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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("../
|
|
16
|
+
export declare const tailLog: import("../index.ts").Action<[target: string, opts?: TailLogOptions | undefined], void, import("../core/types.ts").NoEvents, void>;
|
package/dist/actions/tail-log.js
CHANGED
|
@@ -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 "../
|
|
5
|
+
import { action } from "../core/action.js";
|
|
6
6
|
/**
|
|
7
|
-
* Tails a (possibly not-yet-existing) text
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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("
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
111
|
+
console.log(tail);
|
|
133
112
|
resolveRun();
|
|
134
113
|
};
|
|
135
114
|
if (signal.aborted)
|
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
import type { LogEntry } from "../
|
|
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
|
|
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
|
|
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 "../
|
|
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
|
|
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("
|
|
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
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
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 "../
|
|
4
|
+
import { action } from "../core/action.js";
|
|
5
5
|
/**
|
|
6
|
-
* Watches a directory and emits per-file events
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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("
|
|
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;
|
|
28
|
-
let churned = false;
|
|
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);
|
|
70
|
+
emit("removed", root);
|
|
83
71
|
if (present)
|
|
84
|
-
emit("ready", root);
|
|
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;
|
|
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);
|
|
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
|
|
133
|
+
await tick(false); // final flush before teardown
|
|
149
134
|
for (const w of watchers)
|
|
150
135
|
w.close();
|
|
151
136
|
watchers.length = 0;
|
package/dist/actions/watch.d.ts
CHANGED
|
@@ -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.
|
|
26
|
-
*
|
|
27
|
-
*
|
|
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("../
|
|
25
|
+
export declare const watch: import("../index.ts").Action<[target: string, opts?: WatchOptions | undefined], void, import("../core/types.ts").NoEvents, void>;
|