idt/
main.rs

1//! Install language servers, linters, formatters, and developer helpers concurrently.
2//!
3//! # Arguments
4//! - `dev_tools_dir` Directory for tool installation (created if missing).
5//! - `bin_dir` Directory for binary symlinks (created if missing).
6//! - `tool_names` Optional specific tools to install (defaults to all).
7//!
8//! # Usage
9//! ```bash
10//! idt ~/.dev/tools ~/.local/bin # install all tools
11//! idt ~/.dev/tools ~/.local/bin ruff_lsp rust_analyzer taplo # subset
12//! ```
13//!
14//! # Flow
15//! 1. Ensure target directories.
16//! 2. Auth to GitHub (rate limits, releases).
17//! 3. Resolve selected installers (all or subset).
18//! 4. Spawn scoped threads to run installers.
19//! 5. Cleanup dead symlinks; aggregate failures.
20//!
21//! # Errors
22//! - Missing required argument (`dev_tools_dir` / `bin_dir`).
23//! - Directory creation fails.
24//! - GitHub authentication fails.
25//! - Installer thread panics.
26//! - Individual tool installation fails (installer reports detail).
27//! - Dead symlink cleanup fails.
28#![feature(exit_status_error)]
29
30use std::path::Path;
31
32use color_eyre::eyre::eyre;
33use color_eyre::owo_colors::OwoColorize as _;
34use ytil_sys::SysInfo;
35use ytil_sys::cli::Args;
36
37use crate::installers::Installer;
38use crate::installers::bash_language_server::BashLanguageServer;
39use crate::installers::commitlint::Commitlint;
40use crate::installers::deno::Deno;
41use crate::installers::docker_langserver::DockerLangServer;
42use crate::installers::eslint_d::EslintD;
43use crate::installers::graphql_lsp::GraphQlLsp;
44use crate::installers::hadolint::Hadolint;
45use crate::installers::harper_ls::HarperLs;
46use crate::installers::helm_ls::HelmLs;
47use crate::installers::lua_ls::LuaLanguageServer;
48use crate::installers::marksman::Marksman;
49use crate::installers::nvim::Nvim;
50use crate::installers::prettierd::PrettierD;
51use crate::installers::quicktype::Quicktype;
52use crate::installers::ruff_lsp::RuffLsp;
53use crate::installers::rust_analyzer::RustAnalyzer;
54use crate::installers::shellcheck::Shellcheck;
55use crate::installers::sql_language_server::SqlLanguageServer;
56use crate::installers::sqruff::Sqruff;
57use crate::installers::taplo::Taplo;
58use crate::installers::terraform_ls::TerraformLs;
59use crate::installers::typescript_language_server::TypescriptLanguageServer;
60use crate::installers::typos_lsp::TyposLsp;
61use crate::installers::vscode_langservers::VsCodeLangServers;
62use crate::installers::yaml_language_server::YamlLanguageServer;
63
64mod downloaders;
65mod installers;
66
67/// Summarize installer thread outcomes; collect failing bin names.
68///
69/// # Errors
70/// - Does not construct a rich error enum; instead returns failing bin names. Individual installers are expected to
71///   have already printed detailed stderr output.
72/// - A thread panic is logged immediately and its bin name added to the returned list.
73///
74/// # Rationale
75/// - Simple bin-name list keeps aggregation lightweight for CI scripting.
76/// - Delegates detailed formatting to installers; central function only normalizes aggregation.
77/// - Easier to extend with JSON output later (convert list directly).
78fn report<'a>(
79    installers_res: &'a [(&'a str, std::thread::Result<color_eyre::Result<()>>)],
80) -> Result<(), Vec<&'a str>> {
81    let mut errors_bins = vec![];
82
83    for (bin_name, result) in installers_res {
84        match result {
85            Err(err) => {
86                eprintln!(
87                    "{} installer thread panicked error={}",
88                    bin_name.red(), // removed bold
89                    format!("{err:#?}").red()
90                );
91                errors_bins.push(*bin_name);
92            }
93            Ok(Err(_)) => errors_bins.push(bin_name),
94            Ok(Ok(())) => {}
95        }
96    }
97
98    if errors_bins.is_empty() {
99        return Ok(());
100    }
101    Err(errors_bins)
102}
103
104/// Install language servers, linters, formatters, and developer helpers concurrently.
105#[allow(clippy::too_many_lines)]
106fn main() -> color_eyre::Result<()> {
107    color_eyre::install()?;
108
109    let args = ytil_sys::cli::get();
110    if args.has_help() {
111        println!("{}", include_str!("../help.txt"));
112        return Ok(());
113    }
114    println!(
115        "{:#?} started with args {}",
116        std::env::current_exe()?.bold().cyan(),
117        format!("{args:#?}").white().bold()
118    );
119
120    let dev_tools_dir = args
121        .first()
122        .ok_or_else(|| eyre!("missing dev_tools_dir arg | args={args:#?}"))?
123        .trim_end_matches('/');
124    let bin_dir = args
125        .get(1)
126        .ok_or_else(|| eyre!("missing bin_dir arg | args={args:#?}"))?
127        .trim_end_matches('/');
128    let supplied_bin_names: Vec<&str> = args.iter().skip(2).map(AsRef::as_ref).collect();
129
130    let sys_info = SysInfo::get()?;
131
132    std::fs::create_dir_all(dev_tools_dir)?;
133    std::fs::create_dir_all(bin_dir)?;
134
135    let all_installers: Vec<Box<dyn Installer>> = vec![
136        Box::new(BashLanguageServer {
137            dev_tools_dir: Path::new(dev_tools_dir),
138            bin_dir: Path::new(bin_dir),
139        }),
140        Box::new(Commitlint {
141            dev_tools_dir: Path::new(dev_tools_dir),
142            bin_dir: Path::new(bin_dir),
143        }),
144        Box::new(Deno {
145            bin_dir: Path::new(bin_dir),
146            sys_info: &sys_info,
147        }),
148        Box::new(DockerLangServer {
149            dev_tools_dir: Path::new(dev_tools_dir),
150            bin_dir: Path::new(bin_dir),
151        }),
152        Box::new(EslintD {
153            dev_tools_dir: Path::new(dev_tools_dir),
154            bin_dir: Path::new(bin_dir),
155        }),
156        Box::new(GraphQlLsp {
157            dev_tools_dir: Path::new(dev_tools_dir),
158            bin_dir: Path::new(bin_dir),
159        }),
160        Box::new(Hadolint {
161            bin_dir: Path::new(bin_dir),
162            sys_info: &sys_info,
163        }),
164        Box::new(HarperLs {
165            bin_dir: Path::new(bin_dir),
166        }),
167        Box::new(HelmLs {
168            bin_dir: Path::new(bin_dir),
169            sys_info: &sys_info,
170        }),
171        Box::new(LuaLanguageServer {
172            dev_tools_dir: Path::new(dev_tools_dir),
173            sys_info: &sys_info,
174        }),
175        Box::new(Marksman {
176            bin_dir: Path::new(bin_dir),
177            sys_info: &sys_info,
178        }),
179        Box::new(Nvim {
180            dev_tools_dir: Path::new(dev_tools_dir),
181            bin_dir: Path::new(bin_dir),
182        }),
183        Box::new(PrettierD {
184            dev_tools_dir: Path::new(dev_tools_dir),
185            bin_dir: Path::new(bin_dir),
186        }),
187        Box::new(Quicktype {
188            dev_tools_dir: Path::new(dev_tools_dir),
189            bin_dir: Path::new(bin_dir),
190        }),
191        Box::new(RuffLsp {
192            dev_tools_dir: Path::new(dev_tools_dir),
193            bin_dir: Path::new(bin_dir),
194        }),
195        Box::new(RustAnalyzer {
196            bin_dir: Path::new(bin_dir),
197            sys_info: &sys_info,
198        }),
199        Box::new(Shellcheck {
200            bin_dir: Path::new(bin_dir),
201            sys_info: &sys_info,
202        }),
203        Box::new(Sqruff {
204            bin_dir: Path::new(bin_dir),
205            sys_info: &sys_info,
206        }),
207        Box::new(SqlLanguageServer {
208            dev_tools_dir: Path::new(dev_tools_dir),
209            bin_dir: Path::new(bin_dir),
210        }),
211        Box::new(Taplo {
212            bin_dir: Path::new(bin_dir),
213        }),
214        Box::new(TerraformLs {
215            bin_dir: Path::new(bin_dir),
216            sys_info: &sys_info,
217        }),
218        Box::new(TypescriptLanguageServer {
219            dev_tools_dir: Path::new(dev_tools_dir),
220            bin_dir: Path::new(bin_dir),
221        }),
222        Box::new(TyposLsp {
223            bin_dir: Path::new(bin_dir),
224            sys_info: &sys_info,
225        }),
226        Box::new(VsCodeLangServers {
227            dev_tools_dir: Path::new(dev_tools_dir),
228            bin_dir: Path::new(bin_dir),
229        }),
230        Box::new(YamlLanguageServer {
231            dev_tools_dir: Path::new(dev_tools_dir),
232            bin_dir: Path::new(bin_dir),
233        }),
234    ];
235
236    let (selected_installers, unknown_bin_names): (Vec<_>, Vec<_>) = if supplied_bin_names.is_empty() {
237        (all_installers.iter().collect(), vec![])
238    } else {
239        let mut selected_installers = vec![];
240        let mut unknown_installers = vec![];
241        for chosen_bin in supplied_bin_names {
242            if let Some(i) = all_installers.iter().find(|i| chosen_bin == i.bin_name()) {
243                selected_installers.push(i);
244            } else {
245                unknown_installers.push(chosen_bin);
246            }
247        }
248        (selected_installers, unknown_installers)
249    };
250
251    if !unknown_bin_names.is_empty() {
252        eprintln!(
253            "{} bins without matching installers",
254            format!("{unknown_bin_names:#?}").yellow().bold()
255        );
256    }
257
258    let installers_res = std::thread::scope(|scope| {
259        let mut handles = Vec::with_capacity(selected_installers.len());
260        for installer in selected_installers {
261            handles.push((installer.bin_name(), scope.spawn(move || installer.run())));
262        }
263        let mut res = Vec::with_capacity(handles.len());
264        for (bin_name, handle) in handles {
265            res.push((bin_name, handle.join()));
266        }
267        res
268    });
269
270    if let Err(errors) = report(&installers_res) {
271        eprintln!(
272            "{} | errors_count={} bin_names={errors:#?}",
273            "error installing tools".red(),
274            errors.len()
275        );
276        std::process::exit(1);
277    }
278
279    ytil_sys::rm::rm_dead_symlinks(bin_dir)?;
280
281    Ok(())
282}