use core::fmt::Write as _;
use unilang::data::{ ErrorCode, ErrorData, OutputData };
use unilang::interpreter::ExecutionContext;
use unilang::semantic::VerifiedCommand;
use unilang::types::Value;
use crate::output::{ OutputFormat, OutputOptions, json_escape, format_duration_secs };
fn require_nonempty_string_arg( cmd : &VerifiedCommand, name : &str ) -> Result< String, ErrorData >
{
let val = match cmd.arguments.get( name )
{
Some( Value::String( s ) ) => s.clone(),
_ => return Err( ErrorData::new( ErrorCode::ArgumentMissing, format!( "{name}:: is required" ) ) ),
};
if val.is_empty()
{
return Err( ErrorData::new( ErrorCode::ArgumentMissing, format!( "{name}:: value cannot be empty" ) ) );
}
Ok( val )
}
fn is_dry( cmd : &VerifiedCommand ) -> bool
{
matches!( cmd.arguments.get( "dry" ), Some( Value::Boolean( true ) ) )
}
fn token_status_from_ms( expires_at_ms : u64 ) -> crate::token::TokenStatus
{
use std::time::{ SystemTime, UNIX_EPOCH };
let now_ms = u64::try_from(
SystemTime::now()
.duration_since( UNIX_EPOCH )
.unwrap_or_default()
.as_millis()
).unwrap_or( u64::MAX );
if now_ms >= expires_at_ms
{
crate::token::TokenStatus::Expired
}
else
{
let remaining = core::time::Duration::from_millis( expires_at_ms - now_ms );
if remaining.as_secs() <= crate::token::WARNING_THRESHOLD_SECS
{
crate::token::TokenStatus::ExpiringSoon { expires_in : remaining }
}
else
{
crate::token::TokenStatus::Valid { expires_in : remaining }
}
}
}
fn require_claude_paths() -> Result< crate::ClaudePaths, ErrorData >
{
match std::env::var( "HOME" )
{
Ok( home ) if !home.is_empty() =>
{
crate::ClaudePaths::new().ok_or_else( || ErrorData::new(
ErrorCode::InternalError,
"HOME environment variable not set".to_string(),
) )
}
_ => Err( ErrorData::new( ErrorCode::InternalError, "HOME environment variable not set".to_string() ) ),
}
}
fn io_err_to_error_data( e : &std::io::Error, context : &str ) -> ErrorData
{
let code = match e.kind()
{
std::io::ErrorKind::InvalidInput | std::io::ErrorKind::PermissionDenied =>
ErrorCode::ArgumentTypeMismatch,
_ =>
ErrorCode::InternalError,
};
ErrorData::new( code, format!( "{context}: {e}" ) )
}
fn read_live_cred_meta( paths : &crate::ClaudePaths ) -> ( String, String, String, String )
{
let creds = std::fs::read_to_string( paths.credentials_file() ).unwrap_or_default();
let sub = crate::account::parse_string_field( &creds, "subscriptionType" )
.filter( | s | !s.is_empty() )
.unwrap_or_else( || "N/A".to_string() );
let tier = crate::account::parse_string_field( &creds, "rateLimitTier" )
.filter( | s | !s.is_empty() )
.unwrap_or_else( || "N/A".to_string() );
let cj = std::fs::read_to_string( paths.base().join( ".claude.json" ) ).unwrap_or_default();
let email = crate::account::parse_string_field( &cj, "emailAddress" )
.filter( | s | !s.is_empty() )
.unwrap_or_else( || "N/A".to_string() );
let org = crate::account::parse_string_field( &cj, "organizationName" )
.filter( | s | !s.is_empty() )
.unwrap_or_else( || "N/A".to_string() );
( sub, tier, email, org )
}
#[ allow( clippy::needless_pass_by_value, clippy::missing_inline_in_public_items ) ]
pub fn credentials_status_routine( cmd : VerifiedCommand, _ctx : ExecutionContext ) -> Result< OutputData, ErrorData >
{
let opts = OutputOptions::from_cmd( &cmd )?;
let paths = require_claude_paths()?;
if !paths.credentials_file().exists()
{
return Err( ErrorData::new(
ErrorCode::InternalError,
format!(
"credential file not found: {} \u{2014} run `claude auth login` to authenticate",
paths.credentials_file().display(),
),
) );
}
let ts = crate::token::status_with_threshold( crate::token::WARNING_THRESHOLD_SECS );
let tok = match &ts
{
Ok( crate::token::TokenStatus::Valid { .. } ) => "valid".to_string(),
Ok( crate::token::TokenStatus::ExpiringSoon { expires_in } ) =>
format!( "expiring in {}m", expires_in.as_secs() / 60 ),
Ok( crate::token::TokenStatus::Expired ) => "expired".to_string(),
Err( _ ) => "unknown".to_string(),
};
let exp = match &ts
{
Ok( crate::token::TokenStatus::Valid { expires_in }
| crate::token::TokenStatus::ExpiringSoon { expires_in } ) =>
{
let h = expires_in.as_secs() / 3600;
let m = ( expires_in.as_secs() % 3600 ) / 60;
format!( "in {h}h {m}m" )
}
Ok( crate::token::TokenStatus::Expired ) => "expired".to_string(),
Err( _ ) => "(unavailable)".to_string(),
};
let ( sub, tier, email, org ) = read_live_cred_meta( &paths );
let content = match ( opts.format, opts.verbosity )
{
( OutputFormat::Json, _ ) =>
{
let s = json_escape( &sub );
let t = json_escape( &tier );
let tk = json_escape( &tok );
let exp_secs = match &ts
{
Ok( crate::token::TokenStatus::Valid { expires_in }
| crate::token::TokenStatus::ExpiringSoon { expires_in } ) =>
expires_in.as_secs().to_string(),
_ => "0".to_string(),
};
format!( "{{\"subscription\":\"{s}\",\"tier\":\"{t}\",\"token\":\"{tk}\",\"expires_in_secs\":{exp_secs}}}\n" )
}
( OutputFormat::Text, 0 ) => format!( "Sub: {sub}\nToken: {tok}\n" ),
( OutputFormat::Text, 1 ) =>
format!( "Sub: {sub}\nTier: {tier}\nToken: {tok}\nEmail: {email}\nOrg: {org}\n" ),
( OutputFormat::Text, _ ) =>
format!( "Sub: {sub}\nTier: {tier}\nToken: {tok}\nExpires: {exp}\nEmail: {email}\nOrg: {org}\n" ),
};
Ok( OutputData::new( content, "text" ) )
}
#[ allow( clippy::needless_pass_by_value, clippy::missing_inline_in_public_items ) ]
pub fn account_list_routine( cmd : VerifiedCommand, _ctx : ExecutionContext ) -> Result< OutputData, ErrorData >
{
let opts = OutputOptions::from_cmd( &cmd )?;
let _paths = require_claude_paths()?;
let accounts = crate::account::list()
.map_err( |e| io_err_to_error_data( &e, "account list" ) )?;
let content = match opts.format
{
OutputFormat::Json =>
{
if accounts.is_empty()
{
"[]\n".to_string()
}
else
{
let entries : Vec< String > = accounts.iter().map( |a|
{
format!(
r#"{{"name":"{}","subscription_type":"{}","rate_limit_tier":"{}","expires_at_ms":{},"is_active":{}}}"#,
json_escape( &a.name ),
json_escape( &a.subscription_type ),
json_escape( &a.rate_limit_tier ),
a.expires_at_ms,
a.is_active,
)
} ).collect();
format!( "[{}]\n", entries.join( "," ) )
}
}
OutputFormat::Text =>
{
if accounts.is_empty()
{
"(no accounts configured)\n".to_string()
}
else
{
let mut out = String::new();
for a in &accounts
{
match opts.verbosity
{
0 =>
{
out.push_str( &a.name );
out.push( '\n' );
}
1 =>
{
out.push_str( &a.name );
if a.is_active { out.push_str( " *" ); }
out.push( '\n' );
}
_ =>
{
out.push_str( &a.name );
if a.is_active { out.push_str( " <- active" ); }
let _ = write!(
out, " ({}, {}, expires_at_ms={})",
a.subscription_type, a.rate_limit_tier, a.expires_at_ms,
);
out.push( '\n' );
}
}
}
out
}
}
};
Ok( OutputData::new( content, "text" ) )
}
#[ allow( clippy::needless_pass_by_value ) ]
fn status_active( opts : OutputOptions, paths : crate::ClaudePaths ) -> Result< OutputData, ErrorData >
{
let active_marker = paths.accounts_dir().join( "_active" );
let account_name = std::fs::read_to_string( &active_marker )
.ok()
.map( | s | s.trim().to_string() )
.filter( | s | !s.is_empty() )
.ok_or_else( || ErrorData::new(
ErrorCode::InternalError,
"no active account linked \u{2014} see `.credentials.status` for live credentials, or initialize with `.account.save name::X` + `.account.switch name::X`".to_string(),
) )?;
let ts = crate::token::status_with_threshold( crate::token::WARNING_THRESHOLD_SECS );
let tok = match &ts
{
Ok( crate::token::TokenStatus::Valid { .. } ) => "valid".to_string(),
Ok( crate::token::TokenStatus::ExpiringSoon { expires_in } ) =>
format!( "expiring in {}m", expires_in.as_secs() / 60 ),
Ok( crate::token::TokenStatus::Expired ) => "expired".to_string(),
Err( _ ) => "unknown".to_string(),
};
let exp = match &ts
{
Ok( crate::token::TokenStatus::Valid { expires_in }
| crate::token::TokenStatus::ExpiringSoon { expires_in } ) =>
{
let h = expires_in.as_secs() / 3600;
let m = ( expires_in.as_secs() % 3600 ) / 60;
format!( "in {h}h {m}m" )
}
Ok( crate::token::TokenStatus::Expired ) => "expired".to_string(),
Err( _ ) => "(unavailable)".to_string(),
};
let ( sub, tier, email, org ) = if opts.verbosity >= 1
{
read_live_cred_meta( &paths )
}
else { ( String::new(), String::new(), String::new(), String::new() ) };
let content = match ( opts.format, opts.verbosity )
{
( OutputFormat::Json, _ ) =>
{
let a = json_escape( &account_name );
let t = json_escape( &tok );
format!( "{{\"account\":\"{a}\",\"token\":\"{t}\"}}\n" )
}
( OutputFormat::Text, 0 ) => format!( "{account_name}\n{tok}\n" ),
( OutputFormat::Text, 1 ) =>
format!( "Account: {account_name}\nToken: {tok}\nSub: {sub}\nTier: {tier}\nEmail: {email}\nOrg: {org}\n" ),
( OutputFormat::Text, _ ) =>
format!( "Account: {account_name}\nToken: {tok}\nSub: {sub}\nTier: {tier}\nExpires: {exp}\nEmail: {email}\nOrg: {org}\n" ),
};
Ok( OutputData::new( content, "text" ) )
}
#[ allow( clippy::needless_pass_by_value ) ]
fn status_named(
opts : OutputOptions,
paths : crate::ClaudePaths,
name_arg : &str,
) -> Result< OutputData, ErrorData >
{
crate::account::validate_name( name_arg )
.map_err( |e| io_err_to_error_data( &e, "account status" ) )?;
let accounts = crate::account::list()
.map_err( |e| io_err_to_error_data( &e, "account status" ) )?;
let account = accounts.iter().find( | a | a.name == name_arg )
.ok_or_else( || ErrorData::new(
ErrorCode::InternalError,
format!( "account '{name_arg}' not found" ),
) )?;
let ts = token_status_from_ms( account.expires_at_ms );
let tok = match &ts
{
crate::token::TokenStatus::Valid { .. } => "valid".to_string(),
crate::token::TokenStatus::ExpiringSoon { expires_in } =>
format!( "expiring in {}m", expires_in.as_secs() / 60 ),
crate::token::TokenStatus::Expired => "expired".to_string(),
};
let exp = match &ts
{
crate::token::TokenStatus::Valid { expires_in }
| crate::token::TokenStatus::ExpiringSoon { expires_in } =>
{
let h = expires_in.as_secs() / 3600;
let m = ( expires_in.as_secs() % 3600 ) / 60;
format!( "in {h}h {m}m" )
}
crate::token::TokenStatus::Expired => "expired".to_string(),
};
let sub = if account.subscription_type.is_empty() { "N/A".to_string() }
else { account.subscription_type.clone() };
let tier = if account.rate_limit_tier.is_empty() { "N/A".to_string() }
else { account.rate_limit_tier.clone() };
let ( email, org ) = if account.is_active
{
let content = std::fs::read_to_string( paths.base().join( ".claude.json" ) )
.unwrap_or_default();
let email = crate::account::parse_string_field( &content, "emailAddress" )
.filter( | s | !s.is_empty() )
.unwrap_or_else( || "N/A".to_string() );
let org = crate::account::parse_string_field( &content, "organizationName" )
.filter( | s | !s.is_empty() )
.unwrap_or_else( || "N/A".to_string() );
( email, org )
}
else
{
( "N/A".to_string(), "N/A".to_string() )
};
let content = match ( opts.format, opts.verbosity )
{
( OutputFormat::Json, _ ) =>
{
let a = json_escape( name_arg );
let t = json_escape( &tok );
format!( "{{\"account\":\"{a}\",\"token\":\"{t}\"}}\n" )
}
( OutputFormat::Text, 0 ) => format!( "{name_arg}\n{tok}\n" ),
( OutputFormat::Text, 1 ) =>
format!( "Account: {name_arg}\nToken: {tok}\nSub: {sub}\nTier: {tier}\nEmail: {email}\nOrg: {org}\n" ),
( OutputFormat::Text, _ ) =>
format!( "Account: {name_arg}\nToken: {tok}\nSub: {sub}\nTier: {tier}\nExpires: {exp}\nEmail: {email}\nOrg: {org}\n" ),
};
Ok( OutputData::new( content, "text" ) )
}
#[ allow( clippy::needless_pass_by_value, clippy::missing_inline_in_public_items ) ]
pub fn account_status_routine( cmd : VerifiedCommand, _ctx : ExecutionContext ) -> Result< OutputData, ErrorData >
{
let opts = OutputOptions::from_cmd( &cmd )?;
let paths = require_claude_paths()?;
let name_arg = match cmd.arguments.get( "name" )
{
Some( Value::String( s ) ) => s.clone(),
_ => String::new(),
};
if name_arg.is_empty() { return status_active( opts, paths ); }
status_named( opts, paths, &name_arg )
}
#[ allow( clippy::needless_pass_by_value, clippy::missing_inline_in_public_items ) ]
pub fn account_switch_routine( cmd : VerifiedCommand, _ctx : ExecutionContext ) -> Result< OutputData, ErrorData >
{
let name = require_nonempty_string_arg( &cmd, "name" )?;
let _paths = require_claude_paths()?;
crate::account::check_switch_preconditions( &name )
.map_err( |e| io_err_to_error_data( &e, "account switch" ) )?;
if is_dry( &cmd )
{
return Ok( OutputData::new( format!( "[dry-run] would switch to '{name}'\n" ), "text" ) );
}
crate::account::switch_account( &name )
.map_err( |e| io_err_to_error_data( &e, "account switch" ) )?;
Ok( OutputData::new( format!( "switched to '{name}'\n" ), "text" ) )
}
pub use crate::usage::usage_routine;
fn require_active_credentials( paths : &crate::ClaudePaths ) -> Result< std::path::PathBuf, ErrorData >
{
let creds = paths.credentials_file();
if !creds.exists()
{
return Err( ErrorData::new(
ErrorCode::InternalError,
"no active account \u{2014} run `claude auth login` to authenticate".to_string(),
) );
}
Ok( creds )
}
#[ derive( Debug ) ]
struct RateLimitData
{
utilization_5h : f64,
reset_5h : u64,
utilization_7d : f64,
reset_7d : u64,
status : String,
}
fn read_auth_token( creds_path : &std::path::Path ) -> Result< String, ErrorData >
{
let content = std::fs::read_to_string( creds_path )
.map_err( |e| ErrorData::new(
ErrorCode::InternalError,
format!( "cannot read credentials: {e}" ),
) )?;
crate::account::parse_string_field( &content, "accessToken" )
.ok_or_else( || ErrorData::new(
ErrorCode::InternalError,
"credentials missing 'accessToken' \u{2014} re-authenticate with `claude auth login`".to_string(),
) )
}
fn parse_rate_limit_headers( resp : &ureq::Response ) -> Result< RateLimitData, ErrorData >
{
let h = | name : &str | -> Result< String, ErrorData >
{
resp.header( name )
.map( ToString::to_string )
.ok_or_else( || ErrorData::new(
ErrorCode::InternalError,
format!( "rate limit header '{name}' missing \u{2014} run `claude /usage` to view limits" ),
) )
};
let s_session_util = h( "anthropic-ratelimit-unified-5h-utilization" )?;
let s_session_reset = h( "anthropic-ratelimit-unified-5h-reset" )?;
let s_weekly_util = h( "anthropic-ratelimit-unified-7d-utilization" )?;
let s_weekly_reset = h( "anthropic-ratelimit-unified-7d-reset" )?;
let status = h( "anthropic-ratelimit-unified-status" )?;
let utilization_session = s_session_util.parse::< f64 >()
.map_err( |e| ErrorData::new( ErrorCode::InternalError, format!( "malformed 5h-utilization header: {e}" ) ) )?;
let reset_session_ts = s_session_reset.parse::< u64 >()
.map_err( |e| ErrorData::new( ErrorCode::InternalError, format!( "malformed 5h-reset header: {e}" ) ) )?;
let utilization_weekly = s_weekly_util.parse::< f64 >()
.map_err( |e| ErrorData::new( ErrorCode::InternalError, format!( "malformed 7d-utilization header: {e}" ) ) )?;
let reset_weekly_ts = s_weekly_reset.parse::< u64 >()
.map_err( |e| ErrorData::new( ErrorCode::InternalError, format!( "malformed 7d-reset header: {e}" ) ) )?;
Ok( RateLimitData
{
utilization_5h : utilization_session,
reset_5h : reset_session_ts,
utilization_7d : utilization_weekly,
reset_7d : reset_weekly_ts,
status,
} )
}
fn format_rate_limits_compact( data : &RateLimitData ) -> String
{
let pct_session = format!( "{:.0}", data.utilization_5h * 100.0 );
let pct_weekly = format!( "{:.0}", data.utilization_7d * 100.0 );
let status = &data.status;
format!( "{pct_session}%\n{pct_weekly}%\n{status}\n" )
}
fn format_rate_limits_text( data : &RateLimitData ) -> String
{
use std::time::{ SystemTime, UNIX_EPOCH };
let now_secs = SystemTime::now()
.duration_since( UNIX_EPOCH )
.unwrap_or_default()
.as_secs();
let pct_session = format!( "{:.0}", data.utilization_5h * 100.0 );
let pct_weekly = format!( "{:.0}", data.utilization_7d * 100.0 );
let reset_session_str = format_duration_secs( data.reset_5h.saturating_sub( now_secs ) );
let reset_weekly_str = format_duration_secs( data.reset_7d.saturating_sub( now_secs ) );
let status = &data.status;
format!( "Session (5h): {pct_session}% consumed, resets in {reset_session_str}\nWeekly (7d): {pct_weekly}% consumed, resets in {reset_weekly_str}\nStatus: {status}\n" )
}
fn format_rate_limits_verbose( data : &RateLimitData ) -> String
{
use std::time::{ SystemTime, UNIX_EPOCH };
let now_secs = SystemTime::now()
.duration_since( UNIX_EPOCH )
.unwrap_or_default()
.as_secs();
let reset_session_str = format_duration_secs( data.reset_5h.saturating_sub( now_secs ) );
let reset_weekly_str = format_duration_secs( data.reset_7d.saturating_sub( now_secs ) );
let pct_session = format!( "{:.0}", data.utilization_5h * 100.0 );
let pct_weekly = format!( "{:.0}", data.utilization_7d * 100.0 );
let raw_session = data.utilization_5h;
let raw_weekly = data.utilization_7d;
let ts_session = data.reset_5h;
let ts_weekly = data.reset_7d;
let status = &data.status;
format!(
"Session (5h): {pct_session}% consumed, resets in {reset_session_str}\n raw: {raw_session:.6}, reset_ts: {ts_session}\nWeekly (7d): {pct_weekly}% consumed, resets in {reset_weekly_str}\n raw: {raw_weekly:.6}, reset_ts: {ts_weekly}\nStatus: {status}\n"
)
}
fn format_rate_limits_json( data : &RateLimitData ) -> String
{
let pct_session = format!( "{:.0}", data.utilization_5h * 100.0 );
let pct_weekly = format!( "{:.0}", data.utilization_7d * 100.0 );
let ts_session = data.reset_5h;
let ts_weekly = data.reset_7d;
let status_esc = json_escape( &data.status );
format!( "{{\n \"session_5h_pct\": {pct_session},\n \"session_5h_reset_ts\": {ts_session},\n \"weekly_7d_pct\": {pct_weekly},\n \"weekly_7d_reset_ts\": {ts_weekly},\n \"status\": \"{status_esc}\"\n}}\n" )
}
fn fetch_rate_limits( creds_path : &std::path::Path ) -> Result< RateLimitData, ErrorData >
{
let token = read_auth_token( creds_path )?;
let body = r#"{"model":"claude-haiku-4-5-20251001","max_tokens":1,"messages":[{"role":"user","content":"quota"}]}"#;
let req_result = ureq::post( "https://api.anthropic.com/v1/messages" )
.set( "Authorization", &format!( "Bearer {token}" ) )
.set( "anthropic-beta", "oauth-2025-04-20" )
.set( "anthropic-version", "2023-06-01" )
.set( "Content-Type", "application/json" )
.send_string( body );
let resp = match req_result
{
Ok( r ) | Err( ureq::Error::Status( _, r ) ) => r,
Err( e ) => return Err( ErrorData::new(
ErrorCode::InternalError,
format!( "HTTP request failed: {e} \u{2014} check network connection" ),
) ),
};
parse_rate_limit_headers( &resp )
}
#[ allow( clippy::needless_pass_by_value, clippy::missing_inline_in_public_items ) ]
pub fn account_limits_routine( cmd : VerifiedCommand, _ctx : ExecutionContext ) -> Result< OutputData, ErrorData >
{
let opts = OutputOptions::from_cmd( &cmd )?;
let paths = require_claude_paths()?;
let name_arg = match cmd.arguments.get( "name" )
{
Some( Value::String( s ) ) => s.clone(),
_ => String::new(),
};
let creds_path = if name_arg.is_empty()
{
require_active_credentials( &paths )?
}
else
{
crate::account::validate_name( &name_arg )
.map_err( | e | io_err_to_error_data( &e, "account limits" ) )?;
let path = paths.accounts_dir().join( format!( "{name_arg}.credentials.json" ) );
if !path.exists()
{
return Err( ErrorData::new(
ErrorCode::InternalError,
format!( "account '{name_arg}' not found" ),
) );
}
path
};
let data = fetch_rate_limits( &creds_path )?;
let text = match opts.format
{
OutputFormat::Json => format_rate_limits_json( &data ),
OutputFormat::Text => match opts.verbosity
{
0 => format_rate_limits_compact( &data ),
2 => format_rate_limits_verbose( &data ),
_ => format_rate_limits_text( &data ),
},
};
Ok( OutputData::new( text, "text" ) )
}
#[ allow( clippy::needless_pass_by_value, clippy::missing_inline_in_public_items ) ]
pub fn dot_routine( _cmd : VerifiedCommand, _ctx : ExecutionContext ) -> Result< OutputData, ErrorData >
{
Ok( OutputData::new( String::new(), "text" ) )
}
#[ allow( clippy::needless_pass_by_value, clippy::missing_inline_in_public_items ) ]
pub fn account_save_routine( cmd : VerifiedCommand, _ctx : ExecutionContext ) -> Result< OutputData, ErrorData >
{
let name = require_nonempty_string_arg( &cmd, "name" )?;
let _paths = require_claude_paths()?;
if is_dry( &cmd )
{
return Ok( OutputData::new( format!( "[dry-run] would save current credentials as '{name}'\n" ), "text" ) );
}
crate::account::save( &name ).map_err( |e| io_err_to_error_data( &e, "account save" ) )?;
Ok( OutputData::new( format!( "saved current credentials as '{name}'\n" ), "text" ) )
}
#[ allow( clippy::needless_pass_by_value, clippy::missing_inline_in_public_items ) ]
pub fn account_delete_routine( cmd : VerifiedCommand, _ctx : ExecutionContext ) -> Result< OutputData, ErrorData >
{
let name = require_nonempty_string_arg( &cmd, "name" )?;
let _paths = require_claude_paths()?;
crate::account::check_delete_preconditions( &name )
.map_err( |e| io_err_to_error_data( &e, "account delete" ) )?;
if is_dry( &cmd )
{
return Ok( OutputData::new( format!( "[dry-run] would delete account '{name}'\n" ), "text" ) );
}
crate::account::delete( &name ).map_err( |e| io_err_to_error_data( &e, "account delete" ) )?;
Ok( OutputData::new( format!( "deleted account '{name}'\n" ), "text" ) )
}
#[ allow( clippy::needless_pass_by_value, clippy::missing_inline_in_public_items ) ]
pub fn token_status_routine( cmd : VerifiedCommand, _ctx : ExecutionContext ) -> Result< OutputData, ErrorData >
{
let opts = OutputOptions::from_cmd( &cmd )?;
let _paths = require_claude_paths()?;
let threshold_secs = match cmd.arguments.get( "threshold" )
{
Some( Value::Integer( n ) ) => u64::try_from( *n ).unwrap_or( crate::token::WARNING_THRESHOLD_SECS ),
_ => crate::token::WARNING_THRESHOLD_SECS,
};
let token_result = crate::token::status_with_threshold( threshold_secs )
.map_err( |e| io_err_to_error_data( &e, "token status" ) )?;
let content = match opts.format
{
OutputFormat::Json =>
{
match &token_result
{
crate::token::TokenStatus::Valid { expires_in } =>
format!( "{{\"status\":\"valid\",\"expires_in_secs\":{}}}\n", expires_in.as_secs() ),
crate::token::TokenStatus::ExpiringSoon { expires_in } =>
format!( "{{\"status\":\"expiring_soon\",\"expires_in_secs\":{}}}\n", expires_in.as_secs() ),
crate::token::TokenStatus::Expired =>
"{\"status\":\"expired\"}\n".to_string(),
}
}
OutputFormat::Text =>
{
match ( &token_result, opts.verbosity )
{
( crate::token::TokenStatus::Valid { .. }, 0 ) =>
"valid\n".to_string(),
( crate::token::TokenStatus::Valid { expires_in }, 1 ) =>
format!( "valid — {}m remaining\n", expires_in.as_secs() / 60 ),
( crate::token::TokenStatus::Valid { expires_in }, _ ) =>
format!( "valid — {}s remaining (threshold={}s)\n", expires_in.as_secs(), threshold_secs ),
( crate::token::TokenStatus::ExpiringSoon { .. }, 0 ) =>
"expiring soon\n".to_string(),
( crate::token::TokenStatus::ExpiringSoon { expires_in }, 1 ) =>
format!( "expiring soon — {}m remaining\n", expires_in.as_secs() / 60 ),
( crate::token::TokenStatus::ExpiringSoon { expires_in }, _ ) =>
format!( "expiring soon — {}s remaining (threshold={}s)\n", expires_in.as_secs(), threshold_secs ),
( crate::token::TokenStatus::Expired, _ ) =>
"expired\n".to_string(),
}
}
};
Ok( OutputData::new( content, "text" ) )
}
#[ allow( clippy::needless_pass_by_value, clippy::missing_inline_in_public_items ) ]
pub fn paths_routine( cmd : VerifiedCommand, _ctx : ExecutionContext ) -> Result< OutputData, ErrorData >
{
let opts = OutputOptions::from_cmd( &cmd )?;
let paths = require_claude_paths()?;
let content = match opts.format
{
OutputFormat::Json =>
{
format!(
concat!(
"{{\"base\":\"{}\",",
"\"credentials\":\"{}\",",
"\"accounts\":\"{}\",",
"\"projects\":\"{}\",",
"\"stats\":\"{}\",",
"\"settings\":\"{}\",",
"\"session_env\":\"{}\",",
"\"sessions\":\"{}\"}}\n",
),
json_escape( &paths.base().display().to_string() ),
json_escape( &paths.credentials_file().display().to_string() ),
json_escape( &paths.accounts_dir().display().to_string() ),
json_escape( &paths.projects_dir().display().to_string() ),
json_escape( &paths.stats_file().display().to_string() ),
json_escape( &paths.settings_file().display().to_string() ),
json_escape( &paths.session_env_dir().display().to_string() ),
json_escape( &paths.sessions_dir().display().to_string() ),
)
}
OutputFormat::Text =>
{
match opts.verbosity
{
0 =>
{
format!( "{}\n", paths.base().display() )
}
1 =>
{
format!(
"credentials: {}\naccounts: {}\nprojects: {}\nstats: {}\nsettings: {}\nsession-env: {}\nsessions: {}\n",
paths.credentials_file().display(),
paths.accounts_dir().display(),
paths.projects_dir().display(),
paths.stats_file().display(),
paths.settings_file().display(),
paths.session_env_dir().display(),
paths.sessions_dir().display(),
)
}
_ =>
{
let entries : Vec< ( &str, std::path::PathBuf ) > = vec![
( "credentials", paths.credentials_file() ),
( "accounts", paths.accounts_dir() ),
( "projects", paths.projects_dir() ),
( "stats", paths.stats_file() ),
( "settings", paths.settings_file() ),
( "session-env", paths.session_env_dir() ),
( "sessions", paths.sessions_dir() ),
];
let mut out = String::new();
for ( label, path ) in entries
{
let exists = if path.exists() { "exists" } else { "absent" };
let _ = writeln!( out, "{label}: {} [{exists}]", path.display() );
}
out
}
}
}
};
Ok( OutputData::new( content, "text" ) )
}