use std::time::Duration;
use dbmd_core::harness::{ant, auth, oauth};
use crate::cli::{LoginArgs, LogoutArgs};
use crate::context::Context;
use crate::error::{CliError, CliResult, ExitCode};
const CODEX: &str = "codex";
const ANTHROPIC: &str = "anthropic";
fn config_error(error: dbmd_core::harness::HarnessError) -> CliError {
CliError::new(ExitCode::Runtime, "LOGIN_FAILED", error.to_string())
}
fn unknown_provider(name: &str) -> CliError {
CliError::new(
ExitCode::Runtime,
"LOGIN_UNKNOWN_PROVIDER",
format!("`{name}` has no native login"),
)
.with_hint(
"logins are `dbmd login codex` (a ChatGPT Plus/Pro subscription, \
native PKCE) and `dbmd login anthropic` (runs Anthropic's own `ant \
auth login`). A Copilot subscription, or a Claude subscription you \
would rather drive through its own agent, is reached by delegating \
to that CLI: `dbmd ask … --provider claude-code`",
)
}
const CALLBACK_TIMEOUT: Duration = Duration::from_secs(180);
pub fn run(ctx: &Context, args: &LoginArgs) -> CliResult {
if args.status {
let providers = auth::logged_in_providers().map_err(config_error)?;
if ctx.json {
println!(
"{}",
serde_json::json!({
"logged_in": providers,
"anthropic": anthropic_status(),
"credentials": auth::auth_path().map_err(config_error)?.to_string_lossy(),
})
);
} else if providers.is_empty() && anthropic_status().is_none() {
println!("not signed in to any provider");
} else {
for provider in &providers {
let detail = auth::peek(provider)
.ok()
.flatten()
.and_then(|tokens| oauth::plan_type(&tokens.access))
.map(|plan| format!(" ({plan})"))
.unwrap_or_default();
println!("signed in: {provider}{detail}");
}
if let Some(line) = anthropic_status() {
println!("signed in: anthropic — {line}");
}
}
return Ok(());
}
let provider = args.provider.as_deref().unwrap_or(CODEX);
if provider == ANTHROPIC {
return anthropic_login(ctx, args);
}
if provider != CODEX {
return Err(unknown_provider(provider));
}
let pkce = oauth::generate_pkce().map_err(config_error)?;
let state = oauth::create_state().map_err(config_error)?;
let url = oauth::authorize_url(&pkce.challenge, &state);
let code = if args.code {
eprintln!("Open this URL, sign in, then paste the result below:\n\n{url}\n");
eprint!("code (or the full redirect URL): ");
use std::io::Write as _;
let _ = std::io::stderr().flush();
let mut line = String::new();
std::io::stdin()
.read_line(&mut line)
.map_err(|error| CliError::new(ExitCode::Runtime, "LOGIN_FAILED", error.to_string()))?;
let (code, pasted_state) = oauth::parse_authorization_input(&line);
if let Some(pasted_state) = pasted_state {
if pasted_state != state {
return Err(CliError::new(
ExitCode::Runtime,
"LOGIN_STATE_MISMATCH",
"the pasted state does not match this login attempt",
)
.with_hint("run `dbmd login codex` again and use the newest URL"));
}
}
code.ok_or_else(|| {
CliError::new(
ExitCode::Runtime,
"LOGIN_FAILED",
"no authorization code was pasted",
)
})?
} else {
eprintln!("Opening your browser to sign in to ChatGPT…");
eprintln!("If it does not open, visit:\n\n{url}\n");
let waiting = std::thread::spawn({
let state = state.clone();
move || oauth::wait_for_callback(&state, CALLBACK_TIMEOUT)
});
std::thread::sleep(Duration::from_millis(150));
if !oauth::open_browser(&url) {
eprintln!("(could not launch a browser — open the URL above yourself)");
}
let outcome = waiting.join().map_err(|_| {
CliError::new(
ExitCode::Runtime,
"LOGIN_FAILED",
"the callback listener panicked",
)
})?;
outcome.map_err(config_error)?.ok_or_else(|| {
CliError::new(
ExitCode::Runtime,
"LOGIN_TIMEOUT",
"no authorization arrived within 3 minutes",
)
.with_hint("retry, or use `dbmd login codex --code` to paste the code yourself")
})?
};
let tokens = oauth::exchange_code(&code, &pkce.verifier).map_err(config_error)?;
let plan = oauth::plan_type(&tokens.access);
if oauth::account_id(&tokens.access).is_none() {
return Err(CliError::new(
ExitCode::Runtime,
"LOGIN_NO_ACCOUNT",
"the issued token carries no ChatGPT account id",
)
.with_hint("this account may not have Codex access; check your ChatGPT plan"));
}
auth::save(CODEX, &tokens).map_err(config_error)?;
let path = auth::auth_path().map_err(config_error)?;
if ctx.json {
println!(
"{}",
serde_json::json!({
"logged_in": CODEX,
"plan": plan,
"credentials": path.to_string_lossy(),
})
);
} else {
let plan = plan.map(|plan| format!(" ({plan})")).unwrap_or_default();
println!(
"signed in to ChatGPT{plan} — credentials in {}",
path.display()
);
println!("run: dbmd ask \"…\" --provider codex");
}
Ok(())
}
pub fn run_logout(ctx: &Context, args: &LogoutArgs) -> CliResult {
let provider = args.provider.as_deref().unwrap_or(CODEX);
if provider == ANTHROPIC {
let was_active = anthropic_status().is_some();
ant::logout().map_err(config_error)?;
if ctx.json {
println!(
"{}",
serde_json::json!({ "provider": provider, "removed": was_active })
);
} else if was_active {
println!("signed out of anthropic (`ant auth logout`)");
} else {
println!("not signed in to anthropic");
}
return Ok(());
}
let removed = auth::clear(provider).map_err(config_error)?;
if ctx.json {
println!(
"{}",
serde_json::json!({ "provider": provider, "removed": removed })
);
} else if removed {
println!("signed out of {provider}");
} else {
println!("not signed in to {provider}");
}
Ok(())
}
fn anthropic_status() -> Option<String> {
ant::access_token().ok().flatten()?;
ant::status_line().or_else(|| Some("active profile".to_string()))
}
fn anthropic_login(ctx: &Context, args: &LoginArgs) -> CliResult {
if !ant::installed() {
return Err(CliError::new(
ExitCode::Runtime,
"LOGIN_MISSING_CLI",
"Anthropic's `ant` CLI is not installed",
)
.with_hint(ant::INSTALL_HINT));
}
ant::login(args.code).map_err(config_error)?;
let token = ant::access_token().map_err(config_error)?;
if token.is_none() {
return Err(CliError::new(
ExitCode::Runtime,
"LOGIN_FAILED",
"`ant auth login` finished but no credential is active",
)
.with_hint("run `ant auth status` to see which credential source won"));
}
if ctx.json {
println!(
"{}",
serde_json::json!({ "provider": "anthropic", "status": anthropic_status() })
);
} else {
let detail = anthropic_status().unwrap_or_else(|| "active profile".to_string());
println!("signed in: anthropic — {detail}");
println!("`dbmd ask --provider anthropic` now uses this session.");
}
Ok(())
}