Skip to main content

capo_agent/
paths.rs

1//! Filesystem path helpers for capo's per-user agent directory.
2//!
3//! Mirrors pi's layout: `~/.capo/agent/` is the global app dir
4//! (overridable via `CAPO_AGENT_DIR`), and everything else
5//! (sessions, settings, auth, permissions) lives underneath.
6
7use std::env;
8use std::path::PathBuf;
9
10/// Resolve the per-user agent directory.
11///
12/// Priority:
13/// 1. `$CAPO_AGENT_DIR` env var if set (leading `~/` expanded to `$HOME`).
14/// 2. `$HOME/.capo/agent`.
15/// 3. `./.capo/agent` as a last-resort fallback if `$HOME` is missing.
16pub fn agent_dir() -> PathBuf {
17    agent_dir_with(|name| env::var(name).ok())
18}
19
20/// Path to the user-level extensions manifest.
21pub fn extensions_manifest_path(agent_dir: &std::path::Path) -> std::path::PathBuf {
22    agent_dir.join("extensions.toml")
23}
24
25/// Test seam: lookup env vars through a closure. Production calls
26/// [`agent_dir`] which delegates to `env::var`.
27pub fn agent_dir_with<F>(lookup: F) -> PathBuf
28where
29    F: Fn(&str) -> Option<String>,
30{
31    if let Some(raw) = lookup("CAPO_AGENT_DIR") {
32        return expand_tilde(&raw, &lookup);
33    }
34    if let Some(home) = lookup("HOME") {
35        return PathBuf::from(home).join(".capo").join("agent");
36    }
37    PathBuf::from(".capo").join("agent")
38}
39
40fn expand_tilde<F>(raw: &str, lookup: &F) -> PathBuf
41where
42    F: Fn(&str) -> Option<String>,
43{
44    let trimmed = raw.trim();
45    if trimmed == "~" {
46        return lookup("HOME")
47            .map(PathBuf::from)
48            .unwrap_or_else(|| PathBuf::from("~"));
49    }
50    if let Some(rest) = trimmed.strip_prefix("~/") {
51        if let Some(home) = lookup("HOME") {
52            return PathBuf::from(home).join(rest);
53        }
54    }
55    PathBuf::from(trimmed)
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61    use std::collections::HashMap;
62
63    fn lookup(map: HashMap<&'static str, &'static str>) -> impl Fn(&str) -> Option<String> {
64        move |name| map.get(name).map(|v| (*v).to_string())
65    }
66
67    #[test]
68    fn agent_dir_uses_capo_agent_dir_env() {
69        let env = HashMap::from([("CAPO_AGENT_DIR", "/tmp/x")]);
70        assert_eq!(agent_dir_with(lookup(env)), PathBuf::from("/tmp/x"));
71    }
72
73    #[test]
74    fn agent_dir_expands_tilde_in_capo_agent_dir() {
75        let env = HashMap::from([("CAPO_AGENT_DIR", "~/.test-capo"), ("HOME", "/Users/wade")]);
76        assert_eq!(
77            agent_dir_with(lookup(env)),
78            PathBuf::from("/Users/wade/.test-capo")
79        );
80    }
81
82    #[test]
83    fn agent_dir_defaults_to_home_capo_agent() {
84        let env = HashMap::from([("HOME", "/Users/wade")]);
85        assert_eq!(
86            agent_dir_with(lookup(env)),
87            PathBuf::from("/Users/wade/.capo/agent")
88        );
89    }
90}