1use crate::{
2 Configurable, CredentialManager,
3 credentials::{CredentialStore, get_api_key_interactively},
4 settings::{ClaudeSettings, format_settings_for_display},
5 snapshots::{SnapshotScope, SnapshotStore},
6 templates::{TemplateType, get_template, 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.environment = 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) && !overwrite {
111 if !confirm_action(
112 &format!("Snapshot '{}' already exists. Overwrite?", name),
113 false,
114 )? {
115 return Ok(());
116 }
117 }
118
119 let snapshot = crate::Snapshot::new(
120 name.to_string(),
121 snapshot_settings,
122 scope.clone(),
123 description.clone(),
124 );
125
126 store.save(&snapshot)?;
127 println!(
128 "{} Snapshot '{}' created successfully!",
129 style("✓").green().bold(),
130 name
131 );
132
133 Ok(())
134}
135
136pub fn apply_command(
138 target: &str,
139 scope: &SnapshotScope,
140 model: &Option<String>,
141 settings_path: &Option<PathBuf>,
142 backup: bool,
143 yes: bool,
144) -> Result<()> {
145 let settings_path = get_settings_path(settings_path.clone());
146
147 if let Ok(template_type) = get_template_type(target) {
149 return apply_template_command(&template_type, scope, model, &settings_path, backup, yes);
150 }
151
152 apply_snapshot_command(target, scope, model, &settings_path, backup, yes)
154}
155
156fn apply_template_command(
158 template_type: &TemplateType,
159 scope: &SnapshotScope,
160 model: &Option<String>,
161 settings_path: &PathBuf,
162 backup: bool,
163 yes: bool,
164) -> Result<()> {
165 let template = get_template(template_type);
166 let api_key = get_api_key_interactively(template_type.clone())?;
168
169 let mut settings = template(&api_key, scope);
170
171 if let Some(model_name) = model {
173 if let Some(ref mut model_config) = settings.model {
174 model_config.name = model_name.clone();
175 }
176 }
177
178 let existing_settings = ClaudeSettings::from_file(settings_path)?;
180
181 if backup {
183 backup_settings(settings_path)?;
184 }
185
186 if !yes {
188 let existing_masked = existing_settings.clone().mask_sensitive_data();
189 let new_masked = settings.clone().mask_sensitive_data();
190
191 println!("Current settings:");
192 println!("{}", format_settings_for_display(&existing_masked, false));
193 println!("\nNew settings:");
194 println!("{}", format_settings_for_display(&new_masked, false));
195
196 if !confirm_action("Apply these settings?", false)? {
197 return Ok(());
198 }
199 }
200
201 let final_settings = settings.merge_with(existing_settings);
202
203 final_settings.to_file(settings_path)?;
205
206 println!(
207 "{} Applied template '{}' successfully!",
208 style("✓").green().bold(),
209 template_type
210 );
211
212 Ok(())
213}
214
215fn apply_snapshot_command(
217 snapshot_name: &str,
218 scope: &SnapshotScope,
219 model: &Option<String>,
220 settings_path: &PathBuf,
221 backup: bool,
222 yes: bool,
223) -> Result<()> {
224 let snapshots_dir = get_snapshots_dir();
225 let store = SnapshotStore::new(snapshots_dir);
226
227 let mut snapshot = store.load_by_name(snapshot_name)?;
228
229 snapshot.settings = snapshot.settings.filter_by_scope(scope);
231
232 if let Some(model_name) = model {
234 if let Some(ref mut model_config) = snapshot.settings.model {
235 model_config.name = model_name.clone();
236 }
237 }
238
239 let existing_settings = ClaudeSettings::from_file(settings_path)?;
241
242 if backup {
244 backup_settings(settings_path)?;
245 }
246
247 if !yes {
249 let existing_masked = existing_settings.clone().mask_sensitive_data();
250 let snapshot_masked = snapshot.settings.clone().mask_sensitive_data();
251
252 println!("Current settings:");
253 println!("{}", format_settings_for_display(&existing_masked, false));
254 println!("\nSnapshot settings:");
255 println!("{}", format_settings_for_display(&snapshot_masked, false));
256
257 if !confirm_action("Apply these settings?", false)? {
258 return Ok(());
259 }
260 }
261
262 let final_settings = snapshot.settings.merge_with(existing_settings);
263
264 final_settings.to_file(settings_path)?;
266
267 println!(
268 "{} Applied snapshot '{}' successfully!",
269 style("✓").green().bold(),
270 snapshot_name
271 );
272
273 Ok(())
274}
275
276pub fn delete_command(name: &str, yes: bool) -> Result<()> {
278 let snapshots_dir = get_snapshots_dir();
279 let store = SnapshotStore::new(snapshots_dir);
280
281 if !store.exists_by_name(name) {
282 return Err(anyhow!("Snapshot '{}' not found", name));
283 }
284
285 if !yes {
286 if !confirm_action(&format!("Delete snapshot '{}'?", name), false)? {
287 return Ok(());
288 }
289 }
290
291 store.delete_by_name(name)?;
292 println!(
293 "{} Deleted snapshot '{}' successfully!",
294 style("✓").green().bold(),
295 name
296 );
297
298 Ok(())
299}
300
301pub fn credentials_list_command() -> Result<()> {
303 let _credentials_dir = get_credentials_dir();
304 let credential_store = CredentialStore::new()?;
305
306 let credentials = credential_store.load_credentials()?;
307
308 if credentials.is_empty() {
309 println!("No saved credentials found.");
310 return Ok(());
311 }
312
313 println!("Saved credentials ({} total):", credentials.len());
314
315 for credential in &credentials {
316 let template_type = credential.template_type();
317 let masked_key = mask_api_key(credential.api_key());
318
319 println!(
320 "{}: {} ({} - {})",
321 style(credential.id()).cyan().bold(),
322 credential.name(),
323 template_type,
324 masked_key
325 );
326 }
327
328 Ok(())
329}
330
331pub fn credentials_delete_command(id: &str) -> Result<()> {
333 let _credentials_dir = get_credentials_dir();
334 let credential_store = CredentialStore::new()?;
335
336 if credential_store.delete_credential(id).is_err() {
337 return Err(anyhow!("Credential '{}' not found", id));
338 }
339
340 println!(
341 "{} Deleted credential '{}' successfully!",
342 style("✓").green().bold(),
343 id
344 );
345
346 Ok(())
347}
348
349pub fn credentials_clear_command(yes: bool) -> Result<()> {
351 if !yes {
352 if !confirm_action("Clear all saved credentials?", false)? {
353 return Ok(());
354 }
355 }
356
357 let _credentials_dir = get_credentials_dir();
358 let credential_store = CredentialStore::new()?;
359
360 credential_store.clear_credentials()?;
361
362 println!("{} Cleared all credentials!", style("✓").green().bold());
363
364 Ok(())
365}
366
367fn mask_api_key(api_key: &str) -> String {
369 if api_key.len() <= 8 {
370 "••••••••".to_string()
371 } else {
372 format!("{}••••••••", &api_key[..api_key.len().min(8)])
373 }
374}