rlyx 0.3.1

rlyx is a fast release manager that automatically bumps versions, creates changelogs, tags commits, and publishes GitHub releases across JS, Rust, and Python projects with first class monorepos support.
Documentation
use anyhow::{anyhow, Result};
use regex::Regex;
use std::process::{Command as StdCommand, Stdio};

pub async fn run_capture_async(
    cmd: &str,
    args: &[&str],
) -> Result<String> {
    let output = tokio::process::Command::new(cmd)
        .args(args)
        .output()
        .await?;
    if !output.status.success() {
        let err = String::from_utf8_lossy(&output.stderr);
        return Err(anyhow!(
            "{} {:?} failed: {}",
            cmd,
            args,
            err.trim()
        ));
    }
    Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
}

pub async fn run_quiet_async(
    cmd: &str,
    args: &[&str],
    _label: &str,
) -> Result<()> {
    let status = tokio::process::Command::new(cmd)
        .args(args)
        .stdout(Stdio::null())
        .stderr(Stdio::inherit())
        .status()
        .await?;
    if !status.success() {
        return Err(anyhow!("{} {:?} failed", cmd, args));
    }
    Ok(())
}

pub async fn run_status_async(cmd: &str, args: &[&str]) -> bool {
    tokio::process::Command::new(cmd)
        .args(args)
        .status()
        .await
        .map(|s| s.success())
        .unwrap_or(false)
}

pub fn run_capture(cmd: &str, args: &[&str]) -> Result<String> {
    let output = StdCommand::new(cmd).args(args).output()?;
    if !output.status.success() {
        let err = String::from_utf8_lossy(&output.stderr);
        return Err(anyhow!(
            "{} {:?} failed: {}",
            cmd,
            args,
            err.trim()
        ));
    }
    Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
}

pub fn run_quiet(
    cmd: &str,
    args: &[&str],
    _label: &str,
) -> Result<()> {
    let status = StdCommand::new(cmd)
        .args(args)
        .stdout(Stdio::null())
        .stderr(Stdio::inherit())
        .status()?;
    if !status.success() {
        return Err(anyhow!("{} {:?} failed", cmd, args));
    }
    Ok(())
}

pub fn run_status(cmd: &str, args: &[&str]) -> bool {
    StdCommand::new(cmd)
        .args(args)
        .status()
        .map(|s| s.success())
        .unwrap_or(false)
}

pub fn render_template(tpl: &str, pairs: &[(&str, &str)]) -> String {
    let re = Regex::new(r"\{\{\s*([a-zA-Z0-9_]+)\s*\}\}").unwrap();
    let mut map: std::collections::HashMap<&str, &str> =
        std::collections::HashMap::new();
    for (k, v) in pairs {
        map.insert(*k, *v);
    }
    let replaced = re.replace_all(tpl, |caps: &regex::Captures| {
        let k = &caps[1];
        map.get::<str>(k).copied().unwrap_or("")
    });
    let s = replaced.to_string();
    let s = Regex::new(r"\n{3,}")
        .unwrap()
        .replace_all(&s, "\n\n")
        .to_string();
    let s = s.trim().to_string();
    if s.is_empty() {
        s
    } else {
        format!("{}\n", s)
    }
}

pub async fn run_shell_interactive_async(
    cmdline: &str,
) -> Result<()> {
    let shell = crate::shell::detect_shell();
    let sh = shell.to_string_lossy().to_string();
    let status = tokio::process::Command::new(&sh)
        .args(["-lc", cmdline])
        .stdin(Stdio::inherit())
        .stdout(Stdio::inherit())
        .stderr(Stdio::inherit())
        .status()
        .await?;
    if !status.success() {
        return Err(anyhow!("shell command failed: {}", cmdline));
    }
    Ok(())
}