mirror of
https://github.com/harivansh-afk/clanker-agent.git
synced 2026-04-18 20:03:36 +00:00
- Copy all pi-mono source into apps/companion-os/ - Update Dockerfile to COPY pre-built binary instead of downloading from GitHub Releases - Update deploy-staging.yml to build pi from source (bun compile) before Docker build - Add apps/companion-os/** to path triggers - No more cross-repo dispatch needed Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
104 lines
2.3 KiB
TypeScript
104 lines
2.3 KiB
TypeScript
/**
|
|
* Shared command execution utilities for extensions and custom tools.
|
|
*/
|
|
|
|
import { spawn } from "node:child_process";
|
|
|
|
/**
|
|
* Options for executing shell commands.
|
|
*/
|
|
export interface ExecOptions {
|
|
/** AbortSignal to cancel the command */
|
|
signal?: AbortSignal;
|
|
/** Timeout in milliseconds */
|
|
timeout?: number;
|
|
/** Working directory */
|
|
cwd?: string;
|
|
}
|
|
|
|
/**
|
|
* Result of executing a shell command.
|
|
*/
|
|
export interface ExecResult {
|
|
stdout: string;
|
|
stderr: string;
|
|
code: number;
|
|
killed: boolean;
|
|
}
|
|
|
|
/**
|
|
* Execute a shell command and return stdout/stderr/code.
|
|
* Supports timeout and abort signal.
|
|
*/
|
|
export async function execCommand(
|
|
command: string,
|
|
args: string[],
|
|
cwd: string,
|
|
options?: ExecOptions,
|
|
): Promise<ExecResult> {
|
|
return new Promise((resolve) => {
|
|
const proc = spawn(command, args, {
|
|
cwd,
|
|
shell: false,
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
});
|
|
|
|
let stdout = "";
|
|
let stderr = "";
|
|
let killed = false;
|
|
let timeoutId: NodeJS.Timeout | undefined;
|
|
|
|
const killProcess = () => {
|
|
if (!killed) {
|
|
killed = true;
|
|
proc.kill("SIGTERM");
|
|
// Force kill after 5 seconds if SIGTERM doesn't work
|
|
setTimeout(() => {
|
|
if (!proc.killed) {
|
|
proc.kill("SIGKILL");
|
|
}
|
|
}, 5000);
|
|
}
|
|
};
|
|
|
|
// Handle abort signal
|
|
if (options?.signal) {
|
|
if (options.signal.aborted) {
|
|
killProcess();
|
|
} else {
|
|
options.signal.addEventListener("abort", killProcess, { once: true });
|
|
}
|
|
}
|
|
|
|
// Handle timeout
|
|
if (options?.timeout && options.timeout > 0) {
|
|
timeoutId = setTimeout(() => {
|
|
killProcess();
|
|
}, options.timeout);
|
|
}
|
|
|
|
proc.stdout?.on("data", (data) => {
|
|
stdout += data.toString();
|
|
});
|
|
|
|
proc.stderr?.on("data", (data) => {
|
|
stderr += data.toString();
|
|
});
|
|
|
|
proc.on("close", (code) => {
|
|
if (timeoutId) clearTimeout(timeoutId);
|
|
if (options?.signal) {
|
|
options.signal.removeEventListener("abort", killProcess);
|
|
}
|
|
resolve({ stdout, stderr, code: code ?? 0, killed });
|
|
});
|
|
|
|
proc.on("error", (_err) => {
|
|
if (timeoutId) clearTimeout(timeoutId);
|
|
if (options?.signal) {
|
|
options.signal.removeEventListener("abort", killProcess);
|
|
}
|
|
resolve({ stdout, stderr, code: 1, killed });
|
|
});
|
|
});
|
|
}
|