use crate::{CliEnvironment, RuntimeError};
const AUTHENTICATED_STATUS_NEXT: &str =
"run logbrew releases or logbrew logs --release <release> --environment <environment>";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct AuthSnapshot {
pub(crate) authenticated: bool,
pub(crate) source: &'static str,
pub(crate) label: &'static str,
pub(crate) next: &'static str,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct AuthCredential {
pub(crate) token: String,
pub(crate) source: &'static str,
pub(crate) label: &'static str,
}
pub(crate) fn resolve_credential(env: &CliEnvironment) -> Result<AuthCredential, RuntimeError> {
if let Some(token) = env.token.as_ref().filter(|token| !token.trim().is_empty()) {
return Ok(AuthCredential {
token: token.clone(),
source: AuthSource::Env.key(),
label: AuthSource::Env.human_label(),
});
}
let Some(home) = &env.home else {
return Err(RuntimeError::MissingToken);
};
Ok(AuthCredential {
token: read_token_from_home(home)?,
source: AuthSource::TokenFile.key(),
label: AuthSource::TokenFile.human_label(),
})
}
pub(crate) fn open_browser(url: &str) -> bool {
let command = if cfg!(target_os = "macos") {
("open", vec![url])
} else if cfg!(target_os = "windows") {
("rundll32", vec!["url.dll,FileProtocolHandler", url])
} else {
("xdg-open", vec![url])
};
std::process::Command::new(command.0)
.args(command.1)
.status()
.is_ok_and(|status| status.success())
}
pub(crate) fn write_status_success<W: std::io::Write>(
env: &CliEnvironment,
status_code: u16,
body: &str,
json: bool,
output: &mut W,
) -> Result<(), RuntimeError> {
let auth_status = inspect_auth_snapshot(env)?;
if json {
let mut response = serde_json::json!({
"ok": true,
"status": "reachable",
"status_code": status_code,
"body": body,
"api_url": env.base_url,
"authenticated": auth_status.authenticated,
"auth_source": auth_status.source,
"next": auth_status.next,
});
if auth_status.authenticated {
if let Some(object) = response.as_object_mut() {
drop(object.insert("agent_use".to_owned(), authenticated_agent_use()));
}
}
writeln!(output, "{response}")?;
} else {
writeln!(output, "LogBrew API reachable.")?;
writeln!(output, "API: {}", env.base_url)?;
writeln!(output, "Auth: {}", auth_status.label)?;
if auth_status.authenticated {
write_authenticated_agent_use_prompt(output)?;
}
writeln!(output, "Next: {}", auth_status.next)?;
}
Ok(())
}
fn authenticated_agent_use() -> serde_json::Value {
serde_json::json!({
"prompt": "How should your AI use LogBrew?",
"default": "on_request",
"options": [
{
"id": "on_request",
"label": "Check only when requested",
"token_use": "lower",
"available": true,
"description": "Your AI runs LogBrew commands when you ask."
},
{
"id": "keep_watching",
"label": "Keep watching this session",
"token_use": "higher",
"available": true,
"description": "Your AI watches new events/logs until stopped.",
"command": "logbrew watch --json"
},
{
"id": "watch_errors_critical",
"label": "Watch only errors and critical issues",
"token_use": "moderate",
"available": true,
"description": "Your AI ignores lower-severity logs/events.",
"command": "logbrew watch --severity error,critical --json"
}
]
})
}
fn write_authenticated_agent_use_prompt<W: std::io::Write>(output: &mut W) -> std::io::Result<()> {
writeln!(output, "LogBrew is connected. How should your AI use it?")?;
writeln!(output)?;
writeln!(output, "1. Check only when requested")?;
writeln!(
output,
" Lower token use. Your AI runs LogBrew commands when you ask."
)?;
writeln!(output)?;
writeln!(output, "2. Keep watching this session")?;
writeln!(
output,
" Higher token use. Your AI watches new events/logs until stopped."
)?;
writeln!(output, " Command: logbrew watch --json")?;
writeln!(output)?;
writeln!(output, "3. Watch only errors and critical issues")?;
writeln!(
output,
" Moderate token use. Your AI ignores lower-severity logs/events."
)?;
writeln!(
output,
" Command: logbrew watch --severity error,critical --json"
)?;
writeln!(output)?;
Ok(())
}
pub(crate) fn write_logout_result<W: std::io::Write>(
env: &CliEnvironment,
json: bool,
output: &mut W,
) -> Result<(), RuntimeError> {
let env_token_active = env
.token
.as_ref()
.is_some_and(|token| !token.trim().is_empty());
let removed = remove_token_from_home(env.home.as_deref())?;
let auth_source = if env_token_active {
"env"
} else if removed {
"token_file"
} else {
"missing"
};
let next = logout_next_step(env_token_active, removed);
if json {
let response = serde_json::json!({
"ok": true,
"removed": removed,
"auth_source": auth_source,
"env_token_active": env_token_active,
"next": next,
});
writeln!(output, "{response}")?;
} else if env_token_active {
if removed {
writeln!(output, "Local LogBrew token removed.")?;
} else {
writeln!(output, "No local LogBrew token found.")?;
}
writeln!(output, "Auth: env token still active")?;
writeln!(output, "Next: {next}")?;
} else if removed {
writeln!(output, "Logged out of LogBrew.")?;
writeln!(output, "Removed: local token")?;
writeln!(output, "Next: {next}")?;
} else {
writeln!(output, "No local LogBrew token found.")?;
writeln!(output, "Next: {next}")?;
}
Ok(())
}
pub(crate) fn inspect_auth_snapshot(env: &CliEnvironment) -> Result<AuthSnapshot, RuntimeError> {
let status = inspect_auth_status(env)?;
Ok(AuthSnapshot {
authenticated: status.is_authenticated(),
source: status.source_key(),
label: status.human_label(),
next: status.next_step(),
})
}
fn inspect_auth_status(env: &CliEnvironment) -> Result<AuthStatus, RuntimeError> {
if env
.token
.as_ref()
.is_some_and(|token| !token.trim().is_empty())
{
return Ok(AuthStatus::Configured(AuthSource::Env));
}
let Some(home) = &env.home else {
return Ok(AuthStatus::Missing);
};
match read_token_from_home(home) {
Ok(_) => Ok(AuthStatus::Configured(AuthSource::TokenFile)),
Err(RuntimeError::MissingToken) => Ok(AuthStatus::Missing),
Err(error) => Err(error),
}
}
fn remove_token_from_home(home: Option<&std::path::Path>) -> Result<bool, RuntimeError> {
let Some(home) = home else {
return Ok(false);
};
let token_path = home.join(".logbrew").join("token");
match std::fs::remove_file(token_path) {
Ok(()) => Ok(true),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(false),
Err(error) => Err(RuntimeError::Io(error)),
}
}
const fn logout_next_step(env_token_active: bool, removed: bool) -> &'static str {
if env_token_active {
"unset LOGBREW_TOKEN to fully log out"
} else if removed {
"run logbrew login to authenticate again"
} else {
"run logbrew login to authenticate"
}
}
fn read_token_from_home(home: &std::path::Path) -> Result<String, RuntimeError> {
let token_path = home.join(".logbrew").join("token");
let token = match std::fs::read_to_string(token_path) {
Ok(token) => token,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
return Err(RuntimeError::MissingToken);
}
Err(error) => return Err(RuntimeError::Io(error)),
};
let trimmed = token.trim();
if trimmed.is_empty() {
Err(RuntimeError::MissingToken)
} else {
Ok(trimmed.to_owned())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum AuthStatus {
Configured(AuthSource),
Missing,
}
impl AuthStatus {
const fn is_authenticated(self) -> bool {
matches!(self, Self::Configured(_))
}
const fn source_key(self) -> &'static str {
match self {
Self::Configured(source) => source.key(),
Self::Missing => "missing",
}
}
const fn human_label(self) -> &'static str {
match self {
Self::Configured(source) => source.human_label(),
Self::Missing => "not logged in",
}
}
const fn next_step(self) -> &'static str {
match self {
Self::Configured(_) => AUTHENTICATED_STATUS_NEXT,
Self::Missing => "run logbrew login",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum AuthSource {
Env,
TokenFile,
}
impl AuthSource {
const fn key(self) -> &'static str {
match self {
Self::Env => "env",
Self::TokenFile => "token_file",
}
}
const fn human_label(self) -> &'static str {
match self {
Self::Env => "logged in (env token)",
Self::TokenFile => "logged in (local token)",
}
}
}