mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-07 08:27:12 +00:00
107 lines
3.4 KiB
Rust
107 lines
3.4 KiB
Rust
use tokio::process::Command;
|
|
use tracing::debug;
|
|
|
|
/// Best-effort wrapper around the `gh` CLI.
|
|
///
|
|
/// All methods degrade gracefully: if `gh` is missing or not authenticated
|
|
/// the caller receives `None` / empty results rather than errors.
|
|
pub(crate) struct GhCli {
|
|
_private: (),
|
|
}
|
|
|
|
impl GhCli {
|
|
/// Attempt to find an authenticated `gh` CLI on PATH.
|
|
///
|
|
/// Returns `None` if `gh` is not installed or not authenticated
|
|
/// against github.com.
|
|
pub(crate) async fn detect() -> Option<Self> {
|
|
Self::detect_with_command("gh").await
|
|
}
|
|
|
|
async fn detect_with_command(command: &str) -> Option<Self> {
|
|
let version = Command::new(command).arg("--version").output().await;
|
|
let Ok(output) = version else {
|
|
debug!("gh CLI not found on PATH");
|
|
return None;
|
|
};
|
|
if !output.status.success() {
|
|
debug!("gh --version failed");
|
|
return None;
|
|
}
|
|
|
|
let auth = Command::new(command)
|
|
.args(["auth", "status", "--hostname", "github.com"])
|
|
.output()
|
|
.await;
|
|
match auth {
|
|
Ok(o) if o.status.success() => {
|
|
debug!("gh CLI available and authenticated for github.com");
|
|
Some(Self { _private: () })
|
|
}
|
|
_ => {
|
|
debug!("gh is not authenticated for github.com");
|
|
None
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Return the login of the authenticated GitHub user, or `None` on failure.
|
|
pub(crate) async fn authenticated_user(&self) -> Option<String> {
|
|
let output = Command::new("gh")
|
|
.args(["api", "--hostname", "github.com", "/user", "--jq", ".login"])
|
|
.output()
|
|
.await
|
|
.ok()?;
|
|
if !output.status.success() {
|
|
debug!("gh api /user failed");
|
|
return None;
|
|
}
|
|
let login = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
|
if login.is_empty() { None } else { Some(login) }
|
|
}
|
|
|
|
/// List GitHub organizations the authenticated user is an admin of.
|
|
///
|
|
/// Returns an empty vec on any failure (network, parse, etc.).
|
|
pub(crate) async fn list_admin_orgs(&self) -> Vec<String> {
|
|
let output = Command::new("gh")
|
|
.args([
|
|
"api",
|
|
"--hostname",
|
|
"github.com",
|
|
"--paginate",
|
|
"/user/memberships/orgs",
|
|
"--jq",
|
|
r#".[] | select(.role == "admin" and .state == "active") | .organization.login"#,
|
|
])
|
|
.output()
|
|
.await;
|
|
let Ok(output) = output else {
|
|
debug!("gh api /user/memberships/orgs failed to execute");
|
|
return Vec::new();
|
|
};
|
|
if !output.status.success() {
|
|
debug!(
|
|
"gh api /user/memberships/orgs exited with {}",
|
|
output.status
|
|
);
|
|
return Vec::new();
|
|
}
|
|
String::from_utf8_lossy(&output.stdout)
|
|
.lines()
|
|
.filter(|line| !line.is_empty())
|
|
.map(String::from)
|
|
.collect()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[tokio::test]
|
|
async fn detect_returns_none_when_gh_is_missing() {
|
|
let result = GhCli::detect_with_command("fabro-test-gh-that-should-not-exist").await;
|
|
assert!(result.is_none());
|
|
}
|
|
}
|