- Remove build/upload/monitor subcommands (projects are self-contained) - Remove ctrlc dependency (only used by removed monitor watch mode) - Update next-steps messaging to reference project scripts directly - Add 'anvil refresh [DIR] [--force]' to update project scripts to latest templates without touching user code - Fix Windows port detection: replace fragile findstr/batch TOML parsing with proper comment-skipping logic; add _detect_port.ps1 helper for reliable JSON-based port detection via PowerShell - Add .anvil.local for machine-specific config (gitignored) - 'anvil devices --set [PORT] [-d DIR]' saves port + VID:PID - 'anvil devices --get [-d DIR]' shows saved port status - VID:PID tracks USB devices across COM port reassignment - Port resolution: -p flag > VID:PID > saved port > auto-detect - Uppercase normalization for Windows COM port names - Update all .bat/.sh templates to read from .anvil.local - Remove port entries from .anvil.toml (no machine-specific config in git) - Add .anvil.local to .gitignore template - Expand 'anvil devices' output with VID:PID, serial number, and usage instructions
301 lines
7.8 KiB
Rust
301 lines
7.8 KiB
Rust
use anyhow::{Result, bail};
|
|
use colored::*;
|
|
use std::path::PathBuf;
|
|
|
|
use crate::templates::{TemplateManager, TemplateContext};
|
|
use crate::version::ANVIL_VERSION;
|
|
|
|
pub fn list_templates() -> Result<()> {
|
|
println!("{}", "Available templates:".bright_cyan().bold());
|
|
println!();
|
|
|
|
for info in TemplateManager::list_templates() {
|
|
let marker = if info.is_default { " (default)" } else { "" };
|
|
println!(
|
|
" {}{}",
|
|
info.name.bright_white().bold(),
|
|
marker.bright_cyan()
|
|
);
|
|
println!(" {}", info.description);
|
|
println!();
|
|
}
|
|
|
|
println!("{}", "Usage:".bright_yellow().bold());
|
|
println!(" anvil new <project-name>");
|
|
println!(" anvil new <project-name> --template basic");
|
|
println!();
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn create_project(name: &str, template: Option<&str>) -> Result<()> {
|
|
// Validate project name
|
|
validate_project_name(name)?;
|
|
|
|
let project_path = PathBuf::from(name);
|
|
if project_path.exists() {
|
|
bail!(
|
|
"Directory already exists: {}\n\
|
|
Choose a different name or remove the existing directory.",
|
|
project_path.display()
|
|
);
|
|
}
|
|
|
|
let template_name = template.unwrap_or("basic");
|
|
if !TemplateManager::template_exists(template_name) {
|
|
println!(
|
|
"{}",
|
|
format!("Template '{}' not found.", template_name).red().bold()
|
|
);
|
|
println!();
|
|
list_templates()?;
|
|
bail!("Invalid template");
|
|
}
|
|
|
|
println!(
|
|
"{}",
|
|
format!("Creating Arduino project: {}", name)
|
|
.bright_green()
|
|
.bold()
|
|
);
|
|
println!("{}", format!("Template: {}", template_name).bright_cyan());
|
|
println!();
|
|
|
|
// Create project directory
|
|
std::fs::create_dir_all(&project_path)?;
|
|
|
|
// Extract template
|
|
println!("{}", "Extracting template files...".bright_yellow());
|
|
let context = TemplateContext {
|
|
project_name: name.to_string(),
|
|
anvil_version: ANVIL_VERSION.to_string(),
|
|
};
|
|
|
|
let file_count = TemplateManager::extract(template_name, &project_path, &context)?;
|
|
println!("{} Extracted {} files", "ok".green(), file_count);
|
|
|
|
// Make shell scripts executable on Unix
|
|
#[cfg(unix)]
|
|
{
|
|
make_executable(&project_path);
|
|
}
|
|
|
|
// Initialize git repository
|
|
init_git(&project_path, template_name);
|
|
|
|
// Print success
|
|
println!();
|
|
println!(
|
|
"{}",
|
|
"================================================================"
|
|
.bright_green()
|
|
);
|
|
println!(
|
|
"{}",
|
|
format!(" Project created: {}", name)
|
|
.bright_green()
|
|
.bold()
|
|
);
|
|
println!(
|
|
"{}",
|
|
"================================================================"
|
|
.bright_green()
|
|
);
|
|
println!();
|
|
|
|
print_next_steps(name);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn validate_project_name(name: &str) -> Result<()> {
|
|
if name.is_empty() {
|
|
bail!("Project name cannot be empty");
|
|
}
|
|
if name.len() > 50 {
|
|
bail!("Project name must be 50 characters or less");
|
|
}
|
|
if !name.chars().next().unwrap().is_alphabetic() {
|
|
bail!("Project name must start with a letter");
|
|
}
|
|
let valid = name
|
|
.chars()
|
|
.all(|c| c.is_alphanumeric() || c == '-' || c == '_');
|
|
if !valid {
|
|
bail!(
|
|
"Invalid project name '{}'\n\
|
|
Names may contain: letters, numbers, hyphens, underscores\n\
|
|
Must start with a letter.\n\n\
|
|
Valid examples:\n\
|
|
\x20 blink\n\
|
|
\x20 my-sensor\n\
|
|
\x20 team1234_robot",
|
|
name
|
|
);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn init_git(project_dir: &PathBuf, template_name: &str) {
|
|
println!("{}", "Initializing git repository...".bright_yellow());
|
|
|
|
// Check if git is available
|
|
if which::which("git").is_err() {
|
|
eprintln!(
|
|
"{} git not found, skipping repository initialization.",
|
|
"warn".yellow()
|
|
);
|
|
return;
|
|
}
|
|
|
|
let run = |args: &[&str]| -> bool {
|
|
std::process::Command::new("git")
|
|
.args(args)
|
|
.current_dir(project_dir)
|
|
.output()
|
|
.map(|o| o.status.success())
|
|
.unwrap_or(false)
|
|
};
|
|
|
|
if !run(&["init"]) {
|
|
eprintln!("{} Failed to initialize git.", "warn".yellow());
|
|
return;
|
|
}
|
|
|
|
run(&["add", "."]);
|
|
|
|
let msg = format!(
|
|
"Initial commit from anvil new --template {}",
|
|
template_name
|
|
);
|
|
run(&["commit", "-m", &msg]);
|
|
|
|
println!("{} Git repository initialized", "ok".green());
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
fn make_executable(project_dir: &PathBuf) {
|
|
use std::os::unix::fs::PermissionsExt;
|
|
|
|
let scripts = [
|
|
"build.sh",
|
|
"upload.sh",
|
|
"monitor.sh",
|
|
"test/run_tests.sh",
|
|
];
|
|
for script in &scripts {
|
|
let path = project_dir.join(script);
|
|
if path.exists() {
|
|
if let Ok(meta) = std::fs::metadata(&path) {
|
|
let mut perms = meta.permissions();
|
|
perms.set_mode(0o755);
|
|
let _ = std::fs::set_permissions(&path, perms);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn print_next_steps(project_name: &str) {
|
|
println!("{}", "Next steps:".bright_yellow().bold());
|
|
println!(
|
|
" 1. {}",
|
|
format!("cd {}", project_name).bright_cyan()
|
|
);
|
|
|
|
if cfg!(target_os = "windows") {
|
|
println!(
|
|
" 2. Compile: {}",
|
|
"build.bat".bright_cyan()
|
|
);
|
|
println!(
|
|
" 3. Upload to board: {}",
|
|
"upload.bat".bright_cyan()
|
|
);
|
|
println!(
|
|
" 4. Upload + monitor: {}",
|
|
"upload.bat --monitor".bright_cyan()
|
|
);
|
|
println!(
|
|
" 5. Serial monitor: {}",
|
|
"monitor.bat".bright_cyan()
|
|
);
|
|
println!(
|
|
" 6. Run host tests: {}",
|
|
"test\\run_tests.bat".bright_cyan()
|
|
);
|
|
println!();
|
|
println!(
|
|
" {}",
|
|
"On Linux/macOS: ./build.sh, ./upload.sh, ./monitor.sh"
|
|
.bright_black()
|
|
);
|
|
} else {
|
|
println!(
|
|
" 2. Compile: {}",
|
|
"./build.sh".bright_cyan()
|
|
);
|
|
println!(
|
|
" 3. Upload to board: {}",
|
|
"./upload.sh".bright_cyan()
|
|
);
|
|
println!(
|
|
" 4. Upload + monitor: {}",
|
|
"./upload.sh --monitor".bright_cyan()
|
|
);
|
|
println!(
|
|
" 5. Serial monitor: {}",
|
|
"./monitor.sh".bright_cyan()
|
|
);
|
|
println!(
|
|
" 6. Run host tests: {}",
|
|
"./test/run_tests.sh".bright_cyan()
|
|
);
|
|
println!();
|
|
println!(
|
|
" {}",
|
|
"On Windows: build.bat, upload.bat, monitor.bat, test\\run_tests.bat"
|
|
.bright_black()
|
|
);
|
|
}
|
|
|
|
println!(
|
|
" {}",
|
|
"System check: anvil doctor | Port scan: anvil devices"
|
|
.bright_black()
|
|
);
|
|
println!();
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_validate_name_valid() {
|
|
assert!(validate_project_name("blink").is_ok());
|
|
assert!(validate_project_name("my-sensor").is_ok());
|
|
assert!(validate_project_name("team1234_robot").is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_name_empty() {
|
|
assert!(validate_project_name("").is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_name_starts_with_number() {
|
|
assert!(validate_project_name("123abc").is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_name_special_chars() {
|
|
assert!(validate_project_name("my project").is_err());
|
|
assert!(validate_project_name("my.project").is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_name_too_long() {
|
|
let long_name = "a".repeat(51);
|
|
assert!(validate_project_name(&long_name).is_err());
|
|
}
|
|
} |