@magnusekdahl/parallix 1.2.0 → 1.3.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/index.js +190 -185
- package/lib/agents/agents.js +737 -838
- package/lib/agents/agents.ts +930 -0
- package/lib/agents/claude-telemetry.js +149 -154
- package/lib/agents/claude-telemetry.ts +233 -0
- package/lib/agents/claude.js +121 -131
- package/lib/agents/claude.ts +156 -0
- package/lib/agents/codex-telemetry.js +176 -164
- package/lib/agents/codex-telemetry.ts +205 -0
- package/lib/agents/codex.js +168 -201
- package/lib/agents/codex.ts +236 -0
- package/lib/agents/limit-hit.js +216 -226
- package/lib/agents/limit-hit.ts +262 -0
- package/lib/agents/mistral-telemetry.js +7 -10
- package/lib/agents/mistral-telemetry.ts +44 -0
- package/lib/agents/mistral.js +40 -58
- package/lib/agents/mistral.ts +84 -0
- package/lib/agents/opencode-export.js +126 -127
- package/lib/agents/opencode-export.ts +160 -0
- package/lib/agents/opencode-telemetry.js +282 -261
- package/lib/agents/opencode-telemetry.ts +350 -0
- package/lib/agents/opencode.js +263 -305
- package/lib/agents/opencode.ts +370 -0
- package/lib/agents/stage-telemetry.js +23 -24
- package/lib/agents/stage-telemetry.ts +47 -0
- package/lib/commands/active.js +502 -579
- package/lib/commands/active.ts +665 -0
- package/lib/commands/checkpoint.js +98 -74
- package/lib/commands/checkpoint.ts +79 -0
- package/lib/commands/config.js +61 -31
- package/lib/commands/config.ts +47 -0
- package/lib/commands/coverage-gate.js +271 -246
- package/lib/commands/coverage-gate.ts +362 -0
- package/lib/commands/diff.js +140 -119
- package/lib/commands/diff.ts +125 -0
- package/lib/commands/draft.js +770 -923
- package/lib/commands/draft.ts +1021 -0
- package/lib/commands/handoff.js +588 -608
- package/lib/commands/handoff.ts +694 -0
- package/lib/commands/integrate.js +1332 -1463
- package/lib/commands/integrate.ts +1688 -0
- package/lib/commands/mission-start.js +279 -237
- package/lib/commands/mission-start.ts +263 -0
- package/lib/commands/rebase.js +606 -582
- package/lib/commands/rebase.ts +635 -0
- package/lib/commands/repair-handoff.js +181 -179
- package/lib/commands/repair-handoff.ts +228 -0
- package/lib/commands/resolve-conflict.js +121 -102
- package/lib/commands/resolve-conflict.ts +112 -0
- package/lib/commands/review.js +3 -4
- package/lib/commands/review.ts +14 -0
- package/lib/commands/setup-review.js +4 -5
- package/lib/commands/setup-review.ts +14 -0
- package/lib/commands/setup.js +3 -3
- package/lib/commands/setup.ts +2 -0
- package/lib/commands/stats-backfill.js +394 -359
- package/lib/commands/stats-backfill.ts +418 -0
- package/lib/commands/stats.js +1424 -1665
- package/lib/commands/stats.ts +2186 -0
- package/lib/commands/status.js +219 -200
- package/lib/commands/status.ts +219 -0
- package/lib/commands/verify.js +3 -1
- package/lib/commands/verify.ts +2 -0
- package/lib/core/fmt.js +140 -211
- package/lib/core/fmt.ts +189 -0
- package/lib/core/git.js +121 -102
- package/lib/core/git.ts +146 -0
- package/lib/core/gitignore.js +59 -106
- package/lib/core/gitignore.ts +103 -0
- package/lib/core/mission-utils.js +833 -875
- package/lib/core/mission-utils.ts +998 -0
- package/lib/core/nels.js +112 -127
- package/lib/core/nels.ts +189 -0
- package/lib/core/persistent-data-migration.js +247 -205
- package/lib/core/persistent-data-migration.ts +255 -0
- package/lib/core/product-config.js +400 -476
- package/lib/core/product-config.ts +518 -0
- package/lib/core/runtime-matrix.js +41 -87
- package/lib/core/runtime-matrix.ts +84 -0
- package/lib/core/spawn-tee.js +152 -192
- package/lib/core/spawn-tee.ts +202 -0
- package/lib/core/state-map.js +92 -92
- package/lib/core/state-map.ts +115 -0
- package/lib/core/storage.js +101 -129
- package/lib/core/storage.ts +182 -0
- package/lib/core/subagent-limit.js +15 -16
- package/lib/core/subagent-limit.ts +25 -0
- package/lib/core/verification.js +148 -138
- package/lib/core/verification.ts +200 -0
- package/lib/index.js +142 -59
- package/lib/index.ts +134 -0
- package/lib/review/rebase.js +142 -145
- package/lib/review/rebase.ts +188 -0
- package/lib/review/review-adapter.js +99 -87
- package/lib/review/review-adapter.ts +198 -0
- package/lib/review/review-artifacts.js +486 -629
- package/lib/review/review-artifacts.ts +622 -0
- package/lib/review/review-commands.js +1220 -1373
- package/lib/review/review-commands.ts +1579 -0
- package/lib/review/review-events.js +544 -886
- package/lib/review/review-events.ts +954 -0
- package/lib/review/review-loop.js +945 -1020
- package/lib/review/review-loop.ts +1174 -0
- package/lib/review/review-polling.js +86 -115
- package/lib/review/review-polling.ts +194 -0
- package/lib/review/review-prompts.js +154 -189
- package/lib/review/review-prompts.ts +182 -0
- package/lib/review/review-state.js +246 -225
- package/lib/review/review-state.ts +353 -0
- package/lib/review/review.js +63 -84
- package/lib/review/review.ts +138 -0
- package/lib/tools/backlog.js +694 -680
- package/lib/tools/backlog.ts +835 -0
- package/lib/tools/forgejo.js +1270 -1405
- package/lib/tools/forgejo.ts +1793 -0
- package/lib/tools/gatekeeper.js +106 -96
- package/lib/tools/gatekeeper.ts +124 -0
- package/lib/tools/redgreen.js +200 -181
- package/lib/tools/redgreen.ts +221 -0
- package/lib/tools/sessions.js +58 -52
- package/lib/tools/sessions.ts +81 -0
- package/lib/tools/setup-review.js +902 -975
- package/lib/tools/setup-review.ts +1152 -0
- package/package.json +7 -5
- package/prompts/draft.md +1 -0
- package/prompts/portfolio.md +1 -1
- package/px.js +226 -210
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { eligibleAgentsForStep, workflowLauncherStatus } from '../agents/agents.js';
|
|
4
|
+
|
|
5
|
+
const CONFIG_PATH = path.join(__dirname, '..', '..', 'config', 'agents.json');
|
|
6
|
+
|
|
7
|
+
interface LauncherStatusResult {
|
|
8
|
+
supported: boolean;
|
|
9
|
+
detail: string;
|
|
10
|
+
health?: string;
|
|
11
|
+
reason?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface BuildMatrixOptions {
|
|
15
|
+
step?: string;
|
|
16
|
+
eligibleAgentsForStepFn?: typeof eligibleAgentsForStep;
|
|
17
|
+
workflowLauncherStatusFn?: typeof workflowLauncherStatus;
|
|
18
|
+
configPath?: string;
|
|
19
|
+
existsSyncFn?: typeof fs.existsSync;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function launcherStatus(agent: string, options: { workflowLauncherStatusFn?: typeof workflowLauncherStatus } = {}): LauncherStatusResult {
|
|
23
|
+
const { workflowLauncherStatusFn = workflowLauncherStatus } = options;
|
|
24
|
+
return workflowLauncherStatusFn(agent);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function buildAutonomousReviewMatrix(options: BuildMatrixOptions = {}): { step: string; agents: string[]; configPath: string; configPresent: boolean; launchers: Record<string, LauncherStatusResult> } {
|
|
28
|
+
const {
|
|
29
|
+
step = 'review',
|
|
30
|
+
eligibleAgentsForStepFn = eligibleAgentsForStep,
|
|
31
|
+
workflowLauncherStatusFn = workflowLauncherStatus,
|
|
32
|
+
configPath = CONFIG_PATH,
|
|
33
|
+
existsSyncFn = fs.existsSync
|
|
34
|
+
} = options;
|
|
35
|
+
|
|
36
|
+
const agents = eligibleAgentsForStepFn(step);
|
|
37
|
+
const launchers: Record<string, LauncherStatusResult> = Object.fromEntries(
|
|
38
|
+
agents.map((agent: string) => [agent, workflowLauncherStatusFn(agent)])
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
step,
|
|
43
|
+
agents,
|
|
44
|
+
configPath,
|
|
45
|
+
configPresent: existsSyncFn(configPath),
|
|
46
|
+
launchers
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function formatMatrixSummary(matrix: { step: string; agents: string[]; configPath: string; configPresent: boolean; launchers: Record<string, LauncherStatusResult> }): string[] {
|
|
51
|
+
const lines: string[] = [];
|
|
52
|
+
|
|
53
|
+
lines.push(`Agent eligibility config: ${matrix.configPresent ? 'present' : 'missing'} (${matrix.configPath})`);
|
|
54
|
+
lines.push(`Launcher support matrix (step: ${matrix.step}):`);
|
|
55
|
+
for (const agent of matrix.agents) {
|
|
56
|
+
const launcher = matrix.launchers[agent];
|
|
57
|
+
const status = launcher.supported ? 'supported' : 'blocked';
|
|
58
|
+
const health = launcher.health ? `, ${launcher.health}` : '';
|
|
59
|
+
const reason = launcher.reason ? `; ${launcher.reason}` : '';
|
|
60
|
+
lines.push(` - ${agent}: ${status} (${launcher.detail}${health}${reason})`);
|
|
61
|
+
}
|
|
62
|
+
lines.push(
|
|
63
|
+
'Reviewer is chosen at runtime from the eligible-and-supported pool, ' +
|
|
64
|
+
'excluding the implementer (config-driven via agents.json; no hardcoded routing).'
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
return lines;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface RunnableDiffFamilyOptions {
|
|
71
|
+
step?: string;
|
|
72
|
+
eligibleAgentsForStepFn?: typeof eligibleAgentsForStep;
|
|
73
|
+
workflowLauncherStatusFn?: typeof workflowLauncherStatus;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function runnableDifferentFamilyExists(implementer: string, options: RunnableDiffFamilyOptions = {}): boolean {
|
|
77
|
+
const {
|
|
78
|
+
step = 'review',
|
|
79
|
+
eligibleAgentsForStepFn = eligibleAgentsForStep,
|
|
80
|
+
workflowLauncherStatusFn = workflowLauncherStatus
|
|
81
|
+
} = options;
|
|
82
|
+
const agents = eligibleAgentsForStepFn(step);
|
|
83
|
+
return agents.some((a: string) => a !== implementer && workflowLauncherStatusFn(a).supported);
|
|
84
|
+
}
|
package/lib/core/spawn-tee.js
CHANGED
|
@@ -1,200 +1,160 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
// context land in the buffer even if the agent printed a lot before exiting.
|
|
12
|
-
const DEFAULT_MAX_TAIL_BYTES = 64 * 1024;
|
|
13
|
-
|
|
14
|
-
/** @typedef {Buffer|string} ChunkType */
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.TailBuffer = exports.DEFAULT_MAX_TAIL_BYTES = void 0;
|
|
7
|
+
exports.spawnAndTee = spawnAndTee;
|
|
8
|
+
const node_child_process_1 = __importDefault(require("node:child_process"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
exports.DEFAULT_MAX_TAIL_BYTES = 64 * 1024;
|
|
15
11
|
class TailBuffer {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
12
|
+
maxBytes;
|
|
13
|
+
chunks;
|
|
14
|
+
size;
|
|
15
|
+
constructor(maxBytes) {
|
|
16
|
+
this.maxBytes = maxBytes;
|
|
17
|
+
this.chunks = [];
|
|
18
|
+
this.size = 0;
|
|
19
|
+
}
|
|
20
|
+
push(chunk) {
|
|
21
|
+
this.chunks.push(chunk);
|
|
22
|
+
this.size += chunk.length;
|
|
23
|
+
while (this.size > this.maxBytes && this.chunks.length > 0) {
|
|
24
|
+
const head = this.chunks[0];
|
|
25
|
+
const overflow = this.size - this.maxBytes;
|
|
26
|
+
if (head.length <= overflow) {
|
|
27
|
+
this.chunks.shift();
|
|
28
|
+
this.size -= head.length;
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
if (Buffer.isBuffer(head)) {
|
|
32
|
+
this.chunks[0] = head.subarray(overflow);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
this.chunks.shift();
|
|
36
|
+
this.size -= head.length;
|
|
37
|
+
}
|
|
38
|
+
this.size -= overflow;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
toString() {
|
|
43
|
+
if (this.chunks.length === 0) {
|
|
44
|
+
return '';
|
|
42
45
|
}
|
|
43
|
-
this.
|
|
44
|
-
}
|
|
46
|
+
return Buffer.concat(this.chunks).toString('utf8');
|
|
45
47
|
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
toString() {
|
|
49
|
-
if (this.chunks.length === 0) {return '';}
|
|
50
|
-
return Buffer.concat(/** @type {Buffer[]} */(this.chunks)).toString('utf8');
|
|
51
|
-
}
|
|
52
48
|
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Spawn a child process and tee its stdout/stderr to the parent's streams
|
|
56
|
-
* while keeping a bounded tail of the output in memory. Returns a Promise
|
|
57
|
-
* resolving with a result shaped like `spawnSync` (status, signal, stdout,
|
|
58
|
-
* stderr, error) but with stdout/stderr as utf-8 strings populated even
|
|
59
|
-
* when stdio:'inherit' is requested. Forces stdio to ['inherit', 'pipe',
|
|
60
|
-
* 'pipe'] internally so the parent's stdin is preserved while child output
|
|
61
|
-
* remains capturable.
|
|
62
|
-
*
|
|
63
|
-
* Pure I/O wrapper: no policy, no retry, no agent knowledge.
|
|
64
|
-
*/
|
|
65
|
-
/**
|
|
66
|
-
* @param {string} command
|
|
67
|
-
* @param {string[]} args
|
|
68
|
-
* @param {{stdoutSink?: NodeJS.WriteStream, stderrSink?: NodeJS.WriteStream, maxTailBytes?: number, noOutputWatchdog?: {onNoOutput?: Function, initialDelayMs?: number, intervalMs?: number}, cwd?: string, env?: Record<string, string>, [key: string]: any}} [options]
|
|
69
|
-
*/
|
|
49
|
+
exports.TailBuffer = TailBuffer;
|
|
70
50
|
function spawnAndTee(command, args, options = {}) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
51
|
+
const { stdoutSink = process.stdout, stderrSink = process.stderr, maxTailBytes = exports.DEFAULT_MAX_TAIL_BYTES, noOutputWatchdog = null, ...spawnOptions } = options;
|
|
52
|
+
return new Promise((resolve) => {
|
|
53
|
+
const stdoutTail = new TailBuffer(maxTailBytes);
|
|
54
|
+
const stderrTail = new TailBuffer(maxTailBytes);
|
|
55
|
+
let settled = false;
|
|
56
|
+
let sawOutput = false;
|
|
57
|
+
let watchdogTimer = null;
|
|
58
|
+
const startTime = Date.now();
|
|
59
|
+
const resolvedCwd = node_path_1.default.resolve(spawnOptions.cwd || process.cwd());
|
|
60
|
+
const env = {
|
|
61
|
+
...process.env,
|
|
62
|
+
...(spawnOptions.env || {}),
|
|
63
|
+
PWD: resolvedCwd
|
|
64
|
+
};
|
|
65
|
+
let child;
|
|
66
|
+
try {
|
|
67
|
+
child = node_child_process_1.default.spawn(command, args, {
|
|
68
|
+
...spawnOptions,
|
|
69
|
+
env,
|
|
70
|
+
stdio: ['inherit', 'pipe', 'pipe']
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
resolve({
|
|
75
|
+
status: null, signal: null, stdout: '', stderr: '', error: err,
|
|
76
|
+
startedAt: new Date(startTime).toISOString(), endedAt: new Date().toISOString()
|
|
77
|
+
});
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const clearWatchdog = () => {
|
|
81
|
+
if (watchdogTimer) {
|
|
82
|
+
clearTimeout(watchdogTimer);
|
|
83
|
+
watchdogTimer = null;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
const finish = (payload) => {
|
|
87
|
+
if (settled) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
settled = true;
|
|
91
|
+
clearWatchdog();
|
|
92
|
+
payload.startedAt = new Date(startTime).toISOString();
|
|
93
|
+
payload.endedAt = new Date().toISOString();
|
|
94
|
+
resolve(payload);
|
|
95
|
+
};
|
|
96
|
+
const scheduleWatchdog = (delayMs) => {
|
|
97
|
+
if (!noOutputWatchdog || typeof noOutputWatchdog.onNoOutput !== 'function') {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const delay = Number.isFinite(delayMs) && delayMs >= 0 ? delayMs : 0;
|
|
101
|
+
watchdogTimer = setTimeout(() => {
|
|
102
|
+
watchdogTimer = null;
|
|
103
|
+
if (settled || sawOutput) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (typeof noOutputWatchdog.onNoOutput === 'function') {
|
|
107
|
+
noOutputWatchdog.onNoOutput({
|
|
108
|
+
command,
|
|
109
|
+
args,
|
|
110
|
+
pid: child.pid,
|
|
111
|
+
elapsedMs: Date.now() - startTime
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
scheduleWatchdog(noOutputWatchdog.intervalMs ?? 0);
|
|
115
|
+
}, delay);
|
|
116
|
+
if (typeof watchdogTimer.unref === 'function') {
|
|
117
|
+
watchdogTimer.unref();
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
const noteOutput = () => {
|
|
121
|
+
sawOutput = true;
|
|
122
|
+
clearWatchdog();
|
|
123
|
+
};
|
|
124
|
+
if (noOutputWatchdog) {
|
|
125
|
+
scheduleWatchdog(noOutputWatchdog.initialDelayMs ?? 0);
|
|
126
|
+
}
|
|
127
|
+
child.stdout?.on('data', (chunk) => {
|
|
128
|
+
noteOutput();
|
|
129
|
+
stdoutTail.push(chunk);
|
|
130
|
+
if (stdoutSink && typeof stdoutSink.write === 'function') {
|
|
131
|
+
stdoutSink.write(chunk);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
child.stderr?.on('data', (chunk) => {
|
|
135
|
+
noteOutput();
|
|
136
|
+
stderrTail.push(chunk);
|
|
137
|
+
if (stderrSink && typeof stderrSink.write === 'function') {
|
|
138
|
+
stderrSink.write(chunk);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
child.on('error', (err) => {
|
|
142
|
+
finish({
|
|
143
|
+
status: null,
|
|
144
|
+
signal: null,
|
|
145
|
+
stdout: stdoutTail.toString(),
|
|
146
|
+
stderr: stderrTail.toString(),
|
|
147
|
+
error: err,
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
child.on('close', (code, signal) => {
|
|
151
|
+
finish({
|
|
152
|
+
status: code,
|
|
153
|
+
signal,
|
|
154
|
+
stdout: stdoutTail.toString(),
|
|
155
|
+
stderr: stderrTail.toString(),
|
|
156
|
+
error: null,
|
|
157
|
+
});
|
|
141
158
|
});
|
|
142
|
-
}
|
|
143
|
-
scheduleWatchdog(noOutputWatchdog.intervalMs ?? 0);
|
|
144
|
-
}, delay);
|
|
145
|
-
if (typeof watchdogTimer.unref === 'function') {
|
|
146
|
-
watchdogTimer.unref();
|
|
147
|
-
}
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
const noteOutput = () => {
|
|
151
|
-
sawOutput = true;
|
|
152
|
-
clearWatchdog();
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
if (noOutputWatchdog) {
|
|
156
|
-
scheduleWatchdog(noOutputWatchdog.initialDelayMs ?? 0);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
child.stdout.on('data', (chunk) => {
|
|
160
|
-
noteOutput();
|
|
161
|
-
stdoutTail.push(chunk);
|
|
162
|
-
if (stdoutSink && typeof stdoutSink.write === 'function') {
|
|
163
|
-
stdoutSink.write(chunk);
|
|
164
|
-
}
|
|
165
|
-
});
|
|
166
|
-
child.stderr.on('data', (chunk) => {
|
|
167
|
-
noteOutput();
|
|
168
|
-
stderrTail.push(chunk);
|
|
169
|
-
if (stderrSink && typeof stderrSink.write === 'function') {
|
|
170
|
-
stderrSink.write(chunk);
|
|
171
|
-
}
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
child.on('error', (/** @type {Error} */ err) => {
|
|
175
|
-
finish({
|
|
176
|
-
status: null,
|
|
177
|
-
signal: null,
|
|
178
|
-
stdout: stdoutTail.toString(),
|
|
179
|
-
stderr: stderrTail.toString(),
|
|
180
|
-
error: err,
|
|
181
|
-
startedAt: new Date(startTime).toISOString(),
|
|
182
|
-
endedAt: new Date().toISOString()
|
|
183
|
-
});
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
child.on('close', (/** @type {number|null} */ code, /** @type {string|null} */ signal) => {
|
|
187
|
-
finish({
|
|
188
|
-
status: code,
|
|
189
|
-
signal,
|
|
190
|
-
stdout: stdoutTail.toString(),
|
|
191
|
-
stderr: stderrTail.toString(),
|
|
192
|
-
error: null,
|
|
193
|
-
startedAt: new Date(startTime).toISOString(),
|
|
194
|
-
endedAt: new Date().toISOString()
|
|
195
|
-
});
|
|
196
159
|
});
|
|
197
|
-
});
|
|
198
160
|
}
|
|
199
|
-
|
|
200
|
-
module.exports = { spawnAndTee, DEFAULT_MAX_TAIL_BYTES };
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import childProcess from 'node:child_process';
|
|
2
|
+
import type { SpawnOptions, ChildProcess } from 'node:child_process';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_MAX_TAIL_BYTES = 64 * 1024;
|
|
6
|
+
|
|
7
|
+
type ChunkType = Buffer | string;
|
|
8
|
+
|
|
9
|
+
interface SpawnTeeResult {
|
|
10
|
+
status: number | null;
|
|
11
|
+
signal: string | null;
|
|
12
|
+
stdout: string;
|
|
13
|
+
stderr: string;
|
|
14
|
+
error: unknown | null;
|
|
15
|
+
startedAt: string;
|
|
16
|
+
endedAt: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface NoOutputWatchdog {
|
|
20
|
+
onNoOutput?: (event: { command: string; args: string[]; pid: number | undefined; elapsedMs: number }) => void;
|
|
21
|
+
initialDelayMs?: number;
|
|
22
|
+
intervalMs?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface SpawnTeeOptions {
|
|
26
|
+
stdoutSink?: NodeJS.WriteStream;
|
|
27
|
+
stderrSink?: NodeJS.WriteStream;
|
|
28
|
+
maxTailBytes?: number;
|
|
29
|
+
noOutputWatchdog?: NoOutputWatchdog | null;
|
|
30
|
+
cwd?: string;
|
|
31
|
+
env?: Record<string, string>;
|
|
32
|
+
[key: string]: unknown;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class TailBuffer {
|
|
36
|
+
maxBytes: number;
|
|
37
|
+
chunks: ChunkType[];
|
|
38
|
+
size: number;
|
|
39
|
+
|
|
40
|
+
constructor(maxBytes: number) {
|
|
41
|
+
this.maxBytes = maxBytes;
|
|
42
|
+
this.chunks = [];
|
|
43
|
+
this.size = 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
push(chunk: ChunkType): void {
|
|
47
|
+
this.chunks.push(chunk);
|
|
48
|
+
this.size += chunk.length;
|
|
49
|
+
while (this.size > this.maxBytes && this.chunks.length > 0) {
|
|
50
|
+
const head = this.chunks[0];
|
|
51
|
+
const overflow = this.size - this.maxBytes;
|
|
52
|
+
if (head.length <= overflow) {
|
|
53
|
+
this.chunks.shift();
|
|
54
|
+
this.size -= head.length;
|
|
55
|
+
} else {
|
|
56
|
+
if (Buffer.isBuffer(head)) {
|
|
57
|
+
this.chunks[0] = head.subarray(overflow);
|
|
58
|
+
} else {
|
|
59
|
+
this.chunks.shift();
|
|
60
|
+
this.size -= head.length;
|
|
61
|
+
}
|
|
62
|
+
this.size -= overflow;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
toString(): string {
|
|
68
|
+
if (this.chunks.length === 0) {return '';}
|
|
69
|
+
return Buffer.concat(this.chunks as Buffer[]).toString('utf8');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface FinishPayload {
|
|
74
|
+
status: number | null;
|
|
75
|
+
signal: string | null;
|
|
76
|
+
stdout: string;
|
|
77
|
+
stderr: string;
|
|
78
|
+
error: unknown | null;
|
|
79
|
+
startedAt?: string;
|
|
80
|
+
endedAt?: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function spawnAndTee(command: string, args: string[], options: SpawnTeeOptions = {}): Promise<SpawnTeeResult> {
|
|
84
|
+
const {
|
|
85
|
+
stdoutSink = process.stdout,
|
|
86
|
+
stderrSink = process.stderr,
|
|
87
|
+
maxTailBytes = DEFAULT_MAX_TAIL_BYTES,
|
|
88
|
+
noOutputWatchdog = null,
|
|
89
|
+
...spawnOptions
|
|
90
|
+
} = options;
|
|
91
|
+
|
|
92
|
+
return new Promise((resolve) => {
|
|
93
|
+
const stdoutTail = new TailBuffer(maxTailBytes);
|
|
94
|
+
const stderrTail = new TailBuffer(maxTailBytes);
|
|
95
|
+
let settled = false;
|
|
96
|
+
let sawOutput = false;
|
|
97
|
+
let watchdogTimer: ReturnType<typeof setTimeout> | null = null;
|
|
98
|
+
const startTime = Date.now();
|
|
99
|
+
const resolvedCwd = path.resolve((spawnOptions.cwd as string) || process.cwd());
|
|
100
|
+
const env: Record<string, string> = {
|
|
101
|
+
...process.env,
|
|
102
|
+
...(spawnOptions.env || {}),
|
|
103
|
+
PWD: resolvedCwd
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
let child: ChildProcess;
|
|
107
|
+
try {
|
|
108
|
+
child = childProcess.spawn(command, args, {
|
|
109
|
+
...spawnOptions,
|
|
110
|
+
env,
|
|
111
|
+
stdio: ['inherit', 'pipe', 'pipe']
|
|
112
|
+
} as SpawnOptions);
|
|
113
|
+
} catch (err) {
|
|
114
|
+
resolve({
|
|
115
|
+
status: null, signal: null, stdout: '', stderr: '', error: err,
|
|
116
|
+
startedAt: new Date(startTime).toISOString(), endedAt: new Date().toISOString()
|
|
117
|
+
});
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const clearWatchdog = (): void => {
|
|
122
|
+
if (watchdogTimer) {
|
|
123
|
+
clearTimeout(watchdogTimer);
|
|
124
|
+
watchdogTimer = null;
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const finish = (payload: FinishPayload): void => {
|
|
129
|
+
if (settled) {return;}
|
|
130
|
+
settled = true;
|
|
131
|
+
clearWatchdog();
|
|
132
|
+
payload.startedAt = new Date(startTime).toISOString();
|
|
133
|
+
payload.endedAt = new Date().toISOString();
|
|
134
|
+
resolve(payload as SpawnTeeResult);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const scheduleWatchdog = (delayMs: number): void => {
|
|
138
|
+
if (!noOutputWatchdog || typeof noOutputWatchdog.onNoOutput !== 'function') {return;}
|
|
139
|
+
const delay = Number.isFinite(delayMs) && delayMs >= 0 ? delayMs : 0;
|
|
140
|
+
watchdogTimer = setTimeout(() => {
|
|
141
|
+
watchdogTimer = null;
|
|
142
|
+
if (settled || sawOutput) {return;}
|
|
143
|
+
if (typeof noOutputWatchdog.onNoOutput === 'function') {
|
|
144
|
+
noOutputWatchdog.onNoOutput({
|
|
145
|
+
command,
|
|
146
|
+
args,
|
|
147
|
+
pid: child.pid,
|
|
148
|
+
elapsedMs: Date.now() - startTime
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
scheduleWatchdog(noOutputWatchdog.intervalMs ?? 0);
|
|
152
|
+
}, delay);
|
|
153
|
+
if (typeof watchdogTimer.unref === 'function') {
|
|
154
|
+
watchdogTimer.unref();
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const noteOutput = (): void => {
|
|
159
|
+
sawOutput = true;
|
|
160
|
+
clearWatchdog();
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
if (noOutputWatchdog) {
|
|
164
|
+
scheduleWatchdog(noOutputWatchdog.initialDelayMs ?? 0);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
child.stdout?.on('data', (chunk: Buffer) => {
|
|
168
|
+
noteOutput();
|
|
169
|
+
stdoutTail.push(chunk);
|
|
170
|
+
if (stdoutSink && typeof stdoutSink.write === 'function') {
|
|
171
|
+
stdoutSink.write(chunk);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
child.stderr?.on('data', (chunk: Buffer) => {
|
|
175
|
+
noteOutput();
|
|
176
|
+
stderrTail.push(chunk);
|
|
177
|
+
if (stderrSink && typeof stderrSink.write === 'function') {
|
|
178
|
+
stderrSink.write(chunk);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
child.on('error', (err: Error) => {
|
|
183
|
+
finish({
|
|
184
|
+
status: null,
|
|
185
|
+
signal: null,
|
|
186
|
+
stdout: stdoutTail.toString(),
|
|
187
|
+
stderr: stderrTail.toString(),
|
|
188
|
+
error: err,
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
child.on('close', (code: number | null, signal: string | null) => {
|
|
193
|
+
finish({
|
|
194
|
+
status: code,
|
|
195
|
+
signal,
|
|
196
|
+
stdout: stdoutTail.toString(),
|
|
197
|
+
stderr: stderrTail.toString(),
|
|
198
|
+
error: null,
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
}
|