use std::path::PathBuf;
use crate::error::{Result, SkillError};
#[must_use]
pub fn get_token() -> Option<String> {
if let Ok(token) = std::env::var("GITHUB_TOKEN")
&& !token.is_empty()
{
return Some(token);
}
if let Ok(token) = std::env::var("GH_TOKEN")
&& !token.is_empty()
{
return Some(token);
}
std::process::Command::new("gh")
.args(["auth", "token"])
.output()
.ok()
.and_then(|o| {
if o.status.success() {
String::from_utf8(o.stdout)
.ok()
.map(|s| s.trim().to_owned())
} else {
None
}
})
.filter(|s| !s.is_empty())
}
#[cfg(feature = "network")]
#[derive(serde::Deserialize)]
struct GitTreeResponse {
sha: Option<String>,
#[serde(default)]
tree: Vec<GitTreeEntry>,
}
#[cfg(feature = "network")]
#[derive(serde::Deserialize)]
struct GitTreeEntry {
path: String,
sha: Option<String>,
#[serde(rename = "type")]
entry_type: String,
}
#[cfg(feature = "network")]
pub async fn fetch_skill_folder_hash(
owner_repo: &str,
skill_path: &str,
token: Option<&str>,
) -> Result<Option<String>> {
let folder_path = skill_path
.replace('\\', "/")
.trim_end_matches("/SKILL.md")
.trim_end_matches("SKILL.md")
.trim_end_matches('/')
.to_owned();
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(10))
.build()
.map_err(|e| {
SkillError::io(
PathBuf::from("<network>"),
std::io::Error::other(e.to_string()),
)
})?;
for branch in &["main", "master"] {
let url =
format!("https://api.github.com/repos/{owner_repo}/git/trees/{branch}?recursive=1");
let mut req = client
.get(&url)
.header("Accept", "application/vnd.github.v3+json")
.header("User-Agent", "skills-cli-rs");
if let Some(tok) = token {
req = req.header("Authorization", format!("Bearer {tok}"));
}
let resp = match req.send().await {
Ok(r) if r.status().is_success() => r,
_ => continue,
};
let data: GitTreeResponse = match resp.json().await {
Ok(v) => v,
Err(_) => continue,
};
if folder_path.is_empty() {
return Ok(data.sha);
}
let found = data
.tree
.iter()
.find(|e| e.entry_type == "tree" && e.path == folder_path)
.and_then(|e| e.sha.clone());
if found.is_some() {
return Ok(found);
}
}
Ok(None)
}
#[cfg(feature = "network")]
pub async fn is_repo_private(owner: &str, repo: &str) -> Result<Option<bool>> {
let url = format!("https://api.github.com/repos/{owner}/{repo}");
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(10))
.build()?;
let resp = client
.get(&url)
.header("User-Agent", "skills-cli-rs")
.send()
.await?;
if !resp.status().is_success() {
return Ok(None);
}
let data: serde_json::Value = resp.json().await?;
Ok(data.get("private").and_then(serde_json::Value::as_bool))
}