Skip to main content

codex_cli/auth/
login.rs

1use anyhow::Result;
2use nils_common::process as shared_process;
3use serde_json::json;
4
5use crate::auth::output::{self, AuthLoginResult};
6
7#[derive(Copy, Clone, Debug, Eq, PartialEq)]
8enum LoginMethod {
9    ChatgptBrowser,
10    ChatgptDeviceCode,
11    ApiKey,
12}
13
14pub fn run(api_key: bool, device_code: bool) -> Result<i32> {
15    run_with_json(api_key, device_code, false)
16}
17
18pub fn run_with_json(api_key: bool, device_code: bool, output_json: bool) -> Result<i32> {
19    let method = match resolve_method(api_key, device_code) {
20        Ok(method) => method,
21        Err((code, message, details)) => {
22            if output_json {
23                output::emit_error("auth login", "invalid-usage", message, details)?;
24            } else {
25                eprintln!("{message}");
26            }
27            return Ok(code);
28        }
29    };
30
31    let args = method.codex_args();
32    if output_json {
33        let proc = match shared_process::run_output("codex", &args) {
34            Ok(output) => output,
35            Err(_) => {
36                output::emit_error(
37                    "auth login",
38                    "login-exec-failed",
39                    format!(
40                        "codex-login: failed to run codex login for method {}",
41                        method.as_str()
42                    ),
43                    Some(json!({
44                        "method": method.as_str(),
45                    })),
46                )?;
47                return Ok(1);
48            }
49        };
50
51        if !proc.status.success() {
52            output::emit_error(
53                "auth login",
54                "login-failed",
55                format!("codex-login: login failed for method {}", method.as_str()),
56                Some(json!({
57                    "method": method.as_str(),
58                    "exit_code": proc.status.code(),
59                })),
60            )?;
61            return Ok(proc.status.code().unwrap_or(1).max(1));
62        }
63
64        output::emit_result(
65            "auth login",
66            AuthLoginResult {
67                method: method.as_str().to_string(),
68                provider: method.provider().to_string(),
69                completed: true,
70            },
71        )?;
72        return Ok(0);
73    }
74
75    let status = match shared_process::run_status_inherit("codex", &args) {
76        Ok(status) => status,
77        Err(_) => {
78            eprintln!(
79                "codex-login: failed to run codex login for method {}",
80                method.as_str()
81            );
82            return Ok(1);
83        }
84    };
85
86    if !status.success() {
87        eprintln!("codex-login: login failed for method {}", method.as_str());
88        return Ok(status.code().unwrap_or(1).max(1));
89    }
90
91    println!("codex: login complete (method: {})", method.as_str());
92    Ok(0)
93}
94
95fn resolve_method(
96    api_key: bool,
97    device_code: bool,
98) -> std::result::Result<LoginMethod, ErrorTriplet> {
99    if api_key && device_code {
100        return Err((
101            64,
102            "codex-login: --api-key cannot be combined with --device-code".to_string(),
103            Some(json!({
104                "api_key": true,
105                "device_code": true,
106            })),
107        ));
108    }
109
110    if api_key {
111        return Ok(LoginMethod::ApiKey);
112    }
113    if device_code {
114        return Ok(LoginMethod::ChatgptDeviceCode);
115    }
116    Ok(LoginMethod::ChatgptBrowser)
117}
118
119type ErrorTriplet = (i32, String, Option<serde_json::Value>);
120
121impl LoginMethod {
122    fn as_str(self) -> &'static str {
123        match self {
124            Self::ChatgptBrowser => "chatgpt-browser",
125            Self::ChatgptDeviceCode => "chatgpt-device-code",
126            Self::ApiKey => "api-key",
127        }
128    }
129
130    fn provider(self) -> &'static str {
131        match self {
132            Self::ChatgptBrowser | Self::ChatgptDeviceCode => "chatgpt",
133            Self::ApiKey => "openai-api",
134        }
135    }
136
137    fn codex_args(self) -> Vec<&'static str> {
138        match self {
139            Self::ChatgptBrowser => vec!["login"],
140            Self::ChatgptDeviceCode => vec!["login", "--device-auth"],
141            Self::ApiKey => vec!["login", "--with-api-key"],
142        }
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::{LoginMethod, resolve_method};
149    use pretty_assertions::assert_eq;
150
151    #[test]
152    fn resolve_method_defaults_to_chatgpt_browser() {
153        assert_eq!(
154            resolve_method(false, false).expect("method"),
155            LoginMethod::ChatgptBrowser
156        );
157    }
158
159    #[test]
160    fn resolve_method_selects_device_code_and_api_key() {
161        assert_eq!(
162            resolve_method(false, true).expect("method"),
163            LoginMethod::ChatgptDeviceCode
164        );
165        assert_eq!(
166            resolve_method(true, false).expect("method"),
167            LoginMethod::ApiKey
168        );
169    }
170
171    #[test]
172    fn resolve_method_rejects_conflicting_flags() {
173        let err = resolve_method(true, true).expect_err("conflict should fail");
174        assert_eq!(err.0, 64);
175        assert!(err.1.contains("--api-key"));
176    }
177}