1use super::*;
2
3use roboticus_core::{ProfileEntry, ProfileRegistry, home_dir};
4
5pub fn cmd_profile_list() -> Result<(), Box<dyn std::error::Error>> {
7 let (DIM, BOLD, ACCENT, GREEN, YELLOW, RED, CYAN, RESET, MONO) = colors();
8 let (OK, ACTION, WARN, DETAIL, ERR) = icons();
9
10 let registry = ProfileRegistry::load()?;
11 let profiles = registry.list();
12
13 heading("Installed Profiles");
14
15 if profiles.is_empty() {
16 empty_state("No profiles found. Run `roboticus profile create <name>` to add one.");
17 return Ok(());
18 }
19
20 let widths = [20, 24, 12, 8];
21 table_header(&["ID", "Name", "Source", "Active"], &widths);
22
23 for (id, entry) in &profiles {
24 let active_str = if entry.active {
25 format!("{GREEN}{OK}{RESET}")
26 } else {
27 String::new()
28 };
29 let source = entry.source.as_deref().unwrap_or("manual");
30 table_row(
31 &[
32 format!("{ACCENT}{id}{RESET}"),
33 entry.name.clone(),
34 source.to_string(),
35 active_str,
36 ],
37 &widths,
38 );
39 }
40
41 eprintln!();
42 eprintln!(" {DIM}{} profile(s){RESET}", profiles.len());
43 eprintln!();
44 Ok(())
45}
46
47pub fn cmd_profile_create(
49 name: &str,
50 display_name: Option<&str>,
51) -> Result<(), Box<dyn std::error::Error>> {
52 let (DIM, BOLD, ACCENT, GREEN, YELLOW, RED, CYAN, RESET, MONO) = colors();
53 let (OK, ACTION, WARN, DETAIL, ERR) = icons();
54
55 validate_profile_id(name)?;
56
57 let mut registry = ProfileRegistry::load()?;
58
59 if registry.profiles.contains_key(name) {
60 return Err(format!("profile '{name}' already exists").into());
61 }
62
63 let display = display_name.unwrap_or(name).to_string();
64 let rel_path = format!("profiles/{name}");
65
66 let entry = ProfileEntry {
67 name: display.clone(),
68 description: None,
69 path: rel_path.clone(),
70 active: false,
71 installed_at: Some(chrono_now()),
72 version: None,
73 source: Some("manual".to_string()),
74 };
75
76 registry.profiles.insert(name.to_string(), entry);
77 registry.save()?;
78
79 registry.ensure_profile_dir(name)?;
81
82 eprintln!(" {GREEN}{OK}{RESET} Created profile {ACCENT}{name}{RESET} ({display})");
83 eprintln!(
84 " {DIM}Directory: {}{RESET}",
85 home_dir().join(".roboticus").join(&rel_path).display()
86 );
87 eprintln!(
88 " {DIM}Run {BOLD}roboticus profile switch {name}{RESET}{DIM} to activate it.{RESET}"
89 );
90 eprintln!();
91 Ok(())
92}
93
94pub fn cmd_profile_switch(name: &str) -> Result<(), Box<dyn std::error::Error>> {
96 let (DIM, BOLD, ACCENT, GREEN, YELLOW, RED, CYAN, RESET, MONO) = colors();
97 let (OK, ACTION, WARN, DETAIL, ERR) = icons();
98
99 let mut registry = ProfileRegistry::load()?;
100
101 if !registry.profiles.contains_key(name) {
102 return Err(format!(
103 "profile '{name}' not found. Run `roboticus profile list` to see available profiles."
104 )
105 .into());
106 }
107
108 for (id, entry) in registry.profiles.iter_mut() {
110 entry.active = id == name;
111 }
112
113 registry.save()?;
114
115 eprintln!(" {GREEN}{OK}{RESET} Switched to profile {ACCENT}{name}{RESET}");
116 eprintln!(" {DIM}Restart the daemon for the change to take effect.{RESET}");
117 eprintln!();
118 Ok(())
119}
120
121pub fn cmd_profile_delete(name: &str, keep_data: bool) -> Result<(), Box<dyn std::error::Error>> {
125 let (DIM, BOLD, ACCENT, GREEN, YELLOW, RED, CYAN, RESET, MONO) = colors();
126 let (OK, ACTION, WARN, DETAIL, ERR) = icons();
127
128 if name == "default" {
129 return Err("cannot delete the built-in 'default' profile".into());
130 }
131
132 let mut registry = ProfileRegistry::load()?;
133
134 let entry = registry
135 .profiles
136 .get(name)
137 .cloned()
138 .ok_or_else(|| format!("profile '{name}' not found"))?;
139
140 if entry.active {
141 return Err(format!(
142 "profile '{name}' is currently active. Switch to a different profile first."
143 )
144 .into());
145 }
146
147 let profile_dir = registry.resolve_config_dir(name)?;
148
149 registry.profiles.remove(name);
150 registry.save()?;
151
152 if !keep_data && profile_dir.exists() {
153 let safe_root = home_dir().join(".roboticus").join("profiles");
155 if profile_dir.starts_with(&safe_root) {
156 std::fs::remove_dir_all(&profile_dir).map_err(|e| {
157 format!(
158 "removed from registry but could not delete directory {}: {e}",
159 profile_dir.display()
160 )
161 })?;
162 eprintln!(" {GREEN}{OK}{RESET} Deleted profile {ACCENT}{name}{RESET} and its data");
163 } else {
164 eprintln!(
165 " {WARN} Removed profile {ACCENT}{name}{RESET} from registry. \
166 Directory {} is outside the managed tree and was not removed.",
167 profile_dir.display()
168 );
169 }
170 } else {
171 eprintln!(
172 " {GREEN}{OK}{RESET} Removed profile {ACCENT}{name}{RESET} from registry \
173 (data kept at {})",
174 profile_dir.display()
175 );
176 }
177
178 eprintln!();
179 Ok(())
180}
181
182fn validate_profile_id(name: &str) -> Result<(), Box<dyn std::error::Error>> {
185 if name.is_empty() {
186 return Err("profile name must not be empty".into());
187 }
188 if !name
189 .chars()
190 .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
191 {
192 return Err(
193 "profile name may only contain ASCII letters, digits, hyphens, and underscores".into(),
194 );
195 }
196 Ok(())
197}
198
199fn chrono_now() -> String {
200 use std::time::{SystemTime, UNIX_EPOCH};
202 let secs = SystemTime::now()
203 .duration_since(UNIX_EPOCH)
204 .map(|d| d.as_secs())
205 .unwrap_or(0);
206 format!("{secs}")
209}