Skip to main content

claude_code/builder/
mod.rs

1use std::{collections::BTreeMap, path::PathBuf, time::Duration};
2
3use crate::client::ClaudeClient;
4use crate::home::{ClaudeHomeLayout, ClaudeHomeSeedLevel, ClaudeHomeSeedRequest};
5
6#[derive(Debug, Clone)]
7pub struct ClaudeClientBuilder {
8    pub(crate) binary: Option<PathBuf>,
9    pub(crate) working_dir: Option<PathBuf>,
10    pub(crate) env: BTreeMap<String, String>,
11    pub(crate) claude_home: Option<PathBuf>,
12    pub(crate) create_home_dirs: bool,
13    pub(crate) home_seed: Option<ClaudeHomeSeedRequest>,
14    pub(crate) timeout: Option<Duration>,
15    pub(crate) mirror_stdout: bool,
16    pub(crate) mirror_stderr: bool,
17}
18
19impl Default for ClaudeClientBuilder {
20    fn default() -> Self {
21        Self {
22            binary: None,
23            working_dir: None,
24            env: BTreeMap::new(),
25            claude_home: None,
26            create_home_dirs: true,
27            home_seed: None,
28            timeout: Some(Duration::from_secs(120)),
29            mirror_stdout: false,
30            mirror_stderr: false,
31        }
32    }
33}
34
35impl ClaudeClientBuilder {
36    pub fn binary(mut self, binary: impl Into<PathBuf>) -> Self {
37        self.binary = Some(binary.into());
38        self
39    }
40
41    pub fn working_dir(mut self, dir: impl Into<PathBuf>) -> Self {
42        self.working_dir = Some(dir.into());
43        self
44    }
45
46    pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
47        self.env.insert(key.into(), value.into());
48        self
49    }
50
51    /// Sets an application-scoped Claude home directory (wrapper-managed).
52    ///
53    /// When set, the wrapper injects environment variables (`HOME`, `XDG_*`, and Windows
54    /// equivalents) so the real `claude` CLI writes state beneath this directory.
55    ///
56    /// If you do not call this, the wrapper will also honor `CLAUDE_HOME` when present.
57    pub fn claude_home(mut self, home: impl Into<PathBuf>) -> Self {
58        self.claude_home = Some(home.into());
59        self
60    }
61
62    /// Controls whether the wrapper should create the Claude home directory tree when
63    /// [`Self::claude_home`] (or `CLAUDE_HOME`) is set.
64    pub fn create_home_dirs(mut self, enable: bool) -> Self {
65        self.create_home_dirs = enable;
66        self
67    }
68
69    /// Opt-in seeding of an isolated Claude home from an existing user profile.
70    ///
71    /// This is best-effort for missing sources; copy failures are surfaced when running
72    /// commands (the wrapper will return an error before spawning the real binary).
73    pub fn seed_profile_from(
74        mut self,
75        seed_user_home: impl Into<PathBuf>,
76        level: ClaudeHomeSeedLevel,
77    ) -> Self {
78        self.home_seed = Some(ClaudeHomeSeedRequest {
79            seed_user_home: seed_user_home.into(),
80            level,
81        });
82        self
83    }
84
85    /// Convenience helper that seeds from the current user's home directory (best-effort).
86    ///
87    /// If the home directory cannot be inferred from environment variables, this is a no-op.
88    pub fn seed_profile_from_current_user_home(mut self, level: ClaudeHomeSeedLevel) -> Self {
89        let home = std::env::var_os("HOME").or_else(|| std::env::var_os("USERPROFILE"));
90        if let Some(home) = home {
91            self.home_seed = Some(ClaudeHomeSeedRequest {
92                seed_user_home: PathBuf::from(home),
93                level,
94            });
95        }
96        self
97    }
98
99    pub fn timeout(mut self, timeout: Option<Duration>) -> Self {
100        self.timeout = timeout;
101        self
102    }
103
104    pub fn mirror_stdout(mut self, enabled: bool) -> Self {
105        self.mirror_stdout = enabled;
106        self
107    }
108
109    pub fn mirror_stderr(mut self, enabled: bool) -> Self {
110        self.mirror_stderr = enabled;
111        self
112    }
113
114    pub fn build(mut self) -> ClaudeClient {
115        // Avoid any updater side effects by default; callers may override explicitly.
116        self.env
117            .entry("DISABLE_AUTOUPDATER".to_string())
118            .or_insert_with(|| "1".to_string());
119
120        let claude_home_path = self
121            .claude_home
122            .take()
123            .or_else(|| std::env::var_os("CLAUDE_HOME").map(PathBuf::from));
124        let claude_home = claude_home_path.map(ClaudeHomeLayout::new);
125
126        if let Some(layout) = claude_home.as_ref() {
127            let root = layout.root().to_string_lossy().to_string();
128            self.env
129                .entry("CLAUDE_HOME".to_string())
130                .or_insert(root.clone());
131            self.env.entry("HOME".to_string()).or_insert(root.clone());
132            self.env
133                .entry("XDG_CONFIG_HOME".to_string())
134                .or_insert(layout.xdg_config_home().to_string_lossy().to_string());
135            self.env
136                .entry("XDG_DATA_HOME".to_string())
137                .or_insert(layout.xdg_data_home().to_string_lossy().to_string());
138            self.env
139                .entry("XDG_CACHE_HOME".to_string())
140                .or_insert(layout.xdg_cache_home().to_string_lossy().to_string());
141
142            #[cfg(windows)]
143            {
144                self.env
145                    .entry("USERPROFILE".to_string())
146                    .or_insert(root.clone());
147                self.env
148                    .entry("APPDATA".to_string())
149                    .or_insert(layout.appdata_dir().to_string_lossy().to_string());
150                self.env
151                    .entry("LOCALAPPDATA".to_string())
152                    .or_insert(layout.localappdata_dir().to_string_lossy().to_string());
153            }
154        }
155
156        let home_materialize_status = std::sync::Arc::new(std::sync::OnceLock::new());
157        let home_seed_status = std::sync::Arc::new(std::sync::OnceLock::new());
158
159        if let Some(layout) = claude_home.as_ref() {
160            let res = layout
161                .materialize(self.create_home_dirs)
162                .map_err(|e| e.to_string());
163            let _ = home_materialize_status.set(res);
164
165            if let Some(seed_req) = self.home_seed.as_ref() {
166                let res = layout
167                    .seed_from_user_home(&seed_req.seed_user_home, seed_req.level)
168                    .map(|_| ())
169                    .map_err(|e| e.to_string());
170                let _ = home_seed_status.set(res);
171            }
172        }
173
174        ClaudeClient {
175            binary: self.binary,
176            working_dir: self.working_dir,
177            env: self.env,
178            claude_home,
179            create_home_dirs: self.create_home_dirs,
180            home_seed: self.home_seed,
181            home_materialize_status,
182            home_seed_status,
183            timeout: self.timeout,
184            mirror_stdout: self.mirror_stdout,
185            mirror_stderr: self.mirror_stderr,
186        }
187    }
188}