netsky-sh 0.1.7

Shell utilities for netsky: tmux, git, process wrapping
Documentation
//! Shell utilities for netsky: tmux, git, process wrapping.
//!
//! Forked from `dkdc-io/sh` crate `sh-core` (MIT). Extended for the
//! netsky spawn path — primarily tmux `new-session` with per-session
//! environment propagation, which `netsky-core::spawn` relies on.
//!
//! Minimal, synchronous shell abstractions. No async runtime required.

use std::path::PathBuf;
use std::process::Command;

pub mod git;
pub mod tmux;

#[derive(Debug)]
pub enum Error {
    CommandNotFound(String),
    CommandFailed { cmd: String, detail: String },
    Tmux(String),
    Io(std::io::Error),
}

impl std::fmt::Display for Error {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::CommandNotFound(s) => write!(f, "command not found: {s}"),
            Self::CommandFailed { cmd, detail } => {
                write!(f, "command failed: {cmd}{detail}")
            }
            Self::Tmux(s) => write!(f, "tmux error: {s}"),
            Self::Io(e) => write!(f, "io error: {e}"),
        }
    }
}

impl std::error::Error for Error {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            Self::Io(e) => Some(e),
            _ => None,
        }
    }
}

impl From<std::io::Error> for Error {
    fn from(e: std::io::Error) -> Self {
        Self::Io(e)
    }
}

/// Find a command in PATH, returning its absolute path or None.
pub fn which(cmd: &str) -> Option<PathBuf> {
    ::which::which(cmd).ok()
}

/// Require a command to exist in PATH.
pub fn require(cmd: &str) -> Result<PathBuf, Error> {
    ::which::which(cmd).map_err(|_| Error::CommandNotFound(cmd.to_string()))
}

/// Run a command and return its stdout as a String.
pub fn run(program: &str, args: &[&str]) -> Result<String, Error> {
    run_with_env(program, args, &[])
}

/// Run a command with extra environment variables.
pub fn run_with_env(program: &str, args: &[&str], env: &[(&str, &str)]) -> Result<String, Error> {
    require(program)?;

    let mut command = Command::new(program);
    command.args(args);
    for (k, v) in env {
        command.env(k, v);
    }
    let output = command.output()?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
        return Err(Error::CommandFailed {
            cmd: format!("{program} {}", args.first().unwrap_or(&"")),
            detail: stderr,
        });
    }

    Ok(String::from_utf8_lossy(&output.stdout).into_owned())
}