ref:648ceeeb4f135e9a6ac34e666144914005378920

feat(cli): add issue close, reopen, and edit commands

Adds three new subcommands to `anvil issue`: - close <number>: sets state to closed via PATCH - reopen <number>: sets state to open via PATCH - edit <number> --title --body: updates title and/or body via PATCH, warns if neither flag is provided Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SHA: 648ceeeb4f135e9a6ac34e666144914005378920
Author: Cole Christensen <cole.christensen@macmillan.com>
Date: 2026-03-15 22:04
Parents: 082d717
1 files changed +97 -0
Type
src/commands/issue.rs +97 −0
@@ -46,6 +46,36 @@
#[arg(long)]
labels: Option<String>,
},
/// Close an issue
Close {
/// Issue number
number: u32,
/// Repository (org/repo)
#[arg(long)]
repo: Option<String>,
},
/// Reopen a closed issue
Reopen {
/// Issue number
number: u32,
/// Repository (org/repo)
#[arg(long)]
repo: Option<String>,
},
/// Edit an issue's title or body
Edit {
/// Issue number
number: u32,
/// Repository (org/repo)
#[arg(long)]
repo: Option<String>,
/// New title
#[arg(long)]
title: Option<String>,
/// New body
#[arg(long)]
body: Option<String>,
},
}
#[derive(Debug, Deserialize)]
@@ -81,5 +111,13 @@
body,
labels,
} => create(repo.as_deref(), &title, &body, labels.as_deref()).await,
IssueCommand::Close { number, repo } => close_issue(repo.as_deref(), number).await,
IssueCommand::Reopen { number, repo } => reopen_issue(repo.as_deref(), number).await,
IssueCommand::Edit {
number,
repo,
title,
body,
} => edit_issue(repo.as_deref(), number, title.as_deref(), body.as_deref()).await,
}
}
@@ -226,5 +264,64 @@
);
}
Ok(())
}
async fn close_issue(repo: Option<&str>, number: u32) -> Result<(), Box<dyn std::error::Error>> {
let client = Client::from_config()?;
let (org, name) = config::resolve_repo(repo)?;
let _resp: serde_json::Value = client
.patch(
&format!("/{org}/{name}/issues/{number}"),
&serde_json::json!({"state": "closed"}),
)
.await?;
output::success(&format!("Closed issue #{number}"));
Ok(())
}
async fn reopen_issue(repo: Option<&str>, number: u32) -> Result<(), Box<dyn std::error::Error>> {
let client = Client::from_config()?;
let (org, name) = config::resolve_repo(repo)?;
let _resp: serde_json::Value = client
.patch(
&format!("/{org}/{name}/issues/{number}"),
&serde_json::json!({"state": "open"}),
)
.await?;
output::success(&format!("Reopened issue #{number}"));
Ok(())
}
async fn edit_issue(
repo: Option<&str>,
number: u32,
title: Option<&str>,
body: 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::Map::new();
if let Some(t) = title {
payload.insert("title".into(), serde_json::json!(t));
}
if let Some(b) = body {
payload.insert("body".into(), serde_json::json!(b));
}
if payload.is_empty() {
output::warn("Nothing to update — specify --title or --body");
return Ok(());
}
let _resp: serde_json::Value = client
.patch(
&format!("/{org}/{name}/issues/{number}"),
&serde_json::Value::Object(payload),
)
.await?;
output::success(&format!("Updated issue #{number}"));
Ok(())
}