Layer 3: Templates as pure data, weather template, .anvilignore refresh system

Templates are now composed declaratively via template.toml -- no Rust code
changes needed to add new templates. The weather station is the first
composed template, demonstrating the full pattern.

Template engine:
- Composed templates declare base, required libraries, and per-board pins
- Overlay mechanism replaces base files (app, sketch, tests) cleanly
- Generic orchestration: extract base, apply overlay, install libs, assign pins
- Template name tracked in .anvil.toml for refresh awareness

Weather template (--template weather):
- WeatherApp with 2-second polling, C/F conversion, serial output
- TMP36 driver: TempSensor interface, Tmp36 impl, Tmp36Mock, Tmp36Sim
- Managed example tests in test_weather.cpp (unit + system)
- Minimal student starters in test_unit.cpp and test_system.cpp
- Per-board pin defaults (A0 for uno, A0 for mega, A0 for nano)

.anvilignore system:
- Glob pattern matching (*, ?) with comments and backslash normalization
- Default patterns protect student tests, app code, sketch, config
- anvil refresh --force respects .anvilignore
- anvil refresh --force --file <path> overrides ignore for one file
- anvil refresh --ignore/--unignore manages patterns from CLI
- Missing managed files always recreated even without --force
- .anvilignore itself is in NEVER_REFRESH (cannot be overwritten)

Refresh rewrite:
- Discovers all template-produced files dynamically (no hardcoded list)
- Extracts fresh template + libraries into temp dir for byte comparison
- Config template field drives which files are managed
- Separated missing-file creation from changed-file updates

428 tests passing on Windows MSVC, 0 warnings.
This commit is contained in:
Eric Ratliff
2026-02-21 20:52:48 -06:00
parent 0abe907811
commit ca855dd3af
17 changed files with 5190 additions and 236 deletions

View File

@@ -5,6 +5,41 @@ use std::path::PathBuf;
use crate::library;
use crate::project::config::ProjectConfig;
/// Install a library quietly -- extract files and update config only.
/// Used by the template orchestrator during `anvil new`.
/// Returns the list of written file paths relative to project root.
pub fn install_library(
name: &str,
project_dir: &std::path::Path,
) -> Result<Vec<String>> {
let meta = library::find_library(name).ok_or_else(|| {
anyhow::anyhow!("Unknown library: '{}'", name)
})?;
let project_root = ProjectConfig::find_project_root(project_dir)?;
let mut config = ProjectConfig::load(&project_root)?;
// Skip if already installed
if config.libraries.contains_key(name) {
return Ok(vec![]);
}
// Extract files
let written = library::extract_library(name, &project_root)?;
// Add to include_dirs if not present
let driver_include = format!("lib/drivers/{}", name);
if !config.build.include_dirs.contains(&driver_include) {
config.build.include_dirs.push(driver_include);
}
// Track in config
config.libraries.insert(name.to_string(), meta.version.clone());
config.save(&project_root)?;
Ok(written)
}
/// Add a library to the current project.
pub fn add_library(name: &str, pin: Option<&str>, project_dir: Option<&str>) -> Result<()> {
let meta = library::find_library(name)

View File

@@ -2,6 +2,7 @@ use anyhow::{Result, bail};
use colored::*;
use std::path::PathBuf;
use crate::board::pinmap;
use crate::board::presets::{self, BoardPreset};
use crate::templates::{TemplateManager, TemplateContext};
use crate::version::ANVIL_VERSION;
@@ -18,12 +19,43 @@ pub fn list_templates() -> Result<()> {
marker.bright_cyan()
);
println!(" {}", info.description);
if !info.libraries.is_empty() {
// Show what drivers are included and their wiring needs
let mut wiring_parts: Vec<String> = Vec::new();
for lib_name in &info.libraries {
if let Some(meta) = crate::library::find_library(lib_name) {
wiring_parts.push(format!(
"{} ({})",
meta.name, meta.wiring_summary()
));
}
}
println!(
" Includes: {}",
wiring_parts.join(", ").bright_yellow()
);
}
// Show the create command for non-default templates
if !info.is_default {
println!(
" Create: {}",
format!(
"anvil new <project-name> --template {}",
info.name
)
.bright_cyan()
);
}
println!();
}
println!("{}", "Usage:".bright_yellow().bold());
println!(" anvil new <project-name>");
println!(" anvil new <project-name> --template basic");
println!(
" anvil new <project-name> --template <name> --board <board>"
);
println!();
Ok(())
@@ -58,12 +90,10 @@ pub fn list_boards() -> Result<()> {
println!();
println!(
" {}",
"For boards not listed here, create a project and then:".bright_black()
);
println!(
" {}",
" anvil board --listall".bright_black()
"For boards not listed here, create a project and then:"
.bright_black()
);
println!(" {}", " anvil board --listall".bright_black());
println!();
Ok(())
@@ -90,7 +120,9 @@ pub fn create_project(
if !TemplateManager::template_exists(template_name) {
println!(
"{}",
format!("Template '{}' not found.", template_name).red().bold()
format!("Template '{}' not found.", template_name)
.red()
.bold()
);
println!();
list_templates()?;
@@ -99,40 +131,47 @@ pub fn create_project(
// Resolve board preset
let preset: &BoardPreset = match board {
Some(b) => {
match presets::find_preset(b) {
Some(p) => p,
None => {
println!(
"{}",
format!("Unknown board preset: '{}'", b).red().bold()
);
println!();
list_boards()?;
bail!("Invalid board preset");
}
Some(b) => match presets::find_preset(b) {
Some(p) => p,
None => {
println!(
"{}",
format!("Unknown board preset: '{}'", b).red().bold()
);
println!();
list_boards()?;
bail!("Invalid board preset");
}
}
},
None => presets::find_preset(presets::DEFAULT_PRESET).unwrap(),
};
// Check board compatibility for composed templates (warn, never block)
if let Some(meta) = TemplateManager::composed_meta(template_name) {
check_board_compatibility(preset, &meta.board_capabilities);
}
println!(
"{}",
format!("Creating Arduino project: {}", name)
.bright_green()
.bold()
);
println!("{}", format!("Template: {}", template_name).bright_cyan());
println!(
"{}",
format!("Board: {} ({})", preset.name, preset.description).bright_cyan()
format!("Template: {}", template_name).bright_cyan()
);
println!(
"{}",
format!("Board: {} ({})", preset.name, preset.description)
.bright_cyan()
);
println!();
// Create project directory
std::fs::create_dir_all(&project_path)?;
// Extract template
// Extract template (basic scaffold + overlay for composed templates)
println!("{}", "Extracting template files...".bright_yellow());
let context = TemplateContext {
project_name: name.to_string(),
@@ -142,9 +181,85 @@ pub fn create_project(
baud: preset.baud,
};
let file_count = TemplateManager::extract(template_name, &project_path, &context)?;
let file_count =
TemplateManager::extract(template_name, &project_path, &context)?;
println!("{} Extracted {} files", "ok".green(), file_count);
// Record which template was used (for refresh to know what to manage)
{
let mut config =
crate::project::config::ProjectConfig::load(&project_path)?;
config.project.template = template_name.to_string();
config.save(&project_path)?;
}
// Generate .anvilignore with sensible defaults
crate::ignore::generate_default(&project_path, template_name)?;
// For composed templates: install libraries and assign pins
if let Some(meta) = TemplateManager::composed_meta(template_name) {
// Install required libraries
for lib_name in &meta.libraries {
println!();
println!(
"{}",
format!("Installing library: {}...", lib_name)
.bright_yellow()
);
let written =
crate::commands::lib::install_library(lib_name, &project_path)?;
for f in &written {
println!(" {} {}", "+".bright_green(), f.bright_white());
}
println!("{} {} installed", "ok".green(), lib_name);
}
// Assign default pins for this board
let pin_defaults = meta.pins_for_board(preset.name);
if !pin_defaults.is_empty() {
println!();
println!(
"{}",
"Assigning default pin mappings...".bright_yellow()
);
for pin_def in &pin_defaults {
match crate::commands::pin::install_pin_assignment(
&pin_def.name,
&pin_def.pin,
&pin_def.mode,
preset.name,
&project_path,
) {
Ok(()) => {
println!(
" {} {} -> {} [{}]",
"ok".green(),
pin_def.name.bold(),
pin_def.pin,
pin_def.mode
);
}
Err(e) => {
println!(
" {} {}: {}",
"!!".red(),
pin_def.name,
e
);
println!(
" Assign manually: {}",
format!(
"anvil pin --assign {} {} --mode {}",
pin_def.name, pin_def.pin, pin_def.mode
)
.bright_cyan()
);
}
}
}
}
}
// Make shell scripts executable on Unix
#[cfg(unix)]
{
@@ -174,11 +289,52 @@ pub fn create_project(
);
println!();
print_next_steps(name);
print_next_steps(name, template_name);
Ok(())
}
/// Check if a board supports the required capabilities and warn if not.
fn check_board_compatibility(
preset: &BoardPreset,
required: &[String],
) {
if required.is_empty() {
return;
}
let pm = pinmap::find_pinmap(preset.name);
if pm.is_none() {
// No pinmap data for this board -- can't check
return;
}
let pm = pm.unwrap();
let caps = pinmap::board_capabilities(pm);
for req in required {
if !caps.contains(&req.as_str()) {
println!(
"{}",
format!(
"note: This template needs '{}' pins, which {} \
may not support.",
req, preset.name
)
.bright_yellow()
);
println!(
" {}",
"Your tests will still work (mocks don't need hardware)."
.bright_black()
);
println!(
" {}",
"You may need to adjust pin assignments for your board."
.bright_black()
);
println!();
}
}
}
fn validate_project_name(name: &str) -> Result<()> {
if name.is_empty() {
bail!("Project name cannot be empty");
@@ -266,67 +422,103 @@ fn make_executable(project_dir: &PathBuf) {
}
}
fn print_next_steps(project_name: &str) {
fn print_next_steps(project_name: &str, template_name: &str) {
println!("{}", "Next steps:".bright_yellow().bold());
println!(
" 1. {}",
format!("cd {}", project_name).bright_cyan()
);
if cfg!(target_os = "windows") {
// For composed templates, wiring is step 2
if template_name != "basic" {
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()
" 2. Review wiring: {}",
"anvil pin --audit".bright_cyan()
);
if cfg!(target_os = "windows") {
println!(
" 3. Run host tests: {}",
"test\\run_tests.bat --clean".bright_cyan()
);
println!(
" 4. Compile: {}",
"build.bat".bright_cyan()
);
println!(
" 5. Upload: {}",
"upload.bat --monitor".bright_cyan()
);
} else {
println!(
" 3. Run host tests: {}",
"./test/run_tests.sh --clean".bright_cyan()
);
println!(
" 4. Compile: {}",
"./build.sh".bright_cyan()
);
println!(
" 5. Upload: {}",
"./upload.sh --monitor".bright_cyan()
);
}
} 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()
);
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!(

View File

@@ -237,6 +237,43 @@ pub fn assign_pin(
Ok(())
}
/// Assign a pin quietly -- validate, write config, no printing.
/// Used by the template orchestrator during `anvil new`.
pub fn install_pin_assignment(
name: &str,
pin_str: &str,
mode: &str,
board_name: &str,
project_dir: &std::path::Path,
) -> Result<()> {
let project_root = ProjectConfig::find_project_root(project_dir)?;
let pinmap = require_pinmap(board_name)?;
let pin_num = pinmap::resolve_alias(pinmap, pin_str).ok_or_else(|| {
anyhow::anyhow!(
"Pin '{}' not found on board '{}'",
pin_str,
board_name
)
})?;
if !ALL_MODES.contains(&mode) {
bail!("Unknown pin mode: '{}'", mode);
}
let pin_info = pinmap::get_pin(pinmap, pin_num).unwrap();
validate_mode_for_pin(pin_info, mode)?;
validate_pin_name(name)?;
let assignment = PinAssignment {
pin: pin_num,
mode: mode.to_string(),
};
write_pin_assignment(&project_root, board_name, name, &assignment)?;
Ok(())
}
// =========================================================================
// Assign a bus group
// =========================================================================

View File

@@ -1,39 +1,46 @@
use anyhow::{Result, Context};
use colored::*;
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use std::fs;
use crate::ignore::{self, AnvilIgnore};
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",
"test.sh",
"test.bat",
"_detect_port.ps1",
"_monitor_filter.ps1",
"test/run_tests.sh",
"test/run_tests.bat",
"test/CMakeLists.txt",
"test/mocks/mock_arduino.h",
"test/mocks/mock_arduino.cpp",
"test/mocks/Arduino.h",
"test/mocks/Wire.h",
"test/mocks/SPI.h",
"test/mocks/mock_hal.h",
"test/mocks/sim_hal.h",
/// Files that are never refreshable, even if they appear in the template.
/// .anvilignore itself is the user's protection config -- never overwrite it.
const NEVER_REFRESH: &[&str] = &[
".anvilignore",
];
pub fn run_refresh(project_dir: Option<&str>, force: bool) -> Result<()> {
// Resolve project directory
/// Recursively walk a directory and return all file paths relative to `base`.
/// Uses forward-slash separators regardless of platform.
fn discover_files(base: &Path, dir: &Path) -> Vec<String> {
let mut files = Vec::new();
let entries = match fs::read_dir(dir) {
Ok(e) => e,
Err(_) => return files,
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
files.extend(discover_files(base, &path));
} else if let Ok(rel) = path.strip_prefix(base) {
let rel_str = rel.to_string_lossy().replace('\\', "/");
files.push(rel_str);
}
}
files.sort();
files
}
/// Main refresh entry point.
pub fn run_refresh(
project_dir: Option<&str>,
force: bool,
file_override: Option<&str>,
) -> Result<()> {
let project_path = match project_dir {
Some(dir) => PathBuf::from(dir),
None => std::env::current_dir()
@@ -42,61 +49,101 @@ pub fn run_refresh(project_dir: Option<&str>, force: bool) -> Result<()> {
let project_root = ProjectConfig::find_project_root(&project_path)?;
let config = ProjectConfig::load(&project_root)?;
let anvilignore = AnvilIgnore::load(&project_root)?;
let template_name = &config.project.template;
println!(
"Refreshing project: {}",
config.project.name.bright_white().bold()
);
println!(
"Template: {}",
template_name.bright_cyan()
);
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";
// Build template context for extraction
let context = TemplateContext {
project_name: config.project.name.clone(),
anvil_version: ANVIL_VERSION.to_string(),
board_name: config.build.default.clone(),
fqbn: config.default_fqbn().unwrap_or_else(|_| "arduino:avr:uno".to_string()),
fqbn: config
.default_fqbn()
.unwrap_or_else(|_| "arduino:avr:uno".to_string()),
baud: config.monitor.baud,
};
// Extract template into a temp directory so we can compare
let temp_dir = tempfile::tempdir()
.context("Failed to create temp directory")?;
// Extract fresh template + libraries into temp directory
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();
// Also extract library files into temp (for composed templates)
for lib_name in config.libraries.keys() {
let _ = crate::library::extract_library(lib_name, temp_dir.path());
}
// Discover ALL files the template produces
let all_template_files = discover_files(temp_dir.path(), temp_dir.path());
// Normalize the --file override
let file_override_norm = file_override.map(|f| f.replace('\\', "/"));
// Compare each template-produced file
let mut up_to_date = Vec::new();
let mut will_create: Vec<String> = Vec::new();
let mut has_changes: Vec<String> = Vec::new();
let mut ignored: Vec<(String, String)> = Vec::new(); // (file, pattern)
for filename in &all_template_files {
// Skip files that are never refreshable
if NEVER_REFRESH.iter().any(|&nr| nr == filename) {
continue;
}
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;
}
// Check .anvilignore (unless this file is the --file override)
let is_override = file_override_norm
.as_ref()
.map(|f| f == filename)
.unwrap_or(false);
if !is_override {
if let Some(pattern) = anvilignore.matching_pattern(filename) {
ignored.push((
filename.clone(),
pattern.to_string(),
));
continue;
}
}
let fresh_content = fs::read(&fresh)
.context(format!("Failed to read template file: {}", filename))?;
.context(format!("Failed to read template: {}", filename))?;
if !existing.exists() {
will_create.push(filename);
will_create.push(filename.clone());
continue;
}
let existing_content = fs::read(&existing)
.context(format!("Failed to read project file: {}", filename))?;
.context(format!("Failed to read project: {}", filename))?;
if existing_content == fresh_content {
up_to_date.push(filename);
up_to_date.push(filename.clone());
} else {
has_changes.push(filename);
has_changes.push(filename.clone());
}
}
@@ -111,7 +158,11 @@ pub fn run_refresh(project_dir: Option<&str>, force: bool) -> Result<()> {
if !will_create.is_empty() {
for f in &will_create {
println!(" {} {} (new)", "+".bright_green(), f.bright_white());
println!(
" {} {} (new)",
"+".bright_green(),
f.bright_white()
);
}
}
@@ -125,22 +176,90 @@ pub fn run_refresh(project_dir: Option<&str>, force: bool) -> Result<()> {
}
}
// Report ignored files -- always, so the user knows
if !ignored.is_empty() {
println!();
println!(
"{} {} file(s) protected by .anvilignore:",
"skip".bright_black(),
ignored.len()
);
for (file, pattern) in &ignored {
if file == pattern {
println!(
" {} {} (ignored)",
"-".bright_black(),
file.bright_black()
);
} else {
println!(
" {} {} (matches: {})",
"-".bright_black(),
file.bright_black(),
pattern.bright_black()
);
}
}
if file_override.is_none() {
println!(
" {}",
"Override one: anvil refresh --force --file <path>"
.bright_black()
);
}
}
// Decide what to do
if has_changes.is_empty() && will_create.is_empty() {
println!();
println!(
"{}",
"All scripts are up to date. Nothing to do."
"All managed files are up to date. Nothing to do."
.bright_green()
.bold()
);
return Ok(());
}
// Always create missing files (they are not conflicting, just absent)
if !will_create.is_empty() {
for filename in &will_create {
let fresh = temp_dir.path().join(filename);
let dest = project_root.join(filename);
if let Some(parent) = dest.parent() {
fs::create_dir_all(parent)?;
}
fs::copy(&fresh, &dest)
.context(format!("Failed to write: {}", filename))?;
}
// Make new shell scripts executable on Unix
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
for filename in &will_create {
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!(
"{} Created {} missing file(s).",
"ok".green(),
will_create.len()
);
}
if !has_changes.is_empty() && !force {
println!();
println!(
"{} {} script(s) differ from the latest Anvil templates.",
"{} {} file(s) differ from the latest Anvil templates.",
"!".bright_yellow(),
has_changes.len()
);
@@ -152,54 +271,120 @@ pub fn run_refresh(project_dir: Option<&str>, force: bool) -> Result<()> {
println!();
println!(
" {}",
"Only build scripts are replaced. Your .anvil.toml and source code are never touched."
"Only managed files are replaced. Your source code \
and .anvilignore files 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)?;
// Apply changed file updates (requires --force)
if !has_changes.is_empty() {
for filename in &has_changes {
let fresh = temp_dir.path().join(filename);
let dest = project_root.join(filename);
if let Some(parent) = dest.parent() {
fs::create_dir_all(parent)?;
}
fs::copy(&fresh, &dest)
.context(format!("Failed to write: {}", filename))?;
}
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 &has_changes {
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!(
"{} Updated {} file(s).",
"ok".green(),
has_changes.len()
);
}
// 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);
}
Ok(())
}
/// Add a pattern to .anvilignore.
pub fn add_ignore(project_dir: Option<&str>, pattern: &str) -> Result<()> {
let project_path = match project_dir {
Some(dir) => PathBuf::from(dir),
None => std::env::current_dir()?,
};
let project_root = ProjectConfig::find_project_root(&project_path)?;
// Check if already present
let anvilignore = AnvilIgnore::load(&project_root)?;
let normalized = pattern.replace('\\', "/");
if anvilignore.has_pattern(&normalized) {
println!(
"{} Pattern already in .anvilignore: {}",
"ok".green(),
normalized.bright_white()
);
return Ok(());
}
ignore::add_pattern(&project_root, pattern)?;
println!(
"{} Added to .anvilignore: {}",
"ok".green(),
normalized.bright_white().bold()
);
Ok(())
}
/// Remove a pattern from .anvilignore.
pub fn remove_ignore(
project_dir: Option<&str>,
pattern: &str,
) -> Result<()> {
let project_path = match project_dir {
Some(dir) => PathBuf::from(dir),
None => std::env::current_dir()?,
};
let project_root = ProjectConfig::find_project_root(&project_path)?;
let removed = ignore::remove_pattern(&project_root, pattern)?;
let normalized = pattern.replace('\\', "/");
if removed {
println!(
"{} Removed from .anvilignore: {}",
"ok".green(),
normalized.bright_white().bold()
);
println!(
" {}",
"This file will now be updated by anvil refresh --force."
.bright_black()
);
} else {
println!(
"{} Pattern not found in .anvilignore: {}",
"!".bright_yellow(),
normalized.bright_white()
);
// Show current patterns
let anvilignore = AnvilIgnore::load(&project_root)?;
if !anvilignore.patterns().is_empty() {
println!(" Current patterns:");
for p in anvilignore.patterns() {
println!(" {}", p.bright_black());
}
}
}
println!();
println!(
"{} Updated {} file(s).",
"ok".green(),
files_to_write.len()
);
Ok(())
}