Files
anvil/src/commands/new.rs
Eric Ratliff 8fe1ef0e27 Refactor CLI, add refresh command, fix port detection, add device tracking
- 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
2026-02-18 20:32:42 -06:00

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