use miyabi_types::error::{MiyabiError, Result};
use std::fs;
use std::process::Command;
pub fn discover_token() -> Result<String> {
if let Ok(token) = std::env::var("GITHUB_TOKEN") {
if !token.is_empty() && token.starts_with("ghp_") {
tracing::debug!("Found GitHub token from GITHUB_TOKEN environment variable");
return Ok(token);
}
}
if let Ok(token) = get_token_from_gh_cli() {
tracing::debug!("Found GitHub token from gh CLI");
return Ok(token);
}
if let Ok(token) = get_token_from_gh_config() {
tracing::debug!("Found GitHub token from gh config file");
return Ok(token);
}
Err(MiyabiError::Auth(
"GitHub token not found. Please set up authentication:\n\n\
Option 1: Set environment variable\n\
\x20 export GITHUB_TOKEN=ghp_your_token_here\n\
\x20 # Add to ~/.zshrc or ~/.bashrc for persistence\n\n\
Option 2: Use GitHub CLI (recommended)\n\
\x20 gh auth login\n\
\x20 # Follow the interactive prompts\n\n\
Option 3: Create a Personal Access Token\n\
\x20 1. Go to https://github.com/settings/tokens\n\
\x20 2. Generate new token (classic) with 'repo' scope\n\
\x20 3. Set GITHUB_TOKEN environment variable\n\n\
For more help, see: https://docs.github.com/en/authentication"
.to_string(),
))
}
fn get_token_from_gh_cli() -> Result<String> {
let output = Command::new("gh")
.args(["auth", "token"])
.output()
.map_err(|e| MiyabiError::Auth(format!("Failed to execute gh command: {}", e)))?;
if output.status.success() {
let token = String::from_utf8_lossy(&output.stdout).trim().to_string();
if !token.is_empty() && token.starts_with("ghp_") {
return Ok(token);
}
}
Err(MiyabiError::Auth(
"gh CLI is available but not authenticated".to_string(),
))
}
fn get_token_from_gh_config() -> Result<String> {
let config_path = dirs::home_dir()
.ok_or_else(|| MiyabiError::Auth("Could not determine home directory".to_string()))?
.join(".config")
.join("gh")
.join("hosts.yml");
if !config_path.exists() {
return Err(MiyabiError::Auth("gh config file not found".to_string()));
}
let content = fs::read_to_string(&config_path)
.map_err(|e| MiyabiError::Auth(format!("Failed to read gh config file: {}", e)))?;
for line in content.lines() {
let trimmed = line.trim();
if trimmed.starts_with("oauth_token:") {
if let Some(token) = trimmed.split(':').nth(1) {
let token = token.trim().to_string();
if token.starts_with("ghp_") {
return Ok(token);
}
}
}
}
Err(MiyabiError::Auth(
"No oauth_token found in gh config file".to_string(),
))
}
pub fn validate_token_format(token: &str) -> Result<()> {
if token.is_empty() {
return Err(MiyabiError::Auth("Token is empty".to_string()));
}
if !token.starts_with("ghp_") && !token.starts_with("gho_") && !token.starts_with("ghs_") {
return Err(MiyabiError::Auth(
"Token does not start with expected prefix (ghp_, gho_, or ghs_)".to_string(),
));
}
if token.len() < 20 {
return Err(MiyabiError::Auth(
"Token is too short (expected at least 20 characters)".to_string(),
));
}
Ok(())
}
pub fn check_gh_cli_status() -> GhCliStatus {
let output = Command::new("which").arg("gh").output();
if output.is_err() || !output.as_ref().unwrap().status.success() {
return GhCliStatus::NotInstalled;
}
let output = Command::new("gh").args(["auth", "status"]).output();
if let Ok(output) = output {
if output.status.success() {
return GhCliStatus::Authenticated;
}
}
GhCliStatus::NotAuthenticated
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GhCliStatus {
NotInstalled,
NotAuthenticated,
Authenticated,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validate_token_format_valid() {
assert!(validate_token_format("ghp_1234567890abcdefghij").is_ok());
assert!(validate_token_format("gho_1234567890abcdefghij").is_ok());
assert!(validate_token_format("ghs_1234567890abcdefghij").is_ok());
}
#[test]
fn test_validate_token_format_invalid() {
assert!(validate_token_format("").is_err());
assert!(validate_token_format("invalid").is_err());
assert!(validate_token_format("ghp_123").is_err()); assert!(validate_token_format("xyz_1234567890abcdefghij").is_err());
}
#[test]
fn test_gh_cli_status() {
let status = check_gh_cli_status();
assert!(matches!(
status,
GhCliStatus::NotInstalled | GhCliStatus::NotAuthenticated | GhCliStatus::Authenticated
));
}
#[test]
fn test_discover_token_doesnt_panic() {
let _ = discover_token();
}
}