1use std::{
2 fs, io,
3 path::{Path, PathBuf},
4};
5
6use crate::ClaudeCodeError;
7
8#[derive(Clone, Debug, Eq, PartialEq)]
14pub struct ClaudeHomeLayout {
15 root: PathBuf,
16}
17
18impl ClaudeHomeLayout {
19 pub fn new(root: impl Into<PathBuf>) -> Self {
20 Self { root: root.into() }
21 }
22
23 pub fn root(&self) -> &Path {
24 self.root.as_path()
25 }
26
27 pub fn xdg_config_home(&self) -> PathBuf {
28 self.root.join(".config")
29 }
30
31 pub fn xdg_data_home(&self) -> PathBuf {
32 self.root.join(".local").join("share")
33 }
34
35 pub fn xdg_cache_home(&self) -> PathBuf {
36 self.root.join(".cache")
37 }
38
39 #[cfg(windows)]
40 pub fn userprofile_dir(&self) -> PathBuf {
41 self.root.clone()
42 }
43
44 #[cfg(windows)]
45 pub fn appdata_dir(&self) -> PathBuf {
46 self.root.join("AppData").join("Roaming")
47 }
48
49 #[cfg(windows)]
50 pub fn localappdata_dir(&self) -> PathBuf {
51 self.root.join("AppData").join("Local")
52 }
53
54 pub fn materialize(&self, create_dirs: bool) -> Result<(), ClaudeCodeError> {
55 if !create_dirs {
56 return Ok(());
57 }
58
59 for path in [
60 self.root.as_path(),
61 self.xdg_config_home().as_path(),
62 self.xdg_data_home().as_path(),
63 self.xdg_cache_home().as_path(),
64 ] {
65 fs::create_dir_all(path).map_err(|source| ClaudeCodeError::PrepareClaudeHome {
66 path: path.to_path_buf(),
67 source,
68 })?;
69 }
70
71 #[cfg(windows)]
72 for path in [self.appdata_dir(), self.localappdata_dir()] {
73 fs::create_dir_all(&path).map_err(|source| ClaudeCodeError::PrepareClaudeHome {
74 path: path.to_path_buf(),
75 source,
76 })?;
77 }
78
79 Ok(())
80 }
81
82 pub fn seed_from_user_home(
83 &self,
84 seed_home: &Path,
85 level: ClaudeHomeSeedLevel,
86 ) -> Result<ClaudeHomeSeedOutcome, ClaudeCodeError> {
87 let mut outcome = ClaudeHomeSeedOutcome::default();
88
89 match level {
90 ClaudeHomeSeedLevel::MinimalAuth => {
91 seed_minimal(seed_home, self.root(), &mut outcome)?;
92 }
93 ClaudeHomeSeedLevel::FullProfile => {
94 seed_minimal(seed_home, self.root(), &mut outcome)?;
95 seed_full_profile(seed_home, self.root(), &mut outcome)?;
96 }
97 }
98
99 Ok(outcome)
100 }
101}
102
103#[derive(Clone, Copy, Debug, Eq, PartialEq)]
104pub enum ClaudeHomeSeedLevel {
105 MinimalAuth,
106 FullProfile,
107}
108
109#[derive(Clone, Debug, Default, Eq, PartialEq)]
110pub struct ClaudeHomeSeedOutcome {
111 pub copied_paths: Vec<PathBuf>,
112 pub skipped_paths: Vec<PathBuf>,
113}
114
115#[derive(Clone, Debug, Eq, PartialEq)]
116pub struct ClaudeHomeSeedRequest {
117 pub seed_user_home: PathBuf,
118 pub level: ClaudeHomeSeedLevel,
119}
120
121fn seed_minimal(
122 seed_home: &Path,
123 target_home: &Path,
124 outcome: &mut ClaudeHomeSeedOutcome,
125) -> Result<(), ClaudeCodeError> {
126 let mappings = [
127 (
128 seed_home.join(".claude.json"),
129 target_home.join(".claude.json"),
130 ),
131 (
132 seed_home.join(".claude").join("settings.json"),
133 target_home.join(".claude").join("settings.json"),
134 ),
135 (
136 seed_home.join(".claude").join("settings.local.json"),
137 target_home.join(".claude").join("settings.local.json"),
138 ),
139 ];
140
141 for (src, dst) in mappings {
142 copy_if_exists(&src, &dst, outcome)?;
143 }
144
145 copy_dir_if_exists(
146 &seed_home.join(".claude").join("plugins"),
147 &target_home.join(".claude").join("plugins"),
148 outcome,
149 )?;
150
151 Ok(())
152}
153
154#[cfg(target_os = "macos")]
155fn seed_full_profile(
156 seed_home: &Path,
157 target_home: &Path,
158 outcome: &mut ClaudeHomeSeedOutcome,
159) -> Result<(), ClaudeCodeError> {
160 copy_dir_if_exists(
161 &seed_home
162 .join("Library")
163 .join("Application Support")
164 .join("Claude"),
165 &target_home
166 .join("Library")
167 .join("Application Support")
168 .join("Claude"),
169 outcome,
170 )?;
171 Ok(())
172}
173
174#[cfg(windows)]
175fn seed_full_profile(
176 seed_home: &Path,
177 target_home: &Path,
178 outcome: &mut ClaudeHomeSeedOutcome,
179) -> Result<(), ClaudeCodeError> {
180 copy_dir_if_exists(
181 &seed_home.join("AppData").join("Roaming").join("Claude"),
182 &target_home.join("AppData").join("Roaming").join("Claude"),
183 outcome,
184 )?;
185 copy_dir_if_exists(
186 &seed_home.join("AppData").join("Local").join("Claude"),
187 &target_home.join("AppData").join("Local").join("Claude"),
188 outcome,
189 )?;
190 Ok(())
191}
192
193#[cfg(all(unix, not(target_os = "macos")))]
194fn seed_full_profile(
195 seed_home: &Path,
196 target_home: &Path,
197 outcome: &mut ClaudeHomeSeedOutcome,
198) -> Result<(), ClaudeCodeError> {
199 copy_dir_if_exists(
200 &seed_home.join(".config").join("claude"),
201 &target_home.join(".config").join("claude"),
202 outcome,
203 )?;
204 copy_dir_if_exists(
205 &seed_home.join(".local").join("share").join("claude"),
206 &target_home.join(".local").join("share").join("claude"),
207 outcome,
208 )?;
209 Ok(())
210}
211
212#[cfg(not(any(target_os = "macos", windows, all(unix, not(target_os = "macos")))))]
213fn seed_full_profile(
214 seed_home: &Path,
215 target_home: &Path,
216 outcome: &mut ClaudeHomeSeedOutcome,
217) -> Result<(), ClaudeCodeError> {
218 let _ = (seed_home, target_home, outcome);
219 Ok(())
220}
221
222fn copy_if_exists(
223 src: &Path,
224 dst: &Path,
225 outcome: &mut ClaudeHomeSeedOutcome,
226) -> Result<(), ClaudeCodeError> {
227 match fs::metadata(src) {
228 Ok(meta) => {
229 if !meta.is_file() {
230 outcome.skipped_paths.push(src.to_path_buf());
231 Ok(())
232 } else {
233 copy_file(src, dst)?;
234 outcome.copied_paths.push(dst.to_path_buf());
235 Ok(())
236 }
237 }
238 Err(err) if err.kind() == io::ErrorKind::NotFound => {
239 outcome.skipped_paths.push(src.to_path_buf());
240 Ok(())
241 }
242 Err(source) => Err(ClaudeCodeError::ClaudeHomeSeedIo {
243 path: src.to_path_buf(),
244 source,
245 }),
246 }
247}
248
249fn copy_dir_if_exists(
250 src: &Path,
251 dst: &Path,
252 outcome: &mut ClaudeHomeSeedOutcome,
253) -> Result<(), ClaudeCodeError> {
254 match fs::metadata(src) {
255 Ok(meta) => {
256 if !meta.is_dir() {
257 outcome.skipped_paths.push(src.to_path_buf());
258 Ok(())
259 } else {
260 copy_dir_recursive(src, dst)?;
261 outcome.copied_paths.push(dst.to_path_buf());
262 Ok(())
263 }
264 }
265 Err(err) if err.kind() == io::ErrorKind::NotFound => {
266 outcome.skipped_paths.push(src.to_path_buf());
267 Ok(())
268 }
269 Err(source) => Err(ClaudeCodeError::ClaudeHomeSeedIo {
270 path: src.to_path_buf(),
271 source,
272 }),
273 }
274}
275
276fn copy_file(src: &Path, dst: &Path) -> Result<(), ClaudeCodeError> {
277 if let Some(parent) = dst.parent() {
278 fs::create_dir_all(parent).map_err(|source| ClaudeCodeError::ClaudeHomeSeedIo {
279 path: parent.to_path_buf(),
280 source,
281 })?;
282 }
283 fs::copy(src, dst).map_err(|source| ClaudeCodeError::ClaudeHomeSeedCopy {
284 from: src.to_path_buf(),
285 to: dst.to_path_buf(),
286 error: source,
287 })?;
288 Ok(())
289}
290
291fn copy_dir_recursive(src: &Path, dst: &Path) -> Result<(), ClaudeCodeError> {
292 fs::create_dir_all(dst).map_err(|source| ClaudeCodeError::ClaudeHomeSeedIo {
293 path: dst.to_path_buf(),
294 source,
295 })?;
296
297 for entry in fs::read_dir(src).map_err(|source| ClaudeCodeError::ClaudeHomeSeedIo {
298 path: src.to_path_buf(),
299 source,
300 })? {
301 let entry = entry.map_err(|source| ClaudeCodeError::ClaudeHomeSeedIo {
302 path: src.to_path_buf(),
303 source,
304 })?;
305 let path = entry.path();
306 let file_name = entry.file_name();
307 let target_path = dst.join(file_name);
308
309 let meta =
310 fs::symlink_metadata(&path).map_err(|source| ClaudeCodeError::ClaudeHomeSeedIo {
311 path: path.clone(),
312 source,
313 })?;
314
315 if meta.is_dir() {
316 copy_dir_recursive(&path, &target_path)?;
317 continue;
318 }
319
320 if meta.is_file() {
321 copy_file(&path, &target_path)?;
322 continue;
323 }
324
325 if meta.file_type().is_symlink() {
326 if let Ok(link_target) = fs::read_link(&path) {
328 let resolved = if link_target.is_absolute() {
329 link_target
330 } else {
331 path.parent()
332 .unwrap_or_else(|| Path::new("/"))
333 .join(link_target)
334 };
335 if let Ok(target_meta) = fs::metadata(&resolved) {
336 if target_meta.is_dir() {
337 copy_dir_recursive(&resolved, &target_path)?;
338 } else if target_meta.is_file() {
339 copy_file(&resolved, &target_path)?;
340 }
341 }
342 }
343 continue;
344 }
345 }
346
347 Ok(())
348}