procpilot 0.2.0

Production-grade subprocess runner with typed errors, retry, and timeout
Documentation
# procpilot

Production-grade subprocess runner for Rust — typed errors, retry with backoff, timeout, stdin piping, secret redaction.

Built for CLI tools that spawn external processes and need precise failure handling. Not intended for shell scripting (see [xshell](https://crates.io/crates/xshell) for that).

## Why not `std::process::Command`?

`Command::output()` returns a status the caller must remember to check. `Command::spawn()` gives you a `Child` but no help with timeout, retry, or deadlock-safe pipe draining. Every production CLI ends up writing the same wrapping layer.

`procpilot` is that layer.

- **Typed errors** — [`RunError`] distinguishes `Spawn` (couldn't start), `NonZeroExit` (command ran and failed, with captured stdout/stderr), and `Timeout` (killed after budget).
- **Retry with exponential backoff + jitter** — [`Cmd::retry`] / [`Cmd::retry_when`].
- **Timeout + deadline** — per-attempt timeout or overall wall-clock budget across retries.
- **Stdin piping** — owned bytes (reusable across retries) or a boxed `Read` (one-shot streaming).
- **Stderr routing** — capture / inherit / null / redirect-to-file via [`Redirection`].
- **Secret redaction** — [`Cmd::secret`] replaces args with `<secret>` in error output and logs.

## Usage

```toml
[dependencies]
procpilot = "0.2"
```

```rust
use std::time::Duration;
use procpilot::{Cmd, RunError};

let output = Cmd::new("git")
    .args(["fetch", "origin"])
    .in_dir("/repo")
    .env("GIT_TERMINAL_PROMPT", "0")
    .timeout(Duration::from_secs(30))
    .run()?;
# Ok::<(), procpilot::RunError>(())
```

### Error handling

```rust
use procpilot::{Cmd, RunError};

match Cmd::new("git").args(["show", "maybe-missing-ref"]).run() {
    Ok(output) => Some(output.stdout),
    Err(RunError::NonZeroExit { .. }) => None,   // legitimate in-band signal
    Err(e) => return Err(e.into()),
}
# ; Ok::<(), anyhow::Error>(())
```

`RunError` is `#[non_exhaustive]`; include a wildcard arm. All variants carry a [`CmdDisplay`] that renders the command shell-style (with secret redaction if `.secret()` was set). `NonZeroExit` and `Timeout` include up to the last 128 KiB of stdout/stderr.

### Retry

```rust
use procpilot::{Cmd, RunError, RetryPolicy};

Cmd::new("git")
    .args(["pull"])
    .in_dir("/repo")
    .retry(RetryPolicy::default())
    .retry_when(|err| matches!(err, RunError::NonZeroExit { stderr, .. } if stderr.contains(".lock")))
    .run()?;
# Ok::<(), RunError>(())
```

### Deadline across retries

`.timeout()` bounds a single attempt; `.deadline()` bounds the whole operation (including retry backoff sleeps). Combine them when you want "retry up to 3× but never exceed 10 seconds total".

```rust
use std::time::{Duration, Instant};
use procpilot::{Cmd, RetryPolicy};

Cmd::new("git")
    .args(["fetch", "origin"])
    .in_dir("/repo")
    .timeout(Duration::from_secs(3))
    .deadline(Instant::now() + Duration::from_secs(10))
    .retry(RetryPolicy::default())
    .run()?;
# Ok::<(), procpilot::RunError>(())
```

### Inheriting stderr (live progress)

When the child prompts the user or should stream progress to the terminal, route stderr with `Redirection::Inherit` instead of capturing it.

```rust
use procpilot::{Cmd, Redirection};

Cmd::new("cargo")
    .args(["build", "--release"])
    .stderr(Redirection::Inherit)
    .run()?;
# Ok::<(), procpilot::RunError>(())
```

### Stdin

```rust
use procpilot::Cmd;

let manifest = "apiVersion: v1\nkind: ConfigMap\n...";
Cmd::new("kubectl").args(["apply", "-f", "-"]).stdin(manifest).run()?;
# Ok::<(), procpilot::RunError>(())
```

### Secret redaction

```rust
use procpilot::Cmd;
Cmd::new("docker").args(["login", "-p", "hunter2"]).secret().run()?;
// Error messages show `docker <secret>` instead of the token.
# Ok::<(), procpilot::RunError>(())
```

## License

Licensed under either [Apache-2.0](LICENSE-APACHE) or [MIT](LICENSE-MIT) at your option.