@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.
Files changed (127) hide show
  1. package/index.js +190 -185
  2. package/lib/agents/agents.js +737 -838
  3. package/lib/agents/agents.ts +930 -0
  4. package/lib/agents/claude-telemetry.js +149 -154
  5. package/lib/agents/claude-telemetry.ts +233 -0
  6. package/lib/agents/claude.js +121 -131
  7. package/lib/agents/claude.ts +156 -0
  8. package/lib/agents/codex-telemetry.js +176 -164
  9. package/lib/agents/codex-telemetry.ts +205 -0
  10. package/lib/agents/codex.js +168 -201
  11. package/lib/agents/codex.ts +236 -0
  12. package/lib/agents/limit-hit.js +216 -226
  13. package/lib/agents/limit-hit.ts +262 -0
  14. package/lib/agents/mistral-telemetry.js +7 -10
  15. package/lib/agents/mistral-telemetry.ts +44 -0
  16. package/lib/agents/mistral.js +40 -58
  17. package/lib/agents/mistral.ts +84 -0
  18. package/lib/agents/opencode-export.js +126 -127
  19. package/lib/agents/opencode-export.ts +160 -0
  20. package/lib/agents/opencode-telemetry.js +282 -261
  21. package/lib/agents/opencode-telemetry.ts +350 -0
  22. package/lib/agents/opencode.js +263 -305
  23. package/lib/agents/opencode.ts +370 -0
  24. package/lib/agents/stage-telemetry.js +23 -24
  25. package/lib/agents/stage-telemetry.ts +47 -0
  26. package/lib/commands/active.js +502 -579
  27. package/lib/commands/active.ts +665 -0
  28. package/lib/commands/checkpoint.js +98 -74
  29. package/lib/commands/checkpoint.ts +79 -0
  30. package/lib/commands/config.js +61 -31
  31. package/lib/commands/config.ts +47 -0
  32. package/lib/commands/coverage-gate.js +271 -246
  33. package/lib/commands/coverage-gate.ts +362 -0
  34. package/lib/commands/diff.js +140 -119
  35. package/lib/commands/diff.ts +125 -0
  36. package/lib/commands/draft.js +770 -923
  37. package/lib/commands/draft.ts +1021 -0
  38. package/lib/commands/handoff.js +588 -608
  39. package/lib/commands/handoff.ts +694 -0
  40. package/lib/commands/integrate.js +1332 -1463
  41. package/lib/commands/integrate.ts +1688 -0
  42. package/lib/commands/mission-start.js +279 -237
  43. package/lib/commands/mission-start.ts +263 -0
  44. package/lib/commands/rebase.js +606 -582
  45. package/lib/commands/rebase.ts +635 -0
  46. package/lib/commands/repair-handoff.js +181 -179
  47. package/lib/commands/repair-handoff.ts +228 -0
  48. package/lib/commands/resolve-conflict.js +121 -102
  49. package/lib/commands/resolve-conflict.ts +112 -0
  50. package/lib/commands/review.js +3 -4
  51. package/lib/commands/review.ts +14 -0
  52. package/lib/commands/setup-review.js +4 -5
  53. package/lib/commands/setup-review.ts +14 -0
  54. package/lib/commands/setup.js +3 -3
  55. package/lib/commands/setup.ts +2 -0
  56. package/lib/commands/stats-backfill.js +394 -359
  57. package/lib/commands/stats-backfill.ts +418 -0
  58. package/lib/commands/stats.js +1424 -1665
  59. package/lib/commands/stats.ts +2186 -0
  60. package/lib/commands/status.js +219 -200
  61. package/lib/commands/status.ts +219 -0
  62. package/lib/commands/verify.js +3 -1
  63. package/lib/commands/verify.ts +2 -0
  64. package/lib/core/fmt.js +140 -211
  65. package/lib/core/fmt.ts +189 -0
  66. package/lib/core/git.js +121 -102
  67. package/lib/core/git.ts +146 -0
  68. package/lib/core/gitignore.js +59 -106
  69. package/lib/core/gitignore.ts +103 -0
  70. package/lib/core/mission-utils.js +833 -875
  71. package/lib/core/mission-utils.ts +998 -0
  72. package/lib/core/nels.js +112 -127
  73. package/lib/core/nels.ts +189 -0
  74. package/lib/core/persistent-data-migration.js +247 -205
  75. package/lib/core/persistent-data-migration.ts +255 -0
  76. package/lib/core/product-config.js +400 -476
  77. package/lib/core/product-config.ts +518 -0
  78. package/lib/core/runtime-matrix.js +41 -87
  79. package/lib/core/runtime-matrix.ts +84 -0
  80. package/lib/core/spawn-tee.js +152 -192
  81. package/lib/core/spawn-tee.ts +202 -0
  82. package/lib/core/state-map.js +92 -92
  83. package/lib/core/state-map.ts +115 -0
  84. package/lib/core/storage.js +101 -129
  85. package/lib/core/storage.ts +182 -0
  86. package/lib/core/subagent-limit.js +15 -16
  87. package/lib/core/subagent-limit.ts +25 -0
  88. package/lib/core/verification.js +148 -138
  89. package/lib/core/verification.ts +200 -0
  90. package/lib/index.js +142 -59
  91. package/lib/index.ts +134 -0
  92. package/lib/review/rebase.js +142 -145
  93. package/lib/review/rebase.ts +188 -0
  94. package/lib/review/review-adapter.js +99 -87
  95. package/lib/review/review-adapter.ts +198 -0
  96. package/lib/review/review-artifacts.js +486 -629
  97. package/lib/review/review-artifacts.ts +622 -0
  98. package/lib/review/review-commands.js +1220 -1373
  99. package/lib/review/review-commands.ts +1579 -0
  100. package/lib/review/review-events.js +544 -886
  101. package/lib/review/review-events.ts +954 -0
  102. package/lib/review/review-loop.js +945 -1020
  103. package/lib/review/review-loop.ts +1174 -0
  104. package/lib/review/review-polling.js +86 -115
  105. package/lib/review/review-polling.ts +194 -0
  106. package/lib/review/review-prompts.js +154 -189
  107. package/lib/review/review-prompts.ts +182 -0
  108. package/lib/review/review-state.js +246 -225
  109. package/lib/review/review-state.ts +353 -0
  110. package/lib/review/review.js +63 -84
  111. package/lib/review/review.ts +138 -0
  112. package/lib/tools/backlog.js +694 -680
  113. package/lib/tools/backlog.ts +835 -0
  114. package/lib/tools/forgejo.js +1270 -1405
  115. package/lib/tools/forgejo.ts +1793 -0
  116. package/lib/tools/gatekeeper.js +106 -96
  117. package/lib/tools/gatekeeper.ts +124 -0
  118. package/lib/tools/redgreen.js +200 -181
  119. package/lib/tools/redgreen.ts +221 -0
  120. package/lib/tools/sessions.js +58 -52
  121. package/lib/tools/sessions.ts +81 -0
  122. package/lib/tools/setup-review.js +902 -975
  123. package/lib/tools/setup-review.ts +1152 -0
  124. package/package.json +7 -5
  125. package/prompts/draft.md +1 -0
  126. package/prompts/portfolio.md +1 -1
  127. 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
+ }
@@ -1,200 +1,160 @@
1
- 'use strict';
2
-
3
- const childProcess = require('child_process');
4
- const path = require('path');
5
-
6
- // Bound the in-memory transcript so a noisy or long-running agent cannot
7
- // turn the harness into an O(total-output) memory hog. Detection only needs
8
- // a recent window to find limit-hit phrases (limit-hit.js clips ~200 chars
9
- // around the match), so a tail buffer is sufficient. The cap is generous
10
- // enough that a real limit-hit message and its surrounding reset-time
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
- /** @param {number} maxBytes */
17
- constructor(maxBytes) {
18
- /** @type {number} */
19
- this.maxBytes = maxBytes;
20
- /** @type {ChunkType[]} */
21
- this.chunks = [];
22
- /** @type {number} */
23
- this.size = 0;
24
- }
25
-
26
- /** @param {ChunkType} chunk */
27
- push(chunk) {
28
- this.chunks.push(chunk);
29
- this.size += chunk.length;
30
- while (this.size > this.maxBytes && this.chunks.length > 0) {
31
- const head = this.chunks[0];
32
- const overflow = this.size - this.maxBytes;
33
- if (head.length <= overflow) {
34
- this.chunks.shift();
35
- this.size -= head.length;
36
- } else {
37
- if (Buffer.isBuffer(head)) {
38
- this.chunks[0] = head.subarray(overflow);
39
- } else {
40
- this.chunks.shift();
41
- this.size -= head.length;
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.size -= overflow;
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
- const {
72
- stdoutSink = process.stdout,
73
- stderrSink = process.stderr,
74
- maxTailBytes = DEFAULT_MAX_TAIL_BYTES,
75
- noOutputWatchdog = null,
76
- ...spawnOptions
77
- } = options;
78
-
79
- return new Promise((resolve) => {
80
- const stdoutTail = new TailBuffer(maxTailBytes);
81
- const stderrTail = new TailBuffer(maxTailBytes);
82
- /** @type {boolean} */
83
- let settled = false;
84
- /** @type {boolean} */
85
- let sawOutput = false;
86
- /** @type {ReturnType<typeof setTimeout> | null} */
87
- let watchdogTimer = null;
88
- const startTime = Date.now();
89
- const resolvedCwd = path.resolve(spawnOptions.cwd || process.cwd());
90
- const env = {
91
- ...process.env,
92
- ...(spawnOptions.env || {}),
93
- PWD: resolvedCwd
94
- };
95
-
96
- let child;
97
- try {
98
- child = childProcess.spawn(command, args, {
99
- ...spawnOptions,
100
- env,
101
- stdio: ['inherit', 'pipe', 'pipe']
102
- });
103
- } catch (err) {
104
- resolve({
105
- status: null, signal: null, stdout: '', stderr: '', error: err,
106
- startedAt: new Date(startTime).toISOString(), endedAt: new Date().toISOString()
107
- });
108
- return;
109
- }
110
-
111
- const clearWatchdog = () => {
112
- if (watchdogTimer) {
113
- clearTimeout(watchdogTimer);
114
- watchdogTimer = null;
115
- }
116
- };
117
-
118
- /** @param {{status: number|null, signal: string|null, stdout: string, stderr: string, error: any, startedAt: string, endedAt: string}} payload */
119
- const finish = (payload) => {
120
- if (settled) {return;}
121
- settled = true;
122
- clearWatchdog();
123
- payload.startedAt = new Date(startTime).toISOString();
124
- payload.endedAt = new Date().toISOString();
125
- resolve(payload);
126
- };
127
-
128
- /** @param {number} delayMs */
129
- const scheduleWatchdog = (delayMs) => {
130
- if (!noOutputWatchdog || typeof noOutputWatchdog.onNoOutput !== 'function') {return;}
131
- const delay = Number.isFinite(delayMs) && delayMs >= 0 ? delayMs : 0;
132
- watchdogTimer = setTimeout(() => {
133
- watchdogTimer = null;
134
- if (settled || sawOutput) {return;}
135
- if (typeof noOutputWatchdog.onNoOutput === 'function') {
136
- noOutputWatchdog.onNoOutput({
137
- command,
138
- args,
139
- pid: child.pid,
140
- elapsedMs: Date.now() - startTime
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
+ }