ref:42625225327f111b78cdd71429a7d06ed9a3e4d1

feat(cli): add label management commands

SHA: 42625225327f111b78cdd71429a7d06ed9a3e4d1
Author: Cole Christensen <cole.christensen@macmillan.com>
Date: 2026-03-15 22:09
Parents: 425cfb1
2 files changed +126 -0
Type
src/commands/label.rs +122 −0
@@ -1,0 +1,122 @@
use crate::client::Client;
use crate::config;
use crate::output;
use clap::{Args, Subcommand};
use serde::Deserialize;
#[derive(Args)]
pub struct LabelArgs {
#[command(subcommand)]
pub command: LabelCommand,
}
#[derive(Subcommand)]
pub enum LabelCommand {
/// List labels for a repository
List {
/// Repository (org/repo)
repo: Option<String>,
},
/// Create a new label
Create {
/// Label name
#[arg(long)]
name: String,
/// Label color (hex, e.g. "#ff0000")
#[arg(long, default_value = "#6b7280")]
color: String,
/// Label description
#[arg(long)]
description: Option<String>,
/// Repository (org/repo)
#[arg(long)]
repo: Option<String>,
},
/// Add a label to an issue
Add {
/// Issue number
#[arg(long)]
issue: u32,
/// Label name
#[arg(long)]
name: String,
/// Repository (org/repo)
#[arg(long)]
repo: Option<String>,
},
/// Remove a label from an issue
Remove {
/// Issue number
#[arg(long)]
issue: u32,
/// Label name
#[arg(long)]
name: String,
/// Repository (org/repo)
#[arg(long)]
repo: Option<String>,
},
}
#[derive(Debug, Deserialize)]
struct Label {
name: Option<String>,
color: Option<String>,
description: Option<String>,
}
pub async fn run(args: LabelArgs) -> Result<(), Box<dyn std::error::Error>> {
match args.command {
LabelCommand::List { repo } => list(repo.as_deref()).await,
LabelCommand::Create { name, color, description, repo } => create(repo.as_deref(), &name, &color, description.as_deref()).await,
LabelCommand::Add { issue, name, repo } => add(repo.as_deref(), issue, &name).await,
LabelCommand::Remove { issue, name, repo } => remove(repo.as_deref(), issue, &name).await,
}
}
async fn list(repo: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
let client = Client::from_config()?;
let (org, name) = config::resolve_repo(repo)?;
let resp: serde_json::Value = client.get(&format!("/{org}/{name}/labels")).await?;
let labels: Vec<Label> = resp.get("labels").and_then(|v| serde_json::from_value(v.clone()).ok()).unwrap_or_default();
output::header(&format!("Labels ({org}/{name})"));
let rows: Vec<Vec<String>> = labels.iter().map(|l| {
vec![
l.name.clone().unwrap_or_default(),
l.color.clone().unwrap_or_default(),
l.description.clone().unwrap_or_default(),
]
}).collect();
output::print_table(&["NAME", "COLOR", "DESCRIPTION"], &rows);
Ok(())
}
async fn create(repo: Option<&str>, label_name: &str, color: &str, description: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
let client = Client::from_config()?;
let (org, name) = config::resolve_repo(repo)?;
let mut payload = serde_json::json!({"name": label_name, "color": color});
if let Some(desc) = description {
payload["description"] = serde_json::json!(desc);
}
let _resp: serde_json::Value = client.post(&format!("/{org}/{name}/labels"), &payload).await?;
output::success(&format!("Created label '{label_name}'"));
Ok(())
}
async fn add(repo: Option<&str>, issue_number: u32, label_name: &str) -> Result<(), Box<dyn std::error::Error>> {
let client = Client::from_config()?;
let (org, name) = config::resolve_repo(repo)?;
let _resp: serde_json::Value = client
.post(&format!("/{org}/{name}/issues/{issue_number}/labels"), &serde_json::json!({"name": label_name}))
.await?;
output::success(&format!("Added label '{label_name}' to issue #{issue_number}"));
Ok(())
}
async fn remove(repo: Option<&str>, issue_number: u32, label_name: &str) -> Result<(), Box<dyn std::error::Error>> {
let client = Client::from_config()?;
let (org, name) = config::resolve_repo(repo)?;
client.delete_empty(&format!("/{org}/{name}/issues/{issue_number}/labels/{label_name}")).await?;
output::success(&format!("Removed label '{label_name}' from issue #{issue_number}"));
Ok(())
}
src/commands/mod.rs +4 −0
@@ -5,6 +5,7 @@
pub mod commit;
pub mod deploy;
pub mod issue;
pub mod label;
pub mod pr;
pub mod release;
pub mod repo;
@@ -47,6 +48,8 @@
Agent(agent::AgentArgs),
/// CI runner management
Runner(runner::RunnerArgs),
/// Label operations
Label(label::LabelArgs),
}
pub async fn run(cli: Cli) -> Result<(), Box<dyn std::error::Error>> {
@@ -64,5 +67,6 @@
Command::Runner(args) => runner::run(args)
.await
.map_err(|e| -> Box<dyn std::error::Error> { e }),
Command::Label(args) => label::run(args).await,
}
}