Files
oh-my-pi/packages/coding-agent/src/core/bash-executor.ts
T
can1357 21af96d890 fix: added Cursor OAuth provider and fixed authentication flow race conditions
- Added Cursor OAuth provider support to CLI authentication flow.
- Fixed race conditions in OAuth callback handling and session picker cleanup.
- Improved JWT token parsing and error handling across OAuth providers.
- Replaced Bun.sleep() with abortable sleep for proper cancellation support.
- Enhanced process execution with concurrent stream reading and better timeout handling.
- Added cache cleanup mechanism for auth storage with 5-minute intervals.
2026-01-20 19:50:50 +01:00

141 lines
3.4 KiB
TypeScript

/**
* Bash command execution with streaming support and cancellation.
*
* Provides unified bash execution for AgentSession.executeBash() and direct calls.
*/
import { cspawn, Exception, ptree } from "@oh-my-pi/pi-utils";
import { getShellConfig } from "../utils/shell";
import { getOrCreateSnapshot, getSnapshotSourceCommand } from "../utils/shell-snapshot";
import { OutputSink } from "./streaming-output";
import type { BashOperations } from "./tools/bash";
export interface BashExecutorOptions {
cwd?: string;
timeout?: number;
onChunk?: (chunk: string) => void;
signal?: AbortSignal;
}
export interface BashResult {
output: string;
exitCode: number | undefined;
cancelled: boolean;
truncated: boolean;
fullOutputPath?: string;
}
export async function executeBash(command: string, options?: BashExecutorOptions): Promise<BashResult> {
const { shell, args, env, prefix } = await getShellConfig();
const snapshotPath = await getOrCreateSnapshot(shell, env);
const snapshotPrefix = getSnapshotSourceCommand(snapshotPath);
const prefixedCommand = prefix ? `${prefix} ${command}` : command;
const finalCommand = `${snapshotPrefix}${prefixedCommand}`;
const stream = new OutputSink({ onLine: options?.onChunk });
const child = cspawn([shell, ...args, finalCommand], {
cwd: options?.cwd,
env,
signal: options?.signal,
timeout: options?.timeout,
});
// Pump streams - errors during abort/timeout are expected
await Promise.allSettled([
child.stdout.pipeTo(stream.createWritable()),
child.stderr.pipeTo(stream.createWritable()),
])
.then(() => stream.close())
.catch(() => {});
// Wait for process exit
try {
await child.exited;
return {
exitCode: child.exitCode ?? 0,
cancelled: false,
...stream.dump(),
};
} catch (err) {
// Exception covers NonZeroExitError, AbortError, TimeoutError
if (err instanceof Exception) {
if (err.aborted) {
const isTimeout = err instanceof ptree.TimeoutError || err.message.toLowerCase().includes("timed out");
const annotation = isTimeout
? `Command timed out after ${Math.round((options?.timeout ?? 0) / 1000)} seconds`
: undefined;
return {
exitCode: undefined,
cancelled: true,
...stream.dump(annotation),
};
}
// NonZeroExitError
return {
exitCode: err.exitCode,
cancelled: false,
...stream.dump(),
};
}
throw err;
}
}
export async function executeBashWithOperations(
command: string,
cwd: string,
operations: BashOperations,
options?: BashExecutorOptions,
): Promise<BashResult> {
const stream = new OutputSink({ onLine: options?.onChunk });
const writable = stream.createWritable();
const writer = writable.getWriter();
const closeStreams = async () => {
try {
await writer.close();
} catch {}
try {
await writable.close();
} catch {}
try {
await stream.close();
} catch {}
};
try {
const result = await operations.exec(command, cwd, {
onData: (data) => writer.write(data),
signal: options?.signal,
timeout: options?.timeout,
});
await closeStreams();
const cancelled = options?.signal?.aborted ?? false;
return {
exitCode: cancelled ? undefined : (result.exitCode ?? undefined),
cancelled,
...stream.dump(),
};
} catch (err) {
await closeStreams();
if (options?.signal?.aborted) {
return {
exitCode: undefined,
cancelled: true,
...stream.dump(),
};
}
throw err;
}
}