use std::path::{Path, PathBuf};
use anyhow::{Context, Result, bail};
pub fn find_repo_root() -> Result<PathBuf> {
let start = std::env::current_dir().context("reading the current directory")?;
find_repo_root_from(&start)
}
fn find_repo_root_from(start: &Path) -> Result<PathBuf> {
let mut dir = start;
loop {
let cargo = dir.join("Cargo.toml");
if cargo.is_file()
&& dir.join("bridge").is_dir()
&& std::fs::read_to_string(&cargo)
.map(|text| text.contains("[workspace]") && text.contains("crates/salvor-cli"))
.unwrap_or(false)
{
return Ok(dir.to_path_buf());
}
match dir.parent() {
Some(parent) => dir = parent,
None => bail!(
"not inside a salvor checkout: walked up from {} and found no workspace \
Cargo.toml declaring the salvor members alongside a bridge/ directory",
start.display()
),
}
}
}
pub async fn ensure_node_modules(bridge: &Path) -> Result<()> {
if !bridge.join("node_modules").is_dir() {
println!("installing dashboard dependencies (npm ci)");
run_shell(bridge, "npm ci").await?;
}
Ok(())
}
pub async fn run_shell(dir: &Path, line: &str) -> Result<()> {
let status = tokio::process::Command::new("bash")
.arg("-lc")
.arg(line)
.current_dir(dir)
.status()
.await
.with_context(|| format!("spawning `{line}`"))?;
if !status.success() {
bail!("`{line}` failed ({status})");
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn finds_the_root_from_a_nested_directory() {
let dir = tempfile::tempdir().expect("tempdir");
let root = dir.path();
std::fs::write(
root.join("Cargo.toml"),
"[workspace]\nmembers = [\"crates/salvor-cli\"]\n",
)
.unwrap();
std::fs::create_dir(root.join("bridge")).unwrap();
let nested = root.join("crates").join("salvor-cli").join("src");
std::fs::create_dir_all(&nested).unwrap();
let found = find_repo_root_from(&nested);
assert_eq!(found.unwrap(), root.to_path_buf());
}
#[test]
fn refuses_a_workspace_with_no_bridge_directory() {
let dir = tempfile::tempdir().expect("tempdir");
let root = dir.path();
std::fs::write(
root.join("Cargo.toml"),
"[workspace]\nmembers = [\"crates/salvor-cli\"]\n",
)
.unwrap();
let found = find_repo_root_from(root);
assert!(found.is_err());
}
#[test]
fn refuses_a_directory_outside_any_checkout() {
let dir = tempfile::tempdir().expect("tempdir");
let found = find_repo_root_from(dir.path());
assert!(found.is_err());
}
}