@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.
@@ -0,0 +1,3 @@
1
+ export { buildInheritedEnv, ensurePathInEnv, redactEnvForLogs, resolveCommand, runChildProcess, appendWithCap, MAX_CAPTURE_BYTES, } from './server-utils.js';
2
+ export type { RunChildOpts, RunChildResult } from './server-utils.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -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,2 @@
1
+ export { buildInheritedEnv, ensurePathInEnv, redactEnvForLogs, resolveCommand, runChildProcess, appendWithCap, MAX_CAPTURE_BYTES, } from './server-utils.js';
2
+ //# sourceMappingURL=index.js.map
@@ -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,11 @@
1
+ export {
2
+ buildInheritedEnv,
3
+ ensurePathInEnv,
4
+ redactEnvForLogs,
5
+ resolveCommand,
6
+ runChildProcess,
7
+ appendWithCap,
8
+ MAX_CAPTURE_BYTES,
9
+ } from './server-utils.js';
10
+
11
+ export type { RunChildOpts, RunChildResult } from './server-utils.js';
@@ -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
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src",
6
+ "types": ["node"]
7
+ },
8
+ "include": ["src"]
9
+ }