Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4,207 changes: 4,059 additions & 148 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ resolver = "2"
edition = "2021"
version = "0.3.37"
description = "Tower is the best way to host Python data apps in production"
rust-version = "1.81"
rust-version = "1.89"
authors = ["Brad Heller <brad@tower.dev>"]
license = "MIT"
repository = "https://github.com/tower/tower-cli"
Expand Down
2 changes: 2 additions & 0 deletions crates/tower-cmd/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ tower-api = { workspace = true }
tower-package = { workspace = true }
tower-runtime = { workspace = true }
tower-telemetry = { workspace = true }
tower-uv = { workspace = true }
tower-version = { path = "../tower-version" }
dirs = { workspace = true }
webbrowser = { workspace = true }
rmcp = { version = "0.5.0", features = ["server", "transport-io", "schemars", "transport-sse-server", "transport-streamable-http-server"] }
schemars = "1.0"
Expand Down
125 changes: 125 additions & 0 deletions crates/tower-cmd/src/doctor.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
use crate::output::write;
use crate::api;
use clap::Command;
use colored::Colorize;
use config::Config;

pub fn doctor_cmd() -> Command {
Command::new("doctor")
.about("Check your Tower CLI installation and configuration")
}

pub async fn do_doctor(config: Config) {
write(&format!("{}\n\n", "Running Tower doctor...".bold()));

let mut all_ok = true;

all_ok &= check_uv().await;
all_ok &= check_authentication(&config).await;
all_ok &= check_cli_version().await;

write("\n");
if all_ok {
write(&format!("{}\n", "All checks passed!".green().bold()));
} else {
write(&format!("{}\n", "Some checks failed. See above for details.".yellow().bold()));
}
}

async fn check_uv() -> bool {
write("Checking UV... ");

match tower_uv::Uv::new(None, false).await {
Ok(uv) => {
let version_output = tokio::process::Command::new(&uv.uv_path)
.arg("--version")
.output()
.await;

match version_output {
Ok(cmd_output) => {
let version = String::from_utf8_lossy(&cmd_output.stdout).trim().to_string();
write(&format!("{}\n", "OK".green()));
write(&format!(" Path: {}\n", uv.uv_path.display()));
write(&format!(" Version: {}\n", version));
true
}
Err(e) => {
write(&format!("{}\n", "FAILED".red()));
write(&format!(" Path: {}\n", uv.uv_path.display()));
write(&format!(" Error getting version: {}\n", e));
false
}
}
}
Err(e) => {
write(&format!("{}\n", "FAILED".red()));
write(&format!(" Error: {:?}\n", e));
false
}
}
}

async fn check_authentication(config: &Config) -> bool {
write("Checking authentication... ");

match config::Session::from_config_dir() {
Ok(session) => {
let config_with_session = config.clone().with_session(session.clone());

match api::refresh_session(&config_with_session).await {
Ok(_) => {
write(&format!("{}\n", "OK".green()));
write(&format!(" Logged in as: {}\n", session.user.email));
if let Some(team) = session.active_team {
write(&format!(" Active team: {}\n", team.name));
}
true
}
Err(_) => {
write(&format!("{}\n", "EXPIRED".red()));
write(&format!(" Session for {} has expired.\n", session.user.email));
write(" Run 'tower login' to re-authenticate.\n");
false
}
}
}
Err(_) => {
write(&format!("{}\n", "NOT LOGGED IN".yellow()));
write(" Run 'tower login' to authenticate.\n");
true
}
}
}

async fn check_cli_version() -> bool {
write("Checking CLI version... ");

let current = tower_version::current_version();

match tower_version::check_latest_version().await {
Ok(Some(latest)) => {
if current == latest {
write(&format!("{}\n", "OK".green()));
write(&format!(" Current: {} (latest)\n", current));
} else {
write(&format!("{}\n", "UPDATE AVAILABLE".yellow()));
write(&format!(" Current: {}\n", current));
write(&format!(" Latest: {}\n", latest));
}
true
}
Ok(None) => {
write(&format!("{}\n", "OK".green()));
write(&format!(" Current: {}\n", current));
write(" Could not check for updates.\n");
true
}
Err(_) => {
write(&format!("{}\n", "OK".green()));
write(&format!(" Current: {}\n", current));
write(" Could not check for updates (offline?).\n");
true
}
}
}
3 changes: 3 additions & 0 deletions crates/tower-cmd/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use config::{Config, Session};
pub mod api;
mod apps;
mod deploy;
mod doctor;
mod environments;
pub mod error;
mod mcp;
Expand Down Expand Up @@ -175,6 +176,7 @@ impl App {
eprintln!("MCP server error: {}", e);
std::process::exit(1);
}),
Some(("doctor", _)) => doctor::do_doctor(config).await,
_ => {
cmd_clone.print_help().unwrap();
std::process::exit(2);
Expand Down Expand Up @@ -222,4 +224,5 @@ fn root_cmd() -> Command {
.subcommand(version::version_cmd())
.subcommand(teams::teams_cmd())
.subcommand(mcp::mcp_cmd())
.subcommand(doctor::doctor_cmd())
}
1 change: 0 additions & 1 deletion crates/tower-package/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -637,7 +637,6 @@ pub async fn compute_sha256_file(file_path: &PathBuf) -> Result<String, Error> {
#[cfg(test)]
mod test {
use super::*;
use std::collections::HashMap;
use std::path::PathBuf;

#[tokio::test]
Expand Down
6 changes: 0 additions & 6 deletions crates/tower-uv/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,5 @@ rust-version = { workspace = true }
license = { workspace = true }

[dependencies]
async-compression = { workspace = true }
async_zip = { workspace = true }
dirs = { workspace = true }
futures-lite = { workspace = true }
reqwest = { workspace = true }
tokio = { workspace = true }
tokio-tar = { workspace = true }
tower-telemetry = { workspace = true }
Loading
Loading