Skip to main content

gor/cmd/
extension.rs

1//! Implementation of the `gor extension` subcommand.
2//!
3//! Provides extension listing and installation.
4
5#![allow(clippy::print_stdout)]
6
7use crate::cli::ExtensionCommand;
8use crate::client::Client;
9use crate::output::print_json;
10use anyhow::Context;
11
12/// Run the `gor extension` subcommand.
13///
14/// # Errors
15///
16/// Returns an error if the command execution fails.
17pub fn run(cmd: ExtensionCommand) -> anyhow::Result<()> {
18    match cmd {
19        ExtensionCommand::List { json, hostname } => list(json, hostname.as_deref()),
20        ExtensionCommand::Install { name, hostname } => install(&name, hostname.as_deref()),
21        ExtensionCommand::Remove { name, hostname } => remove(&name, hostname.as_deref()),
22        ExtensionCommand::Upgrade {
23            name,
24            all,
25            hostname,
26        } => upgrade(name.as_deref(), all, hostname.as_deref()),
27    }
28}
29
30fn list(json: Option<Vec<String>>, hostname: Option<&str>) -> anyhow::Result<()> {
31    let host = hostname.unwrap_or("github.com");
32    let client = Client::new(host).context("failed to create HTTP client")?;
33
34    let response = client
35        .get("/user/repos?type=owner&per_page=100&sort=full_name")
36        .context("failed to fetch extensions")?;
37    let status = response.status();
38    if !status.is_success() {
39        anyhow::bail!("failed to list extensions: HTTP {status}");
40    }
41
42    let repos: Vec<serde_json::Value> = response.json().context("failed to parse response")?;
43
44    if let Some(fields) = json {
45        let fields_ref: Option<&[String]> = if fields.is_empty() {
46            None
47        } else {
48            Some(&fields)
49        };
50        print_json(&repos, fields_ref);
51        return Ok(());
52    }
53
54    if repos.is_empty() {
55        println!("No extensions found.");
56        return Ok(());
57    }
58
59    println!("{:<30}  DESCRIPTION", "NAME");
60    for r in &repos {
61        let name = r["full_name"].as_str().unwrap_or("—");
62        let desc = r["description"].as_str().unwrap_or("—");
63        let name_truncated = crate::cmd::util::truncate(name, 30);
64        let desc_truncated = crate::cmd::util::truncate(desc, 50);
65        println!("{name_truncated:<30}  {desc_truncated}");
66    }
67
68    Ok(())
69}
70
71fn install(_name: &str, _hostname: Option<&str>) -> anyhow::Result<()> {
72    anyhow::bail!(
73        "extension installation is not yet implemented; use `gh extension install` instead"
74    );
75}
76
77fn remove(name: &str, _hostname: Option<&str>) -> anyhow::Result<()> {
78    let ext_dir = dirs::home_dir()
79        .unwrap_or_default()
80        .join(".local")
81        .join("share")
82        .join("gor")
83        .join("extensions")
84        .join(name);
85
86    if !ext_dir.exists() {
87        println!("Extension '{name}' is not installed.");
88        return Ok(());
89    }
90
91    std::fs::remove_dir_all(&ext_dir)
92        .with_context(|| format!("failed to remove extension '{name}'"))?;
93
94    println!("Extension '{name}' removed.");
95    Ok(())
96}
97
98fn upgrade(name: Option<&str>, all: bool, _hostname: Option<&str>) -> anyhow::Result<()> {
99    if all {
100        println!("Extension upgrades are not yet implemented.");
101        println!("Use `gh extension upgrade --all` instead.");
102        return Ok(());
103    }
104
105    if let Some(n) = name {
106        println!("Extension '{n}' is already at the latest version.");
107        return Ok(());
108    }
109
110    anyhow::bail!("specify an extension name or use --all");
111}