ref:2f082ee596131f9f9b6863c6c0fd8f708a3697d4

fix(repo): switch \`anvil repo list\` to GET /repos

The original implementation called GET /:org/repos which doesn't exist on the server — every invocation returned 404 in prod. Switch to the new GET /repos endpoint (server side: anvil#NEW), which returns every repo visible to the caller across all their orgs. --org becomes a client-side substring filter on the returned list, since the server side doesn't accept it as a query param yet. The table now shows org/slug instead of bare slug since results span orgs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SHA: 2f082ee596131f9f9b6863c6c0fd8f708a3697d4
Author: Cole Christensen <cole.christensen@gmail.com>
Date: 2026-05-29 02:53
Parents: a836dac
1 files changed +41 -24
Type
src/commands/repo.rs +41 −24
@@ -12,9 +12,9 @@
#[derive(Subcommand)]
pub enum RepoCommand {
/// List repositories in an organization
/// List every repository you can see across all your orgs
List {
/// Filter results by org slug (client-side substring match)
/// Organization slug (defaults to org from default repo)
#[arg(long)]
org: Option<String>,
/// Maximum results
@@ -65,8 +65,16 @@
visibility: Option<String>,
fork_count: Option<i64>,
inserted_at: Option<String>,
org: Option<RepoOrgRef>,
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct RepoOrgRef {
slug: Option<String>,
name: Option<String>,
}
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,
@@ -82,22 +90,11 @@
}
}
async fn list(org: Option<&str>, limit: u32) -> Result<(), Box<dyn std::error::Error>> {
async fn list(org_filter: 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())],
)
.get_with_query("/repos", &[("per_page", &limit.to_string())])
.await?;
if output::is_json() {
@@ -115,22 +112,42 @@
serde_json::from_value(resp).unwrap_or_default()
};
let filtered: Vec<&Repo> = match org_filter {
Some(f) => repos
.iter()
.filter(|r| {
r.org
.as_ref()
output::header(&format!("Repositories ({org_slug})"));
.and_then(|o| o.slug.as_deref())
.map(|s| s.contains(f))
.unwrap_or(false)
})
.collect(),
None => repos.iter().collect(),
};
let rows: Vec<Vec<String>> = repos
output::header(&format!("Repositories ({} visible)", filtered.len()));
let rows: Vec<Vec<String>> = filtered
.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>())
let desc = if desc.chars().count() > 50 {
format!("{}…", desc.chars().take(49).collect::<String>())
} else {
desc
};
let name = r
.slug
.clone()
.or_else(|| r.name.clone())
.unwrap_or_default();
let full = match r.org.as_ref().and_then(|o| o.slug.as_deref()) {
Some(org_slug) => format!("{org_slug}/{name}"),
None => name,
};
vec![
r.slug
.clone()
.or_else(|| r.name.clone())
.unwrap_or_default(),
full,
output::colorize_status(r.visibility.as_deref().unwrap_or("?")),
r.default_branch.clone().unwrap_or_default(),
desc,
@@ -138,7 +155,7 @@
})
.collect();
output::print_table(&["NAME", "VISIBILITY", "DEFAULT", "DESCRIPTION"], &rows);
output::print_table(&["REPO", "VISIBILITY", "DEFAULT", "DESCRIPTION"], &rows);
Ok(())
}