use std::path::{Path, PathBuf};
use crate::error::TuiError;
const EXPECTED_PACKAGE_NAME: &str = "@raishin/vanguard-frontier-agentic";
#[derive(Debug, PartialEq)]
enum MarkerCheck {
Valid,
None,
Partial { missing: String },
}
pub fn detect_workspace(start: Option<&Path>) -> Result<PathBuf, TuiError> {
let start_dir = match start {
Some(p) => p.to_path_buf(),
None => std::env::current_dir().map_err(|_| TuiError::WorkspaceNotFound {
start: ".".to_string(),
})?,
};
let mut current = start_dir.clone();
loop {
match check_markers(¤t) {
MarkerCheck::Valid => return Ok(current),
MarkerCheck::Partial { missing } => {
return Err(TuiError::InvalidWorkspace {
path: current.display().to_string(),
missing,
});
}
MarkerCheck::None => {
if !current.pop() {
break;
}
}
}
}
Err(TuiError::WorkspaceNotFound {
start: start_dir.display().to_string(),
})
}
fn check_markers(dir: &Path) -> MarkerCheck {
let has_catalog = dir.join("catalog").join("agents.json").is_file();
let package_path = dir.join("package.json");
let has_package = package_path.is_file();
match (has_catalog, has_package) {
(false, false) => MarkerCheck::None,
(true, false) => MarkerCheck::Partial {
missing: "package.json".to_string(),
},
(false, true) => {
if package_has_expected_name(&package_path) {
MarkerCheck::Partial {
missing: "catalog/agents.json".to_string(),
}
} else {
MarkerCheck::None
}
}
(true, true) => {
if package_has_expected_name(&package_path) {
MarkerCheck::Valid
} else {
MarkerCheck::Partial {
missing: format!("package.json with name \"{}\"", EXPECTED_PACKAGE_NAME),
}
}
}
}
}
fn package_has_expected_name(package_path: &Path) -> bool {
match std::fs::read_to_string(package_path) {
Ok(content) => match serde_json::from_str::<serde_json::Value>(&content) {
Ok(v) => v.get("name").and_then(|n| n.as_str()) == Some(EXPECTED_PACKAGE_NAME),
Err(_) => false,
},
Err(_) => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
fn create_workspace(dir: &Path) {
let catalog_dir = dir.join("catalog");
fs::create_dir_all(&catalog_dir).unwrap();
fs::write(catalog_dir.join("agents.json"), "[]").unwrap();
fs::write(
dir.join("package.json"),
r#"{"name": "@raishin/vanguard-frontier-agentic"}"#,
)
.unwrap();
}
#[test]
fn detect_at_root() {
let tmp = TempDir::new().unwrap();
create_workspace(tmp.path());
let result = detect_workspace(Some(tmp.path())).unwrap();
assert_eq!(result, tmp.path());
}
#[test]
fn detect_from_subdir() {
let tmp = TempDir::new().unwrap();
create_workspace(tmp.path());
let sub = tmp.path().join("tools").join("vfa-tui");
fs::create_dir_all(&sub).unwrap();
let result = detect_workspace(Some(&sub)).unwrap();
assert_eq!(result, tmp.path());
}
#[test]
fn detect_fails_no_workspace() {
let tmp = TempDir::new().unwrap();
let result = detect_workspace(Some(tmp.path()));
assert!(result.is_err());
match result.unwrap_err() {
TuiError::WorkspaceNotFound { .. } => {}
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn detect_fails_wrong_package_name() {
let tmp = TempDir::new().unwrap();
let catalog_dir = tmp.path().join("catalog");
fs::create_dir_all(&catalog_dir).unwrap();
fs::write(catalog_dir.join("agents.json"), "[]").unwrap();
fs::write(
tmp.path().join("package.json"),
r#"{"name": "something-else"}"#,
)
.unwrap();
let result = detect_workspace(Some(tmp.path()));
assert!(result.is_err());
match result.unwrap_err() {
TuiError::InvalidWorkspace { path, missing } => {
assert_eq!(path, tmp.path().display().to_string());
assert!(missing.contains("package.json with name"));
}
other => panic!("expected InvalidWorkspace, got: {other:?}"),
}
}
#[test]
fn detect_fails_missing_package_json() {
let tmp = TempDir::new().unwrap();
let catalog_dir = tmp.path().join("catalog");
fs::create_dir_all(&catalog_dir).unwrap();
fs::write(catalog_dir.join("agents.json"), "[]").unwrap();
let result = detect_workspace(Some(tmp.path()));
assert!(result.is_err());
match result.unwrap_err() {
TuiError::InvalidWorkspace { path, missing } => {
assert_eq!(path, tmp.path().display().to_string());
assert_eq!(missing, "package.json");
}
other => panic!("expected InvalidWorkspace, got: {other:?}"),
}
}
#[test]
fn detect_fails_missing_catalog() {
let tmp = TempDir::new().unwrap();
fs::write(
tmp.path().join("package.json"),
r#"{"name": "@raishin/vanguard-frontier-agentic"}"#,
)
.unwrap();
let result = detect_workspace(Some(tmp.path()));
assert!(result.is_err());
match result.unwrap_err() {
TuiError::InvalidWorkspace { path, missing } => {
assert_eq!(path, tmp.path().display().to_string());
assert_eq!(missing, "catalog/agents.json");
}
other => panic!("expected InvalidWorkspace, got: {other:?}"),
}
}
#[test]
fn detect_ignores_unrelated_package_json() {
let tmp = TempDir::new().unwrap();
let sub = tmp.path().join("sub");
fs::create_dir_all(&sub).unwrap();
fs::write(sub.join("package.json"), r#"{"name": "unrelated-package"}"#).unwrap();
let result = detect_workspace(Some(&sub));
assert!(result.is_err());
match result.unwrap_err() {
TuiError::WorkspaceNotFound { .. } => {}
other => panic!("expected WorkspaceNotFound, got: {other:?}"),
}
}
#[test]
fn detect_real_workspace() {
let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
let result = detect_workspace(Some(manifest_dir));
assert!(result.is_ok());
let ws = result.unwrap();
assert!(ws.join("catalog").join("agents.json").is_file());
}
#[test]
fn detect_with_nonstandard_whitespace() {
let tmp = TempDir::new().unwrap();
let catalog_dir = tmp.path().join("catalog");
fs::create_dir_all(&catalog_dir).unwrap();
fs::write(catalog_dir.join("agents.json"), "[]").unwrap();
fs::write(
tmp.path().join("package.json"),
r#"{"name":"@raishin/vanguard-frontier-agentic","version":"1.0.0"}"#,
)
.unwrap();
let result = detect_workspace(Some(tmp.path()));
assert!(result.is_ok());
assert_eq!(result.unwrap(), tmp.path());
}
#[test]
fn detect_skips_invalid_json_package() {
let tmp = TempDir::new().unwrap();
let catalog_dir = tmp.path().join("catalog");
fs::create_dir_all(&catalog_dir).unwrap();
fs::write(catalog_dir.join("agents.json"), "[]").unwrap();
fs::write(tmp.path().join("package.json"), "not valid json").unwrap();
let result = detect_workspace(Some(tmp.path()));
assert!(result.is_err());
match result.unwrap_err() {
TuiError::InvalidWorkspace { missing, .. } => {
assert!(missing.contains("package.json with name"));
}
other => panic!("expected InvalidWorkspace, got: {other:?}"),
}
}
#[test]
fn check_markers_both_valid() {
let tmp = TempDir::new().unwrap();
create_workspace(tmp.path());
assert_eq!(check_markers(tmp.path()), MarkerCheck::Valid);
}
#[test]
fn check_markers_none() {
let tmp = TempDir::new().unwrap();
assert_eq!(check_markers(tmp.path()), MarkerCheck::None);
}
#[test]
fn check_markers_partial_missing_package() {
let tmp = TempDir::new().unwrap();
let catalog_dir = tmp.path().join("catalog");
fs::create_dir_all(&catalog_dir).unwrap();
fs::write(catalog_dir.join("agents.json"), "[]").unwrap();
assert_eq!(
check_markers(tmp.path()),
MarkerCheck::Partial {
missing: "package.json".to_string()
}
);
}
#[test]
fn check_markers_partial_missing_catalog() {
let tmp = TempDir::new().unwrap();
fs::write(
tmp.path().join("package.json"),
r#"{"name": "@raishin/vanguard-frontier-agentic"}"#,
)
.unwrap();
assert_eq!(
check_markers(tmp.path()),
MarkerCheck::Partial {
missing: "catalog/agents.json".to_string()
}
);
}
}