Three related CLI bugs found during smoke testing: 1. `get_repository` used `config.get_workspace(name)` which expects the UUID string, so `onyx list create -w dev` or `onyx task add -w dev` always failed with "Workspace 'dev' not found". Unified CLI resolution into a single `resolve_workspace()` helper that accepts either the display name or the UUID; removed sync.rs's duplicated local copy. 2. `workspace switch`/`remove`/`retarget`/`migrate` only accepted the display name — the error message even suggested "Use the workspace ID instead" on ambiguous names, but IDs were then rejected. Updated `resolve_name` to try the map key first. 3. `onyx workspace add` never set `current_workspace`, so the very next command failed with "No workspace set. Use 'onyx init'..." even though a workspace was just created. Now sets the new workspace as current whenever none was previously selected, and reports the fact. Updated the error message to point at the correct `workspace add` / `workspace switch` commands instead of `init`.
251 lines
7.9 KiB
Rust
251 lines
7.9 KiB
Rust
use anyhow::{Context, Result};
|
|
use onyx_core::{TaskRepository, WorkspaceConfig};
|
|
use std::path::PathBuf;
|
|
use colored::*;
|
|
use crate::output;
|
|
use crate::commands::{load_config, save_config};
|
|
|
|
pub fn add(name: String, path: String) -> Result<()> {
|
|
let path_buf = PathBuf::from(path);
|
|
let path_buf = if path_buf.is_relative() {
|
|
std::env::current_dir()?.join(path_buf)
|
|
} else {
|
|
path_buf
|
|
};
|
|
|
|
// Initialize the repository
|
|
let mut repo = TaskRepository::init(path_buf.clone())
|
|
.context("Failed to initialize tasks folder")?;
|
|
|
|
// Create default list if it doesn't exist
|
|
let lists = repo.get_lists().context("Failed to get lists")?;
|
|
if !lists.iter().any(|l| l.title == "My Tasks") {
|
|
repo.create_list("My Tasks".to_string())
|
|
.context("Failed to create default list")?;
|
|
}
|
|
|
|
// Load config
|
|
let mut config = load_config()?;
|
|
|
|
// Add workspace
|
|
let id = config.add_workspace(WorkspaceConfig::new(name.clone(), path_buf.clone()));
|
|
|
|
// Select the new workspace as current when none was previously set, so the
|
|
// very next command doesn't fail with "No workspace set".
|
|
let made_current = config.current_workspace.is_none();
|
|
if made_current {
|
|
config.set_current_workspace(id.clone())?;
|
|
}
|
|
|
|
// Save config
|
|
save_config(&config)?;
|
|
|
|
output::success(&format!("Added workspace \"{}\" ({}) at {}", name, &id[..8], path_buf.display()));
|
|
output::success("Created default list \"My Tasks\"");
|
|
if made_current {
|
|
output::success(&format!("Set \"{}\" as the current workspace", name));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn list() -> Result<()> {
|
|
let config = load_config()?;
|
|
|
|
if config.workspaces.is_empty() {
|
|
output::info("No workspaces configured. Use 'onyx init' to create one.");
|
|
return Ok(());
|
|
}
|
|
|
|
let current = config.current_workspace.as_deref();
|
|
|
|
let mut workspaces: Vec<_> = config.workspaces.iter().collect();
|
|
workspaces.sort_by(|a, b| a.1.name.cmp(&b.1.name));
|
|
|
|
for (id, workspace_config) in workspaces {
|
|
let marker = if Some(id.as_str()) == current {
|
|
" (current)".green()
|
|
} else {
|
|
"".normal()
|
|
};
|
|
output::item(&format!("{}: {}{}", workspace_config.name, workspace_config.path.display(), marker));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Resolve a user-supplied identifier to a workspace ID. Accepts either the
|
|
/// display name or the UUID. Errors if not found or ambiguous.
|
|
fn resolve_name(config: &onyx_core::config::AppConfig, identifier: &str) -> Result<String> {
|
|
// Direct UUID hit on the map key — unambiguous.
|
|
if config.workspaces.contains_key(identifier) {
|
|
return Ok(identifier.to_string());
|
|
}
|
|
let matches: Vec<_> = config.workspaces.iter()
|
|
.filter(|(_, ws)| ws.name == identifier)
|
|
.collect();
|
|
match matches.len() {
|
|
0 => anyhow::bail!("Workspace '{}' not found", identifier),
|
|
1 => Ok(matches[0].0.clone()),
|
|
n => anyhow::bail!("Ambiguous: {} workspaces named '{}'. Use the workspace ID instead.", n, identifier),
|
|
}
|
|
}
|
|
|
|
pub fn switch(name: String) -> Result<()> {
|
|
let mut config = load_config()?;
|
|
let id = resolve_name(&config, &name)?;
|
|
|
|
config.set_current_workspace(id)?;
|
|
save_config(&config)?;
|
|
|
|
output::success(&format!("Switched to workspace \"{}\"", name));
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn remove(name: String) -> Result<()> {
|
|
let mut config = load_config()?;
|
|
let id = resolve_name(&config, &name)?;
|
|
|
|
// Confirm
|
|
output::warning("This will delete workspace config (files remain on disk)");
|
|
print!("Continue? (y/n): ");
|
|
use std::io::{self, Write};
|
|
io::stdout().flush()?;
|
|
|
|
let mut input = String::new();
|
|
io::stdin().read_line(&mut input)?;
|
|
|
|
if input.trim().to_lowercase() != "y" {
|
|
output::info("Cancelled");
|
|
return Ok(());
|
|
}
|
|
|
|
config.remove_workspace(&id);
|
|
save_config(&config)?;
|
|
|
|
output::success(&format!("Removed workspace \"{}\"", name));
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn retarget(name: String, path: String) -> Result<()> {
|
|
let path_buf = PathBuf::from(path);
|
|
let path_buf = if path_buf.is_relative() {
|
|
std::env::current_dir()?.join(path_buf)
|
|
} else {
|
|
path_buf
|
|
};
|
|
|
|
let mut config = load_config()?;
|
|
let id = resolve_name(&config, &name)?;
|
|
|
|
// Update path
|
|
config.workspaces.get_mut(&id)
|
|
.ok_or_else(|| anyhow::anyhow!("Workspace '{}' disappeared from config", name))?
|
|
.path = path_buf.clone();
|
|
save_config(&config)?;
|
|
|
|
output::success(&format!("Workspace \"{}\" now points to {}", name, path_buf.display()));
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn migrate(name: String, new_path: String) -> Result<()> {
|
|
let new_path_buf = PathBuf::from(new_path);
|
|
let new_path_buf = if new_path_buf.is_relative() {
|
|
std::env::current_dir()?.join(new_path_buf)
|
|
} else {
|
|
new_path_buf
|
|
};
|
|
|
|
let mut config = load_config()?;
|
|
let id = resolve_name(&config, &name)?;
|
|
|
|
// Get current workspace config
|
|
let old_path = config.get_workspace(&id)
|
|
.ok_or_else(|| anyhow::anyhow!("Workspace '{}' not found", name))?
|
|
.path.clone();
|
|
|
|
// Confirm
|
|
output::warning(&format!("This will move all files from {} to {}", old_path.display(), new_path_buf.display()));
|
|
print!("Continue? (y/n): ");
|
|
use std::io::{self, Write};
|
|
io::stdout().flush()?;
|
|
|
|
let mut input = String::new();
|
|
io::stdin().read_line(&mut input)?;
|
|
|
|
if input.trim().to_lowercase() != "y" {
|
|
output::info("Cancelled");
|
|
return Ok(());
|
|
}
|
|
|
|
// Validate destination
|
|
if old_path == new_path_buf {
|
|
anyhow::bail!("Source and destination paths are the same");
|
|
}
|
|
if new_path_buf.exists() && new_path_buf.read_dir()?.next().is_some() {
|
|
anyhow::bail!("Destination directory '{}' already contains files", new_path_buf.display());
|
|
}
|
|
|
|
// Create destination directory
|
|
std::fs::create_dir_all(&new_path_buf)?;
|
|
|
|
// Move files, tracking what was moved for rollback
|
|
output::info("Moving files...");
|
|
let entries: Vec<_> = std::fs::read_dir(&old_path)?
|
|
.collect::<std::result::Result<Vec<_>, _>>()?;
|
|
let mut moved: Vec<(std::path::PathBuf, std::path::PathBuf)> = Vec::new();
|
|
|
|
let move_result: Result<()> = (|| {
|
|
for entry in &entries {
|
|
let file_name = entry.file_name();
|
|
let dest = new_path_buf.join(&file_name);
|
|
|
|
if entry.path().is_dir() {
|
|
let mut options = fs_extra::dir::CopyOptions::new();
|
|
options.copy_inside = true;
|
|
fs_extra::dir::move_dir(entry.path(), &new_path_buf, &options)?;
|
|
} else {
|
|
std::fs::rename(entry.path(), &dest)?;
|
|
}
|
|
moved.push((entry.path(), dest));
|
|
output::item(&format!("Moved {}", file_name.to_string_lossy()));
|
|
}
|
|
Ok(())
|
|
})();
|
|
|
|
if let Err(e) = move_result {
|
|
output::error(&format!("Migration failed: {}. Rolling back...", e));
|
|
for (src, dest) in moved.into_iter().rev() {
|
|
if dest.exists() {
|
|
if dest.is_dir() {
|
|
let mut options = fs_extra::dir::CopyOptions::new();
|
|
options.copy_inside = true;
|
|
let _ = fs_extra::dir::move_dir(&dest, &old_path, &options);
|
|
} else {
|
|
let _ = std::fs::rename(&dest, &src);
|
|
}
|
|
}
|
|
}
|
|
anyhow::bail!("Migration failed and was rolled back: {}", e);
|
|
}
|
|
|
|
// Remove old directory if empty
|
|
if old_path.exists() && old_path.read_dir()?.next().is_none() {
|
|
std::fs::remove_dir(&old_path)?;
|
|
}
|
|
|
|
// Update config
|
|
config.workspaces.get_mut(&id)
|
|
.ok_or_else(|| anyhow::anyhow!("Workspace '{}' disappeared from config", name))?
|
|
.path = new_path_buf.clone();
|
|
save_config(&config)?;
|
|
|
|
output::success(&format!("Migrated {} items to {}", moved.len(), new_path_buf.display()));
|
|
output::success(&format!("Workspace \"{}\" now points to {}", name, new_path_buf.display()));
|
|
|
|
Ok(())
|
|
}
|