Skip to main content

gor/cmd/
copilot.rs

1//! Implementation of the `gor copilot` subcommand.
2//!
3//! Provides Copilot status and usage information.
4
5#![allow(clippy::print_stdout)]
6
7use crate::cli::CopilotCommand;
8use crate::client::Client;
9use crate::output::print_json;
10use anyhow::Context;
11
12/// Run the `gor copilot` subcommand.
13///
14/// # Errors
15///
16/// Returns an error if the command execution fails.
17pub fn run(cmd: CopilotCommand) -> anyhow::Result<()> {
18    match cmd {
19        CopilotCommand::Status { json, hostname } => status(json, hostname.as_deref()),
20        CopilotCommand::Usage {
21            org,
22            json,
23            hostname,
24        } => usage(org.as_deref(), json, hostname.as_deref()),
25    }
26}
27
28/// Show Copilot subscription status for the authenticated user.
29///
30/// # Errors
31///
32/// Returns an error if the API request fails.
33fn status(json: Option<Vec<String>>, hostname: Option<&str>) -> anyhow::Result<()> {
34    let host = hostname.unwrap_or("github.com");
35    let client = Client::new(host).context("failed to create HTTP client")?;
36
37    let response = client
38        .get("/user/copilot/subscription")
39        .context("failed to fetch Copilot subscription")?;
40
41    let status_code = response.status();
42    if status_code == reqwest::StatusCode::NOT_FOUND {
43        println!("Copilot is not enabled for your account.");
44        return Ok(());
45    }
46    if !status_code.is_success() {
47        anyhow::bail!("failed to fetch Copilot status: HTTP {status_code}");
48    }
49
50    let sub: serde_json::Value = response.json().context("failed to parse response")?;
51
52    if let Some(fields) = json {
53        let fields_ref: Option<&[String]> = if fields.is_empty() {
54            None
55        } else {
56            Some(&fields)
57        };
58        print_json(&sub, fields_ref);
59        return Ok(());
60    }
61
62    let plan = sub["plan"].as_str().unwrap_or("—");
63    let seat = sub["seat_status"].as_str().unwrap_or("—");
64    let renewal = sub["renewal_date"].as_str().unwrap_or("—");
65
66    println!("Copilot Status");
67    println!("  Plan: {plan}");
68    println!("  Seat: {seat}");
69    println!("  Renewal: {renewal}");
70
71    Ok(())
72}
73
74/// Show Copilot usage statistics for an organization.
75///
76/// # Errors
77///
78/// Returns an error if the API request fails.
79fn usage(
80    org: Option<&str>,
81    json: Option<Vec<String>>,
82    hostname: Option<&str>,
83) -> anyhow::Result<()> {
84    let host = hostname.unwrap_or("github.com");
85    let client = Client::new(host).context("failed to create HTTP client")?;
86
87    let org_name = org.ok_or_else(|| anyhow::anyhow!("--org is required for usage statistics"))?;
88
89    let path = format!("/orgs/{org_name}/copilot/usage");
90    let response = client.get(&path).context("failed to fetch Copilot usage")?;
91
92    let status_code = response.status();
93    if status_code == reqwest::StatusCode::NOT_FOUND {
94        println!("Copilot is not enabled for organization '{org_name}'.");
95        return Ok(());
96    }
97    if !status_code.is_success() {
98        anyhow::bail!("failed to fetch Copilot usage: HTTP {status_code}");
99    }
100
101    let usage_data: serde_json::Value = response.json().context("failed to parse response")?;
102
103    if let Some(fields) = json {
104        let fields_ref: Option<&[String]> = if fields.is_empty() {
105            None
106        } else {
107            Some(&fields)
108        };
109        print_json(&usage_data, fields_ref);
110        return Ok(());
111    }
112
113    // Display usage summary
114    let total = usage_data["total_active_users"].as_u64().unwrap_or(0);
115    let breakdown = usage_data["breakdown"]
116        .as_array()
117        .map_or_else(Vec::new, Clone::clone);
118
119    println!("Copilot Usage for {org_name}");
120    println!("  Total active users: {total}");
121
122    if !breakdown.is_empty() {
123        println!("  Breakdown:");
124        for entry in &breakdown {
125            let lang = entry["language"].as_str().unwrap_or("—");
126            let editors = entry["editor_count"].as_u64().unwrap_or(0);
127            println!("    {lang}: {editors} editors");
128        }
129    }
130
131    Ok(())
132}