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