fix(cli): accept workspace name or UUID, auto-select on first add

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`.
This commit is contained in:
Claude 2026-04-17 16:15:45 +00:00
parent 855fa46a0e
commit 433a950418
No known key found for this signature in database
3 changed files with 44 additions and 31 deletions

View file

@ -6,6 +6,7 @@ pub mod group;
pub mod sync; pub mod sync;
use onyx_core::{AppConfig, TaskRepository}; use onyx_core::{AppConfig, TaskRepository};
use onyx_core::config::WorkspaceConfig;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use std::path::PathBuf; use std::path::PathBuf;
@ -23,18 +24,29 @@ pub fn save_config(config: &AppConfig) -> Result<()> {
config.save_to_file(&path).context("Failed to save config") config.save_to_file(&path).context("Failed to save config")
} }
pub fn get_repository(workspace_name: Option<String>) -> Result<(TaskRepository, String)> { /// Resolve a user-supplied identifier to (id, WorkspaceConfig). Accepts either
let config = load_config()?; /// the workspace's display name or its UUID. Falls back to the current
/// workspace when `identifier` is `None`.
let (name, workspace_config) = if let Some(name) = workspace_name { pub fn resolve_workspace(config: &AppConfig, identifier: Option<&str>) -> Result<(String, WorkspaceConfig)> {
let workspace_config = config.get_workspace(&name) if let Some(s) = identifier {
.ok_or_else(|| anyhow::anyhow!("Workspace '{}' not found", name))?; // Try by UUID first (exact match on map key), then fall back to name lookup.
(name, workspace_config.clone()) if let Some(ws) = config.get_workspace(s) {
return Ok((s.to_string(), ws.clone()));
}
let (id, ws) = config.find_by_name(s)
.ok_or_else(|| anyhow::anyhow!("Workspace '{}' not found", s))?;
Ok((id.clone(), ws.clone()))
} else { } else {
let (name, workspace_config) = config.get_current_workspace() let (id, ws) = config.get_current_workspace()
.context("No workspace set. Use 'onyx init' to create one.")?; .context("No workspace set. Run 'onyx workspace add <name> <path>' to create one, or 'onyx workspace switch <name>' to select one.")?;
(name.clone(), workspace_config.clone()) Ok((id.clone(), ws.clone()))
}; }
}
pub fn get_repository(workspace_identifier: Option<String>) -> Result<(TaskRepository, String)> {
let config = load_config()?;
let (_id, workspace_config) = resolve_workspace(&config, workspace_identifier.as_deref())?;
let name = workspace_config.name.clone();
let repo = TaskRepository::new(workspace_config.path.clone()) let repo = TaskRepository::new(workspace_config.path.clone())
.context(format!("Failed to open workspace '{}'", name))?; .context(format!("Failed to open workspace '{}'", name))?;

View file

@ -2,22 +2,8 @@ use anyhow::{Context, Result};
use colored::Colorize; use colored::Colorize;
use onyx_core::sync::{SyncMode, sync_workspace, get_sync_status}; use onyx_core::sync::{SyncMode, sync_workspace, get_sync_status};
use onyx_core::webdav::{WebDavClient, store_credentials, load_credentials}; use onyx_core::webdav::{WebDavClient, store_credentials, load_credentials};
use onyx_core::config::AppConfig;
use crate::output; use crate::output;
use super::{load_config, save_config}; use super::{load_config, save_config, resolve_workspace};
/// Resolve a workspace name to (id, config). Falls back to current workspace if name is None.
fn resolve_workspace(config: &AppConfig, name: Option<&str>) -> Result<(String, onyx_core::config::WorkspaceConfig)> {
if let Some(name) = name {
let (id, ws) = config.find_by_name(name)
.ok_or_else(|| anyhow::anyhow!("Workspace '{}' not found", name))?;
Ok((id.clone(), ws.clone()))
} else {
let (id, ws) = config.get_current_workspace()
.context("No workspace set. Use 'onyx init' to create one.")?;
Ok((id.clone(), ws.clone()))
}
}
/// Run sync setup: prompt for URL, username, password, test connection, store credentials. /// Run sync setup: prompt for URL, username, password, test connection, store credentials.
pub fn setup(workspace_name: Option<String>) -> Result<()> { pub fn setup(workspace_name: Option<String>) -> Result<()> {

View file

@ -30,11 +30,21 @@ pub fn add(name: String, path: String) -> Result<()> {
// Add workspace // Add workspace
let id = config.add_workspace(WorkspaceConfig::new(name.clone(), path_buf.clone())); 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
save_config(&config)?; save_config(&config)?;
output::success(&format!("Added workspace \"{}\" ({}) at {}", name, &id[..8], path_buf.display())); output::success(&format!("Added workspace \"{}\" ({}) at {}", name, &id[..8], path_buf.display()));
output::success("Created default list \"My Tasks\""); output::success("Created default list \"My Tasks\"");
if made_current {
output::success(&format!("Set \"{}\" as the current workspace", name));
}
Ok(()) Ok(())
} }
@ -64,15 +74,20 @@ pub fn list() -> Result<()> {
Ok(()) Ok(())
} }
/// Resolve a workspace name to its ID. Errors if not found or ambiguous. /// Resolve a user-supplied identifier to a workspace ID. Accepts either the
fn resolve_name(config: &onyx_core::config::AppConfig, name: &str) -> Result<String> { /// 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() let matches: Vec<_> = config.workspaces.iter()
.filter(|(_, ws)| ws.name == name) .filter(|(_, ws)| ws.name == identifier)
.collect(); .collect();
match matches.len() { match matches.len() {
0 => anyhow::bail!("Workspace '{}' not found", name), 0 => anyhow::bail!("Workspace '{}' not found", identifier),
1 => Ok(matches[0].0.clone()), 1 => Ok(matches[0].0.clone()),
n => anyhow::bail!("Ambiguous: {} workspaces named '{}'. Use the workspace ID instead.", n, name), n => anyhow::bail!("Ambiguous: {} workspaces named '{}'. Use the workspace ID instead.", n, identifier),
} }
} }