From 433a9504183c2d41a6f652c712e436b3d5f3c7c2 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 17 Apr 2026 16:15:45 +0000 Subject: [PATCH] fix(cli): accept workspace name or UUID, auto-select on first add MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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`. --- crates/onyx-cli/src/commands/mod.rs | 34 +++++++++++++++-------- crates/onyx-cli/src/commands/sync.rs | 16 +---------- crates/onyx-cli/src/commands/workspace.rs | 25 +++++++++++++---- 3 files changed, 44 insertions(+), 31 deletions(-) diff --git a/crates/onyx-cli/src/commands/mod.rs b/crates/onyx-cli/src/commands/mod.rs index c681294..7731bc0 100644 --- a/crates/onyx-cli/src/commands/mod.rs +++ b/crates/onyx-cli/src/commands/mod.rs @@ -6,6 +6,7 @@ pub mod group; pub mod sync; use onyx_core::{AppConfig, TaskRepository}; +use onyx_core::config::WorkspaceConfig; use anyhow::{Context, Result}; 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") } -pub fn get_repository(workspace_name: Option) -> Result<(TaskRepository, String)> { - let config = load_config()?; - - let (name, workspace_config) = if let Some(name) = workspace_name { - let workspace_config = config.get_workspace(&name) - .ok_or_else(|| anyhow::anyhow!("Workspace '{}' not found", name))?; - (name, workspace_config.clone()) +/// Resolve a user-supplied identifier to (id, WorkspaceConfig). Accepts either +/// the workspace's display name or its UUID. Falls back to the current +/// workspace when `identifier` is `None`. +pub fn resolve_workspace(config: &AppConfig, identifier: Option<&str>) -> Result<(String, WorkspaceConfig)> { + if let Some(s) = identifier { + // Try by UUID first (exact match on map key), then fall back to name lookup. + 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 { - let (name, workspace_config) = config.get_current_workspace() - .context("No workspace set. Use 'onyx init' to create one.")?; - (name.clone(), workspace_config.clone()) - }; + let (id, ws) = config.get_current_workspace() + .context("No workspace set. Run 'onyx workspace add ' to create one, or 'onyx workspace switch ' to select one.")?; + Ok((id.clone(), ws.clone())) + } +} + +pub fn get_repository(workspace_identifier: Option) -> 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()) .context(format!("Failed to open workspace '{}'", name))?; diff --git a/crates/onyx-cli/src/commands/sync.rs b/crates/onyx-cli/src/commands/sync.rs index c34d502..605eaee 100644 --- a/crates/onyx-cli/src/commands/sync.rs +++ b/crates/onyx-cli/src/commands/sync.rs @@ -2,22 +2,8 @@ use anyhow::{Context, Result}; use colored::Colorize; use onyx_core::sync::{SyncMode, sync_workspace, get_sync_status}; use onyx_core::webdav::{WebDavClient, store_credentials, load_credentials}; -use onyx_core::config::AppConfig; use crate::output; -use super::{load_config, save_config}; - -/// 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())) - } -} +use super::{load_config, save_config, resolve_workspace}; /// Run sync setup: prompt for URL, username, password, test connection, store credentials. pub fn setup(workspace_name: Option) -> Result<()> { diff --git a/crates/onyx-cli/src/commands/workspace.rs b/crates/onyx-cli/src/commands/workspace.rs index ac10a94..18b02ba 100644 --- a/crates/onyx-cli/src/commands/workspace.rs +++ b/crates/onyx-cli/src/commands/workspace.rs @@ -30,11 +30,21 @@ pub fn add(name: String, path: String) -> Result<()> { // 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(()) } @@ -64,15 +74,20 @@ pub fn list() -> Result<()> { Ok(()) } -/// Resolve a workspace name to its ID. Errors if not found or ambiguous. -fn resolve_name(config: &onyx_core::config::AppConfig, name: &str) -> Result { +/// 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 { + // 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 == name) + .filter(|(_, ws)| ws.name == identifier) .collect(); match matches.len() { - 0 => anyhow::bail!("Workspace '{}' not found", name), + 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, name), + n => anyhow::bail!("Ambiguous: {} workspaces named '{}'. Use the workspace ID instead.", n, identifier), } }