use std::io;
use std::path::{Path, PathBuf};
use std::process::Command;
const INSTALLED_TO_PREFIX: &str = "par-mux: installed agent skill to ";
pub fn install_agent_skill() -> io::Result<PathBuf> {
let mut failures = Vec::new();
for bin in par_mux_binary_candidates()? {
match run_install(&bin) {
Ok(path) => return Ok(path),
Err(err) => failures.push(format!("{}: {err}", bin.display())),
}
}
Err(io::Error::other(format!(
"could not install the par-mux agent skill — {}",
failures.join("; ")
)))
}
fn run_install(bin: &Path) -> io::Result<PathBuf> {
let output = Command::new(bin)
.arg("--install-skill")
.output()
.map_err(|err| io::Error::other(format!("running it failed: {err}")))?;
if !output.status.success() {
return Err(io::Error::other(format!(
"`--install-skill` exited with {}: {}",
output.status,
String::from_utf8_lossy(&output.stderr).trim()
)));
}
parse_installed_path(&String::from_utf8_lossy(&output.stdout))
}
fn parse_installed_path(stdout: &str) -> io::Result<PathBuf> {
let line = stdout.lines().last().unwrap_or_default();
let path = line.strip_prefix(INSTALLED_TO_PREFIX).unwrap_or_default();
if path.is_empty() {
return Err(io::Error::other(format!(
"unrecognized --install-skill output: {line:?}"
)));
}
Ok(PathBuf::from(path))
}
pub(crate) fn par_mux_binary_candidates() -> io::Result<Vec<PathBuf>> {
#[cfg(unix)]
let file_name = "par-mux";
#[cfg(windows)]
let file_name = "par-mux.exe";
let mut candidates = vec![exe_sibling(file_name)?];
if let Some(path_var) = std::env::var_os("PATH") {
for dir in std::env::split_paths(&path_var) {
let candidate = dir.join(file_name);
if candidate.is_file() {
candidates.push(candidate);
}
}
}
Ok(candidates)
}
fn exe_sibling(file_name: &str) -> io::Result<PathBuf> {
let exe = std::env::current_exe()?;
let Some(mut dir) = exe.parent().map(Path::to_path_buf) else {
return Err(io::Error::new(
io::ErrorKind::NotFound,
format!(
"cannot locate the par-mux binary: {} has no parent directory",
exe.display()
),
));
};
if dir.file_name() == Some(std::ffi::OsStr::new("deps"))
&& let Some(parent) = dir.parent()
{
dir = parent.to_path_buf();
}
Ok(dir.join(file_name))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_the_installed_path_from_binary_stdout() {
let path = parse_installed_path(
"par-mux: installed agent skill to /home/u/.claude/skills/par-mux/SKILL.md\n",
)
.expect("parses");
assert_eq!(
path,
PathBuf::from("/home/u/.claude/skills/par-mux/SKILL.md")
);
}
#[test]
fn unrecognized_output_is_an_error() {
let err = parse_installed_path("something else\n")
.unwrap_err()
.to_string();
assert!(err.contains("unrecognized --install-skill output"), "{err}");
}
#[test]
fn empty_output_is_an_error() {
assert!(parse_installed_path("").is_err());
}
}