5 Commits

Author SHA1 Message Date
Eric Ratliff
78abe1d65c feat: Add weevil uninstall command
Adds `weevil uninstall` with three modes of operation:
- Full uninstall removes the entire .weevil directory
- --dry-run enumerates managed components and their sizes
- --only N removes specific components by index

Acknowledges system-installed dependencies (Android SDK, Gradle)
in dry-run output so users know what will and won't be touched.
2026-01-31 13:05:20 -06:00
Eric Ratliff
d8e3c54f3d refactor: Remove SDK installation from weevil new
Project creation now requires environment setup first.
Checks system health and directs users to `weevil setup` if needed.
Separates concerns: setup installs, new creates projects.
2026-01-31 10:56:34 -06:00
Eric Ratliff
df7ca091ec feat: Add system diagnostics command
Adds `weevil doctor` to check development environment health.
Reports status of Java, FTC SDK, Android SDK, ADB, and Gradle.
Provides clear next steps based on system state.
2026-01-31 10:56:22 -06:00
Eric Ratliff
4e9575cc4f feat: Add environment setup command
Adds `weevil setup` with two modes:
- System setup: installs default SDKs and dependencies
- Project setup: reads .weevil.toml and installs project requirements

Provides platform-specific installation instructions when auto-install fails.
Never leaves users wondering what to do next.
2026-01-31 10:55:53 -06:00
Eric Ratliff
6b6ba058b7 chore: Release v1.0.0 - First stable release
Update version references from 1.0.0-rc2 to 1.0.0 across documentation.

This marks the first production-ready stable release of Weevil with complete
cross-platform support, robust Windows deployment, and comprehensive project
management features.

Changes:
- Update README.md current version to 1.0.0
- Remove "Next Release" section from ROADMAP.md (now tracking post-1.0 features)

Weevil is now stable and ready for FTC teams to use in production. 🎉
2026-01-26 19:39:11 -06:00
9 changed files with 1266 additions and 35 deletions

17
CHANGELOG.md Normal file
View File

@@ -0,0 +1,17 @@
# Changelog
## [1.0.0] - 2026-01-27
First stable release! 🎉
### Added
- Complete Windows deployment support
- Android SDK path in project configuration
- Robust cross-platform build and deployment scripts
- Project upgrade command with config migration
- Comprehensive test suite
### Fixed
- Windows APK discovery and deployment
- Batch file path parsing (quote handling)
- ADB integration and error reporting

View File

@@ -511,7 +511,7 @@ Built with frustration at unnecessarily complex robotics frameworks, and hope th
## Project Status ## Project Status
**Current Version:** 1.0.0-rc2 **Current Version:** 1.0.0
**What Works:** **What Works:**
- ✅ Project generation - ✅ Project generation

View File

@@ -2,9 +2,6 @@
This document outlines the planned feature development for Weevil across multiple versions. Features are subject to change based on user feedback, technical constraints, and market needs. This document outlines the planned feature development for Weevil across multiple versions. Features are subject to change based on user feedback, technical constraints, and market needs.
**Current Version:** 1.0.0-rc2
**Next Release:** 1.1.0 (Target: TBD)
--- ---
## Version 1.1.0 - Core Stability & Team Adoption ## Version 1.1.0 - Core Stability & Team Adoption

267
src/commands/doctor.rs Normal file
View File

@@ -0,0 +1,267 @@
use anyhow::Result;
use std::path::Path;
use std::process::Command;
use colored::*;
use crate::sdk::SdkConfig;
#[derive(Debug)]
pub struct SystemHealth {
pub java_ok: bool,
pub java_version: Option<String>,
pub ftc_sdk_ok: bool,
pub ftc_sdk_version: Option<String>,
pub android_sdk_ok: bool,
pub adb_ok: bool,
pub adb_version: Option<String>,
pub gradle_ok: bool,
pub gradle_version: Option<String>,
}
impl SystemHealth {
pub fn is_healthy(&self) -> bool {
// Required: Java, FTC SDK, Android SDK
// Optional: ADB in PATH (can be in Android SDK), Gradle (projects have wrapper)
self.java_ok && self.ftc_sdk_ok && self.android_sdk_ok
}
}
/// Run system diagnostics and report health status
pub fn run_diagnostics() -> Result<()> {
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
println!("{}", " 🩺 Weevil Doctor - System Diagnostics".bright_cyan().bold());
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
println!();
let health = check_system_health()?;
print_diagnostics(&health);
println!();
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
if health.is_healthy() {
println!("{}", " ✓ System is healthy and ready for FTC development".bright_green().bold());
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
println!();
println!("{}", "You can now:".bright_yellow().bold());
println!(" - Create a new project: {}", "weevil new <project-name>".bright_cyan());
println!(" - Setup a cloned project: {}", "weevil setup <project-path>".bright_cyan());
} else {
println!("{}", " ⚠ Issues found - setup required".bright_yellow().bold());
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
println!();
println!("{}", "To fix issues, run:".bright_yellow().bold());
println!(" {}", "weevil setup".bright_cyan());
}
println!();
Ok(())
}
/// Check system health and return a report
pub fn check_system_health() -> Result<SystemHealth> {
let sdk_config = SdkConfig::new()?;
// Check Java
let (java_ok, java_version) = match check_java() {
Ok(version) => (true, Some(version)),
Err(_) => (false, None),
};
// Check FTC SDK
let (ftc_sdk_ok, ftc_sdk_version) = if sdk_config.ftc_sdk_path.exists() {
match crate::sdk::ftc::verify(&sdk_config.ftc_sdk_path) {
Ok(_) => {
let version = crate::sdk::ftc::get_version(&sdk_config.ftc_sdk_path)
.unwrap_or_else(|_| "unknown".to_string());
(true, Some(version))
}
Err(_) => (false, None),
}
} else {
(false, None)
};
// Check Android SDK
let android_sdk_ok = if sdk_config.android_sdk_path.exists() {
crate::sdk::android::verify(&sdk_config.android_sdk_path).is_ok()
} else {
false
};
// Check ADB
let (adb_ok, adb_version) = match check_adb(&sdk_config.android_sdk_path) {
Ok(version) => (true, Some(version)),
Err(_) => (false, None),
};
// Check Gradle (optional)
let (gradle_ok, gradle_version) = match check_gradle() {
Ok(version) => (true, Some(version)),
Err(_) => (false, None),
};
Ok(SystemHealth {
java_ok,
java_version,
ftc_sdk_ok,
ftc_sdk_version,
android_sdk_ok,
adb_ok,
adb_version,
gradle_ok,
gradle_version,
})
}
fn print_diagnostics(health: &SystemHealth) {
let sdk_config = SdkConfig::new().unwrap();
println!("{}", "Required Components:".bright_yellow().bold());
println!();
// Java
if health.java_ok {
println!(" {} Java JDK {}",
"".green(),
health.java_version.as_ref().unwrap()
);
} else {
println!(" {} Java JDK {}",
"".red(),
"not found".red()
);
}
// FTC SDK
if health.ftc_sdk_ok {
println!(" {} FTC SDK {} at {}",
"".green(),
health.ftc_sdk_version.as_ref().unwrap(),
sdk_config.ftc_sdk_path.display()
);
} else {
println!(" {} FTC SDK {} (expected at {})",
"".red(),
"not found".red(),
sdk_config.ftc_sdk_path.display()
);
}
// Android SDK
if health.android_sdk_ok {
println!(" {} Android SDK at {}",
"".green(),
sdk_config.android_sdk_path.display()
);
} else {
println!(" {} Android SDK {} (expected at {})",
"".red(),
"not found".red(),
sdk_config.android_sdk_path.display()
);
}
println!();
println!("{}", "Optional Components:".bright_yellow().bold());
println!();
// ADB
if health.adb_ok {
println!(" {} ADB {}",
"".green(),
health.adb_version.as_ref().unwrap()
);
} else {
println!(" {} ADB {}",
"".yellow(),
"not in PATH (included in Android SDK)".yellow()
);
}
// Gradle
if health.gradle_ok {
println!(" {} Gradle {}",
"".green(),
health.gradle_version.as_ref().unwrap()
);
} else {
println!(" {} Gradle {}",
"".yellow(),
"not in PATH (projects include wrapper)".yellow()
);
}
}
fn check_java() -> Result<String> {
let output = Command::new("java")
.arg("-version")
.output();
match output {
Ok(out) => {
let stderr = String::from_utf8_lossy(&out.stderr);
for line in stderr.lines() {
if line.contains("version") {
if let Some(version_str) = line.split('"').nth(1) {
return Ok(version_str.to_string());
}
}
}
Ok("installed (version unknown)".to_string())
}
Err(_) => anyhow::bail!("Java JDK not found in PATH"),
}
}
fn check_adb(android_sdk_path: &Path) -> Result<String> {
// First try system PATH
let output = Command::new("adb")
.arg("version")
.output();
if let Ok(out) = output {
if out.status.success() {
let stdout = String::from_utf8_lossy(&out.stdout);
for line in stdout.lines() {
if line.starts_with("Android Debug Bridge version") {
return Ok(line.replace("Android Debug Bridge version ", ""));
}
}
return Ok("installed (version unknown)".to_string());
}
}
// Try Android SDK location
let adb_path = if cfg!(target_os = "windows") {
android_sdk_path.join("platform-tools").join("adb.exe")
} else {
android_sdk_path.join("platform-tools").join("adb")
};
if adb_path.exists() {
anyhow::bail!("ADB found in Android SDK but not in PATH")
} else {
anyhow::bail!("ADB not found")
}
}
fn check_gradle() -> Result<String> {
let output = Command::new("gradle")
.arg("--version")
.output();
match output {
Ok(out) => {
let stdout = String::from_utf8_lossy(&out.stdout);
for line in stdout.lines() {
if line.starts_with("Gradle") {
return Ok(line.replace("Gradle ", ""));
}
}
Ok("installed (version unknown)".to_string())
}
Err(_) => anyhow::bail!("Gradle not found in PATH"),
}
}

View File

@@ -3,3 +3,6 @@ pub mod upgrade;
pub mod deploy; pub mod deploy;
pub mod sdk; pub mod sdk;
pub mod config; pub mod config;
pub mod setup;
pub mod doctor;
pub mod uninstall;

View File

@@ -34,14 +34,47 @@ pub fn create_project(
println!("{}", format!("Creating FTC project: {}", name).bright_green().bold()); println!("{}", format!("Creating FTC project: {}", name).bright_green().bold());
println!(); println!();
// Check system health FIRST
println!("{}", "Checking system prerequisites...".bright_yellow());
let health = crate::commands::doctor::check_system_health()?;
if !health.is_healthy() {
println!();
println!("{}", "═══════════════════════════════════════════════════════════".bright_red());
println!("{}", " ✗ System Setup Required".bright_red().bold());
println!("{}", "═══════════════════════════════════════════════════════════".bright_red());
println!();
println!("{}", "Missing required components:".bright_yellow().bold());
if !health.java_ok {
println!(" {} Java JDK", "".red());
}
if !health.ftc_sdk_ok {
println!(" {} FTC SDK", "".red());
}
if !health.android_sdk_ok {
println!(" {} Android SDK", "".red());
}
println!();
println!("{}", "Before creating a project, you need to set up your development environment.".bright_yellow());
println!();
println!("{}", "Run this command to install required components:".bright_yellow().bold());
println!(" {}", "weevil setup".bright_cyan());
println!();
println!("{}", "Then try creating your project again:".bright_yellow().bold());
println!(" {}", format!("weevil new {}", name).bright_cyan());
println!();
bail!("System setup required");
}
println!("{} All prerequisites met", "".green());
println!();
// Setup or verify SDK configuration // Setup or verify SDK configuration
let sdk_config = SdkConfig::with_paths(ftc_sdk, android_sdk)?; let sdk_config = SdkConfig::with_paths(ftc_sdk, android_sdk)?;
// Install SDKs if needed
println!("{}", "Checking SDKs...".bright_yellow());
ensure_sdks(&sdk_config)?;
println!();
println!("{}", "Creating project structure...".bright_yellow()); println!("{}", "Creating project structure...".bright_yellow());
// Build the project // Build the project
@@ -57,34 +90,12 @@ pub fn create_project(
println!("Version: {}", crate::sdk::ftc::get_version(&sdk_config.ftc_sdk_path).unwrap_or_else(|_| "unknown".to_string())); println!("Version: {}", crate::sdk::ftc::get_version(&sdk_config.ftc_sdk_path).unwrap_or_else(|_| "unknown".to_string()));
println!(); println!();
println!("{}", "Next steps:".bright_yellow().bold()); println!("{}", "Next steps:".bright_yellow().bold());
println!(" 1. cd {}", name); println!(" 1. {}", format!("cd {}", name).bright_cyan());
println!(" 2. Review README.md for project structure"); println!(" 2. Review README.md for project structure");
println!(" 3. Start coding in src/main/java/robot/"); println!(" 3. Start coding in src/main/java/robot/");
println!(" 4. Run: ./gradlew test"); println!(" 4. Run tests: {}", "./gradlew test".bright_cyan());
println!(" 5. Deploy: weevil deploy {}", name); println!(" 5. Deploy to robot: {}", format!("weevil deploy {}", name).bright_cyan());
println!(); println!();
Ok(()) Ok(())
} }
fn ensure_sdks(config: &SdkConfig) -> Result<()> {
// Check FTC SDK
if !config.ftc_sdk_path.exists() {
println!("FTC SDK not found. Installing...");
crate::sdk::ftc::install(&config.ftc_sdk_path, &config.android_sdk_path)?;
} else {
println!("{} FTC SDK found at: {}", "".green(), config.ftc_sdk_path.display());
crate::sdk::ftc::verify(&config.ftc_sdk_path)?;
}
// Check Android SDK
if !config.android_sdk_path.exists() {
println!("Android SDK not found. Installing...");
crate::sdk::android::install(&config.android_sdk_path)?;
} else {
println!("{} Android SDK found at: {}", "".green(), config.android_sdk_path.display());
crate::sdk::android::verify(&config.android_sdk_path)?;
}
Ok(())
}

514
src/commands/setup.rs Normal file
View File

@@ -0,0 +1,514 @@
use anyhow::{Result, Context, bail};
use std::path::{Path, PathBuf};
use std::process::Command;
use colored::*;
use crate::sdk::SdkConfig;
use crate::project::ProjectConfig;
/// Setup development environment - either system-wide or for a specific project
pub fn setup_environment(project_path: Option<&str>) -> Result<()> {
match project_path {
Some(path) => setup_project(path),
None => setup_system(),
}
}
/// Setup system-wide development environment with default SDKs
fn setup_system() -> Result<()> {
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
println!("{}", " System Setup - Preparing FTC Development Environment".bright_cyan().bold());
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
println!();
let mut issues = Vec::new();
let mut installed = Vec::new();
// Check and install SDKs
let sdk_config = SdkConfig::new()?;
// 1. Check Java
println!("{}", "Checking Java JDK...".bright_yellow());
match check_java() {
Ok(version) => {
println!("{} Java JDK {} found", "".green(), version);
installed.push(format!("Java JDK {}", version));
}
Err(e) => {
println!("{} {}", "".red(), e);
issues.push(("Java JDK", get_java_install_instructions()));
}
}
println!();
// 2. Check/Install FTC SDK
println!("{}", "Checking FTC SDK...".bright_yellow());
if sdk_config.ftc_sdk_path.exists() {
match crate::sdk::ftc::verify(&sdk_config.ftc_sdk_path) {
Ok(_) => {
let version = crate::sdk::ftc::get_version(&sdk_config.ftc_sdk_path)
.unwrap_or_else(|_| "unknown".to_string());
println!("{} FTC SDK {} found at: {}",
"".green(),
version,
sdk_config.ftc_sdk_path.display()
);
installed.push(format!("FTC SDK {}", version));
}
Err(_) => {
println!("{} FTC SDK found but incomplete, reinstalling...", "".yellow());
crate::sdk::ftc::install(&sdk_config.ftc_sdk_path, &sdk_config.android_sdk_path)?;
let version = crate::sdk::ftc::get_version(&sdk_config.ftc_sdk_path)
.unwrap_or_else(|_| "unknown".to_string());
installed.push(format!("FTC SDK {} (installed)", version));
}
}
} else {
println!("FTC SDK not found. Installing...");
crate::sdk::ftc::install(&sdk_config.ftc_sdk_path, &sdk_config.android_sdk_path)?;
let version = crate::sdk::ftc::get_version(&sdk_config.ftc_sdk_path)
.unwrap_or_else(|_| "unknown".to_string());
installed.push(format!("FTC SDK {} (installed)", version));
}
println!();
// 3. Check/Install Android SDK
println!("{}", "Checking Android SDK...".bright_yellow());
if sdk_config.android_sdk_path.exists() {
match crate::sdk::android::verify(&sdk_config.android_sdk_path) {
Ok(_) => {
println!("{} Android SDK found at: {}",
"".green(),
sdk_config.android_sdk_path.display()
);
installed.push("Android SDK".to_string());
}
Err(_) => {
println!("{} Android SDK found but incomplete, reinstalling...", "".yellow());
crate::sdk::android::install(&sdk_config.android_sdk_path)?;
installed.push("Android SDK (installed)".to_string());
}
}
} else {
println!("Android SDK not found. Installing...");
crate::sdk::android::install(&sdk_config.android_sdk_path)?;
installed.push("Android SDK (installed)".to_string());
}
println!();
// 4. Check ADB
println!("{}", "Checking ADB (Android Debug Bridge)...".bright_yellow());
match check_adb(&sdk_config.android_sdk_path) {
Ok(version) => {
println!("{} ADB {} found", "".green(), version);
installed.push(format!("ADB {}", version));
}
Err(e) => {
println!("{} {}", "".yellow(), e);
println!(" ADB is included in Android SDK platform-tools");
println!(" Add to PATH: {}", sdk_config.android_sdk_path.join("platform-tools").display());
}
}
println!();
// 5. Check Gradle
println!("{}", "Checking Gradle...".bright_yellow());
match check_gradle() {
Ok(version) => {
println!("{} Gradle {} found", "".green(), version);
installed.push(format!("Gradle {}", version));
}
Err(e) => {
println!("{} {}", "".yellow(), e);
println!(" Note: Weevil projects include Gradle wrapper, so this is optional");
}
}
println!();
// Print summary
print_system_summary(&installed, &issues, &sdk_config);
Ok(())
}
/// Setup dependencies for a specific project by reading its .weevil.toml
fn setup_project(project_path: &str) -> Result<()> {
let project_path = PathBuf::from(project_path);
if !project_path.exists() {
bail!("Project directory not found: {}", project_path.display());
}
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
println!("{}", " Project Setup - Installing Dependencies".bright_cyan().bold());
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
println!();
// Load project configuration
println!("{}", "Reading project configuration...".bright_yellow());
let config = ProjectConfig::load(&project_path)
.context("Failed to load .weevil.toml")?;
println!();
println!("{}", "Project Configuration:".bright_yellow().bold());
println!(" Project: {}", config.project_name.bright_white());
println!(" FTC SDK: {} ({})",
config.ftc_sdk_version.bright_white(),
config.ftc_sdk_path.display()
);
println!(" Android SDK: {}", config.android_sdk_path.display());
println!();
let mut installed = Vec::new();
let mut issues = Vec::new();
// 1. Check Java
println!("{}", "Checking Java JDK...".bright_yellow());
match check_java() {
Ok(version) => {
println!("{} Java JDK {} found", "".green(), version);
installed.push(format!("Java JDK {}", version));
}
Err(e) => {
println!("{} {}", "".red(), e);
issues.push(("Java JDK", get_java_install_instructions()));
}
}
println!();
// 2. Check/Install project-specific FTC SDK
println!("{}", format!("Checking FTC SDK {}...", config.ftc_sdk_version).bright_yellow());
if config.ftc_sdk_path.exists() {
match crate::sdk::ftc::verify(&config.ftc_sdk_path) {
Ok(_) => {
println!("{} FTC SDK {} found at: {}",
"".green(),
config.ftc_sdk_version,
config.ftc_sdk_path.display()
);
installed.push(format!("FTC SDK {}", config.ftc_sdk_version));
}
Err(_) => {
println!("{} FTC SDK path exists but is invalid", "".red());
println!(" Expected at: {}", config.ftc_sdk_path.display());
println!();
println!("{}", "Solution:".bright_yellow().bold());
println!(" The .weevil.toml specifies an FTC SDK location that doesn't exist or is incomplete.");
println!(" You have two options:");
println!();
println!(" 1. Update the project to use a different SDK:");
println!(" weevil config {} --set-sdk <path-to-sdk>", project_path.display());
println!();
println!(" 2. Install the SDK at the expected location:");
println!(" # Clone FTC SDK to the expected path");
println!(" git clone https://github.com/FIRST-Tech-Challenge/FtcRobotController.git \\");
println!(" {}", config.ftc_sdk_path.display());
println!(" cd {}", config.ftc_sdk_path.display());
println!(" git checkout {}", config.ftc_sdk_version);
bail!("FTC SDK verification failed");
}
}
} else {
println!("{} FTC SDK not found at: {}", "".red(), config.ftc_sdk_path.display());
println!();
// Try to install it automatically
println!("{}", "Attempting automatic installation...".bright_yellow());
match crate::sdk::ftc::install(&config.ftc_sdk_path, &config.android_sdk_path) {
Ok(_) => {
println!("{} FTC SDK {} installed successfully",
"".green(),
config.ftc_sdk_version
);
installed.push(format!("FTC SDK {} (installed)", config.ftc_sdk_version));
}
Err(e) => {
println!("{} Automatic installation failed: {}", "".red(), e);
println!();
println!("{}", "Manual Installation Required:".bright_yellow().bold());
println!(" git clone https://github.com/FIRST-Tech-Challenge/FtcRobotController.git \\");
println!(" {}", config.ftc_sdk_path.display());
println!(" cd {}", config.ftc_sdk_path.display());
println!(" git checkout {}", config.ftc_sdk_version);
bail!("FTC SDK installation failed");
}
}
}
println!();
// 3. Check/Install Android SDK
println!("{}", "Checking Android SDK...".bright_yellow());
if config.android_sdk_path.exists() {
match crate::sdk::android::verify(&config.android_sdk_path) {
Ok(_) => {
println!("{} Android SDK found at: {}",
"".green(),
config.android_sdk_path.display()
);
installed.push("Android SDK".to_string());
}
Err(_) => {
println!("{} Android SDK found but incomplete, reinstalling...", "".yellow());
crate::sdk::android::install(&config.android_sdk_path)?;
installed.push("Android SDK (installed)".to_string());
}
}
} else {
println!("Android SDK not found. Installing...");
crate::sdk::android::install(&config.android_sdk_path)?;
installed.push("Android SDK (installed)".to_string());
}
println!();
// 4. Check ADB
println!("{}", "Checking ADB...".bright_yellow());
match check_adb(&config.android_sdk_path) {
Ok(version) => {
println!("{} ADB {} found", "".green(), version);
installed.push(format!("ADB {}", version));
}
Err(e) => {
println!("{} {}", "".yellow(), e);
println!(" Add to PATH: {}", config.android_sdk_path.join("platform-tools").display());
}
}
println!();
// 5. Check Gradle wrapper in project
println!("{}", "Checking Gradle wrapper...".bright_yellow());
let gradlew = if cfg!(target_os = "windows") {
project_path.join("gradlew.bat")
} else {
project_path.join("gradlew")
};
if gradlew.exists() {
println!("{} Gradle wrapper found in project", "".green());
installed.push("Gradle wrapper".to_string());
} else {
println!("{} Gradle wrapper not found in project", "".yellow());
println!(" Run 'weevil upgrade {}' to regenerate project files", project_path.display());
}
println!();
// Print summary
print_project_summary(&installed, &issues, &config, &project_path);
Ok(())
}
fn check_java() -> Result<String> {
let output = Command::new("java")
.arg("-version")
.output();
match output {
Ok(out) => {
let stderr = String::from_utf8_lossy(&out.stderr);
// Java version is typically in stderr, format: java version "11.0.x" or openjdk version "11.0.x"
for line in stderr.lines() {
if line.contains("version") {
if let Some(version_str) = line.split('"').nth(1) {
return Ok(version_str.to_string());
}
}
}
Ok("installed (version unknown)".to_string())
}
Err(_) => bail!("Java JDK not found in PATH"),
}
}
fn check_adb(android_sdk_path: &Path) -> Result<String> {
// First try system PATH
let output = Command::new("adb")
.arg("version")
.output();
if let Ok(out) = output {
if out.status.success() {
let stdout = String::from_utf8_lossy(&out.stdout);
for line in stdout.lines() {
if line.starts_with("Android Debug Bridge version") {
return Ok(line.replace("Android Debug Bridge version ", ""));
}
}
return Ok("installed (version unknown)".to_string());
}
}
// Try Android SDK location
let adb_path = if cfg!(target_os = "windows") {
android_sdk_path.join("platform-tools").join("adb.exe")
} else {
android_sdk_path.join("platform-tools").join("adb")
};
if adb_path.exists() {
bail!("ADB found in Android SDK but not in PATH")
} else {
bail!("ADB not found")
}
}
fn check_gradle() -> Result<String> {
let output = Command::new("gradle")
.arg("--version")
.output();
match output {
Ok(out) => {
let stdout = String::from_utf8_lossy(&out.stdout);
for line in stdout.lines() {
if line.starts_with("Gradle") {
return Ok(line.replace("Gradle ", ""));
}
}
Ok("installed (version unknown)".to_string())
}
Err(_) => bail!("Gradle not found in PATH (optional)"),
}
}
fn get_java_install_instructions() -> String {
if cfg!(target_os = "windows") {
format!(
"Java JDK is required but not found.\n\
\n\
To install Java 11 on Windows:\n\
\n\
1. Download from: {}\n\
2. Run the installer\n\
3. Add Java to your PATH (installer usually does this)\n\
4. Run 'weevil setup' again to verify\n\
\n\
Verify installation: java -version",
"https://adoptium.net/temurin/releases/?version=11".bright_white()
)
} else if cfg!(target_os = "macos") {
format!(
"Java JDK is required but not found.\n\
\n\
To install Java 11 on macOS:\n\
\n\
Using Homebrew (recommended):\n\
{}\n\
\n\
Or download from: {}\n\
\n\
Verify installation: java -version",
" brew install openjdk@11".bright_white(),
"https://adoptium.net/temurin/releases/?version=11".bright_white()
)
} else {
format!(
"Java JDK is required but not found.\n\
\n\
To install Java 11 on Ubuntu/Debian:\n\
{}\n\
{}\n\
\n\
To install on Fedora/RHEL:\n\
{}\n\
\n\
Verify installation: java -version",
" sudo apt update".bright_white(),
" sudo apt install openjdk-11-jdk".bright_white(),
" sudo dnf install java-11-openjdk-devel".bright_white()
)
}
}
fn print_system_summary(installed: &[String], issues: &[(&str, String)], sdk_config: &SdkConfig) {
println!("{}", "═══════════════════════════════════════════════════════════".bright_green());
println!("{}", " System Setup Summary".bright_green().bold());
println!("{}", "═══════════════════════════════════════════════════════════".bright_green());
println!();
if !installed.is_empty() {
println!("{}", "Installed Components:".bright_green().bold());
for component in installed {
println!(" {} {}", "".green(), component);
}
println!();
}
if !issues.is_empty() {
println!("{}", "Manual Installation Required:".bright_yellow().bold());
println!();
for (name, instructions) in issues {
println!("{} {}", "".red(), name.red().bold());
println!();
for line in instructions.lines() {
println!(" {}", line);
}
println!();
}
}
println!("{}", "SDK Locations:".bright_cyan().bold());
println!(" FTC SDK: {}", sdk_config.ftc_sdk_path.display());
println!(" Android SDK: {}", sdk_config.android_sdk_path.display());
println!(" Cache: {}", sdk_config.cache_dir.display());
println!();
if issues.is_empty() {
println!("{}", "✓ System is ready for FTC development!".bright_green().bold());
println!();
println!("{}", "Next steps:".bright_yellow().bold());
println!(" Create a new project: {}", "weevil new my-robot".bright_white());
println!(" Clone existing project: {}", "git clone <repo> && cd <repo> && weevil setup .".bright_white());
} else {
println!("{}", "⚠ Please install the required components listed above".bright_yellow().bold());
println!(" Then run {} to verify", "weevil setup".bright_white());
}
println!();
}
fn print_project_summary(installed: &[String], issues: &[(&str, String)], config: &ProjectConfig, project_path: &Path) {
println!("{}", "═══════════════════════════════════════════════════════════".bright_green());
println!("{}", " Project Setup Summary".bright_green().bold());
println!("{}", "═══════════════════════════════════════════════════════════".bright_green());
println!();
println!("{}", "Project Details:".bright_cyan().bold());
println!(" Name: {}", config.project_name);
println!(" Location: {}", project_path.display());
println!(" FTC SDK: {} at {}", config.ftc_sdk_version, config.ftc_sdk_path.display());
println!();
if !installed.is_empty() {
println!("{}", "Installed Components:".bright_green().bold());
for component in installed {
println!(" {} {}", "".green(), component);
}
println!();
}
if !issues.is_empty() {
println!("{}", "Manual Installation Required:".bright_yellow().bold());
println!();
for (name, instructions) in issues {
println!("{} {}", "".red(), name.red().bold());
println!();
for line in instructions.lines() {
println!(" {}", line);
}
println!();
}
}
if issues.is_empty() {
println!("{}", "✓ Project is ready for development!".bright_green().bold());
println!();
println!("{}", "Next steps:".bright_yellow().bold());
println!(" 1. Review the code: {}", format!("cd {}", project_path.display()).bright_white());
println!(" 2. Run tests: {}", "./gradlew test".bright_white());
println!(" 3. Build: {}", "./build.sh (or build.bat on Windows)".bright_white());
println!(" 4. Deploy to robot: {}", format!("weevil deploy {}", project_path.display()).bright_white());
} else {
println!("{}", "⚠ Please install the required components listed above".bright_yellow().bold());
println!(" Then run {} to verify", format!("weevil setup {}", project_path.display()).bright_white());
}
println!();
}

393
src/commands/uninstall.rs Normal file
View File

@@ -0,0 +1,393 @@
use anyhow::Result;
use std::fs;
use std::io::{self, Write};
use std::path::PathBuf;
use colored::*;
use crate::sdk::SdkConfig;
#[derive(Debug, Clone)]
enum RemoveTarget {
FtcSdk(PathBuf, String), // path, version label
AndroidSdk(PathBuf),
}
impl RemoveTarget {
fn label(&self) -> String {
match self {
RemoveTarget::FtcSdk(_, version) => format!("FTC SDK {}", version),
RemoveTarget::AndroidSdk(_) => "Android SDK".to_string(),
}
}
fn path(&self) -> &PathBuf {
match self {
RemoveTarget::FtcSdk(path, _) => path,
RemoveTarget::AndroidSdk(path) => path,
}
}
fn size(&self) -> u64 {
dir_size(self.path())
}
}
/// Uninstall Weevil-managed dependencies
///
/// - No args: removes ~/.weevil entirely
/// - --dry-run: shows what would be removed
/// - --only N [N ...]: selective removal of specific components
pub fn uninstall_dependencies(dry_run: bool, targets: Option<Vec<usize>>) -> Result<()> {
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
println!("{}", " 🗑️ Weevil Uninstall - Remove Dependencies".bright_cyan().bold());
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
println!();
let sdk_config = SdkConfig::new()?;
// No --only flag: full uninstall, just nuke .weevil
if targets.is_none() {
return full_uninstall(&sdk_config, dry_run);
}
// --only flag: selective removal
let all_targets = scan_targets(&sdk_config);
if all_targets.is_empty() {
println!("{}", "No Weevil-managed components found.".bright_green());
println!();
return Ok(());
}
// Show numbered list
println!("{}", "Found Weevil-managed components:".bright_yellow().bold());
println!();
for (i, target) in all_targets.iter().enumerate() {
println!(" {}. {}{}",
(i + 1).to_string().bright_cyan().bold(),
target.label(),
format!("{} at {}", format_size(target.size()), target.path().display()).dimmed()
);
}
println!();
// Resolve selected indices
let indices = targets.unwrap();
let mut selected = Vec::new();
for idx in indices {
if idx == 0 || idx > all_targets.len() {
println!("{} Invalid selection: {}. Valid range is 1{}",
"".red(), idx, all_targets.len());
return Ok(());
}
selected.push(all_targets[idx - 1].clone());
}
if dry_run {
print_dry_run(&selected);
return Ok(());
}
print_removal_list(&selected);
if !confirm()? {
return Ok(());
}
execute_removal(&selected);
Ok(())
}
/// Full uninstall — removes the entire .weevil directory
fn full_uninstall(sdk_config: &SdkConfig, dry_run: bool) -> Result<()> {
if !sdk_config.cache_dir.exists() {
println!("{}", "No Weevil-managed components found.".bright_green());
println!();
return Ok(());
}
let size = dir_size(&sdk_config.cache_dir);
if dry_run {
let all_targets = scan_targets(sdk_config);
println!("{}", "── Dry Run ─────────────────────────────────────────────────".bright_yellow().bold());
println!();
println!("{}", format!("Contents of {}:", sdk_config.cache_dir.display()).bright_yellow().bold());
println!();
for (i, target) in all_targets.iter().enumerate() {
println!(" {}. {}{}",
(i + 1).to_string().bright_cyan().bold(),
target.label(),
format!("{} at {}", format_size(target.size()), target.path().display()).dimmed()
);
}
// Note any system-installed dependencies that Weevil doesn't manage
let mut has_external = false;
if sdk_config.android_sdk_path.exists()
&& !sdk_config.android_sdk_path.to_string_lossy().contains(".weevil") {
if !has_external {
println!();
has_external = true;
}
println!(" {} Android SDK at {} — not managed by Weevil, will not be removed",
"".bright_cyan(),
sdk_config.android_sdk_path.display()
);
}
if let Ok(gradle_version) = check_gradle() {
if !has_external {
println!();
}
println!(" {} Gradle {} — not managed by Weevil, will not be removed",
"".bright_cyan(),
gradle_version
);
}
println!();
println!("{}", format!("Total: {} ({})", sdk_config.cache_dir.display(), format_size(size)).bright_yellow().bold());
println!();
println!("{}", "To remove everything:".bright_yellow().bold());
println!(" {}", "weevil uninstall".bright_cyan());
println!();
println!("{}", "To remove specific items:".bright_yellow().bold());
println!(" {}", "weevil uninstall --only 1 2".bright_cyan());
println!();
return Ok(());
}
println!("{}", "This will permanently remove:".bright_yellow().bold());
println!();
println!(" {} {} ({})", "".red(), sdk_config.cache_dir.display(), format_size(size));
println!();
println!("{}", "Everything Weevil installed will be gone.".bright_yellow());
println!();
if !confirm()? {
return Ok(());
}
println!();
print!(" Removing {} ... ", sdk_config.cache_dir.display());
match fs::remove_dir_all(&sdk_config.cache_dir) {
Ok(_) => {
println!("{}", "".green());
println!();
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
println!("{}", " ✓ Uninstall Complete".bright_green().bold());
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
println!();
println!("{}", "Weevil binary is still installed. To remove it, delete the weevil executable.".bright_yellow());
println!();
println!("{}", "To reinstall dependencies later:".bright_yellow().bold());
println!(" {}", "weevil setup".bright_cyan());
}
Err(e) => {
println!("{} ({})", "".red(), e);
println!();
println!("{}", "You may need to manually remove this directory.".bright_yellow());
}
}
println!();
Ok(())
}
fn print_dry_run(selected: &[RemoveTarget]) {
println!("{}", "── Dry Run ─────────────────────────────────────────────────".bright_yellow().bold());
println!();
println!("{}", "The following would be removed:".bright_yellow());
println!();
let mut total: u64 = 0;
for target in selected {
let size = target.size();
total += size;
println!(" {} {} ({})", "".red(), target.label(), format_size(size));
println!(" {}", target.path().display().to_string().dimmed());
}
println!();
println!("{}", format!("Total: {}", format_size(total)).bright_yellow().bold());
println!();
println!("{}", "Run without --dry-run to actually remove these components.".dimmed());
println!();
}
fn print_removal_list(selected: &[RemoveTarget]) {
println!("{}", "The following will be removed:".bright_yellow().bold());
println!();
let mut total: u64 = 0;
for target in selected {
let size = target.size();
total += size;
println!(" {} {} ({})", "".red(), target.label(), format_size(size));
println!(" {}", target.path().display().to_string().dimmed());
}
println!();
println!("{}", format!("Total: {}", format_size(total)).bright_yellow().bold());
println!();
}
fn confirm() -> Result<bool> {
print!("{}", "Are you sure you want to continue? (y/N): ".bright_yellow());
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let answer = input.trim().to_lowercase();
if answer != "y" && answer != "yes" {
println!();
println!("{}", "Uninstall cancelled.".bright_green());
println!();
return Ok(false);
}
Ok(true)
}
fn execute_removal(selected: &[RemoveTarget]) {
println!();
println!("{}", "Removing components...".bright_yellow());
println!();
let mut removed = Vec::new();
let mut failed = Vec::new();
for target in selected {
print!(" Removing {}... ", target.label());
match fs::remove_dir_all(target.path()) {
Ok(_) => {
println!("{}", "".green());
removed.push(target.clone());
}
Err(e) => {
println!("{} ({})", "".red(), e);
failed.push((target.clone(), e.to_string()));
}
}
}
println!();
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
if failed.is_empty() {
println!("{}", " ✓ Uninstall Complete".bright_green().bold());
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
println!();
println!("{}", "Removed:".bright_green().bold());
for target in &removed {
println!(" {} {}", "".green(), target.label());
}
} else {
println!("{}", " ⚠ Uninstall Completed with Errors".bright_yellow().bold());
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
println!();
if !removed.is_empty() {
println!("{}", "Removed:".bright_green().bold());
for target in &removed {
println!(" {} {}", "".green(), target.label());
}
println!();
}
println!("{}", "Failed to remove:".bright_red().bold());
for (target, error) in &failed {
println!(" {} {}: {}", "".red(), target.label(), error);
}
println!();
println!("{}", "You may need to manually remove these directories.".bright_yellow());
}
println!();
println!("{}", "To reinstall dependencies later:".bright_yellow().bold());
println!(" {}", "weevil setup".bright_cyan());
println!();
}
/// Scan the cache directory for individual removable components (used by --only)
fn scan_targets(sdk_config: &SdkConfig) -> Vec<RemoveTarget> {
let mut targets = Vec::new();
if sdk_config.cache_dir.exists() {
if let Ok(entries) = fs::read_dir(&sdk_config.cache_dir) {
for entry in entries.flatten() {
let name = entry.file_name().to_string_lossy().to_string();
let path = entry.path();
if !path.is_dir() {
continue;
}
if name.starts_with("ftc-sdk") {
let version = crate::sdk::ftc::get_version(&path)
.unwrap_or_else(|_| {
if name == "ftc-sdk" {
"default".to_string()
} else {
name.trim_start_matches("ftc-sdk-").to_string()
}
});
targets.push(RemoveTarget::FtcSdk(path, version));
}
}
}
}
// Android SDK — only if Weevil installed it (lives inside .weevil)
if sdk_config.android_sdk_path.exists()
&& sdk_config.android_sdk_path.to_string_lossy().contains(".weevil") {
targets.push(RemoveTarget::AndroidSdk(sdk_config.android_sdk_path.clone()));
}
targets
}
fn check_gradle() -> Result<String> {
let output = std::process::Command::new("gradle")
.arg("--version")
.output();
match output {
Ok(out) => {
let stdout = String::from_utf8_lossy(&out.stdout);
for line in stdout.lines() {
if line.starts_with("Gradle") {
return Ok(line.replace("Gradle ", ""));
}
}
Ok("installed (version unknown)".to_string())
}
Err(_) => anyhow::bail!("Gradle not found"),
}
}
fn dir_size(path: &PathBuf) -> u64 {
let mut size: u64 = 0;
if let Ok(entries) = fs::read_dir(path) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
size += dir_size(&path);
} else if let Ok(metadata) = path.metadata() {
size += metadata.len();
}
}
}
size
}
fn format_size(bytes: u64) -> String {
if bytes >= 1_073_741_824 {
format!("{:.1} GB", bytes as f64 / 1_073_741_824.0)
} else if bytes >= 1_048_576 {
format!("{:.1} MB", bytes as f64 / 1_048_576.0)
} else if bytes >= 1_024 {
format!("{:.1} KB", bytes as f64 / 1_024.0)
} else {
format!("{} B", bytes)
}
}

View File

@@ -33,6 +33,26 @@ enum Commands {
android_sdk: Option<String>, android_sdk: Option<String>,
}, },
/// Check system health and diagnose issues
Doctor,
/// Setup development environment (system or project)
Setup {
/// Path to project directory (optional - without it, sets up system)
path: Option<String>,
},
/// Remove Weevil-installed SDKs and dependencies
Uninstall {
/// Show what would be removed without actually removing anything
#[arg(long)]
dry_run: bool,
/// Remove only specific items by number (use --dry-run first to see the list)
#[arg(long, value_name = "NUM", num_args = 1..)]
only: Option<Vec<usize>>,
},
/// Upgrade an existing project to the latest generator version /// Upgrade an existing project to the latest generator version
Upgrade { Upgrade {
/// Path to the project directory /// Path to the project directory
@@ -99,6 +119,15 @@ fn main() -> Result<()> {
Commands::New { name, ftc_sdk, android_sdk } => { Commands::New { name, ftc_sdk, android_sdk } => {
commands::new::create_project(&name, ftc_sdk.as_deref(), android_sdk.as_deref()) commands::new::create_project(&name, ftc_sdk.as_deref(), android_sdk.as_deref())
} }
Commands::Doctor => {
commands::doctor::run_diagnostics()
}
Commands::Setup { path } => {
commands::setup::setup_environment(path.as_deref())
}
Commands::Uninstall { dry_run, only } => {
commands::uninstall::uninstall_dependencies(dry_run, only)
}
Commands::Upgrade { path } => { Commands::Upgrade { path } => {
commands::upgrade::upgrade_project(&path) commands::upgrade::upgrade_project(&path)
} }