Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 86 additions & 2 deletions src/commands/whoami.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,29 @@
std::process::exit(0);
}

if !args.is_empty() {
eprintln!("Error: unknown whoami argument(s): {}", args.join(" "));
let short = args.iter().any(|arg| arg.as_str() == "--short" || arg.as_str() == "-s");

Check notice on line 14 in src/commands/whoami.rs

View check run for this annotation

Autter.dev / autter/review-gate

💡 Suggestion · Missing linked tracker issue

This PR's title and body do not reference any tracker issue (GitHub `#123`, Jira/Linear `KEY-123`, or `Fixes/Closes/Resolves`). **References:** - https://www.conventionalcommits.org/en/v1.0.0/ — A structured commit and PR title format that tooling can parse. - https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue — Link the issue a PR resolves so context and closing are automatic. Suggested fix: In the PR description, add a reference to the tracker issue this change implements (GitHub `#123`, Jira/Linear `PROJ-456`, or a `Fixes/Closes/Resolves` marker). Reviewers need that context to understand why this change exists and what success looks like. Why it matters: reviewers and on-call engineers need the linked issue to understand the why behind a change months from now.
let unknown: Vec<&str> = args
.iter()
.map(|arg| arg.as_str())
.filter(|arg| *arg != "--short" && *arg != "-s")
.collect();
if !unknown.is_empty() {
eprintln!("Error: unknown whoami argument(s): {}", unknown.join(" "));
print_help();
std::process::exit(crate::commands::EXIT_USAGE_ERROR);
}

if short {
let auth = collect_auth_status();
let has_api_key = ApiContext::new(None).api_key.is_some();
let org_slug = auth.orgs.first().and_then(|org| org.org_slug.as_deref());
println!("{}", short_identity(&auth.state, auth.email.as_deref(), org_slug));
if identity_failed(&auth.state, has_api_key) {
std::process::exit(1);
}
return;
}

// Use Config::fresh() to support runtime config updates (daemon mode)
let api_base_url = config::Config::fresh().api_base_url().to_string();
let auth = collect_auth_status();
Expand Down Expand Up @@ -134,10 +151,77 @@
format!("{}...{}", prefix, suffix)
}

/// The auth state in a few words, for `autter whoami --short`.
fn state_label(state: &AuthState) -> String {
match state {
AuthState::LoggedOut => "logged out".to_string(),
AuthState::LoggedIn => "logged in".to_string(),
AuthState::SyncBlocked => "cloud sync blocked".to_string(),
AuthState::RefreshExpired => "credentials expired".to_string(),
AuthState::Error(err) => format!("error: {}", err),

Check notice on line 161 in src/commands/whoami.rs

View check run for this annotation

Autter.dev / autter/review-gate

💡 Suggestion · Broken invariant

Assumes: `autter whoami --short` always prints exactly one line of the form `<email> (<org>) <state>`. Counterexample (boundary input): collect_auth_status returns AuthState::Error containing an embedded newline, e.g. a multi-line keychain or credential-store error message → state_label interpolates the raw error text, so the output spans multiple lines and breaks the one-line contract that scripts and shell prompts rely on. Nothing in short_identity or state_label sanitizes or truncates the message. Blast radius — this counterexample can fail the downstream usage that depends on this file: functions `handle_whoami`, `mask_api_key`, `print_help`; dependent files `crate::api::client::ApiContext`, `crate::auth::{AuthState, collect_auth_status, format_unix_timestamp}`, `crate::config`. Suggested fix: Handle the boundary input case at src/commands/whoami.rs:161. collect_auth_status returns AuthState::Error containing an embedded newline, e.g. a multi-line keychain or credential-store error message currently state_label interpolates the raw error text, so the output spans multiple lines and breaks the one-line contract that scripts and shell prompts rely on. Nothing in short_identity or state_label sanitizes or truncates the message. — add the guard / idempotency / transaction / bound that makes this case safe, then confirm the assumption "`autter whoami --short` always prints exactly one line of the form `<email> (<org>) <state>`" holds. Blast radius — this counterexample can fail the downstream usage that depends on this file: functions `handle_whoami`, `mask_api_key`, `print_help`; dependent files `crate::api::client::ApiContext`, `crate::auth::{AuthState, collect_auth_status, format_unix_timestamp}`, `crate::config`.
}
}

/// One line for scripts and shell prompts: `<email> (<org>) <state>`.
fn short_identity(state: &AuthState, email: Option<&str>, org_slug: Option<&str>) -> String {
let mut parts: Vec<String> = Vec::new();
if let Some(email) = email {
parts.push(email.to_string());
}
if let Some(org) = org_slug {
parts.push(format!("({})", org));
}
parts.push(state_label(state));
parts.join(" ")
}

/// Same rule as the full report: a session that is not logged in fails
/// unless an API key is configured.
fn identity_failed(state: &AuthState, has_api_key: bool) -> bool {
*state != AuthState::LoggedIn && !has_api_key
}

fn print_help() {
eprintln!("autter whoami - Show current auth state and identity");
eprintln!();
eprintln!("Usage:");
eprintln!(" autter whoami");
eprintln!(" autter whoami --short One line: <email> (<org>) <state>");
eprintln!(" autter whoami --help");
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn short_identity_names_the_user_org_and_state() {
assert_eq!(
short_identity(&AuthState::LoggedIn, Some("ada@example.com"), Some("acme")),
"ada@example.com (acme) logged in"
);
assert_eq!(
short_identity(&AuthState::RefreshExpired, Some("ada@example.com"), None),
"ada@example.com credentials expired"
);
assert_eq!(short_identity(&AuthState::LoggedOut, None, None), "logged out");
assert_eq!(
short_identity(&AuthState::Error("keychain locked".to_string()), None, None),
"error: keychain locked"
);
}

#[test]
fn identity_fails_only_without_a_login_or_an_api_key() {
assert!(!identity_failed(&AuthState::LoggedIn, false));
assert!(identity_failed(&AuthState::LoggedOut, false));
assert!(!identity_failed(&AuthState::LoggedOut, true));
assert!(identity_failed(&AuthState::SyncBlocked, false));
}

#[test]
fn api_keys_are_masked() {
assert_eq!(mask_api_key("short"), "*****");
assert_eq!(mask_api_key("abcd1234efgh5678"), "abcd...5678");
}
}
Loading