Skip to main content

ubt_cli/commands/
tool.rs

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    // ── 1. Check detected tool binary ────────────────────────────────────
43    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                        // Try to get version
55                        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    // ── 2. Validate ubt.toml ────────────────────────────────────────────
91    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            // ── 3. Verify alias targets exist ──────────────────────────
102            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}