use anyhow::{Context, Result};
use std::path::{Path, PathBuf};
pub fn find_project_root(start: &Path) -> Result<PathBuf> {
let canonical = start
.canonicalize()
.with_context(|| format!("failed to canonicalize {}", start.display()))?;
let mut cursor: &Path = &canonical;
loop {
if cursor.join("forge.toml").is_file() {
return Ok(cursor.to_path_buf());
}
match cursor.parent() {
Some(parent) => cursor = parent,
None => anyhow::bail!(
"forge.toml not found in {} or any parent directory.\n\
Run this command from inside a Forge project, or create one with:\n \
forge new my-app --template with-svelte/minimal",
start.display()
),
}
}
}
pub fn enter_project_root() -> Result<PathBuf> {
let cwd = std::env::current_dir().context("failed to read current directory")?;
let root = find_project_root(&cwd)?;
if root != cwd {
std::env::set_current_dir(&root)
.with_context(|| format!("failed to chdir to {}", root.display()))?;
}
Ok(root)
}
pub fn cargo_sqlx_available() -> bool {
std::process::Command::new("cargo")
.args(["sqlx", "--version"])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
pub async fn docker_available() -> bool {
tokio::process::Command::new("docker")
.args(["info"])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.await
.map(|s| s.success())
.unwrap_or(false)
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
#[test]
fn finds_root_from_subdirectory() {
let tmp = tempfile::tempdir().expect("create tempdir");
let root = tmp.path();
fs::write(root.join("forge.toml"), "[project]\nname = \"test\"\n")
.expect("write forge.toml");
let nested = root.join("frontend").join("src");
fs::create_dir_all(&nested).expect("create nested dir");
let found = find_project_root(&nested).expect("walk up succeeds");
assert_eq!(found, root.canonicalize().expect("canonical root"));
}
#[test]
fn finds_root_when_already_at_root() {
let tmp = tempfile::tempdir().expect("create tempdir");
let root = tmp.path();
fs::write(root.join("forge.toml"), "").expect("write forge.toml");
let found = find_project_root(root).expect("returns root");
assert_eq!(found, root.canonicalize().expect("canonical root"));
}
#[test]
fn errors_when_no_forge_toml_anywhere() {
let tmp = tempfile::tempdir().expect("create tempdir");
let nested = tmp.path().join("a").join("b");
fs::create_dir_all(&nested).expect("create nested dir");
let err = find_project_root(&nested).expect_err("should error");
let msg = format!("{}", err);
assert!(msg.contains("forge.toml not found"), "msg was: {msg}");
}
}