oboron_cli_core/
commands.rs1use anyhow::{anyhow, Context, Result};
10
11use crate::config::{load_config, save_config, Config};
12use crate::key::normalize_key_to_hex;
13use crate::paths::{config_path, profile_path};
14use crate::profile::{
15 delete_profile, list_profiles, load_profile, load_profile_key,
16 rename_profile, save_profile, validate_profile_name, KeyProfile,
17};
18
19pub fn load_profile_key_with_notice(name: &str) -> Result<String> {
27 let loaded = load_profile_key(name)?;
28 if let Some(backup) = &loaded.migrated_backup {
29 eprintln!(
30 "notice: profile '{name}' had a legacy base64 key; \
31 rewrote it as canonical hex (backup: {})",
32 backup.display(),
33 );
34 eprintln!(
35 " base64 keys are deprecated and will be removed before \
36 oboron 1.0."
37 );
38 }
39 Ok(loaded.hex)
40}
41
42pub struct CliInfo<'a> {
44 pub binary_name: &'a str,
46 pub default_scheme: &'a str,
48 pub default_encoding: Option<&'a str>,
51}
52
53fn require_config(info: &CliInfo<'_>) -> Result<Config> {
55 load_config()?.ok_or_else(|| {
56 let p = config_path()
57 .map(|p| p.display().to_string())
58 .unwrap_or_else(|_| "~/.oboron/config.json".into());
59 anyhow!(
60 "config not found at {p}\nHint: run '{} init' to create one",
61 info.binary_name
62 )
63 })
64}
65
66pub fn init_command(
67 info: &CliInfo<'_>,
68 name: &str,
69 generate_key: impl FnOnce() -> String,
70) -> Result<()> {
71 validate_profile_name(name)?;
72 let path = profile_path(name)?;
73 if path.exists() {
74 eprintln!("❌ Error: Profile '{name}' already exists");
75 eprintln!();
76 eprintln!("'{} init' will not overwrite an existing profile.", info.binary_name);
77 eprintln!();
78 eprintln!("Options:");
79 eprintln!(" {} init <new-profile-name>", info.binary_name);
80 eprintln!(" {} profile delete {name}", info.binary_name);
81 eprintln!(" {} profile create <profile-name>", info.binary_name);
82 anyhow::bail!("profile '{name}' already exists");
83 }
84
85 let key = generate_key();
86 save_profile(name, &KeyProfile { key: Some(key.clone()) })?;
87 save_config(&Config {
88 profile: Some(name.to_string()),
89 scheme: Some(info.default_scheme.to_string()),
90 encoding: info.default_encoding.map(str::to_string),
91 })?;
92
93 let cfg_path = config_path()?;
94 println!("✓ Configuration saved to {}", cfg_path.display());
95 println!("\nYour profile '{name}':");
96 println!(" Default scheme: {}", info.default_scheme);
97 if let Some(enc) = info.default_encoding {
98 println!(" Default encoding: {enc}");
99 }
100 println!(" Key: {key}");
101 println!("\n⚠️ Keep this key secure! Anyone with it can decode your data.");
102
103 Ok(())
104}
105
106pub fn config_show_command(info: &CliInfo<'_>) -> Result<()> {
107 let config = require_config(info)?;
108 let profile_name = config
109 .profile
110 .as_deref()
111 .ok_or_else(|| anyhow!("config has no active profile"))?;
112 let key_hex = load_profile_key_with_notice(profile_name)?;
115
116 println!("Current configuration:");
117 println!(" Profile: {profile_name}");
118 if let Some(s) = &config.scheme {
119 println!(" Scheme: {s}");
120 }
121 if let Some(e) = &config.encoding {
122 println!(" Encoding: {e}");
123 }
124 println!(" Key: {key_hex}");
125
126 Ok(())
127}
128
129pub fn config_set_command(
130 info: &CliInfo<'_>,
131 scheme: Option<String>,
132 encoding: Option<String>,
133 profile: Option<String>,
134) -> Result<()> {
135 let mut config = load_config()?.unwrap_or_else(|| Config {
136 profile: Some("default".to_string()),
137 scheme: Some(info.default_scheme.to_string()),
138 encoding: info.default_encoding.map(str::to_string),
139 });
140
141 if let Some(s) = scheme {
142 config.scheme = Some(s);
143 }
144 if let Some(e) = encoding {
145 config.encoding = Some(e);
146 }
147 if let Some(p) = profile {
148 config.profile = Some(p);
149 }
150
151 save_config(&config)?;
152 println!("✓ Configuration updated");
153 if let Some(p) = &config.profile {
154 println!(" Profile: {p}");
155 }
156 if let Some(s) = &config.scheme {
157 println!(" Scheme: {s}");
158 }
159 if let Some(e) = &config.encoding {
160 println!(" Encoding: {e}");
161 }
162 Ok(())
163}
164
165pub fn profile_list_command(info: &CliInfo<'_>) -> Result<()> {
166 let profiles = list_profiles()?;
167 if profiles.is_empty() {
168 println!("No profiles found. Run '{} init' to create one.", info.binary_name);
169 return Ok(());
170 }
171
172 let active = load_config().ok().flatten().and_then(|c| c.profile);
173 println!("Available profiles:");
174 for p in profiles {
175 let marker = if Some(p.as_str()) == active.as_deref() {
176 " (active)"
177 } else {
178 ""
179 };
180 println!(" {p}{marker}");
181 }
182 Ok(())
183}
184
185pub fn profile_show_command(info: &CliInfo<'_>, name: Option<&str>) -> Result<()> {
186 let profile_name = match name {
187 Some(n) => n.to_string(),
188 None => require_config(info)?
189 .profile
190 .ok_or_else(|| anyhow!("config has no active profile"))?,
191 };
192 let key_hex = load_profile_key_with_notice(&profile_name)?;
193 println!("Profile '{profile_name}':");
194 println!(" Key: {key_hex}");
195 Ok(())
196}
197
198pub fn profile_activate_command(info: &CliInfo<'_>, name: &str) -> Result<()> {
199 validate_profile_name(name)?;
200 load_profile(name)?; let mut cfg = load_config()?.unwrap_or_default();
203 cfg.profile = Some(name.to_string());
204 if cfg.scheme.is_none() {
205 cfg.scheme = Some(info.default_scheme.to_string());
206 }
207 if cfg.encoding.is_none() {
208 cfg.encoding = info.default_encoding.map(str::to_string);
209 }
210 save_config(&cfg)?;
211 println!("✓ Activated profile '{name}'");
212 Ok(())
213}
214
215pub fn profile_create_command(
216 name: &str,
217 key: Option<&str>,
218 generate_key: impl FnOnce() -> String,
219) -> Result<()> {
220 validate_profile_name(name)?;
221 let key_str = if let Some(k) = key {
222 normalize_key_to_hex(k).context("invalid --key")?
223 } else {
224 generate_key()
225 };
226 save_profile(name, &KeyProfile { key: Some(key_str.clone()) })?;
227 println!("✓ Created profile '{name}'");
228 println!(" Key: {key_str}");
229 println!("\n⚠️ Keep this profile secure!");
230 Ok(())
231}
232
233pub fn profile_delete_command(info: &CliInfo<'_>, name: &str) -> Result<()> {
234 validate_profile_name(name)?;
235 if let Ok(Some(cfg)) = load_config() {
236 if cfg.profile.as_deref() == Some(name) {
237 eprintln!("❌ Error: Cannot delete active profile '{name}'");
238 eprintln!();
239 eprintln!("Activate a different profile first:");
240 eprintln!(" {} profile activate <other-profile-name>", info.binary_name);
241 anyhow::bail!("cannot delete active profile '{name}'");
242 }
243 }
244
245 let backup = delete_profile(name)?;
246 println!("✓ Deleted profile '{name}'");
247 if let Some(p) = backup {
248 println!(" Backup saved to: {}", p.display());
249 }
250 Ok(())
251}
252
253pub fn profile_rename_command(old_name: &str, new_name: &str) -> Result<()> {
254 let backup = rename_profile(old_name, new_name)?;
255
256 if let Ok(Some(mut cfg)) = load_config() {
257 if cfg.profile.as_deref() == Some(old_name) {
258 cfg.profile = Some(new_name.to_string());
259 save_config(&cfg)?;
260 println!(
261 "✓ Renamed profile '{old_name}' to '{new_name}' (active profile updated)"
262 );
263 } else {
264 println!("✓ Renamed profile '{old_name}' to '{new_name}'");
265 }
266 } else {
267 println!("✓ Renamed profile '{old_name}' to '{new_name}'");
268 }
269
270 if let Some(p) = backup {
271 println!(" Backup saved to: {}", p.display());
272 }
273 Ok(())
274}
275
276pub fn profile_set_command(name: &str, key: &str) -> Result<()> {
277 validate_profile_name(name)?;
278 let mut profile = load_profile(name)?;
279 profile.key = Some(normalize_key_to_hex(key).context("invalid --key")?);
280 save_profile(name, &profile)?;
281 println!("✓ Updated profile '{name}'");
282 Ok(())
283}
284
285pub fn key_command(info: &CliInfo<'_>, profile_name: Option<&str>) -> Result<()> {
286 let cfg = load_config().ok().flatten();
287 let active = cfg.as_ref().and_then(|c| c.profile.as_deref());
288
289 let prof = profile_name.or(active).ok_or_else(|| {
290 anyhow!(
291 "no profile given and no active profile in config; \
292 run '{} init' or pass --profile",
293 info.binary_name
294 )
295 })?;
296 let key_hex = load_profile_key_with_notice(prof)?;
297 println!("{key_hex}");
298 Ok(())
299}