thoughts-tool 0.12.0

Flexible thought management using filesystem mounts for git repositories
Documentation
use crate::config::Mount;
use crate::config::RepoConfigManager;
use crate::config::RepoMappingManager;
use crate::config::SyncStrategy;
use crate::config::extract_org_repo_from_url;
use crate::git::clone::CloneOptions;
use crate::git::clone::clone_repository;
use crate::git::ref_key::encode_ref_key;
use crate::git::utils::get_control_repo_root;
use crate::mount::MountOptions;
use crate::mount::MountResolver;
use crate::mount::MountSpace;
use crate::mount::get_mount_manager;
use crate::platform::detect_platform;
use crate::utils::paths::ensure_dir;
use anyhow::Result;
use colored::Colorize;
use std::collections::HashMap;
use std::path::PathBuf;

pub async fn update_active_mounts() -> Result<()> {
    let repo_root = get_control_repo_root(&std::env::current_dir()?)?;
    let platform_info = detect_platform()?;
    let mount_manager = get_mount_manager(&platform_info)?;
    let repo_manager = RepoConfigManager::new(repo_root.clone());
    let desired = repo_manager.load_desired_state()?.ok_or_else(|| {
        anyhow::anyhow!("No repository configuration found. Run 'thoughts init'.")
    })?;

    let base = repo_root.join(".thoughts-data");

    // Check for broken symlink before proceeding
    if base.is_symlink() && !base.exists() {
        anyhow::bail!(
            "Worktree .thoughts-data symlink is broken. \
             Re-run 'thoughts init' in the worktree or main repository."
        );
    }

    ensure_dir(&base)?;

    // Canonicalize base for mount comparison
    let base_canon = std::fs::canonicalize(&base).unwrap_or_else(|_| base.clone());

    // Symlink targets (actual mount dirs)
    let thoughts_dir = base.join(&desired.mount_dirs.thoughts);
    let context_dir = base.join(&desired.mount_dirs.context);
    let references_dir = base.join(&desired.mount_dirs.references);
    ensure_dir(&thoughts_dir)?;
    ensure_dir(&context_dir)?;
    ensure_dir(&references_dir)?;

    println!("{} filesystem mounts...", "Synchronizing".cyan());

    // Build desired targets with MountSpace
    let mut desired_targets: Vec<(MountSpace, Mount, bool, Option<String>)> = vec![];

    if let Some(tm) = &desired.thoughts_mount {
        let m = Mount::Git {
            url: tm.remote.clone(),
            subpath: tm.subpath.clone(),
            sync: tm.sync,
        };
        desired_targets.push((MountSpace::Thoughts, m, false, None));
    }

    for cm in &desired.context_mounts {
        let m = Mount::Git {
            url: cm.remote.clone(),
            subpath: cm.subpath.clone(),
            sync: cm.sync,
        };
        let space = MountSpace::Context(cm.mount_path.clone());
        desired_targets.push((space, m, false, None));
    }

    for rm in &desired.references {
        let url = &rm.remote;
        let (org, repo) = match extract_org_repo_from_url(url) {
            Ok(x) => x,
            Err(e) => {
                println!(
                    "  {} Invalid reference in config, skipping: {}\n     {}",
                    "Warning:".yellow(),
                    url,
                    e
                );
                continue;
            }
        };
        let m = Mount::Git {
            url: url.clone(),
            subpath: None,
            sync: SyncStrategy::None,
        };
        let ref_key = rm.ref_name.as_deref().map(encode_ref_key).transpose()?;
        let space = MountSpace::Reference {
            org_path: org,
            repo,
            ref_key,
        };
        desired_targets.push((space, m, true, rm.ref_name.clone()));
    }

    // Query active mounts and key them by relative path under .thoughts-data
    let active = mount_manager.list_mounts().await?;
    let mut active_map = HashMap::<String, PathBuf>::new();
    for mi in active {
        // Canonicalize target for comparison
        let target_canon = std::fs::canonicalize(&mi.target).unwrap_or_else(|_| mi.target.clone());
        if target_canon.starts_with(&base_canon)
            && let Ok(rel) = target_canon.strip_prefix(&base_canon)
        {
            let key = rel.to_string_lossy().to_string();
            active_map.insert(key, mi.target.clone());
        }
    }

    // Unmount no-longer-desired
    for (active_key, target_path) in &active_map {
        if !desired_targets
            .iter()
            .any(|(space, _, _, _)| space.relative_path(&desired.mount_dirs) == *active_key)
        {
            println!("  {} removed mount: {}", "Unmounting".yellow(), active_key);
            mount_manager.unmount(target_path, false).await?;
        }
    }

    // Mount missing targets
    let resolver = MountResolver::new()?;
    for (space, m, _read_only, ref_name) in &desired_targets {
        let key = space.relative_path(&desired.mount_dirs);
        if !active_map.contains_key(&key) {
            let target = desired.get_mount_target(space, &repo_root);
            let parent = target.parent().ok_or_else(|| {
                anyhow::anyhow!("mount target has no parent directory: {}", target.display())
            })?;
            ensure_dir(parent)?;

            // Resolve mount source
            let src = match (space, m) {
                (MountSpace::Reference { .. }, Mount::Git { url, .. }) => {
                    let repo_mapping = RepoMappingManager::new()?;
                    if let Some(path) =
                        repo_mapping.resolve_reference_url(url, ref_name.as_deref())?
                    {
                        path
                    } else {
                        println!("  {} repository {} ...", "Cloning".yellow(), url);
                        clone_and_map(url, ref_name.as_deref())?
                    }
                }
                _ => match resolver.resolve_mount(m) {
                    Ok(p) => p,
                    Err(_) => {
                        if let Mount::Git { url, .. } = &m {
                            println!("  {} repository {} ...", "Cloning".yellow(), url);
                            clone_and_map(url, None)?
                        } else {
                            continue;
                        }
                    }
                },
            };

            // Mount with appropriate options
            let mut options = MountOptions::default();
            if space.is_read_only() {
                options.read_only = true;
            }

            println!(
                "  {} {}: {}",
                "Mounting".green(),
                space,
                if space.is_read_only() {
                    "(read-only)"
                } else {
                    ""
                }
            );

            match mount_manager.mount(&[src], &target, &options).await {
                Ok(()) => println!("    {} Successfully mounted", "".green()),
                Err(e) => eprintln!("    {} Failed to mount: {}", "".red(), e),
            }
        }
    }

    println!("{} Mount synchronization complete", "".green());
    Ok(())
}

fn clone_and_map(url: &str, ref_name: Option<&str>) -> Result<PathBuf> {
    let mut repo_mapping = RepoMappingManager::new()?;
    let default_path = RepoMappingManager::get_default_reference_clone_path(url, ref_name)?;

    // Clone to default location
    let clone_opts = CloneOptions {
        url: url.to_string(),
        target_path: default_path.clone(),
        branch: ref_name.map(str::to_string),
    };
    clone_repository(&clone_opts)?;

    // Add mapping
    repo_mapping.add_reference_mapping(url, ref_name, default_path.clone(), true)?;

    Ok(default_path)
}