homeassistant_cli/commands/
config.rs1use crate::config;
2use crate::output::{OutputConfig, mask_credential};
3
4pub fn show(out: &OutputConfig, profile_arg: Option<&str>) {
5 let _ = profile_arg; let summary = config::config_summary();
7
8 if out.is_json() {
9 let profiles_json: Vec<serde_json::Value> = summary
10 .profiles
11 .iter()
12 .map(|p| {
13 serde_json::json!({
14 "name": p.name,
15 "url": p.url,
16 "token": p.token.as_deref().map(mask_credential)
17 })
18 })
19 .collect();
20
21 out.print_data(
22 &serde_json::to_string_pretty(&serde_json::json!({
23 "ok": true,
24 "data": {
25 "config_file": summary.config_file,
26 "file_exists": summary.file_exists,
27 "profiles": profiles_json,
28 "env": {
29 "HA_URL": summary.env_url,
30 "HA_TOKEN": summary.env_token.as_deref().map(mask_credential),
31 "HA_PROFILE": summary.env_profile
32 }
33 }
34 }))
35 .expect("serialize"),
36 );
37 } else {
38 println!("Config file: {}", summary.config_file.display());
39 if !summary.file_exists {
40 println!(" (not found — run `ha init` to create it)");
41 return;
42 }
43 for p in &summary.profiles {
44 println!("\n[{}]", p.name);
45 println!(" url = {}", p.url.as_deref().unwrap_or("(not set)"));
46 println!(
47 " token = {}",
48 p.token
49 .as_deref()
50 .map(mask_credential)
51 .unwrap_or_else(|| "(not set)".into())
52 );
53 }
54 if summary.env_url.is_some() || summary.env_token.is_some() || summary.env_profile.is_some()
55 {
56 println!("\nEnvironment overrides:");
57 if let Some(v) = &summary.env_url {
58 println!(" HA_URL={v}");
59 }
60 if let Some(v) = &summary.env_token {
61 println!(" HA_TOKEN={}", mask_credential(v));
62 }
63 if let Some(v) = &summary.env_profile {
64 println!(" HA_PROFILE={v}");
65 }
66 }
67 }
68}
69
70pub fn set(out: &OutputConfig, profile_arg: Option<&str>, key: &str, value: &str) {
71 if key != "url" && key != "token" {
72 eprintln!("Unknown config key '{key}'. Valid keys: url, token");
73 std::process::exit(crate::output::exit_codes::GENERAL_ERROR);
74 }
75
76 let path = config::config_path();
77 let profile = profile_arg.unwrap_or("default");
78
79 let (current_url, current_token) =
80 config::read_profile_credentials(&path, profile).unwrap_or_default();
81
82 let (url, token) = if key == "url" {
83 (value.to_owned(), current_token)
84 } else {
85 (current_url, value.to_owned())
86 };
87
88 if let Err(e) = config::write_profile(&path, profile, &url, &token) {
89 eprintln!("{e}");
90 std::process::exit(crate::output::exit_codes::GENERAL_ERROR);
91 }
92
93 if out.is_json() {
94 out.print_data(
95 &serde_json::to_string_pretty(&serde_json::json!({
96 "ok": true,
97 "data": {"key": key, "profile": profile}
98 }))
99 .expect("serialize"),
100 );
101 } else {
102 println!("✔ Set {} for profile '{}'", key, profile);
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109 use crate::output::{OutputConfig, OutputFormat};
110 use crate::test_support::{EnvVarGuard, ProcessEnvLock, write_config};
111 use tempfile::TempDir;
112
113 fn json_out() -> OutputConfig {
114 OutputConfig::new(Some(OutputFormat::Json), false)
115 }
116
117 #[test]
118 fn show_does_not_panic_with_no_config() {
119 let _lock = ProcessEnvLock::acquire().unwrap();
120 let dir = TempDir::new().unwrap();
121 let _env = EnvVarGuard::set("XDG_CONFIG_HOME", &dir.path().to_string_lossy());
122 show(&json_out(), None);
124 }
125
126 #[test]
127 fn set_writes_url_to_config() {
128 let _lock = ProcessEnvLock::acquire().unwrap();
129 let dir = TempDir::new().unwrap();
130 write_config(
131 dir.path(),
132 "[default]\nurl = \"http://old:8123\"\ntoken = \"old-token\"\n",
133 )
134 .unwrap();
135 let _env = EnvVarGuard::set("XDG_CONFIG_HOME", &dir.path().to_string_lossy());
136
137 set(&json_out(), None, "url", "http://new:8123");
138
139 let path = dir.path().join("ha").join("config.toml");
140 let content = std::fs::read_to_string(&path).unwrap();
141 assert!(content.contains("http://new:8123"));
142 assert!(content.contains("old-token"), "token must not be changed");
143 }
144}