1use crate::core::{Command, CommandContext, CommandResult, ComponentType};
12use actr_config::{ConfigParser, RawConfig};
13use anyhow::{Result, bail};
14use async_trait::async_trait;
15use clap::{Args, Subcommand};
16use owo_colors::OwoColorize;
17use std::path::Path;
18
19#[derive(Args, Clone)]
20pub struct ConfigCommand {
21 #[arg(short = 'f', long = "file")]
23 pub config_file: Option<String>,
24
25 #[command(subcommand)]
26 pub command: ConfigSubcommand,
27}
28
29#[derive(Subcommand, Clone)]
30pub enum ConfigSubcommand {
31 Set {
33 key: String,
35 value: String,
37 },
38 Get {
40 key: String,
42 },
43 List,
45 Show {
47 #[arg(long, default_value = "toml")]
49 format: OutputFormat,
50 },
51 Unset {
53 key: String,
55 },
56 Test,
58}
59
60#[derive(Debug, Clone, clap::ValueEnum, Default)]
61pub enum OutputFormat {
62 #[default]
64 Toml,
65 Json,
67 Yaml,
69}
70
71impl ConfigCommand {
72 fn config_path(&self) -> &str {
74 self.config_file.as_deref().unwrap_or("Actr.toml")
75 }
76}
77
78#[async_trait]
79impl Command for ConfigCommand {
80 async fn execute(&self, _ctx: &CommandContext) -> Result<CommandResult> {
81 let config_path = self.config_path();
82
83 match &self.command {
84 ConfigSubcommand::Set { key, value } => self.set_config(config_path, key, value).await,
85 ConfigSubcommand::Get { key } => self.get_config(config_path, key).await,
86 ConfigSubcommand::List => self.list_config(config_path).await,
87 ConfigSubcommand::Show { format } => self.show_config(config_path, format).await,
88 ConfigSubcommand::Unset { key } => self.unset_config(config_path, key).await,
89 ConfigSubcommand::Test => self.test_config(config_path).await,
90 }
91 }
92
93 fn required_components(&self) -> Vec<ComponentType> {
94 vec![] }
96
97 fn name(&self) -> &str {
98 "config"
99 }
100
101 fn description(&self) -> &str {
102 "Manage project configuration"
103 }
104}
105
106impl ConfigCommand {
107 async fn set_config(&self, config_path: &str, key: &str, value: &str) -> Result<CommandResult> {
109 if !Path::new(config_path).exists() {
111 bail!(
112 "Configuration file not found: {}. Run 'actr init' to create a project first.",
113 config_path
114 );
115 }
116
117 let mut raw_config = RawConfig::from_file(config_path)?;
118
119 self.set_nested_value(&mut raw_config, key, value)?;
121
122 raw_config.save_to_file(config_path)?;
124
125 Ok(CommandResult::Success(format!(
126 "{} Configuration updated: {} = {}",
127 "โ
".green(),
128 key.cyan(),
129 value.yellow()
130 )))
131 }
132
133 async fn get_config(&self, config_path: &str, key: &str) -> Result<CommandResult> {
135 if !Path::new(config_path).exists() {
136 bail!("Configuration file not found: {}", config_path);
137 }
138
139 let raw_config = RawConfig::from_file(config_path)?;
140
141 let value = self.get_nested_value(&raw_config, key)?;
143
144 Ok(CommandResult::Success(value))
145 }
146
147 async fn list_config(&self, config_path: &str) -> Result<CommandResult> {
149 if !Path::new(config_path).exists() {
150 return Ok(CommandResult::Success(format!(
151 "{} No configuration file found at: {}",
152 "๐".yellow(),
153 config_path
154 )));
155 }
156
157 let raw_config = RawConfig::from_file(config_path)?;
158
159 let mut output = String::new();
160 output.push_str(&format!("{} Available configuration keys:\n", "๐".cyan()));
161 output.push('\n');
162
163 output.push_str(&format!(" {} Package:\n", "๐ฆ".blue()));
165 output.push_str(" package.name\n");
166 output.push_str(" package.actr_type.manufacturer\n");
167 output.push_str(" package.actr_type.name\n");
168 if raw_config.package.description.is_some() {
169 output.push_str(" package.description\n");
170 }
171
172 output.push_str(&format!("\n {} System:\n", "โ๏ธ".blue()));
174 output.push_str(" signaling.url\n");
175 output.push_str(" deployment.realm_id\n");
176 output.push_str(" discovery.visible\n");
177 output.push_str(" storage.mailbox_path\n");
178
179 output.push_str(&format!("\n {} WebRTC:\n", "๐".blue()));
181 output.push_str(" webrtc.stun_urls\n");
182 output.push_str(" webrtc.turn_urls\n");
183 output.push_str(" webrtc.force_relay\n");
184
185 output.push_str(&format!("\n {} Observability:\n", "๐".blue()));
187 output.push_str(" observability.filter_level\n");
188 output.push_str(" observability.tracing_enabled\n");
189 output.push_str(" observability.tracing_endpoint\n");
190 output.push_str(" observability.tracing_service_name\n");
191
192 if !raw_config.exports.is_empty() {
194 output.push_str(&format!(
195 "\n {} Exports ({} files):\n",
196 "๐ค".blue(),
197 raw_config.exports.len()
198 ));
199 for (i, export) in raw_config.exports.iter().enumerate() {
200 output.push_str(&format!(" exports[{}] = {}\n", i, export.display()));
201 }
202 }
203
204 if !raw_config.dependencies.is_empty() {
206 output.push_str(&format!(
207 "\n {} Dependencies ({}):\n",
208 "๐".blue(),
209 raw_config.dependencies.len()
210 ));
211 for key in raw_config.dependencies.keys() {
212 output.push_str(&format!(" dependencies.{}\n", key));
213 }
214 }
215
216 if !raw_config.scripts.is_empty() {
218 output.push_str(&format!(
219 "\n {} Scripts ({}):\n",
220 "๐".blue(),
221 raw_config.scripts.len()
222 ));
223 for key in raw_config.scripts.keys() {
224 output.push_str(&format!(" scripts.{}\n", key));
225 }
226 }
227
228 Ok(CommandResult::Success(output))
229 }
230
231 async fn show_config(&self, config_path: &str, format: &OutputFormat) -> Result<CommandResult> {
233 if !Path::new(config_path).exists() {
234 bail!("Configuration file not found: {}", config_path);
235 }
236
237 let raw_config = RawConfig::from_file(config_path)?;
238
239 let output = match format {
241 OutputFormat::Toml => toml::to_string_pretty(&raw_config)?,
242 OutputFormat::Json => serde_json::to_string_pretty(&raw_config)?,
243 OutputFormat::Yaml => serde_yaml::to_string(&raw_config)?,
244 };
245
246 Ok(CommandResult::Success(output))
247 }
248
249 async fn unset_config(&self, config_path: &str, key: &str) -> Result<CommandResult> {
251 if !Path::new(config_path).exists() {
252 bail!("Configuration file not found: {}", config_path);
253 }
254
255 let mut raw_config = RawConfig::from_file(config_path)?;
256
257 self.unset_nested_value(&mut raw_config, key)?;
259
260 raw_config.save_to_file(config_path)?;
262
263 Ok(CommandResult::Success(format!(
264 "{} Configuration key '{}' removed successfully",
265 "โ
".green(),
266 key.cyan()
267 )))
268 }
269
270 async fn test_config(&self, config_path: &str) -> Result<CommandResult> {
272 if !Path::new(config_path).exists() {
273 bail!("Configuration file not found: {}", config_path);
274 }
275
276 let mut output = String::new();
277 output.push_str(&format!(
278 "{} Testing configuration file: {}\n\n",
279 "๐งช".cyan(),
280 config_path
281 ));
282
283 let raw_config = match RawConfig::from_file(config_path) {
285 Ok(config) => {
286 output.push_str(&format!(
287 "{} Configuration file syntax is valid\n",
288 "โ
".green()
289 ));
290 config
291 }
292 Err(e) => {
293 bail!("Configuration file syntax error:\n {}", e);
294 }
295 };
296
297 match ConfigParser::from_file(config_path) {
299 Ok(config) => {
300 output.push_str(&format!(
301 "{} Configuration validation passed\n",
302 "โ
".green()
303 ));
304
305 output.push_str(&format!("\n{} Configuration Summary:\n", "๐".cyan()));
307 output.push_str(&format!(" Package: {}\n", config.package.name.yellow()));
308 output.push_str(&format!(
309 " ActrType: {}+{}\n",
310 config.package.actr_type.manufacturer.cyan(),
311 config.package.actr_type.name.cyan()
312 ));
313
314 if let Some(desc) = &config.package.description {
315 output.push_str(&format!(" Description: {}\n", desc));
316 }
317
318 output.push_str(&format!(" Realm: {}\n", config.realm.realm_id));
319 output.push_str(&format!(" Signaling URL: {}\n", config.signaling_url));
320 output.push_str(&format!(
321 " Visible in discovery: {}\n",
322 config.visible_in_discovery
323 ));
324
325 if !config.dependencies.is_empty() {
326 output.push_str(&format!(
327 " Dependencies: {} entries\n",
328 config.dependencies.len()
329 ));
330 }
331
332 if !config.scripts.is_empty() {
333 output.push_str(&format!(" Scripts: {} entries\n", config.scripts.len()));
334 }
335
336 if !raw_config.exports.is_empty() {
337 output.push_str(&format!(
338 " Exports: {} proto files\n",
339 raw_config.exports.len()
340 ));
341 }
342
343 output.push_str(&format!(
344 "\n{} Configuration test completed successfully",
345 "๐ฏ".green()
346 ));
347 }
348 Err(e) => {
349 output.push_str(&format!(
350 "{} Configuration validation failed:\n",
351 "โ".red()
352 ));
353 output.push_str(&format!(" {}\n", e));
354 bail!("Configuration validation failed: {}", e);
355 }
356 }
357
358 Ok(CommandResult::Success(output))
359 }
360
361 fn set_nested_value(&self, config: &mut RawConfig, key: &str, value: &str) -> Result<()> {
363 let parts: Vec<&str> = key.split('.').collect();
364
365 match parts.as_slice() {
366 ["package", "name"] => config.package.name = value.to_string(),
368 ["package", "description"] => config.package.description = Some(value.to_string()),
369 ["package", "actr_type", "manufacturer"] => {
370 config.package.actr_type.manufacturer = value.to_string()
371 }
372 ["package", "actr_type", "name"] => config.package.actr_type.name = value.to_string(),
373
374 ["signaling", "url"] | ["system", "signaling", "url"] => {
376 config.system.signaling.url = Some(value.to_string())
377 }
378
379 ["deployment", "realm_id"] | ["system", "deployment", "realm_id"] => {
381 config.system.deployment.realm_id = Some(
382 value
383 .parse()
384 .map_err(|_| anyhow::anyhow!("deployment.realm_id must be a number"))?,
385 );
386 }
387
388 ["discovery", "visible"] | ["system", "discovery", "visible"] => {
390 config.system.discovery.visible = Some(
391 value
392 .parse()
393 .map_err(|_| anyhow::anyhow!("discovery.visible must be true or false"))?,
394 );
395 }
396
397 ["storage", "mailbox_path"] | ["system", "storage", "mailbox_path"] => {
399 config.system.storage.mailbox_path = Some(value.into());
400 }
401
402 ["webrtc", "stun_urls"] | ["system", "webrtc", "stun_urls"] => {
404 let urls: Vec<String> = value.split(',').map(|s| s.trim().to_string()).collect();
405 config.system.webrtc.stun_urls = urls;
406 }
407 ["webrtc", "turn_urls"] | ["system", "webrtc", "turn_urls"] => {
408 let urls: Vec<String> = value.split(',').map(|s| s.trim().to_string()).collect();
409 config.system.webrtc.turn_urls = urls;
410 }
411 ["webrtc", "force_relay"] | ["system", "webrtc", "force_relay"] => {
412 config.system.webrtc.force_relay = value
413 .parse()
414 .map_err(|_| anyhow::anyhow!("webrtc.force_relay must be true or false"))?;
415 }
416
417 ["observability", "filter_level"] | ["system", "observability", "filter_level"] => {
419 config.system.observability.filter_level = Some(value.to_string());
420 }
421 ["observability", "tracing_enabled"]
422 | ["system", "observability", "tracing_enabled"] => {
423 config.system.observability.tracing_enabled =
424 Some(value.parse().map_err(|_| {
425 anyhow::anyhow!("observability.tracing_enabled must be true or false")
426 })?);
427 }
428 ["observability", "tracing_endpoint"]
429 | ["system", "observability", "tracing_endpoint"] => {
430 config.system.observability.tracing_endpoint = Some(value.to_string());
431 }
432 ["observability", "tracing_service_name"]
433 | ["system", "observability", "tracing_service_name"] => {
434 config.system.observability.tracing_service_name = Some(value.to_string());
435 }
436
437 ["scripts", script_name] => {
439 config
440 .scripts
441 .insert(script_name.to_string(), value.to_string());
442 }
443
444 _ => bail!(
445 "Unknown or unsupported configuration key: {}\n\n๐ก Hint: Run 'actr config list' to see available keys",
446 key
447 ),
448 }
449
450 Ok(())
451 }
452
453 fn get_nested_value(&self, config: &RawConfig, key: &str) -> Result<String> {
455 let parts: Vec<&str> = key.split('.').collect();
456
457 let value = match parts.as_slice() {
458 ["package", "name"] => config.package.name.clone(),
460 ["package", "description"] => config.package.description.clone().unwrap_or_default(),
461 ["package", "actr_type", "manufacturer"] => {
462 config.package.actr_type.manufacturer.clone()
463 }
464 ["package", "actr_type", "name"] => config.package.actr_type.name.clone(),
465
466 ["signaling", "url"] | ["system", "signaling", "url"] => {
468 config.system.signaling.url.clone().unwrap_or_default()
469 }
470
471 ["deployment", "realm_id"] | ["system", "deployment", "realm_id"] => config
473 .system
474 .deployment
475 .realm_id
476 .map(|r| r.to_string())
477 .unwrap_or_default(),
478
479 ["discovery", "visible"] | ["system", "discovery", "visible"] => config
481 .system
482 .discovery
483 .visible
484 .map(|v| v.to_string())
485 .unwrap_or_default(),
486
487 ["storage", "mailbox_path"] | ["system", "storage", "mailbox_path"] => config
489 .system
490 .storage
491 .mailbox_path
492 .as_ref()
493 .map(|p| p.display().to_string())
494 .unwrap_or_default(),
495
496 ["webrtc", "stun_urls"] | ["system", "webrtc", "stun_urls"] => {
498 config.system.webrtc.stun_urls.join(",")
499 }
500 ["webrtc", "turn_urls"] | ["system", "webrtc", "turn_urls"] => {
501 config.system.webrtc.turn_urls.join(",")
502 }
503 ["webrtc", "force_relay"] | ["system", "webrtc", "force_relay"] => {
504 config.system.webrtc.force_relay.to_string()
505 }
506
507 ["observability", "filter_level"] | ["system", "observability", "filter_level"] => {
509 config
510 .system
511 .observability
512 .filter_level
513 .clone()
514 .unwrap_or_default()
515 }
516 ["observability", "tracing_enabled"]
517 | ["system", "observability", "tracing_enabled"] => config
518 .system
519 .observability
520 .tracing_enabled
521 .map(|v| v.to_string())
522 .unwrap_or_default(),
523 ["observability", "tracing_endpoint"]
524 | ["system", "observability", "tracing_endpoint"] => config
525 .system
526 .observability
527 .tracing_endpoint
528 .clone()
529 .unwrap_or_default(),
530 ["observability", "tracing_service_name"]
531 | ["system", "observability", "tracing_service_name"] => config
532 .system
533 .observability
534 .tracing_service_name
535 .clone()
536 .unwrap_or_default(),
537
538 ["scripts", script_name] => config
540 .scripts
541 .get(*script_name)
542 .cloned()
543 .unwrap_or_default(),
544
545 ["dependencies", dep_name] => {
547 if let Some(dep) = config.dependencies.get(*dep_name) {
548 match dep {
549 actr_config::RawDependency::Empty {} => "{}".to_string(),
550 actr_config::RawDependency::WithFingerprint {
551 name,
552 actr_type,
553 fingerprint,
554 realm,
555 } => {
556 let mut parts = vec![];
557 if let Some(n) = name {
558 parts.push(format!("name={}", n));
559 }
560 if let Some(t) = actr_type {
561 parts.push(format!("actr_type={}", t));
562 }
563 parts.push(format!("fingerprint={}", fingerprint));
564 if let Some(r) = realm {
565 parts.push(format!("realm={}", r));
566 }
567 format!("{{ {} }}", parts.join(", "))
568 }
569 }
570 } else {
571 bail!("Dependency not found: {}", dep_name);
572 }
573 }
574
575 _ => bail!(
576 "Unknown configuration key: {}\n\n๐ก Hint: Run 'actr config list' to see available keys",
577 key
578 ),
579 };
580
581 Ok(value)
582 }
583
584 fn unset_nested_value(&self, config: &mut RawConfig, key: &str) -> Result<()> {
586 let parts: Vec<&str> = key.split('.').collect();
587
588 match parts.as_slice() {
589 ["package", "description"] => config.package.description = None,
591
592 ["signaling", "url"] | ["system", "signaling", "url"] => {
594 config.system.signaling.url = None
595 }
596
597 ["deployment", "realm_id"] | ["system", "deployment", "realm_id"] => {
599 config.system.deployment.realm_id = None
600 }
601
602 ["discovery", "visible"] | ["system", "discovery", "visible"] => {
604 config.system.discovery.visible = None
605 }
606
607 ["storage", "mailbox_path"] | ["system", "storage", "mailbox_path"] => {
609 config.system.storage.mailbox_path = None
610 }
611
612 ["webrtc", "stun_urls"] | ["system", "webrtc", "stun_urls"] => {
614 config.system.webrtc.stun_urls = vec![]
615 }
616 ["webrtc", "turn_urls"] | ["system", "webrtc", "turn_urls"] => {
617 config.system.webrtc.turn_urls = vec![]
618 }
619 ["webrtc", "force_relay"] | ["system", "webrtc", "force_relay"] => {
620 config.system.webrtc.force_relay = false
621 }
622
623 ["observability", "filter_level"] | ["system", "observability", "filter_level"] => {
625 config.system.observability.filter_level = None
626 }
627 ["observability", "tracing_enabled"]
628 | ["system", "observability", "tracing_enabled"] => {
629 config.system.observability.tracing_enabled = None
630 }
631 ["observability", "tracing_endpoint"]
632 | ["system", "observability", "tracing_endpoint"] => {
633 config.system.observability.tracing_endpoint = None
634 }
635 ["observability", "tracing_service_name"]
636 | ["system", "observability", "tracing_service_name"] => {
637 config.system.observability.tracing_service_name = None
638 }
639
640 ["scripts", script_name] => {
642 config.scripts.remove(*script_name);
643 }
644
645 ["dependencies", dep_name] => {
647 config.dependencies.remove(*dep_name);
648 }
649
650 ["package", "name"]
652 | ["package", "actr_type", "manufacturer"]
653 | ["package", "actr_type", "name"] => {
654 bail!("Cannot unset required configuration key: {}", key);
655 }
656
657 _ => bail!("Cannot unset configuration key: {}", key),
658 }
659
660 Ok(())
661 }
662}