thoughts_tool/workspace/
mod.rs

1use anyhow::{Context, Result};
2use atomicwrites::{AtomicFile, OverwriteBehavior};
3use chrono::Datelike;
4use serde_json::json;
5use std::fs;
6use std::io::Write;
7use std::path::PathBuf;
8
9use crate::config::{Mount, RepoConfigManager};
10use crate::git::utils::{
11    find_repo_root, get_control_repo_root, get_current_branch, get_remote_url,
12};
13use crate::mount::MountResolver;
14
15/// Paths for the current active work directory
16#[derive(Debug, Clone)]
17pub struct ActiveWork {
18    pub dir_name: String,
19    pub base: PathBuf,
20    pub research: PathBuf,
21    pub plans: PathBuf,
22    pub artifacts: PathBuf,
23}
24
25/// Resolve thoughts root via configured thoughts_mount
26fn resolve_thoughts_root() -> Result<PathBuf> {
27    let control_root = get_control_repo_root(&std::env::current_dir()?)?;
28    let mgr = RepoConfigManager::new(control_root);
29    let ds = mgr.load_desired_state()?.ok_or_else(|| {
30        anyhow::anyhow!("No repository configuration found. Run 'thoughts init'.")
31    })?;
32
33    let tm = ds.thoughts_mount.as_ref().ok_or_else(|| {
34        anyhow::anyhow!(
35            "No thoughts_mount configured in repository configuration.\n\
36             Add thoughts_mount to .thoughts/config.json and run 'thoughts mount update'."
37        )
38    })?;
39
40    let resolver = MountResolver::new()?;
41    let mount = Mount::Git {
42        url: tm.remote.clone(),
43        subpath: tm.subpath.clone(),
44        sync: tm.sync,
45    };
46
47    resolver
48        .resolve_mount(&mount)
49        .context("Thoughts mount not cloned. Run 'thoughts sync' or 'thoughts mount update' first.")
50}
51
52/// Compute work directory name: ISO week for main/master, branch name otherwise
53fn current_work_dir_name() -> Result<String> {
54    let code_root = find_repo_root(&std::env::current_dir()?)?;
55    let branch = get_current_branch(&code_root)?;
56    if branch == "main" || branch == "master" {
57        let now = chrono::Utc::now().date_naive();
58        let iso = now.iso_week();
59        Ok(format!("{}-W{:02}", iso.year(), iso.week()))
60    } else {
61        Ok(branch)
62    }
63}
64
65/// Ensure active work directory exists with subdirs and manifest
66pub fn ensure_active_work() -> Result<ActiveWork> {
67    let thoughts_root = resolve_thoughts_root()?;
68    let dir_name = current_work_dir_name()?;
69    let base = thoughts_root.join("active").join(&dir_name);
70
71    // Create structure if missing
72    if !base.exists() {
73        fs::create_dir_all(&base).context("Failed to create base work directory")?;
74        fs::create_dir_all(base.join("research")).context("Failed to create research directory")?;
75        fs::create_dir_all(base.join("plans")).context("Failed to create plans directory")?;
76        fs::create_dir_all(base.join("artifacts"))
77            .context("Failed to create artifacts directory")?;
78
79        // Create manifest.json atomically
80        let code_root = find_repo_root(&std::env::current_dir()?)?;
81        let source_repo = get_remote_url(&code_root).unwrap_or_else(|_| "unknown".to_string());
82        let manifest = json!({
83            "source_repo": source_repo,
84            "branch_or_week": dir_name,
85            "started_at": chrono::Utc::now().to_rfc3339(),
86        });
87
88        let manifest_path = base.join("manifest.json");
89        AtomicFile::new(&manifest_path, OverwriteBehavior::AllowOverwrite)
90            .write(|f| f.write_all(serde_json::to_string_pretty(&manifest)?.as_bytes()))
91            .with_context(|| format!("Failed to write manifest at {}", manifest_path.display()))?;
92    } else {
93        // Ensure subdirs exist even if base exists
94        for sub in ["research", "plans", "artifacts"] {
95            let subdir = base.join(sub);
96            if !subdir.exists() {
97                fs::create_dir_all(&subdir)
98                    .with_context(|| format!("Failed to ensure {} directory", sub))?;
99            }
100        }
101        // Ensure manifest exists
102        let manifest_path = base.join("manifest.json");
103        if !manifest_path.exists() {
104            let code_root = find_repo_root(&std::env::current_dir()?)?;
105            let source_repo = get_remote_url(&code_root).unwrap_or_else(|_| "unknown".to_string());
106            let manifest = json!({
107                "source_repo": source_repo,
108                "branch_or_week": dir_name,
109                "started_at": chrono::Utc::now().to_rfc3339(),
110            });
111            AtomicFile::new(&manifest_path, OverwriteBehavior::AllowOverwrite)
112                .write(|f| f.write_all(serde_json::to_string_pretty(&manifest)?.as_bytes()))
113                .with_context(|| {
114                    format!("Failed to write manifest at {}", manifest_path.display())
115                })?;
116        }
117    }
118
119    Ok(ActiveWork {
120        dir_name: dir_name.clone(),
121        base: base.clone(),
122        research: base.join("research"),
123        plans: base.join("plans"),
124        artifacts: base.join("artifacts"),
125    })
126}