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

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(())
}