use crate::arg_utils::split_shell_args;
use crate::editor::{parse_editor_command, select_editor_raw};
use crate::fs_utils::atomic_write_new_with_mode;
use crate::path_utils::safe_parent_for_creation;
use crate::tool::command::{SyncIgnoreArgs as ToolSyncIgnoreArgs, SyncIgnoreTarget};
use crate::x::claude_code::config::{Config, ConfigGroup};
use crate::x::claude_code::env_injection::{
EnvSyntax, env_syntax_for_current_platform, render_env_injection_lines,
};
use crate::x::claude_code::interactive;
use crate::x::claude_code::prompts::ClaudeCodePromptsArgs;
use crate::x::claude_code::security::{SecurityChecker, SecurityWarning};
use crate::x::claude_code::stats::ClaudeCodeStatsArgs;
use anyhow::{Context, Result, bail};
use clap::{Args, Subcommand};
use rust_i18n::t;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
#[derive(Args)]
#[command(
author,
version,
about,
long_about = "Commands for managing Claude Code configurations and API settings",
args_conflicts_with_subcommands = true
)]
pub struct ClaudeCodeArgs {
#[command(subcommand)]
pub command: Option<ClaudeCodeCommands>,
#[arg(
trailing_var_arg = true,
allow_hyphen_values = true,
help = "Arguments to pass to claude when using the main command (use -- to separate)"
)]
pub args: Vec<String>,
}
#[derive(Subcommand)]
pub enum ClaudeCodeCommands {
#[command(alias = "a")]
Account {
#[command(subcommand)]
action: Option<AccountAction>,
},
#[command(about = "Run claude with configuration")]
Run {
#[arg(
short = 'i',
long,
help = "Interactive mode: prompt for configuration and arguments"
)]
interactive: bool,
#[arg(long = "group", help = "Configuration group name to use")]
group: Option<String>,
#[arg(
trailing_var_arg = true,
allow_hyphen_values = true,
help = "Arguments to pass to claude (use -- to separate from run options)"
)]
args: Vec<String>,
},
Stats(ClaudeCodeStatsArgs),
Prompts(ClaudeCodePromptsArgs),
#[command(name = "sync-ignore", alias = "si")]
SyncIgnore(ClaudeCodeSyncIgnoreArgs),
}
#[derive(Args, Debug, Clone)]
pub struct ClaudeCodeSyncIgnoreArgs {
#[arg(short = 'y', long, action = clap::ArgAction::SetTrue)]
pub yes: bool,
#[arg(long, action = clap::ArgAction::SetTrue)]
pub interactive: bool,
#[arg(long, action = clap::ArgAction::SetTrue)]
pub force: bool,
#[arg(long, short = 'v', action = clap::ArgAction::SetTrue)]
pub verbose: bool,
#[arg(
long,
short = 't',
value_enum,
value_delimiter = ',',
default_value = "claude-shared"
)]
pub target: Vec<SyncIgnoreTarget>,
#[arg(long, short = 'i')]
pub input: Vec<PathBuf>,
}
#[derive(Subcommand)]
pub enum AccountAction {
Edit,
#[command(about = "List all configuration groups")]
List,
#[command(about = "Import a configuration group from JSON")]
Import {
#[arg(short, long, action = clap::ArgAction::SetTrue)]
force: bool,
},
#[command(about = "Use/select a configuration group")]
Use {
#[arg(help = "Name of the configuration group to use")]
name: String,
#[arg(
trailing_var_arg = true,
allow_hyphen_values = true,
help = "Arguments to pass to claude"
)]
args: Vec<String>,
},
#[command(about = "Emit env injection statements for a group")]
Env {
#[arg(help = "Name of the configuration group")]
name: String,
},
}
pub fn run(args: &ClaudeCodeArgs) -> Result<()> {
match &args.command {
Some(ClaudeCodeCommands::Account { action }) => {
handle_account_command(action.as_ref())?;
}
Some(ClaudeCodeCommands::Run {
interactive,
group,
args,
}) => {
handle_run_command(*interactive, group.as_deref(), args.clone())?;
}
Some(ClaudeCodeCommands::Stats(stats)) => crate::x::claude_code::stats::run_stats(stats)?,
Some(ClaudeCodeCommands::Prompts(prompts)) => crate::x::claude_code::prompts::run(prompts)?,
Some(ClaudeCodeCommands::SyncIgnore(sync_args)) => {
crate::tool::sync_ignore::run(&ToolSyncIgnoreArgs {
yes: sync_args.yes,
interactive: sync_args.interactive,
force: sync_args.force,
verbose: sync_args.verbose,
target: sync_args.target.clone(),
input: sync_args.input.clone(),
})?;
}
None => {
handle_main_command(&args.args)?;
}
}
Ok(())
}
fn handle_main_command(args: &[String]) -> Result<()> {
let config = Config::load().context(t!("claude_code.error.load_config_failed"))?;
if config.is_empty() {
bail!(no_configs_message());
}
if let Some(selected_group) = interactive::select_config_group(&config)? {
let group = config.get_group(&selected_group).ok_or_else(|| {
anyhow::anyhow!(format!(
"{}\n{}",
t!("claude_code.account.group_not_found", name = selected_group),
t!("claude_code.account.use_list_command")
))
})?;
let security_checker = SecurityChecker::from_config(&config)?;
if let Ok(warnings) = security_checker.check_claude_settings() {
print_security_warnings(&warnings);
}
let mut cmd = Command::new("claude");
inject_env_vars(&mut cmd, group);
for arg in args {
cmd.arg(arg);
}
let status = cmd
.status()
.context(t!("claude_code.error.execute_failed"))?;
if !status.success() {
bail!(t!("claude_code.error.failed_claude_command"));
}
}
Ok(())
}
fn handle_account_command(action: Option<&AccountAction>) -> Result<()> {
if matches!(action, Some(AccountAction::Edit)) {
handle_account_edit()?;
return Ok(());
}
let mut config = Config::load().context(t!("claude_code.error.load_config_failed"))?;
match action {
Some(cli_action) => execute_account_action(&mut config, cli_action)?,
None => handle_list_groups(&config),
}
Ok(())
}
fn execute_account_action(config: &mut Config, action: &AccountAction) -> Result<()> {
match action {
AccountAction::Edit => unreachable!("Edit is handled before config load"),
AccountAction::List => handle_list_groups(config),
AccountAction::Import { force } => handle_import_group(config, *force)?,
AccountAction::Use { name, args } => handle_use_group(config, name, args.clone())?,
AccountAction::Env { name } => handle_env_group(config, name)?,
}
Ok(())
}
fn handle_account_edit() -> Result<()> {
let config_path = Config::config_file_path()?;
let editor_raw = select_editor_raw();
handle_account_edit_with(&config_path, &editor_raw)
}
fn handle_account_edit_with(config_path: &Path, editor_raw: &str) -> Result<()> {
if let Some(parent) = safe_parent_for_creation(config_path) {
fs::create_dir_all(parent).context(t!(
"claude_code.config.create_dir_failed",
path = parent.display()
))?;
}
let template = include_str!("../../../templates/claude-code/default.toml");
let created = atomic_write_new_with_mode(config_path, template.as_bytes(), Some(0o600))
.context(t!(
"claude_code.config.write_failed",
path = config_path.display()
))?;
if created {
println!(
"{}",
t!(
"claude_code.account.config_created",
path = config_path.display()
)
);
}
if !created {
println!(
"{}",
t!("claude_code.account.editing", path = config_path.display())
);
}
let (editor_cmd, editor_args) = parse_editor_command(editor_raw).map_err(|e| {
anyhow::anyhow!(t!(
"claude_code.error.invalid_editor_command",
editor = editor_raw,
error = e
))
})?;
let status = Command::new(&editor_cmd)
.args(editor_args)
.arg(config_path)
.status()
.context(t!(
"claude_code.error.open_editor_failed",
editor = editor_raw
))?;
if !status.success() {
bail!(t!("claude_code.error.editor_exit_status", status = status));
}
println!("{}", t!("claude_code.account.edited"));
Ok(())
}
fn handle_import_group(config: &mut Config, force: bool) -> Result<()> {
if let Some((name, group)) = interactive::prompt_import_config()? {
if config.groups.contains_key(&name) {
if !force {
bail!(
"{}\n{}",
t!("claude_code.account.group_exists", name = name),
t!("claude_code.account.use_different_name_or_force")
);
} else {
println!(
"{}",
t!("claude_code.account.overwriting_group", name = name)
);
}
}
config.add_group(name.clone(), group);
config
.save()
.with_context(|| t!("claude_code.error.save_after_import_failed"))?;
println!("{}", t!("claude_code.account.import_success", name = name));
} else {
println!("{}", t!("claude_code.interactive.import_cancelled"));
}
Ok(())
}
fn handle_list_groups(config: &Config) {
interactive::display_config_list(config);
}
fn handle_use_group(config: &Config, name: &str, args: Vec<String>) -> Result<()> {
if let Some(group) = config.get_group(name) {
let security_checker = SecurityChecker::from_config(config)?;
if let Ok(warnings) = security_checker.check_claude_settings() {
print_security_warnings(&warnings);
}
let mut cmd = Command::new("claude");
inject_env_vars(&mut cmd, group);
for arg in args {
cmd.arg(arg);
}
let status = cmd
.status()
.context(t!("claude_code.error.execute_failed"))?;
if !status.success() {
bail!(t!("claude_code.error.failed_claude_command"));
}
} else {
bail!(
"{}\n{}",
t!("claude_code.account.group_not_found", name = name),
t!("claude_code.account.use_list_command")
);
}
Ok(())
}
fn handle_env_group(config: &Config, name: &str) -> Result<()> {
if config.is_empty() {
bail!(no_configs_message());
}
let group = config.get_group(name).ok_or_else(|| {
anyhow::anyhow!(format!(
"{}\n{}",
t!("claude_code.account.group_not_found", name = name),
t!("claude_code.account.use_list_command")
))
})?;
let syntax = env_syntax_for_current_platform();
let lines = render_env_injection_lines(group, syntax)?;
match syntax {
EnvSyntax::PosixExport => {
println!("# Bash/Zsh: source <(llman x claude-code account env {name}) && ...")
}
EnvSyntax::PowerShell => println!(
"# PowerShell: llman x claude-code account env {name} | Out-String | Invoke-Expression"
),
}
for line in lines {
println!("{line}");
}
Ok(())
}
fn handle_run_command(
interactive: bool,
group_name: Option<&str>,
args: Vec<String>,
) -> Result<()> {
let config = Config::load().context(t!("claude_code.error.load_config_failed"))?;
if config.is_empty() {
bail!(no_configs_message());
}
if !interactive && group_name.is_none() {
bail!(
"{}\n{}",
t!("claude_code.run.error.group_required_non_interactive"),
t!("claude_code.run.error.use_i_or_group")
);
}
let (selected_group, claude_args) = if interactive {
handle_interactive_mode(&config)?
} else {
let group = group_name.unwrap().to_string();
(group, args)
};
if let Some(env_vars) = config.get_group(&selected_group) {
println!(
"{}",
t!("claude_code.run.using_config", name = selected_group)
);
let security_checker = SecurityChecker::from_config(&config)?;
if let Ok(warnings) = security_checker.check_claude_settings() {
print_security_warnings(&warnings);
}
let mut cmd = Command::new("claude");
inject_env_vars(&mut cmd, env_vars);
for arg in claude_args {
cmd.arg(arg);
}
let status = cmd
.status()
.context(t!("claude_code.error.execute_failed"))?;
if !status.success() {
bail!(t!("claude_code.error.failed_claude_command"));
}
} else {
bail!(
"{}\n{}",
t!("claude_code.account.group_not_found", name = selected_group),
t!("claude_code.account.use_list_command")
);
}
Ok(())
}
fn no_configs_message() -> String {
let config_path = Config::config_file_path()
.map(|p| p.display().to_string())
.unwrap_or_else(|_| t!("claude_code.main.unknown_path").to_string());
format!(
"{}\n\n{}\n {}\n {}\n\n{}:\n {}",
t!("claude_code.main.no_configs_found"),
t!("claude_code.main.suggestion_import"),
t!("claude_code.main.command_import"),
t!("claude_code.main.command_edit"),
t!("claude_code.main.alternative_config"),
config_path
)
}
fn handle_interactive_mode(config: &Config) -> Result<(String, Vec<String>)> {
let selected_group = interactive::select_config_group(config)?
.ok_or_else(|| anyhow::anyhow!(t!("claude_code.error.no_configuration_selected")))?;
let use_args = inquire::Confirm::new(&t!("claude_code.run.interactive.prompt_args"))
.with_default(false)
.prompt()
.context(t!("claude_code.error.prompt_args_failed"))?;
let claude_args = if use_args {
loop {
let args_text = inquire::Text::new(&t!("claude_code.run.interactive.enter_args"))
.with_help_message(&t!("claude_code.run.interactive.args_help"))
.prompt()
.context(t!("claude_code.error.args_input_failed"))?;
match split_shell_args(&args_text) {
Ok(args) => break args,
Err(e) => {
eprintln!(
"{}",
t!("claude_code.run.interactive.args_parse_failed", error = e)
);
continue;
}
}
}
} else {
Vec::new()
};
Ok((selected_group, claude_args))
}
fn print_security_warnings(warnings: &[SecurityWarning]) {
if warnings.is_empty() {
return;
}
eprintln!(
"\n🔒 {}",
t!(
"claude_code.security.warning_header",
count = warnings.len()
)
);
eprintln!(
"{}",
t!("claude_code.security.warning_separator_char").repeat(60)
);
for warning in warnings {
eprintln!(
"\n{} [{}] {}",
warning.severity.display_symbol(),
warning.severity.display_name_localized(),
t!("claude_code.security.warning_item_title")
);
eprintln!(
" 📍 {} {}",
t!("claude_code.security.label_location"),
warning.config_path
);
eprintln!(
" ⚙️ {} {}",
t!("claude_code.security.label_setting"),
warning.config_item
);
eprintln!(
" 🎯 {} {}",
t!("claude_code.security.label_pattern"),
warning.matched_pattern
);
eprintln!(
" 📝 {} {}",
t!("claude_code.security.label_description"),
warning.description
);
eprintln!(
" 💡 {} {}",
t!("claude_code.security.label_recommendation"),
warning.recommendation
);
}
eprintln!("\n⚠️ {}", t!("claude_code.security.footer_line1"));
eprintln!(" {}", t!("claude_code.security.footer_line2"));
eprintln!();
}
fn inject_env_vars(cmd: &mut Command, group: &ConfigGroup) {
for (key, value) in group {
cmd.env(key, value);
}
}