@fixy/adapter-utils 0.0.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/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/server-utils.d.ts +58 -0
- package/dist/server-utils.d.ts.map +1 -0
- package/dist/server-utils.js +265 -0
- package/dist/server-utils.js.map +1 -0
- package/package.json +21 -0
- package/src/index.ts +11 -0
- package/src/server-utils.ts +315 -0
- package/tsconfig.json +9 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,gBAAgB,EAChB,cAAc,EACd,eAAe,EACf,aAAa,EACb,iBAAiB,GAClB,MAAM,mBAAmB,CAAC;AAE3B,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,gBAAgB,EAChB,cAAc,EACd,eAAe,EACf,aAAa,EACb,iBAAiB,GAClB,MAAM,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export declare const MAX_CAPTURE_BYTES: number;
|
|
2
|
+
export interface RunChildOpts {
|
|
3
|
+
command: string;
|
|
4
|
+
args: string[];
|
|
5
|
+
cwd: string;
|
|
6
|
+
env: Record<string, string>;
|
|
7
|
+
stdin?: string;
|
|
8
|
+
signal?: AbortSignal;
|
|
9
|
+
onLog?: (stream: 'stdout' | 'stderr', chunk: string) => void;
|
|
10
|
+
onSpawn?: (pid: number) => void;
|
|
11
|
+
timeoutMs?: number;
|
|
12
|
+
}
|
|
13
|
+
export interface RunChildResult {
|
|
14
|
+
exitCode: number | null;
|
|
15
|
+
signal: string | null;
|
|
16
|
+
timedOut: boolean;
|
|
17
|
+
stdout: string;
|
|
18
|
+
stderr: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Appends `chunk` to `prev`, keeping the combined string within `cap` bytes.
|
|
22
|
+
* When the cap is exceeded the oldest bytes are dropped (tail is preserved).
|
|
23
|
+
*/
|
|
24
|
+
export declare function appendWithCap(prev: string, chunk: string, cap?: number): string;
|
|
25
|
+
/**
|
|
26
|
+
* Builds an env record that inherits from `process.env`, ensures critical
|
|
27
|
+
* keys (`HOME`, `CLAUDE_CONFIG_DIR`, `CODEX_HOME`, `PATH`) are preserved
|
|
28
|
+
* untouched from the current process, strips Claude Code nesting-guard vars,
|
|
29
|
+
* and finally layers caller-supplied `overrides` on top.
|
|
30
|
+
*/
|
|
31
|
+
export declare function buildInheritedEnv(overrides?: Record<string, string>): Record<string, string>;
|
|
32
|
+
/**
|
|
33
|
+
* Ensures the env record has a non-empty `PATH`.
|
|
34
|
+
* If it is already set, returns the record unchanged.
|
|
35
|
+
* Otherwise injects a sensible macOS/Linux default.
|
|
36
|
+
*/
|
|
37
|
+
export declare function ensurePathInEnv(env: Record<string, string>): Record<string, string>;
|
|
38
|
+
/**
|
|
39
|
+
* Returns a shallow copy of `env` where any key matching `SENSITIVE_ENV_KEY`
|
|
40
|
+
* has its value replaced with `'***REDACTED***'`.
|
|
41
|
+
*/
|
|
42
|
+
export declare function redactEnvForLogs(env: Record<string, string>): Record<string, string>;
|
|
43
|
+
/**
|
|
44
|
+
* Resolves the absolute path of `command` using `which` (or `where` on
|
|
45
|
+
* Windows). Throws if the command is not found.
|
|
46
|
+
*/
|
|
47
|
+
export declare function resolveCommand(command: string): Promise<string>;
|
|
48
|
+
/**
|
|
49
|
+
* Spawns a child process and captures its output.
|
|
50
|
+
*
|
|
51
|
+
* - Inherits env via `ensurePathInEnv({ ...process.env, ...opts.env })` with
|
|
52
|
+
* Claude nesting vars stripped.
|
|
53
|
+
* - Captures up to `MAX_CAPTURE_BYTES` of stdout/stderr each.
|
|
54
|
+
* - Streams data to `opts.onLog` in real time.
|
|
55
|
+
* - Supports optional `AbortSignal` and timeout.
|
|
56
|
+
*/
|
|
57
|
+
export declare function runChildProcess(opts: RunChildOpts): Promise<RunChildResult>;
|
|
58
|
+
//# sourceMappingURL=server-utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server-utils.d.ts","sourceRoot":"","sources":["../src/server-utils.ts"],"names":[],"mappings":"AASA,eAAO,MAAM,iBAAiB,QAAkB,CAAC;AAgBjD,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,QAAQ,GAAG,QAAQ,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7D,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAMD;;;GAGG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,SAAoB,GAAG,MAAM,CAG1F;AAiBD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAmC5F;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAQnF;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAMpF;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAQrE;AAMD;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,cAAc,CAAC,CAgJ3E"}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { spawn, execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
const execFileAsync = promisify(execFile);
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Constants
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
export const MAX_CAPTURE_BYTES = 4 * 1024 * 1024; // 4MB cap per stream
|
|
8
|
+
const SENSITIVE_ENV_KEY = /(key|token|secret|password|passwd|authorization|cookie)/i;
|
|
9
|
+
/** Claude Code nesting-guard env vars that prevent nested spawns from starting. */
|
|
10
|
+
const CLAUDE_NESTING_VARS = [
|
|
11
|
+
'CLAUDECODE',
|
|
12
|
+
'CLAUDE_CODE_ENTRYPOINT',
|
|
13
|
+
'CLAUDE_CODE_SESSION',
|
|
14
|
+
'CLAUDE_CODE_PARENT_SESSION',
|
|
15
|
+
];
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Internal helpers
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
/**
|
|
20
|
+
* Appends `chunk` to `prev`, keeping the combined string within `cap` bytes.
|
|
21
|
+
* When the cap is exceeded the oldest bytes are dropped (tail is preserved).
|
|
22
|
+
*/
|
|
23
|
+
export function appendWithCap(prev, chunk, cap = MAX_CAPTURE_BYTES) {
|
|
24
|
+
const combined = prev + chunk;
|
|
25
|
+
return combined.length > cap ? combined.slice(combined.length - cap) : combined;
|
|
26
|
+
}
|
|
27
|
+
function stripClaudeNestingVars(env) {
|
|
28
|
+
const blocked = new Set(CLAUDE_NESTING_VARS);
|
|
29
|
+
const result = {};
|
|
30
|
+
for (const [k, v] of Object.entries(env)) {
|
|
31
|
+
if (!blocked.has(k)) {
|
|
32
|
+
result[k] = v;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// Exported helpers
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
/**
|
|
41
|
+
* Builds an env record that inherits from `process.env`, ensures critical
|
|
42
|
+
* keys (`HOME`, `CLAUDE_CONFIG_DIR`, `CODEX_HOME`, `PATH`) are preserved
|
|
43
|
+
* untouched from the current process, strips Claude Code nesting-guard vars,
|
|
44
|
+
* and finally layers caller-supplied `overrides` on top.
|
|
45
|
+
*/
|
|
46
|
+
export function buildInheritedEnv(overrides) {
|
|
47
|
+
// Start from process.env, filtering out undefined values.
|
|
48
|
+
const base = {};
|
|
49
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
50
|
+
if (v !== undefined) {
|
|
51
|
+
base[k] = v;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Restore critical keys from process.env (no-op if already present, but
|
|
55
|
+
// ensures overrides cannot accidentally clobber them before we re-pin).
|
|
56
|
+
const pinKeys = ['HOME', 'CLAUDE_CONFIG_DIR', 'CODEX_HOME', 'PATH'];
|
|
57
|
+
for (const key of pinKeys) {
|
|
58
|
+
const val = process.env[key];
|
|
59
|
+
if (val !== undefined) {
|
|
60
|
+
base[key] = val;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// Strip nesting guards so spawned Claude processes don't refuse to start.
|
|
64
|
+
const stripped = stripClaudeNestingVars(base);
|
|
65
|
+
// Layer caller overrides on top (they cannot override pinned keys because
|
|
66
|
+
// we re-pin below).
|
|
67
|
+
const merged = { ...stripped, ...(overrides ?? {}) };
|
|
68
|
+
// Re-pin critical keys so overrides cannot change them.
|
|
69
|
+
for (const key of pinKeys) {
|
|
70
|
+
const val = process.env[key];
|
|
71
|
+
if (val !== undefined) {
|
|
72
|
+
merged[key] = val;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return merged;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Ensures the env record has a non-empty `PATH`.
|
|
79
|
+
* If it is already set, returns the record unchanged.
|
|
80
|
+
* Otherwise injects a sensible macOS/Linux default.
|
|
81
|
+
*/
|
|
82
|
+
export function ensurePathInEnv(env) {
|
|
83
|
+
if (env['PATH'] && env['PATH'].length > 0) {
|
|
84
|
+
return env;
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
...env,
|
|
88
|
+
PATH: '/usr/local/bin:/opt/homebrew/bin:/usr/local/sbin:/usr/bin:/bin:/usr/sbin:/sbin',
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Returns a shallow copy of `env` where any key matching `SENSITIVE_ENV_KEY`
|
|
93
|
+
* has its value replaced with `'***REDACTED***'`.
|
|
94
|
+
*/
|
|
95
|
+
export function redactEnvForLogs(env) {
|
|
96
|
+
const result = {};
|
|
97
|
+
for (const [k, v] of Object.entries(env)) {
|
|
98
|
+
result[k] = SENSITIVE_ENV_KEY.test(k) ? '***REDACTED***' : v;
|
|
99
|
+
}
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Resolves the absolute path of `command` using `which` (or `where` on
|
|
104
|
+
* Windows). Throws if the command is not found.
|
|
105
|
+
*/
|
|
106
|
+
export async function resolveCommand(command) {
|
|
107
|
+
const whichCmd = process.platform === 'win32' ? 'where' : 'which';
|
|
108
|
+
try {
|
|
109
|
+
const { stdout } = await execFileAsync(whichCmd, [command]);
|
|
110
|
+
return stdout.trim();
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
throw new Error(`Command not found: ${command}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// Core child-process runner
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
/**
|
|
120
|
+
* Spawns a child process and captures its output.
|
|
121
|
+
*
|
|
122
|
+
* - Inherits env via `ensurePathInEnv({ ...process.env, ...opts.env })` with
|
|
123
|
+
* Claude nesting vars stripped.
|
|
124
|
+
* - Captures up to `MAX_CAPTURE_BYTES` of stdout/stderr each.
|
|
125
|
+
* - Streams data to `opts.onLog` in real time.
|
|
126
|
+
* - Supports optional `AbortSignal` and timeout.
|
|
127
|
+
*/
|
|
128
|
+
export function runChildProcess(opts) {
|
|
129
|
+
return new Promise((resolve, reject) => {
|
|
130
|
+
// Merge env: process.env base, then caller overrides, then ensure PATH.
|
|
131
|
+
const rawEnv = {};
|
|
132
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
133
|
+
if (v !== undefined)
|
|
134
|
+
rawEnv[k] = v;
|
|
135
|
+
}
|
|
136
|
+
const mergedEnv = ensurePathInEnv(stripClaudeNestingVars({ ...rawEnv, ...opts.env }));
|
|
137
|
+
const stdinMode = opts.stdin != null ? 'pipe' : 'ignore';
|
|
138
|
+
const child = spawn(opts.command, opts.args, {
|
|
139
|
+
cwd: opts.cwd,
|
|
140
|
+
env: mergedEnv,
|
|
141
|
+
stdio: [stdinMode, 'pipe', 'pipe'],
|
|
142
|
+
detached: true,
|
|
143
|
+
shell: false,
|
|
144
|
+
});
|
|
145
|
+
let stdoutBuf = '';
|
|
146
|
+
let stderrBuf = '';
|
|
147
|
+
let timedOut = false;
|
|
148
|
+
let settled = false;
|
|
149
|
+
// -----------------------------------------------------------------------
|
|
150
|
+
// stdin
|
|
151
|
+
// -----------------------------------------------------------------------
|
|
152
|
+
if (opts.stdin != null && child.stdin) {
|
|
153
|
+
child.stdin.write(opts.stdin);
|
|
154
|
+
child.stdin.end();
|
|
155
|
+
}
|
|
156
|
+
// -----------------------------------------------------------------------
|
|
157
|
+
// onSpawn
|
|
158
|
+
// -----------------------------------------------------------------------
|
|
159
|
+
child.on('spawn', () => {
|
|
160
|
+
if (typeof child.pid === 'number' && child.pid > 0) {
|
|
161
|
+
opts.onSpawn?.(child.pid);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
// -----------------------------------------------------------------------
|
|
165
|
+
// stdout / stderr capture
|
|
166
|
+
// -----------------------------------------------------------------------
|
|
167
|
+
child.stdout?.on('data', (chunk) => {
|
|
168
|
+
const text = chunk.toString();
|
|
169
|
+
stdoutBuf = appendWithCap(stdoutBuf, text);
|
|
170
|
+
opts.onLog?.('stdout', text);
|
|
171
|
+
});
|
|
172
|
+
child.stderr?.on('data', (chunk) => {
|
|
173
|
+
const text = chunk.toString();
|
|
174
|
+
stderrBuf = appendWithCap(stderrBuf, text);
|
|
175
|
+
opts.onLog?.('stderr', text);
|
|
176
|
+
});
|
|
177
|
+
// -----------------------------------------------------------------------
|
|
178
|
+
// Kill helper (SIGTERM → SIGKILL after 5 s)
|
|
179
|
+
// -----------------------------------------------------------------------
|
|
180
|
+
function killChild() {
|
|
181
|
+
try {
|
|
182
|
+
// Negative PID kills the entire process group (detached: true).
|
|
183
|
+
if (typeof child.pid === 'number') {
|
|
184
|
+
process.kill(-child.pid, 'SIGTERM');
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
child.kill('SIGTERM');
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
// Process may have already exited — ignore.
|
|
192
|
+
}
|
|
193
|
+
setTimeout(() => {
|
|
194
|
+
try {
|
|
195
|
+
if (typeof child.pid === 'number') {
|
|
196
|
+
process.kill(-child.pid, 'SIGKILL');
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
child.kill('SIGKILL');
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
// Ignore.
|
|
204
|
+
}
|
|
205
|
+
}, 5_000).unref();
|
|
206
|
+
}
|
|
207
|
+
// -----------------------------------------------------------------------
|
|
208
|
+
// AbortSignal support
|
|
209
|
+
// -----------------------------------------------------------------------
|
|
210
|
+
let abortListener;
|
|
211
|
+
if (opts.signal) {
|
|
212
|
+
abortListener = () => {
|
|
213
|
+
if (!settled)
|
|
214
|
+
killChild();
|
|
215
|
+
};
|
|
216
|
+
if (opts.signal.aborted) {
|
|
217
|
+
// Already aborted before spawn.
|
|
218
|
+
killChild();
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
opts.signal.addEventListener('abort', abortListener);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// -----------------------------------------------------------------------
|
|
225
|
+
// Timeout support
|
|
226
|
+
// -----------------------------------------------------------------------
|
|
227
|
+
let timeoutHandle;
|
|
228
|
+
if (opts.timeoutMs != null && opts.timeoutMs > 0) {
|
|
229
|
+
timeoutHandle = setTimeout(() => {
|
|
230
|
+
if (!settled) {
|
|
231
|
+
timedOut = true;
|
|
232
|
+
killChild();
|
|
233
|
+
}
|
|
234
|
+
}, opts.timeoutMs);
|
|
235
|
+
}
|
|
236
|
+
// -----------------------------------------------------------------------
|
|
237
|
+
// close / error
|
|
238
|
+
// -----------------------------------------------------------------------
|
|
239
|
+
child.on('close', (exitCode, exitSignal) => {
|
|
240
|
+
settled = true;
|
|
241
|
+
if (timeoutHandle !== undefined)
|
|
242
|
+
clearTimeout(timeoutHandle);
|
|
243
|
+
if (abortListener && opts.signal) {
|
|
244
|
+
opts.signal.removeEventListener('abort', abortListener);
|
|
245
|
+
}
|
|
246
|
+
resolve({
|
|
247
|
+
exitCode,
|
|
248
|
+
signal: exitSignal,
|
|
249
|
+
timedOut,
|
|
250
|
+
stdout: stdoutBuf,
|
|
251
|
+
stderr: stderrBuf,
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
child.on('error', (err) => {
|
|
255
|
+
settled = true;
|
|
256
|
+
if (timeoutHandle !== undefined)
|
|
257
|
+
clearTimeout(timeoutHandle);
|
|
258
|
+
if (abortListener && opts.signal) {
|
|
259
|
+
opts.signal.removeEventListener('abort', abortListener);
|
|
260
|
+
}
|
|
261
|
+
reject(new Error(`Failed to spawn "${opts.command}": ${err.message}`, { cause: err }));
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
//# sourceMappingURL=server-utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server-utils.js","sourceRoot":"","sources":["../src/server-utils.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fixy/adapter-utils",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"typecheck": "tsc --noEmit"
|
|
16
|
+
},
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@types/node": "^20.0.0"
|
|
20
|
+
}
|
|
21
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import { spawn, execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
|
|
4
|
+
const execFileAsync = promisify(execFile);
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Constants
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
export const MAX_CAPTURE_BYTES = 4 * 1024 * 1024; // 4MB cap per stream
|
|
11
|
+
|
|
12
|
+
const SENSITIVE_ENV_KEY = /(key|token|secret|password|passwd|authorization|cookie)/i;
|
|
13
|
+
|
|
14
|
+
/** Claude Code nesting-guard env vars that prevent nested spawns from starting. */
|
|
15
|
+
const CLAUDE_NESTING_VARS = [
|
|
16
|
+
'CLAUDECODE',
|
|
17
|
+
'CLAUDE_CODE_ENTRYPOINT',
|
|
18
|
+
'CLAUDE_CODE_SESSION',
|
|
19
|
+
'CLAUDE_CODE_PARENT_SESSION',
|
|
20
|
+
] as const;
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Types
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
export interface RunChildOpts {
|
|
27
|
+
command: string;
|
|
28
|
+
args: string[];
|
|
29
|
+
cwd: string;
|
|
30
|
+
env: Record<string, string>;
|
|
31
|
+
stdin?: string;
|
|
32
|
+
signal?: AbortSignal;
|
|
33
|
+
onLog?: (stream: 'stdout' | 'stderr', chunk: string) => void;
|
|
34
|
+
onSpawn?: (pid: number) => void;
|
|
35
|
+
timeoutMs?: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface RunChildResult {
|
|
39
|
+
exitCode: number | null;
|
|
40
|
+
signal: string | null;
|
|
41
|
+
timedOut: boolean;
|
|
42
|
+
stdout: string;
|
|
43
|
+
stderr: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Internal helpers
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Appends `chunk` to `prev`, keeping the combined string within `cap` bytes.
|
|
52
|
+
* When the cap is exceeded the oldest bytes are dropped (tail is preserved).
|
|
53
|
+
*/
|
|
54
|
+
export function appendWithCap(prev: string, chunk: string, cap = MAX_CAPTURE_BYTES): string {
|
|
55
|
+
const combined = prev + chunk;
|
|
56
|
+
return combined.length > cap ? combined.slice(combined.length - cap) : combined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function stripClaudeNestingVars(env: Record<string, string>): Record<string, string> {
|
|
60
|
+
const blocked = new Set<string>(CLAUDE_NESTING_VARS);
|
|
61
|
+
const result: Record<string, string> = {};
|
|
62
|
+
for (const [k, v] of Object.entries(env)) {
|
|
63
|
+
if (!blocked.has(k)) {
|
|
64
|
+
result[k] = v;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// Exported helpers
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Builds an env record that inherits from `process.env`, ensures critical
|
|
76
|
+
* keys (`HOME`, `CLAUDE_CONFIG_DIR`, `CODEX_HOME`, `PATH`) are preserved
|
|
77
|
+
* untouched from the current process, strips Claude Code nesting-guard vars,
|
|
78
|
+
* and finally layers caller-supplied `overrides` on top.
|
|
79
|
+
*/
|
|
80
|
+
export function buildInheritedEnv(overrides?: Record<string, string>): Record<string, string> {
|
|
81
|
+
// Start from process.env, filtering out undefined values.
|
|
82
|
+
const base: Record<string, string> = {};
|
|
83
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
84
|
+
if (v !== undefined) {
|
|
85
|
+
base[k] = v;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Restore critical keys from process.env (no-op if already present, but
|
|
90
|
+
// ensures overrides cannot accidentally clobber them before we re-pin).
|
|
91
|
+
const pinKeys = ['HOME', 'CLAUDE_CONFIG_DIR', 'CODEX_HOME', 'PATH'] as const;
|
|
92
|
+
for (const key of pinKeys) {
|
|
93
|
+
const val = process.env[key];
|
|
94
|
+
if (val !== undefined) {
|
|
95
|
+
base[key] = val;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Strip nesting guards so spawned Claude processes don't refuse to start.
|
|
100
|
+
const stripped = stripClaudeNestingVars(base);
|
|
101
|
+
|
|
102
|
+
// Layer caller overrides on top (they cannot override pinned keys because
|
|
103
|
+
// we re-pin below).
|
|
104
|
+
const merged = { ...stripped, ...(overrides ?? {}) };
|
|
105
|
+
|
|
106
|
+
// Re-pin critical keys so overrides cannot change them.
|
|
107
|
+
for (const key of pinKeys) {
|
|
108
|
+
const val = process.env[key];
|
|
109
|
+
if (val !== undefined) {
|
|
110
|
+
merged[key] = val;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return merged;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Ensures the env record has a non-empty `PATH`.
|
|
119
|
+
* If it is already set, returns the record unchanged.
|
|
120
|
+
* Otherwise injects a sensible macOS/Linux default.
|
|
121
|
+
*/
|
|
122
|
+
export function ensurePathInEnv(env: Record<string, string>): Record<string, string> {
|
|
123
|
+
if (env['PATH'] && env['PATH'].length > 0) {
|
|
124
|
+
return env;
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
...env,
|
|
128
|
+
PATH: '/usr/local/bin:/opt/homebrew/bin:/usr/local/sbin:/usr/bin:/bin:/usr/sbin:/sbin',
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Returns a shallow copy of `env` where any key matching `SENSITIVE_ENV_KEY`
|
|
134
|
+
* has its value replaced with `'***REDACTED***'`.
|
|
135
|
+
*/
|
|
136
|
+
export function redactEnvForLogs(env: Record<string, string>): Record<string, string> {
|
|
137
|
+
const result: Record<string, string> = {};
|
|
138
|
+
for (const [k, v] of Object.entries(env)) {
|
|
139
|
+
result[k] = SENSITIVE_ENV_KEY.test(k) ? '***REDACTED***' : v;
|
|
140
|
+
}
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Resolves the absolute path of `command` using `which` (or `where` on
|
|
146
|
+
* Windows). Throws if the command is not found.
|
|
147
|
+
*/
|
|
148
|
+
export async function resolveCommand(command: string): Promise<string> {
|
|
149
|
+
const whichCmd = process.platform === 'win32' ? 'where' : 'which';
|
|
150
|
+
try {
|
|
151
|
+
const { stdout } = await execFileAsync(whichCmd, [command]);
|
|
152
|
+
return stdout.trim();
|
|
153
|
+
} catch {
|
|
154
|
+
throw new Error(`Command not found: ${command}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
// Core child-process runner
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Spawns a child process and captures its output.
|
|
164
|
+
*
|
|
165
|
+
* - Inherits env via `ensurePathInEnv({ ...process.env, ...opts.env })` with
|
|
166
|
+
* Claude nesting vars stripped.
|
|
167
|
+
* - Captures up to `MAX_CAPTURE_BYTES` of stdout/stderr each.
|
|
168
|
+
* - Streams data to `opts.onLog` in real time.
|
|
169
|
+
* - Supports optional `AbortSignal` and timeout.
|
|
170
|
+
*/
|
|
171
|
+
export function runChildProcess(opts: RunChildOpts): Promise<RunChildResult> {
|
|
172
|
+
return new Promise((resolve, reject) => {
|
|
173
|
+
// Merge env: process.env base, then caller overrides, then ensure PATH.
|
|
174
|
+
const rawEnv: Record<string, string> = {};
|
|
175
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
176
|
+
if (v !== undefined) rawEnv[k] = v;
|
|
177
|
+
}
|
|
178
|
+
const mergedEnv = ensurePathInEnv(stripClaudeNestingVars({ ...rawEnv, ...opts.env }));
|
|
179
|
+
|
|
180
|
+
const stdinMode = opts.stdin != null ? 'pipe' : 'ignore';
|
|
181
|
+
|
|
182
|
+
const child = spawn(opts.command, opts.args, {
|
|
183
|
+
cwd: opts.cwd,
|
|
184
|
+
env: mergedEnv,
|
|
185
|
+
stdio: [stdinMode, 'pipe', 'pipe'],
|
|
186
|
+
detached: true,
|
|
187
|
+
shell: false,
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
let stdoutBuf = '';
|
|
191
|
+
let stderrBuf = '';
|
|
192
|
+
let timedOut = false;
|
|
193
|
+
let settled = false;
|
|
194
|
+
|
|
195
|
+
// -----------------------------------------------------------------------
|
|
196
|
+
// stdin
|
|
197
|
+
// -----------------------------------------------------------------------
|
|
198
|
+
if (opts.stdin != null && child.stdin) {
|
|
199
|
+
child.stdin.write(opts.stdin);
|
|
200
|
+
child.stdin.end();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// -----------------------------------------------------------------------
|
|
204
|
+
// onSpawn
|
|
205
|
+
// -----------------------------------------------------------------------
|
|
206
|
+
child.on('spawn', () => {
|
|
207
|
+
if (typeof child.pid === 'number' && child.pid > 0) {
|
|
208
|
+
opts.onSpawn?.(child.pid);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// -----------------------------------------------------------------------
|
|
213
|
+
// stdout / stderr capture
|
|
214
|
+
// -----------------------------------------------------------------------
|
|
215
|
+
child.stdout?.on('data', (chunk: Buffer) => {
|
|
216
|
+
const text = chunk.toString();
|
|
217
|
+
stdoutBuf = appendWithCap(stdoutBuf, text);
|
|
218
|
+
opts.onLog?.('stdout', text);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
child.stderr?.on('data', (chunk: Buffer) => {
|
|
222
|
+
const text = chunk.toString();
|
|
223
|
+
stderrBuf = appendWithCap(stderrBuf, text);
|
|
224
|
+
opts.onLog?.('stderr', text);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// -----------------------------------------------------------------------
|
|
228
|
+
// Kill helper (SIGTERM → SIGKILL after 5 s)
|
|
229
|
+
// -----------------------------------------------------------------------
|
|
230
|
+
function killChild(): void {
|
|
231
|
+
try {
|
|
232
|
+
// Negative PID kills the entire process group (detached: true).
|
|
233
|
+
if (typeof child.pid === 'number') {
|
|
234
|
+
process.kill(-child.pid, 'SIGTERM');
|
|
235
|
+
} else {
|
|
236
|
+
child.kill('SIGTERM');
|
|
237
|
+
}
|
|
238
|
+
} catch {
|
|
239
|
+
// Process may have already exited — ignore.
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
setTimeout(() => {
|
|
243
|
+
try {
|
|
244
|
+
if (typeof child.pid === 'number') {
|
|
245
|
+
process.kill(-child.pid, 'SIGKILL');
|
|
246
|
+
} else {
|
|
247
|
+
child.kill('SIGKILL');
|
|
248
|
+
}
|
|
249
|
+
} catch {
|
|
250
|
+
// Ignore.
|
|
251
|
+
}
|
|
252
|
+
}, 5_000).unref();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// -----------------------------------------------------------------------
|
|
256
|
+
// AbortSignal support
|
|
257
|
+
// -----------------------------------------------------------------------
|
|
258
|
+
let abortListener: (() => void) | undefined;
|
|
259
|
+
if (opts.signal) {
|
|
260
|
+
abortListener = () => {
|
|
261
|
+
if (!settled) killChild();
|
|
262
|
+
};
|
|
263
|
+
if (opts.signal.aborted) {
|
|
264
|
+
// Already aborted before spawn.
|
|
265
|
+
killChild();
|
|
266
|
+
} else {
|
|
267
|
+
opts.signal.addEventListener('abort', abortListener);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// -----------------------------------------------------------------------
|
|
272
|
+
// Timeout support
|
|
273
|
+
// -----------------------------------------------------------------------
|
|
274
|
+
let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
|
|
275
|
+
if (opts.timeoutMs != null && opts.timeoutMs > 0) {
|
|
276
|
+
timeoutHandle = setTimeout(() => {
|
|
277
|
+
if (!settled) {
|
|
278
|
+
timedOut = true;
|
|
279
|
+
killChild();
|
|
280
|
+
}
|
|
281
|
+
}, opts.timeoutMs);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// -----------------------------------------------------------------------
|
|
285
|
+
// close / error
|
|
286
|
+
// -----------------------------------------------------------------------
|
|
287
|
+
child.on('close', (exitCode: number | null, exitSignal: NodeJS.Signals | null) => {
|
|
288
|
+
settled = true;
|
|
289
|
+
|
|
290
|
+
if (timeoutHandle !== undefined) clearTimeout(timeoutHandle);
|
|
291
|
+
if (abortListener && opts.signal) {
|
|
292
|
+
opts.signal.removeEventListener('abort', abortListener);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
resolve({
|
|
296
|
+
exitCode,
|
|
297
|
+
signal: exitSignal,
|
|
298
|
+
timedOut,
|
|
299
|
+
stdout: stdoutBuf,
|
|
300
|
+
stderr: stderrBuf,
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
child.on('error', (err: Error) => {
|
|
305
|
+
settled = true;
|
|
306
|
+
|
|
307
|
+
if (timeoutHandle !== undefined) clearTimeout(timeoutHandle);
|
|
308
|
+
if (abortListener && opts.signal) {
|
|
309
|
+
opts.signal.removeEventListener('abort', abortListener);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
reject(new Error(`Failed to spawn "${opts.command}": ${err.message}`, { cause: err }));
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
}
|