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
This commit is contained in:
191
src/commands/refresh.rs
Normal file
191
src/commands/refresh.rs
Normal file
@@ -0,0 +1,191 @@
|
||||
use anyhow::{Result, Context};
|
||||
use colored::*;
|
||||
use std::path::PathBuf;
|
||||
use std::fs;
|
||||
|
||||
use crate::project::config::ProjectConfig;
|
||||
use crate::templates::{TemplateManager, TemplateContext};
|
||||
use crate::version::ANVIL_VERSION;
|
||||
|
||||
/// Files that anvil owns and can safely refresh.
|
||||
/// These are build/deploy infrastructure -- not user source code.
|
||||
const REFRESHABLE_FILES: &[&str] = &[
|
||||
"build.sh",
|
||||
"build.bat",
|
||||
"upload.sh",
|
||||
"upload.bat",
|
||||
"monitor.sh",
|
||||
"monitor.bat",
|
||||
"_detect_port.ps1",
|
||||
"test/run_tests.sh",
|
||||
"test/run_tests.bat",
|
||||
];
|
||||
|
||||
pub fn run_refresh(project_dir: Option<&str>, force: bool) -> Result<()> {
|
||||
// Resolve project directory
|
||||
let project_path = match project_dir {
|
||||
Some(dir) => PathBuf::from(dir),
|
||||
None => std::env::current_dir()
|
||||
.context("Could not determine current directory")?,
|
||||
};
|
||||
|
||||
let project_root = ProjectConfig::find_project_root(&project_path)?;
|
||||
let config = ProjectConfig::load(&project_root)?;
|
||||
|
||||
println!(
|
||||
"Refreshing project: {}",
|
||||
config.project.name.bright_white().bold()
|
||||
);
|
||||
println!(
|
||||
"Project directory: {}",
|
||||
project_root.display().to_string().bright_black()
|
||||
);
|
||||
println!();
|
||||
|
||||
// Generate fresh copies of all refreshable files from the template
|
||||
let template_name = "basic";
|
||||
let context = TemplateContext {
|
||||
project_name: config.project.name.clone(),
|
||||
anvil_version: ANVIL_VERSION.to_string(),
|
||||
};
|
||||
|
||||
// Extract template into a temp directory so we can compare
|
||||
let temp_dir = tempfile::tempdir()
|
||||
.context("Failed to create temp directory")?;
|
||||
TemplateManager::extract(template_name, temp_dir.path(), &context)?;
|
||||
|
||||
// Compare each refreshable file
|
||||
let mut up_to_date = Vec::new();
|
||||
let mut will_create = Vec::new();
|
||||
let mut has_changes = Vec::new();
|
||||
|
||||
for &filename in REFRESHABLE_FILES {
|
||||
let existing = project_root.join(filename);
|
||||
let fresh = temp_dir.path().join(filename);
|
||||
|
||||
if !fresh.exists() {
|
||||
// Template doesn't produce this file (shouldn't happen)
|
||||
continue;
|
||||
}
|
||||
|
||||
let fresh_content = fs::read(&fresh)
|
||||
.context(format!("Failed to read template file: {}", filename))?;
|
||||
|
||||
if !existing.exists() {
|
||||
will_create.push(filename);
|
||||
continue;
|
||||
}
|
||||
|
||||
let existing_content = fs::read(&existing)
|
||||
.context(format!("Failed to read project file: {}", filename))?;
|
||||
|
||||
if existing_content == fresh_content {
|
||||
up_to_date.push(filename);
|
||||
} else {
|
||||
has_changes.push(filename);
|
||||
}
|
||||
}
|
||||
|
||||
// Report status
|
||||
if !up_to_date.is_empty() {
|
||||
println!(
|
||||
"{} {} file(s) already up to date",
|
||||
"ok".green(),
|
||||
up_to_date.len()
|
||||
);
|
||||
}
|
||||
|
||||
if !will_create.is_empty() {
|
||||
for f in &will_create {
|
||||
println!(" {} {} (new)", "+".bright_green(), f.bright_white());
|
||||
}
|
||||
}
|
||||
|
||||
if !has_changes.is_empty() {
|
||||
for f in &has_changes {
|
||||
println!(
|
||||
" {} {} (differs from latest)",
|
||||
"~".bright_yellow(),
|
||||
f.bright_white()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Decide what to do
|
||||
if has_changes.is_empty() && will_create.is_empty() {
|
||||
println!();
|
||||
println!(
|
||||
"{}",
|
||||
"All scripts are up to date. Nothing to do."
|
||||
.bright_green()
|
||||
.bold()
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if !has_changes.is_empty() && !force {
|
||||
println!();
|
||||
println!(
|
||||
"{} {} script(s) differ from the latest Anvil templates.",
|
||||
"!".bright_yellow(),
|
||||
has_changes.len()
|
||||
);
|
||||
println!(
|
||||
"This is normal after upgrading Anvil. To update them, run:"
|
||||
);
|
||||
println!();
|
||||
println!(" {}", "anvil refresh --force".bright_cyan());
|
||||
println!();
|
||||
println!(
|
||||
" {}",
|
||||
"Only build scripts are replaced. Your .anvil.toml and source code are never touched."
|
||||
.bright_black()
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Apply updates
|
||||
let files_to_write: Vec<&str> = if force {
|
||||
will_create.iter().chain(has_changes.iter()).copied().collect()
|
||||
} else {
|
||||
will_create.to_vec()
|
||||
};
|
||||
|
||||
for filename in &files_to_write {
|
||||
let fresh = temp_dir.path().join(filename);
|
||||
let dest = project_root.join(filename);
|
||||
|
||||
// Ensure parent directory exists
|
||||
if let Some(parent) = dest.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
|
||||
fs::copy(&fresh, &dest)
|
||||
.context(format!("Failed to write: {}", filename))?;
|
||||
}
|
||||
|
||||
// Make shell scripts executable on Unix
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
for filename in &files_to_write {
|
||||
if filename.ends_with(".sh") {
|
||||
let path = project_root.join(filename);
|
||||
if let Ok(meta) = fs::metadata(&path) {
|
||||
let mut perms = meta.permissions();
|
||||
perms.set_mode(0o755);
|
||||
let _ = fs::set_permissions(&path, perms);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
println!(
|
||||
"{} Updated {} file(s).",
|
||||
"ok".green(),
|
||||
files_to_write.len()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user