1use std::path::Path;
2use std::process;
3
4use crate::cli::{Cli, ToolCommand};
5use crate::config::{UbtConfig, load_config};
6use crate::detect::detect_tool;
7use crate::error::UbtError;
8use crate::plugin::PluginRegistry;
9
10use super::info::cmd_info;
11
12fn cmd_doctor(
13 cli: &Cli,
14 config: Option<&UbtConfig>,
15 project_root: &Path,
16 registry: &PluginRegistry,
17) -> Result<(), UbtError> {
18 let quiet = cli.quiet;
19 let mut any_fail = false;
20
21 macro_rules! check_ok {
22 ($msg:expr) => {
23 if !quiet {
24 println!("[ok] {}", $msg);
25 }
26 };
27 }
28 macro_rules! check_warn {
29 ($msg:expr) => {
30 if !quiet {
31 println!("[warn] {}", $msg);
32 }
33 };
34 }
35 macro_rules! check_fail {
36 ($msg:expr) => {{
37 any_fail = true;
38 eprintln!("[fail] {}", $msg);
39 }};
40 }
41
42 let config_tool = config.and_then(|c| c.project.as_ref()?.tool.as_deref());
44 match detect_tool(cli.tool.as_deref(), config_tool, project_root, registry) {
45 Err(e) => {
46 check_fail!(format!("Tool detection failed: {e}"));
47 }
48 Ok(detection) => {
49 if let Some((plugin, _)) = registry.get(&detection.plugin_name)
50 && let Some(variant) = plugin.variants.get(&detection.variant_name)
51 {
52 match which::which(&variant.binary) {
53 Ok(path) => {
54 let version = std::process::Command::new(&variant.binary)
56 .arg("--version")
57 .output()
58 .ok()
59 .and_then(|o| {
60 let out = if o.stdout.is_empty() {
61 o.stderr
62 } else {
63 o.stdout
64 };
65 String::from_utf8(out).ok()
66 })
67 .map(|s| s.lines().next().unwrap_or("").trim().to_string())
68 .filter(|s| !s.is_empty())
69 .unwrap_or_else(|| "version unknown".to_string());
70 check_ok!(format!(
71 "{} binary: {} ({})",
72 variant.binary,
73 path.display(),
74 version
75 ));
76 }
77 Err(_) => {
78 let hint = plugin
79 .install_help
80 .as_deref()
81 .map(|h| format!(" — install: {h}"))
82 .unwrap_or_default();
83 check_fail!(format!("{} is not installed{}", variant.binary, hint));
84 }
85 }
86 }
87 }
88 }
89
90 let cwd = std::env::current_dir()?;
92 match load_config(&cwd) {
93 Err(e) => check_fail!(format!("ubt.toml parse error: {e}")),
94 Ok(None) => check_warn!("No ubt.toml found in this directory tree"),
95 Ok(Some((cfg, config_root))) => {
96 check_ok!(format!(
97 "ubt.toml valid at {}",
98 config_root.join("ubt.toml").display()
99 ));
100
101 for (alias, target) in &cfg.aliases {
103 let first_word = target.split_whitespace().next().unwrap_or(target.as_str());
104 let known_commands = [
105 "dep.install",
106 "dep.install_pkg",
107 "dep.remove",
108 "dep.update",
109 "dep.outdated",
110 "dep.list",
111 "dep.audit",
112 "dep.lock",
113 "dep.why",
114 "build",
115 "build.dev",
116 "start",
117 "run",
118 "run-file",
119 "exec",
120 "test",
121 "lint",
122 "fmt",
123 "check",
124 "clean",
125 "release",
126 "publish",
127 "db.migrate",
128 "db.rollback",
129 "db.seed",
130 "db.create",
131 "db.drop",
132 "db.reset",
133 "db.status",
134 ];
135 if known_commands.contains(&first_word)
136 || cfg.aliases.contains_key(first_word)
137 || cfg.commands.contains_key(first_word)
138 {
139 check_ok!(format!("alias '{alias}' → valid target '{first_word}'"));
140 } else {
141 check_warn!(format!(
142 "alias '{alias}' target '{first_word}' is not a known ubt command"
143 ));
144 }
145 }
146 }
147 }
148
149 if any_fail {
150 process::exit(1);
151 }
152
153 Ok(())
154}
155
156pub fn cmd_tool(
157 sub: &ToolCommand,
158 cli: &Cli,
159 config: Option<&UbtConfig>,
160 project_root: &std::path::Path,
161 registry: &PluginRegistry,
162) -> Result<(), UbtError> {
163 match sub {
164 ToolCommand::Info => cmd_info(cli, config, project_root, registry),
165 ToolCommand::Doctor => cmd_doctor(cli, config, project_root, registry),
166 ToolCommand::List => {
167 if !cli.quiet {
168 println!("{:<12} {:<30} Variants", "Plugin", "Description");
169 println!("{}", "-".repeat(70));
170 let mut names: Vec<_> = registry.names();
171 names.sort();
172 for name in names {
173 if let Some((plugin, _)) = registry.get(name) {
174 let variants: Vec<_> = plugin.variants.keys().cloned().collect();
175 println!(
176 "{:<12} {:<30} {}",
177 plugin.name,
178 plugin.description,
179 variants.join(", ")
180 );
181 }
182 }
183 }
184 Ok(())
185 }
186 ToolCommand::Docs(args) => {
187 let config_tool = config.and_then(|c| c.project.as_ref()?.tool.as_deref());
188 let detection = detect_tool(cli.tool.as_deref(), config_tool, project_root, registry)?;
189 if let Some((plugin, _)) = registry.get(&detection.plugin_name) {
190 if let Some(hp) = &plugin.homepage {
191 if args.open {
192 if let Err(e) = open::that(hp) {
193 eprintln!("ubt: could not open browser: {e}");
194 if !cli.quiet {
195 println!("{hp}");
196 }
197 }
198 } else if !cli.quiet {
199 println!("{hp}");
200 }
201 } else if !cli.quiet {
202 println!("No documentation URL configured for {}", plugin.name);
203 }
204 }
205 Ok(())
206 }
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213 use crate::cli::{Command, DocsArgs};
214 use crate::plugin::{PluginRegistry, PluginSource};
215 use tempfile::TempDir;
216
217 fn make_cli(quiet: bool, tool: Option<&str>) -> Cli {
218 Cli {
219 verbose: false,
220 quiet,
221 tool: tool.map(|s| s.to_string()),
222 command: Command::Info,
223 }
224 }
225
226 #[test]
227 fn cmd_tool_list_non_quiet_returns_ok() {
228 let registry = PluginRegistry::new().unwrap();
229 let dir = TempDir::new().unwrap();
230 let cli = make_cli(false, None);
231 let result = cmd_tool(&ToolCommand::List, &cli, None, dir.path(), ®istry);
232 assert!(result.is_ok());
233 }
234
235 #[test]
236 fn cmd_tool_list_quiet_returns_ok() {
237 let registry = PluginRegistry::new().unwrap();
238 let dir = TempDir::new().unwrap();
239 let cli = make_cli(true, None);
240 let result = cmd_tool(&ToolCommand::List, &cli, None, dir.path(), ®istry);
241 assert!(result.is_ok());
242 }
243
244 #[test]
245 fn cmd_tool_docs_with_homepage_returns_ok() {
246 let registry = PluginRegistry::new().unwrap();
247 let dir = TempDir::new().unwrap();
248 let cli = make_cli(false, Some("go"));
250 let docs_args = DocsArgs { open: false };
251 let result = cmd_tool(
252 &ToolCommand::Docs(docs_args),
253 &cli,
254 None,
255 dir.path(),
256 ®istry,
257 );
258 assert!(result.is_ok());
259 }
260
261 #[test]
262 fn cmd_tool_docs_quiet_with_homepage_returns_ok() {
263 let registry = PluginRegistry::new().unwrap();
264 let dir = TempDir::new().unwrap();
265 let cli = make_cli(true, Some("go"));
266 let docs_args = DocsArgs { open: false };
267 let result = cmd_tool(
268 &ToolCommand::Docs(docs_args),
269 &cli,
270 None,
271 dir.path(),
272 ®istry,
273 );
274 assert!(result.is_ok());
275 }
276
277 #[test]
278 fn cmd_tool_docs_no_homepage_returns_ok() {
279 let dir = TempDir::new().unwrap();
280 let toml = r#"
282[plugin]
283name = "nohp"
284
285[detect]
286files = ["nohp.txt"]
287
288[variants.default]
289binary = "echo"
290"#;
291 std::fs::write(dir.path().join("nohp.toml"), toml).unwrap();
292 let mut registry = PluginRegistry::new().unwrap();
293 registry
294 .load_dir(dir.path(), PluginSource::File(dir.path().to_path_buf()))
295 .unwrap();
296
297 let cli = make_cli(false, Some("nohp"));
298 let docs_args = DocsArgs { open: false };
299 let result = cmd_tool(
300 &ToolCommand::Docs(docs_args),
301 &cli,
302 None,
303 dir.path(),
304 ®istry,
305 );
306 assert!(result.is_ok());
307 }
308}