ref:0c8d1b09f34c7419858b762934f6577f6e340c94

feat(repo): add `anvil repo list` subcommand

Lists repositories in an organization. Mirrors `repo create`'s org resolution: explicit --org, else org from default repo / git remote. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SHA: 0c8d1b09f34c7419858b762934f6577f6e340c94
Author: Cole Christensen <cole.christensen@gmail.com>
Date: 2026-05-20 17:04
Parents: 0430ea2
1 files changed +71 -0
Type
src/commands/repo.rs +71 −0
@@ -12,6 +12,15 @@
#[derive(Subcommand)]
pub enum RepoCommand {
/// List repositories in an organization
List {
/// Organization slug (defaults to org from default repo)
#[arg(long)]
org: Option<String>,
/// Maximum results
#[arg(long, default_value = "30")]
limit: u32,
},
/// Show repository details
View {
/// Repository (org/repo), or detected from git remote
@@ -60,6 +69,7 @@
pub async fn run(args: RepoArgs) -> Result<(), Box<dyn std::error::Error>> {
match args.command {
RepoCommand::List { org, limit } => list(org.as_deref(), limit).await,
RepoCommand::View { repo } => view(repo.as_deref()).await,
RepoCommand::Create {
name,
@@ -70,6 +80,67 @@
RepoCommand::Clone { repo, dir } => clone(&repo, dir.as_deref()).await,
RepoCommand::SetDefault { repo } => set_default(&repo).await,
}
}
async fn list(org: Option<&str>, limit: u32) -> Result<(), Box<dyn std::error::Error>> {
let client = Client::from_config()?;
let org_slug = match org {
Some(o) => o.to_string(),
None => {
let (resolved_org, _) = config::resolve_repo(None)?;
resolved_org
}
};
let resp: serde_json::Value = client
.get_with_query(
&format!("/{org_slug}/repos"),
&[("per_page", &limit.to_string())],
)
.await?;
if output::is_json() {
output::print_json(&resp);
return Ok(());
}
let repos: Vec<Repo> = if let Some(arr) = resp.get("repositories") {
serde_json::from_value(arr.clone()).unwrap_or_default()
} else if let Some(arr) = resp.get("repos") {
serde_json::from_value(arr.clone()).unwrap_or_default()
} else if let Some(arr) = resp.get("data") {
serde_json::from_value(arr.clone()).unwrap_or_default()
} else {
serde_json::from_value(resp).unwrap_or_default()
};
output::header(&format!("Repositories ({org_slug})"));
let rows: Vec<Vec<String>> = repos
.iter()
.map(|r| {
let desc = r.description.clone().unwrap_or_default();
let desc = if desc.chars().count() > 60 {
format!("{}…", desc.chars().take(59).collect::<String>())
} else {
desc
};
vec![
r.slug
.clone()
.or_else(|| r.name.clone())
.unwrap_or_default(),
output::colorize_status(r.visibility.as_deref().unwrap_or("?")),
r.default_branch.clone().unwrap_or_default(),
desc,
]
})
.collect();
output::print_table(&["NAME", "VISIBILITY", "DEFAULT", "DESCRIPTION"], &rows);
Ok(())
}
async fn view(repo: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {