1 Commits

Author SHA1 Message Date
Eric Ratliff
f83936cbe8 Included new setup command. It has not been tested yet 2026-01-29 22:01:35 -06:00
5 changed files with 31 additions and 724 deletions

View File

@@ -1,267 +0,0 @@
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

@@ -4,5 +4,3 @@ pub mod deploy;
pub mod sdk; pub mod sdk;
pub mod config; pub mod config;
pub mod setup; pub mod setup;
pub mod doctor;
pub mod uninstall;

View File

@@ -34,47 +34,14 @@ 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
@@ -90,12 +57,34 @@ 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. {}", format!("cd {}", name).bright_cyan()); println!(" 1. cd {}", name);
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 tests: {}", "./gradlew test".bright_cyan()); println!(" 4. Run: ./gradlew test");
println!(" 5. Deploy to robot: {}", format!("weevil deploy {}", name).bright_cyan()); println!(" 5. Deploy: weevil deploy {}", name);
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(())
}

View File

@@ -1,393 +0,0 @@
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,26 +33,12 @@ enum Commands {
android_sdk: Option<String>, android_sdk: Option<String>,
}, },
/// Check system health and diagnose issues
Doctor,
/// Setup development environment (system or project) /// Setup development environment (system or project)
Setup { Setup {
/// Path to project directory (optional - without it, sets up system) /// Path to project directory (optional - without it, sets up system)
path: Option<String>, 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
@@ -119,15 +105,9 @@ 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 { path } => {
commands::setup::setup_environment(path.as_deref()) 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)
} }