1use crate::{
2 Configurable, CredentialManager,
3 credentials::{CredentialStore, get_api_key_interactively},
4 settings::{ClaudeSettings, format_settings_comparison, format_settings_for_display},
5 snapshots::{SnapshotScope, SnapshotStore},
6 templates::{Template, TemplateType, get_template_instance_with_input, get_template_type},
7 utils::{
8 backup_settings, confirm_action, get_credentials_dir, get_settings_path, get_snapshots_dir,
9 },
10};
11use anyhow::{Result, anyhow};
12use console::style;
13use std::path::PathBuf;
14
15pub fn run_command(args: &crate::Cli) -> Result<()> {
17 match &args.command {
18 crate::Commands::List { verbose } => list_command(*verbose)?,
19 crate::Commands::Snap {
20 name,
21 scope,
22 settings_path,
23 description,
24 overwrite,
25 } => snap_command(name, scope, settings_path, description, *overwrite)?,
26 crate::Commands::Apply {
27 target,
28 scope,
29 model,
30 settings_path,
31 backup,
32 yes,
33 } => apply_command(target, scope, model, settings_path, *backup, *yes)?,
34 crate::Commands::Delete { name, yes } => delete_command(name, *yes)?,
35 crate::Commands::Credentials(credential_commands) => match credential_commands {
36 crate::CredentialCommands::List => credentials_list_command()?,
37 crate::CredentialCommands::Delete { id } => credentials_delete_command(id)?,
38 crate::CredentialCommands::Clear { yes } => credentials_clear_command(*yes)?,
39 },
40 }
41 Ok(())
42}
43
44pub fn list_command(verbose: bool) -> Result<()> {
46 let snapshots_dir = crate::utils::get_snapshots_dir();
47 let store = SnapshotStore::new(snapshots_dir);
48 let snapshots = store.list()?;
49
50 if snapshots.is_empty() {
51 println!("No snapshots found.");
52 return Ok(());
53 }
54
55 println!("Available snapshots ({} total):", snapshots.len());
56
57 for snapshot in &snapshots {
58 if verbose {
59 println!("\n{} {}", style("Name:").bold(), snapshot.name);
60 println!("{} {}", style("ID:").bold(), snapshot.id);
61 if let Some(ref desc) = snapshot.description {
62 println!("{} {}", style("Description:").bold(), desc);
63 }
64 println!("{} {}", style("Scope:").bold(), snapshot.scope);
65 println!("{} {}", style("Created:").bold(), snapshot.created_at);
66 println!("{} {}", style("Updated:").bold(), snapshot.updated_at);
67
68 let masked_settings = snapshot.settings.clone().mask_sensitive_data();
69 println!(
70 "{}\n{}",
71 style("Settings:").bold(),
72 format_settings_for_display(&masked_settings, true)
73 );
74 } else {
75 println!(
76 "{}: {} (scope: {}, created: {})",
77 style(&snapshot.name).cyan().bold(),
78 snapshot.id,
79 snapshot.scope,
80 snapshot.created_at
81 );
82 }
83 println!();
84 }
85
86 Ok(())
87}
88
89pub fn snap_command(
91 name: &str,
92 scope: &SnapshotScope,
93 settings_path: &Option<PathBuf>,
94 description: &Option<String>,
95 overwrite: bool,
96) -> Result<()> {
97 let settings_path = get_settings_path(settings_path.clone());
98 let settings = ClaudeSettings::from_file(&settings_path)?;
99
100 let mut snapshot_settings = settings;
102
103 if matches!(scope, SnapshotScope::All | SnapshotScope::Env) {
104 snapshot_settings.env = Some(ClaudeSettings::capture_environment());
105 }
106
107 let snapshots_dir = crate::utils::get_snapshots_dir();
108 let store = SnapshotStore::new(snapshots_dir);
109
110 if store.exists_by_name(name)
111 && !overwrite
112 && !confirm_action(
113 &format!("Snapshot '{}' already exists. Overwrite?", name),
114 false,
115 )?
116 {
117 return Ok(());
118 }
119
120 let snapshot = crate::Snapshot::new(
121 name.to_string(),
122 snapshot_settings,
123 scope.clone(),
124 description.clone(),
125 );
126
127 store.save(&snapshot)?;
128 println!(
129 "{} Snapshot '{}' created successfully!",
130 style("✓").green().bold(),
131 name
132 );
133
134 Ok(())
135}
136
137pub fn apply_command(
139 target: &str,
140 scope: &SnapshotScope,
141 model: &Option<String>,
142 settings_path: &Option<PathBuf>,
143 backup: bool,
144 yes: bool,
145) -> Result<()> {
146 let settings_path = get_settings_path(settings_path.clone());
147
148 if let Ok(template_type) = get_template_type(target) {
150 return apply_template_command(
151 &template_type,
152 target,
153 scope,
154 model,
155 &settings_path,
156 backup,
157 yes,
158 );
159 }
160
161 apply_snapshot_command(target, scope, model, &settings_path, backup, yes)
163}
164
165fn apply_template_command(
167 template_type: &TemplateType,
168 target: &str,
169 scope: &SnapshotScope,
170 model: &Option<String>,
171 settings_path: &PathBuf,
172 backup: bool,
173 yes: bool,
174) -> Result<()> {
175 let initial_template = get_template_instance_with_input(template_type, target);
177
178 let template_instance = if initial_template.has_variants()
180 && ((target == "kat-coder" || target == "katcoder" || target == "kat")
181 || (target == "kimi")
182 || (target == "zai" || target == "glm" || target == "zhipu"))
183 {
184 match template_type {
186 crate::templates::TemplateType::KatCoder => {
187 let kat_coder_template =
188 crate::templates::kat_coder::KatCoderTemplate::create_interactively()?;
189 Box::new(kat_coder_template) as Box<dyn Template>
190 }
191 crate::templates::TemplateType::Kimi => {
192 let kimi_template = crate::templates::kimi::KimiTemplate::create_interactively()?;
193 Box::new(kimi_template) as Box<dyn Template>
194 }
195 crate::templates::TemplateType::Zai => {
196 let zai_template = crate::templates::zai::ZaiTemplate::create_interactively()?;
197 Box::new(zai_template) as Box<dyn Template>
198 }
199 _ => initial_template,
200 }
201 } else {
202 initial_template
203 };
204
205 let api_key = {
207 let env_var_name = template_instance.env_var_name();
208 if let Ok(api_key) = std::env::var(env_var_name)
209 && !api_key.trim().is_empty()
210 {
211 println!("✓ Using API key from environment variable {}", env_var_name);
212 api_key
213 } else {
214 get_api_key_interactively(template_type.clone())?
216 }
217 };
218
219 let mut settings = template_instance.create_settings(&api_key, scope);
220 println!(
222 "{}",
223 style("DEBUG: Settings to be applied:").yellow().bold()
224 );
225 println!("{}", format_settings_for_display(&settings, true));
226 if let Some(model_name) = model {
228 settings.model = Some(model_name.clone());
229 }
230
231 let existing_settings = ClaudeSettings::from_file(settings_path)?;
233
234 if backup {
236 backup_settings(settings_path)?;
237 }
238
239 if !yes {
241 let existing_masked = existing_settings.clone().mask_sensitive_data();
242 let new_masked = settings.clone().mask_sensitive_data();
243
244 let comparison = format_settings_comparison(&existing_masked, &new_masked);
245
246 if comparison == "Settings are identical." {
247 println!(
248 "{}",
249 style("Settings are already configured as requested.").green()
250 );
251 settings.to_file(settings_path)?;
254 return Ok(());
255 }
256
257 println!("Changes to be applied:");
258 println!("{}", comparison);
259
260 if !confirm_action("Apply these changes?", false)? {
261 return Ok(());
262 }
263 }
264
265 settings.to_file(settings_path)?;
267
268 println!(
269 "{} Applied template '{}' successfully!",
270 style("✓").green().bold(),
271 template_type
272 );
273
274 Ok(())
275}
276
277fn apply_snapshot_command(
279 snapshot_name: &str,
280 scope: &SnapshotScope,
281 model: &Option<String>,
282 settings_path: &PathBuf,
283 backup: bool,
284 yes: bool,
285) -> Result<()> {
286 let snapshots_dir = get_snapshots_dir();
287 let store = SnapshotStore::new(snapshots_dir);
288
289 let mut snapshot = store.load_by_name(snapshot_name)?;
290
291 snapshot.settings = snapshot.settings.filter_by_scope(scope);
293
294 if let Some(model_name) = model {
296 snapshot.settings.model = Some(model_name.clone());
297 }
298
299 let existing_settings = ClaudeSettings::from_file(settings_path)?;
301
302 if backup {
304 backup_settings(settings_path)?;
305 }
306
307 if !yes {
309 let existing_masked = existing_settings.clone().mask_sensitive_data();
310 let snapshot_masked = snapshot.settings.clone().mask_sensitive_data();
311
312 println!("Current settings:");
313 println!("{}", format_settings_for_display(&existing_masked, false));
314 println!("\nSnapshot settings:");
315 println!("{}", format_settings_for_display(&snapshot_masked, false));
316
317 if !confirm_action("Apply these settings?", false)? {
318 return Ok(());
319 }
320 }
321
322 snapshot.settings.to_file(settings_path)?;
324
325 println!(
326 "{} Applied snapshot '{}' successfully!",
327 style("✓").green().bold(),
328 snapshot_name
329 );
330
331 Ok(())
332}
333
334pub fn delete_command(name: &str, yes: bool) -> Result<()> {
336 let snapshots_dir = get_snapshots_dir();
337 let store = SnapshotStore::new(snapshots_dir);
338
339 if !store.exists_by_name(name) {
340 return Err(anyhow!("Snapshot '{}' not found", name));
341 }
342
343 if !yes && !confirm_action(&format!("Delete snapshot '{}'?", name), false)? {
344 return Ok(());
345 }
346
347 store.delete_by_name(name)?;
348 println!(
349 "{} Deleted snapshot '{}' successfully!",
350 style("✓").green().bold(),
351 name
352 );
353
354 Ok(())
355}
356
357pub fn credentials_list_command() -> Result<()> {
359 let _credentials_dir = get_credentials_dir();
360 let credential_store = CredentialStore::new()?;
361
362 let credentials = credential_store.load_credentials()?;
363
364 if credentials.is_empty() {
365 println!("No saved credentials found.");
366 return Ok(());
367 }
368
369 println!("Saved credentials ({} total):", credentials.len());
370
371 for credential in &credentials {
372 let template_type = credential.template_type();
373 let masked_key = mask_api_key(credential.api_key());
374
375 println!(
376 "{}: {} ({} - {})",
377 style(credential.id()).cyan().bold(),
378 credential.name(),
379 template_type,
380 masked_key
381 );
382 }
383
384 Ok(())
385}
386
387pub fn credentials_delete_command(id: &str) -> Result<()> {
389 let _credentials_dir = get_credentials_dir();
390 let credential_store = CredentialStore::new()?;
391
392 if credential_store.delete_credential(id).is_err() {
393 return Err(anyhow!("Credential '{}' not found", id));
394 }
395
396 println!(
397 "{} Deleted credential '{}' successfully!",
398 style("✓").green().bold(),
399 id
400 );
401
402 Ok(())
403}
404
405pub fn credentials_clear_command(yes: bool) -> Result<()> {
407 if !yes && !confirm_action("Clear all saved credentials?", false)? {
408 return Ok(());
409 }
410
411 let _credentials_dir = get_credentials_dir();
412 let credential_store = CredentialStore::new()?;
413
414 credential_store.clear_credentials()?;
415
416 println!("{} Cleared all credentials!", style("✓").green().bold());
417
418 Ok(())
419}
420
421fn mask_api_key(api_key: &str) -> String {
423 if api_key.len() <= 8 {
424 "••••••••".to_string()
425 } else {
426 format!("{}••••••••", &api_key[..api_key.len().min(8)])
427 }
428}