diff --git a/CHANGELOG.md b/CHANGELOG.md index b739283b7..48b9eab84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,9 @@ - Added experimental support for the [calver](https://calver.org) (calendar versioning) specification. For example: 2024-04, 2024-06-10, etc. - There are some caveats to this approach. Please refer to the documentation. + - This _should_ be backwards compatible with existing WASM plugins and tools, but in the off chance it is not, please pull in the new PDKs and publish a new release, or create an issue. +- Added a new command, `proto diagnose`, that can be used to diagnose any issues with your current proto installation. + - Currently diagnoses proto itself, but in the future will also diagnose currently installed tools. - WASM API - Added `VersionSpec::Calendar` and `UnresolvedVersionSpec::Calendar` variant types. diff --git a/crates/cli/src/app.rs b/crates/cli/src/app.rs index f53260bcc..0d01fca85 100644 --- a/crates/cli/src/app.rs +++ b/crates/cli/src/app.rs @@ -1,9 +1,9 @@ use crate::commands::{ debug::DebugConfigArgs, plugin::{AddPluginArgs, InfoPluginArgs, ListPluginsArgs, RemovePluginArgs, SearchPluginArgs}, - AliasArgs, BinArgs, CleanArgs, CompletionsArgs, InstallArgs, ListArgs, ListRemoteArgs, - MigrateArgs, OutdatedArgs, PinArgs, RegenArgs, RunArgs, SetupArgs, StatusArgs, UnaliasArgs, - UninstallArgs, UnpinArgs, + AliasArgs, BinArgs, CleanArgs, CompletionsArgs, DiagnoseArgs, InstallArgs, ListArgs, + ListRemoteArgs, MigrateArgs, OutdatedArgs, PinArgs, RegenArgs, RunArgs, SetupArgs, StatusArgs, + UnaliasArgs, UninstallArgs, UnpinArgs, }; use clap::builder::styling::{Color, Style, Styles}; use clap::{Parser, Subcommand, ValueEnum}; @@ -144,6 +144,13 @@ pub enum Commands { command: DebugCommands, }, + #[command( + alias = "doctor", + name = "diagnose", + about = "Diagnose potential issues with your proto installation." + )] + Diagnose(DiagnoseArgs), + #[command( alias = "i", name = "install", diff --git a/crates/cli/src/commands/diagnose.rs b/crates/cli/src/commands/diagnose.rs new file mode 100644 index 000000000..5bad88ea9 --- /dev/null +++ b/crates/cli/src/commands/diagnose.rs @@ -0,0 +1,271 @@ +use crate::printer::Printer; +use crate::session::ProtoSession; +use clap::Args; +use serde::Serialize; +use starbase::AppResult; +use starbase_shell::ShellType; +use starbase_styles::color; +use starbase_utils::json; +use std::path::PathBuf; +use std::{env, process}; + +#[derive(Args, Clone, Debug)] +pub struct DiagnoseArgs { + #[arg(long, help = "Shell to diagnose for")] + shell: Option, + + #[arg(long, help = "Print the diagnosis in JSON format")] + json: bool, +} + +#[derive(Serialize)] +struct Issue { + issue: String, + resolution: Option, + comment: Option, +} + +#[derive(Serialize)] +struct Diagnosis { + shell: String, + shell_profile: PathBuf, + errors: Vec, + warnings: Vec, + tips: Vec, +} + +#[tracing::instrument(skip_all)] +pub async fn diagnose(session: ProtoSession, args: DiagnoseArgs) -> AppResult { + let shell = match args.shell { + Some(value) => value, + None => ShellType::try_detect()?, + }; + let shell_data = shell.build(); + let shell_path = session + .env + .store + .load_preferred_profile()? + .unwrap_or_else(|| shell_data.get_env_path(&session.env.home)); + + let paths_env = env::var_os("PATH").unwrap_or_default(); + let paths = env::split_paths(&paths_env).collect::>(); + + // Disable ANSI colors in JSON output + if args.json { + env::set_var("NO_COLOR", "1"); + } + + let mut tips = vec![]; + let errors = gather_errors(&session, &paths, &mut tips); + let warnings = gather_warnings(&session, &paths, &mut tips); + + if args.json { + println!( + "{}", + json::format( + &Diagnosis { + shell: shell.to_string(), + shell_profile: shell_path, + errors, + warnings, + tips, + }, + true + )? + ); + + return Ok(()); + } + + if errors.is_empty() && warnings.is_empty() { + println!( + "{}", + color::success("No issues detected with your proto installation!") + ); + + return Ok(()); + } + + let mut printer = Printer::new(); + + printer.line(); + printer.entry("Shell", color::id(shell.to_string())); + printer.entry( + "Shell profile", + color::path( + session + .env + .store + .load_preferred_profile()? + .unwrap_or_else(|| shell_data.get_env_path(&session.env.home)), + ), + ); + + if !errors.is_empty() { + printer.named_section(color::failure("Errors"), |p| { + print_issues(&errors, p); + + Ok(()) + })?; + } + + if !warnings.is_empty() { + printer.named_section(color::caution("Warnings"), |p| { + print_issues(&warnings, p); + + Ok(()) + })?; + } + + if !tips.is_empty() { + printer.named_section(color::label("Tips"), |p| { + p.list(tips); + + Ok(()) + })?; + } + + printer.flush(); + + if !errors.is_empty() { + process::exit(1); + } + + Ok(()) +} + +fn gather_errors(session: &ProtoSession, paths: &[PathBuf], _tips: &mut Vec) -> Vec { + let mut errors = vec![]; + + let mut has_shims_before_bins = false; + let mut found_shims = false; + let mut found_bin = false; + + for path in paths { + if path == &session.env.store.shims_dir { + found_shims = true; + + if !found_bin { + has_shims_before_bins = true; + } + } else if path == &session.env.store.bin_dir { + found_bin = true; + } + } + + if !has_shims_before_bins && found_shims && found_bin { + errors.push(Issue { + issue: format!( + "Bin directory ({}) was found BEFORE the shims directory ({}) on {}", + color::path(&session.env.store.bin_dir), + color::path(&session.env.store.shims_dir), + color::property("PATH") + ), + resolution: Some( + "Ensure the shims path comes before the bin path in your shell".into(), + ), + comment: Some( + "Runtime version detection will not work correctly unless shims are used".into(), + ), + }) + } + + errors +} + +fn gather_warnings( + session: &ProtoSession, + paths: &[PathBuf], + tips: &mut Vec, +) -> Vec { + let mut warnings = vec![]; + + if env::var("PROTO_HOME").is_err() { + warnings.push(Issue { + issue: format!( + "Missing {} environment variable", + color::property("PROTO_HOME") + ), + resolution: Some(format!( + "Export {} from your shell", + color::label("PROTO_HOME=\"$HOME/.proto\"") + )), + comment: Some(format!( + "Will default to {} if not defined", + color::file("~/.proto") + )), + }); + } + + let has_shims_on_path = paths + .iter() + .any(|path| path == &session.env.store.shims_dir); + + if !has_shims_on_path { + warnings.push(Issue { + issue: format!( + "Shims directory ({}) not found on {}", + color::path(&session.env.store.shims_dir), + color::property("PATH") + ), + resolution: Some(format!( + "Append {} to path in your shell", + color::label("$PROTO_HOME/shims") + )), + comment: Some("If not using shims on purpose, ignore this warning".into()), + }) + } + + let has_bins_on_path = paths.iter().any(|path| path == &session.env.store.bin_dir); + + if !has_bins_on_path { + warnings.push(Issue { + issue: format!( + "Bin directory ({}) not found on {}", + color::path(&session.env.store.bin_dir), + color::property("PATH") + ), + resolution: Some(format!( + "Append {} to path in your shell", + color::label("$PROTO_HOME/bin") + )), + comment: None, + }) + } + + if !warnings.is_empty() { + tips.push(format!( + "Run {} to resolve some of these issues!", + color::shell("proto setup") + )); + } + + warnings +} + +fn print_issues(issues: &[Issue], printer: &mut Printer) { + let length = issues.len() - 1; + + for (index, issue) in issues.iter().enumerate() { + printer.entry( + color::muted_light("Issue"), + format!( + "{} {}", + &issue.issue, + if let Some(comment) = &issue.comment { + color::muted_light(format!("({})", comment)) + } else { + "".into() + } + ), + ); + + if let Some(resolution) = &issue.resolution { + printer.entry(color::muted_light("Resolution"), resolution); + } + + if index != length { + printer.line(); + } + } +} diff --git a/crates/cli/src/commands/mod.rs b/crates/cli/src/commands/mod.rs index 10b2b72ca..6fbbcfa79 100644 --- a/crates/cli/src/commands/mod.rs +++ b/crates/cli/src/commands/mod.rs @@ -3,6 +3,7 @@ mod bin; mod clean; mod completions; pub mod debug; +mod diagnose; mod install; mod install_all; mod list; @@ -24,6 +25,7 @@ pub use alias::*; pub use bin::*; pub use clean::*; pub use completions::*; +pub use diagnose::*; pub use install::*; pub use install_all::*; pub use list::*; diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 7d674f0d8..148cde078 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -79,6 +79,7 @@ async fn main() -> MainResult { DebugCommands::Config(args) => commands::debug::config(session, args).await, DebugCommands::Env => commands::debug::env(session).await, }, + Commands::Diagnose(args) => commands::diagnose(session, args).await, Commands::Install(args) => commands::install(session, args).await, Commands::List(args) => commands::list(session, args).await, Commands::ListRemote(args) => commands::list_remote(session, args).await, diff --git a/crates/cli/src/printer.rs b/crates/cli/src/printer.rs index cac6680a6..05189a12a 100644 --- a/crates/cli/src/printer.rs +++ b/crates/cli/src/printer.rs @@ -109,11 +109,7 @@ impl<'std> Printer<'std> { self.depth += 1; - for item in items { - self.indent(); - - writeln!(&mut self.buffer, "{} {}", color::muted("-"), item.as_ref()).unwrap(); - } + self.list(items); self.depth -= 1; } @@ -162,6 +158,16 @@ impl<'std> Printer<'std> { } } + pub fn list, V: AsRef>(&mut self, list: I) { + let items = list.into_iter().collect::>(); + + for item in items { + self.indent(); + + writeln!(&mut self.buffer, "{} {}", color::muted("-"), item.as_ref()).unwrap(); + } + } + pub fn locator>(&mut self, locator: L) { match locator.as_ref() { PluginLocator::File { path, .. } => {