use crate::cli::ServicesAction;
use crate::formatters::services::{print_service_status_block, print_services_table};
pub(crate) fn services(action: ServicesAction) -> anyhow::Result<()> {
use trusty_mpm::services::{Discoverer, HealthState, ServicesManifest};
let services_yaml_path = claude_mpm_dir().join("services.yaml");
let manifest = if services_yaml_path.exists() {
let text = std::fs::read_to_string(&services_yaml_path)?;
let mut m: ServicesManifest = serde_yaml::from_str(&text)
.map_err(|e| anyhow::anyhow!("failed to parse services.yaml: {e}"))?;
m.validate()
.map_err(|e| anyhow::anyhow!("services.yaml validation failed: {e}"))?;
m.expand_paths()?;
m
} else {
ServicesManifest::default_manifest()
};
let mut discoverer = Discoverer::new(manifest);
match action {
ServicesAction::List { json } => {
let statuses = discoverer.list();
if json {
println!("{}", serde_json::to_string_pretty(&statuses)?);
} else {
print_services_table(&statuses);
}
}
ServicesAction::Status { name, json } => match discoverer.status(&name) {
None => {
eprintln!("unknown service: {name}");
std::process::exit(2);
}
Some(status) => {
if json {
println!("{}", serde_json::to_string_pretty(&status)?);
} else {
print_service_status_block(&status);
}
if !status.running {
std::process::exit(1);
}
}
},
ServicesAction::Port { name } => match discoverer.status(&name) {
None => {
eprintln!("unknown service: {name}");
std::process::exit(2);
}
Some(status) => match status.port {
Some(port) => print!("{port}"),
None => {
eprintln!("{name}: port unavailable (service down or no port)");
std::process::exit(1);
}
},
},
ServicesAction::Url { name } => match discoverer.status(&name) {
None => {
eprintln!("unknown service: {name}");
std::process::exit(2);
}
Some(status) => match status.url {
Some(url) => print!("{url}"),
None => {
eprintln!("{name}: URL unavailable (service down or no port)");
std::process::exit(1);
}
},
},
ServicesAction::Health { name } => match discoverer.health(&name) {
None => {
eprintln!("unknown service: {name}");
std::process::exit(2);
}
Some(result) => match result.state {
HealthState::Ok => {
println!("OK");
}
HealthState::Unknown => {
println!("OK (no health endpoint; process running)");
}
HealthState::Fail { ref detail } => {
eprintln!("FAIL: {detail}");
std::process::exit(1);
}
},
},
ServicesAction::Log { name } => match discoverer.status(&name) {
None => {
eprintln!("unknown service: {name}");
std::process::exit(2);
}
Some(status) => match status.log_path {
Some(path) => print!("{}", path.display()),
None => {
eprintln!("{name}: log path unavailable or file does not exist");
std::process::exit(1);
}
},
},
ServicesAction::Init { force } => {
let path = services_yaml_path;
if path.exists() && !force {
anyhow::bail!(
"{} already exists. Use --force to overwrite.",
path.display()
);
}
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let default = ServicesManifest::default_manifest();
let yaml = serde_yaml::to_string(&default)?;
std::fs::write(&path, yaml)?;
println!("wrote {}", path.display());
}
ServicesAction::Restart { name } => {
let manifest_for_restart = if services_yaml_path.exists() {
let text = std::fs::read_to_string(&services_yaml_path)?;
let m: ServicesManifest = serde_yaml::from_str(&text)
.map_err(|e| anyhow::anyhow!("failed to parse services.yaml: {e}"))?;
m.validate()
.map_err(|e| anyhow::anyhow!("services.yaml validation failed: {e}"))?;
m
} else {
ServicesManifest::default_manifest()
};
match manifest_for_restart.services.get(&name) {
None => {
eprintln!("unknown service: {name}");
std::process::exit(2);
}
Some(decl) => match &decl.restart_cmd {
None => {
eprintln!("{name}: no restart_cmd defined in manifest");
std::process::exit(1);
}
Some(cmd) => {
let status = std::process::Command::new("sh")
.args(["-c", cmd])
.status()?;
if status.success() {
println!("{name}: restarted");
} else {
eprintln!("{name}: restart command failed (exit {status})");
std::process::exit(1);
}
}
},
}
}
}
Ok(())
}
pub(crate) fn claude_mpm_dir() -> std::path::PathBuf {
dirs::home_dir()
.unwrap_or_else(|| std::path::PathBuf::from("."))
.join(".claude-mpm")
}