Skip to main content

xurl_core/provider/
mod.rs

1use std::env;
2use std::path::PathBuf;
3
4use dirs::home_dir;
5
6use crate::error::{Result, XurlError};
7use crate::model::{ProviderKind, ResolvedThread, WriteRequest, WriteResult};
8
9pub mod amp;
10pub mod claude;
11pub mod codex;
12pub mod gemini;
13pub mod opencode;
14pub mod pi;
15pub mod skills;
16
17pub(crate) fn append_passthrough_args(args: &mut Vec<String>, params: &[(String, Option<String>)]) {
18    append_passthrough_args_excluding(args, params, &[]);
19}
20
21pub(crate) fn append_passthrough_args_excluding(
22    args: &mut Vec<String>,
23    params: &[(String, Option<String>)],
24    excluded_keys: &[&str],
25) -> Vec<String> {
26    let mut excluded = Vec::new();
27    for (key, value) in params {
28        if excluded_keys.iter().any(|candidate| candidate == key) {
29            excluded.push(key.clone());
30            continue;
31        }
32        args.push(format!("--{key}"));
33        if let Some(value) = value
34            && !value.is_empty()
35        {
36            args.push(value.clone());
37        }
38    }
39    excluded
40}
41
42pub trait WriteEventSink {
43    fn on_session_ready(&mut self, provider: ProviderKind, session_id: &str) -> Result<()>;
44    fn on_text_delta(&mut self, text: &str) -> Result<()>;
45}
46
47pub trait Provider {
48    fn kind(&self) -> ProviderKind;
49    fn resolve(&self, session_id: &str) -> Result<ResolvedThread>;
50    fn write(&self, req: &WriteRequest, sink: &mut dyn WriteEventSink) -> Result<WriteResult> {
51        let _ = (req, sink);
52        Err(XurlError::UnsupportedProviderWrite(self.kind().to_string()))
53    }
54}
55
56#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct ProviderRoots {
58    pub amp_root: PathBuf,
59    pub codex_root: PathBuf,
60    pub claude_root: PathBuf,
61    pub gemini_root: PathBuf,
62    pub pi_root: PathBuf,
63    pub opencode_root: PathBuf,
64    pub skills_root: PathBuf,
65    pub skills_cache_root: PathBuf,
66}
67
68impl ProviderRoots {
69    pub fn from_env_or_home() -> Result<Self> {
70        let home = home_dir().ok_or(XurlError::HomeDirectoryNotFound)?;
71
72        // Precedence:
73        // 1) XDG_DATA_HOME/amp
74        // 2) ~/.local/share/amp
75        let amp_root = env::var_os("XDG_DATA_HOME")
76            .filter(|path| !path.is_empty())
77            .map(PathBuf::from)
78            .map(|path| path.join("amp"))
79            .unwrap_or_else(|| home.join(".local/share/amp"));
80
81        // Precedence:
82        // 1) CODEX_HOME (official Codex home env)
83        // 2) ~/.codex (Codex default)
84        let codex_root = env::var_os("CODEX_HOME")
85            .map(PathBuf::from)
86            .unwrap_or_else(|| home.join(".codex"));
87
88        // Precedence:
89        // 1) CLAUDE_CONFIG_DIR (official Claude Code config/data root env)
90        // 2) ~/.claude (Claude default)
91        let claude_root = env::var_os("CLAUDE_CONFIG_DIR")
92            .map(PathBuf::from)
93            .unwrap_or_else(|| home.join(".claude"));
94
95        // Precedence:
96        // 1) GEMINI_CLI_HOME/.gemini (official Gemini CLI home env)
97        // 2) ~/.gemini (Gemini default)
98        let gemini_root = env::var_os("GEMINI_CLI_HOME")
99            .map(PathBuf::from)
100            .map(|path| path.join(".gemini"))
101            .unwrap_or_else(|| home.join(".gemini"));
102
103        // Precedence:
104        // 1) PI_CODING_AGENT_DIR (official pi coding agent root env)
105        // 2) ~/.pi/agent (pi default)
106        let pi_root = env::var_os("PI_CODING_AGENT_DIR")
107            .filter(|path| !path.is_empty())
108            .map(PathBuf::from)
109            .unwrap_or_else(|| home.join(".pi/agent"));
110
111        // Precedence:
112        // 1) XDG_DATA_HOME/opencode
113        // 2) ~/.local/share/opencode
114        let opencode_root = env::var_os("XDG_DATA_HOME")
115            .filter(|path| !path.is_empty())
116            .map(PathBuf::from)
117            .map(|path| path.join("opencode"))
118            .unwrap_or_else(|| home.join(".local/share/opencode"));
119
120        // Precedence:
121        // 1) XURL_SKILLS_ROOT
122        // 2) ~/.agents/skills
123        let skills_root = env::var_os("XURL_SKILLS_ROOT")
124            .filter(|path| !path.is_empty())
125            .map(PathBuf::from)
126            .unwrap_or_else(|| home.join(".agents/skills"));
127
128        // Precedence:
129        // 1) XURL_SKILLS_CACHE_ROOT
130        // 2) ~/.xurl/skills
131        let skills_cache_root = env::var_os("XURL_SKILLS_CACHE_ROOT")
132            .filter(|path| !path.is_empty())
133            .map(PathBuf::from)
134            .unwrap_or_else(|| home.join(".xurl/skills"));
135
136        Ok(Self {
137            amp_root,
138            codex_root,
139            claude_root,
140            gemini_root,
141            pi_root,
142            opencode_root,
143            skills_root,
144            skills_cache_root,
145        })
146    }
147}