1use anyhow::Result;
7use clap::Parser;
8use robonix_cli::Config;
9use robonix_scribe::warn;
10use std::path::{Path, PathBuf};
11
12mod cmd;
13mod pb;
14
15#[derive(Parser)]
16#[command(name = "rbnx")]
17#[command(version)]
18#[command(about = "Robonix helper CLI for package validation, build, and local orchestration", long_about = None)]
19struct Cli {
20 #[command(subcommand)]
21 command: cmd::Commands,
22}
23
24fn ensure_loopback_bypasses_proxy() {
35 const LOOPBACK: [&str; 3] = ["127.0.0.1", "localhost", "::1"];
36 for var in ["no_proxy", "NO_PROXY"] {
37 let mut entries: Vec<String> = std::env::var(var)
38 .unwrap_or_default()
39 .split(',')
40 .map(|s| s.trim().to_string())
41 .filter(|s| !s.is_empty())
42 .collect();
43 let have: std::collections::HashSet<String> =
44 entries.iter().map(|s| s.to_ascii_lowercase()).collect();
45 for host in LOOPBACK {
46 if !have.contains(host) {
47 entries.push(host.to_string());
48 }
49 }
50 unsafe { std::env::set_var(var, entries.join(",")) };
53 }
54}
55
56fn boot_log_dir(command: &cmd::Commands) -> Option<PathBuf> {
57 let cmd::Commands::Boot { file, log_dir, .. } = command else {
58 return None;
59 };
60 if let Some(log_dir) = log_dir {
61 return Some(log_dir.clone());
62 }
63 let manifest = if file.is_absolute() {
64 file.clone()
65 } else {
66 std::env::current_dir().ok()?.join(file)
67 };
68 Some(
69 manifest
70 .parent()
71 .unwrap_or_else(|| Path::new("."))
72 .join("rbnx-boot")
73 .join("logs"),
74 )
75}
76
77fn prepare_boot_log_dir(command: &cmd::Commands) -> Result<()> {
78 let Some(log_dir) = boot_log_dir(command) else {
79 return Ok(());
80 };
81 std::fs::create_dir_all(&log_dir)?;
82 for entry in std::fs::read_dir(&log_dir)?.flatten() {
83 let path = entry.path();
84 if path.extension().and_then(|s| s.to_str()) == Some("log") {
85 let _ = std::fs::remove_file(path);
86 }
87 }
88 unsafe { std::env::set_var("SCRIBE_LOG_DIR", &log_dir) };
90 Ok(())
91}
92
93#[tokio::main]
94async fn main() -> Result<()> {
95 ensure_loopback_bypasses_proxy();
96
97 let cli = Cli::parse();
98 if let cmd::Commands::WatchBoot {
102 state,
103 boot_pid,
104 boot_start_time_ticks,
105 boot_id,
106 } = &cli.command
107 {
108 return cmd::boot_watchdog::execute(
109 state.clone(),
110 *boot_pid,
111 *boot_start_time_ticks,
112 boot_id.clone(),
113 )
114 .await;
115 }
116 let verbose_boot = matches!(&cli.command, cmd::Commands::Boot { verbose: true, .. });
117 prepare_boot_log_dir(&cli.command)?;
118
119 unsafe {
124 std::env::set_var(
125 "SCRIBE_CONSOLE_LEVEL",
126 if verbose_boot { "info" } else { "error" },
127 );
128 }
129
130 robonix_scribe::init("rbnx");
131
132 let config = Config::load()?;
133 config.ensure_storage_dir()?;
134
135 let needs_source = matches!(
140 &cli.command,
141 cmd::Commands::Build { .. }
142 | cmd::Commands::Start { .. }
143 | cmd::Commands::Validate { .. }
144 | cmd::Commands::Install { .. }
145 | cmd::Commands::Codegen { .. }
146 );
147 if needs_source {
148 let _ = config.require_source_path();
149 }
150
151 if let Err(e) = robonix_cli::PackageDatabase::sync(&config.package_storage_path) {
152 warn!("Package database sync failed: {}", e);
153 }
154
155 cmd::execute(cli.command, config).await?;
156
157 Ok(())
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163
164 #[test]
165 fn build_accepts_explicit_deploy_manifest() {
166 let cli = Cli::try_parse_from(["rbnx", "build", "-f", "robot/robonix_manifest.yaml"])
167 .expect("build -f should parse");
168 match cli.command {
169 cmd::Commands::Build {
170 file, path, global, ..
171 } => {
172 assert_eq!(file, Some("robot/robonix_manifest.yaml".into()));
173 assert!(path.is_none());
174 assert!(global.is_none());
175 }
176 _ => panic!("expected build command"),
177 }
178 }
179
180 #[test]
181 fn build_manifest_conflicts_with_single_package_selectors() {
182 assert!(Cli::try_parse_from(["rbnx", "build", "-f", "deploy.yaml", "-p", "."]).is_err());
183 assert!(Cli::try_parse_from(["rbnx", "build", "-f", "deploy.yaml", "-g", "pkg"]).is_err());
184 }
185
186 #[test]
187 fn build_accepts_no_update_check() {
188 let cli = Cli::try_parse_from([
189 "rbnx",
190 "build",
191 "-f",
192 "robot/robonix_manifest.yaml",
193 "--no-update-check",
194 ])
195 .expect("build --no-update-check should parse");
196 assert!(matches!(
197 cli.command,
198 cmd::Commands::Build {
199 no_update_check: true,
200 ..
201 }
202 ));
203 }
204
205 #[test]
206 fn boot_accepts_verbose_mode() {
207 let cli = Cli::try_parse_from(["rbnx", "boot", "-v"]).expect("boot -v should parse");
208 assert!(matches!(
209 cli.command,
210 cmd::Commands::Boot { verbose: true, .. }
211 ));
212 }
213
214 #[test]
215 fn boot_uses_manifest_local_scribe_directory() {
216 let cli = Cli::try_parse_from(["rbnx", "boot", "-f", "robot/robonix_manifest.yaml"])
217 .expect("boot manifest should parse");
218 assert_eq!(
219 boot_log_dir(&cli.command),
220 std::env::current_dir()
221 .ok()
222 .map(|cwd| cwd.join("robot/rbnx-boot/logs"))
223 );
224 }
225}