Skip to main content

thoughts_tool/mount/
auto_mount.rs

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