use crate::ai::exec_path;
use crate::ai::types::AiError;
use std::process::{Command, Stdio};
use std::time::{Duration, Instant};
const LOGIN_TIMEOUT: Duration = Duration::from_secs(300);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Login {
Cli(LoginCommand),
Acp { account: &'static str, method: &'static str },
}
impl Login {
pub fn account(&self) -> &'static str {
match self {
Login::Cli(command) => command.account,
Login::Acp { account, .. } => account,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct LoginCommand {
pub program: &'static str,
pub args: &'static [&'static str],
pub account: &'static str,
}
pub fn for_agent(name: &str) -> Option<Login> {
match name {
"claude" => Some(Login::Cli(LoginCommand { program: "claude", args: &["auth", "login"], account: "Claude" })),
"codex" => Some(Login::Acp { account: "ChatGPT", method: "chatgpt" }),
_ => None,
}
}
fn last_line(bytes: &[u8]) -> String {
String::from_utf8_lossy(bytes)
.lines()
.rev()
.map(str::trim)
.find(|line| !line.is_empty())
.unwrap_or_default()
.to_owned()
}
pub fn run_cli(command: &LoginCommand) -> Result<String, AiError> {
let mut child = Command::new(exec_path::resolve(command.program))
.args(command.args)
.env("PATH", exec_path::child_path())
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|error| {
AiError::Transport(format!(
"no se encontró «{}» para iniciar sesión en {}: {error}",
command.program, command.account
))
})?;
let started = Instant::now();
let status = loop {
match child.try_wait().map_err(|error| AiError::Transport(error.to_string()))? {
Some(status) => break status,
None if started.elapsed() > LOGIN_TIMEOUT => {
let _ = child.kill();
let _ = child.wait();
return Err(AiError::Transport("se agotó el tiempo para iniciar sesión en el navegador".into()));
}
None => std::thread::sleep(Duration::from_millis(200)),
}
};
let output = child.wait_with_output().map_err(|error| AiError::Transport(error.to_string()))?;
if status.success() {
Ok(last_line(&output.stdout))
} else {
Err(AiError::Transport(format!("el inicio de sesión falló: {}", last_line(&output.stderr))))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn conoce_las_dos_suscripciones_y_nada_mas() {
assert!(matches!(for_agent("claude"), Some(Login::Cli(command)) if command.args == ["auth", "login"]));
assert_eq!(for_agent("codex"), Some(Login::Acp { account: "ChatGPT", method: "chatgpt" }));
assert_eq!(for_agent("codex").unwrap().account(), "ChatGPT");
assert!(for_agent("otro").is_none());
}
#[test]
fn el_exito_devuelve_la_ultima_linea_y_el_fallo_el_error() {
let ok = LoginCommand { program: "sh", args: &["-c", "echo uno; echo listo"], account: "X" };
assert_eq!(run_cli(&ok).unwrap(), "listo");
let fail = LoginCommand { program: "sh", args: &["-c", "echo no puedo >&2; exit 3"], account: "X" };
assert!(run_cli(&fail).unwrap_err().to_string().contains("no puedo"));
let missing = LoginCommand { program: "aura-no-existe-xyz", args: &[], account: "ChatGPT" };
assert!(run_cli(&missing).unwrap_err().to_string().contains("ChatGPT"));
}
}