/** * Bash command execution with streaming support and cancellation. * * Uses the embedded bash engine (pi-shell) through native bindings for shell * execution. */ import type { ImageContent } from "@oh-my-pi/pi-ai"; import { type MinimizerOptions, type PipeStreams, type ShellFilesystem } from "@oh-my-pi/pi-natives"; import { type ShellTrace } from "@oh-my-pi/pi-wire"; import type { MinimizedMeta } from "@oh-my-pi/pi-tui/tools/output-meta"; import { type OutputSummary } from "@oh-my-pi/pi-tui/tools/streaming-output"; import { type ShellMinimizerSettings } from "./settings.js"; export interface BashExecutorOptions { cwd?: string; /** Milliseconds before aborting the command; 0 disables the executor deadline. */ timeout?: number; onChunk?: (chunk: string) => void; /** * Receives the sink's current inline view ({@link OutputSink.preview}: the * body the final result will carry so far) at the `onChunk` cadence. Use it * for live previews instead of re-buffering `onChunk` chunks. */ onPreview?: (text: string) => void; chunkThrottleMs?: number; signal?: AbortSignal; /** Session key suffix to isolate shell sessions per agent */ sessionKey?: string; /** Additional environment variables to inject */ env?: Record; /** Run through the configured user shell instead of the embedded shell parsing directly. */ useUserShell?: boolean; /** Run supported user shells (zsh/fish) on a headless PTY; requires `useUserShell`. */ pty?: BashPtyOptions; /** * Refuse git commands that discard or move shared work (`bash.gitGuard`), * enforced by the embedded shell's `git` builtin. */ gitGuard?: boolean; /** ovnfs command for the git guard's separate-checkout guidance. */ ovnfs?: string; /** * Filesystem for `scheme://` paths in this run of the embedded shell (a URL * `cwd` included). External shells and processes never see it. */ filesystem?: ShellFilesystem; /** * Receives a copy of every byte crossing the command's pipes in the * embedded shell (`proc:///streams`). The user-shell paths ignore it. */ streams?: PipeStreams; /** * Receives the embedded shell's live trace of the command (pi-shell's * `RunTrace` snapshot) every {@link TRACE_POLL_MS} while it changed, and * once more after the run settles. The PTY path never calls it. */ onTrace?: (trace: ShellTrace) => void; /** * Invoked once the embedded shell starts the command, with a probe of the * live pids it spawned (empty once the run ends). Not called on the * user-shell paths. */ onStart?: (pids: () => readonly number[]) => void; /** * Invoked before the run when the session's persistent shell is busy with * another command, so this one runs in a fork of it instead: it sees the * session's shell state as of before that command, and the state it sets * is discarded. */ onSessionShellBusy?: () => void; /** * Asked once the run settles: `false` means the agent session that owns * `sessionKey` was left (new, fork, switch, branch) while the command ran, * so the persistent shell it used is dropped instead of kept for a next call. */ isSessionCurrent?: () => boolean; /** * `false` skips the shell output minimizer (`shellMinimizer.*`) for this run: the session's Spotlight * experiment shapes the output instead. Default: the setting decides. */ minimize?: boolean; /** Artifact path/id for full output storage */ artifactPath?: string; artifactId?: string; /** * Invoked when the native minimizer rewrote the command's output, giving * the caller a chance to persist the lossless original capture (typically * via the session's `ArtifactManager`). The returned id is spliced into * the sink output as `artifact://` so the agent can retrieve the raw * bytes. Return `undefined` to skip the footer. */ onMinimizedSave?: (originalText: string, info: { filter: string; inputBytes: number; outputBytes: number; }) => Promise; } /** Viewport + raw-output callback enabling the user-shell PTY path (`!` hotkey). */ export interface BashPtyOptions { cols: number; rows: number; /** Receives raw PTY bytes (ANSI intact) for virtual-terminal rendering. */ onChunk: (chunk: string) => void; /** * Receives a writer for the PTY's stdin once the command starts, so the user * can answer prompts (`read`, shell-startup questions). Writes after exit are dropped. */ onStdin?: (write: (data: string) => void) => void; } /** A command's captured output (the sink's summary, column-cap and middle-elision fields included) and how it ended. */ export interface BashResult extends OutputSummary { exitCode: number | undefined; cancelled: boolean; /** True when the command was killed by its timeout deadline (not a user abort). */ timedOut?: boolean; /** The final trace snapshot of an embedded-shell run started with `onTrace`. */ trace?: ShellTrace; /** The native minimizer's rewrite, set when its output replaced the raw stream. */ minimized?: MinimizedMeta; workingDir?: string; /** Terminal graphics extracted from raw stdout before sanitization or truncation. */ images?: ImageContent[]; } export interface DirenvPreflightOptions { /** Caller-supplied env overlay; these values win over direnv-provided ones. */ callerEnv?: Record; signal?: AbortSignal; /** Full direnv-load budget (`bash.direnvLoadTimeoutMs`). A positive * `callerTimeoutMs` clamps the effective load below this; `0`/undefined * leaves the full budget. */ timeoutMs?: number; /** The caller's command deadline (ms). A positive value clamps the direnv * load so a cold `.envrc` can't outlast a short-timeout command; `0` or * undefined means "no caller clamp" — the load keeps its full `timeoutMs` * budget (a disabled command deadline is NOT a 0 ms load). Centralizing the * clamp here keeps every backend (executeBash, ACP terminal, PTY) on one * contract instead of each re-deriving it. */ callerTimeoutMs?: number; /** `bash.direnv` setting — `"off"` skips the load entirely. */ direnvSetting: "auto" | "off"; /** Shell wrapper prefix (profiler/strace) to place *after* the unset prefix, * matching `executeBash`'s ordering. Backends that apply their own shell * wrapping (ACP `wrapShellLineForClientTerminal`) omit this. */ commandPrefix?: string | undefined; } /** * Load the repo's direnv/devenv env and fold it into a `(command, env)` pair so * every bash backend (one-shot `executeBash`, ACP client terminal, PTY) exposes * the same devenv tools. Encapsulates: load the diff, merge `set` under the * caller's overlay (caller wins), and prepend a regex-gated `unset -v` for * variables the `.envrc` removes (skipping any the caller re-supplied). * * Returns the possibly-prefixed command plus the merged env, or the inputs * unchanged (`env` = `callerEnv`) when direnv is off, absent, or has no `.envrc`. * Pure transform: does NOT layer non-interactive env defaults — that stays the * caller's job (so interactive PTY/ACP paths keep their own env shape). */ export declare function applyDirenvPreflight(command: string, cwd: string, opts: DirenvPreflightOptions): Promise<{ command: string; env: Record | undefined; }>; /** * Drops every persistent Shell owned by an agent session (keys built with * `agentSessionKey` as the {@link BashExecutorOptions.sessionKey}). The map is * process-global, so without this each disposed session keeps its native shell * for the life of the process. A Shell with live background jobs is retained * until they exit, matching the `:async:` teardown; an in-flight run keeps its * own reference and drops the Shell when it settles. */ export declare function releaseShellSessions(agentSessionKey: string | undefined): void; /** Translate `ShellMinimizerSettings` into native `MinimizerOptions`, or `undefined` when disabled. */ export declare function buildMinimizerOptions(group: ShellMinimizerSettings): MinimizerOptions | undefined; export declare function isPersistentShellCdCommand(command: string): boolean; export declare function executeBash(command: string, options?: BashExecutorOptions): Promise;