use crate::config::Config;
use crate::paths::Paths;
use anyhow::{Result, bail};
fn dep_hint(cmd: &str) -> &'static str {
match cmd {
"pi" => "see https://github.com/earendil-works/pi (the default tick runner)",
"claude" => "see https://docs.claude.com/claude-code",
_ => "see the tool's docs",
}
}
fn on_path(cmd: &str) -> bool {
let Some(path) = std::env::var_os("PATH") else {
return false;
};
std::env::split_paths(&path).any(|dir| {
let candidate = dir.join(cmd);
candidate.is_file() && is_executable(&candidate)
})
}
#[cfg(unix)]
fn is_executable(p: &std::path::Path) -> bool {
use std::os::unix::fs::PermissionsExt;
std::fs::metadata(p)
.map(|m| m.permissions().mode() & 0o111 != 0)
.unwrap_or(false)
}
#[cfg(not(unix))]
fn is_executable(_p: &std::path::Path) -> bool {
true
}
pub fn require_deps(paths: &Paths) -> Result<()> {
let mut missing: Vec<(String, &'static str)> = Vec::new();
if let Ok(cfg) = Config::load(paths)
&& let Some(tick_cmd) = cfg.active_runner_cmd("tick")
&& let Some(bin) = tick_cmd.split_whitespace().next()
&& !bin.is_empty()
&& !on_path(bin)
{
missing.push((bin.to_string(), dep_hint(bin)));
}
if missing.is_empty() {
return Ok(());
}
let mut msg = String::from("looop: missing required dependencies — cannot run:\n");
for (cmd, hint) in &missing {
msg.push_str(&format!(" {:<8} install: {}\n", cmd, hint));
}
msg.push_str("\nInstall the above, then re-run looop.");
bail!(msg);
}