mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-16 04:01:56 +00:00
Breaking changes: - Settings: 'hooks' and 'customTools' arrays replaced with 'extensions' - CLI: '--hook' and '--tool' flags replaced with '--extension' / '-e' - API: HookMessage renamed to CustomMessage, role 'hookMessage' to 'custom' - API: FileSlashCommand renamed to PromptTemplate - API: discoverSlashCommands() renamed to discoverPromptTemplates() - Directories: commands/ renamed to prompts/ for prompt templates Migration: - Session version bumped to 3 (auto-migrates v2 sessions) - Old 'hookMessage' role entries converted to 'custom' Structural changes: - src/core/hooks/ and src/core/custom-tools/ merged into src/core/extensions/ - src/core/slash-commands.ts renamed to src/core/prompt-templates.ts - examples/hooks/ and examples/custom-tools/ merged into examples/extensions/ - docs/hooks.md and docs/custom-tools.md merged into docs/extensions.md New test coverage: - test/extensions-runner.test.ts (10 tests) - test/extensions-discovery.test.ts (26 tests) - test/prompt-templates.test.ts
104 lines
2.2 KiB
TypeScript
104 lines
2.2 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 });
|
|
});
|
|
});
|
|
}
|