ito_core/
planning_init.rs1use crate::errors::{CoreError, CoreResult};
7use ito_domain::planning::{planning_dir, research_dir};
8use std::io::ErrorKind;
9use std::path::{Path, PathBuf};
10
11#[derive(Debug, Eq, PartialEq)]
13pub struct PlanningWorkspaceStatus {
14 pub planning_dir: PathBuf,
16 pub planning_exists: bool,
18 pub planning_invalid: bool,
20 pub planning_documents: Vec<PathBuf>,
22 pub research_dir: PathBuf,
24 pub research_exists: bool,
26 pub research_invalid: bool,
28}
29
30#[derive(Debug, Eq, PartialEq)]
32pub struct PlanningWorkspaceSummary {
33 pub planning_status: &'static str,
35 pub planning_dir: PathBuf,
37 pub research_status: &'static str,
39 pub research_dir: PathBuf,
41 pub planning_notice: Option<String>,
43 pub research_notice: Option<String>,
45 pub documents_notice: Option<String>,
47 pub document_names: Vec<String>,
49}
50
51struct WorkspacePathState {
52 exists: bool,
53 invalid: bool,
54}
55
56pub fn init_planning_structure(ito_path: &Path) -> CoreResult<()> {
67 let planning = planning_dir(ito_path);
68 std::fs::create_dir_all(&planning)
69 .map_err(|e| workspace_io_error("creating planning workspace", &planning, e))?;
70 let research = research_dir(ito_path);
71 match std::fs::metadata(&research) {
72 Ok(metadata) if metadata.is_dir() => {}
73 Ok(_) => {}
74 Err(err) if err.kind() == ErrorKind::NotFound => std::fs::create_dir_all(&research)
75 .map_err(|e| workspace_io_error("creating research workspace", &research, e))?,
76 Err(err) => {
77 return Err(workspace_io_error(
78 "inspecting research workspace",
79 &research,
80 err,
81 ));
82 }
83 }
84 Ok(())
85}
86
87pub fn summarize_planning_workspace(status: &PlanningWorkspaceStatus) -> PlanningWorkspaceSummary {
93 let document_names = status
94 .planning_documents
95 .iter()
96 .map(|document| {
97 document
98 .file_name()
99 .unwrap_or_else(|| document.as_os_str())
100 .to_string_lossy()
101 .into_owned()
102 })
103 .collect();
104
105 PlanningWorkspaceSummary {
106 planning_status: workspace_label(status.planning_exists, status.planning_invalid),
107 planning_dir: status.planning_dir.clone(),
108 research_status: workspace_label(status.research_exists, status.research_invalid),
109 research_dir: status.research_dir.clone(),
110 planning_notice: status.planning_invalid.then(|| {
111 "Planning path is not a directory. Rename or remove it, then run `ito plan init`."
112 .to_string()
113 }),
114 research_notice: status.research_invalid.then(|| {
115 "Research path is not a directory. Rename or remove it before storing deep-dive research."
116 .to_string()
117 }),
118 documents_notice: if status.planning_invalid {
119 None
120 } else if !status.planning_exists {
121 Some("No planning workspace found. Run `ito plan init` to create one.".to_string())
122 } else if status.planning_documents.is_empty() {
123 Some("No planning documents yet. Use ito-proposal to create the first plan.".to_string())
124 } else {
125 None
126 },
127 document_names,
128 }
129}
130
131pub fn read_planning_workspace_status(ito_path: &Path) -> CoreResult<PlanningWorkspaceStatus> {
139 let planning = planning_dir(ito_path);
140 let planning_state = inspect_workspace_path(&planning, "planning")?;
141 let mut planning_documents = Vec::new();
142
143 if planning_state.exists {
144 let entries = std::fs::read_dir(&planning)
145 .map_err(|e| workspace_io_error("reading planning workspace", &planning, e))?;
146 for entry in entries {
147 let entry = entry.map_err(|e| {
148 workspace_io_error("reading planning workspace entry", &planning, e)
149 })?;
150 let path = entry.path();
151 let file_type = entry.file_type().map_err(|e| {
152 workspace_io_error("reading planning workspace entry metadata", &path, e)
153 })?;
154 let mut is_markdown = false;
155 if let Some(ext) = path.extension().and_then(|ext| ext.to_str()) {
156 is_markdown =
157 ext.eq_ignore_ascii_case("md") || ext.eq_ignore_ascii_case("markdown");
158 }
159 if is_markdown && file_type.is_file() {
160 planning_documents.push(path);
161 }
162 }
163 planning_documents.sort_by(|a, b| a.file_name().cmp(&b.file_name()));
164 }
165
166 let research = research_dir(ito_path);
167 let research_state = inspect_workspace_path(&research, "research")?;
168
169 Ok(PlanningWorkspaceStatus {
170 planning_dir: planning,
171 planning_exists: planning_state.exists,
172 planning_invalid: planning_state.invalid,
173 planning_documents,
174 research_dir: research,
175 research_exists: research_state.exists,
176 research_invalid: research_state.invalid,
177 })
178}
179
180fn inspect_workspace_path(path: &Path, label: &str) -> CoreResult<WorkspacePathState> {
181 match std::fs::metadata(path) {
182 Ok(metadata) => {
183 let is_dir = metadata.is_dir();
184 Ok(WorkspacePathState {
185 exists: is_dir,
186 invalid: !is_dir,
187 })
188 }
189 Err(err) if err.kind() == ErrorKind::NotFound => Ok(WorkspacePathState {
190 exists: false,
191 invalid: false,
192 }),
193 Err(err) => Err(workspace_io_error(
194 format!("inspecting {label} workspace"),
195 path,
196 err,
197 )),
198 }
199}
200
201fn workspace_label(exists: bool, invalid: bool) -> &'static str {
202 if exists {
203 "available"
204 } else if invalid {
205 "invalid"
206 } else {
207 "missing"
208 }
209}
210
211fn workspace_io_error(action: impl AsRef<str>, path: &Path, err: std::io::Error) -> CoreError {
212 CoreError::io(
213 format!(
214 "{} at {} (check permissions and parent directories)",
215 action.as_ref(),
216 path.display()
217 ),
218 err,
219 )
220}