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 use clap::CommandFactory;
164
165 #[test]
166 fn build_accepts_explicit_deploy_manifest() {
167 let cli = Cli::try_parse_from(["rbnx", "build", "-f", "robot/robonix_manifest.yaml"])
168 .expect("build -f should parse");
169 match cli.command {
170 cmd::Commands::Build {
171 file, path, global, ..
172 } => {
173 assert_eq!(file, Some("robot/robonix_manifest.yaml".into()));
174 assert!(path.is_none());
175 assert!(global.is_none());
176 }
177 _ => panic!("expected build command"),
178 }
179 }
180
181 #[test]
182 fn build_manifest_conflicts_with_single_package_selectors() {
183 assert!(Cli::try_parse_from(["rbnx", "build", "-f", "deploy.yaml", "-p", "."]).is_err());
184 assert!(Cli::try_parse_from(["rbnx", "build", "-f", "deploy.yaml", "-g", "pkg"]).is_err());
185 }
186
187 #[test]
188 fn build_accepts_no_update_check() {
189 let cli = Cli::try_parse_from([
190 "rbnx",
191 "build",
192 "-f",
193 "robot/robonix_manifest.yaml",
194 "--no-update-check",
195 ])
196 .expect("build --no-update-check should parse");
197 assert!(matches!(
198 cli.command,
199 cmd::Commands::Build {
200 no_update_check: true,
201 ..
202 }
203 ));
204 }
205
206 #[test]
207 fn boot_accepts_verbose_mode() {
208 let cli = Cli::try_parse_from(["rbnx", "boot", "-v"]).expect("boot -v should parse");
209 assert!(matches!(
210 cli.command,
211 cmd::Commands::Boot { verbose: true, .. }
212 ));
213 }
214
215 #[test]
217 fn boot_help_lists_vitals() {
218 let mut command = Cli::command();
219 let help = command
220 .find_subcommand_mut("boot")
221 .expect("boot subcommand")
222 .render_long_help()
223 .to_string();
224 assert!(help.contains("vitals"));
225 }
226
227 #[test]
228 fn boot_uses_manifest_local_scribe_directory() {
229 let cli = Cli::try_parse_from(["rbnx", "boot", "-f", "robot/robonix_manifest.yaml"])
230 .expect("boot manifest should parse");
231 assert_eq!(
232 boot_log_dir(&cli.command),
233 std::env::current_dir()
234 .ok()
235 .map(|cwd| cwd.join("robot/rbnx-boot/logs"))
236 );
237 }
238}