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.
This commit is contained in:
@@ -4,4 +4,5 @@ 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 doctor;
|
||||||
|
pub mod uninstall;
|
||||||
393
src/commands/uninstall.rs
Normal file
393
src/commands/uninstall.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
14
src/main.rs
14
src/main.rs
@@ -42,6 +42,17 @@ enum Commands {
|
|||||||
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
|
||||||
@@ -114,6 +125,9 @@ fn main() -> Result<()> {
|
|||||||
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)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user