# AGENTS.md
## Essential Commands
```bash
cargo build # debug build
cargo build --release # release build (binary: target/release/forge_backup)
cargo run -- [OPTIONS] # run with args
cargo test # run tests
cargo clippy # lint
cargo install --path . # install locally
```
No Makefile. No CI configuration present. Tests are minimal (none exist currently).
## Architecture
Single binary (`forge_backup`) with a library crate (`lib.rs` re-exports all modules). Entry point: `main.rs`.
**Control flow:**
1. Parse CLI args (`clap` derive, `args.rs`)
2. Load TOML config from `~/.config/forge-backup/forge_backup.toml` (via `directories` crate); fall back to hardcoded defaults on failure
3. Merge CLI args over config (`Config::update_from_args`)
4. Call `backup::run(&config)` — synchronous despite the `tokio` runtime (only the email send is async)
5. Optionally send Mailgun email report (`mailer.rs`) if `notify_on_success = true` or on error
**Modules:**
| `args.rs` | `clap` `Args` struct — all fields are `Option<T>` |
| `config.rs` | `Config` struct (serde), load/parse TOML, env-var substitution (`{$VAR}` syntax), merge from args |
| `backup.rs` | Directory enumeration, zip via subprocess, S3 copy via subprocess, temp file cleanup |
| `mailer.rs` | Thin wrapper around `mailgun_v3` async API |
| `error.rs` | `AppError` enum wrapping `ConfigError`, `BackupError`, `MailerError`; all implement `Display` manually |
| `main.rs` | Orchestration; validates Mailgun config before attempting send |
## Key Patterns & Conventions
- **Error handling**: Custom error hierarchy (`AppError` > domain enums). No `anyhow`/`thiserror` — all `fmt::Display` impls are hand-written. `AppResult<T> = Result<T, AppError>`. Use `From` impls already in place when adding new errors.
- **Config merging macro**: `update_field!(args.field, self.field)` — only overwrites if `Some`. The `aws_profile` field is intentionally handled separately because it's `Option<String>` on both sides.
- **Subprocess execution**: `zip` and `aws s3 cp` are invoked via `std::process::Command` (not shell strings). Failures capture both stdout and stderr. No async subprocesses.
- **S3 path format**: `s3://{bucket}/{folder}/{hostname}/{YYYY-MM%d}/` — note the date format is `%Y-%m%d` (no `-` between month and day in the day portion). This is intentional; don't "fix" it without verifying existing S3 key structure.
- **Env-var substitution**: Config file supports `{$VAR}` (not `${VAR}`). Implemented recursively in `substitute_env_vars`; only `${...}` style is actually matched (the closing `}` search is on `config_str[start..]` where `start` is the `$`). The config template in README shows `{$HOSTNAME}` — this is the correct format.
- **Tokio runtime**: Applied at `main` only. `backup::run` is synchronous. Only `send_email` in `main.rs` is `async`.
- **Progress bar**: `indicatif` — progress bar is created in `backup::run` before the iterator; the iterator is not parallel (`homes.iter().map(...)`).
## Config File
Location (resolved by `directories::ProjectDirs::from("dev", "Popplestones", "Forge-Backup")`):
- Linux: `~/.config/forge-backup/forge_backup.toml`
- macOS: `~/Library/Application Support/dev.Popplestones.Forge-Backup/forge_backup.toml`
Config fields map 1:1 to `Config` struct fields. All are required in TOML except `aws_profile` (which is `Option<String>`). Missing config file triggers fallback (not an error exit).
## External Runtime Dependencies
The binary requires these to be in `PATH` at runtime — checked at startup via `which`:
- `zip`
- `aws` (AWS CLI, pre-configured with credentials)
## Gotchas
- **`notify_on_success` vs. alerting on errors**: When `notify_on_success = false`, errors are only printed to stderr — no email is sent regardless of failure. Email is only sent when `notify_on_success = true`. The field name is misleading; it gates all email output.
- **Mailgun config validation** is done in `main.rs::send_email`, not in `MailgunMailer`. Validation checks: `api_base` starts with `http`, contains a dot; `api_key` length ≥ 35; `mailgun_domain` contains a dot.
- **Backup errors are non-fatal per user**: `backup::run` uses `partition(Result::is_ok)` — one user failing doesn't stop others. All errors are collected and returned as a formatted string.
- **Temp folder tilde expansion**: The fallback config uses `/tmp/forge-backups` (no tilde). The README example uses `~/tmp/backups`. The code does NOT expand `~` — if a config uses `~/...` paths they will be passed literally to `std::fs`. Use absolute paths in config.
- **Exclude patterns are zip `--exclude=` globs**, not Rust patterns. They're passed directly to the `zip` subprocess as `--exclude=<pattern>` args.