kimun_notes/cli/commands/
workspace.rs1use std::path::PathBuf;
6
7use clap::Subcommand;
8use color_eyre::eyre::{Result, eyre};
9use kimun_core::NoteVault;
10use kimun_core::error::VaultError;
11
12use crate::settings::{AppSettings, workspace_config::WorkspaceConfig};
13
14#[derive(Subcommand, Debug)]
15pub enum WorkspaceSubcommand {
16 Init {
18 #[arg(long)]
20 name: Option<String>,
21 path: PathBuf,
23 },
24 List,
26 Use {
28 name: String,
30 },
31 Rename {
33 old_name: String,
35 new_name: String,
37 },
38 Remove {
40 name: String,
42 },
43 Reindex {
45 #[arg(long)]
47 name: Option<String>,
48 },
49}
50
51pub async fn run(subcommand: WorkspaceSubcommand, settings: &mut AppSettings) -> Result<()> {
52 match subcommand {
53 WorkspaceSubcommand::Init { name, path } => run_init(settings, name, path).await,
54 WorkspaceSubcommand::List => run_list(settings),
55 WorkspaceSubcommand::Use { name } => run_use(settings, name),
56 WorkspaceSubcommand::Rename { old_name, new_name } => {
57 run_rename(settings, old_name, new_name)
58 }
59 WorkspaceSubcommand::Remove { name } => run_remove(settings, name),
60 WorkspaceSubcommand::Reindex { name } => run_reindex(settings, name).await,
61 }
62}
63
64async fn run_init(settings: &mut AppSettings, name: Option<String>, path: PathBuf) -> Result<()> {
65 if settings.workspace_config.is_none() {
67 settings.workspace_config = Some(WorkspaceConfig::new_empty());
68 }
69
70 let ws_config = settings
71 .workspace_config
72 .as_ref()
73 .expect("workspace_config must exist after init");
74
75 let workspace_name = match name {
77 Some(n) => n,
78 None => {
79 if ws_config.workspaces.is_empty() {
80 "default".to_string()
81 } else {
82 return Err(eyre!(
83 "A workspace name is required when other workspaces already exist. \
84 Use: kimun workspace init --name <name> <path>"
85 ));
86 }
87 }
88 };
89
90 if ws_config.workspaces.contains_key(&workspace_name) {
92 let existing_path = &ws_config.workspaces[&workspace_name].path;
93 return Err(eyre!(
94 "Workspace '{}' already exists at {}. \
95 Use a different name or remove the existing workspace first.",
96 workspace_name,
97 existing_path.display()
98 ));
99 }
100
101 let created = !path.exists();
103 let canonical_path = kimun_core::ensure_dir_exists(&path).map_err(|e| {
104 eyre!(
105 "Failed to create workspace directory {}: {}",
106 path.display(),
107 e
108 )
109 })?;
110 if created {
111 println!("Created directory: {}", path.display());
112 }
113
114 println!("Initializing workspace database...");
116 let vault = NoteVault::new(&canonical_path).await.map_err(|e| {
117 eyre!(
118 "Failed to create vault at {}: {}",
119 canonical_path.display(),
120 e
121 )
122 })?;
123 vault
124 .validate_and_init()
125 .await
126 .map_err(|e| eyre!("Failed to initialize vault database: {}", e))?;
127
128 let ws_config_mut = settings
130 .workspace_config
131 .as_mut()
132 .expect("workspace_config must exist after init");
133 ws_config_mut
134 .add_workspace(workspace_name.clone(), canonical_path.clone())
135 .map_err(|e| eyre!("{}", e))?;
136
137 settings.config_version = 2;
138 settings.save_to_disk()?;
139
140 println!(
141 "Workspace '{}' initialized at {}",
142 workspace_name,
143 canonical_path.display()
144 );
145
146 let ws_config = settings
147 .workspace_config
148 .as_ref()
149 .expect("workspace_config must exist after init");
150 if ws_config.global.current_workspace == workspace_name {
151 println!("Set as current workspace.");
152 }
153
154 Ok(())
155}
156
157fn run_list(settings: &AppSettings) -> Result<()> {
158 match &settings.workspace_config {
159 None => {
160 println!("No workspaces configured. Run 'kimun workspace init <path>' to create one.");
161 }
162 Some(ws_config) => {
163 if ws_config.workspaces.is_empty() {
164 println!(
165 "No workspaces configured. Run 'kimun workspace init <path>' to create one."
166 );
167 } else {
168 println!("Configured workspaces:");
169 let mut names: Vec<&String> = ws_config.workspaces.keys().collect();
170 names.sort();
171 for name in names {
172 let entry = &ws_config.workspaces[name];
173 let marker = if name == &ws_config.global.current_workspace {
174 "* "
175 } else {
176 " "
177 };
178 println!("{}{} ({})", marker, name, entry.path.display());
179 }
180 }
181 }
182 }
183 Ok(())
184}
185
186fn run_use(settings: &mut AppSettings, name: String) -> Result<()> {
187 let ws_config = settings
188 .workspace_config
189 .as_ref()
190 .ok_or_else(|| eyre!("No workspaces configured."))?;
191
192 let entry = ws_config.get_workspace(&name).ok_or_else(|| {
193 let available: Vec<&String> = ws_config.workspaces.keys().collect();
194 eyre!(
195 "Workspace '{}' not found. Available workspaces: {}",
196 name,
197 available
198 .iter()
199 .map(|s| s.as_str())
200 .collect::<Vec<_>>()
201 .join(", ")
202 )
203 })?;
204
205 if !entry.effective_path().exists() {
207 return Err(eyre!(
208 "Workspace '{}' path no longer exists: {}. \
209 Update the path or remove this workspace.",
210 name,
211 entry.effective_path().display()
212 ));
213 }
214
215 settings
216 .workspace_config
217 .as_mut()
218 .expect("workspace_config must exist")
219 .global
220 .current_workspace = name.clone();
221 settings.save_to_disk()?;
222
223 println!("Switched to workspace '{}'.", name);
224 Ok(())
225}
226
227fn run_rename(settings: &mut AppSettings, old_name: String, new_name: String) -> Result<()> {
228 let ws_config = settings
229 .workspace_config
230 .as_ref()
231 .ok_or_else(|| eyre!("No workspaces configured."))?;
232
233 if !ws_config.workspaces.contains_key(&old_name) {
234 return Err(eyre!("Workspace '{}' not found.", old_name));
235 }
236
237 if ws_config.workspaces.contains_key(&new_name) {
238 return Err(eyre!(
239 "Workspace '{}' already exists. Choose a different name.",
240 new_name
241 ));
242 }
243
244 let ws_config_mut = settings
245 .workspace_config
246 .as_mut()
247 .expect("workspace_config must exist after init");
248
249 let entry = ws_config_mut
251 .workspaces
252 .remove(&old_name)
253 .expect("entry must exist (checked above)");
254 ws_config_mut.workspaces.insert(new_name.clone(), entry);
255
256 if ws_config_mut.global.current_workspace == old_name {
258 ws_config_mut.global.current_workspace = new_name.clone();
259 }
260
261 settings.save_to_disk()?;
262
263 println!("Workspace '{}' renamed to '{}'.", old_name, new_name);
264 Ok(())
265}
266
267fn run_remove(settings: &mut AppSettings, name: String) -> Result<()> {
268 let ws_config = settings
269 .workspace_config
270 .as_ref()
271 .ok_or_else(|| eyre!("No workspaces configured."))?;
272
273 if !ws_config.workspaces.contains_key(&name) {
274 return Err(eyre!("Workspace '{}' not found.", name));
275 }
276
277 if ws_config.global.current_workspace == name {
279 return Err(eyre!(
280 "Cannot remove the current workspace '{}'. \
281 Switch to a different workspace first with: kimun workspace use <name>",
282 name
283 ));
284 }
285
286 settings
287 .workspace_config
288 .as_mut()
289 .expect("workspace_config must exist")
290 .workspaces
291 .remove(&name);
292
293 settings.save_to_disk()?;
294
295 println!("Workspace '{}' removed.", name);
296 Ok(())
297}
298
299async fn run_reindex(settings: &AppSettings, name: Option<String>) -> Result<()> {
300 let ws_config = settings
301 .workspace_config
302 .as_ref()
303 .ok_or_else(|| eyre!("No workspaces configured."))?;
304
305 let workspace_name = match name {
306 Some(n) => n,
307 None => ws_config.global.current_workspace.clone(),
308 };
309
310 if workspace_name.is_empty() {
311 return Err(eyre!("No current workspace set. Specify a workspace name."));
312 }
313
314 let entry = ws_config
315 .get_workspace(&workspace_name)
316 .ok_or_else(|| eyre!("Workspace '{}' not found.", workspace_name))?;
317
318 if !entry.effective_path().exists() {
319 return Err(eyre!(
320 "Workspace '{}' path no longer exists: {}",
321 workspace_name,
322 entry.effective_path().display()
323 ));
324 }
325
326 println!("Reindexing workspace '{}'...", workspace_name);
327
328 let vault = NoteVault::new(entry.effective_path()).await.map_err(|e| {
329 eyre!(
330 "Failed to open vault at {}: {}",
331 entry.effective_path().display(),
332 e
333 )
334 })?;
335
336 let report = match vault.recreate_index().await {
337 Ok(r) => r,
338 Err(VaultError::CaseConflict { conflicts }) => {
339 eprintln!(
340 "Error: vault '{}' has case-sensitivity conflicts:",
341 workspace_name
342 );
343 for c in &conflicts {
344 eprintln!(" {}", c);
345 }
346 eprintln!(
347 "\nResolve the conflicts on disk, then run `kimun workspace use {}` to re-select the vault.",
348 workspace_name
349 );
350 return Err(eyre!(
351 "Vault '{}' has case-sensitivity conflicts",
352 workspace_name
353 ));
354 }
355 Err(e) => {
356 return Err(eyre!(
357 "Failed to reindex workspace '{}': {}",
358 workspace_name,
359 e
360 ));
361 }
362 };
363
364 let _ = report; println!("Reindex complete for workspace '{}'.", workspace_name);
366
367 Ok(())
368}