1use miyabi_types::error::{MiyabiError, Result};
7use std::fs;
8use std::process::Command;
9
10pub fn discover_token() -> Result<String> {
33 if let Ok(token) = std::env::var("GITHUB_TOKEN") {
35 if !token.is_empty() && token.starts_with("ghp_") {
36 tracing::debug!("Found GitHub token from GITHUB_TOKEN environment variable");
37 return Ok(token);
38 }
39 }
40
41 if let Ok(token) = get_token_from_gh_cli() {
43 tracing::debug!("Found GitHub token from gh CLI");
44 return Ok(token);
45 }
46
47 if let Ok(token) = get_token_from_gh_config() {
49 tracing::debug!("Found GitHub token from gh config file");
50 return Ok(token);
51 }
52
53 Err(MiyabiError::Auth(
55 "GitHub token not found. Please set up authentication:\n\n\
56 Option 1: Set environment variable\n\
57 \x20 export GITHUB_TOKEN=ghp_your_token_here\n\
58 \x20 # Add to ~/.zshrc or ~/.bashrc for persistence\n\n\
59 Option 2: Use GitHub CLI (recommended)\n\
60 \x20 gh auth login\n\
61 \x20 # Follow the interactive prompts\n\n\
62 Option 3: Create a Personal Access Token\n\
63 \x20 1. Go to https://github.com/settings/tokens\n\
64 \x20 2. Generate new token (classic) with 'repo' scope\n\
65 \x20 3. Set GITHUB_TOKEN environment variable\n\n\
66 For more help, see: https://docs.github.com/en/authentication"
67 .to_string(),
68 ))
69}
70
71fn get_token_from_gh_cli() -> Result<String> {
73 let output = Command::new("gh")
74 .args(["auth", "token"])
75 .output()
76 .map_err(|e| MiyabiError::Auth(format!("Failed to execute gh command: {}", e)))?;
77
78 if output.status.success() {
79 let token = String::from_utf8_lossy(&output.stdout).trim().to_string();
80 if !token.is_empty() && token.starts_with("ghp_") {
81 return Ok(token);
82 }
83 }
84
85 Err(MiyabiError::Auth("gh CLI is available but not authenticated".to_string()))
86}
87
88fn get_token_from_gh_config() -> Result<String> {
90 let config_path = dirs::home_dir()
91 .ok_or_else(|| MiyabiError::Auth("Could not determine home directory".to_string()))?
92 .join(".config")
93 .join("gh")
94 .join("hosts.yml");
95
96 if !config_path.exists() {
97 return Err(MiyabiError::Auth("gh config file not found".to_string()));
98 }
99
100 let content = fs::read_to_string(&config_path)
101 .map_err(|e| MiyabiError::Auth(format!("Failed to read gh config file: {}", e)))?;
102
103 for line in content.lines() {
107 let trimmed = line.trim();
108 if trimmed.starts_with("oauth_token:") {
109 if let Some(token) = trimmed.split(':').nth(1) {
110 let token = token.trim().to_string();
111 if token.starts_with("ghp_") {
112 return Ok(token);
113 }
114 }
115 }
116 }
117
118 Err(MiyabiError::Auth("No oauth_token found in gh config file".to_string()))
119}
120
121pub fn validate_token_format(token: &str) -> Result<()> {
125 if token.is_empty() {
126 return Err(MiyabiError::Auth("Token is empty".to_string()));
127 }
128
129 if !token.starts_with("ghp_") && !token.starts_with("gho_") && !token.starts_with("ghs_") {
130 return Err(MiyabiError::Auth(
131 "Token does not start with expected prefix (ghp_, gho_, or ghs_)".to_string(),
132 ));
133 }
134
135 if token.len() < 20 {
136 return Err(MiyabiError::Auth(
137 "Token is too short (expected at least 20 characters)".to_string(),
138 ));
139 }
140
141 Ok(())
142}
143
144pub fn check_gh_cli_status() -> GhCliStatus {
146 let output = Command::new("which").arg("gh").output();
148 if output.is_err() || !output.as_ref().unwrap().status.success() {
149 return GhCliStatus::NotInstalled;
150 }
151
152 let output = Command::new("gh").args(["auth", "status"]).output();
154 if let Ok(output) = output {
155 if output.status.success() {
156 return GhCliStatus::Authenticated;
157 }
158 }
159
160 GhCliStatus::NotAuthenticated
161}
162
163#[derive(Debug, Clone, Copy, PartialEq, Eq)]
165pub enum GhCliStatus {
166 NotInstalled,
168 NotAuthenticated,
170 Authenticated,
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177
178 #[test]
179 fn test_validate_token_format_valid() {
180 assert!(validate_token_format("ghp_1234567890abcdefghij").is_ok());
181 assert!(validate_token_format("gho_1234567890abcdefghij").is_ok());
182 assert!(validate_token_format("ghs_1234567890abcdefghij").is_ok());
183 }
184
185 #[test]
186 fn test_validate_token_format_invalid() {
187 assert!(validate_token_format("").is_err());
188 assert!(validate_token_format("invalid").is_err());
189 assert!(validate_token_format("ghp_123").is_err()); assert!(validate_token_format("xyz_1234567890abcdefghij").is_err());
191 }
192
193 #[test]
194 fn test_gh_cli_status() {
195 let status = check_gh_cli_status();
197 assert!(matches!(
199 status,
200 GhCliStatus::NotInstalled | GhCliStatus::NotAuthenticated | GhCliStatus::Authenticated
201 ));
202 }
203
204 #[test]
205 fn test_discover_token_doesnt_panic() {
206 let _ = discover_token();
208 }
209}