use std::path::{Path, PathBuf};
use crate::FallowConfig;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ConfigFixPlan {
Edit { config_path: PathBuf },
BlockedMonorepo { workspace_root: PathBuf },
BlockedNoCreate { target: PathBuf },
Create { target: PathBuf },
}
#[must_use]
pub fn classify_config_fix_plan(
root: &Path,
explicit: Option<&PathBuf>,
no_create_config: bool,
) -> ConfigFixPlan {
if let Some(existing) = resolve_existing_config_path(root, explicit) {
return ConfigFixPlan::Edit {
config_path: existing,
};
}
let target = root.join(".fallowrc.json");
if let Some(workspace_root) = find_workspace_root_above(root) {
return ConfigFixPlan::BlockedMonorepo { workspace_root };
}
if no_create_config {
return ConfigFixPlan::BlockedNoCreate { target };
}
ConfigFixPlan::Create { target }
}
#[must_use]
pub fn is_config_fixable(root: &Path, explicit: Option<&PathBuf>) -> bool {
matches!(
classify_config_fix_plan(root, explicit, false),
ConfigFixPlan::Edit { .. } | ConfigFixPlan::Create { .. }
)
}
fn resolve_existing_config_path(root: &Path, explicit: Option<&PathBuf>) -> Option<PathBuf> {
if let Some(path) = explicit {
let absolute = if path.is_absolute() {
path.clone()
} else {
std::env::current_dir().map_or_else(|_| path.clone(), |cwd| cwd.join(path))
};
if absolute.exists() {
return Some(absolute);
}
return None;
}
FallowConfig::find_config_path(root)
}
fn find_workspace_root_above(start: &Path) -> Option<PathBuf> {
let mut current = start.parent()?;
loop {
if has_workspace_marker(current) {
return Some(current.to_path_buf());
}
current = current.parent()?;
}
}
fn has_workspace_marker(dir: &Path) -> bool {
const SENTINELS: &[&str] = &[
"pnpm-workspace.yaml",
"turbo.json",
"lerna.json",
"rush.json",
];
for name in SENTINELS {
if dir.join(name).exists() {
return true;
}
}
let pkg_path = dir.join("package.json");
if !pkg_path.exists() {
return false;
}
let Ok(content) = std::fs::read_to_string(&pkg_path) else {
return false;
};
let Ok(value) = serde_json::from_str::<serde_json::Value>(&content) else {
return false;
};
value
.get("workspaces")
.is_some_and(|v| v.is_array() || v.is_object())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn config_fixable_true_when_config_exists() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join(".fallowrc.json"), "{}").unwrap();
assert!(is_config_fixable(dir.path(), None));
}
#[test]
fn config_fixable_true_when_can_create_at_root() {
let dir = tempfile::tempdir().unwrap();
assert!(is_config_fixable(dir.path(), None));
}
#[test]
fn config_fixable_false_when_monorepo_subpackage() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("pnpm-workspace.yaml"),
"packages:\n - packages/*\n",
)
.unwrap();
let sub = dir.path().join("packages/app");
std::fs::create_dir_all(&sub).unwrap();
assert!(!is_config_fixable(&sub, None));
}
}