diff --git a/.claude/commands/dotfiles-sync.md b/.claude/commands/dotfiles-sync.md new file mode 100644 index 0000000..b34223e --- /dev/null +++ b/.claude/commands/dotfiles-sync.md @@ -0,0 +1,152 @@ +You are maintaining a chezmoi-managed dotfiles repo. The user operates their Mac freely (installs packages, edits configs, adds API keys). Your job is to detect what changed on the machine, report it clearly, and sync approved changes back into the repo. + +## Step 1: Read context + +Read `docs/sync-log.md` to understand when the last sync happened and what changed. If the file doesn't exist, this is the first sync. + +## Step 2: Scan for drift + +Run these detection commands in parallel where possible: + +### Config drift +```bash +chezmoi status 2>/dev/null +``` +Look for lines starting with ` M` (modified) or `MM` (modified both sides). + +### Brew packages +```bash +# Installed but not in Brewfile +comm -23 <(brew leaves | sort) <(grep '^brew "' ~/.Brewfile 2>/dev/null | sed 's/brew "//;s/".*//' | sort) + +# In Brewfile but not installed +comm -13 <(brew leaves | sort) <(grep '^brew "' ~/.Brewfile 2>/dev/null | sed 's/brew "//;s/".*//' | sort) +``` + +### Cask apps +```bash +# Installed but not in Brewfile +comm -23 <(brew list --cask 2>/dev/null | sort) <(grep '^cask "' ~/.Brewfile 2>/dev/null | sed 's/cask "//;s/".*//' | sort) + +# In Brewfile but not installed +comm -13 <(brew list --cask 2>/dev/null | sort) <(grep '^cask "' ~/.Brewfile 2>/dev/null | sed 's/cask "//;s/".*//' | sort) +``` + +### VS Code extensions +```bash +# Installed but not tracked +comm -23 <(code --list-extensions 2>/dev/null | sort) <(sort ~/.config/code/extensions.txt 2>/dev/null) + +# Tracked but not installed +comm -13 <(code --list-extensions 2>/dev/null | sort) <(sort ~/.config/code/extensions.txt 2>/dev/null) +``` + +### New fish functions (not managed by chezmoi) +```bash +# Functions on disk but not in source +comm -23 <(ls ~/.config/fish/functions/ 2>/dev/null | sort) <(chezmoi managed | grep 'fish/functions/' | xargs -I{} basename {} | sort) +``` + +### New SSH config fragments +```bash +# SSH config.d files not managed +comm -23 <(ls ~/.ssh/config.d/ 2>/dev/null | sort) <(chezmoi managed | grep 'ssh/config.d/' | xargs -I{} basename {} | sort) +``` + +### Hardcoded secrets in fish config +```bash +# Look for set -gx with what looks like API keys (long alphanumeric strings) +grep -n 'set -gx.*[A-Za-z0-9_]\{20,\}' ~/.config/fish/config.fish ~/.config/fish/conf.d/*.fish 2>/dev/null | grep -v 'onepasswordRead\|op://' || true +``` + +## Step 3: Report + +Present findings in plain language, grouped by category. For each category, show: +- What changed (specific names, not counts) +- Brief context if you can infer it ("ollama is probably for local LLM testing") + +Use this format: + +``` +Dotfiles sync report (YYYY-MM-DD) + +Config drift (N files): + - path — what changed (brief description of the diff) + +New packages (N brew, N casks): + Brew: pkg1, pkg2, ... + Cask: app1, app2, ... + +Stale entries (N brew, N casks): + Brew: pkg1, pkg2, ... (in Brewfile but not installed) + Cask: app1, app2, ... (in Brewfile but not installed) + +VS Code extensions: + New: ext1, ext2, ... + Removed: ext1, ... + +New fish functions (N): + func1, func2, ... + +New SSH configs (N): + host1, host2, ... + +Secrets: + [any findings or "no issues"] + +What would you like me to do? +``` + +If a category has no findings, omit it from the report. + +## Step 4: Wait for decisions + +Do NOT make any changes yet. Ask the user what to do. They'll respond in plain language: +- "Add the new packages" +- "Drop raycast and slack" +- "Keep btop in Brewfile even though not installed" +- "Sync the Zed config" +- "Do it all" + +## Step 5: Execute + +Based on the user's decisions: + +| Action | Method | +|--------|--------| +| Absorb config drift | `chezmoi re-add ` | +| Add brew packages | Edit `home/dot_Brewfile.tmpl`, add `brew "pkg"` in correct section | +| Remove stale brew | Edit `home/dot_Brewfile.tmpl`, delete lines | +| Add casks | Edit `home/dot_Brewfile.tmpl`, add `cask "app"` in correct section | +| Remove stale casks | Edit `home/dot_Brewfile.tmpl`, delete lines | +| Sync VS Code extensions | Update `home/dot_config/code/extensions.txt` | +| Track fish functions | `chezmoi add ~/.config/fish/functions/NAME.fish` | +| Track SSH configs | `chezmoi add ~/.ssh/config.d/NAME` | +| Register secrets | Append to `home/.chezmoidata/secrets.toml` | + +When editing the Brewfile, preserve the existing section structure (base/dev/apps). Place new entries in the appropriate section. + +## Step 6: Log + +Append an entry to `docs/sync-log.md`: + +```markdown +## [YYYY-MM-DD] sync + +[Category]: + - [what changed] + +--- +``` + +## Step 7: Commit + +Stage all changes and commit with a descriptive message: + +``` +chore(sync): dotfiles sync YYYY-MM-DD + +[Summary of changes by category] +``` + +Then ask: "Push to remote?" Only push if the user confirms. diff --git a/CLAUDE.md b/CLAUDE.md index ad16a8a..8ef0dce 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,6 +6,8 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co A chezmoi-managed dotfiles repo for macOS (Apple Silicon). The `home/` directory is the chezmoi source state — it maps to `$HOME` on the target machine. The `install.sh` script bootstraps a fresh Mac from zero. +User-facing customization flows (how to change Brewfile, secrets, editors, etc.) live in [`docs/guide.md`](docs/guide.md). When a user asks "how do I change X", point them there rather than reinventing. + ## Key commands ```bash @@ -33,7 +35,12 @@ chezmoi uses filename prefixes to encode target attributes: ``` {{ onepasswordRead (printf "op://%s/ItemName/credential" .op_vault) }} ``` -Used in: `secrets.fish.tmpl`, `dot_gitconfig.tmpl`, `dot_config/zed/settings.json.tmpl` +Used directly in: `dot_gitconfig.tmpl`, `dot_config/zed/settings.json.tmpl`. + +For auto-loaded shell env vars, prefer the data-driven workflow: register +entries in `.chezmoidata/secrets.toml` via `dotfiles secret add VAR op://...` +and let `secrets.fish.tmpl` iterate. Do not hand-edit the template to add +new env vars — use `dotfiles secret add` / `dotfiles secret rm` / `dotfiles secret list`. **macOS Keychain** — via `keyring` template function or runtime fish functions: ``` diff --git a/README.md b/README.md index 1cbeba2..65e048a 100644 --- a/README.md +++ b/README.md @@ -8,298 +8,141 @@ ![1Password](https://img.shields.io/badge/1Password-Secrets-0572EC?logo=1password&logoColor=white) ![CI](https://img.shields.io/github/actions/workflow/status/dwarvesf/dotfiles/test.yml?label=CI&logo=github) -A modern developer tooling stack for macOS, deployed in one command. Every tool is chosen for speed, ergonomics, and native macOS integration; no legacy defaults, no bloat. +A dotfiles repo maintained by an LLM. You operate your Mac freely; Claude detects what drifted and syncs it back to the repo on your approval. **You never manually keep this repo in sync.** -**The stack:** [Fish](https://fishshell.com/) replaces Zsh (faster startup, better defaults). [Starship](https://starship.rs/) replaces Oh My Zsh themes (cross-shell, instant). [Ghostty](https://ghostty.org/) replaces iTerm2 (GPU-rendered, native). [delta](https://github.com/dandavison/delta) replaces diff (syntax-highlighted). [eza](https://eza.rocks/), [bat](https://github.com/sharkdp/bat), [fd](https://github.com/sharkdp/fd), [ripgrep](https://github.com/BurntSushi/ripgrep), [zoxide](https://github.com/ajeetdsouza/zoxide), [fzf](https://github.com/junegunn/fzf) replace ls, cat, find, grep, cd, Ctrl+R. [1Password](https://1password.com/) handles secrets via `op://` templates; nothing is ever stored in git. All managed by [chezmoi](https://www.chezmoi.io/) with a [gum](https://github.com/charmbracelet/gum)-powered setup wizard. +## The idea - - +Most dotfiles repos expect you to edit the source, apply, commit, push. In practice, nobody does this consistently. You `brew install` while debugging, tweak a config directly, add an API key, and move on. After a few weeks, the repo is stale. -**Requirements:** macOS 12+, Apple Silicon (Intel works too). First run takes ~30 minutes (Homebrew downloads). +This repo works differently. You change things on your machine. Periodically, you ask Claude to catch up: -## Quick start - -```bash -git clone https://github.com/dwarvesf/dotfiles ~/dotfiles -cd ~/dotfiles && ./install.sh ``` +You: /dotfiles-sync +Claude: [scans machine — packages, configs, extensions, secrets] -A styled setup wizard ([gum](https://github.com/charmbracelet/gum)) will prompt for your name, email, editor, headless mode, and whether you use 1Password. Everything adapts accordingly. On a headless/server machine, GUI apps, dev toolchains, and casks are skipped automatically. - -**Flags:** -- `./install.sh --check` -- dry-run, validates without applying -- `./install.sh --force` -- teardown and reinit from scratch -- `./install.sh --config-only` -- deploy config files only, skip brew/mas/defaults - -
-Adopt on an existing Mac - -Already have brew, fish, and your tools installed? Use `--config-only` to deploy just the config files without re-running brew bundle, mas installs, or macOS defaults: - -```bash -git clone https://github.com/dwarvesf/dotfiles ~/dotfiles -cd ~/dotfiles && ./install.sh --config-only -``` - -This will: -1. Link chezmoi source to `~/dotfiles/home` -2. Prompt for your name, email, editor, headless mode, 1Password -3. Deploy all config files to `$HOME` -4. **Skip** brew bundle, Mac App Store apps, macOS defaults, toolchain installs - -Then switch your shell and reload: -```bash -# Set fish as default (if not already) -grep -q /opt/homebrew/bin/fish /etc/shells || echo /opt/homebrew/bin/fish | sudo tee -a /etc/shells -chsh -s /opt/homebrew/bin/fish - -# Open a new terminal to pick up the configs -``` - -
- -
-Alternative: bootstrap without git +Claude: Dotfiles sync report + Config drift: Zed settings (2 new MCP servers) + New packages: ollama, rclone, pandoc + Stale: raycast, slack (not installed) + VS Code: 5 new extensions + What should I do? -On a truly fresh Mac, git requires Xcode CLT (10+ minutes to install). These methods skip that: +You: sync everything, drop raycast and slack -**Via chezmoi directly (no git, no Homebrew):** -```bash -sh -c "$(curl -fsLS get.chezmoi.io)" -- init --apply dwarvesf -``` +Claude: [edits Brewfile, re-adds configs, updates extensions, + logs to sync-log.md, commits] + Done. Push? -**Via Homebrew + chezmoi (no git):** -```bash -/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" -eval "$(/opt/homebrew/bin/brew shellenv)" -brew install chezmoi -chezmoi init --apply dwarvesf +You: push ``` -> **Note:** These methods clone into `~/.local/share/chezmoi/` (chezmoi's default) instead of `~/dotfiles`. The git clone method is better for active development since you control the repo location. +Two sentences from you. The LLM handled 6 file edits, a commit message, and a push. -
- -
-Fork and customize +The pattern is general and works with any dotfiles manager and any LLM agent. The full write-up, including setup instructions, is in **[docs/llm-dotfiles.md](docs/llm-dotfiles.md)**. -```bash -# 1. Fork this repo on GitHub -# 2. Clone your fork -git clone https://github.com/YOUR_USERNAME/dotfiles ~/dotfiles -cd ~/dotfiles - -# 3. Edit what you want (see "Customization" below) -# 4. Run -./install.sh -``` - -
- -## What happens on install +## How it works

- Bootstrap flow + LLM sync workflow: machine drifts, Claude syncs

-1. Installs Homebrew (if missing) -2. Installs chezmoi -3. Runs setup wizard (styled prompts for name, email, editor, headless mode, 1Password) -4. Deploys all config files to `$HOME` -5. Runs automation scripts: - - `brew bundle` -- installs ~80 packages + casks - - Mac App Store apps via `mas` - - macOS defaults (Dock, Finder, keyboard, trackpad, screenshots) - - Sets Fish as default shell - - Installs Foundry (cast), Rust, npm/uv tools - - VS Code extensions -6. Verifies key files were deployed - -## What's included - -| Layer | Tools | -|-------|-------| -| **Shell** | Fish + Starship prompt + plugins (autopair, done, sponge, async-prompt) | -| **Terminal** | Ghostty (catppuccin-mocha, JetBrains Mono) | -| **Multiplexer** | tmux (C-a prefix, vim nav, fzf session picker, project launcher) | -| **Editors** | VS Code + Zed (settings, extensions, MCP servers) | -| **Git** | .gitconfig (delta diffs, aliases) + .gitignore + commit template | -| **SSH** | 1Password SSH Agent (optional), modular config.d/ | -| **Secrets** | 1Password (`op://`) + macOS Keychain -- never in git | -| **Packages** | Layered Brewfile (base/dev/apps) + Mac App Store (`mas`) | -| **Languages** | mise (Node, Python, Go, Ruby) via `.tool-versions` | -| **Containers** | OrbStack / Docker config | -| **macOS** | 30+ `defaults write` (Dock left, fast key repeat, Finder, screenshots) | -| **Web3/DeFi** | Foundry (`cast`), fish aliases + helper functions | -| **Claude Code** | Settings, hooks, statusline, verify-dotfiles subagent, `/implement-feature` command | - -## Why this setup - -- **Layered Brewfile** -- base tools always install; dev toolchains and GUI apps are conditional. Set `headless=true` for servers. -- **Zero plaintext secrets** -- 1Password `op://` references in templates, macOS Keychain for the rest. The rendered secrets only exist on your machine, never in git. -- **13-command CLI** -- `dotfiles sync`, `dotfiles doctor`, `dotfiles bench`, `dotfiles backup`... no need to remember raw chezmoi commands. -- **CI-tested weekly** -- shellcheck + chezmoi dry-run on macOS. Catches regressions before your next fresh install. -- **Graceful degradation** -- works with or without 1Password. Skip web3, skip Mac App Store, pick your editor. Everything is opt-in. - -## Daily usage - -The `dotfiles` wrapper provides ergonomic commands: - -```fish -dotfiles edit ~/.config/fish/config.fish # edit a config -dotfiles diff # preview changes -dotfiles sync # apply everything -dotfiles update # pull latest + apply -dotfiles status # managed file count + pending diffs -dotfiles doctor # health check (tools, config, drift) -dotfiles bench # benchmark shell startup time -dotfiles backup # back up config + age key to 1Password -dotfiles encrypt-setup # guided age encryption setup -``` - -Adding a Homebrew package: -```fish -dotfiles edit ~/.Brewfile # add the line -dotfiles sync # auto-runs brew bundle -``` - -
-Raw chezmoi commands - -```bash -chezmoi edit ~/.config/fish/config.fish -chezmoi diff -chezmoi apply -chezmoi apply --refresh-externals -``` - -
- -## Customization +[chezmoi](https://www.chezmoi.io/) is the backbone. It separates the source (repo) from the target ($HOME), renders templates with injected secrets, and provides drift detection via `chezmoi status`. This two-layer model is what makes LLM-maintained sync possible: the LLM can safely scan, diff, and re-add without touching secrets in git. -### Files you'll want to edit +The `/dotfiles-sync` command is installed to `~/.claude/commands/` during setup, so it's available in Claude Code from any directory. The command prompt (at [.claude/commands/dotfiles-sync.md](.claude/commands/dotfiles-sync.md)) teaches Claude what to scan: -| File | What to change | -|------|---------------| -| `home/dot_Brewfile.tmpl` | Add/remove Homebrew packages and casks (layered: base/dev/apps) | -| `home/dot_config/fish/config.fish.tmpl` | Shell aliases, paths, tool integrations | -| `home/dot_config/ghostty/config` | Terminal theme, font, keybindings | -| `home/dot_config/tmux/tmux.conf` | tmux prefix, keybindings, status bar | -| `home/dot_config/code/settings.json` | VS Code theme, font, settings | -| `home/dot_config/code/extensions.txt` | VS Code extensions (one per line) | -| `home/dot_config/zed/settings.json.tmpl` | Zed theme, MCP servers | -| `home/dot_tool-versions` | Global language versions | -| `home/.chezmoiscripts/run_once_after_mas-apps.sh.tmpl` | Mac App Store apps | -| `home/.chezmoiscripts/run_once_after_macos-defaults.sh.tmpl` | macOS system preferences | -| `home/.chezmoiexternal.toml` | Fish plugins to auto-download | +| Dimension | What it detects | +|-----------|----------------| +| Config drift | Files changed on machine but not in repo | +| Brew packages | Installed but not in Brewfile (and vice versa) | +| Cask apps | GUI apps installed but not tracked | +| VS Code extensions | New or removed extensions | +| Fish functions | Functions created outside chezmoi | +| SSH configs | New host configs in config.d/ | +| Secrets | Hardcoded keys that should be in 1Password | -### Adding secrets +Every sync is logged in [docs/sync-log.md](docs/sync-log.md) so future syncs have context. -Secrets are injected at `chezmoi apply` time and never stored in git. +## Quick start -**With 1Password** (recommended): ```bash -# Store the secret -op item create --vault=Developer --category=api_credential --title="OpenAI" password="sk-..." - -# Reference it in a template (e.g., secrets.fish.tmpl) -set -gx OPENAI_API_KEY "{{ onepasswordRead "op://Developer/OpenAI/password" }}" +git clone https://github.com/dwarvesf/dotfiles ~/dotfiles +cd ~/dotfiles && ./install.sh ``` -**With macOS Keychain:** -```fish -keychain-set MY_TOKEN "secret-value" # store -keychain-env MY_TOKEN # load into current shell -``` +A [gum](https://github.com/charmbracelet/gum)-powered wizard prompts for your name, email, editor, headless mode, and 1Password. First run takes ~30 minutes (Homebrew downloads). After that, just use `/dotfiles-sync` to keep things current. -**On-demand loading (no apply needed):** -```fish -op-env GITHUB_TOKEN "op://Vault/GitHub Token/password" # 1Password -keychain-env MY_TOKEN # Keychain -web3-env # ETH_RPC_URL + Etherscan -``` +**Requirements:** macOS 12+, Apple Silicon (Intel works too).
-Encrypted files (age) - -For files too complex for template injection (kubeconfig, VPN configs, certificates): - -```fish -# Guided setup (generates key, prints next steps) -dotfiles encrypt-setup +Other install methods -# Then add encrypted files -chezmoi add --encrypt ~/.kube/config -# Creates home/encrypted_dot_kube/config.age in the repo +**Existing Mac** (configs only, skip brew/mas/defaults): +```bash +cd ~/dotfiles && ./install.sh --config-only ``` -Manual setup if you prefer: +**Without git** (fresh Mac, no Xcode CLT): ```bash -brew install age -age-keygen -o ~/.config/chezmoi/key.txt -# Copy the public key (age1...) from output -chezmoi edit-config # uncomment age section, paste public key -# Backup key.txt to 1Password as a Secure Note +sh -c "$(curl -fsLS get.chezmoi.io)" -- init --apply dwarvesf ``` +**Flags:** `--check` (dry-run), `--force` (reinit from scratch) +
-
-Removing what you don't need +## The stack -- **No web3?** Delete web3 aliases from `config.fish.tmpl`, remove `cast_*` functions, remove Foundry from install script -- **No 1Password?** Answer "no" during `chezmoi init` -- all 1Password sections are skipped -- **No Mac App Store?** Delete `run_once_after_mas-apps.sh.tmpl` -- **Different editor?** `chezmoi init` prompts for your choice (VS Code, Zed, Neovim, Vim) +| Layer | Tools | +|-------|-------| +| **Shell** | Fish + Starship prompt + plugins (autopair, done, sponge, async-prompt) | +| **Terminal** | Ghostty (GPU-rendered, catppuccin-mocha, JetBrains Mono) | +| **Multiplexer** | tmux (C-a prefix, vim nav, fzf session picker) | +| **Editors** | VS Code + Zed (settings, extensions, MCP servers with 1P secrets) | +| **Git** | delta diffs, aliases, commit template | +| **SSH** | 1Password SSH Agent, modular config.d/ | +| **Secrets** | 1Password `op://` templates + data-driven registry | +| **Packages** | Layered Brewfile (base/dev/apps) + Mac App Store | +| **Languages** | mise (Node, Python, Go, Ruby) | +| **macOS** | 30+ `defaults write` (Dock, Finder, keyboard, screenshots) | -
+Every tool is chosen for speed, ergonomics, and native macOS integration. No legacy defaults, no bloat. -## Troubleshooting +## Offline fallback -Run `dotfiles doctor` to diagnose issues: +When you're not in a Claude session (SSH, airplane, quick edit), the `dotfiles` CLI works standalone: -``` -$ dotfiles doctor -Dotfiles health check -===================== - -[ok] chezmoi installed -[ok] chezmoi source linked -[ok] fish is default shell -[ok] homebrew installed -[ok] 1Password CLI: signed in -[ok] 1Password SSH agent: socket exists -[ok] ~/.gitconfig exists -[ok] ~/.config/fish/config.fish exists -[ok] ~/.ssh/config exists -[ok] git identity: Your Name -[ok] fzf -[ok] bat -... -[ok] no drift detected - -All checks passed. +```fish +dotfiles edit ~/.config/fish/config.fish # edit + apply + auto-commit +dotfiles drift # detect and re-absorb drift +dotfiles doctor # health check ``` -
-How secrets flow +Full command reference, walkthroughs, secrets management, multi-machine setup, and troubleshooting are in the **[user guide](docs/guide.md)**. -

- Secrets flow -

+## Lifecycle -On a new Mac: clone -> `./install.sh` -> `op signin` -> `chezmoi apply` -> done. +| Stage | Command | +|-------|---------| +| **Install** | `git clone ... ~/dotfiles && cd ~/dotfiles && ./install.sh` | +| **Update** (LLM) | `/dotfiles-sync` in Claude Code | +| **Update** (manual) | `dotfiles update` (pull + apply) | +| **Reinstall** | `./install.sh --force` | +| **Uninstall** | See [guide](docs/guide.md#9-lifecycle-install-update-uninstall) | -
+## Security -
-Architecture +This repo is safe to make public. Actual secrets (API keys, tokens, passwords) are never committed; only `op://` references to 1Password items appear in the source. Real values are resolved at `chezmoi apply` time and only exist on your machine. -

- Architecture -

+The `op://` references do reveal 1Password vault and item names (e.g. `op://Private/OpenAI/credential`). This is intentional: it makes the repo forkable. If you fork, replace the item names with your own. The vault structure tells someone what services you use, not how to access them. -
+## Docs + +| Document | What it covers | +|----------|---------------| +| **[docs/llm-dotfiles.md](docs/llm-dotfiles.md)** | The LLM-maintained dotfiles pattern. Shareable, stack-agnostic. Includes setup instructions. | +| **[docs/guide.md](docs/guide.md)** | Full user guide. chezmoi details, manual commands, customization, secrets, multi-machine, troubleshooting. | +| **[docs/decisions/](docs/decisions/)** | Architecture decision records (why chezmoi, Fish, Ghostty, 1Password, auto-commit). | +| **[docs/sync-log.md](docs/sync-log.md)** | Sync history. Append-only log of every Claude-assisted sync. | ## Credits diff --git a/docs/decisions/006-auto-commit-workflow.md b/docs/decisions/006-auto-commit-workflow.md new file mode 100644 index 0000000..0f02708 --- /dev/null +++ b/docs/decisions/006-auto-commit-workflow.md @@ -0,0 +1,88 @@ +# ADR-006: Auto-commit workflow for dotfile changes + +## Status: accepted + +## Context + +chezmoi separates the **source state** (the repo in `~/.local/share/chezmoi/`) from the **target state** (deployed files in `$HOME`). This two-layer model is powerful but creates a recurring failure mode: you change a config file, it works on your machine, but you forget to commit. Days later you set up a new machine or lose a disk, and the change is gone. + +The gap between "applied to my machine" and "backed up in git" is where config changes go to die. In practice, three things go wrong: + +1. **Forgot to commit.** You run `chezmoi edit --apply`, the change works, you move on. The source file is modified but never staged or committed. +2. **Edited the wrong layer.** You open `~/.config/fish/config.fish` directly instead of the chezmoi source. The deployed file now differs from the source (drift). Next `chezmoi apply` silently overwrites your change. +3. **Partial commit.** You edit three files, commit one, forget the other two. The repo is in an inconsistent state you won't notice until something breaks. + +All three are human memory failures, not technical failures. The tooling should close the loop automatically. + +## Decision + +Make **commit-on-change the default** for all dotfile editing workflows. Every helper that modifies the chezmoi source tree auto-commits the diff after a successful apply. Push remains manual. + +### The helpers + +**`dotfiles edit` (dotfile edit)** handles the forward path: source → machine. + +``` +dotfiles edit ~/.config/fish/config.fish +``` + +This runs `chezmoi edit --apply` to open the source file in your editor, applies on save, then auto-commits any changed files under `home/`. The commit message is mechanical: `chore(config): update config.fish via dotfiles edit`. + +**`dotfiles drift` (dotfile sync)** handles the reverse path: machine → source. + +``` +dotfiles drift +``` + +This runs `chezmoi status` to find deployed files that differ from the source (drift), shows you the list, asks for confirmation, then runs `chezmoi re-add` to pull the live versions back into the source tree and commits the result. + +**`dotfiles secret add` / `dotfiles secret rm`** modify `.chezmoidata/secrets.toml` (the secret registry) and auto-commit the change. + +All four helpers accept `--no-commit` to opt out. + +### Why push is manual + +Auto-commit is safe because it's local and reversible (`git reset`). Auto-push is not: + +- You might be mid-experiment and want to squash commits before pushing. +- A bad config could propagate to other machines that pull automatically. +- Push failures (auth, network) would need retry logic and error handling that doesn't belong in a fish function. + +Instead, each helper prints the exact `git push` command after committing so you can run it when ready. This keeps the helpers simple and the blast radius local. + +### Why commit is opt-out, not opt-in + +The original design had `--commit` as an opt-in flag. This was backwards: the safe default should be "your changes are backed up." Forgetting `--commit` means losing work silently. Forgetting `--no-commit` means at worst an extra commit you can amend or squash. + +The cost of a forgotten `--commit` (lost work) is much higher than the cost of a forgotten `--no-commit` (extra commit). So commit is the default. + +### The two-direction model + +Config drift can happen in either direction: + +| Direction | Cause | Tool | +|-----------|-------|------| +| Source → machine | You edit the source, need to apply | `dotfiles edit` | +| Machine → source | You edited the deployed file directly, or an app rewrote its config | `dotfiles drift` | + +Together, `dotfiles edit` and `dotfiles drift` cover both directions. The workflow is: + +1. **Normal edits**: use `dotfiles edit`. One command: edit, apply, commit. +2. **Accidental direct edits**: run `dotfiles drift` to detect and re-absorb drift. +3. **Secrets**: use `dotfiles secret add` / `dotfiles secret rm`. Same auto-commit behavior. + +## Alternatives considered + +- **fswatch / file watcher daemon**: Auto-apply on every source file save. Too aggressive; you lose the ability to make multi-file changes before applying. Also requires a background process that can die silently. +- **Git hooks (post-commit apply)**: Couples git operations to chezmoi apply. Breaks when you commit non-chezmoi files. Direction is wrong: we want apply-then-commit, not commit-then-apply. +- **Makefile / justfile targets**: `make apply`, `make sync`. Works but adds a dependency and another tool to remember. Fish functions are discoverable via tab completion and don't require being in the repo directory. +- **chezmoi's built-in git auto-commit**: chezmoi has `git.autoCommit` in its config, but it commits on every `chezmoi apply` including no-op runs, and the commit messages aren't customizable. Our helpers only commit when there are actual changes and produce descriptive messages. + +## Consequences + +- Every helper that touches source files auto-commits by default. Contributors should expect this. +- Commit messages from helpers follow the pattern `chore(config): ...`. They are mechanical and meant for backup, not human-readable changelogs. +- Push is always manual. No helper will ever run `git push` automatically. +- The `--no-commit` flag is the escape hatch for all helpers when you need to batch changes or are experimenting. +- `dotfiles drift` requires user confirmation before re-absorbing drift. It will never silently overwrite source files. +- The workflow assumes you're working in a git repo. If the dotfiles source isn't a git repo (unlikely but possible), the commit step is silently skipped. diff --git a/docs/dotfiles_chezmoi_model.svg b/docs/dotfiles_chezmoi_model.svg new file mode 100644 index 0000000..3585e05 --- /dev/null +++ b/docs/dotfiles_chezmoi_model.svg @@ -0,0 +1,69 @@ + + + + + + +How chezmoi works: source to target + + + +Source state (repo) + + +dot_gitconfig.tmpl + + +dot_config/fish/... + + +secrets.toml (registry) + + +Contains op:// refs +Never real values + + + +1Password vault + +sk-proj-abc123... + + + +chezmoi apply +Render + inject + + + + + + + +secrets + + + +Target state ($HOME) + + +~/.gitconfig + + +~/.config/fish/... + + +secrets.fish + + +Contains real values +Only on your machine + + + + + + +Git only sees the left column. Real secrets only exist in the right column. +The repo is safe to publish. Rotation happens in 1Password, then re-apply. + \ No newline at end of file diff --git a/docs/dotfiles_dfe_workflow.svg b/docs/dotfiles_dfe_workflow.svg new file mode 100644 index 0000000..f15548a --- /dev/null +++ b/docs/dotfiles_dfe_workflow.svg @@ -0,0 +1,55 @@ + + + + + + +dfe: edit, apply, and commit in one step + + + +dfe ~/.config/fish/config.fish + + + + + +chezmoi edit --apply +Opens the source file in your $EDITOR + + + + + +You save and close the editor + + + + + +chezmoi apply (automatic) +Renders template, deploys to $HOME + + + + + +git add + commit (automatic) +"chore(config): update config.fish via dfe" + + + + + +Done. Prints push command. +Push is always manual (--no-commit to skip step 5) + + + +Machine updated + + + +Repo backed up + + \ No newline at end of file diff --git a/docs/dotfiles_dfs_workflow.svg b/docs/dotfiles_dfs_workflow.svg new file mode 100644 index 0000000..4b3ca8a --- /dev/null +++ b/docs/dotfiles_dfs_workflow.svg @@ -0,0 +1,70 @@ + + + + + + + +dfs: detect drift and sync back to source + + + +Deployed file was edited directly +e.g. vim ~/.gitconfig instead of dfe ~/.gitconfig + + + + + +dfs + + + + + +chezmoi status +Finds files where deployed differs from source + + + + + +Re-absorb? y/N + + +no + + +aborted + + +yes + + + + +chezmoi re-add +Pulls deployed file back into source tree + + + + + +git add + commit (automatic) +"chore(config): sync drift from machine" + + + +Done. Prints push command. (--no-commit to skip step 5) + + + + + +Source updated + + + +Repo backed up + + \ No newline at end of file diff --git a/docs/dotfiles_llm_sync_workflow.svg b/docs/dotfiles_llm_sync_workflow.svg new file mode 100644 index 0000000..4a9797e --- /dev/null +++ b/docs/dotfiles_llm_sync_workflow.svg @@ -0,0 +1,70 @@ + + + + + + +LLM-maintained dotfiles: the sync workflow + + + +Your machine + + +brew install ollama + + +tweak Zed settings + + +add API key for new tool + + +install new VS Code ext + + +You don't think about +the repo at all + + +days / weeks pass... + + + +Repo (stale) + + +Brewfile (missing ollama) + + +Zed config (old version) + + +extensions.txt (outdated) + + + +/dotfiles-sync +Claude scans, reports, waits for approval + + +"catch up with my dotfiles" + + + + +scan + + + +diff + + + + + + +Repo (current) +Brewfile updated, configs synced, extensions tracked +Committed + logged in sync-log.md + \ No newline at end of file diff --git a/docs/dotfiles_workflow_brew.svg b/docs/dotfiles_workflow_brew.svg new file mode 100644 index 0000000..b01a6e2 --- /dev/null +++ b/docs/dotfiles_workflow_brew.svg @@ -0,0 +1,60 @@ + + + + + + + +Adding a Homebrew package + + + +The wrong way (don't do this) + + +brew install ripgrep + + + + +Package installed locally +but NOT in your Brewfile + + + + +Next machine: package missing +brew bundle won't install it +because Brewfile doesn't know about it + + + +The right way (one command) + + +dfe ~/.Brewfile + + + + +Editor opens Brewfile +Add: brew "ripgrep" + + + + +chezmoi apply triggers +brew bundle runs automatically + + + + +git auto-commit +Brewfile change saved in repo + + + + +Package installed + backed up +Next machine gets it too + \ No newline at end of file diff --git a/docs/dotfiles_workflow_config.svg b/docs/dotfiles_workflow_config.svg new file mode 100644 index 0000000..6cfae57 --- /dev/null +++ b/docs/dotfiles_workflow_config.svg @@ -0,0 +1,72 @@ + + + + + + + +Changing a config file (two paths) + + + +Scenario A: you edit via dfe (ideal) + + +Scenario B: you edit the file directly + + + +dfe ~/.config/fish/config.fish + + + + +Edit source in $EDITOR, save + + + + +chezmoi apply (automatic) + + + + +git commit (automatic) + + + + +Done. Machine + repo in sync. + + + +vim ~/.config/fish/config.fish +(edited deployed file, not source) + + + + +Machine updated, repo NOT +Next chezmoi apply will overwrite! + + + + + +Fix: run dfs + + + + +Detects drift, shows changes + + + + +Confirm, re-add, auto-commit + + + + +Recovered. Now in sync. + \ No newline at end of file diff --git a/docs/dotfiles_workflow_secrets.svg b/docs/dotfiles_workflow_secrets.svg new file mode 100644 index 0000000..72d3d65 --- /dev/null +++ b/docs/dotfiles_workflow_secrets.svg @@ -0,0 +1,65 @@ + + + + + + +Adding or updating a secret + + + +New secret (never in 1Password) + +add-secret OPENAI_API_KEY \ + "op://Private/OpenAI/credential" + + +Rotate existing secret + +Update value in 1Password app +or: op item edit "OpenAI" credential=sk-... + + + + + + + + + + +What happens automatically + + + +1 +Creates 1Password item if it doesn't exist +op item create --vault=Private --title="OpenAI" ... + + + + + +2 +Registers binding in secrets.toml +OPENAI_API_KEY = "op://Private/OpenAI/credential" + + + + + +3 +chezmoi apply renders secrets.fish +Template reads op:// ref, 1Password returns real value + + + + + +4 +git auto-commit secrets.toml change +Only the op:// reference, never the real value + + +For rotation: just update in 1Password, then chezmoi apply. No repo change needed. + \ No newline at end of file diff --git a/docs/guide.md b/docs/guide.md new file mode 100644 index 0000000..7a3ab53 --- /dev/null +++ b/docs/guide.md @@ -0,0 +1,794 @@ +# User guide + +Everything you need to use and customize this dotfiles setup. The +[README](../README.md) covers the pitch and quick start; this guide +covers how it all works and how to make it yours. For the general +LLM-maintained dotfiles pattern, see [llm-dotfiles.md](llm-dotfiles.md). + +--- + +## 1. The LLM workflow + +The primary way to maintain this repo is to ask Claude. + +Run `/dotfiles-sync` in Claude Code (or just say "catch up with my +dotfiles"). Claude scans your machine across 10 dimensions: config +drift, brew packages, casks, VS Code extensions, fish functions, SSH +configs, secrets, and more. It reports what changed in plain language +and waits for your instructions. + +``` +You: /dotfiles-sync +Claude: Config drift: Zed settings (2 new MCP servers) + New packages: ollama, rclone, pandoc + Stale: raycast, slack (not installed) + What should I do? +You: sync everything, drop raycast and slack +Claude: Done. 1 commit. Push? +You: push +``` + +Every sync is logged in `docs/sync-log.md`. Claude reads it at the +start of each session for context ("last sync was 2 weeks ago, you +added ollama"). + +The `/dotfiles-sync` command is installed to `~/.claude/commands/` +during `chezmoi apply`, so it works from any directory in Claude Code, +not just the dotfiles repo. If it's missing, run `chezmoi apply` to +deploy it. + +The manual commands in the sections below are fallbacks for when you're +offline, SSH'd into a server, or want a quick one-off edit. You don't +need to learn them to use this repo day-to-day. + +--- + +## 2. How chezmoi works + +### The two layers + +chezmoi keeps two copies of every config file: + +| Layer | Location | Contains | +|-------|----------|----------| +| **Source** (repo) | `~/.local/share/chezmoi/home/` | Templates, `op://` refs, chezmoi prefixes | +| **Target** (machine) | `$HOME` | Rendered files with real values | + +You edit the source. chezmoi renders templates and copies the result to +your home directory. This is a one-way flow: source to target. + +

+ chezmoi model: source to target +

+ +**Why two layers?** The source can live in a public git repo because it +never contains real secrets — only `op://` references. The target has +your actual API keys, but it's never committed. + +Here's the full bootstrap flow when you first install: + +

+ Bootstrap flow +

+ +### Where things live + +``` +~/dotfiles/ ← the repo (git clone location) +├── home/ ← chezmoi source state +│ ├── dot_gitconfig.tmpl ← becomes ~/.gitconfig +│ ├── dot_config/ +│ │ ├── fish/ +│ │ │ ├── config.fish.tmpl ← shell config +│ │ │ ├── functions/ ← one file per function +│ │ │ ├── completions/ ← tab completions +│ │ │ └── conf.d/ ← auto-sourced snippets +│ │ ├── ghostty/config ← terminal config +│ │ ├── starship.toml ← prompt config +│ │ ├── tmux/tmux.conf ← multiplexer config +│ │ ├── code/ ← VS Code settings +│ │ └── zed/ ← Zed settings +│ ├── dot_Brewfile.tmpl ← Homebrew packages +│ ├── .chezmoidata/secrets.toml ← secret registry (op:// refs) +│ └── .chezmoiscripts/ ← automation scripts +├── docs/ ← this guide, specs, ADRs +└── install.sh ← bootstrap script +``` + +Naming: `dot_` becomes `.`, `.tmpl` means "render this template", +`private_` sets mode 0600. + +--- + +### What happens on install + +

+ Bootstrap flow +

+ +1. Installs Homebrew (if missing) +2. Installs chezmoi + runs setup wizard (name, email, editor, headless, 1Password) +3. Deploys all config files to `$HOME` +4. Runs `brew bundle` (~80 packages + casks) +5. Mac App Store apps via `mas` +6. macOS defaults (Dock, Finder, keyboard, trackpad, screenshots) +7. Sets Fish as default shell +8. Installs toolchains (Foundry, Rust, npm/uv), VS Code extensions +9. Verifies key files were deployed + +**Install flags:** +- `./install.sh --check` -- dry-run, validates without applying +- `./install.sh --force` -- teardown and reinit from scratch +- `./install.sh --config-only` -- deploy config files only, skip brew/mas/defaults + +**Adopt on an existing Mac:** +```bash +cd ~/dotfiles && ./install.sh --config-only +``` +Then set Fish as default: `chsh -s /opt/homebrew/bin/fish` + +**Bootstrap without git** (fresh Mac, no Xcode CLT): +```bash +sh -c "$(curl -fsLS get.chezmoi.io)" -- init --apply dwarvesf +``` + +--- + +## 3. Your first 30 minutes + +You just ran `install.sh`. Here's what's on your machine now. + +### Shell: Fish + +Your default shell is now [Fish](https://fishshell.com/). It differs +from bash/zsh in a few ways: + +- **Autosuggestions** — type a few characters, Fish suggests from history + in gray. Press `→` to accept. +- **Tab completion** — press `Tab` for rich completions with descriptions. +- **No `.bashrc`** — config lives at `~/.config/fish/config.fish`. +- **Abbreviations, not aliases** — type `gs` then space, it expands to + `git status`. Your abbreviations: + +| Category | Abbreviations | +|----------|--------------| +| Git | `g` `gs` `gd` `gl` `gp` `gc` `gco` `gpr` | +| Navigation | `ls` `l` `ll` `la` `lt` `..` `...` | +| Modern replacements | `cat`→bat `top`→btop `du`→dust `df`→duf `ps`→procs | +| Tmux | `tx` `tml` `tma` `tmk` | +| Kubernetes/Docker | `k` `k9` `d` `dc` | + +Fish plugins (installed without a plugin manager, via direct download): + +| Plugin | What it does | +|--------|-------------| +| **autopair** | Auto-closes brackets, quotes, parentheses | +| **done** | Desktop notification when a long command finishes | +| **sponge** | Removes failed commands from history | +| **async-prompt** | Keeps the prompt responsive while git info loads | + +### Terminal: Ghostty + +[Ghostty](https://ghostty.org/) is your terminal. Key settings: + +- **Font**: SauceCodePro Nerd Font, size 12 +- **Theme**: base16-eighties-dark (switch with `ghostty-theme N`) +- **Quick toggle**: `Cmd+`` brings up the terminal from anywhere +- **Splits**: `Cmd+Shift+Enter` (right), `Cmd+Shift+-` (down) +- **Navigate splits**: `Cmd+Alt+Arrows` + +### Prompt: Starship + +[Starship](https://starship.rs/) shows: directory, git branch/status, +language versions (Go, Python, Node, Rust), cloud context (AWS, k8s), +and command duration (if >3s). Green arrow = last command OK, red = error. + +### Editors + +Your editor was set during install (`chezmoi init`). VS Code and Zed +both have managed settings and extensions. MCP servers in Zed are +pre-configured with 1Password-injected API keys. + +### Verify everything works + +```fish +dotfiles doctor +``` + +This checks: chezmoi installed, fish is default shell, Homebrew, 1Password, +key config files exist, git identity set, CLI tools present, no drift. + +--- + +## 4. Manual commands (offline fallback) + +### Editing any config + +Use `dotfiles edit` (dotfile edit) for all config changes. It edits the source, +applies, and auto-commits in one step: + +```fish +dotfiles edit ~/.config/fish/config.fish +``` + +

+ dfe workflow +

+ +Pass `--no-commit` to skip the auto-commit. + +### Adding a Homebrew package + +Don't run `brew install` directly — that installs locally but doesn't +update the Brewfile, so your next machine won't have it. + +```fish +dotfiles edit ~/.Brewfile # add the line, save, brew bundle runs automatically +``` + +

+ Homebrew workflow +

+ +### Handling drift + +If you (or an app) edited a deployed file directly, `dotfiles drift` detects and +fixes it: + +```fish +dotfiles drift # shows drifted files, prompts, re-absorbs into source, commits +``` + +

+ dfs workflow +

+ +### Changing a config: two paths + +

+ Config change: dfe vs direct edit +

+ +### The `dotfiles` CLI + +For everything beyond editing, use the `dotfiles` wrapper: + +| Command | Alias | Abbr | What it does | +|---------|-------|------|-------------| +| `dotfiles edit ` | `e` | `de` | Edit + apply + auto-commit | +| `dotfiles drift` | | `dd` | Detect and re-absorb drifted files | +| `dotfiles secret ` | | | Manage 1Password secrets (add/rm/list) | +| `dotfiles diff` | `d` | | Show pending changes | +| `dotfiles sync` | `s` | `ds` | Apply all changes | +| `dotfiles update` | `u` | `dfu` | Pull latest + apply | +| `dotfiles status` | `st` | | Managed file count + pending diffs | +| `dotfiles cd` | | | Go to chezmoi source directory | +| `dotfiles refresh` | `r` | | Re-download fish plugins | +| `dotfiles add ` | `a` | | Add a new file to chezmoi | +| `dotfiles doctor` | | | Health check | +| `dotfiles bench` | | | Benchmark shell startup time | +| `dotfiles backup` | | | Back up config + age key to 1Password | +| `dotfiles encrypt-setup` | | | Set up age encryption | + +All subcommands have tab completions. Abbreviations expand on space (e.g. type `de` then space, it becomes `dotfiles edit`). + +--- + +## 5. Customization cookbook + +### Quick-change table + +| Change | File to edit | Command | +|--------|-------------|---------| +| Homebrew packages | `dot_Brewfile.tmpl` | `dotfiles edit ~/.Brewfile` | +| Fish abbreviations/env | `config.fish.tmpl` | `dotfiles edit ~/.config/fish/config.fish` | +| Fish function | `functions/NAME.fish` | `dotfiles edit ~/.config/fish/functions/NAME.fish` | +| Starship prompt | `starship.toml` | `dotfiles edit ~/.config/starship.toml` | +| Ghostty terminal | `ghostty/config` | `dotfiles edit ~/.config/ghostty/config` | +| Ghostty theme | any time | `ghostty-theme N` | +| tmux | `tmux/tmux.conf` | `dotfiles edit ~/.config/tmux/tmux.conf` | +| VS Code settings | `code/settings.json` | `dotfiles edit ~/.config/code/settings.json` | +| VS Code extensions | `code/extensions.txt` | edit + `chezmoi apply` | +| Zed settings + MCP | `zed/settings.json.tmpl` | `dotfiles edit ~/.config/zed/settings.json` | +| Git config | `dot_gitconfig.tmpl` | `dotfiles edit ~/.gitconfig` | +| SSH hosts | `dot_ssh/config.d/*` | drop file + `chezmoi apply` | +| macOS defaults | `run_once_after_macos-defaults.sh.tmpl` | edit + `chezmoi apply` | +| Fish plugins | `.chezmoiexternal.toml` | edit + `chezmoi apply --refresh-externals` | +| Setup answers | `~/.config/chezmoi/chezmoi.toml` | `chezmoi init` | +| 1Password secrets | `.chezmoidata/secrets.toml` | `dotfiles secret add VAR op://...` | + +### Walkthrough: add a new fish function + +**Goal:** create a `weather` function that shows the forecast. +**File:** `home/dot_config/fish/functions/weather.fish` (new file) + +```fish +dotfiles edit ~/.config/fish/functions/weather.fish +``` + +In the editor, write: +```fish +function weather --description "Show weather forecast" + curl -s "wttr.in/?format=3" +end +``` + +Save and close. `dotfiles edit` applies (function is immediately available) and +auto-commits. Test it: + +```fish +weather +``` + +**Expected result:** prints something like `Da Nang: ☀️ +31°C`. +The function is available in every new shell. The change is committed. + +### Walkthrough: change your Starship prompt + +**Goal:** shorten the directory display from 3 levels to 2. +**File:** `home/dot_config/starship.toml` + +```fish +dotfiles edit ~/.config/starship.toml +``` + +Edit the `[directory]` section: +```toml +[directory] +truncation_length = 2 +``` + +Save and close. + +**Expected result:** your prompt immediately shows shorter paths +(e.g. `~/w/dotfiles` instead of `~/workspace/tieubao/dotfiles`). +The change is auto-committed. + +### Walkthrough: add a VS Code extension + +**Goal:** install the GitHub Copilot extension and persist it. +**File:** `home/dot_config/code/extensions.txt` + +```fish +dotfiles edit ~/.config/code/extensions.txt +``` + +Add one extension ID per line (e.g. `github.copilot`). Save. Then: + +```fish +chezmoi apply # triggers the VS Code extension sync script +``` + +**Expected result:** VS Code installs the extension. The extensions +list is committed, so your next machine gets it too. + +### Walkthrough: switch Ghostty theme + +**Goal:** preview and switch terminal themes. +No file editing needed — use the built-in helper: + +```fish +ghostty-theme # lists available themes with numbers +ghostty-theme 5 # switch to theme #5 +``` + +**Expected result:** the terminal theme changes immediately (live +reload, no restart). The config file is updated in place. + +### Walkthrough: add an SSH host + +**Goal:** add a `staging` SSH host for quick access. +**File:** `home/dot_ssh/config.d/work-servers` + +```fish +dotfiles edit ~/.ssh/config.d/work-servers +``` + +Write: +``` +Host staging + HostName 10.0.1.50 + User deploy + IdentityFile ~/.ssh/id_ed25519 +``` + +Save and close. + +**Expected result:** `ssh staging` connects to 10.0.1.50. The main +`~/.ssh/config` includes everything in `config.d/` via `Include config.d/*`. +The change is auto-committed. + +--- + +## 6. Secrets management + +### The three tiers + +| Tier | When to use | Command | +|------|------------|---------| +| **Auto-loaded** | Env var in every shell session | `dotfiles secret add VAR "op://..."` | +| **Runtime** | Occasional CLI use, don't pollute every env | `op-env VAR "op://..."` | +| **One-off** | Quick inline use | `set -x VAR (op read "op://...")` | + +### Adding a new secret + +```fish +dotfiles secret add OPENAI_API_KEY "op://Private/OpenAI/credential" +``` + +

+ Secrets workflow +

+ +This command: +1. Creates the 1Password item if it doesn't exist (prompts for the value) +2. Registers the `op://` binding in `secrets.toml` +3. Runs `chezmoi apply` to render `secrets.fish` with the real value +4. Auto-commits the change (only the `op://` ref, never the value) + +Open a new shell (or `exec fish`) to pick up the variable. + +### Rotating a token + +Update the value in 1Password (app or CLI). Then: + +```fish +chezmoi apply ~/.config/fish/conf.d/secrets.fish +exec fish +``` + +No repo change needed — the `op://` reference hasn't changed. + +### Removing a secret + +```fish +dotfiles secret rm OPENAI_API_KEY # unregisters from secrets.toml, auto-commits +``` + +### Listing current secrets + +```fish +dotfiles secret list # shows all registered VAR → op:// bindings +``` + +### How it works under the hood + +

+ Secrets flow: template to machine +

+ +`secrets.toml` is a chezmoi data file loaded as `.secrets` in templates. +`secrets.fish.tmpl` iterates the registry and emits one `set -gx` per +entry, resolving each `op://` ref via `onepasswordRead`. The rendered +file at `~/.config/fish/conf.d/secrets.fish` contains real tokens but +never leaves your machine. + +--- + +## 7. Multi-machine setup + +### Deploying to a second Mac + +On the new machine: + +```bash +git clone https://github.com/dwarvesf/dotfiles ~/dotfiles +cd ~/dotfiles && ./install.sh +``` + +The setup wizard prompts for the same config (name, email, editor, +1Password). If you use 1Password, sign in first (`eval $(op signin)`), +and all secrets are pulled automatically. + +### Headless/server mode + +During `chezmoi init`, answer `headless = true`. This skips: +- GUI apps (Ghostty, VS Code, Zed) +- Mac App Store apps +- Dev casks (Docker, Figma, etc.) +- macOS defaults + +You get: Fish shell, CLI tools, git config, SSH config, tmux. + +### Keeping machines in sync + +On any machine: + +```fish +dotfiles update # git pull + chezmoi apply +``` + +Or manually: + +```fish +cd ~/dotfiles && git pull +chezmoi apply +``` + +### Machine-specific overrides + +chezmoi templates handle per-machine differences. The `.chezmoi` variable +provides hostname, OS, and arch. Example in a `.tmpl` file: + +``` +{{ if eq .chezmoi.hostname "work-mbp" }} +# work-specific config here +{{ end }} +``` + +The `headless` flag is the most common override — it gates entire +sections of the Brewfile and script execution. + +--- + +## 8. Troubleshooting + +Start with `dotfiles doctor` — it catches most issues. + +### "I edited the wrong file" + +You edited `~/.config/fish/config.fish` directly instead of the source. +Your change works now but will be lost on the next `chezmoi apply`. + +**Fix:** run `dotfiles drift` to detect the drift and re-absorb it into the source. + +### "chezmoi apply wants to overwrite my change" + +Same root cause: the deployed file differs from the source. Options: + +1. `dotfiles drift` — pull the deployed version back into source +2. `chezmoi merge ` — three-way merge +3. `chezmoi apply --force` — overwrite deployed with source (destructive) + +### "1Password errors" + +- **"not signed in"**: run `eval (op signin)` or `eval $(op signin)` in bash +- **"item not found"**: check vault name and item title match the `op://` path +- **"connect timeout"**: 1Password CLI needs internet for the first auth + +### "Template rendering failed" + +A `.tmpl` file is missing a variable. Usually means `chezmoi init` needs +to be re-run: + +```fish +chezmoi init +chezmoi apply +``` + +### "Brewfile didn't re-run" + +The brew script only fires when the Brewfile content changes (chezmoi +tracks a hash). Force it: + +```fish +brew bundle --file=~/.Brewfile +``` + +### "Fish function not found" + +The function file must be named exactly `FUNCTIONNAME.fish` in +`~/.config/fish/functions/`. Check: + +```fish +functions --names | grep yourfunction +ls ~/.config/fish/functions/yourfunction.fish +``` + +### "Shell startup is slow" + +Benchmark it: + +```fish +dotfiles bench +``` + +Common causes: 1Password CLI calls on every shell (move to auto-loaded +secrets), too many PATH additions, slow network for async prompt. + +### Nuclear options + +If something is deeply broken: + +```fish +# Re-run setup wizard (re-prompts for all config) +chezmoi init + +# Force apply everything (overwrites all deployed files) +chezmoi apply --force + +# Full reinstall (teardown + rebuild) +cd ~/dotfiles && ./install.sh --force +``` + +--- + +## 9. Lifecycle: install, update, uninstall + +### Install (first time) + +```bash +git clone https://github.com/dwarvesf/dotfiles ~/dotfiles +cd ~/dotfiles && ./install.sh +``` + +This installs Homebrew, chezmoi, runs the setup wizard, deploys all +configs (including `~/.claude/commands/dotfiles-sync.md`), installs +packages, sets Fish as default shell, and prints a summary. + +After install, `/dotfiles-sync` is available in Claude Code from any +directory. See [section 1](#1-the-llm-workflow). + +### Update (ongoing) + +**Primary (LLM-assisted):** run `/dotfiles-sync` in Claude Code. Claude +detects drift, you approve, it syncs. + +**Pull from remote** (new commits from another machine): + +```fish +dotfiles update # git pull + chezmoi apply +``` + +**Re-run setup wizard** (change name, email, editor, 1Password config): + +```fish +chezmoi init # re-prompts for all answers +chezmoi apply # deploy with new answers +``` + +**Reinstall from scratch** (teardown + rebuild): + +```bash +cd ~/dotfiles && ./install.sh --force +``` + +This removes chezmoi state and config, re-links, re-runs the wizard, +and re-applies everything. + +### Uninstall + +There is no automated uninstall. To remove this dotfiles setup: + +**1. Restore default shell** (if you're also removing Fish): + +```bash +chsh -s /bin/zsh +``` + +Skip this if you want to keep Fish as your shell. + +**2. Remove deployed configs:** + +```bash +# See what chezmoi manages +chezmoi managed + +# Remove all chezmoi-managed files from $HOME +chezmoi managed | while read f; do rm -f "$HOME/$f"; done + +# Remove chezmoi state and config +rm -rf ~/.local/share/chezmoi +rm -rf ~/.config/chezmoi +``` + +**3. Remove the Claude Code slash command:** + +```bash +rm -rf ~/.claude/commands/dotfiles-sync.md +``` + +**4. Remove the repo:** + +```bash +rm -rf ~/dotfiles +``` + +**5. Optionally uninstall Homebrew packages:** + +```bash +# See what was installed via the Brewfile +brew bundle list --file=~/.Brewfile + +# Remove everything in the Brewfile +brew bundle cleanup --force --file=~/.Brewfile + +# Or uninstall Homebrew entirely +/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/uninstall.sh)" +``` + +After uninstall, your Mac reverts to Zsh with default configs. Any +1Password secrets remain in your vault (unchanged). + +--- + +## 10. Architecture reference + +

+ Architecture overview +

+ +### Script execution order + +During `chezmoi apply`, scripts run in this order: + +1. `run_before_aa-init.sh` — resets the apply log +2. `run_before_ab-1password-check.sh` — validates 1Password CLI +3. `run_onchange_before_brew-bundle.sh` — `brew bundle` (triggers on Brewfile change) +4. **File deployment** — templates rendered, files copied +5. `run_once_after_*` — one-time setup (Fish shell, macOS defaults, MAS apps, toolchains) +6. `run_onchange_after_*` — VS Code extensions, Zed config (triggers on content change) +7. `run_after_zz-summary.sh` — styled apply summary with OK/warning/error counts + +`run_once_` scripts won't re-run unless their content changes. +`run_onchange_` scripts re-run when the template output changes. + +### How templates work + +Any file ending in `.tmpl` is rendered through Go's `text/template` +engine at apply time. chezmoi provides variables from: + +- `chezmoi init` answers (`.name`, `.email`, `.editor`, `.headless`, `.use_1password`) +- `.chezmoidata/*.toml` files (`.secrets` from `secrets.toml`) +- Built-in `.chezmoi.*` (hostname, OS, arch, username) + +Every `.tmpl` file validates required variables at the top with +`hasKey`/`fail` guards. Missing variables produce actionable error +messages instead of cryptic Go template errors. + +### External downloads + +Fish plugins and completions are defined in `.chezmoiexternal.toml` as +GitHub URLs. chezmoi downloads and caches them with a 30-day refresh. +Force re-download with `dotfiles refresh` or `chezmoi apply --refresh-externals`. + +### Design decisions + +Architectural choices are documented as ADRs in `docs/decisions/`: + +- [001: chezmoi over GNU Stow](decisions/001-chezmoi-over-stow.md) +- [002: Fish over Zsh](decisions/002-fish-over-zsh.md) +- [003: Ghostty over Kitty](decisions/003-ghostty-over-kitty.md) +- [004: 1Password for secrets](decisions/004-1password-for-secrets.md) +- [005: No plugin manager for Fish](decisions/005-no-plugin-manager-for-fish.md) +- [006: Auto-commit workflow](decisions/006-auto-commit-workflow.md) + +--- + +## Appendix: cheat sheet + +### Commands at a glance + +| I want to... | Command | Abbreviation | +|--------------|---------|-------------| +| **Batch sync everything** | **`/dotfiles-sync` (in Claude Code)** | | +| Edit any config | `dotfiles edit ` | `de` | +| Detect and fix drift | `dotfiles drift` | `dd` | +| Apply all changes | `dotfiles sync` | `ds` | +| Pull latest + apply | `dotfiles update` | `dfu` | +| See what would change | `dotfiles diff` | | +| Check health | `dotfiles doctor` | | +| Add a Homebrew package | `dotfiles edit ~/.Brewfile` | | +| Add a secret | `dotfiles secret add VAR "op://..."` | | +| List secrets | `dotfiles secret list` | | +| Remove a secret | `dotfiles secret rm VAR` | | +| Rotate a secret | Update in 1Password, then `chezmoi apply` | | +| Add a fish function | `dotfiles edit ~/.config/fish/functions/NAME.fish` | | +| Switch Ghostty theme | `ghostty-theme N` | | +| Benchmark startup | `dotfiles bench` | | +| Backup config | `dotfiles backup` | | +| Re-run setup wizard | `chezmoi init` | | + +### Key file locations + +| What | Source (repo) | Target (machine) | +|------|--------------|-------------------| +| Fish config | `home/dot_config/fish/config.fish.tmpl` | `~/.config/fish/config.fish` | +| Fish functions | `home/dot_config/fish/functions/` | `~/.config/fish/functions/` | +| Ghostty | `home/dot_config/ghostty/config` | `~/.config/ghostty/config` | +| Starship | `home/dot_config/starship.toml` | `~/.config/starship.toml` | +| tmux | `home/dot_config/tmux/tmux.conf` | `~/.config/tmux/tmux.conf` | +| Git | `home/dot_gitconfig.tmpl` | `~/.gitconfig` | +| Brewfile | `home/dot_Brewfile.tmpl` | `~/.Brewfile` | +| Secrets registry | `home/.chezmoidata/secrets.toml` | (data file, not deployed) | +| Rendered secrets | `home/dot_config/fish/conf.d/secrets.fish.tmpl` | `~/.config/fish/conf.d/secrets.fish` | diff --git a/docs/llm-dotfiles.md b/docs/llm-dotfiles.md new file mode 100644 index 0000000..7c57947 --- /dev/null +++ b/docs/llm-dotfiles.md @@ -0,0 +1,330 @@ +# LLM-maintained dotfiles + +A pattern for keeping dotfiles repos in sync using LLMs. + +This is an idea file. It describes a general pattern, not a specific +implementation. Copy it into your own LLM agent (Claude Code, Codex, +OpenCode, etc.) and adapt it to your stack. Your agent will figure out +the specifics. + +

+ LLM sync workflow +

+ +## The core idea + +Most dotfiles repos assume you'll manually keep the repo in sync with +your machine. Edit the source, run apply, commit, push. The tooling is +good: chezmoi, GNU Stow, yadm, bare git repos. But the workflow has a +fundamental flaw: it depends on human memory. + +In practice, you `brew install` something while debugging and forget to +update the Brewfile. You tweak a config file directly instead of going +through the dotfiles manager. You add an API key and never register it +in the secrets manager. After a few weeks, the repo is stale. After a +few months, it's fiction. + +The idea here is different. Instead of expecting you to maintain the +repo, **the LLM maintains it.** You operate your machine freely. +Periodically, you ask the LLM to catch up. It scans your machine, +detects everything that drifted, reports it in plain language, and waits +for your decisions. Then it syncs the changes back into the source, +commits, and pushes. + +The human curates and decides; the LLM does the bookkeeping. The tedious +part of dotfiles management isn't choosing your tools; it's keeping the +repo in sync with your choices. The LLM handles that. + +## Architecture + +Three layers: + +**Machine state** (the source of truth) -- what's actually installed +and configured on your computer right now. Brew packages, cask apps, +config files, shell functions, API keys, editor extensions. You change +this freely, without thinking about the repo. + +**The repo** (the persistent artifact) -- your dotfiles source tree. +Managed by chezmoi, Stow, yadm, or a bare git repo. This is what gets +deployed on a new machine. The LLM's job is to keep it accurate. + +**The schema** (the instructions) -- a document that tells the LLM how +to scan your machine, what to detect, how to report, and how to sync. +In Claude Code this lives in `CLAUDE.md` or a slash command file. In +other agents it might be a system prompt or an AGENTS.md. This is the +key configuration file; it's what turns a generic LLM into a dotfiles +maintainer. + +## Operations + +### Scan + +The LLM runs detection commands to compare machine state against the +repo. What to detect depends on your stack, but common dimensions: + +| Dimension | Detection approach | +|-----------|-------------------| +| Config drift | Compare deployed files against source (chezmoi status, diff) | +| Package drift | Compare installed packages against package list (brew leaves vs Brewfile) | +| App drift | Compare installed apps against tracked list (brew casks, snap, flatpak) | +| Extension drift | Compare editor extensions against tracked list | +| New files | Find files in managed directories that the dotfiles tool doesn't know about | +| Secret drift | Check for hardcoded keys that should be in a secrets manager | +| Stale entries | Find entries in the repo that no longer exist on the machine | + +The LLM runs these commands in parallel and collects the findings. No +custom scripts needed; the LLM calls the detection commands directly. + +### Report + +The LLM presents findings in plain language, grouped by category: + +``` +Dotfiles sync report + +Config drift (2 files): + - Zed settings — 2 new MCP servers added + - tmux config — changed prefix key + +New packages (5): + ollama, rclone, pandoc, ... + +Stale entries (3): + raycast, slack (not installed, in Brewfile) + +What should I do? +``` + +The report is conversational. The LLM adds context where it can: +"ollama is probably for local LLM testing." The user sees what changed +and why, not raw diffs. + +### Decide + +The LLM waits. It never syncs without approval. The user responds in +plain language: + +- "Add the new packages" +- "Drop raycast, I use Spotlight now" +- "Keep btop in the Brewfile, I want it on my next machine" +- "Sync the Zed config" +- "Do everything" + +This is the critical design choice: the LLM does the bookkeeping, but +the human makes the decisions. Fully automated sync is tempting but +dangerous; a bad config could propagate to every machine. + +### Sync + +The LLM executes the approved changes: re-adds config files, edits +package lists, updates extension lists, registers secrets. It uses the +dotfiles tool's native commands (chezmoi re-add, chezmoi add, etc.). + +### Log + +After each sync, the LLM appends an entry to a log file: + +```markdown +## [2026-04-14] sync + +Config: re-add Zed settings (2 new MCP servers) +Packages: add 5 (ollama, rclone, ...) +Stale: remove raycast, slack from Brewfile +``` + +The log serves two purposes. First, context for future syncs: the LLM +reads it at the start of each session and knows what happened recently. +Second, an audit trail that's easier to scan than git log. + +### Commit and push + +The LLM commits with a descriptive message summarizing all changes by +category. Pushing is manual: the LLM asks, the user confirms. + +## Example session + +``` +User: catch up with my dotfiles + +Claude: [scans machine, 30 seconds] + +Claude: Dotfiles sync report (2026-04-14) + + Config drift (1 file): + - Zed settings.json — 2 new MCP servers + + New brew packages (5): + ollama, rclone, pandoc, pipx, opencode + + Stale casks (2): + raycast, slack (in Brewfile but not installed) + + VS Code extensions (3 new): + docker.docker, ms-vsliveshare.vsliveshare, ocaml-platform + + What should I do? + +User: sync everything, drop raycast and slack + +Claude: [re-adds Zed settings, adds packages to Brewfile, + removes raycast/slack, updates extensions.txt, + appends to sync log, commits] + +Claude: Done. 1 commit ready. Push? + +User: push + +Claude: [pushes] +``` + +The user spoke 2 sentences. The LLM handled 6 file edits, a commit +message, and a push. + +## What stays manual + +- **Initial setup.** Clone, install, configure. The LLM can guide you + through it, but the first run is yours. +- **Destructive decisions.** The LLM will never auto-remove a package + or overwrite a config without asking. +- **Push.** Always manual. A bad push propagates to every machine. +- **New tool adoption.** If you switch from Zsh to Fish, that's a + design decision, not bookkeeping. Tell the LLM what you decided. + +## Design principles + +**LLM does bookkeeping, you make decisions.** The LLM never auto-syncs +without asking. It scans, reports, and waits. + +**Plain language over commands.** You say "drop raycast, I switched to +Spotlight." The LLM figures out which line to delete. + +**One session covers everything.** Packages, configs, extensions, +secrets, all in one pass. No separate workflows per category. + +**The repo is the persistent artifact.** It compounds over time. Every sync makes it more accurate. + +**No daemon, no watcher.** You trigger the sync when you want it. This +is intentional: dotfiles sync should be a conscious decision, not +background automation. + +## Setting it up + +### Prerequisites + +You need two things: + +1. **A dotfiles repo** managed by any tool (chezmoi, Stow, yadm, bare + git). If you don't have one yet, start with + [chezmoi](https://www.chezmoi.io/quick-start/). +2. **An LLM agent with shell access.** Claude Code, OpenAI Codex, + OpenCode, or similar. The agent needs to run commands on your + machine and edit files. + +### Step 1: Create the sync command + +Create a slash command file in two places: + +``` +~/dotfiles/.claude/commands/dotfiles-sync.md # project-level (works in repo dir) +~/.claude/commands/dotfiles-sync.md # user-level (works from any dir) +``` + +If you use chezmoi, add it to your source tree at +`home/dot_claude/commands/dotfiles-sync.md` and chezmoi deploys it to +`~/.claude/commands/` on apply. Otherwise, copy the file manually. + +This file is the schema. It teaches the LLM what to scan, how to +report, and how to sync. Write it as a prompt that instructs the LLM +to: + +1. Read the sync log for context +2. Run detection commands for each drift dimension +3. Diff results against the repo state +4. Format a plain-language report +5. Wait for user decisions +6. Execute approved changes +7. Append to the sync log +8. Commit and ask before pushing + +The detection commands depend on your stack: + +| Stack | Drift detection | +|-------|----------------| +| chezmoi | `chezmoi status` | +| GNU Stow | `diff -r ~/.dotfiles/ ~/` | +| Homebrew | `brew leaves` vs Brewfile | +| apt | `apt-mark showmanual` vs packages list | +| VS Code | `code --list-extensions` vs extensions.txt | +| Fish/Zsh functions | `ls` functions dir vs managed list | +| Secrets | grep for hardcoded keys in shell config | + +A working reference implementation for chezmoi + Homebrew + Fish is at +[dwarvesf/dotfiles/.claude/commands/dotfiles-sync.md](https://github.com/dwarvesf/dotfiles/blob/main/.claude/commands/dotfiles-sync.md). + +### Step 2: Create the sync log + +``` +docs/sync-log.md +``` + +Start with a header. The LLM appends entries after each sync: + +```markdown +# Dotfiles sync log + +--- + +## [2026-04-14] sync + +Config: re-add Zed settings +Packages: add ollama, rclone +Stale: remove raycast + +--- +``` + +### Step 3: Add project context (optional but recommended) + +If your LLM agent supports project instructions (CLAUDE.md, AGENTS.md, +etc.), add a brief section explaining your repo layout: where source +files live, which are templates, how secrets work. This helps the LLM +make better decisions during sync. + +### Step 4: Run your first sync + +``` +/dotfiles-sync +``` + +Or just tell the agent: "catch up with my dotfiles." + +### Adapting to other stacks + +The pattern works with any combination: + +| Layer | Options | +|-------|---------| +| Dotfiles manager | chezmoi, GNU Stow, yadm, bare git, Nix Home Manager | +| Shell | Fish, Zsh, Bash | +| Package manager | Homebrew, apt, pacman, nix | +| Secrets | 1Password, Bitwarden, SOPS, age, pass | +| LLM agent | Claude Code, OpenAI Codex, OpenCode, any agent with shell access | + +The detection commands change; the pattern doesn't. + +## Why this works + +The tedious part of dotfiles management is not choosing tools; it's the +ongoing maintenance. Updating the Brewfile after every `brew install`. +Committing after every config tweak. Noticing when a deployed file +drifted from the source. Nobody does this consistently because the cost +of each individual sync is low but the friction is constant. + +LLMs are good at exactly this kind of work: scanning for differences, +formatting reports, making mechanical edits to text files, writing +commit messages. The human's job is to operate their machine and make +decisions. The LLM's job is everything in between. + +The result: a dotfiles repo that's always accurate, a sync log that +documents your machine's evolution, and a workflow that takes 30 seconds +instead of being silently skipped. diff --git a/docs/session_state.md b/docs/session_state.md deleted file mode 100644 index 936abae..0000000 --- a/docs/session_state.md +++ /dev/null @@ -1,62 +0,0 @@ -# Session State - -Updated: 2026-04-02 -Project: dwarvesf/dotfiles -Phase: Stable, expanding - -## POSITION - -### Phase -Mature. All 12 original features (F-01 through F-12) and 5 cleanup specs (R-03 through R-09) are shipped except F-03 (bootstrap without git), F-04 (Brewfile split), and F-12 (v0.1.0 tag). 24+ commits on main. CI passing weekly. - -### What is decided -- chezmoi as dotfiles manager (over GNU Stow, yadm, Nix) -- Fish shell (over Zsh/Oh My Zsh/Prezto) for startup speed and built-in features -- Ghostty terminal (over Kitty, WezTerm, iTerm2) for native macOS feel + performance -- 1Password for secrets (op:// template injection, never plaintext in git) -- XDG Base Directory compliance where possible -- Fish plugins via .chezmoiexternal.toml (no plugin manager, ADR-005) -- mise for language version management (.tool-versions) -- age encryption for complex sensitive files (encrypt-setup command exists) -- Modular SSH config with config.d/ and 1Password agent -- tmux with C-a prefix, vim nav, fzf session picker - -### What is still open -- Whether to add Starship prompt (recommended, not yet added) -- Brewfile split into base/dev/apps layers (F-04) -- Window tiling manager (Aerospace or similar) -- Multi-machine profiles (chezmoi tags for work vs personal) - -### Codebase status -Production-grade dotfiles system with: -- Idempotent install.sh (--check, --force flags, exit codes) -- CI pipeline (shellcheck + dry-run on macOS, weekly schedule) -- `dotfiles` CLI with 9 subcommands (edit, diff, sync, status, cd, refresh, add, doctor, encrypt-setup) -- Drift detection (dotfiles-drift function) -- 9 custom Fish functions with tab completions -- 5 ADRs documenting key decisions -- 14 external Fish plugins/completions via .chezmoiexternal.toml -- ~80 Homebrew packages + 26 MAS apps - -## CONTEXT - -### User preferences -- Brutally honest feedback, no yes-man behavior -- Visual learner, likes diagrams and charts -- Light theme for visual elements -- No em dashes in text -- Leadership at Dwarves Foundation, based in Da Nang/Saigon -- Uses Claude Code heavily, has custom skills/MCP infrastructure -- Familiar with chezmoi, not a beginner - -### Constraints -- macOS primary (Apple Silicon) -- Must work with 1Password or without (graceful degradation) -- Public repo (no plaintext secrets ever) -- XDG-compliant where tools support it -- Fish as default shell - -## INTENT - -### Next steps (v0.2.0 roadmap) -See docs/tasks.md for the full backlog. diff --git a/docs/specs/S-31-user-guide.md b/docs/specs/S-31-user-guide.md new file mode 100644 index 0000000..679fadb --- /dev/null +++ b/docs/specs/S-31-user-guide.md @@ -0,0 +1,154 @@ +--- +id: S-31 +title: User guide +type: docs +status: done +--- + +# Comprehensive user guide + +### Problem + +The repo has good docs scattered across multiple files: +- **README.md** — quick start, what's included, daily usage summary +- **docs/customization.md** — the editing workflow, secrets, troubleshooting +- **docs/decisions/*.md** — 6 ADRs explaining architectural choices +- **docs/tool-comparison.md** — why each tool over alternatives +- **7 SVG workflow diagrams** — visual references + +But three user journeys are poorly served: + +1. **Day-1 newcomer**: installed, now staring at a fish prompt. Doesn't know what tools are available, what the keybindings are, what the fish plugins do, or how chezmoi's two-layer model works. README says "what's included" as a table but doesn't orient them. + +2. **Day-2 customizer**: wants to change things but keeps hitting chezmoi's source-vs-target confusion. `customization.md` helps but lacks a guided walkthrough with a real example end-to-end. + +3. **Returning user**: hasn't touched dotfiles in 6 months. Needs a quick refresher on the workflow without re-reading everything. Needs a cheat sheet. + +### Approach + +Expand `docs/customization.md` into `docs/guide.md` — a single comprehensive user guide. Don't create a separate manual that duplicates existing content. The README stays as the "front door" (quick start + overview) and links to the guide for depth. + +### Proposed structure + +``` +docs/guide.md +├── 1. How this works (mental model) +│ ├── The two layers: source vs target +│ ├── What chezmoi does (diagram: dotfiles_chezmoi_model.svg) +│ ├── Where things live (directory map) +│ └── Templates and secrets (diagram: dotfiles_secrets_flow.svg) +│ +├── 2. Your first 30 minutes +│ ├── What just got installed (tool tour with one-liner for each) +│ ├── Fish shell orientation (keybindings, abbreviations, autosuggestions) +│ ├── Terminal: Ghostty basics (theme switching, splits, font) +│ ├── Editor setup check (VS Code / Zed / nvim) +│ └── Verify everything works: dotfiles doctor +│ +├── 3. Daily workflows +│ ├── Editing any config (diagram: dotfiles_dfe_workflow.svg) +│ ├── Adding a Homebrew package (diagram: dotfiles_workflow_brew.svg) +│ ├── Handling drift (diagram: dotfiles_dfs_workflow.svg) +│ ├── The dotfiles CLI (subcommand reference table) +│ └── Cheat sheet (one-page quick reference) +│ +├── 4. Customization cookbook +│ ├── Quick-change table (migrated from customization.md) +│ ├── Walkthrough: add a new fish function +│ ├── Walkthrough: change your Starship prompt +│ ├── Walkthrough: add a new Homebrew package +│ ├── Walkthrough: add a VS Code extension +│ ├── Walkthrough: switch Ghostty theme +│ └── Walkthrough: add an SSH host +│ +├── 5. Secrets management +│ ├── The three tiers (auto-loaded, runtime, one-off) +│ ├── Adding a new secret (diagram: dotfiles_workflow_secrets.svg) +│ ├── Rotating a token +│ ├── Removing a secret +│ └── How it works under the hood +│ +├── 6. Multi-machine setup +│ ├── Deploying to a second Mac +│ ├── Headless/server mode +│ ├── Keeping machines in sync (git pull + apply) +│ └── Machine-specific overrides (chezmoi templates) +│ +├── 7. Troubleshooting +│ ├── Common issues (migrated + expanded from customization.md) +│ ├── "I edited the wrong file" +│ ├── "chezmoi apply wants to overwrite my change" +│ ├── "1Password errors" +│ ├── "Template rendering failed" +│ └── Nuclear options (reinit, state reset) +│ +├── 8. Architecture reference +│ ├── Directory layout (diagram: dotfiles_architecture.svg) +│ ├── Script execution order +│ ├── How templates work +│ ├── How external downloads work +│ └── Design decisions (links to ADRs) +│ +└── Appendix: cheat sheet + ├── Commands at a glance (1-page table) + ├── File locations + └── "I want to X" → command mapping +``` + +### What gets migrated vs written new + +| Section | Source | Work needed | +|---------|--------|-------------| +| Mental model | New | Write ~300 words wrapping the chezmoi model diagram | +| First 30 minutes | New | Write tool tour, fish orientation, editor check | +| Daily workflows | customization.md (partial) | Restructure + add diagrams | +| Customization cookbook | customization.md quick-change table | Migrate table + write 6 walkthroughs | +| Secrets management | customization.md secrets section | Migrate + add diagrams | +| Multi-machine | New | Write ~400 words | +| Troubleshooting | customization.md troubleshooting | Migrate + expand | +| Architecture | CLAUDE.md + README | Extract into reader-friendly prose | +| Cheat sheet | New | Compile from all sections | + +### What happens to existing docs + +- `docs/customization.md` → **deleted**, content migrated to `docs/guide.md` +- `README.md` → **updated**: "Customization" section links to guide instead of duplicating +- `docs/decisions/*.md` → **unchanged**, linked from guide section 8 +- `docs/tool-comparison.md` → **unchanged**, linked from guide section 2 + +### Diagrams placement + +| Diagram | Section | +|---------|---------| +| `dotfiles_chezmoi_model.svg` | 1. Mental model | +| `dotfiles_bootstrap_flow.svg` | 1. Mental model (or section 8) | +| `dotfiles_secrets_flow.svg` | 1. Mental model + 5. Secrets | +| `dotfiles_dfe_workflow.svg` | 3. Daily workflows | +| `dotfiles_dfs_workflow.svg` | 3. Daily workflows | +| `dotfiles_workflow_brew.svg` | 3. Daily workflows + 4. Cookbook | +| `dotfiles_workflow_config.svg` | 3. Daily workflows | +| `dotfiles_workflow_secrets.svg` | 5. Secrets management | +| `dotfiles_architecture.svg` | 8. Architecture | + +### Acceptance criteria + +- [ ] `docs/guide.md` exists with all 8 sections + appendix +- [ ] `docs/customization.md` removed, no broken links +- [ ] README "Customization" section links to `docs/guide.md` +- [ ] All 9 SVG diagrams embedded in appropriate sections +- [ ] Each walkthrough in section 4 has: goal, file to edit, exact command, expected result +- [ ] Cheat sheet fits in one screen (terminal height) +- [ ] No content duplication between README and guide (README = overview, guide = depth) +- [ ] `docs/guide.md` reads well as a standalone doc (no assumed context from README) +- [ ] Troubleshooting covers at least 8 common issues +- [ ] Multi-machine section covers fresh Mac, headless server, and sync workflow + +### Estimated size + +~2000-2500 words (excluding diagrams). Target: 15-minute read for the full guide, 2-minute skim via cheat sheet. + +### Non-goals + +- Video/screencast (that's S-29: VHS demo) +- Contributing guide (not needed yet, single-user repo) +- Plugin development guide (no custom plugin system) diff --git a/docs/specs/S-32-claude-assisted-sync.md b/docs/specs/S-32-claude-assisted-sync.md new file mode 100644 index 0000000..4949c95 --- /dev/null +++ b/docs/specs/S-32-claude-assisted-sync.md @@ -0,0 +1,301 @@ +--- +id: S-32 +title: Claude-assisted dotfiles sync +type: workflow +status: planned +--- + +# Claude-assisted dotfiles sync + +## The core idea + +Most dotfiles setups assume the user will manually keep the repo in sync +with their machine. Edit the source, run apply, commit, push. In +practice, nobody does this consistently. You `brew install` something in +the heat of debugging, tweak a config file directly, add an API key for +a new tool. The changes accumulate on your machine but never make it +back to the repo. After a few weeks, the repo is stale. + +The idea here is different. Instead of expecting the user to maintain +the repo, **the LLM maintains it.** You operate your machine naturally. +Periodically (weekly, or whenever you think of it), you ask Claude to +catch up. Claude scans your machine, detects everything that drifted, +reports it in plain language, and waits for your decisions. Then Claude +syncs the changes back into the chezmoi source, commits, and pushes. + +This is the same pattern as the LLM Wiki: the human curates and +decides, the LLM does the bookkeeping. The tedious part of dotfiles +management is not choosing your tools — it's keeping the repo in sync +with your choices. Claude handles that. + +## Architecture + +Three layers, mirroring the LLM Wiki: + +**Machine state** (the "raw sources") — what's actually installed and +configured on your Mac right now. Brew packages, cask apps, config +files, secrets, shell functions. This is the source of truth. You +change it freely by installing tools, editing configs, whatever. + +**The repo** (the "wiki") — the chezmoi source at `~/dotfiles/home/`. +This is the persistent artifact that Claude maintains. It should reflect +your machine state, but it often lags behind. Claude's job is to close +the gap. + +**The schema** (the "CLAUDE.md") — instructions that tell Claude how to +scan, what to detect, how to report, and how to sync. Lives in +`.claude/commands/dotfiles-sync.md` as a slash command, plus detection +logic in the CLAUDE.md project instructions. + +## Operations + +### Scan (detect) + +Claude runs detection across multiple dimensions: + +| Dimension | Detection method | What it finds | +|-----------|-----------------|---------------| +| **Config drift** | `chezmoi status` | Managed files where deployed differs from source (Ghostty, Starship, tmux, Git, Zed, SSH, fish config, etc.) | +| **New brew packages** | `brew leaves` vs `dot_Brewfile.tmpl` | Packages you installed but didn't track | +| **Removed brew packages** | `dot_Brewfile.tmpl` vs `brew leaves` | Packages in Brewfile but no longer installed | +| **New casks** | `brew list --cask` vs `dot_Brewfile.tmpl` | GUI apps you installed but didn't track | +| **Removed casks** | `dot_Brewfile.tmpl` vs `brew list --cask` | Casks in Brewfile but no longer installed | +| **VS Code extensions** | `code --list-extensions` vs `extensions.txt` | New or removed extensions | +| **New fish functions** | `ls ~/.config/fish/functions/` vs `chezmoi managed` | Functions created outside chezmoi | +| **New SSH config** | `ls ~/.ssh/config.d/` vs `chezmoi managed` | SSH host configs added directly | +| **Secrets** | `secrets.toml` entries vs `op read` | Stale or broken secret refs | +| **Env vars** | Scan fish config for `set -gx` with hardcoded keys | API keys that should be in 1Password | + +Note: `chezmoi status` covers most config drift (any file chezmoi +already manages). The "new files" checks cover the gap: files that +exist on the machine but chezmoi doesn't know about yet. + +macOS defaults (`defaults write`) are not detectable — those are +fire-and-forget commands with no clean diff mechanism. + +Each scan produces a structured finding. Claude collects all findings +into a plain-language report. + +### Report (review) + +Claude presents findings grouped by category, not as raw diffs. Example: + +``` +Dotfiles sync report (2026-04-14) + +Config drift (1 file): + - Zed settings.json — modified outside chezmoi + (MCP server config changed, 2 new servers added) + +New packages (25 brew, 10 casks): + Brew: chezmoi, ollama, rclone, pandoc, opencode, ... + Cask: claude, cursor, calibre, codexbar, ... + +Stale entries (9 brew, 9 casks): + Brew: age, btop, caddy, ... (in Brewfile but not installed) + Cask: raycast, slack, meetingbar, ... (in Brewfile but not installed) + +New VS Code extensions (3): + github.copilot-chat, ms-python.debugpy, ... + +No secret drift detected. + +What would you like me to do? +``` + +The report is conversational. Claude may add context: "ollama was +probably installed for local LLM testing" or "raycast might have been +replaced by spotlight." The user decides what to sync. + +### Sync (act) + +Based on the user's decisions, Claude executes: + +| Action | Method | +|--------|--------| +| Absorb config drift | `chezmoi re-add ` | +| Add brew packages to Brewfile | Edit `dot_Brewfile.tmpl`, add `brew "pkg"` lines | +| Remove stale Brewfile entries | Edit `dot_Brewfile.tmpl`, delete lines | +| Add casks to Brewfile | Edit `dot_Brewfile.tmpl`, add `cask "app"` lines | +| Remove stale cask entries | Edit `dot_Brewfile.tmpl`, delete lines | +| Sync VS Code extensions | Update `extensions.txt` | +| Register new secrets | Append to `secrets.toml` | +| Track new fish functions | `chezmoi add ` | + +Each action modifies the chezmoi source. Claude batches related changes +and commits with a descriptive message: + +``` +chore(sync): weekly dotfiles sync + +Config: + - re-add Zed settings (2 new MCP servers) + +Brewfile: + - add 25 packages: chezmoi, ollama, rclone, ... + - add 10 casks: claude, cursor, calibre, ... + - remove 9 stale packages: age, btop, ... + - remove 9 stale casks: raycast, slack, ... + +VS Code: + - add 3 extensions +``` + +### Log (record) + +After every sync, Claude appends an entry to `docs/sync-log.md`. This +is an append-only chronological record of what changed and when. + +```markdown +## [2026-04-14] sync + +Config: + - re-add Zed settings (2 new MCP servers) + +Brewfile: + - add 25 packages: chezmoi, ollama, rclone, ... + - remove 9 stale: raycast, slack, ... + +Secrets: + - no changes + +--- +``` + +The log serves two purposes: + +**Context for future syncs.** Claude reads the log at the start of each +session. "Last sync was 2 weeks ago, you added ollama and cursor." This +helps Claude spot patterns ("you keep installing ML tools, want me to +add a comment group in the Brewfile?") and ask smarter questions. + +**Audit trail.** "When did I add rclone?" Grep the log instead of +digging through git history. Each entry starts with a consistent +`## [date] sync` prefix so it's parseable with unix tools. + +The log is committed alongside the sync changes. It's part of the repo, +not ephemeral. + +### Publish (push) + +Claude asks before pushing. The user can review the commit, amend, or +just say "push it." + +## The sync session flow + +A typical sync session looks like this: + +``` +User: /dotfiles-sync +Claude: [runs scan, produces report] +Claude: "Here's what I found. What should I do?" +User: "Add the new packages. Drop raycast and slack, I use Spotlight now. + Keep btop in the Brewfile even though it's not installed, I want it + on my next machine. Sync the Zed config." +Claude: [executes sync, commits] +Claude: "Done. 1 commit ready. Push?" +User: "Push." +Claude: [pushes] +``` + +The user speaks in plain language. Claude translates to chezmoi/git +operations. No commands to remember. + +## What happens to existing commands + +The manual commands (`dfe`, `dfs`, `add-secret`, etc.) stay in the repo +as **escape hatches** for when you're not in a Claude session: + +| Scenario | Tool | +|----------|------| +| Weekly sync, batch changes | `/dotfiles-sync` (Claude) | +| Quick edit during a Claude session | Ask Claude directly | +| On a plane, no Claude | `dfe`, `dfs`, `dotfiles` CLI | +| SSH into a server | `dotfiles` CLI | +| CI/CD | `chezmoi apply` directly | + +The guide and README reframe: Claude-assisted sync is the primary +workflow. Manual commands are the fallback for offline/headless use. + +## What to build + +### Phase 1: the slash command (MVP) + +| Artifact | Purpose | +|----------|---------| +| `.claude/commands/dotfiles-sync.md` | Slash command prompt template. Tells Claude what to scan, how to report, how to sync. | +| `docs/sync-log.md` | Append-only sync history. Created on first sync. | + +The slash command is the core artifact. It instructs Claude to: + +1. Read `docs/sync-log.md` for context on last sync +2. Run detection commands (chezmoi status, brew leaves, code --list-extensions, ls key dirs, etc.) +3. Diff against the repo state +4. Format a plain-language report +5. Wait for user decisions +6. Execute sync actions +7. Append to `docs/sync-log.md` +8. Commit and optionally push + +No shell scripts needed. Claude runs the detection commands directly +via Bash tool. The intelligence is in the prompt, not in code. + +### Phase 2: scan helper (optional optimization) + +If the scan step takes too long interactively, extract it into a fish +function that outputs structured JSON: + +```fish +dotfiles scan # outputs machine state as JSON +``` + +Claude reads the JSON instead of running 8 separate commands. This is +an optimization, not a requirement. Start without it. + +### Phase 3: scheduled sync (optional) + +Use Claude Code's cron/schedule feature to run the scan weekly and +notify the user if drift is detected. The user can then start a sync +session or ignore it. + +## Design principles + +**Claude does the bookkeeping, you make the decisions.** Claude never +auto-syncs without asking. It scans, reports, and waits. You decide +what to sync, what to drop, what to keep. + +**Plain language over commands.** You say "drop raycast, I switched to +Spotlight." Claude figures out which line to delete from the Brewfile. + +**One sync session covers everything.** Brew packages, casks, configs, +extensions, secrets — all in one pass. No separate workflows for +different types of drift. + +**The repo is the persistent artifact.** Like the LLM Wiki, the repo +compounds over time. Every sync session makes it more accurate. Claude +writes good commit messages so the history is readable. + +**No daemon, no watcher, no automation.** You trigger the sync when you +want it. This is intentional: dotfiles sync should be a conscious +decision, not a background process that might commit garbage. + +## Acceptance criteria + +- [ ] `.claude/commands/dotfiles-sync.md` exists +- [ ] Running `/dotfiles-sync` produces a scan report +- [ ] Report covers: config drift, brew packages, casks, VS Code extensions, new fish functions, new SSH configs, secrets, env vars +- [ ] Claude reads `docs/sync-log.md` for context at start of scan +- [ ] Claude waits for user decisions before making changes +- [ ] Sync modifies the correct chezmoi source files +- [ ] Commit message summarizes all changes by category +- [ ] Claude appends entry to `docs/sync-log.md` after sync +- [ ] Claude asks before pushing +- [ ] Works as a normal conversation too ("catch up with my dotfiles") +- [ ] Guide updated to describe Claude-assisted sync as primary workflow + +## Non-goals + +- Automated/scheduled sync (Phase 3, not MVP) +- `dotfiles scan` fish helper (Phase 2, not MVP) +- Syncing across multiple machines (separate concern) +- Replacing `chezmoi apply` (that's deployment, not sync) diff --git a/docs/sync-log.md b/docs/sync-log.md new file mode 100644 index 0000000..2f4b0a7 --- /dev/null +++ b/docs/sync-log.md @@ -0,0 +1,22 @@ +# Dotfiles sync log + +Append-only record of Claude-assisted sync sessions. Each entry logs +what changed and when. Read by Claude at the start of each sync for +context. + +--- + +## [2026-04-14] sync + +Config: + - re-add Zed settings.json (MCP server changes, local edits absorbed) + +VS Code extensions: + - add 5: docker.docker, dwarvesf.md-ar-ext, ms-vsliveshare.vsliveshare, ocamllabs.ocaml-platform, openai.openai-chatgpt-adhoc + +Fish functions: + - removed 5 orphaned standalone functions from machine (add-secret, dfe, dfs, list-secrets, rm-secret) — consolidated into dotfiles subcommands + +Brew/casks: deferred to next sync + +--- diff --git a/docs/tasks.md b/docs/tasks.md index 85ee454..988afce 100644 --- a/docs/tasks.md +++ b/docs/tasks.md @@ -1,6 +1,6 @@ # Task Backlog: dwarvesf/dotfiles -Updated: 2026-04-04 +Updated: 2026-04-14 @@ -29,6 +29,8 @@ Updated: 2026-04-04 - [x] S-21: Consolidate toolchain scripts into install-toolchains.sh - [x] S-22: Gum TUI onboarding - [x] S-23: Error message system (gum-styled output, template guards, apply summary) +- [x] Data-driven secret registry (secrets.toml, add-secret, rm-secret, list-secrets) +- [x] Auto-commit workflow (dfe auto-commits, dfs reverse drift sync, ADR-006) ## Next up @@ -39,6 +41,8 @@ Updated: 2026-04-04 - [x] S-28: README tool showcase icons — skillicons.dev + shields.io badges - [ ] S-29: VHS terminal demo — animated GIF of install wizard - [x] S-30: Verification loop — CLAUDE.md rules, subagent, hooks, slash command +- [x] S-31: User guide — comprehensive manual replacing customization.md +- [ ] S-32: Claude-assisted dotfiles sync — LLM scans drift, reports, syncs on approval ## Backlog (no immediate plans) diff --git a/home/.chezmoidata/secrets.toml b/home/.chezmoidata/secrets.toml new file mode 100644 index 0000000..c550423 --- /dev/null +++ b/home/.chezmoidata/secrets.toml @@ -0,0 +1,5 @@ +[secrets] +# Key = "op://vault/item/field" — one line per secret. +# Managed by the `add-secret` / `rm-secret` fish functions. +# Values are 1Password references, not secrets, so this file is safe to commit. +CLOUDFLARE_API_TOKEN = "op://Private/Cloudflare R2/credential" diff --git a/home/.chezmoiscripts/run_after_zz-summary.sh b/home/.chezmoiscripts/run_after_zz-summary.sh index e6b5fe1..fa9b532 100644 --- a/home/.chezmoiscripts/run_after_zz-summary.sh +++ b/home/.chezmoiscripts/run_after_zz-summary.sh @@ -30,10 +30,13 @@ if [ "$warn_count" -eq 0 ] && [ "$fail_count" -eq 0 ]; then # All OK — short message if _has_gum; then BODY=$(_gum style --foreground 78 --bold "✓ dotfiles apply complete — all OK") + TIP=$(_gum style --faint " Tip: use /dotfiles-sync in Claude Code to detect drift and keep the repo current") + BODY=$(_gum join --vertical "$BODY" "" "$TIP") _gum style --border rounded --border-foreground $border_color --padding "1 2" --margin "1 0" "$BODY" else echo "" printf '\033[38;5;78m ✓ dotfiles apply complete — all OK\033[0m\n' + printf '\033[38;5;245m Tip: use /dotfiles-sync in Claude Code to detect drift and keep the repo current\033[0m\n' echo "" fi exit 0 diff --git a/home/dot_claude/commands/dotfiles-sync.md b/home/dot_claude/commands/dotfiles-sync.md new file mode 100644 index 0000000..b34223e --- /dev/null +++ b/home/dot_claude/commands/dotfiles-sync.md @@ -0,0 +1,152 @@ +You are maintaining a chezmoi-managed dotfiles repo. The user operates their Mac freely (installs packages, edits configs, adds API keys). Your job is to detect what changed on the machine, report it clearly, and sync approved changes back into the repo. + +## Step 1: Read context + +Read `docs/sync-log.md` to understand when the last sync happened and what changed. If the file doesn't exist, this is the first sync. + +## Step 2: Scan for drift + +Run these detection commands in parallel where possible: + +### Config drift +```bash +chezmoi status 2>/dev/null +``` +Look for lines starting with ` M` (modified) or `MM` (modified both sides). + +### Brew packages +```bash +# Installed but not in Brewfile +comm -23 <(brew leaves | sort) <(grep '^brew "' ~/.Brewfile 2>/dev/null | sed 's/brew "//;s/".*//' | sort) + +# In Brewfile but not installed +comm -13 <(brew leaves | sort) <(grep '^brew "' ~/.Brewfile 2>/dev/null | sed 's/brew "//;s/".*//' | sort) +``` + +### Cask apps +```bash +# Installed but not in Brewfile +comm -23 <(brew list --cask 2>/dev/null | sort) <(grep '^cask "' ~/.Brewfile 2>/dev/null | sed 's/cask "//;s/".*//' | sort) + +# In Brewfile but not installed +comm -13 <(brew list --cask 2>/dev/null | sort) <(grep '^cask "' ~/.Brewfile 2>/dev/null | sed 's/cask "//;s/".*//' | sort) +``` + +### VS Code extensions +```bash +# Installed but not tracked +comm -23 <(code --list-extensions 2>/dev/null | sort) <(sort ~/.config/code/extensions.txt 2>/dev/null) + +# Tracked but not installed +comm -13 <(code --list-extensions 2>/dev/null | sort) <(sort ~/.config/code/extensions.txt 2>/dev/null) +``` + +### New fish functions (not managed by chezmoi) +```bash +# Functions on disk but not in source +comm -23 <(ls ~/.config/fish/functions/ 2>/dev/null | sort) <(chezmoi managed | grep 'fish/functions/' | xargs -I{} basename {} | sort) +``` + +### New SSH config fragments +```bash +# SSH config.d files not managed +comm -23 <(ls ~/.ssh/config.d/ 2>/dev/null | sort) <(chezmoi managed | grep 'ssh/config.d/' | xargs -I{} basename {} | sort) +``` + +### Hardcoded secrets in fish config +```bash +# Look for set -gx with what looks like API keys (long alphanumeric strings) +grep -n 'set -gx.*[A-Za-z0-9_]\{20,\}' ~/.config/fish/config.fish ~/.config/fish/conf.d/*.fish 2>/dev/null | grep -v 'onepasswordRead\|op://' || true +``` + +## Step 3: Report + +Present findings in plain language, grouped by category. For each category, show: +- What changed (specific names, not counts) +- Brief context if you can infer it ("ollama is probably for local LLM testing") + +Use this format: + +``` +Dotfiles sync report (YYYY-MM-DD) + +Config drift (N files): + - path — what changed (brief description of the diff) + +New packages (N brew, N casks): + Brew: pkg1, pkg2, ... + Cask: app1, app2, ... + +Stale entries (N brew, N casks): + Brew: pkg1, pkg2, ... (in Brewfile but not installed) + Cask: app1, app2, ... (in Brewfile but not installed) + +VS Code extensions: + New: ext1, ext2, ... + Removed: ext1, ... + +New fish functions (N): + func1, func2, ... + +New SSH configs (N): + host1, host2, ... + +Secrets: + [any findings or "no issues"] + +What would you like me to do? +``` + +If a category has no findings, omit it from the report. + +## Step 4: Wait for decisions + +Do NOT make any changes yet. Ask the user what to do. They'll respond in plain language: +- "Add the new packages" +- "Drop raycast and slack" +- "Keep btop in Brewfile even though not installed" +- "Sync the Zed config" +- "Do it all" + +## Step 5: Execute + +Based on the user's decisions: + +| Action | Method | +|--------|--------| +| Absorb config drift | `chezmoi re-add ` | +| Add brew packages | Edit `home/dot_Brewfile.tmpl`, add `brew "pkg"` in correct section | +| Remove stale brew | Edit `home/dot_Brewfile.tmpl`, delete lines | +| Add casks | Edit `home/dot_Brewfile.tmpl`, add `cask "app"` in correct section | +| Remove stale casks | Edit `home/dot_Brewfile.tmpl`, delete lines | +| Sync VS Code extensions | Update `home/dot_config/code/extensions.txt` | +| Track fish functions | `chezmoi add ~/.config/fish/functions/NAME.fish` | +| Track SSH configs | `chezmoi add ~/.ssh/config.d/NAME` | +| Register secrets | Append to `home/.chezmoidata/secrets.toml` | + +When editing the Brewfile, preserve the existing section structure (base/dev/apps). Place new entries in the appropriate section. + +## Step 6: Log + +Append an entry to `docs/sync-log.md`: + +```markdown +## [YYYY-MM-DD] sync + +[Category]: + - [what changed] + +--- +``` + +## Step 7: Commit + +Stage all changes and commit with a descriptive message: + +``` +chore(sync): dotfiles sync YYYY-MM-DD + +[Summary of changes by category] +``` + +Then ask: "Push to remote?" Only push if the user confirms. diff --git a/home/dot_config/code/extensions.txt b/home/dot_config/code/extensions.txt index 555676a..31c4f2f 100644 --- a/home/dot_config/code/extensions.txt +++ b/home/dot_config/code/extensions.txt @@ -1,6 +1,8 @@ anthropic.claude-code azemoh.one-monokai davidanson.vscode-markdownlint +docker.docker +dwarvesf.md-ar-ext github.codespaces github.vscode-github-actions golang.go @@ -19,5 +21,8 @@ ms-vscode-remote.remote-ssh ms-vscode-remote.remote-ssh-edit ms-vscode.makefile-tools ms-vscode.remote-explorer +ms-vsliveshare.vsliveshare +ocamllabs.ocaml-platform +openai.openai-chatgpt-adhoc pgourlain.erlang shopify.ruby-lsp diff --git a/home/dot_config/fish/completions/dotfiles.fish b/home/dot_config/fish/completions/dotfiles.fish index c34d49d..4dfa8b9 100644 --- a/home/dot_config/fish/completions/dotfiles.fish +++ b/home/dot_config/fish/completions/dotfiles.fish @@ -1,5 +1,7 @@ complete -c dotfiles -f -complete -c dotfiles -n "__fish_use_subcommand" -a "edit" -d "Edit managed file" +complete -c dotfiles -n "__fish_use_subcommand" -a "edit" -d "Edit + apply + auto-commit" +complete -c dotfiles -n "__fish_use_subcommand" -a "drift" -d "Detect and re-absorb drifted files" +complete -c dotfiles -n "__fish_use_subcommand" -a "secret" -d "Manage 1Password secrets" complete -c dotfiles -n "__fish_use_subcommand" -a "diff" -d "Show pending changes" complete -c dotfiles -n "__fish_use_subcommand" -a "sync" -d "Apply all changes" complete -c dotfiles -n "__fish_use_subcommand" -a "status" -d "Show status" @@ -11,3 +13,15 @@ complete -c dotfiles -n "__fish_use_subcommand" -a "doctor" -d "Health check" complete -c dotfiles -n "__fish_use_subcommand" -a "bench" -d "Benchmark shell startup" complete -c dotfiles -n "__fish_use_subcommand" -a "backup" -d "Back up config + age key" complete -c dotfiles -n "__fish_use_subcommand" -a "encrypt-setup" -d "Set up age encryption" + +# dotfiles edit accepts file paths +complete -c dotfiles -n "__fish_seen_subcommand_from edit" -a "--no-commit" -d "Skip auto-commit" +complete -c dotfiles -n "__fish_seen_subcommand_from edit" -F + +# dotfiles drift +complete -c dotfiles -n "__fish_seen_subcommand_from drift" -a "--no-commit" -d "Skip auto-commit" + +# dotfiles secret subcommands +complete -c dotfiles -n "__fish_seen_subcommand_from secret; and not __fish_seen_subcommand_from add rm list ls" -a "add" -d "Register a secret" +complete -c dotfiles -n "__fish_seen_subcommand_from secret; and not __fish_seen_subcommand_from add rm list ls" -a "rm" -d "Unregister a secret" +complete -c dotfiles -n "__fish_seen_subcommand_from secret; and not __fish_seen_subcommand_from add rm list ls" -a "list" -d "Show all bindings" diff --git a/home/dot_config/fish/conf.d/secrets.fish.tmpl b/home/dot_config/fish/conf.d/secrets.fish.tmpl index 11f78fc..e5585fb 100644 --- a/home/dot_config/fish/conf.d/secrets.fish.tmpl +++ b/home/dot_config/fish/conf.d/secrets.fish.tmpl @@ -3,26 +3,22 @@ {{- fail (printf "\n\n%s[38;5;204m ✗ Missing config variable: use_1password%s[0m\n Run: %s[38;5;78mchezmoi init%s[0m\n" $e $e $e $e) -}} {{- end -}} # secrets.fish — Secret loading for fish shell. -# Edit: chezmoi edit ~/.config/fish/conf.d/secrets.fish -# Apply: chezmoi apply # -# Load secrets on demand via fish functions: -# op-env GITHUB_TOKEN "op://Vault/GitHub Token/password" -# keychain-env MY_TOKEN [service-name] -# web3-env [vault-name] +# Auto-loaded secrets live in .chezmoidata/secrets.toml. Use the fish helpers +# to manage them — never hand-edit the template: # -# For apply-time injection, uncomment and edit lines below. +# add-secret VAR "op://Vault/Item/field" # append and apply +# rm-secret VAR # remove and apply +# list-secrets # show current bindings +# +# For one-off loads without persistence: op-env / keychain-env. if status is-interactive {{ if .use_1password }} # ── 1Password (resolved at `chezmoi apply` time) ───────────────── - # To inject secrets at apply time, create the item in 1Password first: - # op item create --vault="{{ .op_vault }}" --category=api_credential --title="OpenAI" password=sk-... - # - # Then add lines like this (replace ITEM and FIELD with your 1Password item): - # set -gx OPENAI_API_KEY "{{"{{"}} onepasswordRead (printf "op://%s/OpenAI/credential" .op_vault) {{"}}"}}" - # - # The onepasswordRead call runs at `chezmoi apply` time and injects the real value. +{{- range $var, $ref := .secrets }} + set -gx {{ $var }} "{{ onepasswordRead $ref }}" +{{- end }} {{ else }} # 1Password not configured. Enable: chezmoi init (answer "yes" to 1Password) {{ end }} diff --git a/home/dot_config/fish/config.fish.tmpl b/home/dot_config/fish/config.fish.tmpl index cabdbb3..b912f50 100644 --- a/home/dot_config/fish/config.fish.tmpl +++ b/home/dot_config/fish/config.fish.tmpl @@ -103,6 +103,12 @@ if status is-interactive abbr -a dc "docker compose" abbr -a tf terraform + # ── Dotfiles ───────────────────────────────────────────────────── + abbr -a de "dotfiles edit" + abbr -a dd "dotfiles drift" + abbr -a ds "dotfiles sync" + abbr -a dfu "dotfiles update" + # ── tmux ────────────────────────────────────────────────────────── abbr -a tx tmux abbr -a tml "tmux list-sessions" diff --git a/home/dot_config/fish/functions/dotfiles.fish b/home/dot_config/fish/functions/dotfiles.fish index 84ece19..284d010 100644 --- a/home/dot_config/fish/functions/dotfiles.fish +++ b/home/dot_config/fish/functions/dotfiles.fish @@ -1,7 +1,224 @@ function dotfiles -d "Manage dotfiles via chezmoi" switch $argv[1] case edit e - chezmoi edit $argv[2..] + if test (count $argv) -lt 2 + echo "Usage: dotfiles edit [--no-commit]" + echo " Edits the source, applies on save, auto-commits the diff." + return 1 + end + + set -l no_commit 0 + set -l paths + for a in $argv[2..] + switch $a + case --no-commit + set no_commit 1 + case '*' + set -a paths $a + end + end + + chezmoi edit --apply $paths; or return 1 + + if test $no_commit -eq 1 + return 0 + end + + set -l repo (dirname (chezmoi source-path)) + if not git -C $repo diff --quiet -- home/ + set -l changed (git -C $repo diff --name-only -- home/) + if test (count $changed) -gt 0 + git -C $repo add $changed + set -l summary (string join ", " (for p in $changed; basename $p; end)) + git -C $repo commit -m "chore(config): update $summary via dotfiles edit" >/dev/null + echo "✓ committed: $summary" + echo " push with: git -C $repo push" + end + end + + case drift + set -l no_commit 0 + contains -- --no-commit $argv; and set no_commit 1 + + set -l drifted (chezmoi status 2>/dev/null | string match -r '^ M\s+(.+)$' | string replace -r '^ M\s+' '') + set -l paths + set -l i 2 + while test $i -le (count $drifted) + set -a paths $drifted[$i] + set i (math $i + 2) + end + + if test (count $paths) -eq 0 + echo "✓ no drift — deployed files match source" + return 0 + end + + echo "Drifted files (deployed ≠ source):" + for p in $paths + echo " $p" + end + echo "" + echo "Run 'chezmoi diff $paths[1]' to preview; 'dotfiles drift' will re-absorb all of them." + read -P "Re-absorb into source? [y/N] " ans + if not string match -qri '^y' -- $ans + echo "aborted" + return 1 + end + + chezmoi re-add $paths; or return 1 + echo "✓ source updated" + + if test $no_commit -eq 1 + return 0 + end + + set -l repo (dirname (chezmoi source-path)) + if not git -C $repo diff --quiet -- home/ + set -l changed (git -C $repo diff --name-only -- home/) + git -C $repo add $changed + set -l summary (string join ", " (for p in $changed; basename $p; end)) + git -C $repo commit -m "chore(config): sync drift from machine ($summary)" >/dev/null + echo "✓ committed. Push with: git -C $repo push" + end + + case secret + switch $argv[2] + case add + if test (count $argv) -lt 4 + echo "Usage: dotfiles secret add VAR_NAME \"op://Vault/Item/field\" [--no-commit]" + echo "Example: dotfiles secret add OPENAI_API_KEY \"op://Private/OpenAI/credential\"" + echo "" + echo "If the 1Password item doesn't exist yet you'll be prompted for the" + echo "value and the item will be created automatically." + return 1 + end + + set -l var $argv[3] + set -l ref $argv[4] + set -l do_commit 1 + contains -- --no-commit $argv; and set do_commit 0 + + if not string match -qr '^[A-Z_][A-Z0-9_]*$' -- $var + echo "✗ VAR_NAME must be UPPER_SNAKE_CASE, got: $var" + return 1 + end + + set -l parts (string split / -- (string replace 'op://' '' -- $ref)) + if test (count $parts) -lt 3 + echo "✗ reference must be op://Vault/Item/field , got: $ref" + return 1 + end + set -l op_vault $parts[1] + set -l op_field $parts[-1] + set -l op_item (string join / $parts[2..-2]) + + if not op read "$ref" >/dev/null 2>&1 + echo "No item found at $ref — creating it now." + if not op account list >/dev/null 2>&1 + echo "✗ 1Password CLI is not signed in. Run: eval (op signin)" + return 1 + end + read -s -P "Enter value for $var: " value + echo "" + if test -z "$value" + echo "✗ empty value, aborting" + return 1 + end + if not op item create --vault="$op_vault" --category="API Credential" \ + --title="$op_item" "$op_field=$value" >/dev/null 2>&1 + echo "✗ op item create failed (vault=$op_vault title=$op_item)" + return 1 + end + echo "✓ created 1Password item: $op_item" + if not op read "$ref" >/dev/null 2>&1 + echo "✗ item created but $ref still unreadable; check field name" + return 1 + end + end + + set -l data (chezmoi source-path)/.chezmoidata/secrets.toml + if test ! -f $data + echo "✗ $data missing; dotfiles may need reinstall" + return 1 + end + + if grep -q "^$var = " $data + echo "⚠ $var already registered; use 'dotfiles secret rm' first to replace" + return 1 + end + + printf '%s = "%s"\n' "$var" "$ref" >> $data + echo "✓ added $var → $ref" + + echo "→ chezmoi apply" + chezmoi apply; or begin + echo "✗ chezmoi apply failed; reverting registry" + sed -i '' "/^$var = /d" $data + return 1 + end + + echo "✓ applied. Open a new shell (or `exec fish`) to load \$$var." + + set -l repo (dirname (chezmoi source-path)) + if test $do_commit -eq 1 + git -C $repo add .chezmoidata/secrets.toml + git -C $repo commit -m "feat(secrets): register $var" >/dev/null + echo "✓ committed. Push with: git -C $repo push" + else + echo "Commit: git -C $repo add .chezmoidata/secrets.toml && git commit -m 'feat(secrets): register $var'" + end + + case rm + if test (count $argv) -lt 3 + echo "Usage: dotfiles secret rm VAR_NAME [--no-commit]" + return 1 + end + + set -l var $argv[3] + set -l do_commit 1 + contains -- --no-commit $argv; and set do_commit 0 + + set -l data (chezmoi source-path)/.chezmoidata/secrets.toml + if not grep -q "^$var = " $data + echo "⚠ $var not registered" + return 1 + end + + sed -i '' "/^$var = /d" $data + echo "✓ removed $var" + + echo "→ chezmoi apply" + chezmoi apply; or return 1 + echo "✓ applied. \$$var will be absent from new shells." + + if test $do_commit -eq 1 + set -l repo (dirname (chezmoi source-path)) + git -C $repo add .chezmoidata/secrets.toml + git -C $repo commit -m "chore(secrets): unregister $var" >/dev/null + echo "✓ committed." + end + + case list ls + set -l data (chezmoi source-path)/.chezmoidata/secrets.toml + if test ! -f $data + echo "(no secrets registered)" + return 0 + end + grep -E '^[A-Z_][A-Z0-9_]* = ' $data | sed 's/ = / → /; s/"//g' + + case '' + echo "Usage: dotfiles secret " + echo "" + echo " add VAR \"op://...\" Register a secret" + echo " rm VAR Unregister a secret" + echo " list Show all bindings" + + case '*' + echo "Unknown secret command: $argv[2]" + echo "Usage: dotfiles secret " + return 1 + end + case diff d chezmoi diff --no-pager case sync s @@ -26,7 +243,6 @@ function dotfiles -d "Manage dotfiles via chezmoi" echo "=====================" echo "" - # chezmoi if command -q chezmoi echo "[ok] chezmoi installed" else @@ -34,7 +250,6 @@ function dotfiles -d "Manage dotfiles via chezmoi" set issues (math $issues + 1) end - # Source link if test -L ~/.local/share/chezmoi echo "[ok] chezmoi source linked" else @@ -42,7 +257,6 @@ function dotfiles -d "Manage dotfiles via chezmoi" set issues (math $issues + 1) end - # Fish is default shell if string match -q "*/fish" $SHELL echo "[ok] fish is default shell" else @@ -50,7 +264,6 @@ function dotfiles -d "Manage dotfiles via chezmoi" set issues (math $issues + 1) end - # Homebrew if command -q brew echo "[ok] homebrew installed" else @@ -58,7 +271,6 @@ function dotfiles -d "Manage dotfiles via chezmoi" set issues (math $issues + 1) end - # 1Password CLI if command -q op if op account list &>/dev/null echo "[ok] 1Password CLI: signed in" @@ -69,14 +281,12 @@ function dotfiles -d "Manage dotfiles via chezmoi" echo "[--] 1Password CLI: not installed (optional)" end - # 1Password SSH Agent if test -e "$HOME/Library/Group Containers/2BUA8C4S2C.com.1password/t/agent.sock" echo "[ok] 1Password SSH agent: socket exists" else echo "[--] 1Password SSH agent: not found (optional)" end - # Key config files for f in ~/.gitconfig ~/.config/fish/config.fish ~/.ssh/config if test -f $f echo "[ok] $f exists" @@ -86,7 +296,6 @@ function dotfiles -d "Manage dotfiles via chezmoi" end end - # Git identity set -l git_name (git config --global user.name 2>/dev/null) if test -n "$git_name" echo "[ok] git identity: $git_name <"(git config --global user.email)">" @@ -95,7 +304,6 @@ function dotfiles -d "Manage dotfiles via chezmoi" set issues (math $issues + 1) end - # Key CLI tools for tool in fzf bat eza zoxide delta mise starship if command -q $tool echo "[ok] $tool" @@ -105,14 +313,12 @@ function dotfiles -d "Manage dotfiles via chezmoi" end end - # Age key if test -f ~/.config/chezmoi/key.txt echo "[ok] age encryption key exists" else echo "[--] age encryption key: not set up (optional)" end - # Drift set -l drift_count (chezmoi diff --no-pager 2>/dev/null | grep '^diff' | wc -l | string trim) if test "$drift_count" -gt 0 echo "[!!] $drift_count file(s) have drifted from source" @@ -165,14 +371,14 @@ function dotfiles -d "Manage dotfiles via chezmoi" echo " identity = \"$key_path\"" echo " recipient = \"$pubkey\"" echo "" + set -l vault (chezmoi data 2>/dev/null | grep op_vault | awk '{print $NF}' | tr -d '"'); or set vault Private echo " 3. Back up the key to 1Password:" - echo " op document create $key_path --title 'chezmoi age key' --vault=Developer" + echo " op document create $key_path --title 'chezmoi age key' --vault=$vault" echo "" echo " 4. Add encrypted files:" echo " chezmoi add --encrypt ~/.kube/config" case update u - # Source path is home/ inside the git repo; find the repo root set -l src (chezmoi source-path) if not test -d $src echo "chezmoi source not found" @@ -225,6 +431,7 @@ function dotfiles -d "Manage dotfiles via chezmoi" set -l config_path (chezmoi config-path 2>/dev/null) set -l key_path "$HOME/.config/chezmoi/key.txt" + set -l vault (chezmoi data 2>/dev/null | grep op_vault | awk '{print $NF}' | tr -d '"'); or set vault Private if test -z "$config_path"; or not test -f "$config_path" echo "[!!] chezmoi config not found" @@ -234,7 +441,7 @@ function dotfiles -d "Manage dotfiles via chezmoi" echo "[1/3] chezmoi config: $config_path" if command -q op echo " Backing up to 1Password..." - op document create "$config_path" --title "chezmoi config (dotfiles backup)" --vault=Developer 2>/dev/null + op document create "$config_path" --title "chezmoi config (dotfiles backup)" --vault="$vault" 2>/dev/null and echo " [ok] Uploaded to 1Password" or echo " [!!] Upload failed. Are you signed in? (op signin)" else @@ -246,7 +453,7 @@ function dotfiles -d "Manage dotfiles via chezmoi" if test -f "$key_path" if command -q op echo " Backing up to 1Password..." - op document create "$key_path" --title "chezmoi age key (dotfiles backup)" --vault=Developer 2>/dev/null + op document create "$key_path" --title "chezmoi age key (dotfiles backup)" --vault="$vault" 2>/dev/null and echo " [ok] Uploaded to 1Password" or echo " [!!] Upload failed" else @@ -276,18 +483,20 @@ function dotfiles -d "Manage dotfiles via chezmoi" echo "Usage: dotfiles " echo "" echo "Commands:" - echo " edit Edit a managed file" - echo " diff Show pending changes" - echo " sync Apply all changes" - echo " update Pull latest + apply" - echo " status Show managed file count + pending diffs" - echo " cd cd to chezmoi source directory" - echo " refresh Re-download external files (fish plugins)" - echo " add Add a new file to chezmoi" - echo " doctor Run health check on dotfiles setup" - echo " bench Benchmark shell startup time" - echo " backup Back up chezmoi config + age key" - echo " encrypt-setup Set up age encryption" + echo " edit Edit + apply + auto-commit" + echo " drift Detect and re-absorb drifted files" + echo " secret Manage 1Password secrets (add/rm/list)" + echo " diff Show pending changes" + echo " sync Apply all changes" + echo " update Pull latest + apply" + echo " status Show managed file count + pending diffs" + echo " cd cd to chezmoi source directory" + echo " refresh Re-download external files (fish plugins)" + echo " add Add a new file to chezmoi" + echo " doctor Run health check on dotfiles setup" + echo " bench Benchmark shell startup time" + echo " backup Back up chezmoi config + age key" + echo " encrypt-setup Set up age encryption" case '*' chezmoi $argv end