use std::process::{Command, Stdio};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Manager {
Uv,
Pip,
Cargo,
Npm,
Brew,
Dotnet,
}
impl Manager {
#[must_use]
pub fn parse(s: &str) -> Option<Self> {
match s.trim().to_ascii_lowercase().as_str() {
"uv" => Some(Self::Uv),
"pip" | "pip3" => Some(Self::Pip),
"cargo" => Some(Self::Cargo),
"npm" => Some(Self::Npm),
"brew" | "homebrew" => Some(Self::Brew),
"dotnet" => Some(Self::Dotnet),
_ => None,
}
}
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Uv => "uv",
Self::Pip => "pip",
Self::Cargo => "cargo",
Self::Npm => "npm",
Self::Brew => "brew",
Self::Dotnet => "dotnet",
}
}
#[must_use]
pub fn program(self) -> String {
let key = format!("LEANCTX_BOOTSTRAP_{}", self.as_str().to_ascii_uppercase());
std::env::var(&key)
.ok()
.filter(|v| !v.trim().is_empty())
.unwrap_or_else(|| self.as_str().to_string())
}
#[must_use]
pub fn install_hint(self) -> &'static str {
match self {
Self::Uv => "install uv → https://docs.astral.sh/uv/getting-started/installation/",
Self::Pip => "install Python & pip → https://pip.pypa.io/en/stable/installation/",
Self::Cargo => "install Rust (cargo) → https://rustup.rs",
Self::Npm => "install Node.js (ships npm) → https://nodejs.org/",
Self::Brew => "install Homebrew → https://brew.sh",
Self::Dotnet => "install the .NET SDK → https://dotnet.microsoft.com/download",
}
}
#[must_use]
pub fn is_available(self) -> bool {
let prog = self.program();
if prog.contains('/') || prog.contains('\\') {
is_executable(std::path::Path::new(&prog))
} else {
binary_on_path(&prog)
}
}
#[must_use]
fn install_argv(self, package: &str, version: &str) -> Vec<String> {
let pkg = package.trim();
let ver = version.trim();
match self {
Self::Uv => vec!["tool".into(), "install".into(), format!("{pkg}=={ver}")],
Self::Pip => vec!["install".into(), "--user".into(), format!("{pkg}=={ver}")],
Self::Cargo => vec![
"install".into(),
package_base(pkg).into(),
"--version".into(),
ver.into(),
],
Self::Npm => vec!["install".into(), "-g".into(), format!("{pkg}@{ver}")],
Self::Brew => vec!["install".into(), pkg.into()],
Self::Dotnet => vec![
"tool".into(),
"install".into(),
"--global".into(),
package_base(pkg).into(),
"--version".into(),
ver.into(),
],
}
}
#[must_use]
fn uninstall_argv(self, package: &str) -> Vec<String> {
let base = package_base(package.trim());
match self {
Self::Uv => vec!["tool".into(), "uninstall".into(), base.into()],
Self::Pip => vec!["uninstall".into(), "-y".into(), base.into()],
Self::Npm => vec!["rm".into(), "-g".into(), base.into()],
Self::Cargo | Self::Brew => vec!["uninstall".into(), base.into()],
Self::Dotnet => vec![
"tool".into(),
"uninstall".into(),
"--global".into(),
base.into(),
],
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct AddonInstall {
pub manager: String,
pub package: String,
pub version: String,
pub bin: String,
pub verify: Vec<String>,
}
impl AddonInstall {
#[must_use]
pub fn is_declared(&self) -> bool {
!self.manager.trim().is_empty() && !self.package.trim().is_empty()
}
#[must_use]
pub fn is_absent(&self) -> bool {
!self.is_declared()
}
#[must_use]
pub fn manager(&self) -> Option<Manager> {
self.is_declared()
.then(|| Manager::parse(&self.manager))
.flatten()
}
#[must_use]
pub fn bin(&self) -> &str {
let b = self.bin.trim();
if b.is_empty() {
package_base(self.package.trim())
} else {
b
}
}
pub fn validate(&self) -> Result<(), String> {
if !self.is_declared() {
return Ok(());
}
if self.manager().is_none() {
return Err(format!(
"[install] manager `{}` is not supported — use one of: uv, pip, cargo, npm, brew, dotnet",
self.manager.trim()
));
}
let ver = self.version.trim();
if ver.is_empty() {
return Err(format!(
"[install] `{}` must pin an exact `version` — floating installs are rejected",
self.package.trim()
));
}
if mentions_latest(ver) {
return Err("[install] `version` must be an exact pin, not `latest`".into());
}
for (field, val) in [
("package", self.package.as_str()),
("version", self.version.as_str()),
("bin", self.bin.as_str()),
] {
if has_shell_meta(val) {
return Err(format!(
"[install] `{field}` contains shell metacharacters (| ; & $ ` > <) — rejected"
));
}
}
if self.verify.iter().any(|a| has_shell_meta(a)) {
return Err("[install] `verify` argv contains shell metacharacters — rejected".into());
}
Ok(())
}
#[must_use]
pub fn to_receipt(&self) -> InstallReceipt {
InstallReceipt {
manager: self.manager.trim().to_ascii_lowercase(),
package: self.package.trim().to_string(),
version: self.version.trim().to_string(),
bin: self.bin().to_string(),
}
}
#[must_use]
pub fn install_argv(&self) -> Vec<String> {
self.manager()
.map(|m| m.install_argv(&self.package, &self.version))
.unwrap_or_default()
}
#[must_use]
pub fn uninstall_argv(&self) -> Vec<String> {
self.manager()
.map(|m| m.uninstall_argv(&self.package))
.unwrap_or_default()
}
#[must_use]
fn already_satisfied(&self) -> bool {
if let Some((prog, rest)) = self.verify.split_first() {
return probe_ok(prog, rest);
}
binary_on_path(self.bin())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct InstallReceipt {
pub manager: String,
pub package: String,
pub version: String,
pub bin: String,
}
impl InstallReceipt {
fn manager(&self) -> Option<Manager> {
Manager::parse(&self.manager)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BootstrapStatus {
AlreadyPresent,
Installed,
}
#[derive(Debug, Clone)]
pub struct BootstrapOutcome {
pub status: BootstrapStatus,
pub receipt: InstallReceipt,
pub warning: Option<String>,
}
pub fn ensure_installed(install: &AddonInstall) -> Result<BootstrapOutcome, String> {
install.validate()?;
let manager = install
.manager()
.ok_or_else(|| format!("unsupported package manager `{}`", install.manager.trim()))?;
let receipt = install.to_receipt();
if install.already_satisfied() {
return Ok(BootstrapOutcome {
status: BootstrapStatus::AlreadyPresent,
receipt,
warning: None,
});
}
if !manager.is_available() {
return Err(format!(
"the `{mgr}` package manager is not installed (or not on PATH), so `{pkg}` cannot be \
installed.\n → {hint}\n Or install `{bin}` yourself, then re-run — `addon add` \
detects it and skips the bootstrap.",
mgr = manager.as_str(),
pkg = install.package.trim(),
hint = manager.install_hint(),
bin = install.bin(),
));
}
run(
&manager.program(),
&manager.install_argv(&install.package, &install.version),
)?;
let warning = (!install.already_satisfied()).then(|| {
format!(
"`{}` installed but `{}` is not on your PATH yet — add the manager's bin directory \
(e.g. ~/.local/bin) to PATH so the MCP server can launch.",
install.package.trim(),
install.bin()
)
});
Ok(BootstrapOutcome {
status: BootstrapStatus::Installed,
receipt,
warning,
})
}
pub fn uninstall(receipt: &InstallReceipt) -> Result<(), String> {
let manager = receipt.manager().ok_or_else(|| {
format!(
"unsupported package manager `{}` in receipt",
receipt.manager
)
})?;
run(
&manager.program(),
&manager.uninstall_argv(&receipt.package),
)
}
fn package_base(package: &str) -> &str {
let p = package.trim();
let p = p.split('[').next().unwrap_or(p);
let p = p.split("==").next().unwrap_or(p);
match p.rsplit_once('@') {
Some((head, _)) if !head.is_empty() => head,
_ => p,
}
.trim()
}
fn run(program: &str, argv: &[String]) -> Result<(), String> {
let status = Command::new(program).args(argv).status().map_err(|e| {
format!("could not launch `{program}`: {e} — is it installed and on your PATH?")
})?;
if status.success() {
return Ok(());
}
Err(format!(
"`{program} {}` failed ({})",
argv.join(" "),
status.code().map_or_else(
|| "terminated by signal".to_string(),
|c| format!("exit {c}")
)
))
}
fn probe_ok(program: &str, argv: &[String]) -> bool {
Command::new(program)
.args(argv)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.is_ok_and(|s| s.success())
}
fn binary_on_path(bin: &str) -> bool {
if bin.is_empty() {
return false;
}
let Some(path) = std::env::var_os("PATH") else {
return false;
};
std::env::split_paths(&path).any(|dir| is_executable(&dir.join(bin)))
}
#[cfg(unix)]
fn is_executable(path: &std::path::Path) -> bool {
use std::os::unix::fs::PermissionsExt;
std::fs::metadata(path).is_ok_and(|m| m.is_file() && m.permissions().mode() & 0o111 != 0)
}
#[cfg(not(unix))]
fn is_executable(path: &std::path::Path) -> bool {
path.is_file()
}
fn has_shell_meta(s: &str) -> bool {
s.chars()
.any(|c| matches!(c, '|' | ';' | '&' | '`' | '>' | '<' | '\n' | '\r'))
|| s.contains("$(")
}
fn mentions_latest(version: &str) -> bool {
let v = version.trim().to_ascii_lowercase();
v == "latest" || v.ends_with("@latest") || v.ends_with(":latest") || v == "*"
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(unix)]
use std::sync::Mutex;
#[cfg(unix)]
static ENV_LOCK: Mutex<()> = Mutex::new(());
fn declared(manager: &str, package: &str, version: &str) -> AddonInstall {
AddonInstall {
manager: manager.into(),
package: package.into(),
version: version.into(),
..Default::default()
}
}
#[test]
fn absent_block_is_a_noop() {
let empty = AddonInstall::default();
assert!(!empty.is_declared());
assert!(empty.is_absent());
assert!(empty.validate().is_ok());
assert!(empty.manager().is_none());
}
#[test]
fn validate_requires_known_manager_and_pin() {
assert!(declared("uv", "pkg", "1.2.3").validate().is_ok());
assert!(declared("conda", "pkg", "1.2.3").validate().is_err());
assert!(declared("uv", "pkg", "").validate().is_err());
assert!(declared("uv", "pkg", "latest").validate().is_err());
assert!(declared("npm", "pkg", "*").validate().is_err());
}
#[test]
fn validate_rejects_shell_metacharacters() {
assert!(declared("uv", "pkg; rm -rf /", "1.0.0").validate().is_err());
assert!(declared("uv", "pkg", "1.0.0 && evil").validate().is_err());
assert!(declared("uv", "pkg`whoami`", "1.0.0").validate().is_err());
assert!(
declared("uv", "headroom-ai[all]", "1.4.2")
.validate()
.is_ok()
);
}
#[test]
fn install_argv_is_pinned_per_manager() {
assert_eq!(
declared("uv", "headroom-ai[all]", "1.4.2").install_argv(),
["tool", "install", "headroom-ai[all]==1.4.2"]
);
assert_eq!(
declared("pip", "cognee", "0.1.0").install_argv(),
["install", "--user", "cognee==0.1.0"]
);
assert_eq!(
declared("cargo", "ripgrep", "14.1.0").install_argv(),
["install", "ripgrep", "--version", "14.1.0"]
);
assert_eq!(
declared("npm", "@scope/cli", "2.0.0").install_argv(),
["install", "-g", "@scope/cli@2.0.0"]
);
assert_eq!(
declared("brew", "node@22", "22.0.0").install_argv(),
["install", "node@22"]
);
assert_eq!(
declared("dotnet", "CodeCompress.Server", "0.15.0").install_argv(),
[
"tool",
"install",
"--global",
"CodeCompress.Server",
"--version",
"0.15.0"
]
);
}
#[test]
fn uninstall_argv_targets_the_base_name() {
assert_eq!(
declared("uv", "headroom-ai[all]", "1.4.2").uninstall_argv(),
["tool", "uninstall", "headroom-ai"]
);
assert_eq!(
declared("npm", "@scope/cli", "2.0.0").uninstall_argv(),
["rm", "-g", "@scope/cli"]
);
assert_eq!(
declared("pip", "cognee==0.1.0", "0.1.0").uninstall_argv(),
["uninstall", "-y", "cognee"]
);
assert_eq!(
declared("dotnet", "CodeCompress.Server", "0.15.0").uninstall_argv(),
["tool", "uninstall", "--global", "CodeCompress.Server"]
);
}
#[test]
fn bin_defaults_to_package_base_else_explicit() {
assert_eq!(
declared("uv", "headroom-ai[all]", "1.0.0").bin(),
"headroom-ai"
);
let mut with_bin = declared("uv", "headroom-ai[all]", "1.0.0");
with_bin.bin = "headroom".into();
assert_eq!(with_bin.bin(), "headroom");
}
#[test]
fn package_base_strips_extras_version_and_keeps_npm_scope() {
assert_eq!(package_base("headroom-ai[all]==1.4.2"), "headroom-ai");
assert_eq!(package_base("pkg@1.2.3"), "pkg");
assert_eq!(package_base("@scope/pkg"), "@scope/pkg");
assert_eq!(package_base("@scope/pkg@1.0.0"), "@scope/pkg");
}
#[test]
fn receipt_round_trips_and_normalises_manager() {
let r = declared("UV", "pkg", "1.0.0").to_receipt();
assert_eq!(r.manager, "uv");
assert_eq!(r.manager().unwrap(), Manager::Uv);
let json = serde_json::to_string(&r).unwrap();
let back: InstallReceipt = serde_json::from_str(&json).unwrap();
assert_eq!(r, back);
}
#[test]
fn binary_on_path_finds_a_standard_tool() {
#[cfg(unix)]
assert!(binary_on_path("sh"));
assert!(!binary_on_path("lean-ctx-definitely-not-a-real-binary-xyz"));
assert!(!binary_on_path(""));
}
#[cfg(unix)]
fn write_script(path: &std::path::Path, body: &str) {
use std::os::unix::fs::PermissionsExt;
std::fs::write(path, format!("#!/bin/sh\n{body}\n")).unwrap();
let mut perms = std::fs::metadata(path).unwrap().permissions();
perms.set_mode(0o755);
std::fs::set_permissions(path, perms).unwrap();
}
#[test]
#[cfg(unix)]
fn ensure_installed_runs_manager_then_verifies() {
let _env_lock = crate::core::data_dir::test_env_lock();
let _guard = ENV_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let tmp = std::env::temp_dir().join(format!("leanctx-boot-{}", std::process::id()));
std::fs::create_dir_all(&tmp).unwrap();
let marker = tmp.join("installed.marker");
let fake_uv = tmp.join("uv");
write_script(&fake_uv, &format!("touch '{}'", marker.display()));
let mut install = declared("uv", "demo-pkg", "1.0.0");
install.verify = vec!["test".into(), "-f".into(), marker.display().to_string()];
unsafe { std::env::set_var("LEANCTX_BOOTSTRAP_UV", &fake_uv) };
let _ = std::fs::remove_file(&marker);
let out = ensure_installed(&install).expect("install");
assert_eq!(out.status, BootstrapStatus::Installed);
assert!(out.warning.is_none(), "verify passed → no warning");
assert!(marker.exists(), "fake manager ran");
std::fs::remove_file(&fake_uv).unwrap(); let out2 = ensure_installed(&install).expect("idempotent");
assert_eq!(out2.status, BootstrapStatus::AlreadyPresent);
unsafe { std::env::remove_var("LEANCTX_BOOTSTRAP_UV") };
let _ = std::fs::remove_dir_all(&tmp);
}
#[test]
#[cfg(unix)]
fn ensure_installed_propagates_manager_failure() {
let _env_lock = crate::core::data_dir::test_env_lock();
let _guard = ENV_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let tmp = std::env::temp_dir().join(format!("leanctx-boot-fail-{}", std::process::id()));
std::fs::create_dir_all(&tmp).unwrap();
let fake_uv = tmp.join("uv");
write_script(&fake_uv, "exit 1");
let mut install = declared("uv", "demo-pkg", "1.0.0");
install.verify = vec!["false".into()];
unsafe { std::env::set_var("LEANCTX_BOOTSTRAP_UV", &fake_uv) };
let err = ensure_installed(&install).expect_err("manager failed");
assert!(err.contains("failed"), "got: {err}");
unsafe { std::env::remove_var("LEANCTX_BOOTSTRAP_UV") };
let _ = std::fs::remove_dir_all(&tmp);
}
#[test]
fn install_hint_is_present_for_every_manager() {
for m in [
Manager::Uv,
Manager::Pip,
Manager::Cargo,
Manager::Npm,
Manager::Brew,
Manager::Dotnet,
] {
assert!(!m.install_hint().is_empty(), "{m:?} needs an install hint");
}
}
#[test]
#[cfg(unix)]
fn ensure_installed_preflights_a_missing_manager() {
let _env_lock = crate::core::data_dir::test_env_lock();
let _guard = ENV_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let tmp = std::env::temp_dir().join(format!("leanctx-boot-miss-{}", std::process::id()));
std::fs::create_dir_all(&tmp).unwrap();
let missing = tmp.join("uv-not-here");
let mut install = declared("uv", "demo-pkg", "1.0.0");
install.verify = vec!["false".into()];
unsafe { std::env::set_var("LEANCTX_BOOTSTRAP_UV", &missing) };
let err = ensure_installed(&install).expect_err("missing manager rejected");
unsafe { std::env::remove_var("LEANCTX_BOOTSTRAP_UV") };
let _ = std::fs::remove_dir_all(&tmp);
assert!(err.contains("uv"), "names the manager: {err}");
assert!(err.contains("not installed"), "explains why: {err}");
assert!(err.contains("astral.sh"), "gives an install hint: {err}");
}
}