Browse Source

refactor: add useful diagnostics to `tauri info` (#6570)

* refactor: add useful diagnostics to `tauri info`

this also paves the way for interactive fixes in the future

* refactor: add useful diagnostics to `tauri info`

this also paves the way for interactive fixes in the future

* fix use_npm logic [skip ci]

* lint [skip ci]

---------

Co-authored-by: Lucas Nogueira <lucas@tauri.studio>
Amr Bashir 2 years ago
parent
commit
1487b18dcb

BIN
tooling/cli/scripts/vswhere.exe


+ 0 - 937
tooling/cli/src/info.rs

@@ -1,937 +0,0 @@
-// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
-// SPDX-License-Identifier: Apache-2.0
-// SPDX-License-Identifier: MIT
-
-use crate::{
-  helpers::{config::get as get_config, framework::infer_from_package_json as infer_framework},
-  interface::rust::get_workspace_dir,
-  Result,
-};
-use clap::Parser;
-use colored::Colorize;
-use serde::Deserialize;
-
-use std::{
-  collections::HashMap,
-  fmt::Write,
-  fs::{read_dir, read_to_string},
-  panic,
-  path::{Path, PathBuf},
-  process::Command,
-};
-
-#[derive(Deserialize)]
-struct YarnVersionInfo {
-  data: Vec<String>,
-}
-
-#[derive(Clone, Deserialize)]
-struct CargoLockPackage {
-  name: String,
-  version: String,
-  source: Option<String>,
-}
-
-#[derive(Deserialize)]
-struct CargoLock {
-  package: Vec<CargoLockPackage>,
-}
-
-#[derive(Deserialize)]
-struct JsCliVersionMetadata {
-  version: String,
-  node: String,
-}
-
-#[derive(Deserialize)]
-#[serde(rename_all = "camelCase")]
-struct VersionMetadata {
-  #[serde(rename = "cli.js")]
-  js_cli: JsCliVersionMetadata,
-}
-
-#[derive(Clone, Deserialize)]
-struct CargoManifestDependencyPackage {
-  version: Option<String>,
-  git: Option<String>,
-  branch: Option<String>,
-  rev: Option<String>,
-  path: Option<PathBuf>,
-}
-
-#[derive(Clone, Deserialize)]
-#[serde(untagged)]
-enum CargoManifestDependency {
-  Version(String),
-  Package(CargoManifestDependencyPackage),
-}
-
-#[derive(Deserialize)]
-struct CargoManifestPackage {
-  version: String,
-}
-
-#[derive(Deserialize)]
-struct CargoManifest {
-  package: CargoManifestPackage,
-  dependencies: HashMap<String, CargoManifestDependency>,
-}
-
-#[derive(Debug, PartialEq, Eq)]
-enum PackageManager {
-  Npm,
-  Pnpm,
-  Yarn,
-  Berry,
-}
-
-#[derive(Debug, Parser)]
-#[clap(about = "Shows information about Tauri dependencies and project configuration")]
-pub struct Options;
-
-fn version_metadata() -> Result<VersionMetadata> {
-  serde_json::from_str::<VersionMetadata>(include_str!("../metadata.json")).map_err(Into::into)
-}
-
-#[cfg(not(debug_assertions))]
-pub(crate) fn cli_current_version() -> Result<String> {
-  version_metadata().map(|meta| meta.js_cli.version)
-}
-
-#[cfg(not(debug_assertions))]
-pub(crate) fn cli_upstream_version() -> Result<String> {
-  let upstream_metadata = match ureq::get(
-    "https://raw.githubusercontent.com/tauri-apps/tauri/dev/tooling/cli/metadata.json",
-  )
-  .timeout(std::time::Duration::from_secs(3))
-  .call()
-  {
-    Ok(r) => r,
-    Err(ureq::Error::Status(code, _response)) => {
-      let message = format!("Unable to find updates at the moment. Code: {}", code);
-      return Err(anyhow::Error::msg(message));
-    }
-    Err(ureq::Error::Transport(transport)) => {
-      let message = format!(
-        "Unable to find updates at the moment. Error: {:?}",
-        transport.kind()
-      );
-      return Err(anyhow::Error::msg(message));
-    }
-  };
-
-  upstream_metadata
-    .into_string()
-    .and_then(|meta_str| Ok(serde_json::from_str::<VersionMetadata>(&meta_str)))
-    .and_then(|json| Ok(json.unwrap().js_cli.version))
-    .map_err(|e| anyhow::Error::new(e))
-}
-
-fn crate_latest_version(name: &str) -> Option<String> {
-  let url = format!("https://docs.rs/crate/{name}/");
-  match ureq::get(&url).call() {
-    Ok(response) => match (response.status(), response.header("location")) {
-      (302, Some(location)) => Some(location.replace(&url, "")),
-      _ => None,
-    },
-    Err(_) => None,
-  }
-}
-
-#[allow(clippy::let_and_return)]
-fn cross_command(bin: &str) -> Command {
-  #[cfg(target_os = "windows")]
-  let cmd = {
-    let mut cmd = Command::new("cmd");
-    cmd.arg("/c").arg(bin);
-    cmd
-  };
-  #[cfg(not(target_os = "windows"))]
-  let cmd = Command::new(bin);
-  cmd
-}
-
-fn npm_latest_version(pm: &PackageManager, name: &str) -> crate::Result<Option<String>> {
-  match pm {
-    PackageManager::Yarn => {
-      let mut cmd = cross_command("yarn");
-
-      let output = cmd
-        .arg("info")
-        .arg(name)
-        .args(["version", "--json"])
-        .output()?;
-      if output.status.success() {
-        let stdout = String::from_utf8_lossy(&output.stdout);
-        let info: YarnVersionInfo = serde_json::from_str(&stdout)?;
-        Ok(Some(info.data.last().unwrap().to_string()))
-      } else {
-        Ok(None)
-      }
-    }
-    PackageManager::Berry => {
-      let mut cmd = cross_command("yarn");
-
-      let output = cmd
-        .arg("npm")
-        .arg("info")
-        .arg(name)
-        .args(["--fields", "version", "--json"])
-        .output()?;
-      if output.status.success() {
-        let info: crate::PackageJson =
-          serde_json::from_reader(std::io::Cursor::new(output.stdout)).unwrap();
-        Ok(info.version)
-      } else {
-        Ok(None)
-      }
-    }
-    PackageManager::Npm => {
-      let mut cmd = cross_command("npm");
-
-      let output = cmd.arg("show").arg(name).arg("version").output()?;
-      if output.status.success() {
-        let stdout = String::from_utf8_lossy(&output.stdout);
-        Ok(Some(stdout.replace('\n', "")))
-      } else {
-        Ok(None)
-      }
-    }
-    PackageManager::Pnpm => {
-      let mut cmd = cross_command("pnpm");
-
-      let output = cmd.arg("info").arg(name).arg("version").output()?;
-      if output.status.success() {
-        let stdout = String::from_utf8_lossy(&output.stdout);
-        Ok(Some(stdout.replace('\n', "")))
-      } else {
-        Ok(None)
-      }
-    }
-  }
-}
-
-fn npm_package_version<P: AsRef<Path>>(
-  pm: &PackageManager,
-  name: &str,
-  app_dir: P,
-) -> crate::Result<Option<String>> {
-  let (output, regex) = match pm {
-    PackageManager::Yarn => (
-      cross_command("yarn")
-        .args(["list", "--pattern"])
-        .arg(name)
-        .args(["--depth", "0"])
-        .current_dir(app_dir)
-        .output()?,
-      None,
-    ),
-    PackageManager::Berry => (
-      cross_command("yarn")
-        .arg("info")
-        .arg(name)
-        .current_dir(app_dir)
-        .output()?,
-      Some(regex::Regex::new("Version: ([\\da-zA-Z\\-\\.]+)").unwrap()),
-    ),
-    PackageManager::Npm => (
-      cross_command("npm")
-        .arg("list")
-        .arg(name)
-        .args(["version", "--depth", "0"])
-        .current_dir(app_dir)
-        .output()?,
-      None,
-    ),
-    PackageManager::Pnpm => (
-      cross_command("pnpm")
-        .arg("list")
-        .arg(name)
-        .args(["--parseable", "--depth", "0"])
-        .current_dir(app_dir)
-        .output()?,
-      None,
-    ),
-  };
-  if output.status.success() {
-    let stdout = String::from_utf8_lossy(&output.stdout);
-    let regex = regex.unwrap_or_else(|| regex::Regex::new("@([\\da-zA-Z\\-\\.]+)").unwrap());
-    Ok(
-      regex
-        .captures_iter(&stdout)
-        .last()
-        .and_then(|cap| cap.get(1).map(|v| v.as_str().to_string())),
-    )
-  } else {
-    Ok(None)
-  }
-}
-
-fn get_version(command: &str, args: &[&str]) -> crate::Result<Option<String>> {
-  let output = cross_command(command)
-    .args(args)
-    .arg("--version")
-    .output()?;
-  let version = if output.status.success() {
-    Some(String::from_utf8_lossy(&output.stdout).replace(['\n', '\r'], ""))
-  } else {
-    None
-  };
-  Ok(version)
-}
-
-#[cfg(windows)]
-fn webview2_version() -> crate::Result<Option<String>> {
-  let powershell_path = std::env::var("SYSTEMROOT").map_or_else(
-    |_| "powershell.exe".to_string(),
-    |p| format!("{p}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"),
-  );
-  // check 64bit machine-wide installation
-  let output = Command::new(&powershell_path)
-      .args(["-NoProfile", "-Command"])
-      .arg("Get-ItemProperty -Path 'HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}' | ForEach-Object {$_.pv}")
-      .output()?;
-  if output.status.success() {
-    return Ok(Some(
-      String::from_utf8_lossy(&output.stdout).replace('\n', ""),
-    ));
-  }
-  // check 32bit machine-wide installation
-  let output = Command::new(&powershell_path)
-        .args(["-NoProfile", "-Command"])
-        .arg("Get-ItemProperty -Path 'HKLM:\\SOFTWARE\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}' | ForEach-Object {$_.pv}")
-        .output()?;
-  if output.status.success() {
-    return Ok(Some(
-      String::from_utf8_lossy(&output.stdout).replace('\n', ""),
-    ));
-  }
-  // check user-wide installation
-  let output = Command::new(&powershell_path)
-      .args(["-NoProfile", "-Command"])
-      .arg("Get-ItemProperty -Path 'HKCU:\\SOFTWARE\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}' | ForEach-Object {$_.pv}")
-      .output()?;
-  if output.status.success() {
-    return Ok(Some(
-      String::from_utf8_lossy(&output.stdout).replace('\n', ""),
-    ));
-  }
-
-  Ok(None)
-}
-
-#[cfg(windows)]
-#[derive(Deserialize, Debug)]
-#[serde(rename_all = "camelCase")]
-struct VsInstanceInfo {
-  display_name: String,
-}
-
-#[cfg(windows)]
-const VSWHERE: &[u8] = include_bytes!("../scripts/vswhere.exe");
-
-#[cfg(windows)]
-fn build_tools_version() -> crate::Result<Option<Vec<String>>> {
-  let mut vswhere = std::env::temp_dir();
-  vswhere.push("vswhere.exe");
-
-  if !vswhere.exists() {
-    if let Ok(mut file) = std::fs::File::create(&vswhere) {
-      use std::io::Write;
-      let _ = file.write_all(VSWHERE);
-    }
-  }
-  let output = cross_command(vswhere.to_str().unwrap())
-    .args([
-      "-prerelease",
-      "-products",
-      "*",
-      "-requiresAny",
-      "-requires",
-      "Microsoft.VisualStudio.Workload.NativeDesktop",
-      "-requires",
-      "Microsoft.VisualStudio.Workload.VCTools",
-      "-format",
-      "json",
-    ])
-    .output()?;
-  Ok(if output.status.success() {
-    let stdout = String::from_utf8_lossy(&output.stdout);
-    let instances: Vec<VsInstanceInfo> = serde_json::from_str(&stdout)?;
-    Some(
-      instances
-        .iter()
-        .map(|i| i.display_name.clone())
-        .collect::<Vec<String>>(),
-    )
-  } else {
-    None
-  })
-}
-
-fn active_rust_toolchain() -> crate::Result<Option<String>> {
-  let output = cross_command("rustup")
-    .args(["show", "active-toolchain"])
-    .output()?;
-  let toolchain = if output.status.success() {
-    Some(
-      String::from_utf8_lossy(&output.stdout)
-        .replace(['\n', '\r'], "")
-        .split('(')
-        .collect::<Vec<&str>>()[0]
-        .into(),
-    )
-  } else {
-    None
-  };
-  Ok(toolchain)
-}
-
-fn crate_version(
-  tauri_dir: &Path,
-  manifest: Option<&CargoManifest>,
-  lock: Option<&CargoLock>,
-  name: &str,
-) -> (String, Option<String>) {
-  let crate_lock_packages: Vec<CargoLockPackage> = lock
-    .as_ref()
-    .map(|lock| {
-      lock
-        .package
-        .iter()
-        .filter(|p| p.name == name)
-        .cloned()
-        .collect()
-    })
-    .unwrap_or_default();
-  let (crate_version_string, found_crate_versions) =
-    match (&manifest, &lock, crate_lock_packages.len()) {
-      (Some(_manifest), Some(_lock), 1) => {
-        let crate_lock_package = crate_lock_packages.first().unwrap();
-        let version_string = if let Some(s) = &crate_lock_package.source {
-          if s.starts_with("git") {
-            format!("{} ({})", s, crate_lock_package.version)
-          } else {
-            crate_lock_package.version.clone()
-          }
-        } else {
-          crate_lock_package.version.clone()
-        };
-        (version_string, vec![crate_lock_package.version.clone()])
-      }
-      (None, Some(_lock), 1) => {
-        let crate_lock_package = crate_lock_packages.first().unwrap();
-        let version_string = if let Some(s) = &crate_lock_package.source {
-          if s.starts_with("git") {
-            format!("{} ({})", s, crate_lock_package.version)
-          } else {
-            crate_lock_package.version.clone()
-          }
-        } else {
-          crate_lock_package.version.clone()
-        };
-        (
-          format!("{version_string} (no manifest)"),
-          vec![crate_lock_package.version.clone()],
-        )
-      }
-      _ => {
-        let mut found_crate_versions = Vec::new();
-        let mut is_git = false;
-        let manifest_version = match manifest.and_then(|m| m.dependencies.get(name).cloned()) {
-          Some(tauri) => match tauri {
-            CargoManifestDependency::Version(v) => {
-              found_crate_versions.push(v.clone());
-              v
-            }
-            CargoManifestDependency::Package(p) => {
-              if let Some(v) = p.version {
-                found_crate_versions.push(v.clone());
-                v
-              } else if let Some(p) = p.path {
-                let manifest_path = tauri_dir.join(&p).join("Cargo.toml");
-                let v = match read_to_string(manifest_path)
-                  .map_err(|_| ())
-                  .and_then(|m| toml::from_str::<CargoManifest>(&m).map_err(|_| ()))
-                {
-                  Ok(manifest) => manifest.package.version,
-                  Err(_) => "unknown version".to_string(),
-                };
-                format!("path:{p:?} [{v}]")
-              } else if let Some(g) = p.git {
-                is_git = true;
-                let mut v = format!("git:{g}");
-                if let Some(branch) = p.branch {
-                  let _ = write!(v, "&branch={branch}");
-                } else if let Some(rev) = p.rev {
-                  let _ = write!(v, "#{rev}");
-                }
-                v
-              } else {
-                "unknown manifest".to_string()
-              }
-            }
-          },
-          None => "no manifest".to_string(),
-        };
-
-        let lock_version = match (lock, crate_lock_packages.is_empty()) {
-          (Some(_lock), false) => crate_lock_packages
-            .iter()
-            .map(|p| p.version.clone())
-            .collect::<Vec<String>>()
-            .join(", "),
-          (Some(_lock), true) => "unknown lockfile".to_string(),
-          _ => "no lockfile".to_string(),
-        };
-
-        (
-          format!(
-            "{} {}({})",
-            manifest_version,
-            if is_git { "(git manifest)" } else { "" },
-            lock_version
-          ),
-          found_crate_versions,
-        )
-      }
-    };
-
-  let crate_version = found_crate_versions
-    .into_iter()
-    .map(|v| semver::Version::parse(&v).ok())
-    .max();
-  let suffix = match (crate_version, crate_latest_version(name)) {
-    (Some(Some(version)), Some(target_version)) => {
-      let target_version = semver::Version::parse(&target_version).unwrap();
-      if version < target_version {
-        Some(format!(" (outdated, latest: {target_version})"))
-      } else {
-        None
-      }
-    }
-    _ => None,
-  };
-  (crate_version_string, suffix)
-}
-
-fn indent(spaces: usize) {
-  print!("{}", " ".repeat(spaces));
-}
-
-struct Section(&'static str);
-impl Section {
-  fn display(&self) {
-    println!();
-    println!("{}", self.0.yellow().bold());
-  }
-}
-
-struct VersionBlock {
-  name: String,
-  version: String,
-  target_version: String,
-  indentation: usize,
-}
-
-impl VersionBlock {
-  fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
-    Self {
-      name: name.into(),
-      version: version.into(),
-      target_version: "".into(),
-      indentation: 2,
-    }
-  }
-
-  fn target_version(mut self, version: impl Into<String>) -> Self {
-    self.target_version = version.into();
-    self
-  }
-
-  fn display(&self) {
-    indent(self.indentation);
-    print!("{} ", "›".cyan());
-    print!("{}", self.name.bold());
-    print!(": ");
-    print!(
-      "{}",
-      if self.version.is_empty() {
-        "Not installed!".red().to_string()
-      } else {
-        self.version.clone()
-      }
-    );
-    if !(self.version.is_empty() || self.target_version.is_empty()) {
-      let version = semver::Version::parse(self.version.as_str()).unwrap();
-      let target_version = semver::Version::parse(self.target_version.as_str()).unwrap();
-      if version < target_version {
-        print!(
-          " ({}, latest: {})",
-          "outdated".red(),
-          self.target_version.green()
-        );
-      }
-    }
-    println!();
-  }
-}
-
-struct InfoBlock {
-  key: String,
-  value: String,
-  indentation: usize,
-}
-
-impl InfoBlock {
-  fn new(key: impl Into<String>, val: impl Into<String>) -> Self {
-    Self {
-      key: key.into(),
-      value: val.into(),
-      indentation: 2,
-    }
-  }
-
-  fn display(&self) {
-    indent(self.indentation);
-    print!("{} ", "›".cyan());
-    print!("{}", self.key.bold());
-    print!(": ");
-    print!("{}", self.value.clone());
-    println!();
-  }
-}
-
-pub fn command(_options: Options) -> Result<()> {
-  Section("Environment").display();
-
-  let os_info = os_info::get();
-  VersionBlock::new(
-    "OS",
-    format!(
-      "{} {} {:?}",
-      os_info.os_type(),
-      os_info.version(),
-      os_info.bitness()
-    ),
-  )
-  .display();
-
-  #[cfg(windows)]
-  VersionBlock::new(
-    "Webview2",
-    webview2_version().unwrap_or_default().unwrap_or_default(),
-  )
-  .display();
-
-  #[cfg(windows)]
-  {
-    let build_tools = build_tools_version()
-      .unwrap_or_default()
-      .unwrap_or_default();
-
-    if build_tools.is_empty() {
-      InfoBlock::new("MSVC", "").display();
-    } else {
-      InfoBlock::new("MSVC", "").display();
-      for i in build_tools {
-        indent(6);
-        println!("{} {}", "-".cyan(), i);
-      }
-    }
-  }
-
-  let hook = panic::take_hook();
-  panic::set_hook(Box::new(|_info| {
-    // do nothing
-  }));
-  let app_dir = panic::catch_unwind(crate::helpers::app_paths::app_dir)
-    .map(Some)
-    .unwrap_or_default();
-  panic::set_hook(hook);
-
-  let yarn_version = get_version("yarn", &[])
-    .unwrap_or_default()
-    .unwrap_or_default();
-
-  let metadata = version_metadata()?;
-  VersionBlock::new(
-    "Node.js",
-    get_version("node", &[])
-      .unwrap_or_default()
-      .unwrap_or_default()
-      .chars()
-      .skip(1)
-      .collect::<String>(),
-  )
-  .target_version(metadata.js_cli.node.replace(">= ", ""))
-  .display();
-
-  VersionBlock::new(
-    "npm",
-    get_version("npm", &[])
-      .unwrap_or_default()
-      .unwrap_or_default(),
-  )
-  .display();
-  VersionBlock::new(
-    "pnpm",
-    get_version("pnpm", &[])
-      .unwrap_or_default()
-      .unwrap_or_default(),
-  )
-  .display();
-  VersionBlock::new("yarn", &yarn_version).display();
-  VersionBlock::new(
-    "rustup",
-    get_version("rustup", &[])
-      .unwrap_or_default()
-      .map(|v| {
-        let mut s = v.split(' ');
-        s.next();
-        s.next().unwrap().to_string()
-      })
-      .unwrap_or_default(),
-  )
-  .display();
-  VersionBlock::new(
-    "rustc",
-    get_version("rustc", &[])
-      .unwrap_or_default()
-      .map(|v| {
-        let mut s = v.split(' ');
-        s.next();
-        s.next().unwrap().to_string()
-      })
-      .unwrap_or_default(),
-  )
-  .display();
-  VersionBlock::new(
-    "cargo",
-    get_version("cargo", &[])
-      .unwrap_or_default()
-      .map(|v| {
-        let mut s = v.split(' ');
-        s.next();
-        s.next().unwrap().to_string()
-      })
-      .unwrap_or_default(),
-  )
-  .display();
-  InfoBlock::new(
-    "Rust toolchain",
-    active_rust_toolchain()
-      .unwrap_or_default()
-      .unwrap_or_default(),
-  )
-  .display();
-
-  Section("Packages").display();
-
-  let mut package_manager = PackageManager::Npm;
-  if let Some(app_dir) = &app_dir {
-    let app_dir_entries = read_dir(app_dir)
-      .unwrap()
-      .map(|e| e.unwrap().file_name().to_string_lossy().into_owned())
-      .collect::<Vec<String>>();
-    package_manager = get_package_manager(&app_dir_entries)?;
-  }
-
-  if package_manager == PackageManager::Yarn
-    && yarn_version
-      .chars()
-      .next()
-      .map(|c| c > '1')
-      .unwrap_or_default()
-  {
-    package_manager = PackageManager::Berry;
-  }
-
-  VersionBlock::new(
-    format!("{} {}", "@tauri-apps/cli", "[NPM]".dimmed()),
-    metadata.js_cli.version,
-  )
-  .target_version(
-    npm_latest_version(&package_manager, "@tauri-apps/cli")
-      .unwrap_or_default()
-      .unwrap_or_default(),
-  )
-  .display();
-  if let Some(app_dir) = &app_dir {
-    VersionBlock::new(
-      format!("{} {}", "@tauri-apps/api", "[NPM]".dimmed()),
-      npm_package_version(&package_manager, "@tauri-apps/api", app_dir)
-        .unwrap_or_default()
-        .unwrap_or_default(),
-    )
-    .target_version(
-      npm_latest_version(&package_manager, "@tauri-apps/api")
-        .unwrap_or_default()
-        .unwrap_or_default(),
-    )
-    .display();
-  }
-
-  let hook = panic::take_hook();
-  panic::set_hook(Box::new(|_info| {
-    // do nothing
-  }));
-  let tauri_dir = panic::catch_unwind(crate::helpers::app_paths::tauri_dir)
-    .map(Some)
-    .unwrap_or_default();
-  panic::set_hook(hook);
-
-  if tauri_dir.is_some() || app_dir.is_some() {
-    if let Some(tauri_dir) = tauri_dir.clone() {
-      let manifest: Option<CargoManifest> =
-        if let Ok(manifest_contents) = read_to_string(tauri_dir.join("Cargo.toml")) {
-          toml::from_str(&manifest_contents).ok()
-        } else {
-          None
-        };
-      let lock: Option<CargoLock> = get_workspace_dir()
-        .ok()
-        .and_then(|p| read_to_string(p.join("Cargo.lock")).ok())
-        .and_then(|s| toml::from_str(&s).ok());
-
-      for (dep, label) in [
-        ("tauri", format!("{} {}", "tauri", "[RUST]".dimmed())),
-        (
-          "tauri-build",
-          format!("{} {}", "tauri-build", "[RUST]".dimmed()),
-        ),
-        ("tao", format!("{} {}", "tao", "[RUST]".dimmed())),
-        ("wry", format!("{} {}", "wry", "[RUST]".dimmed())),
-      ] {
-        let (version_string, version_suffix) =
-          crate_version(&tauri_dir, manifest.as_ref(), lock.as_ref(), dep);
-        VersionBlock::new(
-          label,
-          format!(
-            "{},{}",
-            version_string,
-            version_suffix.unwrap_or_else(|| "".into())
-          ),
-        )
-        .display();
-      }
-    }
-  }
-
-  if tauri_dir.is_some() || app_dir.is_some() {
-    Section("App").display();
-    if tauri_dir.is_some() {
-      if let Ok(config) = get_config(None) {
-        let config_guard = config.lock().unwrap();
-        let config = config_guard.as_ref().unwrap();
-        InfoBlock::new(
-          "build-type",
-          if config.tauri.bundle.active {
-            "bundle".to_string()
-          } else {
-            "build".to_string()
-          },
-        )
-        .display();
-        InfoBlock::new(
-          "CSP",
-          config
-            .tauri
-            .security
-            .csp
-            .clone()
-            .map(|c| c.to_string())
-            .unwrap_or_else(|| "unset".to_string()),
-        )
-        .display();
-        InfoBlock::new("distDir", config.build.dist_dir.to_string()).display();
-        InfoBlock::new("devPath", config.build.dev_path.to_string()).display();
-      }
-    }
-
-    if let Some(app_dir) = app_dir {
-      if let Ok(package_json) = read_to_string(app_dir.join("package.json")) {
-        let (framework, bundler) = infer_framework(&package_json);
-        if let Some(framework) = framework {
-          InfoBlock::new("framework", framework.to_string()).display();
-        }
-        if let Some(bundler) = bundler {
-          InfoBlock::new("bundler", bundler.to_string()).display();
-        }
-      } else {
-        println!("package.json not found");
-      }
-    }
-  }
-
-  if let Some(app_dir) = app_dir {
-    Section("App directory structure").display();
-    let dirs = read_dir(app_dir)?
-      .filter(|p| p.is_ok() && p.as_ref().unwrap().path().is_dir())
-      .collect::<Vec<Result<std::fs::DirEntry, _>>>();
-    let dirs_len = dirs.len();
-    for (i, entry) in dirs.into_iter().enumerate() {
-      let entry = entry?;
-      let prefix = if i + 1 == dirs_len {
-        "└─".cyan()
-      } else {
-        "├─".cyan()
-      };
-      println!(
-        "  {} {}",
-        prefix,
-        entry.path().file_name().unwrap().to_string_lossy()
-      );
-    }
-  }
-
-  Ok(())
-}
-
-fn get_package_manager<T: AsRef<str>>(app_dir_entries: &[T]) -> crate::Result<PackageManager> {
-  let mut use_npm = false;
-  let mut use_pnpm = false;
-  let mut use_yarn = false;
-
-  for name in app_dir_entries {
-    if name.as_ref() == "package-lock.json" {
-      use_npm = true;
-    } else if name.as_ref() == "pnpm-lock.yaml" {
-      use_pnpm = true;
-    } else if name.as_ref() == "yarn.lock" {
-      use_yarn = true;
-    }
-  }
-
-  if !use_npm && !use_pnpm && !use_yarn {
-    println!("WARNING: no lock files found, defaulting to npm");
-    return Ok(PackageManager::Npm);
-  }
-
-  let mut found = Vec::new();
-
-  if use_npm {
-    found.push("npm");
-  }
-  if use_pnpm {
-    found.push("pnpm");
-  }
-  if use_yarn {
-    found.push("yarn");
-  }
-
-  if found.len() > 1 {
-    return Err(anyhow::anyhow!(
-        "only one package manager should be used, but found {}\nplease remove unused package manager lock files",
-        found.join(" and ")
-      ));
-  }
-
-  if use_npm {
-    Ok(PackageManager::Npm)
-  } else if use_pnpm {
-    Ok(PackageManager::Pnpm)
-  } else {
-    Ok(PackageManager::Yarn)
-  }
-}

+ 77 - 0
tooling/cli/src/info/app.rs

@@ -0,0 +1,77 @@
+// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+use super::{SectionItem, Status};
+use crate::helpers::framework;
+use std::{fs::read_to_string, path::PathBuf};
+
+pub fn items(app_dir: Option<&PathBuf>, tauri_dir: Option<PathBuf>) -> Vec<SectionItem> {
+  let mut items = Vec::new();
+  if tauri_dir.is_some() {
+    if let Ok(config) = crate::helpers::config::get(None) {
+      let config_guard = config.lock().unwrap();
+      let config = config_guard.as_ref().unwrap();
+
+      let bundle_or_build = if config.tauri.bundle.active {
+        "bundle".to_string()
+      } else {
+        "build".to_string()
+      };
+      items.push(SectionItem::new(
+        move || Some((format!("build-type: {bundle_or_build}"), Status::Neutral)),
+        || None,
+        false,
+      ));
+
+      let csp = config
+        .tauri
+        .security
+        .csp
+        .clone()
+        .map(|c| c.to_string())
+        .unwrap_or_else(|| "unset".to_string());
+      items.push(SectionItem::new(
+        move || Some((format!("CSP: {csp}"), Status::Neutral)),
+        || None,
+        false,
+      ));
+
+      let dist_dir = config.build.dist_dir.to_string();
+      items.push(SectionItem::new(
+        move || Some((format!("distDir: {dist_dir}"), Status::Neutral)),
+        || None,
+        false,
+      ));
+
+      let dev_path = config.build.dev_path.to_string();
+      items.push(SectionItem::new(
+        move || Some((format!("devPath: {dev_path}"), Status::Neutral)),
+        || None,
+        false,
+      ));
+
+      if let Some(app_dir) = app_dir {
+        if let Ok(package_json) = read_to_string(app_dir.join("package.json")) {
+          let (framework, bundler) = framework::infer_from_package_json(&package_json);
+          if let Some(framework) = framework {
+            items.push(SectionItem::new(
+              move || Some((format!("framework: {framework}"), Status::Neutral)),
+              || None,
+              false,
+            ));
+          }
+          if let Some(bundler) = bundler {
+            items.push(SectionItem::new(
+              move || Some((format!("bundler: {bundler}"), Status::Neutral)),
+              || None,
+              false,
+            ));
+          }
+        }
+      }
+    }
+  }
+
+  items
+}

+ 125 - 0
tooling/cli/src/info/env_nodejs.rs

@@ -0,0 +1,125 @@
+// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+use super::{cross_command, VersionMetadata};
+use super::{SectionItem, Status};
+use colored::Colorize;
+
+pub fn items(metadata: &VersionMetadata) -> (Vec<SectionItem>, Option<String>) {
+  let yarn_version = cross_command("yarn")
+    .arg("-v")
+    .output()
+    .map(|o| {
+      if o.status.success() {
+        let v = String::from_utf8_lossy(o.stdout.as_slice()).to_string();
+        Some(v.split('\n').next().unwrap().to_string())
+      } else {
+        None
+      }
+    })
+    .ok()
+    .unwrap_or_default();
+  let yarn_version_c = yarn_version.clone();
+  let node_target_ver = metadata.js_cli.node.replace(">= ", "");
+
+  (
+    vec![
+      SectionItem::new(
+        move || {
+          cross_command("node")
+            .arg("-v")
+            .output()
+            .map(|o| {
+              if o.status.success() {
+                let v = String::from_utf8_lossy(o.stdout.as_slice()).to_string();
+                let v = v
+                  .split('\n')
+                  .next()
+                  .unwrap()
+                  .strip_prefix('v')
+                  .unwrap_or_default()
+                  .trim();
+                Some((
+                  format!("node: {}{}", v, {
+                    let version = semver::Version::parse(v).unwrap();
+                    let target_version = semver::Version::parse(node_target_ver.as_str()).unwrap();
+                    if version < target_version {
+                      format!(
+                        " ({}, latest: {})",
+                        "outdated".red(),
+                        target_version.to_string().green()
+                      )
+                    } else {
+                      "".into()
+                    }
+                  }),
+                  Status::Neutral,
+                ))
+              } else {
+                None
+              }
+            })
+            .ok()
+            .unwrap_or_default()
+        },
+        || None,
+        false,
+      ),
+      SectionItem::new(
+        || {
+          cross_command("pnpm")
+            .arg("-v")
+            .output()
+            .map(|o| {
+              if o.status.success() {
+                let v = String::from_utf8_lossy(o.stdout.as_slice()).to_string();
+                Some((
+                  format!("pnpm: {}", v.split('\n').next().unwrap()),
+                  Status::Neutral,
+                ))
+              } else {
+                None
+              }
+            })
+            .ok()
+            .unwrap_or_default()
+        },
+        || None,
+        false,
+      ),
+      SectionItem::new(
+        move || {
+          yarn_version_c
+            .as_ref()
+            .map(|v| (format!("yarn: {v}"), Status::Neutral))
+        },
+        || None,
+        false,
+      ),
+      SectionItem::new(
+        || {
+          cross_command("npm")
+            .arg("-v")
+            .output()
+            .map(|o| {
+              if o.status.success() {
+                let v = String::from_utf8_lossy(o.stdout.as_slice()).to_string();
+                Some((
+                  format!("npm: {}", v.split('\n').next().unwrap()),
+                  Status::Neutral,
+                ))
+              } else {
+                None
+              }
+            })
+            .ok()
+            .unwrap_or_default()
+        },
+        || None,
+        false,
+      ),
+    ],
+    yarn_version,
+  )
+}

+ 145 - 0
tooling/cli/src/info/env_rust.rs

@@ -0,0 +1,145 @@
+// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+use super::SectionItem;
+use super::Status;
+use colored::Colorize;
+use std::process::Command;
+
+pub fn items() -> Vec<SectionItem> {
+  vec![
+    SectionItem::new(
+      || {
+        Some(
+          Command::new("rustc")
+            .arg("-V")
+            .output()
+            .map(|o| String::from_utf8_lossy(o.stdout.as_slice()).to_string())
+            .map(|v| {
+              format!(
+                "rustc: {}",
+                v.split('\n')
+                  .next()
+                  .unwrap()
+                  .strip_prefix("rustc ")
+                  .unwrap_or_default()
+              )
+            })
+            .map(|desc| (desc, Status::Success))
+            .ok()
+            .unwrap_or_else(|| {
+              (
+                format!(
+                  "rustc: {}\nmaybe you don't have rust installed! Visist {}",
+                  "not installed!".red(),
+                  "https://rustup.rs/".cyan()
+                ),
+                Status::Error,
+              )
+            }),
+        )
+      },
+      || None,
+      false,
+    ),
+    SectionItem::new(
+      || {
+        Some(
+          Command::new("cargo")
+            .arg("-V")
+            .output()
+            .map(|o| String::from_utf8_lossy(o.stdout.as_slice()).to_string())
+            .map(|v| {
+              format!(
+                "Cargo: {}",
+                v.split('\n')
+                  .next()
+                  .unwrap()
+                  .strip_prefix("cargo ")
+                  .unwrap_or_default()
+              )
+            })
+            .map(|desc| (desc, Status::Success))
+            .ok()
+            .unwrap_or_else(|| {
+              (
+                format!(
+                  "Cargo: {}\nmaybe you don't have rust installed! Visit {}",
+                  "not installed!".red(),
+                  "https://rustup.rs/".cyan()
+                ),
+                Status::Error,
+              )
+            }),
+        )
+      },
+      || None,
+      false,
+    ),
+    SectionItem::new(
+      || {
+        Some(
+          Command::new("rustup")
+            .arg("-V")
+            .output()
+            .map(|o| String::from_utf8_lossy(o.stdout.as_slice()).to_string())
+            .map(|v| {
+              format!(
+                "rustup: {}",
+                v.split('\n')
+                  .next()
+                  .unwrap()
+                  .strip_prefix("rustup ")
+                  .unwrap_or_default()
+              )
+            })
+            .map(|desc| (desc, Status::Success))
+            .ok()
+            .unwrap_or_else(|| {
+              (
+                format!(
+                  "rustup: {}\nIf you have rust installed some other way, we recommend uninstalling it\nthen use rustup instead. Visit {}",
+                  "not installed!".red(),
+                  "https://rustup.rs/".cyan()
+                ),
+                Status::Warning,
+              )
+            }),
+        )
+      },
+      || None,
+      false,
+    ),
+    SectionItem::new(
+      || {
+        Some(
+          Command::new("rustup")
+            .args(["show", "active-toolchain"])
+            .output()
+            .map(|o| String::from_utf8_lossy(o.stdout.as_slice()).to_string())
+            .map(|v| {
+              format!(
+                "Rust toolchain: {}",
+                v.split('\n')
+                  .next()
+                  .unwrap()
+              )
+            })
+            .map(|desc| (desc, Status::Success))
+            .ok()
+            .unwrap_or_else(|| {
+              (
+                format!(
+                  "Rust toolchain: couldn't be deteceted!\nmaybe you don't have rustup installed? if so, Visit {}", "https://rustup.rs/".cyan()
+                ),
+                Status::Warning,
+              )
+            }),
+        )
+      },
+      || None,
+      false,
+    ),
+  ]
+}

+ 299 - 0
tooling/cli/src/info/env_system.rs

@@ -0,0 +1,299 @@
+// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+use super::SectionItem;
+use super::Status;
+use colored::Colorize;
+#[cfg(windows)]
+use serde::Deserialize;
+use std::process::Command;
+
+#[cfg(windows)]
+#[derive(Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+struct VsInstanceInfo {
+  display_name: String,
+}
+
+#[cfg(windows)]
+const VSWHERE: &[u8] = include_bytes!("../../scripts/vswhere.exe");
+
+#[cfg(windows)]
+fn build_tools_version() -> crate::Result<Option<Vec<String>>> {
+  let mut vswhere = std::env::temp_dir();
+  vswhere.push("vswhere.exe");
+
+  if !vswhere.exists() {
+    if let Ok(mut file) = std::fs::File::create(&vswhere) {
+      use std::io::Write;
+      let _ = file.write_all(VSWHERE);
+    }
+  }
+  let output = Command::new(vswhere)
+    .args([
+      "-prerelease",
+      "-products",
+      "*",
+      "-requiresAny",
+      "-requires",
+      "Microsoft.VisualStudio.Workload.NativeDesktop",
+      "-requires",
+      "Microsoft.VisualStudio.Workload.VCTools",
+      "-format",
+      "json",
+    ])
+    .output()?;
+  Ok(if output.status.success() {
+    let stdout = String::from_utf8_lossy(&output.stdout);
+    let instances: Vec<VsInstanceInfo> = serde_json::from_str(&stdout)?;
+    Some(
+      instances
+        .iter()
+        .map(|i| i.display_name.clone())
+        .collect::<Vec<String>>(),
+    )
+  } else {
+    None
+  })
+}
+
+#[cfg(windows)]
+fn webview2_version() -> crate::Result<Option<String>> {
+  let powershell_path = std::env::var("SYSTEMROOT").map_or_else(
+    |_| "powershell.exe".to_string(),
+    |p| format!("{p}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"),
+  );
+  // check 64bit machine-wide installation
+  let output = Command::new(&powershell_path)
+      .args(["-NoProfile", "-Command"])
+      .arg("Get-ItemProperty -Path 'HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}' | ForEach-Object {$_.pv}")
+      .output()?;
+  if output.status.success() {
+    return Ok(Some(
+      String::from_utf8_lossy(&output.stdout).replace('\n', ""),
+    ));
+  }
+  // check 32bit machine-wide installation
+  let output = Command::new(&powershell_path)
+        .args(["-NoProfile", "-Command"])
+        .arg("Get-ItemProperty -Path 'HKLM:\\SOFTWARE\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}' | ForEach-Object {$_.pv}")
+        .output()?;
+  if output.status.success() {
+    return Ok(Some(
+      String::from_utf8_lossy(&output.stdout).replace('\n', ""),
+    ));
+  }
+  // check user-wide installation
+  let output = Command::new(&powershell_path)
+      .args(["-NoProfile", "-Command"])
+      .arg("Get-ItemProperty -Path 'HKCU:\\SOFTWARE\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}' | ForEach-Object {$_.pv}")
+      .output()?;
+  if output.status.success() {
+    return Ok(Some(
+      String::from_utf8_lossy(&output.stdout).replace('\n', ""),
+    ));
+  }
+
+  Ok(None)
+}
+
+#[cfg(any(
+  target_os = "linux",
+  target_os = "dragonfly",
+  target_os = "freebsd",
+  target_os = "openbsd",
+  target_os = "netbsd"
+))]
+fn pkg_conf_version(package: &str) -> Option<String> {
+  Command::new("pkg-config")
+    .args([package, "--print-provides"])
+    .output()
+    .map(|o| {
+      String::from_utf8_lossy(&o.stdout)
+        .split('=')
+        .nth(1)
+        .map(|s| s.trim().to_string())
+    })
+    .unwrap_or(None)
+}
+#[cfg(any(
+  target_os = "linux",
+  target_os = "dragonfly",
+  target_os = "freebsd",
+  target_os = "openbsd",
+  target_os = "netbsd"
+))]
+fn webkit2gtk_ver() -> Option<String> {
+  pkg_conf_version("webkit2gtk-4.0")
+}
+#[cfg(any(
+  target_os = "linux",
+  target_os = "dragonfly",
+  target_os = "freebsd",
+  target_os = "openbsd",
+  target_os = "netbsd"
+))]
+fn rsvg2_ver() -> Option<String> {
+  pkg_conf_version("librsvg-2.0")
+}
+
+#[cfg(target_os = "macos")]
+fn is_xcode_command_line_tools_installed() -> bool {
+  Command::new("xcode-select")
+    .arg("-p")
+    .output()
+    .map(|o| o.status.success())
+    .unwrap_or(false)
+}
+
+pub fn items() -> Vec<SectionItem> {
+  vec![
+    SectionItem::new(
+      || {
+        let os_info = os_info::get();
+        Some((
+          format!(
+            "OS: {} {} {:?}",
+            os_info.os_type(),
+            os_info.version(),
+            os_info.bitness()
+          ),
+          Status::Neutral,
+        ))
+      },
+      || None,
+      false,
+    ),
+    #[cfg(windows)]
+    SectionItem::new(
+      || {
+        let error = || {
+          format!(
+            "Webview2: {}\nVisit {}",
+            "not installed!".red(),
+            "https://developer.microsoft.com/en-us/microsoft-edge/webview2/".cyan()
+          )
+        };
+        Some(
+          webview2_version()
+            .map(|v| {
+              v.map(|v| (format!("WebView2: {}", v), Status::Success))
+                .unwrap_or_else(|| (error(), Status::Error))
+            })
+            .unwrap_or_else(|_| (error(), Status::Error)),
+        )
+      },
+      || None,
+      false,
+    ),
+    #[cfg(windows)]
+    SectionItem::new(
+      || {
+        let build_tools = build_tools_version()
+          .unwrap_or_default()
+          .unwrap_or_default();
+        if build_tools.is_empty() {
+          Some((
+            format!(
+              "Couldn't detect Visual Studio or Visual Studio Build Tools. Download from {}",
+              "https://aka.ms/vs/17/release/vs_BuildTools.exe".cyan()
+            ),
+            Status::Error,
+          ))
+        } else {
+          Some((
+            format!(
+              "MSVC: {}{}",
+              if build_tools.len() > 1 {
+                format!("\n  {} ", "-".cyan())
+              } else {
+                "".into()
+              },
+              build_tools.join(format!("\n  {} ", "-".cyan()).as_str()),
+            ),
+            Status::Success,
+          ))
+        }
+      },
+      || None,
+      false,
+    ),
+    #[cfg(any(
+      target_os = "linux",
+      target_os = "dragonfly",
+      target_os = "freebsd",
+      target_os = "openbsd",
+      target_os = "netbsd"
+    ))]
+    SectionItem::new(
+      || {
+        Some(
+          webkit2gtk_ver()
+            .map(|v| (format!("webkit2gtk-4.0: {v}"), Status::Success))
+            .unwrap_or_else(|| {
+              (
+                format!(
+                  "webkit2gtk-4.0: {}\nVisit {} to learn more about tauri prerequisites",
+                  "not installed".red(),
+                  "https://tauri.app/v1/guides/getting-started/prerequisites".cyan()
+                ),
+                Status::Error,
+              )
+            }),
+        )
+      },
+      || None,
+      false,
+    ),
+    #[cfg(any(
+      target_os = "linux",
+      target_os = "dragonfly",
+      target_os = "freebsd",
+      target_os = "openbsd",
+      target_os = "netbsd"
+    ))]
+    SectionItem::new(
+      || {
+        Some(
+          rsvg2_ver()
+            .map(|v| (format!("rsvg2: {v}"), Status::Success))
+            .unwrap_or_else(|| {
+              (
+                format!(
+                  "rsvg2: {}\nVisit {} to learn more about tauri prerequisites",
+                  "not installed".red(),
+                  "https://tauri.app/v1/guides/getting-started/prerequisites".cyan()
+                ),
+                Status::Error,
+              )
+            }),
+        )
+      },
+      || None,
+      false,
+    ),
+    #[cfg(target_os = "macos")]
+    SectionItem::new(
+      || {
+        Some(if is_xcode_command_line_tools_installed() {
+          (
+            "Xcode Command Line Tools: installed".into(),
+            Status::Success,
+          )
+        } else {
+          (
+            format!(
+              "Xcode Command Line Tools: {}\n Run `{}`",
+              "not installed!".red(),
+              "xcode-select --install".cyan()
+            ),
+            Status::Error,
+          )
+        })
+      },
+      || None,
+      false,
+    ),
+  ]
+}

+ 268 - 0
tooling/cli/src/info/mod.rs

@@ -0,0 +1,268 @@
+// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+use crate::Result;
+use clap::Parser;
+use colored::Colorize;
+use dialoguer::{theme::ColorfulTheme, Confirm};
+use serde::Deserialize;
+use std::{
+  fmt::{self, Display, Formatter},
+  panic,
+  process::Command,
+};
+
+mod app;
+mod env_nodejs;
+mod env_rust;
+mod env_system;
+mod packages_nodejs;
+mod packages_rust;
+
+#[derive(Deserialize)]
+struct JsCliVersionMetadata {
+  version: String,
+  node: String,
+}
+
+#[derive(Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct VersionMetadata {
+  #[serde(rename = "cli.js")]
+  js_cli: JsCliVersionMetadata,
+}
+
+fn version_metadata() -> Result<VersionMetadata> {
+  serde_json::from_str::<VersionMetadata>(include_str!("../../metadata.json")).map_err(Into::into)
+}
+
+#[cfg(not(debug_assertions))]
+pub(crate) fn cli_current_version() -> Result<String> {
+  version_metadata().map(|meta| meta.js_cli.version)
+}
+
+#[cfg(not(debug_assertions))]
+pub(crate) fn cli_upstream_version() -> Result<String> {
+  let upstream_metadata = match ureq::get(
+    "https://raw.githubusercontent.com/tauri-apps/tauri/dev/tooling/cli/metadata.json",
+  )
+  .timeout(std::time::Duration::from_secs(3))
+  .call()
+  {
+    Ok(r) => r,
+    Err(ureq::Error::Status(code, _response)) => {
+      let message = format!("Unable to find updates at the moment. Code: {}", code);
+      return Err(anyhow::Error::msg(message));
+    }
+    Err(ureq::Error::Transport(transport)) => {
+      let message = format!(
+        "Unable to find updates at the moment. Error: {:?}",
+        transport.kind()
+      );
+      return Err(anyhow::Error::msg(message));
+    }
+  };
+
+  upstream_metadata
+    .into_string()
+    .and_then(|meta_str| Ok(serde_json::from_str::<VersionMetadata>(&meta_str)))
+    .and_then(|json| Ok(json.unwrap().js_cli.version))
+    .map_err(|e| anyhow::Error::new(e))
+}
+
+pub fn cross_command(bin: &str) -> Command {
+  #[cfg(target_os = "windows")]
+  let cmd = {
+    let mut cmd = Command::new("cmd");
+    cmd.arg("/c").arg(bin);
+    cmd
+  };
+  #[cfg(not(target_os = "windows"))]
+  let cmd = Command::new(bin);
+  cmd
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Default)]
+pub enum Status {
+  Neutral = 0,
+  #[default]
+  Success,
+  Warning,
+  Error,
+}
+
+impl Display for Status {
+  fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+    write!(
+      f,
+      "{}",
+      match self {
+        Status::Neutral => "-".cyan(),
+        Status::Success => "✔".green(),
+        Status::Warning => "⚠".yellow(),
+        Status::Error => "✘".red(),
+      }
+    )
+  }
+}
+
+pub struct SectionItem {
+  /// If description is none, the item is skipped
+  description: Option<String>,
+  status: Status,
+  /// This closure return will be assigned to status and description
+  action: Box<dyn FnMut() -> Option<(String, Status)>>,
+  /// This closure return will be assigned to status and description
+  action_if_err: Box<dyn FnMut() -> Option<(String, Status)>>,
+  has_action_if_err: bool,
+}
+
+impl Display for SectionItem {
+  fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+    let desc = self
+      .description
+      .as_ref()
+      .map(|s| s.replace('\n', "\n      "))
+      .unwrap_or_default();
+    let (first, second) = desc.split_once(':').unwrap();
+    write!(f, "{} {}:{}", self.status, first.bold(), second)
+  }
+}
+
+impl SectionItem {
+  fn new<
+    F1: FnMut() -> Option<(String, Status)> + 'static,
+    F2: FnMut() -> Option<(String, Status)> + 'static,
+  >(
+    action: F1,
+    action_if_err: F2,
+    has_action_if_err: bool,
+  ) -> Self {
+    Self {
+      action: Box::new(action),
+      action_if_err: Box::new(action_if_err),
+      has_action_if_err,
+      description: None,
+      status: Status::Neutral,
+    }
+  }
+  fn run(&mut self, interactive: bool) -> Status {
+    if let Some(ret) = (self.action)() {
+      self.description = Some(ret.0);
+      self.status = ret.1;
+    }
+
+    if self.status == Status::Error && interactive && self.has_action_if_err {
+      if let Some(description) = &self.description {
+        let confirmed = Confirm::with_theme(&ColorfulTheme::default())
+          .with_prompt(format!(
+            "{}\n  Run the automatic fix?",
+            description.replace('\n', "\n  ")
+          ))
+          .interact()
+          .unwrap_or(false);
+        if confirmed {
+          if let Some(ret) = (self.action_if_err)() {
+            self.description = Some(ret.0);
+            self.status = ret.1;
+          }
+        }
+      }
+    }
+    self.status
+  }
+}
+
+struct Section<'a> {
+  label: &'a str,
+  interactive: bool,
+  items: Vec<SectionItem>,
+}
+
+impl Section<'_> {
+  fn display(&mut self) {
+    let mut status = Status::Neutral;
+
+    for item in &mut self.items {
+      let s = item.run(self.interactive);
+      if s > status {
+        status = s;
+      }
+    }
+
+    let status_str = format!("[{status}]");
+    let status = match status {
+      Status::Neutral => status_str.normal(),
+      Status::Success => status_str.green(),
+      Status::Warning => status_str.yellow(),
+      Status::Error => status_str.red(),
+    };
+
+    println!();
+    println!("{} {}", status, self.label.bold().yellow());
+    for item in &self.items {
+      if item.description.is_some() {
+        println!("    {item}");
+      }
+    }
+  }
+}
+
+#[derive(Debug, Parser)]
+#[clap(about = "Shows information about Tauri dependencies and project configuration")]
+pub struct Options {
+  /// Interactive mode to apply automatic fixes.
+  #[clap(long)]
+  pub interactive: bool,
+}
+
+pub fn command(options: Options) -> Result<()> {
+  let Options { interactive } = options;
+  let hook = panic::take_hook();
+  panic::set_hook(Box::new(|_info| {
+    // do nothing
+  }));
+  let app_dir = panic::catch_unwind(crate::helpers::app_paths::app_dir)
+    .map(Some)
+    .unwrap_or_default();
+  let tauri_dir = panic::catch_unwind(crate::helpers::app_paths::tauri_dir)
+    .map(Some)
+    .unwrap_or_default();
+  panic::set_hook(hook);
+  let metadata = version_metadata()?;
+
+  let mut environment = Section {
+    label: "Environment",
+    interactive,
+    items: Vec::new(),
+  };
+  environment.items.extend(env_system::items());
+  environment.items.extend(env_rust::items());
+  let (items, yarn_version) = env_nodejs::items(&metadata);
+  environment.items.extend(items);
+
+  let mut packages = Section {
+    label: "Packages",
+    interactive,
+    items: Vec::new(),
+  };
+  packages
+    .items
+    .extend(packages_rust::items(app_dir, tauri_dir.clone()));
+  packages
+    .items
+    .extend(packages_nodejs::items(app_dir, &metadata, yarn_version));
+
+  let mut app = Section {
+    label: "App",
+    interactive,
+    items: Vec::new(),
+  };
+  app.items.extend(app::items(app_dir, tauri_dir));
+
+  environment.display();
+  packages.display();
+  app.display();
+  Ok(())
+}

+ 287 - 0
tooling/cli/src/info/packages_nodejs.rs

@@ -0,0 +1,287 @@
+// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+use super::{cross_command, VersionMetadata};
+use super::{SectionItem, Status};
+use colored::Colorize;
+use serde::Deserialize;
+use std::fmt::Display;
+use std::path::{Path, PathBuf};
+
+#[derive(Deserialize)]
+struct YarnVersionInfo {
+  data: Vec<String>,
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+enum PackageManager {
+  Npm,
+  Pnpm,
+  Yarn,
+  YarnBerry,
+}
+
+impl Display for PackageManager {
+  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+    write!(
+      f,
+      "{}",
+      match self {
+        PackageManager::Npm => "npm",
+        PackageManager::Pnpm => "pnpm",
+        PackageManager::Yarn => "yarn",
+        PackageManager::YarnBerry => "yarn berry",
+      }
+    )
+  }
+}
+
+fn npm_latest_version(pm: &PackageManager, name: &str) -> crate::Result<Option<String>> {
+  match pm {
+    PackageManager::Yarn => {
+      let mut cmd = cross_command("yarn");
+
+      let output = cmd
+        .arg("info")
+        .arg(name)
+        .args(["version", "--json"])
+        .output()?;
+      if output.status.success() {
+        let stdout = String::from_utf8_lossy(&output.stdout);
+        let info: YarnVersionInfo = serde_json::from_str(&stdout)?;
+        Ok(Some(info.data.last().unwrap().to_string()))
+      } else {
+        Ok(None)
+      }
+    }
+    PackageManager::YarnBerry => {
+      let mut cmd = cross_command("yarn");
+
+      let output = cmd
+        .arg("npm")
+        .arg("info")
+        .arg(name)
+        .args(["--fields", "version", "--json"])
+        .output()?;
+      if output.status.success() {
+        let info: crate::PackageJson =
+          serde_json::from_reader(std::io::Cursor::new(output.stdout)).unwrap();
+        Ok(info.version)
+      } else {
+        Ok(None)
+      }
+    }
+    PackageManager::Npm => {
+      let mut cmd = cross_command("npm");
+
+      let output = cmd.arg("show").arg(name).arg("version").output()?;
+      if output.status.success() {
+        let stdout = String::from_utf8_lossy(&output.stdout);
+        Ok(Some(stdout.replace('\n', "")))
+      } else {
+        Ok(None)
+      }
+    }
+    PackageManager::Pnpm => {
+      let mut cmd = cross_command("pnpm");
+
+      let output = cmd.arg("info").arg(name).arg("version").output()?;
+      if output.status.success() {
+        let stdout = String::from_utf8_lossy(&output.stdout);
+        Ok(Some(stdout.replace('\n', "")))
+      } else {
+        Ok(None)
+      }
+    }
+  }
+}
+
+fn npm_package_version<P: AsRef<Path>>(
+  pm: &PackageManager,
+  name: &str,
+  app_dir: P,
+) -> crate::Result<Option<String>> {
+  let (output, regex) = match pm {
+    PackageManager::Yarn => (
+      cross_command("yarn")
+        .args(["list", "--pattern"])
+        .arg(name)
+        .args(["--depth", "0"])
+        .current_dir(app_dir)
+        .output()?,
+      None,
+    ),
+    PackageManager::YarnBerry => (
+      cross_command("yarn")
+        .arg("info")
+        .arg(name)
+        .arg("--json")
+        .current_dir(app_dir)
+        .output()?,
+      Some(regex::Regex::new("\"Version\":\"([\\da-zA-Z\\-\\.]+)\"").unwrap()),
+    ),
+    PackageManager::Npm => (
+      cross_command("npm")
+        .arg("list")
+        .arg(name)
+        .args(["version", "--depth", "0"])
+        .current_dir(app_dir)
+        .output()?,
+      None,
+    ),
+    PackageManager::Pnpm => (
+      cross_command("pnpm")
+        .arg("list")
+        .arg(name)
+        .args(["--parseable", "--depth", "0"])
+        .current_dir(app_dir)
+        .output()?,
+      None,
+    ),
+  };
+  if output.status.success() {
+    let stdout = String::from_utf8_lossy(&output.stdout);
+    let regex = regex.unwrap_or_else(|| regex::Regex::new("@(\\d[\\da-zA-Z\\-\\.]+)").unwrap());
+    Ok(
+      regex
+        .captures_iter(&stdout)
+        .last()
+        .and_then(|cap| cap.get(1).map(|v| v.as_str().to_string())),
+    )
+  } else {
+    Ok(None)
+  }
+}
+
+fn get_package_manager<T: AsRef<str>>(app_dir_entries: &[T]) -> PackageManager {
+  let mut use_npm = false;
+  let mut use_pnpm = false;
+  let mut use_yarn = false;
+
+  for name in app_dir_entries {
+    if name.as_ref() == "package-lock.json" {
+      use_npm = true;
+    } else if name.as_ref() == "pnpm-lock.yaml" {
+      use_pnpm = true;
+    } else if name.as_ref() == "yarn.lock" {
+      use_yarn = true;
+    }
+  }
+
+  if !use_npm && !use_pnpm && !use_yarn {
+    println!(
+      "{}: no lock files found, defaulting to npm",
+      "WARNING".yellow()
+    );
+    return PackageManager::Npm;
+  }
+
+  let mut found = Vec::new();
+
+  if use_npm {
+    found.push(PackageManager::Npm);
+  }
+  if use_pnpm {
+    found.push(PackageManager::Pnpm);
+  }
+  if use_yarn {
+    found.push(PackageManager::Yarn);
+  }
+
+  if found.len() > 1 {
+    let pkg_manger = found[0];
+    println!(
+      "{}: Only one package manager should be used, but found {}.\n         Please remove unused package manager lock files, will use {} for now!",
+      "WARNING".yellow(),
+      found.iter().map(ToString::to_string).collect::<Vec<_>>().join(" and "),
+      pkg_manger
+    );
+    return pkg_manger;
+  }
+
+  if use_npm {
+    PackageManager::Npm
+  } else if use_pnpm {
+    PackageManager::Pnpm
+  } else {
+    PackageManager::Yarn
+  }
+}
+
+pub fn items(
+  app_dir: Option<&PathBuf>,
+  metadata: &VersionMetadata,
+  yarn_version: Option<String>,
+) -> Vec<SectionItem> {
+  let mut package_manager = PackageManager::Npm;
+  if let Some(app_dir) = &app_dir {
+    let app_dir_entries = std::fs::read_dir(app_dir)
+      .unwrap()
+      .map(|e| e.unwrap().file_name().to_string_lossy().into_owned())
+      .collect::<Vec<String>>();
+    package_manager = get_package_manager(&app_dir_entries);
+  }
+
+  if package_manager == PackageManager::Yarn
+    && yarn_version
+      .map(|v| v.chars().next().map(|c| c > '1').unwrap_or_default())
+      .unwrap_or(false)
+  {
+    package_manager = PackageManager::YarnBerry;
+  }
+
+  let mut items = Vec::new();
+  if let Some(app_dir) = app_dir {
+    for (package, version) in [
+      ("@tauri-apps/api", None),
+      ("@tauri-apps/cli", Some(metadata.js_cli.version.clone())),
+    ] {
+      let app_dir = app_dir.clone();
+      let item = SectionItem::new(
+        move || {
+          let version = version.clone().unwrap_or_else(|| {
+            npm_package_version(&package_manager, package, &app_dir)
+              .unwrap_or_default()
+              .unwrap_or_default()
+          });
+          let latest_ver = npm_latest_version(&package_manager, package)
+            .unwrap_or_default()
+            .unwrap_or_default();
+
+          Some((
+            if version.is_empty() {
+              format!("{} {}: not installed!", package, "[NPM]".dimmed())
+            } else {
+              format!(
+                "{} {}: {}{}",
+                package,
+                "[NPM]".dimmed(),
+                version,
+                if !(version.is_empty() || latest_ver.is_empty()) {
+                  let version = semver::Version::parse(version.as_str()).unwrap();
+                  let target_version = semver::Version::parse(latest_ver.as_str()).unwrap();
+
+                  if version < target_version {
+                    format!(" ({}, latest: {})", "outdated".yellow(), latest_ver.green())
+                  } else {
+                    "".into()
+                  }
+                } else {
+                  "".into()
+                }
+              )
+            },
+            Status::Neutral,
+          ))
+        },
+        || None,
+        false,
+      );
+
+      items.push(item);
+    }
+  }
+
+  items
+}

+ 240 - 0
tooling/cli/src/info/packages_rust.rs

@@ -0,0 +1,240 @@
+// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+use super::{SectionItem, Status};
+use crate::interface::rust::get_workspace_dir;
+use colored::Colorize;
+use serde::Deserialize;
+use std::collections::HashMap;
+use std::fmt::Write;
+use std::fs::read_to_string;
+use std::path::{Path, PathBuf};
+
+#[derive(Clone, Deserialize)]
+struct CargoLockPackage {
+  name: String,
+  version: String,
+  source: Option<String>,
+}
+
+#[derive(Deserialize)]
+struct CargoLock {
+  package: Vec<CargoLockPackage>,
+}
+
+#[derive(Clone, Deserialize)]
+struct CargoManifestDependencyPackage {
+  version: Option<String>,
+  git: Option<String>,
+  branch: Option<String>,
+  rev: Option<String>,
+  path: Option<PathBuf>,
+}
+
+#[derive(Clone, Deserialize)]
+#[serde(untagged)]
+enum CargoManifestDependency {
+  Version(String),
+  Package(CargoManifestDependencyPackage),
+}
+
+#[derive(Deserialize)]
+struct CargoManifestPackage {
+  version: String,
+}
+
+#[derive(Deserialize)]
+struct CargoManifest {
+  package: CargoManifestPackage,
+  dependencies: HashMap<String, CargoManifestDependency>,
+}
+
+fn crate_latest_version(name: &str) -> Option<String> {
+  let url = format!("https://docs.rs/crate/{name}/");
+  match ureq::get(&url).call() {
+    Ok(response) => match (response.status(), response.header("location")) {
+      (302, Some(location)) => Some(location.replace(&url, "")),
+      _ => None,
+    },
+    Err(_) => None,
+  }
+}
+
+fn crate_version(
+  tauri_dir: &Path,
+  manifest: Option<&CargoManifest>,
+  lock: Option<&CargoLock>,
+  name: &str,
+) -> (String, Option<String>) {
+  let crate_lock_packages: Vec<CargoLockPackage> = lock
+    .as_ref()
+    .map(|lock| {
+      lock
+        .package
+        .iter()
+        .filter(|p| p.name == name)
+        .cloned()
+        .collect()
+    })
+    .unwrap_or_default();
+  let (crate_version_string, found_crate_versions) =
+    match (&manifest, &lock, crate_lock_packages.len()) {
+      (Some(_manifest), Some(_lock), 1) => {
+        let crate_lock_package = crate_lock_packages.first().unwrap();
+        let version_string = if let Some(s) = &crate_lock_package.source {
+          if s.starts_with("git") {
+            format!("{} ({})", s, crate_lock_package.version)
+          } else {
+            crate_lock_package.version.clone()
+          }
+        } else {
+          crate_lock_package.version.clone()
+        };
+        (version_string, vec![crate_lock_package.version.clone()])
+      }
+      (None, Some(_lock), 1) => {
+        let crate_lock_package = crate_lock_packages.first().unwrap();
+        let version_string = if let Some(s) = &crate_lock_package.source {
+          if s.starts_with("git") {
+            format!("{} ({})", s, crate_lock_package.version)
+          } else {
+            crate_lock_package.version.clone()
+          }
+        } else {
+          crate_lock_package.version.clone()
+        };
+        (
+          format!("{version_string} (no manifest)"),
+          vec![crate_lock_package.version.clone()],
+        )
+      }
+      _ => {
+        let mut found_crate_versions = Vec::new();
+        let mut is_git = false;
+        let manifest_version = match manifest.and_then(|m| m.dependencies.get(name).cloned()) {
+          Some(tauri) => match tauri {
+            CargoManifestDependency::Version(v) => {
+              found_crate_versions.push(v.clone());
+              v
+            }
+            CargoManifestDependency::Package(p) => {
+              if let Some(v) = p.version {
+                found_crate_versions.push(v.clone());
+                v
+              } else if let Some(p) = p.path {
+                let manifest_path = tauri_dir.join(&p).join("Cargo.toml");
+                let v = match read_to_string(manifest_path)
+                  .map_err(|_| ())
+                  .and_then(|m| toml::from_str::<CargoManifest>(&m).map_err(|_| ()))
+                {
+                  Ok(manifest) => manifest.package.version,
+                  Err(_) => "unknown version".to_string(),
+                };
+                format!("path:{p:?} [{v}]")
+              } else if let Some(g) = p.git {
+                is_git = true;
+                let mut v = format!("git:{g}");
+                if let Some(branch) = p.branch {
+                  let _ = write!(v, "&branch={branch}");
+                } else if let Some(rev) = p.rev {
+                  let _ = write!(v, "#{rev}");
+                }
+                v
+              } else {
+                "unknown manifest".to_string()
+              }
+            }
+          },
+          None => "no manifest".to_string(),
+        };
+
+        let lock_version = match (lock, crate_lock_packages.is_empty()) {
+          (Some(_lock), false) => crate_lock_packages
+            .iter()
+            .map(|p| p.version.clone())
+            .collect::<Vec<String>>()
+            .join(", "),
+          (Some(_lock), true) => "unknown lockfile".to_string(),
+          _ => "no lockfile".to_string(),
+        };
+
+        (
+          format!(
+            "{} {}({})",
+            manifest_version,
+            if is_git { "(git manifest)" } else { "" },
+            lock_version
+          ),
+          found_crate_versions,
+        )
+      }
+    };
+
+  let crate_version = found_crate_versions
+    .into_iter()
+    .map(|v| semver::Version::parse(&v).ok())
+    .max();
+  let suffix = match (crate_version, crate_latest_version(name)) {
+    (Some(Some(version)), Some(target_version)) => {
+      let target_version = semver::Version::parse(&target_version).unwrap();
+      if version < target_version {
+        Some(format!(
+          " ({}, latest: {})",
+          "outdated".yellow(),
+          target_version.to_string().green()
+        ))
+      } else {
+        None
+      }
+    }
+    _ => None,
+  };
+  (crate_version_string, suffix)
+}
+
+pub fn items(app_dir: Option<&PathBuf>, tauri_dir: Option<PathBuf>) -> Vec<SectionItem> {
+  let mut items = Vec::new();
+  if tauri_dir.is_some() || app_dir.is_some() {
+    if let Some(tauri_dir) = tauri_dir {
+      let manifest: Option<CargoManifest> =
+        if let Ok(manifest_contents) = read_to_string(tauri_dir.join("Cargo.toml")) {
+          toml::from_str(&manifest_contents).ok()
+        } else {
+          None
+        };
+      let lock: Option<CargoLock> = get_workspace_dir()
+        .ok()
+        .and_then(|p| read_to_string(p.join("Cargo.lock")).ok())
+        .and_then(|s| toml::from_str(&s).ok());
+
+      for dep in ["tauri", "tauri-build", "wry", "tao"] {
+        let (version_string, version_suffix) =
+          crate_version(&tauri_dir, manifest.as_ref(), lock.as_ref(), dep);
+        let dep = dep.to_string();
+        let item = SectionItem::new(
+          move || {
+            Some((
+              format!(
+                "{} {}: {}{}",
+                dep,
+                "[RUST]".dimmed(),
+                version_string,
+                version_suffix
+                  .clone()
+                  .map(|s| format!(",{s}"))
+                  .unwrap_or_else(|| "".into())
+              ),
+              Status::Neutral,
+            ))
+          },
+          || None,
+          false,
+        );
+        items.push(item);
+      }
+    }
+  }
+
+  items
+}