Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 0 additions & 20 deletions .chezmoiignore

This file was deleted.

119 changes: 65 additions & 54 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,96 +18,107 @@ jobs:
sudo apt-get update
sudo apt-get install -y shellcheck zsh

- name: Install chezmoi
run: sh -c "$(curl -fsLS get.chezmoi.io)" -- -b /usr/local/bin

- name: Render chezmoi templates and run shellcheck
- name: shellcheck (non-zsh shell scripts)
run: |
rendered_dir="$(mktemp -d)"
# Render *.tmpl run scripts so shellcheck can parse them
for tmpl in run_*.sh.tmpl; do
[ -f "$tmpl" ] || continue
out="$rendered_dir/$(basename "${tmpl%.tmpl}")"
chezmoi execute-template --source="$PWD" < "$tmpl" > "$out"
done

# shellcheck does not support zsh; filter zsh-shebanged scripts and
# the vendored fzf-git.sh out of the target set.
# The only .sh files are zsh-targeted or vendored (fzf-git.sh); shellcheck
# does not support zsh, so filter those out.
targets=()
while IFS= read -r -d '' f; do
case "$f" in
*/dot_zsh/functions/fzf-git.sh) continue ;;
*/functions/fzf-git.sh) continue ;;
esac
if head -n1 "$f" | grep -qE '^#!.*\bzsh\b'; then
continue
fi
targets+=("$f")
done < <(
find . -type f -name "*.sh" -not -path "./.git/*" -print0
find "$rendered_dir" -type f -name "*.sh" -print0
)
done < <(find . -type f -name "*.sh" -not -path "./.git/*" -print0)

if [ ${#targets[@]} -gt 0 ]; then
shellcheck "${targets[@]}"
else
echo "No shellcheck-eligible scripts in repo (all .sh files are zsh)."
echo "No shellcheck-eligible scripts (all .sh files are zsh or vendored)."
fi

- name: Check zsh syntax
run: |
find . -type f \( -name "*.zsh" -o -name "dot_zshrc" -o -name "executable_*" \) -not -path "./.git/*" -print0 \
find home -type f \( -name "*.zsh" -o -name ".zshrc" \) -print0 \
| while IFS= read -r -d '' file; do
zsh -n "$file" || exit 1
done
zsh -n home/.zsh/bin/reload

- name: shellcheck mise task scripts
# --severity=warning: ignore info/style notes (e.g. SC2016 on the
# intentional literal backticks in echo messages); fail on warning+.
run: shellcheck --severity=warning mise-tasks/*

- name: Check mise task script syntax
run: |
for f in mise-tasks/*; do
bash -n "$f" || exit 1
done

chezmoi-verify:
name: chezmoi verify
mise-bootstrap-verify:
name: mise bootstrap verify
runs-on: macos-latest
env:
MISE_EXPERIMENTAL: "1"
CI: "1"
steps:
- uses: actions/checkout@v4

- name: Install chezmoi
run: sh -c "$(curl -fsLS get.chezmoi.io)" -- -b /usr/local/bin
# The macOS runner ships an older mise that predates `mise bootstrap`
# (introduced in 2026.6.6). Install a recent mise explicitly and put it
# first on PATH so it shadows the pre-installed one.
- name: Install mise
uses: jdx/mise-action@v2
with:
version: 2026.6.11
install: false
cache: false

- name: chezmoi doctor
run: chezmoi --source="$GITHUB_WORKSPACE" doctor || true
- name: Show mise version
run: mise --version

- name: Apply to ephemeral HOME and verify rendered files
- name: Validate mise.toml (tasks parse, formatting)
run: |
mise trust "$GITHUB_WORKSPACE/mise.toml"
mise -C "$GITHUB_WORKSPACE" tasks ls
mise -C "$GITHUB_WORKSPACE" fmt --check

- name: Apply dotfiles to ephemeral HOME and verify symlinks resolve
run: |
export HOME="$(mktemp -d)"
export CI=1
export XDG_CACHE_HOME="$HOME/.cache"
chezmoi init --apply --source="$GITHUB_WORKSPACE"

test -f "$HOME/.zshrc"
test -f "$HOME/.config/mise/config.toml"
test -f "$HOME/.config/ghostty/config"
test -f "$HOME/.hammerspoon/init.lua"
test -f "$HOME/Library/Application Support/Code/User/settings.json"
mise trust "$GITHUB_WORKSPACE/mise.toml"
mise -C "$GITHUB_WORKSPACE" dotfiles apply -y

# Symlinks must resolve to real files (-e follows symlinks)
test -e "$HOME/.zshrc"
test -e "$HOME/.config/mise/config.toml"
test -e "$HOME/.config/ghostty/config"
test -e "$HOME/.hammerspoon/init.lua"
test -e "$HOME/Library/Application Support/Code/User/settings.json"
test -x "$HOME/.zsh/bin/reload"
test -L "$HOME/.zshrc" # must be a symlink, not a copy

grep -F 'idiomatic_version_file_enable_tools = ["ruby"]' "$HOME/.config/mise/config.toml"
grep -F 'appName = "Ghostty"' "$HOME/.hammerspoon/init.lua"

chezmoi verify --source="$GITHUB_WORKSPACE"
- name: Full bootstrap plan parses (dry-run, no side effects)
run: |
export HOME="$(mktemp -d)"
mise trust "$GITHUB_WORKSPACE/mise.toml"
mise -C "$GITHUB_WORKSPACE" bootstrap --dry-run --yes

- name: Idempotency check
- name: Idempotency check (dotfiles status clean after apply)
run: |
export HOME="$(mktemp -d)"
export CI=1
export XDG_CACHE_HOME="$HOME/.cache"
chezmoi init --apply --source="$GITHUB_WORKSPACE"
chezmoi apply --source="$GITHUB_WORKSPACE"
status_output="$(chezmoi status --source="$GITHUB_WORKSPACE")"
if [ -n "$status_output" ]; then
echo "::error::chezmoi status is non-empty after re-apply"
echo "$status_output"
exit 1
fi
diff_output="$(chezmoi diff --source="$GITHUB_WORKSPACE")"
if [ -n "$diff_output" ]; then
echo "::error::chezmoi diff is non-empty after re-apply"
echo "$diff_output"
mise trust "$GITHUB_WORKSPACE/mise.toml"
mise -C "$GITHUB_WORKSPACE" dotfiles apply -y
mise -C "$GITHUB_WORKSPACE" dotfiles apply -y
status_output="$(mise -C "$GITHUB_WORKSPACE" dotfiles status 2>&1)"
echo "$status_output"
if echo "$status_output" | grep -qiE 'pending|differs|missing|conflict'; then
echo "::error::dotfiles status not clean after re-apply"
exit 1
fi
chezmoi verify --source="$GITHUB_WORKSPACE"
22 changes: 11 additions & 11 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
.netrwhist
.DS_Store

# Local environment settings
dot_zsh/env_local.zsh
# Local environment settings (machine-specific; lives inside the symlinked ~/.zsh)
home/.zsh/env_local.zsh

# Worktree directories
.worktree/

# Claude AI (local settings only)
dot_claude/local/
dot_claude/settings.local.json
dot_claude/hooks/
dot_claude/projects/
dot_claude/scripts/
dot_claude/shell-snapshots/
dot_claude/statsig/
dot_claude/todos/
# Claude AI (local settings only; ~/.claude is a real dir, only settings.json is symlinked)
home/.claude/local/
home/.claude/settings.local.json
home/.claude/hooks/
home/.claude/projects/
home/.claude/scripts/
home/.claude/shell-snapshots/
home/.claude/statsig/
home/.claude/todos/

# Firebase
firebase-debug.log
9 changes: 0 additions & 9 deletions .mise.toml

This file was deleted.

File renamed without changes.
112 changes: 42 additions & 70 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,113 +4,85 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

## Repository Overview

This is a dotfiles repository for a macOS development environment, managed by [chezmoi](https://www.chezmoi.io/). Dotfiles, run-once installers, and run-onchange configuration scripts are organized using chezmoi's filename conventions (`dot_*`, `private_*`, `executable_*`, `run_once_*`, `run_onchange_*`).
This is a dotfiles repository for a macOS development environment, managed by [`mise bootstrap`](https://mise.jdx.dev/bootstrap.html). A single `mise.toml` at the repo root declares everything: language runtimes (`[tools]`), dotfile symlinks (`[dotfiles]`), macOS defaults (`[bootstrap.macos.defaults]`), lifecycle hooks (`[bootstrap.hooks]`), and tasks (`[tasks]`). Homebrew packages live in `Brewfile`.

Language runtimes (Flutter, Rust, Node, Ruby) are managed by [mise](https://mise.jdx.dev/) via `dot_config/mise/config.toml`. Homebrew packages are defined in `dot_Brewfile` and installed by `run_onchange_install-brew-packages.sh.tmpl`.
> `mise bootstrap` is **experimental** (requires `experimental = true` / `MISE_EXPERIMENTAL=1`) and was introduced in mise **2026.6.6**. mise itself is installed via Homebrew (`brew 'mise'` in `Brewfile`); bootstrap is a mise subcommand, so mise must exist before it can run.

## Commands

### Setup and Installation

```bash
# First time on a new machine
brew install chezmoi
chezmoi init --apply ry-itto/dotfiles
# First time on a new machine (mise must already be installed via brew)
brew install mise
ghq get ry-itto/dotfiles
cd "$(ghq root)/github.com/ry-itto/dotfiles"
MISE_EXPERIMENTAL=1 mise bootstrap --yes # env var needed on first run only

# Pull latest changes from the repo and re-apply
chezmoi update
# Pull latest changes and re-apply
git pull && mise bootstrap --yes

# See what would change before applying
chezmoi diff
# Preview without making changes
mise bootstrap --dry-run

# Apply pending changes
chezmoi apply
# Apply only the dotfile symlinks
mise dotfiles apply
```

### Editing Managed Files

After migrating to chezmoi, editing files directly under `$HOME` does **not** sync back to this repository. Always use one of:
Dotfiles are **symlinks** into `home/` (not copies). Editing `~/.zshrc` edits `home/.zshrc` in this repo directly — no apply step is needed for content changes. Run `mise bootstrap` only when adding new `[tools]` / `[dotfiles]` / packages.

```bash
# Open the source file in $EDITOR
chezmoi edit ~/.zshrc

# Or jump to the source directory and edit there
cd "$(chezmoi source-path)"
$EDITOR dot_zshrc
chezmoi apply
```
> **Do not delete or move the cloned repo** — the symlinks point into it and would break.

### Adding New Files

```bash
# Move an existing $HOME file into chezmoi management
chezmoi add ~/.somefile
```
Add the source file under `home/` (mirroring its `$HOME` path), then add a `[dotfiles]` entry in `mise.toml` and run `mise dotfiles apply`. `mise dotfiles add ~/.somefile` can do both steps.

## Architecture

### Source Layout

The repository **is** the chezmoi source directory. chezmoi reads filename prefixes to decide where each file goes in `$HOME`:

- `dot_<name>` → `~/.<name>` (e.g. `dot_zshrc` → `~/.zshrc`)
- `executable_<name>` → preserves +x bit on apply
- `private_<name>` → applied with mode 0600/0700
- `<name>.tmpl` → rendered with chezmoi's template engine before apply
- `run_once_<name>.sh` → executed once per machine
- `run_onchange_<name>.sh` → executed when the script's content changes

### Top-Level Files
- `mise.toml` — bootstrap config: `[tools]`, `[dotfiles]`, `[bootstrap.macos.defaults]`, `[bootstrap.hooks]`, `[tasks]`.
- `Brewfile` — Homebrew formulae **and casks**. mise's native `[bootstrap.packages]` only resolves formulae, so the Brewfile is the source of truth and is installed via the `brew-bundle` task.
- `home/` — source tree mirroring `$HOME`. Each file/dir is symlinked to its target by `[dotfiles]` (e.g. `home/.zshrc` → `~/.zshrc`). Executable bits are preserved through the symlink.
- `home/.config/mise/config.toml` — the user's **global** mise settings (`idiomatic_version_file_enable_tools`, `experimental = true`), symlinked to `~/.config/mise/config.toml`. Distinct from the repo-root `mise.toml` (the bootstrap orchestrator).

**Managed dotfiles** (chezmoi targets):
- `dot_zshrc` — entrypoint that sources modules under `~/.zsh/`
- `dot_zsh/` — modular Zsh config: `alias.zsh`, `env.zsh`, `style.zsh`, `plugin.zsh`, `functions/`, `bin/executable_reload`
- `dot_gitconfig`, `dot_Brewfile`, `dot_commit_template`
- `dot_vim/`, `dot_hammerspoon/`, `dot_claude/`
- `dot_config/nvim/`, `dot_config/starship.toml`, `dot_config/mise/config.toml`
- `private_Library/private_Application Support/Code/User/settings.json` — VSCode user settings
### Bootstrap Step Order

**Run scripts** (executed during `chezmoi apply`):
- `run_onchange_install-brew-packages.sh.tmpl` — re-runs when `dot_Brewfile` changes
- `run_onchange_configure-macos-defaults.sh` — `defaults write` for NSGlobalDomain, Finder, key repeat, Caps Lock → Control
- `run_onchange_configure-xcode.sh` — `defaults write` for Xcode build settings
- `run_once_install-zplug.sh` — bootstrap zplug
- `run_once_install-dein.sh` — bootstrap dein.vim
- `run_once_install-mise-tools.sh` — runs `mise install` for tools defined in `dot_config/mise/config.toml`
`mise bootstrap` runs steps in this fixed order (see `mise bootstrap --help`):

**Configuration**:
- `.chezmoiignore` — paths chezmoi should skip during apply (README, scripts/, CI files, destination-side local files)
1. `[bootstrap.packages]` install + `post-packages` hook → `mise run brew-bundle` (Homebrew formulae + casks from `Brewfile`).
2. `[dotfiles]` apply → symlinks under `home/`.
3. `[bootstrap.macos.defaults]` + `post-defaults` hook → declarative defaults, then `mise run macos-extra` for imperative settings (Caps Lock → Control, Xcode dynamic core count, `xcodes install`).
4. `mise install` → language runtimes in `[tools]` (Flutter, Rust, Vim).
5. `bootstrap` task → vim/zsh plugin managers (dein.vim, zplug).

**Repository support files** (excluded from `chezmoi apply` via `.chezmoiignore`):
- `.github/workflows/ci.yml` — lint, chezmoi-verify
- `README.md`, `CLAUDE.md`, `LICENSE`
Hooks delegate to tasks because hooks do **not** expand `{{config_root}}` and lack `$MISE_PROJECT_ROOT`; tasks do (and `mise run` works from a hook since the hook's cwd is the repo root).

### Run Script Execution Order
### Tasks (`[tasks]` in mise.toml)

`chezmoi apply` runs `run_*` scripts in lexical order of their filename. The current ordering ensures:
- `brew-bundle` — `brew bundle` from `Brewfile`. Uses `{{config_root}}/Brewfile`.
- `macos-extra` — imperative macOS settings with no declarative form.
- `bootstrap` — dein.vim + zplug installers. Runs **every** bootstrap, so each step self-gates on an existence check for idempotency.

1. `run_onchange_configure-macos-defaults.sh`
2. `run_onchange_configure-xcode.sh`
3. `run_onchange_install-brew-packages.sh` (installs `mise` via Brewfile)
4. `run_once_install-dein.sh`
5. `run_once_install-mise-tools.sh` (skips with a notice if `mise` is not yet on PATH)
6. `run_once_install-zplug.sh`
All tasks exit early when `CI` is set (`[ -n "${CI:-}" ] && exit 0`).

If `mise` is not yet installed when `run_once_install-mise-tools.sh` runs, the script exits cleanly. Re-running `chezmoi apply` after the brew bundle finishes will trigger it again.
### CI (`.github/workflows/ci.yml`)

All run scripts honor `CI=1` and exit early in CI to avoid expensive operations.
- **lint** (ubuntu): shellcheck for non-zsh `.sh`, `zsh -n` syntax check on `home/` zsh files.
- **mise-bootstrap-verify** (macOS): `brew install mise`, validate `mise.toml` (`tasks ls`, `fmt --check`), `mise dotfiles apply` to an ephemeral HOME and assert symlinks resolve, `mise bootstrap --dry-run`, and an idempotency check. Runs with `CI=1` and `MISE_EXPERIMENTAL=1`.

## Development Stack

- **iOS Development**: Xcode, XcodeGen, xcbeautify (Homebrew)
- **Flutter / Rust / Node / Ruby**: managed by mise (`dot_config/mise/config.toml`)
- **Web Development**: Node.js (via mise), npm/yarn ecosystem
- **General**: Git, GitHub CLI, Neovim, Starship prompt
- **Flutter / Rust / Vim**: managed by mise (`[tools]` in `mise.toml`)
- **General**: Git, GitHub CLI, Neovim, Starship prompt, Ghostty, Hammerspoon

## Key Design Principles

1. **Single source of truth**: chezmoi manages all dotfiles; mise manages all language runtimes.
1. **Single source of truth**: `mise bootstrap` (`mise.toml`) manages dotfiles, packages, defaults, and tools; Homebrew owns package installation via `Brewfile`.
2. **macOS-only**: no OS branching. `defaults write` and other macOS-specific commands run unconditionally.
3. **Idempotency**: `run_once_*` scripts gate themselves on existence checks; `run_onchange_*` scripts re-run only when their content (or referenced files) change.
4. **Hand-off to upstream tools**: chezmoi delegates package management to Homebrew (`brew bundle`) and mise (`mise install`) rather than reimplementing version logic.
3. **Idempotency**: declarative steps (dotfiles, defaults, tools) converge; the `bootstrap` task self-gates on existence checks because it runs on every bootstrap.
4. **Hand-off to upstream tools**: bootstrap delegates package management to Homebrew (`brew bundle`) and runtime management to mise (`mise install`) rather than reimplementing version logic.
5. **Symlinks, not copies**: dotfiles live in `home/` and are symlinked into `$HOME`; the repo must remain present at its clone location.
Loading
Loading