Skip to main content

ward/cli/
repos.rs

1use anyhow::Result;
2use clap::Args;
3use console::style;
4use tabled::{Table, settings::Style};
5
6use crate::config::Manifest;
7use crate::github::Client;
8
9#[derive(Args)]
10pub struct ReposCommand {
11    #[command(subcommand)]
12    action: ReposAction,
13}
14
15#[derive(clap::Subcommand)]
16enum ReposAction {
17    /// List repositories with metadata
18    List,
19
20    /// Inspect a single repository in detail
21    Inspect {
22        /// Repository name (without org prefix)
23        name: String,
24    },
25}
26
27impl ReposCommand {
28    pub async fn run(
29        &self,
30        client: &Client,
31        manifest: &Manifest,
32        system: Option<&str>,
33    ) -> Result<()> {
34        match &self.action {
35            ReposAction::List => list_repos(client, manifest, system).await,
36            ReposAction::Inspect { name } => inspect_repo(client, name).await,
37        }
38    }
39}
40
41async fn list_repos(client: &Client, manifest: &Manifest, system: Option<&str>) -> Result<()> {
42    let repos = if let Some(sys) = system {
43        let excludes = manifest.exclude_patterns_for_system(sys);
44        let explicit = manifest.explicit_repos_for_system(sys);
45        client
46            .list_repos_for_system(sys, &excludes, &explicit)
47            .await?
48    } else {
49        client.list_repos().await?
50    };
51
52    if repos.is_empty() {
53        println!("  No repositories found.");
54        return Ok(());
55    }
56
57    let rows: Vec<[String; 4]> = repos
58        .iter()
59        .map(|r| {
60            [
61                r.name.clone(),
62                r.language.clone().unwrap_or_else(|| "-".to_owned()),
63                r.visibility.clone(),
64                r.default_branch.clone(),
65            ]
66        })
67        .collect();
68
69    let _table = Table::new(rows).with(Style::rounded()).to_string();
70
71    println!();
72    println!(
73        "  {} repositories in {}{}\n",
74        style(repos.len()).bold().cyan(),
75        style(&client.org).bold(),
76        system
77            .map(|s| format!(" (system: {s})"))
78            .unwrap_or_default()
79    );
80
81    // Manual header + data table
82    println!(
83        "  {:40} {:15} {:12} {}",
84        style("Repository").bold().underlined(),
85        style("Language").bold().underlined(),
86        style("Visibility").bold().underlined(),
87        style("Branch").bold().underlined(),
88    );
89
90    for r in &repos {
91        println!(
92            "  {:40} {:15} {:12} {}",
93            r.name,
94            r.language.as_deref().unwrap_or("-"),
95            r.visibility,
96            r.default_branch,
97        );
98    }
99
100    let _ = _table; // table available for --json mode later
101    Ok(())
102}
103
104async fn inspect_repo(client: &Client, name: &str) -> Result<()> {
105    let repo = client.get_repo(name).await?;
106    let security = client.get_security_state(name).await?;
107
108    println!();
109    println!("  {} {}", style("Repository:").bold(), repo.full_name);
110    println!(
111        "  {} {}",
112        style("Description:").bold(),
113        repo.description.as_deref().unwrap_or("-")
114    );
115    println!(
116        "  {} {}",
117        style("Language:").bold(),
118        repo.language.as_deref().unwrap_or("-")
119    );
120    println!("  {} {}", style("Visibility:").bold(), repo.visibility);
121    println!(
122        "  {} {}",
123        style("Default Branch:").bold(),
124        repo.default_branch
125    );
126    println!("  {} {}", style("Archived:").bold(), repo.archived);
127
128    println!();
129    println!("  {}", style("Security Status:").bold().underlined());
130    print_feature("  Dependabot Alerts", security.dependabot_alerts);
131    print_feature(
132        "  Dependabot Security Updates",
133        security.dependabot_security_updates,
134    );
135    print_feature("  Secret Scanning", security.secret_scanning);
136    print_feature(
137        "  Secret Scanning AI",
138        security.secret_scanning_ai_detection,
139    );
140    print_feature("  Push Protection", security.push_protection);
141
142    Ok(())
143}
144
145fn print_feature(name: &str, enabled: bool) {
146    let icon = if enabled {
147        style("✅").green()
148    } else {
149        style("❌").red()
150    };
151    println!(
152        "{name}: {icon} {}",
153        if enabled { "enabled" } else { "disabled" }
154    );
155}