feat: Weevil v1.0.0-beta1 - FTC Project Generator

Cross-platform tool for generating clean, testable FTC robot projects
without editing the SDK installation.

Features:
- Standalone project generation with proper separation from SDK
- Per-project SDK configuration via .weevil.toml
- Local unit testing support (no robot required)
- Cross-platform build/deploy scripts (Linux/macOS/Windows)
- Project upgrade system preserving user code
- Configuration management commands
- Comprehensive test suite (11 passing tests)
- Zero-warning builds

Architecture:
- Pure Rust implementation with embedded Gradle wrapper
- Projects use deployToSDK task to copy code to FTC SDK TeamCode
- Git-ready projects with automatic initialization
- USB and WiFi deployment with auto-detection

Commands:
- weevil new <name> - Create new project
- weevil upgrade <path> - Update project infrastructure
- weevil config <path> - View/modify project configuration
- weevil sdk status/install/update - Manage SDKs

Addresses the core problem: FTC's SDK structure forces students to
edit framework internals instead of separating concerns like industry
standard practices. Weevil enables proper software engineering workflows
for robotics education.
This commit is contained in:
Eric Ratliff
2026-01-24 15:20:18 -06:00
commit 70a1acc2a1
35 changed files with 3558 additions and 0 deletions

46
src/commands/config.rs Normal file
View File

@@ -0,0 +1,46 @@
use anyhow::{Result, Context};
use colored::*;
use std::path::PathBuf;
use crate::project::ProjectConfig;
pub fn show_config(path: &str) -> Result<()> {
let project_path = PathBuf::from(path);
let config = ProjectConfig::load(&project_path)
.context("Failed to load project config")?;
config.display();
Ok(())
}
pub fn set_sdk(path: &str, sdk_path: &str) -> Result<()> {
let project_path = PathBuf::from(path);
let new_sdk_path = PathBuf::from(sdk_path);
println!("{}", "Updating FTC SDK path...".bright_yellow());
println!();
// Load existing config
let mut config = ProjectConfig::load(&project_path)
.context("Failed to load project config")?;
println!("Current SDK: {}", config.ftc_sdk_path.display());
println!("New SDK: {}", new_sdk_path.display());
println!();
// Update and validate
config.update_sdk_path(new_sdk_path.clone())
.context("Failed to update SDK path")?;
// Save config
config.save(&project_path)
.context("Failed to save config")?;
println!("{} FTC SDK updated to: {}", "".green(), new_sdk_path.display());
println!("{} Version: {}", "".green(), config.ftc_sdk_version);
println!();
println!("Note: Run {} to update build files", "weevil upgrade .".bright_cyan());
println!();
Ok(())
}

35
src/commands/deploy.rs Normal file
View File

@@ -0,0 +1,35 @@
use anyhow::Result;
use colored::*;
pub fn deploy_project(
path: &str,
usb: bool,
wifi: bool,
ip: Option<&str>,
) -> Result<()> {
println!("{}", format!("Deploying project: {}", path).bright_yellow());
println!();
if usb {
println!("Mode: USB");
} else if wifi {
println!("Mode: WiFi");
} else {
println!("Mode: Auto-detect");
}
if let Some(ip_addr) = ip {
println!("Target IP: {}", ip_addr);
}
println!();
println!("{}", "⚠ Deploy functionality coming soon!".yellow());
println!();
println!("This will:");
println!(" • Build the APK");
println!(" • Detect Control Hub (USB or WiFi)");
println!(" • Install to robot");
println!();
Ok(())
}

5
src/commands/mod.rs Normal file
View File

@@ -0,0 +1,5 @@
pub mod new;
pub mod upgrade;
pub mod deploy;
pub mod sdk;
pub mod config;

90
src/commands/new.rs Normal file
View File

@@ -0,0 +1,90 @@
use anyhow::{Result, bail};
use std::path::PathBuf;
use colored::*;
use crate::sdk::SdkConfig;
use crate::project::ProjectBuilder;
pub fn create_project(
name: &str,
ftc_sdk: Option<&str>,
android_sdk: Option<&str>,
) -> Result<()> {
// Validate project name
if name.is_empty() {
bail!("Project name cannot be empty");
}
if !name.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_') {
bail!("Project name must contain only alphanumeric characters, hyphens, and underscores");
}
let project_path = PathBuf::from(name);
// Check if project already exists
if project_path.exists() {
bail!(
"{}\n\nDirectory already exists: {}\n\nTo upgrade: weevil upgrade {}",
"Project Already Exists".red().bold(),
project_path.display(),
name
);
}
println!("{}", format!("Creating FTC project: {}", name).bright_green().bold());
println!();
// Setup or verify SDK configuration
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());
// Build the project
let builder = ProjectBuilder::new(name, &sdk_config)?;
builder.create(&project_path, &sdk_config)?;
println!();
println!("{}", "═══════════════════════════════════════════════════════════".bright_green());
println!("{}", format!(" ✓ Project Created: {}", name).bright_green().bold());
println!("{}", "═══════════════════════════════════════════════════════════".bright_green());
println!();
println!("FTC SDK: {}", sdk_config.ftc_sdk_path.display());
println!("Version: {}", crate::sdk::ftc::get_version(&sdk_config.ftc_sdk_path).unwrap_or_else(|_| "unknown".to_string()));
println!();
println!("{}", "Next steps:".bright_yellow().bold());
println!(" 1. cd {}", name);
println!(" 2. Review README.md for project structure");
println!(" 3. Start coding in src/main/java/robot/");
println!(" 4. Run: ./gradlew test");
println!(" 5. Deploy: weevil deploy {}", name);
println!();
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)?;
} 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(())
}

60
src/commands/sdk.rs Normal file
View File

@@ -0,0 +1,60 @@
use anyhow::Result;
use colored::*;
use crate::sdk::SdkConfig;
pub fn install_sdks() -> Result<()> {
println!("{}", "Installing SDKs...".bright_yellow().bold());
println!();
let config = SdkConfig::new()?;
// Install FTC SDK
crate::sdk::ftc::install(&config.ftc_sdk_path)?;
// Install Android SDK
crate::sdk::android::install(&config.android_sdk_path)?;
println!();
println!("{} All SDKs installed successfully", "".green().bold());
config.print_status();
Ok(())
}
pub fn show_status() -> Result<()> {
let config = SdkConfig::new()?;
config.print_status();
// Verify SDKs
println!();
println!("{}", "Verification:".bright_yellow().bold());
match crate::sdk::ftc::verify(&config.ftc_sdk_path) {
Ok(_) => println!("{} FTC SDK is valid", "".green()),
Err(e) => println!("{} FTC SDK: {}", "".red(), e),
}
match crate::sdk::android::verify(&config.android_sdk_path) {
Ok(_) => println!("{} Android SDK is valid", "".green()),
Err(e) => println!("{} Android SDK: {}", "".red(), e),
}
println!();
Ok(())
}
pub fn update_sdks() -> Result<()> {
println!("{}", "Updating SDKs...".bright_yellow().bold());
println!();
let config = SdkConfig::new()?;
// Update FTC SDK
crate::sdk::ftc::update(&config.ftc_sdk_path)?;
println!();
println!("{} SDKs updated successfully", "".green().bold());
Ok(())
}

119
src/commands/upgrade.rs Normal file
View File

@@ -0,0 +1,119 @@
use anyhow::{Result, bail};
use colored::*;
use std::path::PathBuf;
use std::fs;
pub fn upgrade_project(path: &str) -> Result<()> {
let project_path = PathBuf::from(path);
// Verify it's a weevil project (check for old .weevil-version or new .weevil.toml)
let has_old_version = project_path.join(".weevil-version").exists();
let has_config = project_path.join(".weevil.toml").exists();
if !has_old_version && !has_config {
bail!("Not a weevil project: {} (missing .weevil-version or .weevil.toml)", path);
}
println!("{}", format!("Upgrading project: {}", path).bright_yellow());
println!();
// Get SDK config
let sdk_config = crate::sdk::SdkConfig::new()?;
// Load or create project config
let project_config = if has_config {
println!("Found existing .weevil.toml");
crate::project::ProjectConfig::load(&project_path)?
} else {
println!("Creating .weevil.toml (migrating from old format)");
let project_name = project_path.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown");
crate::project::ProjectConfig::new(project_name, sdk_config.ftc_sdk_path.clone())?
};
println!("Current SDK: {}", project_config.ftc_sdk_path.display());
println!("SDK Version: {}", project_config.ftc_sdk_version);
println!();
// Files that are safe to overwrite (infrastructure, not user code)
let safe_to_overwrite = vec![
"build.gradle.kts",
"settings.gradle.kts",
"build.sh",
"build.bat",
"deploy.sh",
"deploy.bat",
"gradlew",
"gradlew.bat",
"gradle/wrapper/gradle-wrapper.properties",
"gradle/wrapper/gradle-wrapper.jar",
".gitignore",
];
println!("{}", "Updating infrastructure files...".bright_yellow());
// Create a modified SDK config that uses the project's configured FTC SDK
let project_sdk_config = crate::sdk::SdkConfig {
ftc_sdk_path: project_config.ftc_sdk_path.clone(),
android_sdk_path: sdk_config.android_sdk_path.clone(),
cache_dir: sdk_config.cache_dir.clone(),
};
// Regenerate safe files using the project's configured SDK
let builder = crate::project::ProjectBuilder::new(
&project_config.project_name,
&project_sdk_config
)?;
// Temporarily create files in a temp location
let temp_dir = tempfile::tempdir()?;
builder.create(temp_dir.path(), &project_sdk_config)?;
// Copy only the safe files
for file in safe_to_overwrite {
let src = temp_dir.path().join(file);
let dst = project_path.join(file);
if src.exists() {
if let Some(parent) = dst.parent() {
fs::create_dir_all(parent)?;
}
fs::copy(&src, &dst)?;
println!(" {} {}", "".green(), file);
// Make executable if needed
#[cfg(unix)]
if file.ends_with(".sh") || file == "gradlew" {
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(&dst)?.permissions();
perms.set_mode(0o755);
fs::set_permissions(&dst, perms)?;
}
}
}
// Save/update the config
project_config.save(&project_path)?;
println!(" {} {}", "".green(), ".weevil.toml");
// Remove old version marker if it exists
let old_version_file = project_path.join(".weevil-version");
if old_version_file.exists() {
fs::remove_file(old_version_file)?;
println!(" {} {}", "".green(), "Removed old .weevil-version");
}
println!();
println!("{}", "═══════════════════════════════════════════════════════════".bright_green());
println!("{}", " ✓ Project Upgraded!".bright_green().bold());
println!("{}", "═══════════════════════════════════════════════════════════".bright_green());
println!();
println!("{}", "Your code in src/ was preserved.".green());
println!("{}", "Build scripts and gradle configuration updated.".green());
println!();
println!("Test it: ./gradlew test");
println!();
Ok(())
}