use unilang::data::{ ErrorCode, ErrorData };
use unilang::semantic::VerifiedCommand;
use unilang::types::Value;
use crate::output::json_escape;
pub( crate ) 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 )
}
pub( crate ) fn is_dry( cmd : &VerifiedCommand ) -> bool
{
matches!( cmd.arguments.get( "dry" ), Some( Value::Boolean( true ) ) )
}
pub( crate ) 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,
"credential store path could not be resolved".to_string(),
) )
}
_ => Err( ErrorData::new( ErrorCode::InternalError, "HOME environment variable not set".to_string() ) ),
}
}
pub( crate ) fn require_credential_store() -> Result< std::path::PathBuf, ErrorData >
{
crate::PersistPaths::new()
.map( | p | p.credential_store() )
.map_err( |e| ErrorData::new(
ErrorCode::InternalError,
format!( "persistent storage unavailable: {e}" ),
) )
}
pub( crate ) fn io_err_to_error_data( e : &std::io::Error, context : &str ) -> ErrorData
{
let code = match e.kind()
{
std::io::ErrorKind::InvalidInput => ErrorCode::ArgumentTypeMismatch,
_ => ErrorCode::InternalError,
};
ErrorData::new( code, format!( "{context}: {e}" ) )
}
pub( crate ) fn resolve_account_name( raw : &str, store : &std::path::Path ) -> Result< String, ErrorData >
{
if raw.contains( '@' )
{
return Ok( raw.to_string() );
}
if raw.contains( '/' ) || raw.contains( '\\' ) || raw.contains( '*' )
{
return Err( ErrorData::new(
ErrorCode::ArgumentTypeMismatch,
format!( "account name prefix '{raw}' contains invalid characters" ),
) );
}
let accounts = crate::account::list( store )
.map_err( |e| ErrorData::new( ErrorCode::InternalError, format!( "cannot list accounts: {e}" ) ) )?;
let exact : Vec< &str > = accounts.iter()
.filter( | a | a.name.split_once( '@' ).is_some_and( | ( local, _ ) | local == raw ) )
.map( | a | a.name.as_str() )
.collect();
if exact.len() == 1
{
return Ok( exact[ 0 ].to_string() );
}
let matches : Vec< &str > = accounts.iter()
.filter( |a| a.name.starts_with( raw ) )
.map( |a| a.name.as_str() )
.collect();
match matches.len()
{
1 => Ok( matches[ 0 ].to_string() ),
0 => Err( ErrorData::new(
ErrorCode::InternalError,
format!( "account '{raw}' not found" ),
) ),
_ => Err( ErrorData::new(
ErrorCode::ArgumentTypeMismatch,
format!( "ambiguous prefix '{raw}': matches {}", matches.join( ", " ) ),
) ),
}
}
pub( crate ) fn derive_token_state(
ts : &Result< crate::token::TokenStatus, std::io::Error >,
) -> ( String, String, u64 )
{
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 exp_secs = match ts
{
Ok( crate::token::TokenStatus::Valid { expires_in }
| crate::token::TokenStatus::ExpiringSoon { expires_in } ) => expires_in.as_secs(),
_ => 0,
};
( tok, exp, exp_secs )
}
pub( crate ) fn caps_to_json( caps : &[ String ] ) -> String
{
if caps.is_empty() { return "[]".to_string(); }
let inner : Vec< String > = caps.iter()
.map( | c | format!( "\"{}\"", json_escape( c ) ) )
.collect();
format!( "[{}]", inner.join( "," ) )
}