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}