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