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