1use anyhow::{Context, Result};
2use serde::{Deserialize, Serialize};
3use serde_json::{json, Value};
4use std::fs;
5use std::path::PathBuf;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct InstallerConfig {
9 pub scripts_dir: String,
10 pub log_dir: String,
11 pub install_path: String,
12 pub batch_mode: bool,
13 pub install_method: String,
14 pub auto_confirm: bool,
15 pub star_repos: bool,
16 pub force_reinstall: bool,
17 pub theme: String,
18 pub performance_profile: String,
19 #[serde(skip)]
20 pub config_path: PathBuf,
21}
22
23impl InstallerConfig {
24 pub fn load_or_default(scripts_dir: &str) -> Result<Self> {
25 let config_path = config_file_path()?;
26 let log_dir = config_path
27 .parent()
28 .and_then(|p| p.parent())
29 .map(|p| p.join("logs"))
30 .unwrap_or_else(|| PathBuf::from("/tmp/mlstack/logs"));
31
32 let mut config = InstallerConfig::default_with_paths(
33 scripts_dir,
34 log_dir.to_string_lossy().to_string(),
35 config_path.clone(),
36 );
37
38 if config_path.exists() {
39 let raw = fs::read_to_string(&config_path).context("Failed to read config file")?;
40 let value: Value = serde_json::from_str(&raw).unwrap_or_else(|_| json!({}));
41 config.apply_from_value(&value);
42 } else {
43 let _ = config.save(None);
45 }
46
47 Ok(config)
48 }
49
50 pub fn save(&self, existing: Option<Value>) -> Result<()> {
51 if let Some(parent) = self.config_path.parent() {
52 fs::create_dir_all(parent).context("Failed to create config directory")?;
53 }
54 let mut value = existing.unwrap_or_else(|| json!({}));
55
56 value["scripts_dir"] = json!(self.scripts_dir);
57 value["log_dir"] = json!(self.log_dir);
58 value["batch_mode"] = json!(self.batch_mode);
59 value["install_method"] = json!(self.install_method);
60
61 let user_prefs = value
62 .get("user_preferences")
63 .cloned()
64 .unwrap_or_else(|| json!({}));
65 let mut user_prefs = user_prefs;
66 user_prefs["installation_path"] = json!(self.install_path);
67 user_prefs["install_method"] = json!(self.install_method);
68 user_prefs["auto_confirm"] = json!(self.auto_confirm);
69 user_prefs["star_repos"] = json!(self.star_repos);
70 user_prefs["force_reinstall"] = json!(self.force_reinstall);
71 user_prefs["theme"] = json!(self.theme);
72 user_prefs["performance_profile"] = json!(self.performance_profile);
73 value["user_preferences"] = user_prefs;
74
75 fs::write(&self.config_path, serde_json::to_string_pretty(&value)?)
76 .context("Failed to save config")?;
77
78 Ok(())
79 }
80
81 fn apply_from_value(&mut self, value: &Value) {
82 if let Some(dir) = value.get("scripts_dir").and_then(|v| v.as_str()) {
83 self.scripts_dir = dir.to_string();
84 }
85 if let Some(dir) = value.get("log_dir").and_then(|v| v.as_str()) {
86 self.log_dir = dir.to_string();
87 }
88 if let Some(batch) = value.get("batch_mode").and_then(|v| v.as_bool()) {
89 self.batch_mode = batch;
90 }
91 if let Some(method) = value.get("install_method").and_then(|v| v.as_str()) {
92 self.install_method = normalize_install_method(method);
93 }
94
95 if let Some(prefs) = value.get("user_preferences") {
96 if let Some(path) = prefs.get("installation_path").and_then(|v| v.as_str()) {
97 self.install_path = path.to_string();
98 }
99 if let Some(method) = prefs.get("install_method").and_then(|v| v.as_str()) {
100 self.install_method = normalize_install_method(method);
101 }
102 if let Some(auto) = prefs.get("auto_confirm").and_then(|v| v.as_bool()) {
103 self.auto_confirm = auto;
104 }
105 if let Some(star) = prefs.get("star_repos").and_then(|v| v.as_bool()) {
106 self.star_repos = star;
107 }
108 if let Some(force) = prefs.get("force_reinstall").and_then(|v| v.as_bool()) {
109 self.force_reinstall = force;
110 }
111 if let Some(theme) = prefs.get("theme").and_then(|v| v.as_str()) {
112 self.theme = theme.to_string();
113 }
114 if let Some(profile) = prefs.get("performance_profile").and_then(|v| v.as_str()) {
115 self.performance_profile = profile.to_string();
116 }
117 }
118 }
119
120 pub fn default_with_paths(scripts_dir: &str, log_dir: String, config_path: PathBuf) -> Self {
121 Self {
122 scripts_dir: scripts_dir.to_string(),
123 log_dir,
124 install_path: "/opt/rocm".into(),
125 batch_mode: std::env::var("MLSTACK_BATCH_MODE").unwrap_or_default() == "1",
126 install_method: normalize_install_method(
127 &std::env::var("MLSTACK_INSTALL_METHOD").unwrap_or_else(|_| "auto".into()),
128 ),
129 auto_confirm: false,
130 star_repos: true,
131 force_reinstall: false,
132 theme: "dark".into(),
133 performance_profile: "balanced".into(),
134 config_path,
135 }
136 }
137}
138
139fn normalize_install_method(input: &str) -> String {
140 match input.trim().to_ascii_lowercase().as_str() {
141 "global" => "global".into(),
142 "venv" => "venv".into(),
143 _ => "auto".into(),
144 }
145}
146
147pub fn config_file_path() -> Result<PathBuf> {
148 let home = std::env::var("HOME").unwrap_or_else(|_| ".".into());
149 let config_dir = PathBuf::from(home).join(".mlstack").join("config");
150 fs::create_dir_all(&config_dir).context("Failed to create config directory")?;
151 Ok(config_dir.join("config.json"))
152}