kimun_notes/cli/commands/
workspace.rs1use 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 Init {
20 #[arg(long)]
22 name: Option<String>,
23 path: PathBuf,
25 },
26 List,
28 Use {
30 name: String,
32 },
33 Rename {
35 old_name: String,
37 new_name: String,
39 },
40 Remove {
42 name: String,
44 },
45 Reindex {
47 #[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 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 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 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 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 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 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 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 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 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 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; println!(
339 "Reindex complete for workspace '{}'.",
340 workspace_name
341 );
342
343 Ok(())
344}