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::{eyre, Result};
9use kimun_core::NoteVault;
10
11use crate::settings::{
12    workspace_config::WorkspaceConfig,
13    AppSettings,
14};
15
16#[derive(Subcommand, Debug)]
17pub enum WorkspaceSubcommand {
18    /// Initialize a new workspace
19    Init {
20        /// Name for the workspace (defaults to "default" for first workspace)
21        #[arg(long)]
22        name: Option<String>,
23        /// Path to the workspace directory
24        path: PathBuf,
25    },
26    /// List all configured workspaces
27    List,
28    /// Switch to a different workspace
29    Use {
30        /// Name of the workspace to switch to
31        name: String,
32    },
33    /// Rename a workspace
34    Rename {
35        /// Current workspace name
36        old_name: String,
37        /// New workspace name
38        new_name: String,
39    },
40    /// Remove a workspace from the configuration
41    Remove {
42        /// Name of the workspace to remove
43        name: String,
44    },
45    /// Reindex a workspace
46    Reindex {
47        /// Workspace name (defaults to current workspace)
48        #[arg(long)]
49        name: Option<String>,
50    },
51}
52
53pub async fn run(
54    subcommand: WorkspaceSubcommand,
55    settings: &mut AppSettings,
56) -> Result<()> {
57    match subcommand {
58        WorkspaceSubcommand::Init { name, path } => run_init(settings, name, path).await,
59        WorkspaceSubcommand::List => run_list(settings),
60        WorkspaceSubcommand::Use { name } => run_use(settings, name),
61        WorkspaceSubcommand::Rename { old_name, new_name } => {
62            run_rename(settings, old_name, new_name)
63        }
64        WorkspaceSubcommand::Remove { name } => run_remove(settings, name),
65        WorkspaceSubcommand::Reindex { name } => run_reindex(settings, name).await,
66    }
67}
68
69async fn run_init(
70    settings: &mut AppSettings,
71    name: Option<String>,
72    path: PathBuf,
73) -> Result<()> {
74    // Ensure workspace_config exists
75    if settings.workspace_config.is_none() {
76        settings.workspace_config = Some(WorkspaceConfig::new_empty());
77    }
78
79    let ws_config = settings.workspace_config.as_ref().unwrap();
80
81    // Determine workspace name
82    let workspace_name = match name {
83        Some(n) => n,
84        None => {
85            if ws_config.workspaces.is_empty() {
86                "default".to_string()
87            } else {
88                return Err(eyre!(
89                    "A workspace name is required when other workspaces already exist. \
90                     Use: kimun workspace init --name <name> <path>"
91                ));
92            }
93        }
94    };
95
96    // Check for duplicates
97    if ws_config.workspaces.contains_key(&workspace_name) {
98        let existing_path = &ws_config.workspaces[&workspace_name].path;
99        return Err(eyre!(
100            "Workspace '{}' already exists at {}. \
101             Use a different name or remove the existing workspace first.",
102            workspace_name,
103            existing_path.display()
104        ));
105    }
106
107    // Validate/create the target path
108    if !path.exists() {
109        std::fs::create_dir_all(&path).map_err(|e| {
110            eyre!(
111                "Failed to create workspace directory {}: {}",
112                path.display(),
113                e
114            )
115        })?;
116        println!("Created directory: {}", path.display());
117    }
118
119    let canonical_path = path.canonicalize().map_err(|e| {
120        eyre!(
121            "Failed to resolve workspace path {}: {}",
122            path.display(),
123            e
124        )
125    })?;
126
127    // Initialize NoteVault database (creates kimun.sqlite)
128    println!("Initializing workspace database...");
129    let vault = NoteVault::new(&canonical_path).await.map_err(|e| {
130        eyre!("Failed to create vault at {}: {}", canonical_path.display(), e)
131    })?;
132    vault.validate_and_init().await.map_err(|e| {
133        eyre!("Failed to initialize vault database: {}", e)
134    })?;
135
136    // Add workspace to config and save
137    let ws_config_mut = settings.workspace_config.as_mut().unwrap();
138    ws_config_mut
139        .add_workspace(workspace_name.clone(), canonical_path.clone())
140        .map_err(|e| eyre!("{}", e))?;
141
142    settings.config_version = 2;
143    settings.save_to_disk()?;
144
145    println!(
146        "Workspace '{}' initialized at {}",
147        workspace_name,
148        canonical_path.display()
149    );
150
151    let ws_config = settings.workspace_config.as_ref().unwrap();
152    if ws_config.global.current_workspace == workspace_name {
153        println!("Set as current workspace.");
154    }
155
156    Ok(())
157}
158
159fn run_list(settings: &AppSettings) -> Result<()> {
160    match &settings.workspace_config {
161        None => {
162            println!("No workspaces configured. Run 'kimun workspace init <path>' to create one.");
163        }
164        Some(ws_config) => {
165            if ws_config.workspaces.is_empty() {
166                println!("No workspaces configured. Run 'kimun workspace init <path>' to create one.");
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
193        .get_workspace(&name)
194        .ok_or_else(|| {
195            let available: Vec<&String> = ws_config.workspaces.keys().collect();
196            eyre!(
197                "Workspace '{}' not found. Available workspaces: {}",
198                name,
199                available
200                    .iter()
201                    .map(|s| s.as_str())
202                    .collect::<Vec<_>>()
203                    .join(", ")
204            )
205        })?;
206
207    // Validate workspace path still exists
208    if !entry.path.exists() {
209        return Err(eyre!(
210            "Workspace '{}' path no longer exists: {}. \
211             Update the path or remove this workspace.",
212            name,
213            entry.path.display()
214        ));
215    }
216
217    settings.workspace_config.as_mut().unwrap().global.current_workspace = name.clone();
218    settings.save_to_disk()?;
219
220    println!("Switched to workspace '{}'.", name);
221    Ok(())
222}
223
224fn run_rename(
225    settings: &mut AppSettings,
226    old_name: String,
227    new_name: String,
228) -> Result<()> {
229    let ws_config = settings
230        .workspace_config
231        .as_ref()
232        .ok_or_else(|| eyre!("No workspaces configured."))?;
233
234    if !ws_config.workspaces.contains_key(&old_name) {
235        return Err(eyre!(
236            "Workspace '{}' not found.",
237            old_name
238        ));
239    }
240
241    if ws_config.workspaces.contains_key(&new_name) {
242        return Err(eyre!(
243            "Workspace '{}' already exists. Choose a different name.",
244            new_name
245        ));
246    }
247
248    let ws_config_mut = settings.workspace_config.as_mut().unwrap();
249
250    // Move entry to new key
251    let entry = ws_config_mut
252        .workspaces
253        .remove(&old_name)
254        .expect("entry must exist (checked above)");
255    ws_config_mut.workspaces.insert(new_name.clone(), entry);
256
257    // Update current_workspace reference if needed
258    if ws_config_mut.global.current_workspace == old_name {
259        ws_config_mut.global.current_workspace = new_name.clone();
260    }
261
262    settings.save_to_disk()?;
263
264    println!("Workspace '{}' renamed to '{}'.", old_name, new_name);
265    Ok(())
266}
267
268fn run_remove(settings: &mut AppSettings, name: String) -> Result<()> {
269    let ws_config = settings
270        .workspace_config
271        .as_ref()
272        .ok_or_else(|| eyre!("No workspaces configured."))?;
273
274    if !ws_config.workspaces.contains_key(&name) {
275        return Err(eyre!("Workspace '{}' not found.", name));
276    }
277
278    // Prevent removing the current workspace
279    if ws_config.global.current_workspace == name {
280        return Err(eyre!(
281            "Cannot remove the current workspace '{}'. \
282             Switch to a different workspace first with: kimun workspace use <name>",
283            name
284        ));
285    }
286
287    settings
288        .workspace_config
289        .as_mut()
290        .unwrap()
291        .workspaces
292        .remove(&name);
293
294    settings.save_to_disk()?;
295
296    println!("Workspace '{}' removed.", name);
297    Ok(())
298}
299
300async fn run_reindex(settings: &AppSettings, name: Option<String>) -> Result<()> {
301    let ws_config = settings
302        .workspace_config
303        .as_ref()
304        .ok_or_else(|| eyre!("No workspaces configured."))?;
305
306    let workspace_name = match name {
307        Some(n) => n,
308        None => ws_config.global.current_workspace.clone(),
309    };
310
311    if workspace_name.is_empty() {
312        return Err(eyre!("No current workspace set. Specify a workspace name."));
313    }
314
315    let entry = ws_config
316        .get_workspace(&workspace_name)
317        .ok_or_else(|| eyre!("Workspace '{}' not found.", workspace_name))?;
318
319    if !entry.path.exists() {
320        return Err(eyre!(
321            "Workspace '{}' path no longer exists: {}",
322            workspace_name,
323            entry.path.display()
324        ));
325    }
326
327    println!("Reindexing workspace '{}'...", workspace_name);
328
329    let vault = NoteVault::new(&entry.path).await.map_err(|e| {
330        eyre!("Failed to open vault at {}: {}", entry.path.display(), e)
331    })?;
332
333    let report = vault.recreate_index().await.map_err(|e| {
334        eyre!("Failed to reindex workspace '{}': {}", workspace_name, e)
335    })?;
336
337    let _ = report; // IndexReport only contains timing info
338    println!(
339        "Reindex complete for workspace '{}'.",
340        workspace_name
341    );
342
343    Ok(())
344}