Skip to content

Commit

Permalink
new: Add proto diagnose command. (#517)
Browse files Browse the repository at this point in the history
  • Loading branch information
milesj committed Jun 16, 2024
1 parent d5a8657 commit 19b7cb0
Show file tree
Hide file tree
Showing 6 changed files with 298 additions and 8 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
13 changes: 10 additions & 3 deletions crates/cli/src/app.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -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",
Expand Down
271 changes: 271 additions & 0 deletions crates/cli/src/commands/diagnose.rs
Original file line number Diff line number Diff line change
@@ -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<ShellType>,

#[arg(long, help = "Print the diagnosis in JSON format")]
json: bool,
}

#[derive(Serialize)]
struct Issue {
issue: String,
resolution: Option<String>,
comment: Option<String>,
}

#[derive(Serialize)]
struct Diagnosis {
shell: String,
shell_profile: PathBuf,
errors: Vec<Issue>,
warnings: Vec<Issue>,
tips: Vec<String>,
}

#[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::<Vec<_>>();

// 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<String>) -> Vec<Issue> {
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<String>,
) -> Vec<Issue> {
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();
}
}
}
2 changes: 2 additions & 0 deletions crates/cli/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ mod bin;
mod clean;
mod completions;
pub mod debug;
mod diagnose;
mod install;
mod install_all;
mod list;
Expand All @@ -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::*;
Expand Down
1 change: 1 addition & 0 deletions crates/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
16 changes: 11 additions & 5 deletions crates/cli/src/printer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -162,6 +158,16 @@ impl<'std> Printer<'std> {
}
}

pub fn list<I: IntoIterator<Item = V>, V: AsRef<str>>(&mut self, list: I) {
let items = list.into_iter().collect::<Vec<_>>();

for item in items {
self.indent();

writeln!(&mut self.buffer, "{} {}", color::muted("-"), item.as_ref()).unwrap();
}
}

pub fn locator<L: AsRef<PluginLocator>>(&mut self, locator: L) {
match locator.as_ref() {
PluginLocator::File { path, .. } => {
Expand Down

0 comments on commit 19b7cb0

Please sign in to comment.