kimun_notes/cli/commands/
workspace.rs1use 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 Init {
21 #[arg(long)]
23 name: Option<String>,
24 path: PathBuf,
26 },
27 List,
29 Use {
31 name: String,
33 },
34 Rename {
36 old_name: String,
38 new_name: String,
40 },
41 Remove {
43 name: String,
45 },
46 Reindex {
48 #[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 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 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 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 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 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 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 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 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 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 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; println!(
345 "Reindex complete for workspace '{}'.",
346 workspace_name
347 );
348
349 Ok(())
350}