fallow_config/
fixability.rs1use std::path::{Path, PathBuf};
2
3use crate::FallowConfig;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
7pub enum ConfigFixPlan {
8 Edit { config_path: PathBuf },
10 BlockedMonorepo { workspace_root: PathBuf },
13 BlockedNoCreate { target: PathBuf },
15 Create { target: PathBuf },
17}
18
19#[must_use]
21pub fn classify_config_fix_plan(
22 root: &Path,
23 explicit: Option<&PathBuf>,
24 no_create_config: bool,
25) -> ConfigFixPlan {
26 if let Some(existing) = resolve_existing_config_path(root, explicit) {
27 return ConfigFixPlan::Edit {
28 config_path: existing,
29 };
30 }
31 let target = root.join(".fallowrc.json");
32 if let Some(workspace_root) = find_workspace_root_above(root) {
33 return ConfigFixPlan::BlockedMonorepo { workspace_root };
34 }
35 if no_create_config {
36 return ConfigFixPlan::BlockedNoCreate { target };
37 }
38 ConfigFixPlan::Create { target }
39}
40
41#[must_use]
44pub fn is_config_fixable(root: &Path, explicit: Option<&PathBuf>) -> bool {
45 matches!(
46 classify_config_fix_plan(root, explicit, false),
47 ConfigFixPlan::Edit { .. } | ConfigFixPlan::Create { .. }
48 )
49}
50
51fn resolve_existing_config_path(root: &Path, explicit: Option<&PathBuf>) -> Option<PathBuf> {
52 if let Some(path) = explicit {
53 let absolute = if path.is_absolute() {
54 path.clone()
55 } else {
56 std::env::current_dir().map_or_else(|_| path.clone(), |cwd| cwd.join(path))
57 };
58 if absolute.exists() {
59 return Some(absolute);
60 }
61 return None;
62 }
63 FallowConfig::find_config_path(root)
64}
65
66fn find_workspace_root_above(start: &Path) -> Option<PathBuf> {
67 let mut current = start.parent()?;
68 loop {
69 if has_workspace_marker(current) {
70 return Some(current.to_path_buf());
71 }
72 current = current.parent()?;
73 }
74}
75
76fn has_workspace_marker(dir: &Path) -> bool {
77 const SENTINELS: &[&str] = &[
78 "pnpm-workspace.yaml",
79 "turbo.json",
80 "lerna.json",
81 "rush.json",
82 ];
83 for name in SENTINELS {
84 if dir.join(name).exists() {
85 return true;
86 }
87 }
88 let pkg_path = dir.join("package.json");
89 if !pkg_path.exists() {
90 return false;
91 }
92 let Ok(content) = std::fs::read_to_string(&pkg_path) else {
93 return false;
94 };
95 let Ok(value) = serde_json::from_str::<serde_json::Value>(&content) else {
96 return false;
97 };
98 value
99 .get("workspaces")
100 .is_some_and(|v| v.is_array() || v.is_object())
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106
107 #[test]
108 fn config_fixable_true_when_config_exists() {
109 let dir = tempfile::tempdir().unwrap();
110 std::fs::write(dir.path().join(".fallowrc.json"), "{}").unwrap();
111 assert!(is_config_fixable(dir.path(), None));
112 }
113
114 #[test]
115 fn config_fixable_true_when_can_create_at_root() {
116 let dir = tempfile::tempdir().unwrap();
117 assert!(is_config_fixable(dir.path(), None));
118 }
119
120 #[test]
121 fn config_fixable_false_when_monorepo_subpackage() {
122 let dir = tempfile::tempdir().unwrap();
123 std::fs::write(
124 dir.path().join("pnpm-workspace.yaml"),
125 "packages:\n - packages/*\n",
126 )
127 .unwrap();
128 let sub = dir.path().join("packages/app");
129 std::fs::create_dir_all(&sub).unwrap();
130 assert!(!is_config_fixable(&sub, None));
131 }
132}