Architecture
Design Principles
- Registry-driven: a single
ConfigEntry[]array is the source of truth for all config paths, collection, backup, and restore - Safe by default: sensitivity scanning runs on every collect and backup unless explicitly disabled
- Graceful degradation: missing tools, files, or directories are silently skipped — never hard-fail on optional data
- Parallel where possible: independent collectors run via
Promise.allSettled - Platform-aware: per-OS paths in registry, platform guards on OS-specific collectors
Project Structure
dotfiles/
├── bin/
│ └── dotfiles.ts # Entry point — Bun runtime check, imports src/cli.ts
│
├── src/
│ ├── cli.ts # Command router — switch on argv[2], delegates to commands/
│ │
│ ├── commands/ # One file per CLI command
│ │ ├── collect.ts # .dotf snapshot generation (parallel collectors → scan → stringify)
│ │ ├── backup.ts # Structured file backup (registry sources → scan → copy)
│ │ ├── scan.ts # Standalone sensitivity scanner (file or directory)
│ │ ├── restore.ts # Restore from backup (plan → pick → execute)
│ │ ├── diff.ts # Backup vs live comparison (color-coded, TTY-aware)
│ │ ├── status.ts # Quick backup summary (age, counts, modified list)
│ │ ├── compare.ts # Diff two .dotf files (via @dotformat/core)
│ │ └── list.ts # Fuzzy section query from latest report
│ │
│ ├── registry/ # Config source definitions (single source of truth)
│ │ ├── types.ts # ConfigEntry, Platform, EntryKind type definitions
│ │ ├── entries.ts # 23 config entries — all file-based configs
│ │ ├── resolve.ts # resolvePath() and getEntriesForPlatform()
│ │ ├── collector.ts # registryCollector() — generates Collector from entries
│ │ ├── backup.ts # registryBackupSources() — generates BackupSource[] from entries
│ │ └── index.ts # Public re-exports
│ │
│ ├── collectors/ # Data collection functions
│ │ ├── types.ts # CollectorContext, CollectorResult, Collector, makeSection()
│ │ ├── meta.ts # Machine metadata (hostname, OS, date)
│ │ ├── ssh.ts # SSH config parser → structured host table
│ │ ├── ollama.ts # Ollama model list (parses `ollama list` output)
│ │ ├── apps.ts # macOS apps, Raycast, AltTab (darwin-only)
│ │ └── homebrew.ts # Homebrew formulae + casks (darwin-only)
│ │
│ ├── scan/ # Sensitivity detection engine
│ │ ├── types.ts # ScanPattern, ScanResult, ScanFinding, Severity, ScanSummary
│ │ ├── patterns.ts # 27+ detection patterns (cached, username-aware)
│ │ ├── scanner.ts # scanContent(), scanFile(), summarize()
│ │ ├── redactor.ts # applyRedactions() — replaces matched values with [REDACTED]
│ │ ├── report.ts # formatReport() — human-readable sensitivity summary
│ │ └── index.ts # Public re-exports
│ │
│ ├── backup/ # Backup file management
│ │ ├── types.ts # BackupEntry (file|dir), BackupSource, BackupFile, BackupDir
│ │ └── sources.ts # Thin wrapper: registryBackupSources(registryEntries)
│ │
│ ├── restore/ # Restore engine
│ │ ├── types.ts # RestoreEntry, RestorePlan, FileStatus, ConflictAction
│ │ ├── plan.ts # buildRestoreMap(), buildRestorePlan()
│ │ ├── execute.ts # executeRestore(), createSnapshot(), printPlan()
│ │ ├── prompt.ts # Interactive conflict prompt (o/s/d/a/l)
│ │ └── index.ts # Public re-exports
│ │
│ └── utils/ # Shared utilities
│ ├── constants.ts # REDACTION_MARKER = "[REDACTED]"
│ ├── home.ts # getHome() — validates $HOME, exits on missing
│ ├── redact.ts # redactSshConfig(), redactNpmTokens()
│ ├── resolve-output.ts # resolveOutputDir() — -o / git repo / ~/Downloads
│ ├── find-backup.ts # findLatestBackup(), getBackupAge()
│ └── timestamp.ts # generateTimestamp() → YYYYMMDDHHMMSS
│
├── tests/ # 102 tests across 14 files
│ ├── collectors/ # meta, ssh, npm, claude tests
│ ├── commands/ # list fuzzy match tests
│ ├── registry/ # entries validation, resolve, collector tests
│ ├── scan/ # scanner, patterns, redactor tests
│ ├── restore/ # plan, execute, snapshot tests
│ └── utils/ # redact, timestamp tests
│
├── docs/ # VitePress documentation site
│ ├── .vitepress/config.mts # VitePress config with mermaid plugin
│ └── *.md # Documentation pages
│
├── PLAN.md # Master plan and roadmap
├── CLAUDE.md # Project-level Claude Code instructions
└── package.json # @dotformat/cli package definitionType System
Core Types
// === Collector Types (src/collectors/types.ts) ===
interface CollectorContext {
redact: boolean; // true by default — controls sensitivity redaction
home: string; // $HOME — injected for testability
}
type CollectorResult = Record<string, DotfSection>;
type Collector = (ctx: CollectorContext) => Promise<CollectorResult>;
// makeSection() helper creates DotfSection objects
function makeSection(name: string, opts?: {
pairs?: Record<string, string>;
items?: { raw: string; columns: string[] }[];
content?: string | null;
}): DotfSection;// === Registry Types (src/registry/types.ts) ===
type Platform = "darwin" | "linux" | "win32";
type EntryKind =
| { type: "file" } // Read file content
| { type: "file"; metadata: true } // Only existence + line count
| { type: "dir" } // List directory contents
| { type: "json-extract"; fields: string[] }; // Extract specific JSON fields as pairs
interface ConfigEntry {
id: string; // Section name: "shell.zshrc"
name: string; // Human label: ".zshrc"
paths: Partial<Record<Platform, string>>; // Per-OS paths (~ expanded at runtime)
category: string; // Powers --only/--skip filtering
kind: EntryKind; // How to collect this entry
backupDest: string; // Relative path in backup directory
sensitivity: "low" | "medium" | "high"; // Sensitivity classification hint
redact?: (content: string) => string; // Optional custom redaction function
}// === Backup Types (src/backup/types.ts) ===
interface BackupFile {
type: "file";
src: string; // Absolute source path
dest: string; // Relative destination in backup
redact?: (content: string) => string; // Custom redaction
}
interface BackupDir {
type: "dir";
src: string; // Absolute source directory
dest: string; // Relative destination in backup
}
type BackupEntry = BackupFile | BackupDir;
interface BackupSource {
category: string; // Category name for filtering
entries: (home: string) => BackupEntry[]; // Lazy — evaluated with $HOME at runtime
}// === Restore Types (src/restore/types.ts) ===
type FileStatus = "new" | "conflict" | "same" | "redacted";
interface RestoreEntry {
backupPath: string; // Relative path in backup dir
targetPath: string; // Absolute destination on machine
category: string; // Category for picker
status: FileStatus; // Computed status
}
interface RestorePlan {
entries: RestoreEntry[];
backupDir: string;
categories: string[]; // Sorted unique categories
}
type ConflictAction = "overwrite" | "skip" | "diff";
type BatchConflictAction = ConflictAction | "overwrite-all" | "skip-all";// === Scan Types (src/scan/types.ts) ===
type Severity = "HIGH" | "MEDIUM" | "LOW";
type ScanAction = "skip" | "redact" | "include";
interface ScanPattern {
id: string; // Pattern identifier
label: string; // Human-readable label
severity: Severity;
regex: RegExp;
defaultAction: ScanAction;
}
interface ScanFinding {
pattern: ScanPattern;
match: string; // Truncated to 40 chars
line: number; // 1-based line number
}
interface ScanResult {
filePath: string;
action: ScanAction; // Highest severity finding determines
findings: ScanFinding[];
}
interface ScanSummary {
skipped: number;
redacted: number;
included: number;
results: ScanResult[]; // Only results with findings
}Data Flow
Collect Flow
CLI args → parseArgs()
→ resolveOutputDir()
→ build CollectorContext { redact, home }
→ Promise.allSettled(collectors)
→ merge fulfilled results → CollectorResult
→ if redact: scanContent() each section → skip/redact/include
→ if slim: truncate content to 10 lines
→ stringify(doc) via @dotformat/core
→ Bun.write() timestamped .dotf fileBackup Flow
CLI args → parseArgs()
→ resolveOutputDir()
→ filterSources(backupSources, only, skip)
→ for each source:
→ entries(home) → BackupEntry[]
→ file: Bun.file().text() → scanContent() → entry.redact?() → applyRedactions() → Bun.write()
→ dir: Bun.Glob('**/*').scan() → copy all files
→ if archive: tar czf → rm -rf dir
→ summarize() → formatReport()Restore Flow
CLI args → parseArgs()
→ buildRestorePlan(backupDir, home):
→ buildRestoreMap() from backupSources
→ Bun.Glob('**/*').scan(backupDir)
→ for each file: match to map → resolveFileStatus() (Bun.hash comparison)
→ handle dir entries, .local overrides
→ if --pick: pickCategories() → filter plan
→ if --dry-run: printPlan() → exit
→ createSnapshot() for conflicts
→ for each entry: prompt if conflict → Bun.write()Key Design Decisions
Registry as Single Source of Truth
Before the registry, config paths were hardcoded in 3 places: 11 collector files, backup/sources.ts, and restore/plan.ts. Adding a new tool meant editing all three.
Now, src/registry/entries.ts is the only place paths are defined. The registry generates:
- Collectors via
registryCollector(entries)— handles file, dir, json-extract, and metadata kinds - Backup sources via
registryBackupSources(entries)— groups by category, resolves paths
Hand-written collectors remain for complex cases: meta (dynamic data), ssh (structured parsing), ollama (command output), apps (macOS-specific checks), homebrew (command output).
Promise.allSettled for Collectors
Collectors run in parallel. If Homebrew isn't installed or Ollama isn't running, those collectors fail silently while others succeed. The report includes whatever data was available.
Bun.hash() for File Comparison
Restore uses Bun.hash() (xxHash) to compare backup content against live files. This is significantly faster than string equality for large files and avoids loading both strings into memory for comparison.
Lazy BackupSource.entries(home)
Backup sources don't resolve paths at import time. The entries() function takes home as a parameter, making the entire backup pipeline testable with any directory.
Sensitivity as Pipeline, Not Gate
The scan system doesn't just block or allow — it operates as a three-stage pipeline (detect → classify → act) where each finding gets its own action. A single file can have both redacted and included findings. The highest-severity action determines the file-level action.
Dependencies
| Package | Role |
|---|---|
@dotformat/core | .dotf format parser, stringifier, comparator, diff formatter (source) |
vitepress | Documentation site (dev dependency) |
vitepress-plugin-mermaid | Mermaid diagram support in docs (dev dependency) |
@types/bun | TypeScript definitions for Bun APIs (dev dependency) |
No other runtime dependencies. The CLI uses only Bun built-ins (Bun.file, Bun.$, Bun.Glob, Bun.hash, Bun.color, Bun.env) and Node.js standard library modules (path, os, fs/promises).