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 };
use claude_version_core::config_catalog::catalog;
use claude_version_core::config_resolve::{ resolve, resolve_all, ResolvedValue };
use claude_version_core::settings_io::{ StoredAs, infer_type, remove_setting, set_setting };
#[ allow(
clippy::needless_pass_by_value,
clippy::missing_inline_in_public_items,
clippy::too_many_lines,
) ]
pub fn config_routine( cmd : VerifiedCommand, _ctx : ExecutionContext ) -> Result< OutputData, ErrorData >
{
if let Some( Value::String( s ) ) = cmd.arguments.get( "key" )
{
if s.is_empty()
{
return Err( ErrorData::new( ErrorCode::ArgumentTypeMismatch,
"key:: value cannot be empty".to_string() ) );
}
}
let key_opt = match cmd.arguments.get( "key" )
{
Some( Value::String( s ) ) if !s.is_empty() => Some( s.clone() ),
_ => None,
};
let value_opt = match cmd.arguments.get( "value" )
{
Some( Value::String( s ) ) if !s.is_empty() => Some( s.clone() ),
_ => None,
};
let is_unset = matches!( cmd.arguments.get( "unset" ), Some( Value::Boolean( true ) ) );
let scope_explicit = cmd.arguments.contains_key( "scope" );
let scope_str = match cmd.arguments.get( "scope" )
{
Some( Value::String( s ) ) => s.clone(),
_ => "user".to_string(),
};
let opts = OutputOptions::from_cmd( &cmd )?;
if value_opt.is_some() && key_opt.is_none()
{
return Err( ErrorData::new( ErrorCode::ArgumentMissing,
"key:: is required when value:: is provided".to_string() ) );
}
if is_unset && key_opt.is_none()
{
return Err( ErrorData::new( ErrorCode::ArgumentMissing,
"key:: is required when unset::1 is provided".to_string() ) );
}
if value_opt.is_some() && is_unset
{
return Err( ErrorData::new( ErrorCode::ArgumentMissing,
"value:: and unset::1 are mutually exclusive".to_string() ) );
}
let is_write = key_opt.is_some() && ( value_opt.is_some() || is_unset );
if scope_explicit && !is_write
{
return Err( ErrorData::new( ErrorCode::ArgumentMissing,
"scope:: applies to write operations only; provide key:: with value:: or unset::1".to_string() ) );
}
match scope_str.as_str()
{
"user" | "project" => {}
other => return Err( ErrorData::new( ErrorCode::ArgumentTypeMismatch,
format!( "unknown scope '{other}': expected user or project" ) ) ),
}
match ( key_opt.as_deref(), value_opt.as_deref(), is_unset )
{
( None, None, false ) =>
{
let ( home_dir, cwd ) = config_resolve_context()?;
let all = resolve_all( &home_dir, &cwd, catalog() );
let content = render_config_show_all( &all, &opts );
Ok( OutputData::new( content, "text" ) )
}
( Some( key ), None, false ) =>
{
let ( home_dir, cwd ) = config_resolve_context()?;
let resolved = resolve( key, &home_dir, &cwd, catalog() );
let content = render_config_get( key, &resolved, &opts );
Ok( OutputData::new( content, "text" ) )
}
( Some( key ), Some( value ), false ) =>
{
let stored_as = infer_type( value );
if super::is_dry( &cmd )
{
let type_label = match stored_as
{
StoredAs::Bool => "bool",
StoredAs::Number => "number",
StoredAs::Str => "string",
StoredAs::Raw => "object",
};
return Ok( OutputData::new(
format!( "[dry-run] would set {key} = {value} ({type_label})\n" ),
"text",
) );
}
let target = config_scope_target( &scope_str )?;
if let Some( parent ) = target.parent()
{
std::fs::create_dir_all( parent ).map_err( | e |
ErrorData::new( ErrorCode::InternalError,
format!( "failed to create settings directory: {e}" ) )
)?;
}
set_setting( &target, key, value ).map_err( | e |
ErrorData::new( ErrorCode::InternalError, format!( "failed to write settings: {e}" ) )
)?;
Ok( OutputData::new( format!( "set {key} = {value}\n" ), "text" ) )
}
( Some( key ), None, true ) =>
{
if super::is_dry( &cmd )
{
return Ok( OutputData::new(
format!( "[dry-run] would unset {key}\n" ),
"text",
) );
}
let target = config_scope_target( &scope_str )?;
remove_setting( &target, key ).map_err( | e |
ErrorData::new( ErrorCode::InternalError, format!( "failed to unset setting: {e}" ) )
)?;
Ok( OutputData::new( format!( "unset {key}\n" ), "text" ) )
}
_ => unreachable!( "all invalid combinations rejected above" ),
}
}
fn config_resolve_context() -> Result< ( std::path::PathBuf, std::path::PathBuf ), ErrorData >
{
let paths = super::require_claude_paths()?;
let home_dir = paths.base().parent()
.expect( "ClaudePaths base always has HOME as parent" )
.to_path_buf();
let cwd = std::env::current_dir().unwrap_or_else( | _ | home_dir.clone() );
Ok( ( home_dir, cwd ) )
}
fn config_scope_target( scope : &str ) -> Result< std::path::PathBuf, ErrorData >
{
match scope
{
"user" =>
{
let paths = super::require_claude_paths()?;
Ok( paths.settings_file() )
}
"project" =>
{
let cwd = std::env::current_dir().map_err( | e |
ErrorData::new( ErrorCode::InternalError, format!( "failed to get current directory: {e}" ) )
)?;
Ok( cwd.join( ".claude" ).join( "settings.json" ) )
}
_ => unreachable!( "scope validated before config_scope_target call" ),
}
}
fn render_config_show_all(
all : &[ ( String, ResolvedValue ) ],
opts : &OutputOptions,
) -> String
{
match opts.format
{
OutputFormat::Json =>
{
let entries : Vec< String > = all.iter().map( | ( k, rv ) |
{
let src = rv.source.to_string();
let val_json = config_value_to_json( rv.value.as_deref() );
format!( " \"{}\": {{\"value\": {val_json}, \"source\": \"{src}\"}}",
json_escape( k ) )
} ).collect();
if entries.is_empty() { "{}\n".to_string() }
else { format!( "{{\n{}\n}}\n", entries.join( ",\n" ) ) }
}
OutputFormat::Text =>
{
let lines : Vec< String > = all.iter().map( | ( k, rv ) |
{
match &rv.value
{
None => format!( "{k}: ({})", rv.source ),
Some( v ) => format!( "{k}: {v} ({})", rv.source ),
}
} ).collect();
if lines.is_empty() { String::new() }
else { format!( "{}\n", lines.join( "\n" ) ) }
}
}
}
fn render_config_get(
key : &str,
rv : &ResolvedValue,
opts : &OutputOptions,
) -> String
{
match opts.format
{
OutputFormat::Json =>
{
let src = rv.source.to_string();
let val_json = config_value_to_json( rv.value.as_deref() );
format!( "{{\"key\": \"{}\", \"value\": {val_json}, \"source\": \"{src}\"}}\n",
json_escape( key ) )
}
OutputFormat::Text =>
{
match ( opts.verbosity, &rv.value )
{
( 0, None ) => "(absent)\n".to_string(),
( 0, Some( v ) ) => format!( "{v} ({})\n", rv.source ),
( _, None ) => format!( "{key}: (absent)\n" ),
( _, Some( v ) ) => format!( "{key}: {v} ({})\n", rv.source ),
}
}
}
}
fn config_value_to_json( raw : Option< &str > ) -> String
{
match raw
{
None => "null".to_string(),
Some( v ) => match infer_type( v )
{
StoredAs::Bool | StoredAs::Number | StoredAs::Raw => v.to_string(),
StoredAs::Str => format!( "\"{}\"", json_escape( v ) ),
},
}
}