use std::fmt::Write;
use std::process::exit;
use color_eyre::eyre::Result;
use console::{pad_str, style, Alignment};
use indenter::indented;
use crate::build_time::built_info;
use crate::cli::command::Command;
use crate::cli::version::VERSION;
use crate::config::Config;
use crate::git::Git;
use crate::output::Output;
use crate::plugins::PluginType;
use crate::shell::ShellType;
use crate::toolset::ToolsetBuilder;
use crate::{cli, cmd};
use crate::{duration, env};
#[derive(Debug, clap::Args)]
#[clap(verbatim_doc_comment, after_long_help = AFTER_LONG_HELP)]
pub struct Doctor {}
impl Command for Doctor {
fn run(self, mut config: Config, out: &mut Output) -> Result<()> {
let ts = ToolsetBuilder::new().build(&mut config)?;
rtxprintln!(out, "{}", rtx_version());
rtxprintln!(out, "{}", build_info());
rtxprintln!(out, "{}", shell());
rtxprintln!(out, "{}", rtx_data_dir());
rtxprintln!(out, "{}", rtx_env_vars());
rtxprintln!(
out,
"{}\n{}\n",
style("settings:").bold(),
indent(config.settings.to_string())
);
rtxprintln!(out, "{}", render_config_files(&config));
rtxprintln!(out, "{}", render_plugins(&config));
rtxprintln!(
out,
"{}\n{}\n",
style("toolset:").bold(),
indent(ts.to_string())
);
let mut checks = Vec::new();
for plugin in config.tools.values() {
if !plugin.is_installed() {
checks.push(format!("plugin {} is not installed", &plugin.name));
continue;
}
}
if let Some(latest) = cli::version::check_for_new_version(duration::HOURLY) {
checks.push(format!(
"new rtx version {} available, currently on {}",
latest,
env!("CARGO_PKG_VERSION")
));
}
if !config.is_activated() {
let cmd = style("rtx activate").yellow().for_stderr();
checks.push(format!(
"rtx is not activated, run `{cmd}` for setup instructions"
));
}
if checks.is_empty() {
rtxprintln!(out, "No problems found");
} else {
let checks_plural = if checks.len() == 1 { "" } else { "s" };
let summary = format!("{} problem{checks_plural} found:", checks.len());
rtxprintln!(out, "{}", style(summary).red().bold());
for check in &checks {
rtxprintln!(out, "{}\n", check);
}
exit(1);
}
Ok(())
}
}
fn rtx_data_dir() -> String {
let mut s = style("rtx data directory:\n").bold().to_string();
s.push_str(&format!(" {}\n", env::RTX_DATA_DIR.to_string_lossy()));
s
}
fn rtx_env_vars() -> String {
let vars = env::vars()
.filter(|(k, _)| k.starts_with("RTX_"))
.collect::<Vec<(String, String)>>();
let mut s = style("rtx environment variables:\n").bold().to_string();
if vars.is_empty() {
s.push_str(" (none)\n");
}
for (k, v) in vars {
s.push_str(&format!(" {}={}\n", k, v));
}
s
}
fn render_config_files(config: &Config) -> String {
let mut s = style("config files:\n").bold().to_string();
for f in config.config_files.keys().rev() {
s.push_str(&format!(" {}\n", f.display()));
}
s
}
fn render_plugins(config: &Config) -> String {
let mut s = style("plugins:\n").bold().to_string();
let plugins = config
.tools
.values()
.filter(|p| p.is_installed())
.collect::<Vec<_>>();
let max_plugin_name_len = plugins.iter().map(|p| p.name.len()).max().unwrap_or(0) + 2;
for p in plugins {
let padded_name = pad_str(&p.name, max_plugin_name_len, Alignment::Left, None);
let si = match p.plugin.get_type() {
PluginType::External => {
let git = Git::new(p.plugin_path.clone());
match git.get_remote_url() {
Some(url) => {
let sha = git
.current_sha_short()
.unwrap_or_else(|_| "(unknown)".to_string());
format!(" {padded_name} {url}#{sha}\n")
}
None => format!(" {padded_name}\n"),
}
}
PluginType::Core => format!(" {padded_name} (core)\n"),
};
s.push_str(&si);
}
s
}
fn rtx_version() -> String {
let mut s = style("rtx version:\n").bold().to_string();
s.push_str(&format!(" {}\n", *VERSION));
s
}
fn build_info() -> String {
let mut s = style("build:\n").bold().to_string();
s.push_str(&format!(" Target: {}\n", built_info::TARGET));
s.push_str(&format!(" Features: {}\n", built_info::FEATURES_STR));
s.push_str(&format!(" Built: {}\n", built_info::BUILT_TIME_UTC));
s.push_str(&format!(" Rust Version: {}\n", built_info::RUSTC_VERSION));
s.push_str(&format!(" Profile: {}\n", built_info::PROFILE));
s
}
fn shell() -> String {
let mut s = style("shell:\n").bold().to_string();
match ShellType::load().map(|s| s.to_string()) {
Some(shell) => {
let shell_cmd = if env::SHELL.ends_with(shell.as_str()) {
&*env::SHELL
} else {
&shell
};
let version = cmd!(shell_cmd, "--version")
.read()
.unwrap_or_else(|e| format!("failed to get shell version: {}", e));
let out = format!("{}\n{}\n", shell_cmd, version);
s.push_str(&indent(out));
}
None => s.push_str(" (unknown)\n"),
}
s
}
fn indent(s: String) -> String {
let mut out = String::new();
write!(indented(&mut out).with_str(" "), "{}", s).unwrap();
out
}
static AFTER_LONG_HELP: &str = color_print::cstr!(
r#"<bold><underline>Examples:</underline></bold>
$ <bold>rtx doctor</bold>
[WARN] plugin node is not installed
"#
);