use std::fs;
use std::io;
use std::path::{Path, PathBuf};
const PI_EXTENSION_INSTALL_NAME: &str = "par-mux-agent-state.ts";
const OMP_EXTENSION_INSTALL_NAME: &str = "par-mux-omp-agent-state.ts";
const PI_EXTENSION_ASSET: &str = include_str!("../mux_extensions/pi/par-mux-agent-state.ts");
const OMP_EXTENSION_ASSET: &str = include_str!("../mux_extensions/omp/par-mux-omp-agent-state.ts");
const PI_MARKER: &str = "PAR_MUX_INTEGRATION_ID=pi";
const OMP_MARKER: &str = "PAR_MUX_INTEGRATION_ID=omp";
const PI_CODING_AGENT_DIR_ENV_VAR: &str = "PI_CODING_AGENT_DIR";
const OMP_CONFIG_DIR_ENV_VAR: &str = "PI_CONFIG_DIR";
pub struct ExtensionRemoval {
pub path: PathBuf,
pub removed: bool,
}
pub fn pi_extension_dir() -> io::Result<PathBuf> {
Ok(
agent_dir_from_env_or_home(PI_CODING_AGENT_DIR_ENV_VAR, &[".pi", "agent"])?
.join("extensions"),
)
}
pub fn omp_extension_dir() -> io::Result<PathBuf> {
if let Some(value) = env_var_nonempty(PI_CODING_AGENT_DIR_ENV_VAR) {
return expand_tilde(PathBuf::from(value)).map(|path| path.join("extensions"));
}
let config_dir = env_var_nonempty(OMP_CONFIG_DIR_ENV_VAR).unwrap_or_else(|| ".omp".into());
Ok(home_dir()?
.join(config_dir)
.join("agent")
.join("extensions"))
}
pub fn install_pi_extension() -> io::Result<PathBuf> {
install_pi_extension_into(&pi_extension_dir()?)
}
pub fn install_pi_extension_into(dir: &Path) -> io::Result<PathBuf> {
ensure_extension_dir(dir, "pi")?;
let path = dir.join(PI_EXTENSION_INSTALL_NAME);
fs::write(&path, PI_EXTENSION_ASSET)?;
Ok(path)
}
pub fn install_omp_extension() -> io::Result<PathBuf> {
let pi_dir = pi_extension_dir()?;
let omp_dir = omp_extension_dir()?;
install_omp_extension_into(&pi_dir, &omp_dir)
}
pub fn install_omp_extension_into(pi_dir: &Path, omp_dir: &Path) -> io::Result<PathBuf> {
if omp_dir == pi_dir {
return Err(io::Error::other(format!(
"Pi and OMP resolve to the same extension directory at {}; configure separate agent directories before installing the OMP extension",
omp_dir.display()
)));
}
ensure_extension_dir(omp_dir, "omp")?;
let path = omp_dir.join(OMP_EXTENSION_INSTALL_NAME);
fs::write(&path, OMP_EXTENSION_ASSET)?;
Ok(path)
}
pub fn uninstall_pi_extension() -> io::Result<ExtensionRemoval> {
uninstall_from(&pi_extension_dir()?, PI_EXTENSION_INSTALL_NAME, PI_MARKER)
}
pub fn uninstall_omp_extension() -> io::Result<ExtensionRemoval> {
uninstall_from(
&omp_extension_dir()?,
OMP_EXTENSION_INSTALL_NAME,
OMP_MARKER,
)
}
fn ensure_extension_dir(dir: &Path, agent: &str) -> io::Result<()> {
if dir.is_dir() {
return Ok(());
}
if dir.parent().is_some_and(|parent| parent.is_dir()) {
return fs::create_dir_all(dir);
}
Err(io::Error::other(format!(
"{agent} extension directory not found at {}. install {agent} first",
dir.display()
)))
}
fn uninstall_from(dir: &Path, install_name: &str, marker: &str) -> io::Result<ExtensionRemoval> {
let path = dir.join(install_name);
if !path.is_file() {
return Ok(ExtensionRemoval {
path,
removed: false,
});
}
let content = fs::read_to_string(&path)?;
if !content.contains(marker) {
return Ok(ExtensionRemoval {
path,
removed: false,
});
}
fs::remove_file(&path)?;
Ok(ExtensionRemoval {
path,
removed: true,
})
}
fn env_var_nonempty(name: &str) -> Option<std::ffi::OsString> {
std::env::var_os(name).filter(|value| !value.is_empty())
}
fn agent_dir_from_env_or_home(env_var: &str, home_segments: &[&str]) -> io::Result<PathBuf> {
if let Some(value) = env_var_nonempty(env_var) {
return expand_tilde(PathBuf::from(value));
}
let mut dir = home_dir()?;
for segment in home_segments {
dir = dir.join(segment);
}
Ok(dir)
}
fn home_dir() -> io::Result<PathBuf> {
dirs::home_dir().ok_or_else(|| io::Error::other("Could not determine home directory"))
}
fn expand_tilde(path: PathBuf) -> io::Result<PathBuf> {
let Some(text) = path.to_str() else {
return Ok(path);
};
if text == "~" {
return home_dir();
}
if let Some(rest) = text.strip_prefix("~/") {
return Ok(home_dir()?.join(rest));
}
Ok(path)
}
#[cfg(test)]
mod tests {
use super::*;
fn temp_root() -> tempfile::TempDir {
tempfile::tempdir().expect("temp dir")
}
#[test]
fn install_writes_the_bundled_asset_with_markers() {
let root = temp_root();
let pi_home = root.path().join("pi-home");
fs::create_dir_all(&pi_home).unwrap();
let path = install_pi_extension_into(&pi_home.join("extensions")).unwrap();
assert_eq!(
path,
pi_home.join("extensions").join(PI_EXTENSION_INSTALL_NAME)
);
let content = fs::read_to_string(&path).unwrap();
assert_eq!(content, PI_EXTENSION_ASSET);
assert!(content.contains(PI_MARKER));
assert!(
!content.contains("HERDR_"),
"the port must be fully env-renamed: {content}"
);
assert!(
!content.contains("source = \"herdr:"),
"source ids must be par-mux namespaced: {content}"
);
}
#[test]
fn omp_asset_carries_the_omp_marker() {
let root = temp_root();
let omp_agent_root = root.path().join("omp-home").join("agent");
fs::create_dir_all(&omp_agent_root).unwrap();
let path = install_omp_extension_into(
&root.path().join("pi-home"),
&omp_agent_root.join("extensions"),
)
.unwrap();
let content = fs::read_to_string(&path).unwrap();
assert_eq!(content, OMP_EXTENSION_ASSET);
assert!(content.contains(OMP_MARKER));
assert!(content.contains("PAR_MUX_OMP_IDLE_DEBOUNCE_MS"));
}
#[test]
fn reinstall_overwrites_cleanly() {
let root = temp_root();
let pi_home = root.path().join("pi-home");
fs::create_dir_all(&pi_home).unwrap();
let path = install_pi_extension_into(&pi_home.join("extensions")).unwrap();
fs::write(&path, "// user edit").unwrap();
install_pi_extension_into(&pi_home.join("extensions")).unwrap();
assert_eq!(fs::read_to_string(&path).unwrap(), PI_EXTENSION_ASSET);
}
#[test]
fn missing_agent_directory_is_an_actionable_error() {
let root = temp_root();
let err = install_pi_extension_into(&root.path().join("never").join("extensions"))
.unwrap_err()
.to_string();
assert!(
err.contains("pi extension directory not found at"),
"error must name the agent and the path: {err}"
);
assert!(
err.contains("install pi first"),
"error must say what to do: {err}"
);
assert!(
err.contains(
root.path()
.join("never")
.join("extensions")
.display()
.to_string()
.as_str()
),
"error must contain the resolved path: {err}"
);
}
#[test]
fn omp_install_refuses_when_dirs_collide() {
let root = temp_root();
let same = root.path().join("same").join("extensions");
fs::create_dir_all(root.path().join("same")).unwrap();
let err = install_omp_extension_into(&same, &same)
.unwrap_err()
.to_string();
assert!(
err.contains("same extension directory"),
"collision must be named: {err}"
);
assert!(err.contains(same.display().to_string().as_str()));
assert!(
!same.exists(),
"the collision must refuse before writing anything"
);
}
#[test]
fn uninstall_removes_only_par_terms_own_file() {
let root = temp_root();
let pi_home = root.path().join("pi-home");
let ext_dir = pi_home.join("extensions");
fs::create_dir_all(&ext_dir).unwrap();
let path = install_pi_extension_into(&ext_dir).unwrap();
let sibling = ext_dir.join("user-extension.ts");
fs::write(&sibling, "// user's own").unwrap();
let removal = uninstall_from(&ext_dir, PI_EXTENSION_INSTALL_NAME, PI_MARKER).unwrap();
assert!(removal.removed);
assert_eq!(removal.path, path);
assert!(!path.exists(), "our file is gone");
assert!(sibling.exists(), "other extensions are untouched");
let again = uninstall_from(&ext_dir, PI_EXTENSION_INSTALL_NAME, PI_MARKER).unwrap();
assert!(!again.removed, "uninstall is idempotent");
}
#[test]
fn uninstall_leaves_a_foreign_file_with_our_name_alone() {
let root = temp_root();
let ext_dir = root.path().join("ext");
fs::create_dir_all(&ext_dir).unwrap();
let path = ext_dir.join(PI_EXTENSION_INSTALL_NAME);
fs::write(&path, "// a user file that happens to share our name").unwrap();
let removal = uninstall_from(&ext_dir, PI_EXTENSION_INSTALL_NAME, PI_MARKER).unwrap();
assert!(!removal.removed, "a file without our marker is not ours");
assert!(path.exists(), "the user's file survives");
assert_eq!(
fs::read_to_string(&path).unwrap(),
"// a user file that happens to share our name"
);
}
}