Skip to main content

kimun_notes/cli/commands/
workspace.rs

1// tui/src/cli/commands/workspace.rs
2//
3// Workspace management CLI commands: init, list, use, rename, remove, reindex.
4
5use 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    /// Initialize a new workspace
17    Init {
18        /// Name for the workspace (defaults to "default" for first workspace)
19        #[arg(long)]
20        name: Option<String>,
21        /// Path to the workspace directory
22        path: PathBuf,
23    },
24    /// List all configured workspaces
25    List,
26    /// Switch to a different workspace
27    Use {
28        /// Name of the workspace to switch to
29        name: String,
30    },
31    /// Rename a workspace
32    Rename {
33        /// Current workspace name
34        old_name: String,
35        /// New workspace name
36        new_name: String,
37    },
38    /// Remove a workspace from the configuration
39    Remove {
40        /// Name of the workspace to remove
41        name: String,
42    },
43    /// Reindex a workspace
44    Reindex {
45        /// Workspace name (defaults to current workspace)
46        #[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    // Ensure workspace_config exists
66    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    // Determine workspace name
76    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    // Check for duplicates
91    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    // Validate/create the target path
102    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    // Initialize NoteVault database (creates kimun.sqlite)
115    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    // Add workspace to config and save
129    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    // Validate workspace path still exists
206    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    // Move entry to new key
250    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    // Update current_workspace reference if needed
257    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    // Prevent removing the current workspace
278    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; // IndexReport only contains timing info
365    println!("Reindex complete for workspace '{}'.", workspace_name);
366
367    Ok(())
368}