use std::path::PathBuf;
use clap::Subcommand;
use color_eyre::eyre::{Result, eyre};
use kimun_core::error::VaultError;
use kimun_core::{NoteVault, VaultConfig};
use crate::settings::{
AppSettings, config_migration::CURRENT_CONFIG_VERSION, workspace_config::WorkspaceConfig,
};
#[derive(Subcommand, Debug)]
pub enum WorkspaceSubcommand {
Init {
#[arg(long)]
name: Option<String>,
path: PathBuf,
},
List,
Use {
name: String,
},
Rename {
old_name: String,
new_name: String,
},
Remove {
name: String,
},
Reindex {
#[arg(long)]
name: Option<String>,
},
}
pub async fn run(subcommand: WorkspaceSubcommand, settings: &mut AppSettings) -> Result<()> {
match subcommand {
WorkspaceSubcommand::Init { name, path } => run_init(settings, name, path).await,
WorkspaceSubcommand::List => run_list(settings),
WorkspaceSubcommand::Use { name } => run_use(settings, name),
WorkspaceSubcommand::Rename { old_name, new_name } => {
run_rename(settings, old_name, new_name)
}
WorkspaceSubcommand::Remove { name } => run_remove(settings, name),
WorkspaceSubcommand::Reindex { name } => run_reindex(settings, name).await,
}
}
async fn run_init(settings: &mut AppSettings, name: Option<String>, path: PathBuf) -> Result<()> {
if settings.workspace_config.is_none() {
settings.workspace_config = Some(WorkspaceConfig::new_empty());
}
let ws_config = settings
.workspace_config
.as_ref()
.expect("workspace_config must exist after init");
let workspace_name = match name {
Some(n) => n.to_lowercase(),
None => {
if ws_config.workspaces.is_empty() {
"default".to_string()
} else {
return Err(eyre!(
"A workspace name is required when other workspaces already exist. \
Use: kimun workspace init --name <name> <path>"
));
}
}
};
if ws_config.workspaces.contains_key(&workspace_name) {
let existing_path = &ws_config.workspaces[&workspace_name].path;
return Err(eyre!(
"Workspace '{}' already exists at {}. \
Use a different name or remove the existing workspace first.",
workspace_name,
existing_path.display()
));
}
let created = !path.exists();
let canonical_path = kimun_core::ensure_dir_exists(&path).map_err(|e| {
eyre!(
"Failed to create workspace directory {}: {}",
path.display(),
e
)
})?;
if created {
println!("Created directory: {}", path.display());
}
println!("Initializing workspace database...");
let cache_path = settings.cache_path_for(&workspace_name);
let vault = NoteVault::new(VaultConfig::new(&canonical_path).with_db_path(cache_path))
.await
.map_err(|e| {
eyre!(
"Failed to create vault at {}: {}",
canonical_path.display(),
e
)
})?;
vault
.validate_and_init()
.await
.map_err(|e| eyre!("Failed to initialize vault database: {}", e))?;
let ws_config_mut = settings
.workspace_config
.as_mut()
.expect("workspace_config must exist after init");
ws_config_mut
.add_workspace(workspace_name.clone(), canonical_path.clone())
.map_err(|e| eyre!("{}", e))?;
settings.config_version = CURRENT_CONFIG_VERSION;
settings.save_to_disk()?;
println!(
"Workspace '{}' initialized at {}",
workspace_name,
canonical_path.display()
);
let ws_config = settings
.workspace_config
.as_ref()
.expect("workspace_config must exist after init");
if ws_config.global.current_workspace == workspace_name {
println!("Set as current workspace.");
}
Ok(())
}
fn run_list(settings: &AppSettings) -> Result<()> {
match &settings.workspace_config {
None => {
println!("No workspaces configured. Run 'kimun workspace init <path>' to create one.");
}
Some(ws_config) => {
if ws_config.workspaces.is_empty() {
println!(
"No workspaces configured. Run 'kimun workspace init <path>' to create one."
);
} else {
println!("Configured workspaces:");
let mut names: Vec<&String> = ws_config.workspaces.keys().collect();
names.sort();
for name in names {
let entry = &ws_config.workspaces[name];
let marker = if name == &ws_config.global.current_workspace {
"* "
} else {
" "
};
println!("{}{} ({})", marker, name, entry.path.display());
}
}
}
}
Ok(())
}
fn run_use(settings: &mut AppSettings, name: String) -> Result<()> {
let ws_config = settings
.workspace_config
.as_ref()
.ok_or_else(|| eyre!("No workspaces configured."))?;
let entry = ws_config.get_workspace(&name).ok_or_else(|| {
let available: Vec<&String> = ws_config.workspaces.keys().collect();
eyre!(
"Workspace '{}' not found. Available workspaces: {}",
name,
available
.iter()
.map(|s| s.as_str())
.collect::<Vec<_>>()
.join(", ")
)
})?;
if !entry.effective_path().exists() {
return Err(eyre!(
"Workspace '{}' path no longer exists: {}. \
Update the path or remove this workspace.",
name,
entry.effective_path().display()
));
}
settings
.workspace_config
.as_mut()
.expect("workspace_config must exist")
.global
.current_workspace = name.clone();
settings.save_to_disk()?;
println!("Switched to workspace '{}'.", name);
Ok(())
}
fn run_rename(settings: &mut AppSettings, old_name: String, new_name: String) -> Result<()> {
let new_name = new_name.to_lowercase();
kimun_core::nfs::filename::validate_filename(&new_name).map_err(|e| eyre!("{}", e))?;
let ws_config = settings
.workspace_config
.as_ref()
.ok_or_else(|| eyre!("No workspaces configured."))?;
if !ws_config.workspaces.contains_key(&old_name) {
return Err(eyre!("Workspace '{}' not found.", old_name));
}
if ws_config.workspaces.contains_key(&new_name) {
return Err(eyre!(
"Workspace '{}' already exists. Choose a different name.",
new_name
));
}
let old_cache = settings.cache_path_for(&old_name);
let new_cache = settings.cache_path_for(&new_name);
let old_history = settings.history_path_for(&old_name);
let new_history = settings.history_path_for(&new_name);
if new_cache.exists() {
return Err(eyre!(
"Destination cache already exists at {}. Refusing to overwrite.",
new_cache.display()
));
}
if new_history.exists() {
return Err(eyre!(
"Destination history already exists at {}. Refusing to overwrite.",
new_history.display()
));
}
if old_cache.exists() {
std::fs::rename(&old_cache, &new_cache).map_err(|e| {
eyre!(
"failed to move cache {} -> {}: {}",
old_cache.display(),
new_cache.display(),
e
)
})?;
}
if old_history.exists() {
std::fs::rename(&old_history, &new_history).map_err(|e| {
eyre!(
"failed to move history {} -> {}: {}",
old_history.display(),
new_history.display(),
e
)
})?;
}
let ws_config_mut = settings
.workspace_config
.as_mut()
.expect("workspace_config must exist after init");
let entry = ws_config_mut
.workspaces
.remove(&old_name)
.expect("entry must exist (checked above)");
ws_config_mut.workspaces.insert(new_name.clone(), entry);
if ws_config_mut.global.current_workspace == old_name {
ws_config_mut.global.current_workspace = new_name.clone();
}
settings.save_to_disk()?;
println!("Workspace '{}' renamed to '{}'.", old_name, new_name);
Ok(())
}
fn run_remove(settings: &mut AppSettings, name: String) -> Result<()> {
let ws_config = settings
.workspace_config
.as_ref()
.ok_or_else(|| eyre!("No workspaces configured."))?;
if !ws_config.workspaces.contains_key(&name) {
return Err(eyre!("Workspace '{}' not found.", name));
}
if ws_config.global.current_workspace == name {
return Err(eyre!(
"Cannot remove the current workspace '{}'. \
Switch to a different workspace first with: kimun workspace use <name>",
name
));
}
let cache_path = settings.cache_path_for(&name);
let history_path = settings.history_path_for(&name);
settings
.workspace_config
.as_mut()
.expect("workspace_config must exist")
.workspaces
.remove(&name);
settings.save_to_disk()?;
for path in [&cache_path, &history_path] {
if path.exists() {
match std::fs::remove_file(path) {
Ok(()) => tracing::info!("removed {}", path.display()),
Err(e) => tracing::warn!("failed to remove {}: {}", path.display(), e),
}
}
}
println!("Workspace '{}' removed.", name);
Ok(())
}
async fn run_reindex(settings: &AppSettings, name: Option<String>) -> Result<()> {
let ws_config = settings
.workspace_config
.as_ref()
.ok_or_else(|| eyre!("No workspaces configured."))?;
let workspace_name = match name {
Some(n) => n,
None => ws_config.global.current_workspace.clone(),
};
if workspace_name.is_empty() {
return Err(eyre!("No current workspace set. Specify a workspace name."));
}
let entry = ws_config
.get_workspace(&workspace_name)
.ok_or_else(|| eyre!("Workspace '{}' not found.", workspace_name))?;
if !entry.effective_path().exists() {
return Err(eyre!(
"Workspace '{}' path no longer exists: {}",
workspace_name,
entry.effective_path().display()
));
}
println!("Reindexing workspace '{}'...", workspace_name);
let cache_path = settings.cache_path_for(&workspace_name);
let workspace_path = entry.effective_path().clone();
let vault = NoteVault::new(VaultConfig::new(&workspace_path).with_db_path(cache_path))
.await
.map_err(|e| {
eyre!(
"Failed to open vault at {}: {}",
workspace_path.display(),
e
)
})?;
let report = match vault.recreate_index().await {
Ok(r) => r,
Err(VaultError::CaseConflict { conflicts }) => {
eprintln!(
"Error: vault '{}' has case-sensitivity conflicts:",
workspace_name
);
for c in &conflicts {
eprintln!(" {}", c);
}
eprintln!(
"\nResolve the conflicts on disk, then run `kimun workspace use {}` to re-select the vault.",
workspace_name
);
return Err(eyre!(
"Vault '{}' has case-sensitivity conflicts",
workspace_name
));
}
Err(e) => {
return Err(eyre!(
"Failed to reindex workspace '{}': {}",
workspace_name,
e
));
}
};
let _ = report; println!("Reindex complete for workspace '{}'.", workspace_name);
Ok(())
}