thoughts_tool/mount/
auto_mount.rs1use crate::config::Mount;
2use crate::config::RepoConfigManager;
3use crate::config::RepoMappingManager;
4use crate::config::SyncStrategy;
5use crate::config::extract_org_repo_from_url;
6use crate::git::clone::CloneOptions;
7use crate::git::clone::clone_repository;
8use crate::git::ref_key::encode_ref_key;
9use crate::git::utils::get_control_repo_root;
10use crate::mount::MountOptions;
11use crate::mount::MountResolver;
12use crate::mount::MountSpace;
13use crate::mount::get_mount_manager;
14use crate::platform::detect_platform;
15use crate::utils::paths::ensure_dir;
16use anyhow::Result;
17use colored::*;
18use std::collections::HashMap;
19use std::path::PathBuf;
20
21pub async fn update_active_mounts() -> Result<()> {
22 let repo_root = get_control_repo_root(&std::env::current_dir()?)?;
23 let platform_info = detect_platform()?;
24 let mount_manager = get_mount_manager(&platform_info)?;
25 let repo_manager = RepoConfigManager::new(repo_root.clone());
26 let desired = repo_manager.load_desired_state()?.ok_or_else(|| {
27 anyhow::anyhow!("No repository configuration found. Run 'thoughts init'.")
28 })?;
29
30 let base = repo_root.join(".thoughts-data");
31
32 if base.is_symlink() && !base.exists() {
34 anyhow::bail!(
35 "Worktree .thoughts-data symlink is broken. \
36 Re-run 'thoughts init' in the worktree or main repository."
37 );
38 }
39
40 ensure_dir(&base)?;
41
42 let base_canon = std::fs::canonicalize(&base).unwrap_or_else(|_| base.clone());
44
45 let thoughts_dir = base.join(&desired.mount_dirs.thoughts);
47 let context_dir = base.join(&desired.mount_dirs.context);
48 let references_dir = base.join(&desired.mount_dirs.references);
49 ensure_dir(&thoughts_dir)?;
50 ensure_dir(&context_dir)?;
51 ensure_dir(&references_dir)?;
52
53 println!("{} filesystem mounts...", "Synchronizing".cyan());
54
55 let mut desired_targets: Vec<(MountSpace, Mount, bool, Option<String>)> = vec![];
57
58 if let Some(tm) = &desired.thoughts_mount {
59 let m = Mount::Git {
60 url: tm.remote.clone(),
61 subpath: tm.subpath.clone(),
62 sync: tm.sync,
63 };
64 desired_targets.push((MountSpace::Thoughts, m, false, None));
65 }
66
67 for cm in &desired.context_mounts {
68 let m = Mount::Git {
69 url: cm.remote.clone(),
70 subpath: cm.subpath.clone(),
71 sync: cm.sync,
72 };
73 let space = MountSpace::Context(cm.mount_path.clone());
74 desired_targets.push((space, m, false, None));
75 }
76
77 for rm in &desired.references {
78 let url = &rm.remote;
79 let (org, repo) = match extract_org_repo_from_url(url) {
80 Ok(x) => x,
81 Err(e) => {
82 println!(
83 " {} Invalid reference in config, skipping: {}\n {}",
84 "Warning:".yellow(),
85 url,
86 e
87 );
88 continue;
89 }
90 };
91 let m = Mount::Git {
92 url: url.clone(),
93 subpath: None,
94 sync: SyncStrategy::None,
95 };
96 let ref_key = rm.ref_name.as_deref().map(encode_ref_key).transpose()?;
97 let space = MountSpace::Reference {
98 org_path: org,
99 repo,
100 ref_key,
101 };
102 desired_targets.push((space, m, true, rm.ref_name.clone()));
103 }
104
105 let active = mount_manager.list_mounts().await?;
107 let mut active_map = HashMap::<String, PathBuf>::new();
108 for mi in active {
109 let target_canon = std::fs::canonicalize(&mi.target).unwrap_or_else(|_| mi.target.clone());
111 if target_canon.starts_with(&base_canon)
112 && let Ok(rel) = target_canon.strip_prefix(&base_canon)
113 {
114 let key = rel.to_string_lossy().to_string();
115 active_map.insert(key, mi.target.clone());
116 }
117 }
118
119 for (active_key, target_path) in &active_map {
121 if !desired_targets
122 .iter()
123 .any(|(space, _, _, _)| space.relative_path(&desired.mount_dirs) == *active_key)
124 {
125 println!(" {} removed mount: {}", "Unmounting".yellow(), active_key);
126 mount_manager.unmount(target_path, false).await?;
127 }
128 }
129
130 let resolver = MountResolver::new()?;
132 for (space, m, _read_only, ref_name) in &desired_targets {
133 let key = space.relative_path(&desired.mount_dirs);
134 if !active_map.contains_key(&key) {
135 let target = desired.get_mount_target(space, &repo_root);
136 ensure_dir(target.parent().unwrap())?;
137
138 let src = match (space, m) {
140 (MountSpace::Reference { .. }, Mount::Git { url, .. }) => {
141 let repo_mapping = RepoMappingManager::new()?;
142 match repo_mapping.resolve_reference_url(url, ref_name.as_deref())? {
143 Some(path) => path,
144 None => {
145 println!(" {} repository {} ...", "Cloning".yellow(), url);
146 clone_and_map(url, ref_name.as_deref()).await?
147 }
148 }
149 }
150 _ => match resolver.resolve_mount(m) {
151 Ok(p) => p,
152 Err(_) => {
153 if let Mount::Git { url, .. } = &m {
154 println!(" {} repository {} ...", "Cloning".yellow(), url);
155 clone_and_map(url, None).await?
156 } else {
157 continue;
158 }
159 }
160 },
161 };
162
163 let mut options = MountOptions::default();
165 if space.is_read_only() {
166 options.read_only = true;
167 }
168
169 println!(
170 " {} {}: {}",
171 "Mounting".green(),
172 space,
173 if space.is_read_only() {
174 "(read-only)"
175 } else {
176 ""
177 }
178 );
179
180 match mount_manager.mount(&[src], &target, &options).await {
181 Ok(_) => println!(" {} Successfully mounted", "✓".green()),
182 Err(e) => eprintln!(" {} Failed to mount: {}", "✗".red(), e),
183 }
184 }
185 }
186
187 println!("{} Mount synchronization complete", "✓".green());
188 Ok(())
189}
190
191async fn clone_and_map(url: &str, ref_name: Option<&str>) -> Result<PathBuf> {
192 let mut repo_mapping = RepoMappingManager::new()?;
193 let default_path = RepoMappingManager::get_default_reference_clone_path(url, ref_name)?;
194
195 let clone_opts = CloneOptions {
197 url: url.to_string(),
198 target_path: default_path.clone(),
199 branch: ref_name.map(str::to_string),
200 };
201 clone_repository(&clone_opts)?;
202
203 repo_mapping.add_reference_mapping(url.to_string(), ref_name, default_path.clone(), true)?;
205
206 Ok(default_path)
207}