1use anyhow::{Context, Result};
7use dirs;
8use serde::{Deserialize, Serialize};
9use std::path::PathBuf;
10
11pub const ROBONIX_HOME_ENV: &str = "ROBONIX_HOME";
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct Config {
15 pub package_storage_path: PathBuf,
16 #[serde(skip_serializing_if = "Option::is_none")]
20 pub robonix_source_path: Option<PathBuf>,
21}
22
23impl Config {
24 pub fn robonix_home_dir() -> Result<PathBuf> {
25 if let Some(raw) = std::env::var_os(ROBONIX_HOME_ENV)
26 && !raw.is_empty()
27 {
28 return Ok(PathBuf::from(raw));
29 }
30
31 let home_dir = dirs::home_dir().context("Failed to get home directory")?;
32 Ok(home_dir.join(".robonix"))
33 }
34
35 pub fn config_file_path() -> Result<PathBuf> {
36 Ok(Self::robonix_home_dir()?.join("config.yaml"))
37 }
38
39 pub fn load() -> Result<Self> {
40 let config_path = Self::config_file_path()?;
41
42 if !config_path.exists() {
43 let default = Self::default();
45 default.save()?;
46 return Ok(default);
47 }
48
49 let content = std::fs::read_to_string(&config_path)
50 .with_context(|| format!("Failed to read config file: {}", config_path.display()))?;
51
52 let config: Config = serde_yaml::from_str(&content)
53 .with_context(|| format!("Failed to parse config file: {}", config_path.display()))?;
54
55 Ok(config)
56 }
57
58 pub fn require_source_path(&self) -> Result<&std::path::Path> {
63 match self.robonix_source_path.as_deref() {
64 Some(p) if p.exists() => Ok(p),
65 Some(p) => {
66 eprintln!(
67 "[rbnx] configured robonix_source_path no longer exists: {}",
68 p.display()
69 );
70 eprintln!(
71 "Re-run `rbnx setup` from the robonix source repo root (containing `rust/`)."
72 );
73 std::process::exit(2);
74 }
75 None => {
76 eprintln!(
77 "[rbnx] config is missing robonix_source_path (legacy config from before the `rbnx setup` migration)."
78 );
79 eprintln!(
80 "This is required so packages anywhere on disk can resolve capabilities/IDL paths."
81 );
82 eprintln!();
83 eprintln!("Fix: cd /path/to/robonix # the repo root (containing `rust/`)");
84 eprintln!(" rbnx setup");
85 eprintln!();
86 eprintln!(
87 "Config file: {}",
88 Self::config_file_path()
89 .map(|p| p.display().to_string())
90 .unwrap_or_else(|_| "~/.robonix/config.yaml".to_string())
91 );
92 std::process::exit(2);
93 }
94 }
95 }
96
97 pub fn save(&self) -> Result<()> {
98 let config_path = Self::config_file_path()?;
99
100 if let Some(parent) = config_path.parent() {
102 std::fs::create_dir_all(parent).with_context(|| {
103 format!("Failed to create config directory: {}", parent.display())
104 })?;
105 }
106
107 let content = serde_yaml::to_string(self).context("Failed to serialize config")?;
108
109 std::fs::write(&config_path, content)
110 .with_context(|| format!("Failed to write config file: {}", config_path.display()))?;
111
112 Ok(())
113 }
114
115 #[allow(clippy::should_implement_trait)]
116 pub fn default() -> Self {
117 let default_path = Self::robonix_home_dir()
118 .unwrap_or_else(|_| PathBuf::from("/tmp/robonix"))
119 .join("packages");
120
121 Self {
122 package_storage_path: default_path,
123 robonix_source_path: None,
124 }
125 }
126
127 pub fn resolve_source_path(&self, key: SourcePathKey) -> Result<PathBuf> {
131 let root = self.robonix_source_path.as_ref().ok_or_else(|| {
132 anyhow::anyhow!(
133 "robonix_source_path is not set. Run `rbnx setup` from the robonix source root (the directory containing `rust/`)."
134 )
135 })?;
136 let abs = match key {
137 SourcePathKey::Root => root.clone(),
138 SourcePathKey::RustRoot => root.clone(),
142 SourcePathKey::Capabilities => root.join("capabilities"),
143 SourcePathKey::InterfacesLib => root.join("capabilities").join("lib"),
148 SourcePathKey::RuntimeProto => root.join("system").join("atlas").join("proto"),
150 SourcePathKey::RobonixApi => root.join("pylib").join("robonix-api"),
151 };
152 if !abs.exists() {
153 anyhow::bail!(
154 "resolved path does not exist: {} (robonix_source_path={}). The source tree may be incomplete — re-run `rbnx setup` from the correct root.",
155 abs.display(),
156 root.display()
157 );
158 }
159 Ok(abs)
160 }
161}
162
163#[derive(Debug, Clone, Copy)]
165pub enum SourcePathKey {
166 Root,
168 RustRoot,
170 Capabilities,
172 InterfacesLib,
174 RuntimeProto,
176 RobonixApi,
180}
181
182impl std::str::FromStr for SourcePathKey {
183 type Err = String;
184 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
185 match s {
186 "root" | "source" => Ok(Self::Root),
187 "rust" | "rust-root" => Ok(Self::RustRoot),
188 "capabilities" => Ok(Self::Capabilities),
189 "interfaces-lib" | "idl" => Ok(Self::InterfacesLib),
190 "runtime-proto" => Ok(Self::RuntimeProto),
191 "robonix-api" => Ok(Self::RobonixApi),
192 other => Err(format!(
193 "unknown path key: {other}. Valid: root, rust, capabilities, interfaces-lib, runtime-proto, robonix-api"
194 )),
195 }
196 }
197}
198
199impl Config {
200 pub fn ensure_storage_dir(&self) -> Result<()> {
201 if let Ok(metadata) = std::fs::metadata(&self.package_storage_path) {
203 if metadata.is_dir() {
204 return Ok(());
206 } else {
207 anyhow::bail!(
208 "Package storage path exists but is not a directory: {}",
209 self.package_storage_path.display()
210 );
211 }
212 }
213
214 std::fs::create_dir_all(&self.package_storage_path).with_context(|| {
216 format!(
217 "Failed to create package storage directory: {}",
218 self.package_storage_path.display()
219 )
220 })?;
221 Ok(())
222 }
223}