# `Omni.Tools.Bash.Runner`
[🔗](https://github.com/aaronrussell/omni_tools/blob/v0.1.0/lib/omni/tools/bash/runner.ex#L1)

Executes shell commands via a Port and returns captured output.

Each invocation spawns a new shell process, captures merged stdout/stderr,
and returns the output alongside the exit code. No state carries over
between calls.

    Runner.run("echo hello", {"/bin/bash", ["-c"]}, dir: "/tmp")
    #=> {:ok, %{output: "hello\n", exit_code: 0}}

    Runner.run("exit 1", {"/bin/bash", ["-c"]}, dir: "/tmp")
    #=> {:error, :nonzero, %{output: "", exit_code: 1}}

The runner executes arbitrary shell commands with full system access. It is
not a security boundary — OS-level sandboxing (containers, restricted users)
is the caller's responsibility.

## Options

  * `:dir` (required) — working directory for the command
  * `:env` — extra environment variables as `[{String.t(), String.t()}]`,
    merged additively with the inherited environment. Default `[]`
  * `:timeout` — execution timeout in milliseconds. Default `30_000`
  * `:max_output` — output truncation limit in bytes. Tail-biased, snapped
    to line boundaries. Default `50_000`
  * `:command_prefix` — string prepended to every command. Default `nil`

## Return values

    {:ok, %{output: "hello\n", exit_code: 0}}
    {:error, :nonzero, %{output: "error msg\n", exit_code: 1}}
    {:error, :timeout, %{output: "partial..."}}

On success, `exit_code` is always `0`. On a non-zero exit, the output
captured up to that point is included. On timeout, partial output
collected before the deadline is returned.

# `result`

```elixir
@type result() ::
  {:ok, %{output: String.t(), exit_code: 0}}
  | {:error, :nonzero, %{output: String.t(), exit_code: pos_integer()}}
  | {:error, :timeout, %{output: String.t()}}
```

Result of a command execution — success, non-zero exit, or timeout.

# `resolve_shell`

```elixir
@spec resolve_shell(keyword()) :: {String.t(), [String.t()]}
```

Resolves the shell to use for command execution.

Checks in order: explicit `:shell` option, `/bin/bash`, `/bin/sh`.
Returns a `{executable, args}` tuple suitable for `Port.open/2`.

Raises `ArgumentError` if no usable shell is found or the option is invalid.

    Runner.resolve_shell([])
    #=> {"/bin/bash", ["-c"]}

    Runner.resolve_shell(shell: {"/bin/zsh", ["-c"]})
    #=> {"/bin/zsh", ["-c"]}

# `run`

```elixir
@spec run(String.t(), {String.t(), [String.t()]}, keyword()) :: result()
```

Executes `command` in the given `shell` and returns captured output.

The `shell` argument is a `{executable, args}` tuple — the command string
is appended to `args` when spawning the port.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
