use std::fmt::Write;
use std::process::exit;
use console::{pad_str, style, Alignment};
use eyre::Result;
use indenter::indented;
use crate::build_time::built_info;
use crate::cli::version::VERSION;
use crate::config::{Config, Settings};
use crate::file::display_path;
use crate::git::Git;
use crate::plugins::PluginType;
use crate::shell::ShellType;
use crate::toolset::ToolsetBuilder;
use crate::{cli, cmd, dirs, forge};
use crate::{duration, env};
#[derive(Debug, clap::Args)]
#[clap(verbatim_doc_comment, after_long_help = AFTER_LONG_HELP)]
pub struct Doctor {}
impl Doctor {
pub fn run(self) -> Result<()> {
let mut checks = Vec::new();
if let Err(err) = Config::try_get() {
checks.push(format!("failed to load config: {}", err));
}
miseprintln!("{}", mise_version());
miseprintln!("{}", build_info());
miseprintln!("{}", shell());
miseprintln!("{}", mise_data_dir());
miseprintln!("{}", mise_env_vars());
match Settings::try_get() {
Ok(settings) => {
miseprintln!(
"{}\n{}\n",
style("settings:").bold(),
indent(settings.to_string())
);
}
Err(err) => warn!("failed to load settings: {}", err),
}
match Config::try_get() {
Ok(config) => {
miseprintln!("{}", render_config_files(&config));
miseprintln!("{}", render_plugins());
for plugin in forge::list() {
if !plugin.is_installed() {
checks.push(format!("plugin {} is not installed", &plugin.id()));
continue;
}
}
if !config.is_activated() && !shims_on_path() {
let cmd = style("mise help activate").yellow().for_stderr();
let url = style("https://mise.jdx.dev").underlined().for_stderr();
let shims = style(dirs::SHIMS.display()).cyan().for_stderr();
checks.push(formatdoc!(
r#"mise is not activated, run {cmd} or
read documentation at {url} for activation instructions.
Alternatively, add the shims directory {shims} to PATH.
Using the shims directory is preferred for non-interactive setups."#
));
}
match ToolsetBuilder::new().build(&config) {
Ok(ts) => {
miseprintln!("{}\n{}\n", style("toolset:").bold(), indent(ts.to_string()))
}
Err(err) => warn!("failed to load toolset: {}", err),
}
}
Err(err) => warn!("failed to load config: {}", err),
}
if let Some(latest) = cli::version::check_for_new_version(duration::HOURLY) {
checks.push(format!(
"new mise version {} available, currently on {}",
latest,
env!("CARGO_PKG_VERSION")
));
}
if checks.is_empty() {
miseprintln!("No problems found");
} else {
let checks_plural = if checks.len() == 1 { "" } else { "s" };
let summary = format!("{} problem{checks_plural} found:", checks.len());
miseprintln!("{}", style(summary).red().bold());
for check in &checks {
miseprintln!("{}\n", check);
}
exit(1);
}
Ok(())
}
}
fn shims_on_path() -> bool {
env::PATH.contains(&*dirs::SHIMS)
}
fn mise_data_dir() -> String {
let mut s = style("mise data directory:\n").bold().to_string();
s.push_str(&format!(" {}\n", env::MISE_DATA_DIR.to_string_lossy()));
s
}
fn mise_env_vars() -> String {
let vars = env::vars()
.filter(|(k, _)| k.starts_with("MISE_"))
.collect::<Vec<(String, String)>>();
let mut s = style("mise 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", display_path(f)));
}
s
}
fn render_plugins() -> String {
let mut s = style("plugins:\n").bold().to_string();
let plugins = forge::list()
.into_iter()
.filter(|p| p.is_installed())
.collect::<Vec<_>>();
let max_plugin_name_len = plugins.iter().map(|p| p.id().len()).max().unwrap_or(0) + 2;
for p in plugins {
let padded_name = pad_str(p.id(), max_plugin_name_len, Alignment::Left, None);
let si = match p.get_plugin_type() {
PluginType::External => {
let git = Git::new(dirs::PLUGINS.join(p.id()));
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 mise_version() -> String {
let mut s = style("mise 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>mise doctor</bold>
[WARN] plugin node is not installed
"#
);