use unilang::data::{ ErrorCode, ErrorData, OutputData };
use unilang::interpreter::ExecutionContext;
use unilang::semantic::VerifiedCommand;
use unilang::types::Value;
use super::shared::{
require_nonempty_string_arg, is_dry, require_claude_paths, require_credential_store,
io_err_to_error_data, resolve_account_name,
};
#[ inline ]
#[ allow( clippy::too_many_lines ) ]
pub fn account_use_routine( cmd : VerifiedCommand, _ctx : ExecutionContext ) -> Result< OutputData, ErrorData >
{
let raw_name = require_nonempty_string_arg( &cmd, "name" )?;
let touch = crate::output::parse_int_flag( &cmd, "touch", 1 )?;
let trace = crate::output::parse_int_flag( &cmd, "trace", 0 )? != 0;
let imodel_str = match cmd.arguments.get( "imodel" )
{
None => "auto".to_string(),
Some( Value::String( s ) ) =>
{
crate::usage::validate_imodel_str( s )
.map_err( |e| ErrorData::new( ErrorCode::ArgumentTypeMismatch, e ) )?;
s.clone()
}
_ => return Err( ErrorData::new( ErrorCode::ArgumentTypeMismatch, "imodel:: must be a string".to_string() ) ),
};
let effort_str = match cmd.arguments.get( "effort" )
{
None => "auto".to_string(),
Some( Value::String( s ) ) =>
{
crate::usage::validate_effort_str( s )
.map_err( |e| ErrorData::new( ErrorCode::ArgumentTypeMismatch, e ) )?;
s.clone()
}
_ => return Err( ErrorData::new( ErrorCode::ArgumentTypeMismatch, "effort:: must be a string".to_string() ) ),
};
let set_model_str = match cmd.arguments.get( "set_model" )
{
None => None,
Some( Value::String( s ) ) =>
{
crate::usage::validate_set_model( s )
.map_err( |e| ErrorData::new( ErrorCode::ArgumentTypeMismatch, e ) )?;
Some( s.clone() )
}
_ => return Err( ErrorData::new( ErrorCode::ArgumentTypeMismatch, "set_model:: must be a string".to_string() ) ),
};
let refresh = crate::output::parse_int_flag( &cmd, "refresh", 1 )?;
let paths = require_claude_paths()?;
let credential_store = require_credential_store()?;
let name = resolve_account_name( &raw_name, &credential_store )?;
crate::account::check_switch_preconditions( &name, &credential_store )
.map_err( |e| io_err_to_error_data( &e, "account use" ) )?;
let force = crate::output::parse_int_flag( &cmd, "force", 0 )? != 0;
let owner = crate::account::read_owner( &credential_store, &name );
if !force && !crate::account::is_owned( &owner )
{
return Err( ErrorData::new(
ErrorCode::ArgumentTypeMismatch,
format!( "ownership violation: this account is owned by {owner}" ),
) );
}
if is_dry( &cmd )
{
return Ok( OutputData::new( format!( "[dry-run] would switch to '{name}'\n" ), "text" ) );
}
let mut outcome = if touch != 0
{
crate::usage::pre_switch_touch_ctx( &name, &credential_store, trace, &imodel_str, &effort_str )
}
else
{
crate::usage::PreSwitchOutcome::Unavailable
};
if touch != 0 && matches!( outcome, crate::usage::PreSwitchOutcome::Unavailable )
{
outcome = check_expiry_and_refresh(
&name, &credential_store, &paths, refresh, trace, &imodel_str, &effort_str,
);
}
crate::account::switch_account( &name, &credential_store, &paths )
.map_err( |e| io_err_to_error_data( &e, "account use" ) )?;
match outcome
{
crate::usage::PreSwitchOutcome::NeedTouch( ctx ) =>
{
crate::usage::apply_post_switch_touch( &name, ctx, &imodel_str, &effort_str, trace, &paths, &credential_store );
}
crate::usage::PreSwitchOutcome::Unavailable => {}
}
if let Some( ref sm ) = set_model_str
{
let model_id = crate::usage::validate_set_model( sm ).ok().flatten();
claude_profile_core::account::set_session_model( &paths, model_id );
if trace { eprintln!( "[trace] account.use {name} set_model: {sm}" ) }
}
Ok( OutputData::new( format!( "switched to '{name}'\n" ), "text" ) )
}
fn check_expiry_and_refresh(
name : &str,
credential_store : &std::path::Path,
paths : &crate::ClaudePaths,
refresh : i64,
trace : bool,
imodel_str : &str,
effort_str : &str,
) -> crate::usage::PreSwitchOutcome
{
let cred_path = credential_store.join( format!( "{name}.credentials.json" ) );
let Ok( cred_str ) = std::fs::read_to_string( &cred_path )
else { return crate::usage::PreSwitchOutcome::Unavailable };
let needle = "\"expiresAt\":";
let expires_ms = cred_str.find( needle ).and_then( | pos |
{
let rest = cred_str[ pos + needle.len().. ].trim_start();
let end = rest.find( | c : char | !c.is_ascii_digit() ).unwrap_or( rest.len() );
rest[ ..end ].parse::< u64 >().ok()
} );
let Some( exp_ms ) = expires_ms
else { return crate::usage::PreSwitchOutcome::Unavailable };
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 <= exp_ms
{
if trace
{
let rem_s = ( exp_ms - now_ms ) / 1000;
eprintln!( "[trace] account.use {name} expiry check: valid (expires in {}h {}m)", rem_s / 3600, ( rem_s % 3600 ) / 60 );
}
return crate::usage::PreSwitchOutcome::Unavailable;
}
let elapsed_s = ( now_ms - exp_ms ) / 1000;
let h = elapsed_s / 3600;
let m = ( elapsed_s % 3600 ) / 60;
if refresh != 0
{
if trace { eprintln!( "[trace] account.use {name} expiry check: expired({h}h {m}m ago) → attempting refresh" ) }
let refreshed = crate::usage::attempt_expired_token_refresh( name, credential_store, paths, trace, imodel_str, effort_str );
if refreshed
{
if trace { eprintln!( "[trace] account.use {name} expiry check: refresh OK — re-probing touch context" ) }
return crate::usage::pre_switch_touch_ctx( name, credential_store, trace, imodel_str, effort_str );
}
if trace { eprintln!( "[trace] account.use {name} expiry check: refresh failed → refused" ) }
eprintln!( "account credentials expired and refresh failed: {name} (expired {h}h {m}m ago)" );
}
else
{
if trace { eprintln!( "[trace] account.use {name} expiry check: expired({h}h {m}m ago) → refused (refresh::0)" ) }
eprintln!( "account credentials expired: {name} (expired {h}h {m}m ago)" );
}
std::process::exit( 3 );
}
#[ inline ]
pub fn account_save_routine( cmd : VerifiedCommand, _ctx : ExecutionContext ) -> Result< OutputData, ErrorData >
{
let paths = require_claude_paths()?;
let trace = crate::output::parse_int_flag( &cmd, "trace", 0 )? != 0;
let name = match cmd.arguments.get( "name" )
{
Some( Value::String( s ) ) if !s.is_empty() => s.clone(),
Some( Value::String( _ ) ) =>
return Err( ErrorData::new( ErrorCode::ArgumentMissing, "name:: value cannot be empty".to_string() ) ),
_ =>
{
let cs = require_credential_store()?;
let cj = std::fs::read_to_string( paths.claude_json_file() ).unwrap_or_default();
let oauth_email = cj
.find( "\"oauthAccount\":" )
.and_then( | pos | crate::account::parse_string_field( &cj[ pos.. ], "emailAddress" ) )
.filter( | s | !s.is_empty() );
if let Some( email ) = oauth_email
{
email
}
else
{
let marker_path = cs.join( crate::account::active_marker_filename() );
std::fs::read_to_string( &marker_path )
.ok()
.map( | s | s.trim().to_string() )
.filter( | s | !s.is_empty() )
.ok_or_else( || ErrorData::new(
ErrorCode::ArgumentMissing,
"cannot infer account name: no active account set — pass name:: explicitly".to_string(),
) )?
}
}
};
let credential_store = require_credential_store()?;
if trace { eprintln!( "[trace] account.save reading {}", paths.credentials_file().display() ) }
crate::account::validate_name( &name )
.map_err( | e | io_err_to_error_data( &e, "account save" ) )?;
if is_dry( &cmd )
{
return Ok( OutputData::new( format!( "[dry-run] would save current credentials as '{name}'\n" ), "text" ) );
}
let host_val = match cmd.arguments.get( "host" )
{
Some( Value::String( s ) ) if !s.is_empty() => s.clone(),
_ =>
{
let user = std::env::var( "USER" ).unwrap_or_default();
let hostname = crate::account::resolve_hostname();
format!( "{user}@{hostname}" )
}
};
let role_val = match cmd.arguments.get( "role" )
{
Some( Value::String( s ) ) => s.clone(),
_ => String::new(),
};
crate::account::save( &name, &credential_store, &paths, true, None, Some( &host_val ), Some( &role_val ), None )
.map_err( |e| io_err_to_error_data( &e, "account save" ) )?;
if trace { eprintln!( "[trace] account.save write: OK host={host_val} role={role_val}" ) }
Ok( OutputData::new( format!( "saved current credentials as '{name}'\n" ), "text" ) )
}
#[ inline ]
pub fn account_delete_routine( cmd : VerifiedCommand, _ctx : ExecutionContext ) -> Result< OutputData, ErrorData >
{
let trace = crate::output::parse_int_flag( &cmd, "trace", 0 )? != 0;
let raw_name = require_nonempty_string_arg( &cmd, "name" )?;
let credential_store = require_credential_store()?;
if trace { eprintln!( "[trace] account.delete store: {}", credential_store.display() ) }
let name = resolve_account_name( &raw_name, &credential_store )?;
crate::account::check_delete_preconditions( &name, &credential_store )
.map_err( |e| io_err_to_error_data( &e, "account delete" ) )?;
let force = crate::output::parse_int_flag( &cmd, "force", 0 )? != 0;
let owner = crate::account::read_owner( &credential_store, &name );
if !force && !crate::account::is_owned( &owner )
{
return Err( ErrorData::new(
ErrorCode::ArgumentTypeMismatch,
format!( "ownership violation: this account is owned by {owner}" ),
) );
}
if is_dry( &cmd )
{
return Ok( OutputData::new( format!( "[dry-run] would delete account '{name}'\n" ), "text" ) );
}
crate::account::delete( &name, &credential_store )
.map_err( |e| io_err_to_error_data( &e, "account delete" ) )?;
Ok( OutputData::new( format!( "deleted account '{name}'\n" ), "text" ) )
}