diff --git a/.chezmoi.toml.tmpl b/.chezmoi.toml.tmpl new file mode 100644 index 00000000000..6dcdae01134 --- /dev/null +++ b/.chezmoi.toml.tmpl @@ -0,0 +1,12 @@ +{{- /* Valid classes: mac, linux, wsl, ephemeral — see CLAUDE.md. + The prompt text stays exactly "machineClass" so scripts can answer it + with --promptString machineClass=. */ -}} +{{- $machineClass := promptStringOnce . "machineClass" "machineClass" -}} + +{{ if eq $machineClass "mac" -}} +# The development clone doubles as the chezmoi source directory on macs. +sourceDir = "~/code/dotfiles" + +{{ end -}} +[data] + machineClass = {{ $machineClass | quote }} diff --git a/.chezmoiignore b/.chezmoiignore new file mode 100644 index 00000000000..adc167bcdb4 --- /dev/null +++ b/.chezmoiignore @@ -0,0 +1,27 @@ +# Entries here are TARGET paths (relative to $HOME) that chezmoi must not +# manage. This file is parsed as a template, so ignores can vary by machine. + +# Repo-level files — never deployed. (Dot-prefixed source files like .macos, +# .github, and .gitignore are invisible to chezmoi already and need no entry.) +README.md +LICENSE-MIT.txt +Brewfile +bootstrap.sh +tests +init +theme + +{{ if ne .chezmoi.os "darwin" -}} +# macOS GUI configuration has no business on Linux/WSL +com.googlecode.iterm2.plist +.mackup.cfg +{{ end -}} + +{{ if eq .machineClass "ephemeral" -}} +# Disposable boxes get shell config only: no identity, no secrets, and no +# tooling that assumes a long-lived machine. +.extra +.gitconfig.local +.claude +CLAUDE.md +{{ end -}} diff --git a/.extra.tmpl b/.extra.tmpl deleted file mode 100644 index ffa481e9bf2..00000000000 --- a/.extra.tmpl +++ /dev/null @@ -1,11 +0,0 @@ -# shellcheck shell=sh -# -# ~/.extra — machine-local secrets / overrides, sourced last by .zshrc. -# -# Rendered to ~/.extra by 1Password's `op inject` (bootstrap.sh runs it when -# ~/.extra is missing). See CLAUDE.md "Machine-local secrets" for the exact -# reference format and workflow. -# -# WARNING: op inject parses this ENTIRE file, comments included. Never write -# curly-brace pairs or an "op" URI scheme anywhere in it except as a real, -# brace-wrapped secret reference — anything else is a render error. diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1b4cc67ccfb..95188cf8bf9 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -21,6 +21,10 @@ jobs: - name: Install Bats on macOS if: matrix.os == 'macos-latest' run: brew install bats-core + - name: Install chezmoi + run: | + sh -c "$(curl -fsLS get.chezmoi.io)" -- -b "$HOME/.local/bin" + echo "$HOME/.local/bin" >> "$GITHUB_PATH" - name: Run tests run: bats tests - name: Apply .macos on the throwaway runner and verify @@ -33,12 +37,15 @@ jobs: container: rockylinux:9 steps: - name: Install dependencies + # No explicit curl: the image ships curl-minimal, which conflicts + # with the full package but already provides the curl binary. run: dnf install -y git zsh rsync - uses: actions/checkout@v3 - - name: Install Bats + - name: Install Bats and chezmoi run: | git clone --depth 1 https://github.com/bats-core/bats-core.git /tmp/bats-core /tmp/bats-core/install.sh /usr/local + sh -c "$(curl -fsLS get.chezmoi.io)" -- -b /usr/local/bin - name: Run tests run: bats tests @@ -58,6 +65,8 @@ jobs: - uses: Vampire/setup-wsl@v5 with: distribution: Ubuntu-24.04 - additional-packages: bats zsh rsync + additional-packages: bats zsh rsync curl + - name: Install chezmoi + run: sh -c "$(curl -fsLS get.chezmoi.io)" -- -b /usr/local/bin - name: Run tests run: bats tests diff --git a/Brewfile b/Brewfile index 28ce3aacc18..4746068a705 100644 --- a/Brewfile +++ b/Brewfile @@ -12,6 +12,7 @@ brew 'bat' # Better cat with syntax highlighting and git integration brew 'bats-core' # Bash Automated Testing System (used by dotfiles tests) brew 'bfg' # Removes large or troublesome blobs like git-filter-branch does, but faster. And written in Scala brew 'binutils' # The GNU Binutils are a collection of binary tools +brew 'chezmoi' # manage your dotfiles across multiple diverse machines, securely brew 'cifer' # Work on automating classical cipher cracking in C. brew 'coreutils' # The GNU Core Utilities are the basic file, shell and text manipulation utilities of the GNU operating system. brew 'curl' diff --git a/CLAUDE.md b/CLAUDE.md index 82a2753bd1e..5e8d31cc3b6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,22 +4,49 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## What this repo is -Personal dotfiles for macOS/zsh, including shell configuration, macOS system defaults, and a Homebrew bundle. The primary shell is **zsh** (`.bash_profile` auto-upgrades any bash session to zsh). +Personal dotfiles managed by **chezmoi**, targeting macOS, Linux, WSL, and +disposable remote boxes. Shell configuration (zsh-first; `.bash_profile` +auto-upgrades bash sessions to zsh), macOS system defaults, and a Homebrew +bundle. A **machine class** chosen once at `chezmoi init` (`mac` / `linux` / +`wsl` / `ephemeral`, answerable non-interactively with +`--promptString machineClass=`) drives all per-machine templating: +`ephemeral` deploys shell config only (no identity, no secrets), `wsl` adds +1Password's ssh.exe forwarding pattern, `mac` pins chezmoi's `sourceDir` to +`~/code/dotfiles` so the clone is the source of truth. + +Source naming follows chezmoi conventions: `dot_zshrc` deploys to +`~/.zshrc`, `private_dot_extra.tmpl` renders to `~/.extra` (mode 0600), +`bin/executable_has-glyphs` to `~/bin/has-glyphs`, `create_` files are +written only when missing. Repo-level files (`.gitignore`, `.macos`, +`.github`, dot-prefixed in general) are invisible to chezmoi; non-dot +repo files are excluded via `.chezmoiignore`. ## Installation ```bash -# Clone and apply dotfiles to $HOME -source bootstrap.sh +# Fresh machine (no root needed) +sh -c "$(curl -fsLS get.chezmoi.io)" -- init --apply hadees -# Apply macOS system preferences (requires sudo) -# Optionally set a machine name first: +# Disposable box — applies, then removes chezmoi and all traces of itself +sh -c "$(curl -fsLS get.chezmoi.io)" -- init --one-shot hadees + +# Development clone (this Mac) +chezmoi init --source ~/code/dotfiles --apply + +# Day-to-day: edit in ~/code/dotfiles, then +chezmoi diff && chezmoi apply # `dotfiles` is aliased to `chezmoi apply` + +# Apply macOS system preferences (requires sudo; run manually, chezmoi +# never touches it). Optionally set a machine name first: COMPUTER_NAME="My-Mac" ./.macos -# Install Homebrew packages +# Install Homebrew packages (macOS) brew bundle ``` +`bootstrap.sh` is a deprecated wrapper that just calls +`chezmoi init --source . --apply`; it will be removed. + ## Running tests Tests use [bats-core](https://github.com/bats-core/bats-core). Install it first (`brew install bats-core`), then: @@ -54,7 +81,9 @@ VMs; the test skips everywhere else unless `MACOS_APPLY_OK=1` is set. ### Key files -- **`bootstrap.sh`** — uses `rsync` to copy repo files to `$HOME`; excludes `bootstrap.sh`, `README.md`, `Brewfile`, `LICENSE-MIT.txt` +- **`.chezmoi.toml.tmpl`** — config template; prompts once for the machine class and pins `sourceDir` to the clone on macs +- **`.chezmoiignore`** — target paths chezmoi must not manage (repo-level files everywhere; macOS GUI config off-mac; identity/secrets on `ephemeral`) +- **`bootstrap.sh`** — deprecated wrapper around `chezmoi init --source . --apply` - **`.macos`** — macOS `defaults write` settings; reads `$COMPUTER_NAME` env var for machine-specific naming - **`Brewfile`** — Homebrew formulae, casks, and Mac App Store apps - **`bin/`** — personal scripts added to `$PATH` @@ -89,17 +118,18 @@ Add `~/.extra` (not committed) for per-machine overrides. Add `~/.path` for per- ### Machine-local secrets (~/.extra) -`~/.extra` is rendered from the tracked `.extra.tmpl` by 1Password's -`op inject`. bootstrap.sh runs it automatically when `~/.extra` is missing; -re-render manually with `op inject -i .extra.tmpl -o ~/.extra -f`. The -template stores only secret *references*, never values: +`~/.extra` is rendered by chezmoi from `private_dot_extra.tmpl` (mode 0600) +on every `chezmoi apply`, using chezmoi's built-in 1Password template +functions — secret *values* come from the 1Password CLI at apply time, only +references live in the repo: - export GITHUB_TOKEN="{{ op://Private/GitHub PAT/token }}" + export GITHUB_TOKEN={{ onepasswordRead "op://Private/GitHub PAT/token" | quote }} Get a reference path from the 1Password app: right-click a field → -"Copy Secret Reference". +"Copy Secret Reference". The `ephemeral` machine class never deploys this +file (see `.chezmoiignore`). -CAUTION: `op inject` parses the whole template, comments included, and -errors on any curly-brace pair or bare `op://` text that isn't a real -brace-wrapped reference. That's why these instructions live here and not in -the template itself. `tests/extra-tmpl.bats` enforces the invariant. +CAUTION: chezmoi parses the template as a Go template, comments included — +a stray `{{` anywhere breaks rendering. That's why these instructions live +here and not in the template itself. `tests/extra-tmpl.bats` enforces the +invariant. diff --git a/README.md b/README.md index 3a312070252..a2366a23a99 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,45 @@ -# Hadees’ dotfilesßß +# Hadees’ dotfiles + +Managed with [chezmoi](https://www.chezmoi.io/). One repo targets macOS, +Linux, WSL, and disposable remote boxes; a **machine class** chosen at init +time (`mac` / `linux` / `wsl` / `ephemeral`) controls what gets deployed. ## Installation **Warning:** If you want to give these dotfiles a try, you should first fork this repository, review the code, and remove things you don’t want or need. Don’t blindly use my settings unless you know what that entails. Use at your own risk! -### Using Git and the bootstrap script - -You can clone the repository wherever you want. (I like to keep it in `~/code/dotfiles`, with `~/.dotfiles` as a symlink.) The bootstrapper script will pull in the latest version and copy the files to your home folder. +### Fresh machine (one-liner, no root needed) ```bash -git clone https://github.com/hadees/dotfiles.git && cd dotfiles && source bootstrap.sh +sh -c "$(curl -fsLS get.chezmoi.io)" -- init --apply hadees ``` -To update, `cd` into your local `dotfiles` repository and then: - -```bash -source bootstrap.sh -``` +Installs chezmoi to `~/.local/bin`, clones this repo, prompts once for the +machine class, and applies. -Alternatively, to update while avoiding the confirmation prompt: +### Disposable box (leave no trace) ```bash -set -- -f; source bootstrap.sh +sh -c "$(curl -fsLS get.chezmoi.io)" -- init --one-shot hadees ``` -### Git-free install +Applies the dotfiles, then removes chezmoi, its source clone, and its config +from the machine. Answer `ephemeral` to the machine-class prompt: that class +deploys shell config only — no git identity and no secrets ever touch disk. +For commit signing on a box you keep around, forward your local 1Password +SSH agent (per-host `ForwardAgent yes`) instead of putting keys there. -To install these dotfiles without Git: +### Development clone (macOS) ```bash -cd; curl -#L https://github.com/hadees/dotfiles/tarball/master | tar -xzv --strip-components 1 --exclude={README.md,bootstrap.sh,.osx,LICENSE-MIT.txt} +brew install chezmoi +git clone git@github.com:hadees/dotfiles.git ~/code/dotfiles +chezmoi init --source ~/code/dotfiles --apply ``` -To update later on, just run that command again. +The `mac` machine class pins chezmoi’s `sourceDir` to the clone, so the +day-to-day loop is: edit files in `~/code/dotfiles`, review with +`chezmoi diff`, deploy with `chezmoi apply` (aliased to `dotfiles`). ### Specify the `$PATH` @@ -46,20 +53,11 @@ export PATH="/usr/local/bin:$PATH" ### Add custom commands without creating a new fork -If `~/.extra` exists, it will be sourced along with the other files. You can use this to add a few custom commands without the need to fork this entire repository, or to add commands you don’t want to commit to a public repository. - -My `~/.extra` looks something like this: - -```bash -# Git credentials -# Not in the repository, to prevent people from accidentally committing under my name -GIT_AUTHOR_NAME="John Doe" -GIT_COMMITTER_NAME="$GIT_AUTHOR_NAME" -git config --global user.name "$GIT_AUTHOR_NAME" -GIT_AUTHOR_EMAIL="mathias@mailinator.com" -GIT_COMMITTER_EMAIL="$GIT_AUTHOR_EMAIL" -git config --global user.email "$GIT_AUTHOR_EMAIL" -``` +If `~/.extra` exists, it will be sourced along with the other files. On +managed machines it is rendered by chezmoi from `private_dot_extra.tmpl`, +which pulls secret values out of 1Password at apply time — see CLAUDE.md +“Machine-local secrets” for the syntax. On the `ephemeral` machine class it +is never deployed at all. You could also use `~/.extra` to override settings, functions and aliases from my dotfiles repository. It’s probably better to [fork this repository](https://github.com/hadees/dotfiles/fork) instead, though. diff --git a/bin/has-glyphs b/bin/executable_has-glyphs similarity index 100% rename from bin/has-glyphs rename to bin/executable_has-glyphs diff --git a/bin/lndir.sh b/bin/executable_lndir.sh similarity index 100% rename from bin/lndir.sh rename to bin/executable_lndir.sh diff --git a/bin/subl b/bin/subl deleted file mode 120000 index 0170a200254..00000000000 --- a/bin/subl +++ /dev/null @@ -1 +0,0 @@ -/Applications/Sublime Text.app/Contents/SharedSupport/bin/subl \ No newline at end of file diff --git a/bin/symlink_subl b/bin/symlink_subl new file mode 100644 index 00000000000..6a4e7cf72ac --- /dev/null +++ b/bin/symlink_subl @@ -0,0 +1 @@ +/Applications/Sublime Text.app/Contents/SharedSupport/bin/subl diff --git a/bootstrap.sh b/bootstrap.sh index 473cc0e6b43..b5221be0646 100755 --- a/bootstrap.sh +++ b/bootstrap.sh @@ -1,59 +1,19 @@ #!/usr/bin/env bash -cd "$(dirname "${BASH_SOURCE}")"; +# DEPRECATED: these dotfiles are now managed by chezmoi. This wrapper +# survives one release for muscle memory, then goes away. +# +# Fresh machine: sh -c "$(curl -fsLS get.chezmoi.io)" -- init --apply hadees +# This clone: chezmoi init --source . && chezmoi apply -git pull --ff-only origin master || echo "Could not pull latest changes; continuing with local copy."; +cd "$(dirname "${BASH_SOURCE}")" || { return 1 2>/dev/null || exit 1; }; -function doIt() { - rsync --exclude ".git/" \ - --exclude ".claude/settings.local.json" \ - --exclude ".DS_Store" \ - --exclude ".macos" \ - --exclude "tests/" \ - --exclude "bootstrap.sh" \ - --exclude "README.md" \ - --exclude "LICENSE-MIT.txt" \ - --exclude "Brewfile" \ - -avh --no-perms . ~; - if [ ! -f ~/.claude/CLAUDE.local.md ]; then - mkdir -p ~/.claude; - printf '# Machine-local memory (generated by bootstrap.sh, edit freely)\n\nTerminal: %s (TERM=%s), OS: %s%s%s\n' \ - "${TERM_PROGRAM:-unknown}" "${TERM:-unknown}" "$(uname -s)" \ - "${WSL_DISTRO_NAME:+, WSL: $WSL_DISTRO_NAME}" \ - "${TMUX:+, tmux}" > ~/.claude/CLAUDE.local.md; - fi; - # Render machine-local secrets from the tracked template (op:// references - # only — values come from 1Password). Never overwrites an existing ~/.extra. - if command -v op > /dev/null 2>&1 && [ ! -f ~/.extra ]; then - op inject -i .extra.tmpl -o ~/.extra \ - || echo "op inject failed (not signed in?) — render ~/.extra manually later."; - fi; - # ~/.zshrc is zsh-only syntax; only source it when actually running in zsh - if [ -n "$ZSH_VERSION" ]; then - source ~/.zshrc; - else - echo "Restart your shell (or run: exec zsh) to pick up the new config."; - fi; - echo ""; - echo "Next steps on a new machine:"; - echo " brew bundle — install Brewfile packages"; - echo " 1Password → set up SSH agent + op-ssh-sign, then add signing config"; - echo " to ~/.gitconfig.local (commit.gpgsign=true,"; - echo " gpg.format=ssh, signingkey=*.pub)"; - echo " bash init/mackup.sh — restore app settings from ~/.config/Mackup/"; - echo " op inject -i .extra.tmpl -o ~/.extra"; - echo " — render secrets (auto-runs above when op is"; - echo " installed and ~/.extra doesn't exist yet)"; -} - -if [ "$1" = "--force" -o "$1" = "-f" ]; then - doIt; -else - printf "This may overwrite existing files in your home directory. Are you sure? (y/n) "; - read REPLY; - echo ""; - if [[ $REPLY =~ ^[Yy]$ ]]; then - doIt; - fi; +if ! command -v chezmoi > /dev/null 2>&1; then + echo "chezmoi is not installed. Install it first:"; + echo " brew install chezmoi # macOS"; + echo " sh -c \"\$(curl -fsLS get.chezmoi.io)\" -- -b ~/.local/bin # anywhere"; + return 1 2>/dev/null || exit 1; fi; -unset doIt; + +echo "bootstrap.sh is deprecated — applying via chezmoi instead."; +chezmoi init --source "$PWD" --apply; diff --git a/.aliases b/dot_aliases similarity index 98% rename from .aliases rename to dot_aliases index ac6863b1b14..a0f0d01e744 100644 --- a/.aliases +++ b/dot_aliases @@ -166,8 +166,8 @@ alias afk="/System/Library/CoreServices/Menu\ Extras/User.menu/Contents/Resource # Reload the shell (i.e. invoke as a login shell) alias reload="exec ${SHELL} -l" -# Sync dotfiles repo to $HOME -alias dotfiles='source ~/code/dotfiles/bootstrap.sh -f' +# Sync dotfiles repo to $HOME (chezmoi's sourceDir points at the clone) +alias dotfiles='chezmoi apply' # Print each PATH entry on a separate line alias path='echo -e ${PATH//:/\\n}' diff --git a/.bash_profile b/dot_bash_profile similarity index 100% rename from .bash_profile rename to dot_bash_profile diff --git a/.bashrc b/dot_bashrc similarity index 100% rename from .bashrc rename to dot_bashrc diff --git a/.claude/CLAUDE.md b/dot_claude/CLAUDE.md similarity index 100% rename from .claude/CLAUDE.md rename to dot_claude/CLAUDE.md diff --git a/dot_claude/create_CLAUDE.local.md.tmpl b/dot_claude/create_CLAUDE.local.md.tmpl new file mode 100644 index 00000000000..aa8d7a214c7 --- /dev/null +++ b/dot_claude/create_CLAUDE.local.md.tmpl @@ -0,0 +1,3 @@ +# Machine-local memory (generated by chezmoi, edit freely) + +Terminal: {{ env "TERM_PROGRAM" | default "unknown" }} (TERM={{ env "TERM" | default "unknown" }}), OS: {{ if eq .chezmoi.os "darwin" }}Darwin (macOS){{ else }}{{ .chezmoi.os }}{{ end }}{{ if env "WSL_DISTRO_NAME" }}, WSL: {{ env "WSL_DISTRO_NAME" }}{{ else }}, no WSL{{ end }}{{ if env "TMUX" }}, tmux{{ else }}, no tmux{{ end }} diff --git a/.claude/settings.json b/dot_claude/settings.json similarity index 100% rename from .claude/settings.json rename to dot_claude/settings.json diff --git a/.config/ghostty/config b/dot_config/ghostty/config similarity index 100% rename from .config/ghostty/config rename to dot_config/ghostty/config diff --git a/.config/k9s/config.yaml b/dot_config/k9s/config.yaml similarity index 100% rename from .config/k9s/config.yaml rename to dot_config/k9s/config.yaml diff --git a/.config/k9s/skins/base16-eighties.yaml b/dot_config/k9s/skins/base16-eighties.yaml similarity index 100% rename from .config/k9s/skins/base16-eighties.yaml rename to dot_config/k9s/skins/base16-eighties.yaml diff --git a/.config/lazygit/config.yml b/dot_config/lazygit/config.yml similarity index 100% rename from .config/lazygit/config.yml rename to dot_config/lazygit/config.yml diff --git a/.curlrc b/dot_curlrc similarity index 100% rename from .curlrc rename to dot_curlrc diff --git a/.editorconfig b/dot_editorconfig similarity index 100% rename from .editorconfig rename to dot_editorconfig diff --git a/.exports b/dot_exports similarity index 100% rename from .exports rename to dot_exports diff --git a/.functions b/dot_functions similarity index 100% rename from .functions rename to dot_functions diff --git a/.gdbinit b/dot_gdbinit similarity index 100% rename from .gdbinit rename to dot_gdbinit diff --git a/dot_gitattributes b/dot_gitattributes new file mode 100644 index 00000000000..6bdc702247e --- /dev/null +++ b/dot_gitattributes @@ -0,0 +1,3 @@ +# Automatically normalize line endings for all text-based files +#* text=auto +# Disabled because of https://github.com/mathiasbynens/dotfiles/issues/149 :( diff --git a/.gitconfig b/dot_gitconfig similarity index 100% rename from .gitconfig rename to dot_gitconfig diff --git a/dot_gitconfig.local.tmpl b/dot_gitconfig.local.tmpl new file mode 100644 index 00000000000..7230e83f6d7 --- /dev/null +++ b/dot_gitconfig.local.tmpl @@ -0,0 +1,47 @@ +{{- /* Machine-local git identity + signing, generated per machine class. + Work identity (~/.gitconfig-ica) stays untracked and machine-local — + the includeIf hooks below reference it only where it exists. */ -}} +[user] + name = Evan Alter + email = evan.alter@gmail.com +{{- if eq .machineClass "mac" }} + # SSH commit signing via 1Password — personal (hadees) key + signingkey = ~/.ssh/id_github_hadees.pub +{{- end }} +[github] + user = hadees +{{- if eq .machineClass "mac" }} +[gpg] + format = ssh +[gpg "ssh"] + program = "/Applications/1Password.app/Contents/MacOS/op-ssh-sign" + allowedSignersFile = ~/.config/git/allowed_signers + +# Work identity follows the remote: any repo whose remote uses the +# github-icaevan SSH alias gets the icanalytica identity (requires git 2.36+). +[includeIf "hasconfig:remote.*.url:github-icaevan:*/**"] + path = ~/.gitconfig-ica +[includeIf "hasconfig:remote.*.url:git@github-icaevan:*/**"] + path = ~/.gitconfig-ica +[commit] + gpgsign = true +{{- end }} +{{- if eq .machineClass "wsl" }} +[core] + # 1Password's official WSL pattern: SSH requests are forwarded to the + # Windows ssh.exe, which talks to the 1Password agent on the host. + # SSH config lives Windows-side in %USERPROFILE%\.ssh\config. + sshCommand = ssh.exe + +# To sign commits from WSL, use 1Password's "Configure Commit Signing" +# flow with the WSL checkbox (needs Git 2.34+); it emits a snippet using +# op-ssh-sign-wsl.exe. Paste it below. +{{- end }} +{{- if eq .machineClass "linux" }} + +# Signing is off by default here. To sign on a box you keep around: +# forward the local 1Password SSH agent (per-host ForwardAgent yes), then +# set gpg.format=ssh, user.signingkey=, and +# commit.gpgsign=true. Do NOT reference op-ssh-sign — it only exists on +# the machine that runs 1Password. +{{- end }} diff --git a/dot_gitignore b/dot_gitignore new file mode 100644 index 00000000000..d754e61b8ea --- /dev/null +++ b/dot_gitignore @@ -0,0 +1,23 @@ +# Global git excludes — referenced by .gitconfig's core.excludesfile. +# Repo-specific ignores belong in each repo's own .gitignore, not here. + +# Compiled Python files +*.pyc + +# Folder view configuration files +.DS_Store +Desktop.ini + +# Thumbnail cache files +._* +Thumbs.db + +# Files that might appear on external disks +.Spotlight-V100 +.Trashes + +# Scratch pads +scratch.* + +# Generated zsh completion cache +.zcompdump diff --git a/.gvimrc b/dot_gvimrc similarity index 100% rename from .gvimrc rename to dot_gvimrc diff --git a/.hgignore b/dot_hgignore similarity index 100% rename from .hgignore rename to dot_hgignore diff --git a/.hushlogin b/dot_hushlogin similarity index 100% rename from .hushlogin rename to dot_hushlogin diff --git a/.inputrc b/dot_inputrc similarity index 100% rename from .inputrc rename to dot_inputrc diff --git a/.mackup.cfg b/dot_mackup.cfg similarity index 100% rename from .mackup.cfg rename to dot_mackup.cfg diff --git a/.railsrc b/dot_railsrc similarity index 100% rename from .railsrc rename to dot_railsrc diff --git a/.screenrc b/dot_screenrc similarity index 100% rename from .screenrc rename to dot_screenrc diff --git a/.tmux.conf b/dot_tmux.conf similarity index 100% rename from .tmux.conf rename to dot_tmux.conf diff --git a/.vim/backups/.gitkeep b/dot_vim/backups/.gitkeep similarity index 100% rename from .vim/backups/.gitkeep rename to dot_vim/backups/.gitkeep diff --git a/.vim/colors/base16-eighties.vim b/dot_vim/colors/base16-eighties.vim similarity index 100% rename from .vim/colors/base16-eighties.vim rename to dot_vim/colors/base16-eighties.vim diff --git a/.vim/swaps/.gitkeep b/dot_vim/swaps/.gitkeep similarity index 100% rename from .vim/swaps/.gitkeep rename to dot_vim/swaps/.gitkeep diff --git a/.vim/syntax/json.vim b/dot_vim/syntax/json.vim similarity index 100% rename from .vim/syntax/json.vim rename to dot_vim/syntax/json.vim diff --git a/.vim/undo/.gitkeep b/dot_vim/undo/.gitkeep similarity index 100% rename from .vim/undo/.gitkeep rename to dot_vim/undo/.gitkeep diff --git a/.vimrc b/dot_vimrc similarity index 100% rename from .vimrc rename to dot_vimrc diff --git a/.wgetrc b/dot_wgetrc similarity index 100% rename from .wgetrc rename to dot_wgetrc diff --git a/.zsh_prompt b/dot_zsh_prompt similarity index 100% rename from .zsh_prompt rename to dot_zsh_prompt diff --git a/.zshrc b/dot_zshrc similarity index 100% rename from .zshrc rename to dot_zshrc diff --git a/private_dot_extra.tmpl b/private_dot_extra.tmpl new file mode 100644 index 00000000000..ecece3f6cb6 --- /dev/null +++ b/private_dot_extra.tmpl @@ -0,0 +1,11 @@ +# shellcheck shell=sh +# +# ~/.extra — machine-local secrets / overrides, sourced last by .zshrc. +# +# Rendered by chezmoi (private_dot_extra.tmpl, mode 0600). Secret VALUES +# come from 1Password at apply time; only references live in the repo. +# See CLAUDE.md "Machine-local secrets" for the workflow and syntax. +# +# WARNING: chezmoi parses this whole file as a Go template — comments +# included. Never write a curly-brace pair here except as a real template +# action (a onepasswordRead call producing an export line). diff --git a/tests/bootstrap.bats b/tests/bootstrap.bats deleted file mode 100755 index 2423e268ca8..00000000000 --- a/tests/bootstrap.bats +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env bats - -setup() { - TMPHOME="$(mktemp -d)" -} - -teardown() { - rm -rf "$TMPHOME" -} - -@test "bootstrap script syncs dotfiles" { - git() { :; } - op() { :; } - export -f git op - cd "$BATS_TEST_DIRNAME/.." - HOME="$TMPHOME" run bash bootstrap.sh --force - [ "$status" -eq 0 ] - [ -f "$TMPHOME/.bashrc" ] - [ -f "$TMPHOME/.bash_profile" ] - [ -f "$TMPHOME/.claude/CLAUDE.md" ] - [ ! -e "$TMPHOME/.claude/settings.local.json" ] - [ -f "$TMPHOME/.claude/CLAUDE.local.md" ] - [ ! -e "$TMPHOME/.macos" ] -} - -@test "bootstrap does not overwrite existing CLAUDE.local.md" { - git() { :; } - op() { :; } - export -f git op - cd "$BATS_TEST_DIRNAME/.." - mkdir -p "$TMPHOME/.claude" - echo "my custom notes" > "$TMPHOME/.claude/CLAUDE.local.md" - HOME="$TMPHOME" run bash bootstrap.sh --force - [ "$status" -eq 0 ] - grep -q "my custom notes" "$TMPHOME/.claude/CLAUDE.local.md" -} diff --git a/tests/chezmoi.bats b/tests/chezmoi.bats new file mode 100644 index 00000000000..ef959547242 --- /dev/null +++ b/tests/chezmoi.bats @@ -0,0 +1,80 @@ +#!/usr/bin/env bats + +# Full init + apply into an isolated $HOME per machine class, so the real +# machine is never touched and no real config is read or written. XDG dirs +# must be pinned too, not just HOME: GitHub's ubuntu runners export +# XDG_CONFIG_HOME, which chezmoi prefers over $HOME/.config — without the +# override, the first test's machineClass leaks into every later test via +# promptStringOnce. + +chez() { + HOME="$TMPHOME" \ + XDG_CONFIG_HOME="$TMPHOME/.config" \ + XDG_DATA_HOME="$TMPHOME/.local/share" \ + XDG_STATE_HOME="$TMPHOME/.local/state" \ + XDG_CACHE_HOME="$TMPHOME/.cache" \ + chezmoi "$@" +} + +setup() { + command -v chezmoi > /dev/null 2>&1 || skip "chezmoi not installed" + TMPHOME="$(mktemp -d)" +} + +teardown() { + rm -rf "$TMPHOME" +} + +@test "ephemeral class deploys shell config and nothing sensitive" { + cd "$BATS_TEST_DIRNAME/.." + chez init --source "$PWD" --promptString machineClass=ephemeral --apply + # Shell layer lands + [ -f "$TMPHOME/.zshrc" ] + [ -f "$TMPHOME/.aliases" ] + [ -f "$TMPHOME/.functions" ] + [ -x "$TMPHOME/bin/has-glyphs" ] + # Rendered shell files parse + zsh -n "$TMPHOME/.zshrc" + zsh -n "$TMPHOME/.aliases" + # No identity, secrets, or long-lived-machine tooling + [ ! -e "$TMPHOME/.extra" ] + [ ! -e "$TMPHOME/.gitconfig.local" ] + [ ! -e "$TMPHOME/.claude" ] + [ ! -e "$TMPHOME/CLAUDE.md" ] + # Repo-level files never deploy + [ ! -e "$TMPHOME/README.md" ] + [ ! -e "$TMPHOME/Brewfile" ] + [ ! -e "$TMPHOME/tests" ] + [ ! -e "$TMPHOME/.macos" ] +} + +@test "linux class deploys identity without signing" { + cd "$BATS_TEST_DIRNAME/.." + chez init --source "$PWD" --promptString machineClass=linux --apply + [ -f "$TMPHOME/.gitconfig.local" ] + grep -q "email = evan.alter@gmail.com" "$TMPHOME/.gitconfig.local" + grep -q "user = hadees" "$TMPHOME/.gitconfig.local" + ! grep -q "gpgsign = true" "$TMPHOME/.gitconfig.local" + ! grep -q "sshCommand" "$TMPHOME/.gitconfig.local" + # Secrets template renders (currently reference-free) with private mode + [ -f "$TMPHOME/.extra" ] + run stat -f "%Lp" "$TMPHOME/.extra" + [ "$output" = "600" ] || { run stat -c "%a" "$TMPHOME/.extra"; [ "$output" = "600" ]; } + # Machine-local memory is created when missing + [ -f "$TMPHOME/.claude/CLAUDE.local.md" ] +} + +@test "wsl class wires git through Windows ssh.exe" { + cd "$BATS_TEST_DIRNAME/.." + chez init --source "$PWD" --promptString machineClass=wsl --apply + grep -q "sshCommand = ssh.exe" "$TMPHOME/.gitconfig.local" + ! grep -q "gpgsign = true" "$TMPHOME/.gitconfig.local" +} + +@test "create_ file is never overwritten on re-apply" { + cd "$BATS_TEST_DIRNAME/.." + mkdir -p "$TMPHOME/.claude" + echo "my custom notes" > "$TMPHOME/.claude/CLAUDE.local.md" + chez init --source "$PWD" --promptString machineClass=linux --apply + grep -q "my custom notes" "$TMPHOME/.claude/CLAUDE.local.md" +} diff --git a/tests/extra-tmpl.bats b/tests/extra-tmpl.bats index 9cd4b59c666..1f727f29f55 100644 --- a/tests/extra-tmpl.bats +++ b/tests/extra-tmpl.bats @@ -1,31 +1,43 @@ #!/usr/bin/env bats -# op inject parses ALL of .extra.tmpl — comments included — and errors on -# any curly-brace pair or bare op:// text that isn't a real brace-wrapped -# secret reference. These tests enforce that invariant without needing the -# op CLI (CI runners don't have it); real references only ever appear on -# non-comment export lines. +# chezmoi parses private_dot_extra.tmpl as a Go template — comments +# included — so a stray {{ }} anywhere breaks rendering. These tests +# enforce the invariants that keep the template renderable; real template +# actions only ever appear on non-comment export lines. -TMPL=".extra.tmpl" +TMPL="private_dot_extra.tmpl" -@test "extra.tmpl comment lines contain no curly braces" { +@test "extra template comment lines contain no template actions" { cd "$BATS_TEST_DIRNAME/.." run grep -nE '^[[:space:]]*#.*(\{\{|\}\})' "$TMPL" [ "$status" -ne 0 ] } -@test "extra.tmpl comment lines contain no op:// references" { +@test "extra template secret reads are well-formed onepasswordRead calls" { cd "$BATS_TEST_DIRNAME/.." - run grep -nE '^[[:space:]]*#.*op://' "$TMPL" - [ "$status" -ne 0 ] + # Any op:// on an active line must sit inside a onepasswordRead action + # with vault/item/field segments. + while IFS= read -r line; do + [[ "$line" =~ \{\{\ *onepasswordRead\ \"op://[^/]+/[^/]+/[^\"]+\" ]] + done < <(grep -E '^[^#]*op://' "$TMPL" || true) } -@test "extra.tmpl secret references are brace-wrapped and well-formed" { +@test "extra template renders as a valid Go template" { + command -v chezmoi > /dev/null 2>&1 || skip "chezmoi not installed" cd "$BATS_TEST_DIRNAME/.." - # Any op:// on an active line must look like "{{ op://vault/item/field }}" - # (at least three path segments) — a bare or malformed reference is a - # render error waiting to happen. - while IFS= read -r line; do - [[ "$line" =~ \{\{\ *op://[^/]+/[^/]+/[^}]+\ *\}\} ]] - done < <(grep -E '^[^#]*op://' "$TMPL" || true) + # Reference-free today, so this needs no 1Password auth; if references + # are ever added, CI would need op or this becomes a syntax-only check. + # XDG dirs pinned alongside HOME — CI runners export XDG_CONFIG_HOME, + # which chezmoi prefers, and state must not leak between tests. + TMPHOME="$(mktemp -d)" + chez() { + HOME="$TMPHOME" XDG_CONFIG_HOME="$TMPHOME/.config" \ + XDG_DATA_HOME="$TMPHOME/.local/share" XDG_STATE_HOME="$TMPHOME/.local/state" \ + XDG_CACHE_HOME="$TMPHOME/.cache" chezmoi "$@" + } + chez init --source "$PWD" --promptString machineClass=linux + run chez execute-template --source "$PWD" < "$TMPL" + rm -rf "$TMPHOME" + [ "$status" -eq 0 ] + [[ "$output" == *"machine-local secrets"* ]] } diff --git a/tests/shell.bats b/tests/shell.bats index 2ac7101b7fd..c561026865b 100644 --- a/tests/shell.bats +++ b/tests/shell.bats @@ -1,19 +1,21 @@ #!/usr/bin/env bats # The shell dotfiles must parse and source cleanly under zsh on every CI -# platform (ubuntu + macos). Catches zsh syntax errors and unguarded -# macOS-only commands that execute at source time. +# platform (ubuntu + macos + rocky + wsl). Catches zsh syntax errors and +# unguarded macOS-only commands that execute at source time. Source files +# use chezmoi naming (dot_*) but are plain files, so they parse directly; +# tests/chezmoi.bats covers the rendered output. @test "zsh parses the zsh dotfiles" { cd "$BATS_TEST_DIRNAME/.." - for f in .zshrc .zsh_prompt .exports .aliases .functions; do + for f in dot_zshrc dot_zsh_prompt dot_exports dot_aliases dot_functions; do zsh -n "$f" done } @test "bash parses the bash dotfiles and bootstrap" { cd "$BATS_TEST_DIRNAME/.." - for f in .bash_profile .bashrc bootstrap.sh; do + for f in dot_bash_profile dot_bashrc bootstrap.sh; do bash -n "$f" done } @@ -21,10 +23,10 @@ @test "exports, aliases, functions, and prompt source cleanly in zsh" { cd "$BATS_TEST_DIRNAME/.." run zsh -c ' - source ./.exports; true - source ./.aliases; true - source ./.functions; true - source ./.zsh_prompt; true + source ./dot_exports; true + source ./dot_aliases; true + source ./dot_functions; true + source ./dot_zsh_prompt; true ' [ "$status" -eq 0 ] [[ "$output" != *"command not found"* ]]