ralph_workflow/config/path_resolver/
boundary.rs1use std::collections::HashMap;
12use std::io;
13use std::path::{Path, PathBuf};
14use std::sync::{Arc, RwLock};
15
16use crate::config::ConfigEnvironment;
17
18#[derive(Debug, Clone, Default)]
39pub struct MemoryConfigEnvironment {
40 unified_config_path: Option<PathBuf>,
41 prompt_path: Option<PathBuf>,
42 local_config_path: Option<PathBuf>,
43 worktree_root: Option<PathBuf>,
44 files: Arc<RwLock<HashMap<PathBuf, String>>>,
45 dirs: Arc<RwLock<std::collections::HashSet<PathBuf>>>,
46 env_vars: HashMap<String, String>,
47}
48
49impl MemoryConfigEnvironment {
50 #[must_use]
51 pub fn new() -> Self {
52 Self::default()
53 }
54
55 #[must_use]
56 pub fn with_unified_config_path<P: Into<PathBuf>>(mut self, path: P) -> Self {
57 self.unified_config_path = Some(path.into());
58 self
59 }
60
61 #[must_use]
62 pub fn with_local_config_path<P: Into<PathBuf>>(mut self, path: P) -> Self {
63 self.local_config_path = Some(path.into());
64 self
65 }
66
67 #[must_use]
68 pub fn with_prompt_path<P: Into<PathBuf>>(mut self, path: P) -> Self {
69 self.prompt_path = Some(path.into());
70 self
71 }
72
73 #[must_use]
74 pub fn with_file<P: Into<PathBuf>, S: Into<String>>(self, path: P, content: S) -> Self {
75 let path = path.into();
76 self.files.write()
77 .expect("RwLock poisoned - indicates panic in another thread holding MemoryConfigEnvironment files lock")
78 .insert(path, content.into());
79 self
80 }
81
82 #[must_use]
83 pub fn with_worktree_root<P: Into<PathBuf>>(mut self, path: P) -> Self {
84 self.worktree_root = Some(path.into());
85 self
86 }
87
88 #[must_use]
89 pub fn with_env_var(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
90 self.env_vars.insert(key.into(), value.into());
91 self
92 }
93
94 #[must_use]
95 pub fn get_file(&self, path: &Path) -> Option<String> {
96 self.files.read()
97 .expect("RwLock poisoned - indicates panic in another thread holding MemoryConfigEnvironment files lock")
98 .get(path).cloned()
99 }
100
101 #[must_use]
102 pub fn was_written(&self, path: &Path) -> bool {
103 self.files.read()
104 .expect("RwLock poisoned - indicates panic in another thread holding MemoryConfigEnvironment files lock")
105 .contains_key(path)
106 }
107}
108
109impl ConfigEnvironment for MemoryConfigEnvironment {
110 fn unified_config_path(&self) -> Option<PathBuf> {
111 self.unified_config_path.clone()
112 }
113
114 fn get_env_var(&self, key: &str) -> Option<String> {
115 self.env_vars.get(key).cloned()
116 }
117
118 fn local_config_path(&self) -> Option<PathBuf> {
119 if let Some(ref path) = self.local_config_path {
120 return Some(path.clone());
121 }
122
123 self.worktree_root()
124 .map(|root| root.join(".agent/ralph-workflow.toml"))
125 .or_else(|| Some(PathBuf::from(".agent/ralph-workflow.toml")))
126 }
127
128 fn prompt_path(&self) -> PathBuf {
129 self.prompt_path
130 .clone()
131 .unwrap_or_else(|| PathBuf::from("PROMPT.md"))
132 }
133
134 fn file_exists(&self, path: &Path) -> bool {
135 self.files.read()
136 .expect("RwLock poisoned - indicates panic in another thread holding MemoryConfigEnvironment files lock")
137 .contains_key(path)
138 }
139
140 fn read_file(&self, path: &Path) -> io::Result<String> {
141 self.files
142 .read()
143 .expect("RwLock poisoned - indicates panic in another thread holding MemoryConfigEnvironment files lock")
144 .get(path)
145 .cloned()
146 .ok_or_else(|| {
147 io::Error::new(
148 io::ErrorKind::NotFound,
149 format!("File not found: {}", path.display()),
150 )
151 })
152 }
153
154 fn write_file(&self, path: &Path, content: &str) -> io::Result<()> {
155 if let Some(parent) = path.parent() {
156 self.dirs.write()
157 .expect("RwLock poisoned - indicates panic in another thread holding MemoryConfigEnvironment dirs lock")
158 .insert(parent.to_path_buf());
159 }
160 self.files
161 .write()
162 .expect("RwLock poisoned - indicates panic in another thread holding MemoryConfigEnvironment files lock")
163 .insert(path.to_path_buf(), content.to_string());
164 Ok(())
165 }
166
167 fn create_dir_all(&self, path: &Path) -> io::Result<()> {
168 self.dirs.write()
169 .expect("RwLock poisoned - indicates panic in another thread holding MemoryConfigEnvironment dirs lock")
170 .insert(path.to_path_buf());
171 Ok(())
172 }
173
174 fn worktree_root(&self) -> Option<PathBuf> {
175 self.worktree_root.clone()
176 }
177}
178
179#[derive(Debug, Default, Clone, Copy)]
184pub struct RealConfigEnvironment;
185
186fn compute_canonical_repo_root(gitdir: &Path) -> Option<PathBuf> {
187 let parent = gitdir.parent()?;
188 if parent.file_name().and_then(|n| n.to_str()) == Some("worktrees") {
189 parent.parent().and_then(|p| p.parent()).map(PathBuf::from)
190 } else {
191 None
192 }
193}
194
195impl ConfigEnvironment for RealConfigEnvironment {
196 fn unified_config_path(&self) -> Option<PathBuf> {
197 crate::config::unified::unified_config_path()
198 }
199
200 fn get_env_var(&self, key: &str) -> Option<String> {
201 std::env::var(key).ok()
202 }
203
204 fn file_exists(&self, path: &Path) -> bool {
205 path.exists()
206 }
207
208 fn read_file(&self, path: &Path) -> io::Result<String> {
209 std::fs::read_to_string(path)
210 }
211
212 fn write_file(&self, path: &Path, content: &str) -> io::Result<()> {
213 if let Some(parent) = path.parent() {
214 std::fs::create_dir_all(parent)?;
215 }
216 std::fs::write(path, content)
217 }
218
219 fn create_dir_all(&self, path: &Path) -> io::Result<()> {
220 std::fs::create_dir_all(path)
221 }
222
223 fn worktree_root(&self) -> Option<PathBuf> {
224 let repo = git2::Repository::discover(".").ok()?;
225 let gitdir = repo.path();
226
227 compute_canonical_repo_root(gitdir).or_else(|| repo.workdir().map(PathBuf::from))
228 }
229
230 fn local_config_path(&self) -> Option<PathBuf> {
231 self.worktree_root()
232 .map(|root| root.join(".agent/ralph-workflow.toml"))
233 .or_else(|| Some(PathBuf::from(".agent/ralph-workflow.toml")))
234 }
235}