thoughts_tool/workspace/
mod.rs1use 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#[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
25fn 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
52fn 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
65pub 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 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 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 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 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}