toolpath_opencode/
paths.rs1use crate::error::{ConvoError, Result};
17use sha1::{Digest, Sha1};
18use std::path::{Path, PathBuf};
19
20const SNAPSHOT_SUBDIR: &str = "snapshot";
21const DB_FILE: &str = "opencode.db";
22const LOG_SUBDIR: &str = "log";
23
24#[derive(Debug, Clone)]
26pub struct PathResolver {
27 home_dir: Option<PathBuf>,
28 data_dir: Option<PathBuf>,
29}
30
31impl Default for PathResolver {
32 fn default() -> Self {
33 Self::new()
34 }
35}
36
37impl PathResolver {
38 pub fn new() -> Self {
39 Self {
40 home_dir: home_dir(),
41 data_dir: None,
42 }
43 }
44
45 pub fn with_home<P: Into<PathBuf>>(mut self, home: P) -> Self {
46 self.home_dir = Some(home.into());
47 self
48 }
49
50 pub fn with_data_dir<P: Into<PathBuf>>(mut self, data_dir: P) -> Self {
53 self.data_dir = Some(data_dir.into());
54 self
55 }
56
57 pub fn home_dir(&self) -> Result<&Path> {
58 self.home_dir.as_deref().ok_or(ConvoError::NoHomeDirectory)
59 }
60
61 pub fn data_dir(&self) -> Result<PathBuf> {
62 if let Some(d) = &self.data_dir {
63 return Ok(d.clone());
64 }
65 if let Some(xdg) = std::env::var_os("XDG_DATA_HOME") {
67 let p = PathBuf::from(xdg).join("opencode");
68 if !p.as_os_str().is_empty() {
69 return Ok(p);
70 }
71 }
72 Ok(self.home_dir()?.join(".local/share/opencode"))
73 }
74
75 pub fn db_path(&self) -> Result<PathBuf> {
76 Ok(self.data_dir()?.join(DB_FILE))
77 }
78
79 pub fn snapshot_root(&self) -> Result<PathBuf> {
80 Ok(self.data_dir()?.join(SNAPSHOT_SUBDIR))
81 }
82
83 pub fn log_dir(&self) -> Result<PathBuf> {
84 Ok(self.data_dir()?.join(LOG_SUBDIR))
85 }
86
87 pub fn snapshot_gitdir(&self, project_id: &str, worktree: &Path) -> Result<PathBuf> {
101 let root = self.snapshot_root()?;
102 let worktree_hash = sha1_hex(worktree.to_string_lossy().as_bytes());
103 let nested = root.join(project_id).join(&worktree_hash);
104 if nested.exists() {
105 return Ok(nested);
106 }
107 let flat = root.join(project_id);
108 if flat.exists() && flat.join("config").exists() {
109 return Ok(flat);
110 }
111 Ok(nested)
112 }
113
114 pub fn exists(&self) -> bool {
115 self.data_dir().map(|p| p.exists()).unwrap_or(false)
116 }
117
118 pub fn db_exists(&self) -> bool {
119 self.db_path().map(|p| p.exists()).unwrap_or(false)
120 }
121}
122
123fn home_dir() -> Option<PathBuf> {
124 std::env::var_os("HOME")
125 .or_else(|| std::env::var_os("USERPROFILE"))
126 .map(PathBuf::from)
127}
128
129pub(crate) fn sha1_hex(bytes: &[u8]) -> String {
130 let mut h = Sha1::new();
131 h.update(bytes);
132 let digest = h.finalize();
133 let mut out = String::with_capacity(40);
134 for b in digest {
135 use std::fmt::Write;
136 let _ = write!(out, "{:02x}", b);
137 }
138 out
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144 use std::fs;
145 use tempfile::TempDir;
146
147 fn setup() -> (TempDir, PathResolver) {
148 let temp = TempDir::new().unwrap();
149 let data = temp.path().join(".local/share/opencode");
150 fs::create_dir_all(&data).unwrap();
151 let resolver = PathResolver::new()
152 .with_home(temp.path())
153 .with_data_dir(&data);
154 (temp, resolver)
155 }
156
157 #[test]
158 fn data_dir_defaults_to_home_when_no_xdg() {
159 let temp = TempDir::new().unwrap();
160 let r = PathResolver::new().with_home(temp.path());
163 let d = r.data_dir().unwrap();
164 assert!(d.ends_with(".local/share/opencode"), "got {:?}", d);
165 }
166
167 #[test]
168 fn db_path_under_data_dir() {
169 let (_t, r) = setup();
170 assert!(r.db_path().unwrap().ends_with("opencode/opencode.db"));
171 }
172
173 #[test]
174 fn snapshot_gitdir_uses_sha1_of_worktree() {
175 let (_t, r) = setup();
176 let pid = "4e82d608d080e9d92be51e24b592302df6a8cbf8";
177 let wt = Path::new("/Users/ben/empathic/oss/toolpath");
178 let gd = r.snapshot_gitdir(pid, wt).unwrap();
179 assert!(gd.to_string_lossy().contains(pid));
181 assert!(
182 gd.to_string_lossy()
183 .contains("bb93f39a69862ba18e7893cc96424f83876a9687")
184 );
185 }
186
187 #[test]
188 fn sha1_of_known_string() {
189 assert_eq!(
190 sha1_hex(b"/Users/ben/empathic/oss/toolpath"),
191 "bb93f39a69862ba18e7893cc96424f83876a9687"
192 );
193 }
194
195 #[test]
196 fn exists_reflects_data_dir() {
197 let (_t, r) = setup();
198 assert!(r.exists());
199 let missing = PathResolver::new().with_data_dir("/never/exists");
200 assert!(!missing.exists());
201 }
202}