1pub mod gh_cli;
7pub mod ssh;
8
9use crate::config::WorkspaceProvider;
10use crate::errors::AppError;
11use tracing::{debug, warn};
12
13#[derive(Debug, Clone)]
15pub struct AuthResult {
16 pub token: String,
18 pub method: ResolvedAuthMethod,
20 pub username: Option<String>,
22}
23
24#[derive(Debug, Clone, PartialEq, Eq)]
26pub enum ResolvedAuthMethod {
27 GhCli,
29}
30
31impl std::fmt::Display for ResolvedAuthMethod {
32 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33 match self {
34 ResolvedAuthMethod::GhCli => write!(f, "GitHub CLI"),
35 }
36 }
37}
38
39pub fn get_auth() -> Result<AuthResult, AppError> {
43 debug!("Resolving authentication via gh CLI");
44
45 let gh_installed = gh_cli::is_installed();
46 let gh_authenticated = gh_installed && gh_cli::is_authenticated();
47 debug!(gh_installed, gh_authenticated, "Checking GitHub CLI status");
48
49 if gh_installed && gh_authenticated {
50 match gh_cli::get_token() {
51 Ok(token) => {
52 let username = gh_cli::get_username().ok();
53 debug!(
54 username = username.as_deref().unwrap_or("<unknown>"),
55 "Authenticated via GitHub CLI"
56 );
57 return Ok(AuthResult {
58 token,
59 method: ResolvedAuthMethod::GhCli,
60 username,
61 });
62 }
63 Err(e) => {
64 warn!(error = %e, "gh CLI token retrieval failed");
65 }
66 }
67 }
68
69 let ssh_note = if ssh::has_ssh_keys() {
70 "\n\nNote: SSH keys detected. While SSH keys work for git clone/push,\n\
71 you still need a provider API token for repository discovery.\n\
72 The SSH keys will be used automatically for cloning."
73 } else {
74 ""
75 };
76
77 Err(AppError::auth(format!(
78 "No authentication found.\n\n\
79 Please authenticate using the GitHub CLI:\n\n\
80 For GitHub.com: gh auth login\n\
81 For GitHub Enterprise: gh auth login --hostname <your-host>\n\
82 {}\n\
83 Install from: https://cli.github.com/",
84 ssh_note
85 )))
86}
87
88pub fn get_auth_for_provider(provider: &WorkspaceProvider) -> Result<AuthResult, AppError> {
90 debug!(
91 api_url = provider.api_url.as_deref().unwrap_or("default"),
92 "Resolving authentication for provider"
93 );
94
95 if let Some(api_url) = &provider.api_url {
97 if let Some(host) = extract_host(api_url) {
98 if host != "api.github.com" {
99 debug!(host, "Attempting GitHub Enterprise authentication");
100 if let Ok(token) = gh_cli::get_token_for_host(&host) {
101 debug!(host, "Authenticated via gh CLI for enterprise host");
102 return Ok(AuthResult {
103 token,
104 method: ResolvedAuthMethod::GhCli,
105 username: None,
106 });
107 }
108 }
109 }
110 }
111
112 if !gh_cli::is_installed() {
113 debug!("gh CLI not installed");
114 return Err(AppError::auth(
115 "GitHub CLI is not installed. Install from https://cli.github.com/",
116 ));
117 }
118 if !gh_cli::is_authenticated() {
119 debug!("gh CLI not authenticated");
120 return Err(AppError::auth(
121 "GitHub CLI is not authenticated. Run: gh auth login",
122 ));
123 }
124
125 let token = gh_cli::get_token()?;
126 let username = gh_cli::get_username().ok();
127 debug!(
128 username = username.as_deref().unwrap_or("<unknown>"),
129 "Authenticated via gh CLI"
130 );
131
132 Ok(AuthResult {
133 token,
134 method: ResolvedAuthMethod::GhCli,
135 username,
136 })
137}
138
139fn extract_host(url: &str) -> Option<String> {
141 let without_scheme = url.split_once("://").map(|(_, rest)| rest).unwrap_or(url);
142 let host = without_scheme.split('/').next()?;
143 if host.is_empty() {
144 return None;
145 }
146 Some(host.to_string())
147}
148
149#[cfg(test)]
150#[path = "mod_tests.rs"]
151mod tests;