use std::path::PathBuf;
use std::process::{Command, ExitCode};
use anyhow::{anyhow, Result};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Component {
pub bin: &'static str,
pub krate: &'static str,
pub about: &'static str,
}
pub const TUI: Component = Component {
bin: "scema-tui",
krate: "scema-tui",
about: "the console — the loop as a terminal application",
};
pub const DAEMON: Component = Component {
bin: "scema-omnid",
krate: "scema-daemon",
about: "the local daemon — loopback HTTP, token-authenticated",
};
pub const MCP: Component = Component {
bin: "scema-mcp",
krate: "scema-mcp",
about: "the MCP server — the loop as tools, for a model",
};
pub const ALL: [Component; 3] = [TUI, DAEMON, MCP];
fn exe_name(bin: &str) -> String {
if cfg!(windows) {
format!("{bin}.exe")
} else {
bin.to_string()
}
}
pub fn locate(component: Component) -> Option<PathBuf> {
if let Ok(me) = std::env::current_exe() {
if let Some(dir) = me.parent() {
let candidate = dir.join(exe_name(component.bin));
if candidate.is_file() {
return Some(candidate);
}
}
}
which(component.bin)
}
fn which(bin: &str) -> Option<PathBuf> {
let path = std::env::var_os("PATH")?;
for dir in std::env::split_paths(&path) {
let direct = dir.join(exe_name(bin));
if direct.is_file() {
return Some(direct);
}
if cfg!(windows) {
let exts = std::env::var("PATHEXT").unwrap_or_else(|_| ".EXE;.CMD;.BAT".into());
for ext in exts.split(';') {
let candidate = dir.join(format!("{bin}{}", ext.to_ascii_lowercase()));
if candidate.is_file() {
return Some(candidate);
}
}
}
}
None
}
pub fn run(component: Component, args: &[String]) -> Result<ExitCode> {
let Some(path) = locate(component) else {
return Err(anyhow!(
"`{}` is not installed.\n\n It is a separate binary so that `scema` itself stays small — the console\n pulls in a whole terminal stack that a CI machine running `scema verify` has\n no use for.\n\n Install it with:\n\n cargo install {}\n\n …or build it in a checkout with:\n\n cargo build --release -p {}",
component.bin,
component.krate,
component.krate
));
};
let status = Command::new(&path)
.args(args)
.status()
.map_err(|e| anyhow!("could not start {}: {e}", path.display()))?;
Ok(match status.code() {
Some(code) => ExitCode::from((code & 0xff) as u8),
None => ExitCode::from(130),
})
}
pub fn inventory() -> Vec<(Component, Option<PathBuf>)> {
ALL.iter().map(|c| (*c, locate(*c))).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_missing_component_names_the_crate_that_provides_it() {
let missing = Component {
bin: "scema-does-not-exist",
krate: "scema-nowhere",
about: "x",
};
let err = run(missing, &[]).unwrap_err().to_string();
assert!(err.contains("cargo install scema-nowhere"), "{err}");
}
#[test]
fn the_windows_extension_is_only_added_on_windows() {
if cfg!(windows) {
assert_eq!(exe_name("scema-tui"), "scema-tui.exe");
} else {
assert_eq!(exe_name("scema-tui"), "scema-tui");
}
}
#[test]
fn every_component_is_a_distinct_binary_and_crate() {
let mut bins: Vec<&str> = ALL.iter().map(|c| c.bin).collect();
bins.sort_unstable();
let before = bins.len();
bins.dedup();
assert_eq!(before, bins.len());
}
}