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

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