greentic_dev/cmd/
config.rs1use std::fs;
2use std::path::Path;
3
4use crate::cli::{ConfigCommand, ConfigSetArgs};
5use anyhow::{Context, Result, anyhow, bail};
6use toml_edit::{DocumentMut, Item, Table, value};
7
8use crate::config;
9
10pub fn run(command: ConfigCommand) -> Result<()> {
11 match command {
12 ConfigCommand::Set(args) => set_value(&args),
13 }
14}
15
16fn set_value(args: &ConfigSetArgs) -> Result<()> {
17 let path = match &args.file {
18 Some(path) => path.clone(),
19 None => config::config_path().ok_or_else(|| {
20 anyhow!("failed to resolve ~/.greentic/config.toml (no home directory found)")
21 })?,
22 };
23
24 ensure_parent(&path)?;
25
26 let mut doc = if path.exists() {
27 let raw = fs::read_to_string(&path)
28 .with_context(|| format!("failed to read {}", path.display()))?;
29 if raw.trim().is_empty() {
30 DocumentMut::new()
31 } else {
32 raw.parse::<DocumentMut>()
33 .with_context(|| format!("failed to parse {}", path.display()))?
34 }
35 } else {
36 DocumentMut::new()
37 };
38
39 apply_key(&mut doc, &args.key, &args.value)?;
40
41 fs::write(&path, doc.to_string())
42 .with_context(|| format!("failed to write {}", path.display()))?;
43 println!("Updated {}", path.display());
44 Ok(())
45}
46
47fn ensure_parent(path: &Path) -> Result<()> {
48 if let Some(parent) = path.parent() {
49 fs::create_dir_all(parent)
50 .with_context(|| format!("failed to create {}", parent.display()))?;
51 }
52 Ok(())
53}
54
55fn apply_key(doc: &mut DocumentMut, key: &str, value_str: &str) -> Result<()> {
56 let segments = key
57 .split('.')
58 .filter(|segment| !segment.is_empty())
59 .collect::<Vec<_>>();
60 if segments.is_empty() {
61 bail!("config key cannot be empty");
62 }
63
64 let mut current = doc.as_table_mut();
65 for segment in &segments[..segments.len() - 1] {
66 current = current
67 .entry(segment)
68 .or_insert(Item::Table(Table::new()))
69 .as_table_mut()
70 .ok_or_else(|| anyhow!("path `{segment}` is not a table in the config"))?;
71 }
72
73 current.insert(segments.last().unwrap(), value(value_str));
74 Ok(())
75}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80 use tempfile::TempDir;
81
82 #[test]
83 fn creates_new_document() {
84 let temp = TempDir::new().unwrap();
85 let path = temp.path().join("config.toml");
86 let args = ConfigSetArgs {
87 key: "defaults.component.org".into(),
88 value: "ai.greentic".into(),
89 file: Some(path.clone()),
90 };
91 set_value(&args).unwrap();
92 let written = fs::read_to_string(path).unwrap();
93 assert!(written.contains("defaults"));
94 assert!(written.contains("ai.greentic"));
95 }
96
97 #[test]
98 fn updates_nested_tables() {
99 let temp = TempDir::new().unwrap();
100 let path = temp.path().join("config.toml");
101 fs::write(
102 &path,
103 r#"
104[defaults]
105[defaults.component]
106org = "ai.greentic"
107"#,
108 )
109 .unwrap();
110
111 let args = ConfigSetArgs {
112 key: "defaults.component.template".into(),
113 value: "rust-wasi".into(),
114 file: Some(path.clone()),
115 };
116 set_value(&args).unwrap();
117 let written = fs::read_to_string(path).unwrap();
118 assert!(written.contains("template = \"rust-wasi\""));
119 }
120}