@webpresso/agent-kit 3.1.28 → 3.1.30
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/catalog/AGENTS.md.tpl +4 -4
- package/catalog/agent/rules/ci-cost-local-first.md +139 -0
- package/catalog/agent/rules/ci-test-perf.md +5 -1
- package/catalog/agent/rules/cmd-execution.md +9 -0
- package/catalog/agent/rules/pre-implementation.md +2 -1
- package/catalog/agent/rules/test-scan-perf.md +80 -0
- package/catalog/agent/rules/workflow-skills-routing.md +4 -2
- package/catalog/agent/skills/fix/SKILL.md +3 -0
- package/catalog/agent/skills/verify/SKILL.md +18 -0
- package/dist/esm/audit/blueprint-pr-coverage.js +68 -7
- package/dist/esm/audit/registry.d.ts +4 -4
- package/dist/esm/audit/registry.js +3 -0
- package/dist/esm/audit/roadmap-links.js +6 -0
- package/dist/esm/audit/test-scan-perf.d.ts +18 -0
- package/dist/esm/audit/test-scan-perf.js +275 -0
- package/dist/esm/blueprint/lifecycle/engine.d.ts +9 -0
- package/dist/esm/blueprint/lifecycle/engine.js +53 -6
- package/dist/esm/blueprint/lifecycle/review-provenance.d.ts +6 -0
- package/dist/esm/blueprint/lifecycle/review-provenance.js +93 -12
- package/dist/esm/blueprint/markdown/helpers.js +6 -2
- package/dist/esm/ci/act-replay.js +110 -7
- package/dist/esm/ci/act-runner.d.ts +4 -0
- package/dist/esm/ci/act-runner.js +85 -26
- package/dist/esm/ci/act-worktree-git.d.ts +11 -0
- package/dist/esm/ci/act-worktree-git.js +219 -0
- package/dist/esm/cli/commands/audit.js +3 -0
- package/dist/esm/cli/commands/blueprint/mutations.js +35 -18
- package/dist/esm/cli/commands/blueprint/router.js +27 -2
- package/dist/esm/cli/commands/ci.d.ts +8 -1
- package/dist/esm/cli/commands/ci.js +80 -21
- package/dist/esm/cli/commands/dash/keymap.js +1 -0
- package/dist/esm/cli/commands/dash/launch-preferences.d.ts +15 -0
- package/dist/esm/cli/commands/dash/launch-preferences.js +46 -17
- package/dist/esm/cli/commands/dash/plan-once-label.d.ts +7 -0
- package/dist/esm/cli/commands/dash/plan-once-label.js +5 -0
- package/dist/esm/cli/commands/dash/plan-once-membership.d.ts +67 -0
- package/dist/esm/cli/commands/dash/plan-once-membership.js +151 -0
- package/dist/esm/cli/commands/dash/plan-once-merge.d.ts +143 -0
- package/dist/esm/cli/commands/dash/plan-once-merge.js +238 -0
- package/dist/esm/cli/commands/dash/plan-once.d.ts +22 -2
- package/dist/esm/cli/commands/dash/plan-once.js +41 -4
- package/dist/esm/cli/commands/dash/tui/attention-hud.d.ts +9 -0
- package/dist/esm/cli/commands/dash/tui/attention-hud.js +9 -0
- package/dist/esm/cli/commands/dash/tui/chat-bridge.d.ts +41 -0
- package/dist/esm/cli/commands/dash/tui/chat-bridge.js +73 -0
- package/dist/esm/cli/commands/dash/tui/chat-mode.d.ts +47 -0
- package/dist/esm/cli/commands/dash/tui/chat-mode.js +98 -0
- package/dist/esm/cli/commands/dash/tui/frame.d.ts +3 -0
- package/dist/esm/cli/commands/dash/tui/frame.js +3 -1
- package/dist/esm/cli/commands/dash/tui/palette.d.ts +1 -1
- package/dist/esm/cli/commands/dash/tui/palette.js +14 -0
- package/dist/esm/cli/commands/dash/tui/runtime.js +239 -4
- package/dist/esm/cli/commands/dash/tui/state.d.ts +41 -0
- package/dist/esm/cli/commands/dash/tui/state.js +257 -4
- package/dist/esm/cli/commands/format.js +0 -2
- package/dist/esm/cli/commands/init/config.d.ts +0 -1
- package/dist/esm/cli/commands/init/config.js +1 -4
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/emitters/claude.d.ts +5 -6
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/emitters/claude.js +6 -13
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/ir.d.ts +0 -2
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/ir.js +0 -2
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/merge.d.ts +6 -3
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/merge.js +39 -26
- package/dist/esm/cli/commands/init/scaffolders/codex-mcp/index.js +61 -23
- package/dist/esm/cli/commands/logs.d.ts +15 -0
- package/dist/esm/cli/commands/logs.js +192 -3
- package/dist/esm/cli/commands/quality-log-store.d.ts +1 -0
- package/dist/esm/cli/commands/quality-log-store.js +187 -51
- package/dist/esm/cli/commands/review.js +5 -8
- package/dist/esm/cli/commands/secrets.d.ts +1 -0
- package/dist/esm/cli/commands/secrets.js +4 -1
- package/dist/esm/cli/direct-provider-launch.js +25 -1
- package/dist/esm/hooks/doctor.d.ts +15 -0
- package/dist/esm/hooks/doctor.js +56 -0
- package/dist/esm/hooks/pretool-guard/validators/forbidden-commands.js +4 -4
- package/dist/esm/hooks/pretool-guard/validators/mcp-redirect.d.ts +0 -1
- package/dist/esm/hooks/pretool-guard/validators/mcp-redirect.js +8 -5
- package/dist/esm/hooks/pretool-guard/validators/worktree-discipline.d.ts +9 -7
- package/dist/esm/hooks/pretool-guard/validators/worktree-discipline.js +126 -14
- package/dist/esm/mcp/tools/_shared/audit-kinds.d.ts +1 -1
- package/dist/esm/mcp/tools/audit.d.ts +2 -1
- package/dist/esm/mcp/tools/audit.js +10 -0
- package/dist/esm/mcp/tools/audits.d.ts +1 -0
- package/dist/esm/mcp/tools/ci-act.js +9 -11
- package/dist/esm/paths/state-root.js +3 -0
- package/dist/esm/review/authority.js +17 -2
- package/dist/esm/review/availability.js +18 -5
- package/dist/esm/review/subject.d.ts +15 -0
- package/dist/esm/review/subject.js +105 -10
- package/dist/esm/runtime/executor.d.ts +1 -0
- package/dist/esm/runtime/executor.js +2 -1
- package/dist/esm/secret-gate/runner.d.ts +1 -0
- package/dist/esm/secret-gate/runner.js +4 -1
- package/dist/esm/worktrees/current.d.ts +1 -0
- package/dist/esm/worktrees/current.js +1 -0
- package/dist/esm/worktrees/project-agent-surfaces.d.ts +1 -0
- package/dist/esm/worktrees/project-agent-surfaces.js +2 -1
- package/package.json +14 -14
|
@@ -23,6 +23,7 @@ import { accessSync, constants, existsSync, mkdirSync, readFileSync, statSync }
|
|
|
23
23
|
import { homedir } from "node:os";
|
|
24
24
|
import { dirname, join } from "node:path";
|
|
25
25
|
import { fileURLToPath } from "node:url";
|
|
26
|
+
import lockfile from "proper-lockfile";
|
|
26
27
|
import { isJsonRecord, isUnsafeRuntimeLauncher, parseJson, parseProjectMcpServers, parseTomlString, } from "#hooks/host-config-contract.js";
|
|
27
28
|
import { appendGlobalCapableVpArgs, resolveGlobalCapableVpCommand, } from "#cli/global-vp.js";
|
|
28
29
|
import { pathCandidates } from "#runtime/command-exists.js";
|
|
@@ -41,6 +42,57 @@ const PLAYWRIGHT_STDIO_MCP_DEFINITION = {
|
|
|
41
42
|
command: "vp",
|
|
42
43
|
args: ["dlx", PLAYWRIGHT_MCP_PACKAGE, ...PLAYWRIGHT_MCP_ARGS],
|
|
43
44
|
};
|
|
45
|
+
const CODEX_CONFIG_LOCK_TIMEOUT_MS = 5_000;
|
|
46
|
+
const CODEX_CONFIG_LOCK_POLL_MS = 25;
|
|
47
|
+
const CODEX_CONFIG_LOCK_STALE_MS = 30_000;
|
|
48
|
+
function sleepSync(ms) {
|
|
49
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
50
|
+
}
|
|
51
|
+
function acquireCodexConfigLock(configPath) {
|
|
52
|
+
mkdirSync(dirname(configPath), { recursive: true });
|
|
53
|
+
const startedAt = Date.now();
|
|
54
|
+
while (true) {
|
|
55
|
+
try {
|
|
56
|
+
return lockfile.lockSync(configPath, {
|
|
57
|
+
realpath: false,
|
|
58
|
+
stale: CODEX_CONFIG_LOCK_STALE_MS,
|
|
59
|
+
update: CODEX_CONFIG_LOCK_STALE_MS / 2,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
if (error.code !== "ELOCKED")
|
|
64
|
+
throw error;
|
|
65
|
+
if (Date.now() - startedAt >= CODEX_CONFIG_LOCK_TIMEOUT_MS) {
|
|
66
|
+
throw new Error(`Timed out waiting for Codex config lock: ${configPath}`, {
|
|
67
|
+
cause: error,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
sleepSync(CODEX_CONFIG_LOCK_POLL_MS);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function withCodexConfigLock(configPath, action) {
|
|
75
|
+
const release = acquireCodexConfigLock(configPath);
|
|
76
|
+
try {
|
|
77
|
+
return action();
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
release();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function readCodexConfig(configPath) {
|
|
84
|
+
return existsSync(configPath) ? readFileSync(configPath, "utf8") : "";
|
|
85
|
+
}
|
|
86
|
+
function updateCodexConfig(configPath, transform) {
|
|
87
|
+
return withCodexConfigLock(configPath, () => {
|
|
88
|
+
const existing = readCodexConfig(configPath);
|
|
89
|
+
const next = transform(existing);
|
|
90
|
+
if (next === existing)
|
|
91
|
+
return "unchanged";
|
|
92
|
+
writeFileAtomic(configPath, next, "utf8");
|
|
93
|
+
return "written";
|
|
94
|
+
});
|
|
95
|
+
}
|
|
44
96
|
function tomlString(value) {
|
|
45
97
|
return JSON.stringify(value);
|
|
46
98
|
}
|
|
@@ -271,13 +323,8 @@ export function ensurePinnedCodexMcp(input, definition) {
|
|
|
271
323
|
reason: provisioned.reason,
|
|
272
324
|
};
|
|
273
325
|
}
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
if (next === existing)
|
|
277
|
-
return { kind: "unchanged", path: configPath };
|
|
278
|
-
mkdirSync(dirname(configPath), { recursive: true });
|
|
279
|
-
writeFileAtomic(configPath, next, "utf8");
|
|
280
|
-
return { kind: "written", path: configPath };
|
|
326
|
+
const result = updateCodexConfig(configPath, (existing) => upsertCommandMcpServer(existing, provisioned.definition));
|
|
327
|
+
return { kind: result === "written" ? "written" : "unchanged", path: configPath };
|
|
281
328
|
}
|
|
282
329
|
function removeUnsafePackageManagerMcpBlocks(raw) {
|
|
283
330
|
const lines = raw.split(/\r?\n/u);
|
|
@@ -329,15 +376,11 @@ function removeUnsafePackageManagerMcpBlocks(raw) {
|
|
|
329
376
|
export function ensureCodexProjectMcpServers(input) {
|
|
330
377
|
const configPath = input.configPath ?? defaultConfigPath();
|
|
331
378
|
const sourcePath = input.sourcePath ?? join(input.repoRoot, ".mcp.json");
|
|
332
|
-
const existing = existsSync(configPath) ? readFileSync(configPath, "utf8") : "";
|
|
333
|
-
const withoutUnsafeLaunchers = removeUnsafePackageManagerMcpBlocks(existing);
|
|
334
|
-
const cleanedUnsafeLaunchers = withoutUnsafeLaunchers !== existing;
|
|
335
379
|
const persistSafetyCleanup = () => {
|
|
336
|
-
if (
|
|
380
|
+
if (input.options.dryRun)
|
|
337
381
|
return undefined;
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
return true;
|
|
382
|
+
const result = updateCodexConfig(configPath, removeUnsafePackageManagerMcpBlocks);
|
|
383
|
+
return result === "written" ? true : undefined;
|
|
341
384
|
};
|
|
342
385
|
const parsed = existsSync(sourcePath)
|
|
343
386
|
? parseProjectMcpServers(readFileSync(sourcePath, "utf8"))
|
|
@@ -368,12 +411,10 @@ export function ensureCodexProjectMcpServers(input) {
|
|
|
368
411
|
}
|
|
369
412
|
provisioned.push(result.definition);
|
|
370
413
|
}
|
|
371
|
-
const
|
|
372
|
-
if (
|
|
414
|
+
const result = updateCodexConfig(configPath, (existing) => provisioned.reduce(upsertCommandMcpServer, removeUnsafePackageManagerMcpBlocks(existing)));
|
|
415
|
+
if (result === "unchanged") {
|
|
373
416
|
return { kind: "codex-project-mcp-unchanged", path: configPath, servers };
|
|
374
417
|
}
|
|
375
|
-
mkdirSync(dirname(configPath), { recursive: true });
|
|
376
|
-
writeFileAtomic(configPath, next, "utf8");
|
|
377
418
|
return { kind: "codex-project-mcp-written", path: configPath, servers };
|
|
378
419
|
}
|
|
379
420
|
function claudePlaywrightServer() {
|
|
@@ -561,12 +602,9 @@ export function ensureCodexWebpressoMcp(input) {
|
|
|
561
602
|
const checked = resolution?.checked ?? [];
|
|
562
603
|
return { kind: "codex-webpresso-mcp-not-installed", path: configPath, checked };
|
|
563
604
|
}
|
|
564
|
-
const
|
|
565
|
-
|
|
566
|
-
if (next === existing) {
|
|
605
|
+
const result = updateCodexConfig(configPath, (existing) => upsertWebpressoMcpServer(existing, entryPath));
|
|
606
|
+
if (result === "unchanged") {
|
|
567
607
|
return { kind: "codex-webpresso-mcp-unchanged", path: configPath, entryPath };
|
|
568
608
|
}
|
|
569
|
-
mkdirSync(dirname(configPath), { recursive: true });
|
|
570
|
-
writeFileAtomic(configPath, next, "utf8");
|
|
571
609
|
return { kind: "codex-webpresso-mcp-written", path: configPath, entryPath };
|
|
572
610
|
}
|
|
@@ -1,3 +1,18 @@
|
|
|
1
1
|
import type { CAC } from "cac";
|
|
2
|
+
interface LogsCommandOptions {
|
|
3
|
+
readonly follow?: boolean;
|
|
4
|
+
readonly tail?: string | number | boolean;
|
|
5
|
+
readonly watch?: boolean;
|
|
6
|
+
}
|
|
7
|
+
interface FollowLogFileOptions {
|
|
8
|
+
readonly pollIntervalMs?: number;
|
|
9
|
+
readonly signal?: AbortSignal;
|
|
10
|
+
readonly write?: (chunk: string) => void;
|
|
11
|
+
}
|
|
2
12
|
export declare const LOGS_COMMAND_HELP: string;
|
|
3
13
|
export declare function registerLogsCommand(cli: CAC): void;
|
|
14
|
+
export declare function parseTailLineCount(value: LogsCommandOptions["tail"]): number | null;
|
|
15
|
+
export declare function tailLogContent(content: string, lineCount: number): string;
|
|
16
|
+
export declare function readTailLogContent(logPath: string, lineCount: number, chunkSize?: number): string;
|
|
17
|
+
export declare function followLogFile(logPath: string, startOffset: number, options?: FollowLogFileOptions): Promise<number>;
|
|
18
|
+
export {};
|
|
@@ -1,16 +1,22 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
1
|
+
import { closeSync, createReadStream, openSync, readFileSync, readSync, statSync, unwatchFile, watchFile, } from "node:fs";
|
|
2
2
|
import { CLI_LOG_COMMANDS, isCliLogCommandName, readCliLogEntry } from "./quality-log-store.js";
|
|
3
|
+
const TAIL_READ_CHUNK_BYTES = 64 * 1024;
|
|
3
4
|
export const LOGS_COMMAND_HELP = [
|
|
4
5
|
"Print persisted raw output for a recent summary-first quality command run.",
|
|
5
6
|
"",
|
|
6
7
|
"Examples:",
|
|
7
8
|
" wp logs test",
|
|
8
9
|
" wp logs qa 2",
|
|
10
|
+
" wp logs test --tail 200",
|
|
11
|
+
" wp logs test --tail 200 --follow",
|
|
9
12
|
].join("\n");
|
|
10
13
|
export function registerLogsCommand(cli) {
|
|
11
14
|
cli
|
|
12
15
|
.command("logs <command> [n]", LOGS_COMMAND_HELP)
|
|
13
|
-
.
|
|
16
|
+
.option("--follow", "Keep printing appended output from the selected log")
|
|
17
|
+
.option("--tail <lines>", "Print only the last N lines from the selected log")
|
|
18
|
+
.option("--watch", "Alias for --follow")
|
|
19
|
+
.action((command, n, options = {}) => {
|
|
14
20
|
if (!isCliLogCommandName(command)) {
|
|
15
21
|
console.error(`Unknown logs command: ${command}. Expected one of: ${CLI_LOG_COMMANDS.join(", ")}`);
|
|
16
22
|
return 1;
|
|
@@ -20,12 +26,195 @@ export function registerLogsCommand(cli) {
|
|
|
20
26
|
console.error("Usage: wp logs <command> [n]\n`n` must be an integer in the range 1..10.");
|
|
21
27
|
return 1;
|
|
22
28
|
}
|
|
29
|
+
const tailLines = parseTailLineCount(options.tail);
|
|
30
|
+
if (tailLines !== null && tailLines < 1) {
|
|
31
|
+
console.error("Usage: wp logs <command> [n] [--tail <lines>]\n`tail` must be a positive integer.");
|
|
32
|
+
return 1;
|
|
33
|
+
}
|
|
23
34
|
const entry = readCliLogEntry(command, ordinal);
|
|
24
35
|
if (!entry) {
|
|
25
36
|
console.log(`No logs yet for ${command}.`);
|
|
26
37
|
return 0;
|
|
27
38
|
}
|
|
28
|
-
|
|
39
|
+
const snapshot = tailLines === null
|
|
40
|
+
? readFullLogSnapshot(entry.logPath)
|
|
41
|
+
: readTailLogSnapshot(entry.logPath, tailLines);
|
|
42
|
+
const printed = snapshot.content;
|
|
43
|
+
process.stdout.write(printed);
|
|
44
|
+
if (options.follow === true || options.watch === true) {
|
|
45
|
+
return followLogFile(entry.logPath, snapshot.offset);
|
|
46
|
+
}
|
|
29
47
|
return 0;
|
|
30
48
|
});
|
|
31
49
|
}
|
|
50
|
+
export function parseTailLineCount(value) {
|
|
51
|
+
if (value === undefined || value === null || value === false)
|
|
52
|
+
return null;
|
|
53
|
+
if (value === true)
|
|
54
|
+
return 0;
|
|
55
|
+
const count = typeof value === "number" ? value : Number(value);
|
|
56
|
+
if (!Number.isInteger(count) || count < 1)
|
|
57
|
+
return 0;
|
|
58
|
+
return count;
|
|
59
|
+
}
|
|
60
|
+
export function tailLogContent(content, lineCount) {
|
|
61
|
+
const lines = content.split("\n");
|
|
62
|
+
const trailingNewline = lines.at(-1) === "";
|
|
63
|
+
const body = trailingNewline ? lines.slice(0, -1) : lines;
|
|
64
|
+
const selected = body.slice(-lineCount);
|
|
65
|
+
if (selected.length === 0)
|
|
66
|
+
return "";
|
|
67
|
+
return `${selected.join("\n")}${trailingNewline ? "\n" : ""}`;
|
|
68
|
+
}
|
|
69
|
+
export function readTailLogContent(logPath, lineCount, chunkSize = TAIL_READ_CHUNK_BYTES) {
|
|
70
|
+
return readTailLogSnapshot(logPath, lineCount, chunkSize).content;
|
|
71
|
+
}
|
|
72
|
+
function readFullLogSnapshot(logPath) {
|
|
73
|
+
const content = readFileSync(logPath, "utf8");
|
|
74
|
+
return { content, offset: Buffer.byteLength(content) };
|
|
75
|
+
}
|
|
76
|
+
function readTailLogSnapshot(logPath, lineCount, chunkSize = TAIL_READ_CHUNK_BYTES) {
|
|
77
|
+
const fileSize = statSync(logPath).size;
|
|
78
|
+
if (fileSize === 0 || lineCount < 1)
|
|
79
|
+
return { content: "", offset: fileSize };
|
|
80
|
+
const fd = openSync(logPath, "r");
|
|
81
|
+
try {
|
|
82
|
+
const hasTrailingNewline = readLastByte(fd, fileSize) === 0x0a;
|
|
83
|
+
const targetNewlines = hasTrailingNewline ? lineCount + 1 : lineCount;
|
|
84
|
+
const chunks = [];
|
|
85
|
+
let position = fileSize;
|
|
86
|
+
let newlineCount = 0;
|
|
87
|
+
while (position > 0 && newlineCount < targetNewlines) {
|
|
88
|
+
const bytesToRead = Math.min(chunkSize, position);
|
|
89
|
+
position -= bytesToRead;
|
|
90
|
+
const buffer = Buffer.allocUnsafe(bytesToRead);
|
|
91
|
+
const bytesRead = readSync(fd, buffer, 0, bytesToRead, position);
|
|
92
|
+
const chunk = buffer.subarray(0, bytesRead);
|
|
93
|
+
chunks.unshift(chunk);
|
|
94
|
+
newlineCount += countNewlines(chunk);
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
content: tailLogContent(Buffer.concat(chunks).toString("utf8"), lineCount),
|
|
98
|
+
offset: fileSize,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
closeSync(fd);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function readLastByte(fd, fileSize) {
|
|
106
|
+
if (fileSize < 1)
|
|
107
|
+
return null;
|
|
108
|
+
const buffer = Buffer.allocUnsafe(1);
|
|
109
|
+
const bytesRead = readSync(fd, buffer, 0, 1, fileSize - 1);
|
|
110
|
+
return bytesRead === 1 ? buffer.readUInt8(0) : null;
|
|
111
|
+
}
|
|
112
|
+
function countNewlines(buffer) {
|
|
113
|
+
let count = 0;
|
|
114
|
+
for (const byte of buffer) {
|
|
115
|
+
if (byte === 0x0a)
|
|
116
|
+
count++;
|
|
117
|
+
}
|
|
118
|
+
return count;
|
|
119
|
+
}
|
|
120
|
+
export function followLogFile(logPath, startOffset, options = {}) {
|
|
121
|
+
if (options.signal?.aborted === true)
|
|
122
|
+
return Promise.resolve(0);
|
|
123
|
+
let offset = startOffset;
|
|
124
|
+
let pendingSize = startOffset;
|
|
125
|
+
let reading = false;
|
|
126
|
+
let readGeneration = 0;
|
|
127
|
+
let settled = false;
|
|
128
|
+
let activeStream = null;
|
|
129
|
+
let poll = null;
|
|
130
|
+
const write = options.write ??
|
|
131
|
+
((chunk) => {
|
|
132
|
+
process.stdout.write(chunk);
|
|
133
|
+
});
|
|
134
|
+
let resolvePromise = null;
|
|
135
|
+
const settle = () => {
|
|
136
|
+
if (settled)
|
|
137
|
+
return;
|
|
138
|
+
settled = true;
|
|
139
|
+
unwatchFile(logPath, onChange);
|
|
140
|
+
activeStream?.destroy();
|
|
141
|
+
if (poll !== null)
|
|
142
|
+
clearInterval(poll);
|
|
143
|
+
process.off("SIGINT", onSignal);
|
|
144
|
+
process.off("SIGTERM", onSignal);
|
|
145
|
+
options.signal?.removeEventListener("abort", onSignal);
|
|
146
|
+
resolvePromise?.(0);
|
|
147
|
+
};
|
|
148
|
+
const onSignal = () => {
|
|
149
|
+
settle();
|
|
150
|
+
};
|
|
151
|
+
const drain = () => {
|
|
152
|
+
if (settled || reading || pendingSize <= offset)
|
|
153
|
+
return;
|
|
154
|
+
reading = true;
|
|
155
|
+
const nextOffset = pendingSize;
|
|
156
|
+
const generation = readGeneration;
|
|
157
|
+
const stream = createReadStream(logPath, {
|
|
158
|
+
encoding: "utf8",
|
|
159
|
+
start: offset,
|
|
160
|
+
end: nextOffset - 1,
|
|
161
|
+
});
|
|
162
|
+
activeStream = stream;
|
|
163
|
+
stream.on("data", (chunk) => {
|
|
164
|
+
if (settled)
|
|
165
|
+
return;
|
|
166
|
+
write(String(chunk));
|
|
167
|
+
});
|
|
168
|
+
stream.on("close", () => {
|
|
169
|
+
if (activeStream === stream)
|
|
170
|
+
activeStream = null;
|
|
171
|
+
if (settled || generation !== readGeneration)
|
|
172
|
+
return;
|
|
173
|
+
offset = nextOffset;
|
|
174
|
+
reading = false;
|
|
175
|
+
drain();
|
|
176
|
+
});
|
|
177
|
+
stream.on("error", onSignal);
|
|
178
|
+
};
|
|
179
|
+
const onChange = (current) => {
|
|
180
|
+
if (settled)
|
|
181
|
+
return;
|
|
182
|
+
if (current.size < pendingSize) {
|
|
183
|
+
readGeneration++;
|
|
184
|
+
activeStream?.destroy();
|
|
185
|
+
activeStream = null;
|
|
186
|
+
reading = false;
|
|
187
|
+
offset = 0;
|
|
188
|
+
pendingSize = current.size;
|
|
189
|
+
drain();
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (current.size <= pendingSize)
|
|
193
|
+
return;
|
|
194
|
+
pendingSize = current.size;
|
|
195
|
+
drain();
|
|
196
|
+
};
|
|
197
|
+
return new Promise((resolve) => {
|
|
198
|
+
resolvePromise = resolve;
|
|
199
|
+
process.once("SIGINT", onSignal);
|
|
200
|
+
process.once("SIGTERM", onSignal);
|
|
201
|
+
options.signal?.addEventListener("abort", onSignal, { once: true });
|
|
202
|
+
watchFile(logPath, { interval: options.pollIntervalMs ?? 250 }, onChange);
|
|
203
|
+
poll = setInterval(() => {
|
|
204
|
+
if (settled)
|
|
205
|
+
return;
|
|
206
|
+
try {
|
|
207
|
+
onChange(statSync(logPath));
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
if (isMissingFileError(error))
|
|
211
|
+
return;
|
|
212
|
+
onSignal();
|
|
213
|
+
}
|
|
214
|
+
}, options.pollIntervalMs ?? 250);
|
|
215
|
+
onChange(statSync(logPath));
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
function isMissingFileError(error) {
|
|
219
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
220
|
+
}
|