refactor: P3 polish — Interactor type safety, error response builder, OXC (#316)
* refactor: polish improvements from codebase review (P3)
- Export Interactor type and add `satisfies Interactor` to both return
objects in getInteractor() for compile-time completeness checking
- Create errorResponse/successResponse builder in handlers/response.ts
and adopt it in 5 representative handler files (find, record-trace,
interaction-press, session-deploy, snapshot-wait)
- Add ESLint with typescript-eslint, fix unused imports and trivial
lint issues, configure rules to warn for stylistic patterns
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: complete P3 items — consolidate runner files, document macOS exports, standardize dispatch
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address P3 review feedback — remove invalid ESLint rules, dead code, unused export
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: replace eslint and prettier with oxc
* chore: tighten oxc config and upgrade typescript
* refactor: extract ios runner contract helpers
* docs: tighten agent guidance and add check scripts
* fix: resolve post-rebase validation issues
* fix: remove shell interpolation from metro launcher
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 20:43:24 +02:00
|
|
|
import { spawn, spawnSync, type StdioOptions } from 'node:child_process';
|
2026-01-30 19:41:00 +01:00
|
|
|
import { AppError } from './errors.ts';
|
|
|
|
|
|
|
|
|
|
export type ExecResult = {
|
|
|
|
|
stdout: string;
|
|
|
|
|
stderr: string;
|
|
|
|
|
exitCode: number;
|
|
|
|
|
stdoutBuffer?: Buffer;
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-20 19:44:58 +01:00
|
|
|
type ExecOptions = {
|
2026-01-30 19:41:00 +01:00
|
|
|
cwd?: string;
|
|
|
|
|
env?: NodeJS.ProcessEnv;
|
|
|
|
|
allowFailure?: boolean;
|
|
|
|
|
binaryStdout?: boolean;
|
2026-02-02 15:12:23 +01:00
|
|
|
stdin?: string | Buffer;
|
2026-02-12 17:17:56 +01:00
|
|
|
timeoutMs?: number;
|
2026-02-20 17:18:37 +01:00
|
|
|
detached?: boolean;
|
2026-01-30 19:41:00 +01:00
|
|
|
};
|
|
|
|
|
|
2026-02-20 19:44:58 +01:00
|
|
|
type ExecStreamOptions = ExecOptions & {
|
2026-01-30 19:41:00 +01:00
|
|
|
onStdoutChunk?: (chunk: string) => void;
|
|
|
|
|
onStderrChunk?: (chunk: string) => void;
|
2026-02-20 17:18:37 +01:00
|
|
|
onSpawn?: (child: ReturnType<typeof spawn>) => void;
|
2026-01-30 19:41:00 +01:00
|
|
|
};
|
|
|
|
|
|
2026-02-02 21:33:45 +01:00
|
|
|
export type ExecBackgroundResult = {
|
|
|
|
|
child: ReturnType<typeof spawn>;
|
|
|
|
|
wait: Promise<ExecResult>;
|
|
|
|
|
};
|
|
|
|
|
|
refactor: P3 polish — Interactor type safety, error response builder, OXC (#316)
* refactor: polish improvements from codebase review (P3)
- Export Interactor type and add `satisfies Interactor` to both return
objects in getInteractor() for compile-time completeness checking
- Create errorResponse/successResponse builder in handlers/response.ts
and adopt it in 5 representative handler files (find, record-trace,
interaction-press, session-deploy, snapshot-wait)
- Add ESLint with typescript-eslint, fix unused imports and trivial
lint issues, configure rules to warn for stylistic patterns
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: complete P3 items — consolidate runner files, document macOS exports, standardize dispatch
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address P3 review feedback — remove invalid ESLint rules, dead code, unused export
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: replace eslint and prettier with oxc
* chore: tighten oxc config and upgrade typescript
* refactor: extract ios runner contract helpers
* docs: tighten agent guidance and add check scripts
* fix: resolve post-rebase validation issues
* fix: remove shell interpolation from metro launcher
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 20:43:24 +02:00
|
|
|
type ExecDetachedOptions = ExecOptions & {
|
|
|
|
|
stdio?: StdioOptions;
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-30 19:41:00 +01:00
|
|
|
export async function runCmd(
|
|
|
|
|
cmd: string,
|
|
|
|
|
args: string[],
|
|
|
|
|
options: ExecOptions = {},
|
|
|
|
|
): Promise<ExecResult> {
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
const child = spawn(cmd, args, {
|
|
|
|
|
cwd: options.cwd,
|
|
|
|
|
env: options.env,
|
2026-02-02 15:12:23 +01:00
|
|
|
stdio: ['pipe', 'pipe', 'pipe'],
|
2026-02-20 17:18:37 +01:00
|
|
|
detached: options.detached,
|
2026-01-30 19:41:00 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let stdout = '';
|
|
|
|
|
let stdoutBuffer: Buffer | undefined = options.binaryStdout ? Buffer.alloc(0) : undefined;
|
|
|
|
|
let stderr = '';
|
2026-02-12 17:17:56 +01:00
|
|
|
let didTimeout = false;
|
|
|
|
|
const timeoutMs = normalizeTimeoutMs(options.timeoutMs);
|
|
|
|
|
const timeoutHandle = timeoutMs
|
|
|
|
|
? setTimeout(() => {
|
2026-03-17 18:18:53 +01:00
|
|
|
didTimeout = true;
|
|
|
|
|
child.kill('SIGKILL');
|
|
|
|
|
}, timeoutMs)
|
2026-02-12 17:17:56 +01:00
|
|
|
: null;
|
2026-01-30 19:41:00 +01:00
|
|
|
|
|
|
|
|
if (!options.binaryStdout) child.stdout.setEncoding('utf8');
|
|
|
|
|
child.stderr.setEncoding('utf8');
|
|
|
|
|
|
2026-02-02 15:12:23 +01:00
|
|
|
if (options.stdin !== undefined) {
|
|
|
|
|
child.stdin.write(options.stdin);
|
|
|
|
|
}
|
|
|
|
|
child.stdin.end();
|
|
|
|
|
|
2026-01-30 19:41:00 +01:00
|
|
|
child.stdout.on('data', (chunk) => {
|
|
|
|
|
if (options.binaryStdout) {
|
|
|
|
|
stdoutBuffer = Buffer.concat([
|
|
|
|
|
stdoutBuffer ?? Buffer.alloc(0),
|
|
|
|
|
Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk),
|
|
|
|
|
]);
|
|
|
|
|
} else {
|
|
|
|
|
stdout += chunk;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
child.stderr.on('data', (chunk) => {
|
|
|
|
|
stderr += chunk;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
child.on('error', (err) => {
|
2026-02-12 17:17:56 +01:00
|
|
|
if (timeoutHandle) clearTimeout(timeoutHandle);
|
2026-01-30 19:41:00 +01:00
|
|
|
const code = (err as NodeJS.ErrnoException).code;
|
|
|
|
|
if (code === 'ENOENT') {
|
|
|
|
|
reject(new AppError('TOOL_MISSING', `${cmd} not found in PATH`, { cmd }, err));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
reject(new AppError('COMMAND_FAILED', `Failed to run ${cmd}`, { cmd, args }, err));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
child.on('close', (code) => {
|
2026-02-12 17:17:56 +01:00
|
|
|
if (timeoutHandle) clearTimeout(timeoutHandle);
|
2026-01-30 19:41:00 +01:00
|
|
|
const exitCode = code ?? 1;
|
2026-02-12 17:17:56 +01:00
|
|
|
if (didTimeout && timeoutMs) {
|
|
|
|
|
reject(
|
|
|
|
|
new AppError('COMMAND_FAILED', `${cmd} timed out after ${timeoutMs}ms`, {
|
|
|
|
|
cmd,
|
|
|
|
|
args,
|
|
|
|
|
stdout,
|
|
|
|
|
stderr,
|
|
|
|
|
exitCode,
|
|
|
|
|
timeoutMs,
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-01-30 19:41:00 +01:00
|
|
|
if (exitCode !== 0 && !options.allowFailure) {
|
|
|
|
|
reject(
|
|
|
|
|
new AppError('COMMAND_FAILED', `${cmd} exited with code ${exitCode}`, {
|
|
|
|
|
cmd,
|
|
|
|
|
args,
|
|
|
|
|
stdout,
|
|
|
|
|
stderr,
|
|
|
|
|
exitCode,
|
2026-02-23 18:34:27 +01:00
|
|
|
processExitError: true,
|
2026-01-30 19:41:00 +01:00
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
resolve({ stdout, stderr, exitCode, stdoutBuffer });
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function whichCmd(cmd: string): Promise<boolean> {
|
|
|
|
|
try {
|
|
|
|
|
const { shell, args } = resolveWhichArgs(cmd);
|
|
|
|
|
const result = await runCmd(shell, args, { allowFailure: true });
|
|
|
|
|
return result.exitCode === 0 && result.stdout.trim().length > 0;
|
|
|
|
|
} catch {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function runCmdSync(cmd: string, args: string[], options: ExecOptions = {}): ExecResult {
|
|
|
|
|
const result = spawnSync(cmd, args, {
|
|
|
|
|
cwd: options.cwd,
|
|
|
|
|
env: options.env,
|
2026-02-02 15:12:23 +01:00
|
|
|
stdio: ['pipe', 'pipe', 'pipe'],
|
2026-01-30 19:41:00 +01:00
|
|
|
encoding: options.binaryStdout ? undefined : 'utf8',
|
2026-02-02 15:12:23 +01:00
|
|
|
input: options.stdin,
|
2026-02-12 17:17:56 +01:00
|
|
|
timeout: normalizeTimeoutMs(options.timeoutMs),
|
2026-01-30 19:41:00 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (result.error) {
|
|
|
|
|
const code = (result.error as NodeJS.ErrnoException).code;
|
2026-02-12 17:17:56 +01:00
|
|
|
if (code === 'ETIMEDOUT') {
|
2026-03-17 18:18:53 +01:00
|
|
|
throw new AppError(
|
|
|
|
|
'COMMAND_FAILED',
|
|
|
|
|
`${cmd} timed out after ${normalizeTimeoutMs(options.timeoutMs)}ms`,
|
|
|
|
|
{
|
|
|
|
|
cmd,
|
|
|
|
|
args,
|
|
|
|
|
timeoutMs: normalizeTimeoutMs(options.timeoutMs),
|
|
|
|
|
},
|
|
|
|
|
result.error,
|
|
|
|
|
);
|
2026-02-12 17:17:56 +01:00
|
|
|
}
|
2026-01-30 19:41:00 +01:00
|
|
|
if (code === 'ENOENT') {
|
|
|
|
|
throw new AppError('TOOL_MISSING', `${cmd} not found in PATH`, { cmd }, result.error);
|
|
|
|
|
}
|
|
|
|
|
throw new AppError('COMMAND_FAILED', `Failed to run ${cmd}`, { cmd, args }, result.error);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const stdoutBuffer = options.binaryStdout
|
|
|
|
|
? Buffer.isBuffer(result.stdout)
|
|
|
|
|
? result.stdout
|
|
|
|
|
: Buffer.from(result.stdout ?? '')
|
|
|
|
|
: undefined;
|
|
|
|
|
const stdout = options.binaryStdout
|
|
|
|
|
? ''
|
|
|
|
|
: typeof result.stdout === 'string'
|
|
|
|
|
? result.stdout
|
|
|
|
|
: (result.stdout ?? '').toString();
|
|
|
|
|
const stderr =
|
|
|
|
|
typeof result.stderr === 'string' ? result.stderr : (result.stderr ?? '').toString();
|
|
|
|
|
const exitCode = result.status ?? 1;
|
|
|
|
|
|
|
|
|
|
if (exitCode !== 0 && !options.allowFailure) {
|
|
|
|
|
throw new AppError('COMMAND_FAILED', `${cmd} exited with code ${exitCode}`, {
|
|
|
|
|
cmd,
|
|
|
|
|
args,
|
|
|
|
|
stdout,
|
|
|
|
|
stderr,
|
|
|
|
|
exitCode,
|
2026-02-23 18:34:27 +01:00
|
|
|
processExitError: true,
|
2026-01-30 19:41:00 +01:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return { stdout, stderr, exitCode, stdoutBuffer };
|
|
|
|
|
}
|
|
|
|
|
|
refactor: P3 polish — Interactor type safety, error response builder, OXC (#316)
* refactor: polish improvements from codebase review (P3)
- Export Interactor type and add `satisfies Interactor` to both return
objects in getInteractor() for compile-time completeness checking
- Create errorResponse/successResponse builder in handlers/response.ts
and adopt it in 5 representative handler files (find, record-trace,
interaction-press, session-deploy, snapshot-wait)
- Add ESLint with typescript-eslint, fix unused imports and trivial
lint issues, configure rules to warn for stylistic patterns
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: complete P3 items — consolidate runner files, document macOS exports, standardize dispatch
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address P3 review feedback — remove invalid ESLint rules, dead code, unused export
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: replace eslint and prettier with oxc
* chore: tighten oxc config and upgrade typescript
* refactor: extract ios runner contract helpers
* docs: tighten agent guidance and add check scripts
* fix: resolve post-rebase validation issues
* fix: remove shell interpolation from metro launcher
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 20:43:24 +02:00
|
|
|
export function runCmdDetached(
|
|
|
|
|
cmd: string,
|
|
|
|
|
args: string[],
|
|
|
|
|
options: ExecDetachedOptions = {},
|
|
|
|
|
): number {
|
2026-01-30 19:41:00 +01:00
|
|
|
const child = spawn(cmd, args, {
|
|
|
|
|
cwd: options.cwd,
|
|
|
|
|
env: options.env,
|
refactor: P3 polish — Interactor type safety, error response builder, OXC (#316)
* refactor: polish improvements from codebase review (P3)
- Export Interactor type and add `satisfies Interactor` to both return
objects in getInteractor() for compile-time completeness checking
- Create errorResponse/successResponse builder in handlers/response.ts
and adopt it in 5 representative handler files (find, record-trace,
interaction-press, session-deploy, snapshot-wait)
- Add ESLint with typescript-eslint, fix unused imports and trivial
lint issues, configure rules to warn for stylistic patterns
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: complete P3 items — consolidate runner files, document macOS exports, standardize dispatch
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address P3 review feedback — remove invalid ESLint rules, dead code, unused export
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: replace eslint and prettier with oxc
* chore: tighten oxc config and upgrade typescript
* refactor: extract ios runner contract helpers
* docs: tighten agent guidance and add check scripts
* fix: resolve post-rebase validation issues
* fix: remove shell interpolation from metro launcher
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 20:43:24 +02:00
|
|
|
stdio: options.stdio ?? 'ignore',
|
2026-01-30 19:41:00 +01:00
|
|
|
detached: true,
|
|
|
|
|
});
|
|
|
|
|
child.unref();
|
refactor: P3 polish — Interactor type safety, error response builder, OXC (#316)
* refactor: polish improvements from codebase review (P3)
- Export Interactor type and add `satisfies Interactor` to both return
objects in getInteractor() for compile-time completeness checking
- Create errorResponse/successResponse builder in handlers/response.ts
and adopt it in 5 representative handler files (find, record-trace,
interaction-press, session-deploy, snapshot-wait)
- Add ESLint with typescript-eslint, fix unused imports and trivial
lint issues, configure rules to warn for stylistic patterns
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: complete P3 items — consolidate runner files, document macOS exports, standardize dispatch
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address P3 review feedback — remove invalid ESLint rules, dead code, unused export
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: replace eslint and prettier with oxc
* chore: tighten oxc config and upgrade typescript
* refactor: extract ios runner contract helpers
* docs: tighten agent guidance and add check scripts
* fix: resolve post-rebase validation issues
* fix: remove shell interpolation from metro launcher
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 20:43:24 +02:00
|
|
|
return child.pid ?? 0;
|
2026-01-30 19:41:00 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function runCmdStreaming(
|
|
|
|
|
cmd: string,
|
|
|
|
|
args: string[],
|
|
|
|
|
options: ExecStreamOptions = {},
|
|
|
|
|
): Promise<ExecResult> {
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
const child = spawn(cmd, args, {
|
|
|
|
|
cwd: options.cwd,
|
|
|
|
|
env: options.env,
|
2026-02-02 15:12:23 +01:00
|
|
|
stdio: ['pipe', 'pipe', 'pipe'],
|
2026-02-20 17:18:37 +01:00
|
|
|
detached: options.detached,
|
2026-01-30 19:41:00 +01:00
|
|
|
});
|
2026-02-20 17:18:37 +01:00
|
|
|
options.onSpawn?.(child);
|
2026-01-30 19:41:00 +01:00
|
|
|
|
|
|
|
|
let stdout = '';
|
|
|
|
|
let stderr = '';
|
|
|
|
|
let stdoutBuffer: Buffer | undefined = options.binaryStdout ? Buffer.alloc(0) : undefined;
|
2026-03-28 18:55:53 +01:00
|
|
|
let didTimeout = false;
|
|
|
|
|
const timeoutMs = normalizeTimeoutMs(options.timeoutMs);
|
|
|
|
|
const timeoutHandle = timeoutMs
|
|
|
|
|
? setTimeout(() => {
|
|
|
|
|
didTimeout = true;
|
|
|
|
|
child.kill('SIGKILL');
|
|
|
|
|
}, timeoutMs)
|
|
|
|
|
: null;
|
2026-01-30 19:41:00 +01:00
|
|
|
|
|
|
|
|
if (!options.binaryStdout) child.stdout.setEncoding('utf8');
|
|
|
|
|
child.stderr.setEncoding('utf8');
|
|
|
|
|
|
2026-02-02 15:12:23 +01:00
|
|
|
if (options.stdin !== undefined) {
|
|
|
|
|
child.stdin.write(options.stdin);
|
|
|
|
|
}
|
|
|
|
|
child.stdin.end();
|
|
|
|
|
|
2026-01-30 19:41:00 +01:00
|
|
|
child.stdout.on('data', (chunk) => {
|
|
|
|
|
if (options.binaryStdout) {
|
|
|
|
|
stdoutBuffer = Buffer.concat([
|
|
|
|
|
stdoutBuffer ?? Buffer.alloc(0),
|
|
|
|
|
Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk),
|
|
|
|
|
]);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const text = String(chunk);
|
|
|
|
|
stdout += text;
|
|
|
|
|
options.onStdoutChunk?.(text);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
child.stderr.on('data', (chunk) => {
|
|
|
|
|
const text = String(chunk);
|
|
|
|
|
stderr += text;
|
|
|
|
|
options.onStderrChunk?.(text);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
child.on('error', (err) => {
|
2026-03-28 18:55:53 +01:00
|
|
|
if (timeoutHandle) clearTimeout(timeoutHandle);
|
2026-01-30 19:41:00 +01:00
|
|
|
const code = (err as NodeJS.ErrnoException).code;
|
|
|
|
|
if (code === 'ENOENT') {
|
|
|
|
|
reject(new AppError('TOOL_MISSING', `${cmd} not found in PATH`, { cmd }, err));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
reject(new AppError('COMMAND_FAILED', `Failed to run ${cmd}`, { cmd, args }, err));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
child.on('close', (code) => {
|
2026-03-28 18:55:53 +01:00
|
|
|
if (timeoutHandle) clearTimeout(timeoutHandle);
|
2026-01-30 19:41:00 +01:00
|
|
|
const exitCode = code ?? 1;
|
2026-03-28 18:55:53 +01:00
|
|
|
if (didTimeout && timeoutMs) {
|
|
|
|
|
reject(
|
|
|
|
|
new AppError('COMMAND_FAILED', `${cmd} timed out after ${timeoutMs}ms`, {
|
|
|
|
|
cmd,
|
|
|
|
|
args,
|
|
|
|
|
stdout,
|
|
|
|
|
stderr,
|
|
|
|
|
exitCode,
|
|
|
|
|
timeoutMs,
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-01-30 19:41:00 +01:00
|
|
|
if (exitCode !== 0 && !options.allowFailure) {
|
|
|
|
|
reject(
|
|
|
|
|
new AppError('COMMAND_FAILED', `${cmd} exited with code ${exitCode}`, {
|
|
|
|
|
cmd,
|
|
|
|
|
args,
|
|
|
|
|
stdout,
|
|
|
|
|
stderr,
|
|
|
|
|
exitCode,
|
2026-02-23 18:34:27 +01:00
|
|
|
processExitError: true,
|
2026-01-30 19:41:00 +01:00
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
resolve({ stdout, stderr, exitCode, stdoutBuffer });
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 21:33:45 +01:00
|
|
|
export function runCmdBackground(
|
|
|
|
|
cmd: string,
|
|
|
|
|
args: string[],
|
|
|
|
|
options: ExecOptions = {},
|
|
|
|
|
): ExecBackgroundResult {
|
|
|
|
|
const child = spawn(cmd, args, {
|
|
|
|
|
cwd: options.cwd,
|
|
|
|
|
env: options.env,
|
|
|
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
2026-02-20 17:18:37 +01:00
|
|
|
detached: options.detached,
|
2026-02-02 21:33:45 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let stdout = '';
|
|
|
|
|
let stderr = '';
|
|
|
|
|
|
|
|
|
|
child.stdout.setEncoding('utf8');
|
|
|
|
|
child.stderr.setEncoding('utf8');
|
|
|
|
|
|
|
|
|
|
child.stdout.on('data', (chunk) => {
|
|
|
|
|
stdout += chunk;
|
|
|
|
|
});
|
|
|
|
|
child.stderr.on('data', (chunk) => {
|
|
|
|
|
stderr += chunk;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const wait = new Promise<ExecResult>((resolve, reject) => {
|
|
|
|
|
child.on('error', (err) => {
|
|
|
|
|
const code = (err as NodeJS.ErrnoException).code;
|
|
|
|
|
if (code === 'ENOENT') {
|
|
|
|
|
reject(new AppError('TOOL_MISSING', `${cmd} not found in PATH`, { cmd }, err));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
reject(new AppError('COMMAND_FAILED', `Failed to run ${cmd}`, { cmd, args }, err));
|
|
|
|
|
});
|
|
|
|
|
child.on('close', (code) => {
|
|
|
|
|
const exitCode = code ?? 1;
|
|
|
|
|
if (exitCode !== 0 && !options.allowFailure) {
|
|
|
|
|
reject(
|
|
|
|
|
new AppError('COMMAND_FAILED', `${cmd} exited with code ${exitCode}`, {
|
|
|
|
|
cmd,
|
|
|
|
|
args,
|
|
|
|
|
stdout,
|
|
|
|
|
stderr,
|
|
|
|
|
exitCode,
|
2026-02-23 18:34:27 +01:00
|
|
|
processExitError: true,
|
2026-02-02 21:33:45 +01:00
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
resolve({ stdout, stderr, exitCode });
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return { child, wait };
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-30 19:41:00 +01:00
|
|
|
function resolveWhichArgs(cmd: string): { shell: string; args: string[] } {
|
|
|
|
|
if (process.platform === 'win32') {
|
|
|
|
|
return { shell: 'cmd.exe', args: ['/c', 'where', cmd] };
|
|
|
|
|
}
|
|
|
|
|
return { shell: 'bash', args: ['-lc', `command -v ${cmd}`] };
|
|
|
|
|
}
|
2026-02-12 17:17:56 +01:00
|
|
|
|
|
|
|
|
function normalizeTimeoutMs(value: number | undefined): number | undefined {
|
|
|
|
|
if (!Number.isFinite(value)) return undefined;
|
|
|
|
|
const timeout = Math.floor(value as number);
|
|
|
|
|
if (timeout <= 0) return undefined;
|
|
|
|
|
return timeout;
|
|
|
|
|
}
|