use std::io::Read;
use aristo_core::auth::{self, derive_repo_full_name, AuthError, ServerUrl, Token};
use crate::{AuthAction, CliError, CliResult};
pub(crate) fn run(action: AuthAction) -> CliResult<()> {
match action {
AuthAction::Login {
stdin,
token,
server,
repo,
} => login(stdin, token, &server, repo),
AuthAction::Status => status(),
AuthAction::Logout => logout(),
}
}
fn login(
read_stdin: bool,
token_flag: Option<String>,
server_spec: &str,
repo_flag: Option<String>,
) -> CliResult<()> {
let server = ServerUrl::parse(server_spec);
if read_stdin || token_flag.is_some() {
return login_with_raw_token(read_stdin, token_flag);
}
login_via_oauth(&server, repo_flag)
}
fn login_via_oauth(server: &ServerUrl, repo_flag: Option<String>) -> CliResult<()> {
let repo_full_name = resolve_repo_full_name(repo_flag)?;
let init = auth::oauth_start(server).map_err(auth_error_to_cli)?;
eprintln!();
eprintln!("Authenticating against {server}");
eprintln!("Scoping token to repo: {repo_full_name}");
eprintln!();
eprintln!("Open this URL to authorize with GitHub:");
eprintln!();
eprintln!(" {}", init.authorize_url);
eprintln!();
let _ = try_open_browser(&init.authorize_url);
eprintln!("After authorizing, the page will display a code. Paste it here:");
let mut line = String::new();
std::io::stdin()
.read_line(&mut line)
.map_err(CliError::Io)?;
let code = line.trim();
if code.is_empty() {
return Err(CliError::Other {
message: "no OAuth code provided. Re-run `aristo auth login` and paste the code from the callback page.".into(),
exit_code: 2,
});
}
let resp = auth::oauth_exchange(server, code, &repo_full_name, Some("aristo-cli"))
.map_err(auth_error_to_cli)?;
let token = Token::new(&resp.arta_token);
let creds = aristo_core::auth::CredentialsRecord {
token,
server: server.clone(),
user_login: Some(resp.user.login.clone()),
user_id: Some(resp.user.id),
repo: Some(resp.repo_full_name.clone()),
};
aristo_core::auth::save_full(&creds).map_err(CliError::Io)?;
let path = auth::credentials_path().map_err(auth_error_to_cli)?;
println!(
"ok: authenticated as {} for {}",
resp.user.login, resp.repo_full_name
);
println!(" token saved to {}", path.display());
println!(" `aristo auth status` to verify; `aristo auth logout` to remove.");
Ok(())
}
fn login_with_raw_token(read_stdin: bool, token_flag: Option<String>) -> CliResult<()> {
let token_raw = collect_raw_token(read_stdin, token_flag)?;
let trimmed = token_raw.trim();
if trimmed.is_empty() {
return Err(CliError::Other {
message: "no token provided.\n\
Get an API token at https://code.aretta.ai/dashboard/settings/tokens, then run\n \
`aristo auth login` (OAuth flow, default),\n \
`aristo auth login --stdin` (pipe), or\n \
`aristo auth login --token <TOKEN>` (scripting)."
.into(),
exit_code: 2,
});
}
let token = Token::new(trimmed);
auth::save(&token).map_err(CliError::Io)?;
let path = auth::credentials_path().map_err(auth_error_to_cli)?;
println!("ok: authenticated. token saved to {}", path.display());
println!(" `aristo auth status` to verify; `aristo auth logout` to remove.");
Ok(())
}
fn collect_raw_token(read_stdin: bool, token_flag: Option<String>) -> CliResult<String> {
if let Some(t) = token_flag {
return Ok(t);
}
if read_stdin {
let mut buf = String::new();
std::io::stdin()
.read_to_string(&mut buf)
.map_err(CliError::Io)?;
return Ok(buf);
}
Err(CliError::Other {
message: "internal: collect_raw_token called without --stdin or --token".into(),
exit_code: 1,
})
}
fn resolve_repo_full_name(repo_flag: Option<String>) -> CliResult<String> {
if let Some(r) = repo_flag {
let trimmed = r.trim();
if trimmed.is_empty() {
return Err(CliError::Other {
message: "--repo must be `owner/repo` (got empty string)".into(),
exit_code: 2,
});
}
if !trimmed.contains('/') {
return Err(CliError::Other {
message: format!("--repo `{trimmed}` is not in `owner/repo` form"),
exit_code: 2,
});
}
return Ok(trimmed.to_string());
}
let cwd = std::env::current_dir().map_err(CliError::Io)?;
derive_repo_full_name(&cwd).map_err(auth_error_to_cli)
}
fn try_open_browser(url: &str) -> std::io::Result<()> {
if std::env::var("ARISTO_NO_BROWSER").is_ok() {
return Ok(());
}
let cmd = if cfg!(target_os = "macos") {
"open"
} else if cfg!(target_os = "windows") {
"start"
} else {
"xdg-open"
};
std::process::Command::new(cmd)
.arg(url)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
.map(|_| ())
}
fn auth_error_to_cli(e: AuthError) -> CliError {
CliError::Other {
message: e.to_string(),
exit_code: 1,
}
}
fn status() -> CliResult<()> {
match aristo_core::auth::resolve_full() {
Ok(creds) => {
let from_env = std::env::var(auth::ENV_VAR)
.ok()
.is_some_and(|v| !v.trim().is_empty());
if from_env {
println!(
"ok: authenticated via {} environment variable.",
auth::ENV_VAR
);
println!(" (env var takes precedence over the on-disk credentials file.)");
} else {
let path = auth::credentials_path().map_err(|e| CliError::Other {
message: format!("couldn't resolve credentials path: {e}"),
exit_code: 1,
})?;
println!("ok: authenticated via {}", path.display());
}
println!(" server: {}", creds.server);
if let Some(login) = &creds.user_login {
println!(" user: {login}");
}
if let Some(repo) = &creds.repo {
println!(" repo: {repo}");
}
Ok(())
}
Err(AuthError::NoToken) => {
println!("not authenticated.");
println!(
" Run `aristo auth login` to log in, or set the {} env var for CI.",
auth::ENV_VAR
);
Ok(())
}
Err(AuthError::Invalid) => {
Err(CliError::Other {
message: "stored token was rejected by the server. \
Run `aristo auth login` to refresh."
.into(),
exit_code: 1,
})
}
Err(AuthError::Malformed(msg)) => Err(CliError::Other {
message: format!(
"credentials file is malformed: {msg}\n \
Run `aristo auth logout` then `aristo auth login` to re-create it."
),
exit_code: 1,
}),
}
}
fn logout() -> CliResult<()> {
let path = auth::credentials_path().map_err(|e| CliError::Other {
message: format!("couldn't resolve credentials path: {e}"),
exit_code: 1,
})?;
let existed = path.exists();
auth::clear().map_err(CliError::Io)?;
if existed {
println!(
"ok: logged out. credentials cleared from {}",
path.display()
);
} else {
println!("ok: not logged in (no credentials to clear).");
}
if std::env::var(auth::ENV_VAR).is_ok() {
println!(
" note: {} is set in the environment; canon calls will still use it.",
auth::ENV_VAR
);
}
Ok(())
}