1use anyhow::{Context, Result};
12use robonix_cli::config::Config;
13use robonix_cli::output;
14use serde_yaml::Value;
15use std::io::{self, BufRead, Write};
16use std::path::{Path, PathBuf};
17
18use super::run_package;
19
20fn prompt_sudo_retry(failed: Vec<(PathBuf, std::io::Error)>) -> Result<()> {
25 if failed.is_empty() {
26 return Ok(());
27 }
28 eprintln!();
29 eprintln!("These paths could not be removed (likely docker-build root-owned files):");
30 for (p, e) in &failed {
31 eprintln!(" ✗ {} : {}", p.display(), e);
32 }
33 eprintln!();
34 eprint!(
35 "Retry with `sudo rm -rf` for the {} path(s) above? [y/N] ",
36 failed.len()
37 );
38 io::stderr().flush().ok();
39 let mut line = String::new();
40 io::stdin().lock().read_line(&mut line)?;
41 let yes = matches!(line.trim().to_ascii_lowercase().as_str(), "y" | "yes");
42 if !yes {
43 anyhow::bail!(
44 "{} path(s) skipped (run `sudo rm -rf <path>` manually if needed)",
45 failed.len()
46 );
47 }
48 let mut cmd = std::process::Command::new("sudo");
49 cmd.arg("rm").arg("-rf");
50 for (p, _) in &failed {
51 cmd.arg(p);
52 }
53 let status = cmd
54 .status()
55 .context("failed to spawn sudo (is it installed?)")?;
56 if !status.success() {
57 anyhow::bail!("sudo rm exited with {:?}", status.code());
58 }
59 Ok(())
60}
61
62pub async fn execute(
63 config: Config,
64 package: Option<PathBuf>,
65 file: Option<PathBuf>,
66 cache: bool,
67) -> Result<()> {
68 match (package, file) {
69 (Some(_), Some(_)) => {
70 anyhow::bail!("pass one of -p / -f, not both")
71 }
72 (Some(pkg), None) => clean_package(&pkg),
73 (None, Some(f)) => clean_deploy(&config, &f, cache),
74 (None, None) => {
75 let cwd = std::env::current_dir().context("get cwd")?;
80 let deploy = cwd.join("robonix_manifest.yaml");
81 if deploy.is_file() {
82 return clean_deploy(&config, &deploy, cache);
83 }
84 match run_package::find_package_from_cwd() {
85 Ok(pkg) => clean_package(&pkg),
86 Err(_) => anyhow::bail!(
87 "no robonix_manifest.yaml in {} and no package_manifest.yaml in any parent. \
88 Pass `-f <manifest>` (deploy) or `-p <pkg>` (package).",
89 cwd.display()
90 ),
91 }
92 }
93 }
94}
95
96fn clean_package(pkg: &Path) -> Result<()> {
97 let pkg = pkg
98 .canonicalize()
99 .with_context(|| format!("not a directory: {}", pkg.display()))?;
100 let build = pkg.join("rbnx-build");
101 if !build.exists() {
102 output::sub_step(&format!(
103 "nothing to clean: {} (no rbnx-build/)",
104 pkg.display()
105 ));
106 return Ok(());
107 }
108 output::action("Cleaning", &build.display().to_string());
109 if let Err(e) = std::fs::remove_dir_all(&build) {
110 return prompt_sudo_retry(vec![(build, e)]);
114 }
115 Ok(())
116}
117
118fn clean_deploy(config: &Config, manifest_path: &Path, also_cache: bool) -> Result<()> {
119 let manifest_path = manifest_path
120 .canonicalize()
121 .with_context(|| format!("manifest not found: {}", manifest_path.display()))?;
122 let manifest_dir = manifest_path
123 .parent()
124 .context("deploy manifest has no parent directory")?
125 .to_path_buf();
126 let raw = std::fs::read_to_string(&manifest_path)
127 .with_context(|| format!("read {}", manifest_path.display()))?;
128 let root: Value =
129 serde_yaml::from_str(&raw).with_context(|| format!("parse {}", manifest_path.display()))?;
130 let cache_root = manifest_dir.join("rbnx-boot").join("cache");
131
132 output::action("Cleaning deploy", &manifest_path.display().to_string());
133
134 let mut pkgs: Vec<PathBuf> = Vec::new();
136 for section in &["primitive", "service", "skill"] {
137 let Some(seq) = root.get(*section).and_then(|v| v.as_sequence()) else {
138 continue;
139 };
140 for entry in seq {
141 let name = entry
142 .get("name")
143 .and_then(|v| v.as_str())
144 .unwrap_or("(unnamed)")
145 .to_string();
146 let local = entry.get("path").and_then(|v| v.as_str());
147 let url = entry.get("url").and_then(|v| v.as_str());
148 match (local, url) {
149 (Some(p), _) => pkgs.push(manifest_dir.join(p)),
150 (None, Some(_)) => pkgs.push(cache_root.join(&name)),
151 _ => {}
152 }
153 }
154 }
155 const SYSTEM_BUILTINS: &[&str] = &["atlas", "executor", "pilot", "liaison"];
158 if let Some(map) = root.get("system").and_then(|v| v.as_mapping())
159 && let Some(source_root) = config.robonix_source_path.as_ref()
160 {
161 for (key, _) in map {
162 let Some(k) = key.as_str() else { continue };
163 if SYSTEM_BUILTINS.contains(&k) {
164 continue;
165 }
166 let pkg = source_root.join("system").join(k);
167 if pkg.exists() {
168 pkgs.push(pkg);
169 }
170 }
171 }
172
173 output::step("packages", &format!("{} to inspect", pkgs.len()));
176 let mut failed: Vec<(PathBuf, std::io::Error)> = Vec::new();
177 for p in &pkgs {
178 if !p.exists() {
179 continue;
180 }
181 let build = p.join("rbnx-build");
182 if build.exists() {
183 output::sub_step(&format!("rm -rf {}", build.display()));
184 if let Err(e) = std::fs::remove_dir_all(&build) {
185 failed.push((build, e));
186 }
187 }
188 }
189
190 let rbnx_boot = manifest_dir.join("rbnx-boot");
194 if rbnx_boot.exists() {
195 if let Ok(entries) = std::fs::read_dir(&rbnx_boot) {
196 for entry in entries.flatten() {
197 let path = entry.path();
198 let name = entry.file_name();
199 let is_cache = name == "cache";
200 if is_cache && !also_cache {
201 output::sub_step(&format!("keep {} (use --cache to wipe)", path.display()));
202 continue;
203 }
204 let label = if is_cache { " (cache)" } else { "" };
205 let kind = entry.file_type().ok();
206 if kind.map(|k| k.is_dir()).unwrap_or(false) {
207 output::sub_step(&format!("rm -rf {}{}", path.display(), label));
208 if let Err(e) = std::fs::remove_dir_all(&path) {
209 failed.push((path, e));
210 }
211 } else {
212 output::sub_step(&format!("rm {}{}", path.display(), label));
213 if let Err(e) = std::fs::remove_file(&path) {
214 failed.push((path, e));
215 }
216 }
217 }
218 }
219 let _ = std::fs::remove_dir(&rbnx_boot);
222 }
223
224 prompt_sudo_retry(failed)
225}