The skill wasn't firing reliably, so instead of polluting the global config with a skill that only helped *sometimes*, convert it into project-specific instructions in .config/dot Future work on this project (dotfiles) should always start from this directory, even if changes are made outside. This helps Claude keep the scope narrow.
4.9 KiB
Dotfiles
This machine's dotfiles are a bare git repo at ~/.dotfiles, checked out with
$HOME as its work-tree. The dot fish function wraps that invocation
(git --git-dir=~/.dotfiles --work-tree=$HOME $argv, declared with
--wraps=git), so every git subcommand works through it: dot status,
dot add, dot commit, dot push, etc.
This directory (~/.config/dot) holds the dot CLI's custom subcommands,
tests, and package lists, but the repo tracks files across $HOME — fish
config, git identity, the dot function itself, and more. To see everything
tracked, run dot ls-tree -r --name-only HEAD from $HOME (paths are shown
relative to cwd, so running it from elsewhere silently truncates the list).
Always add by explicit path
status.showUntrackedFiles=no is set locally (see dot init below), and
.gitignore only excludes .dotfiles itself plus OS/editor cruft — it is
not a whitelist. That
means virtually everything under $HOME reads as untracked, and git status
deliberately hides all of it.
Always run dot add <specific-path>. Never dot add -A, dot add ., or
any wildcard add — that would try to stage the entire home directory (caches,
secrets, everything).
The dot CLI
Architecture
dot is defined in one file: ~/.config/fish/functions/dot.fish. It holds
three functions:
dot(--wraps=git) — dispatchesinit,help, and any file found under~/.config/dot/commands/, otherwise forwards everything togit --git-dir=~/.dotfiles --work-tree=$HOME $argv(full passthrough).__dot_init— the bootstrap logic, inlined in the same file rather than autoloaded separately, because it's the one subcommand that must work before the dotfiles repo has ever been cloned onto a machine.__dot_help— prints usage: the built-in commands plus whatever is currently found under~/.config/dot/commands/, generated by globbing that directory rather than a hardcoded list, so it can't drift from reality.
__dot_help's glob over ~/.config/dot/commands/*.fish is duplicated in
~/.config/fish/completions/dot.fish's __dot_custom_subcommands rather than
shared: fish only autoloads a function from a file named after that function,
so a helper defined inside dot.fish would be undefined if tab-completion
ran before dot had ever been sourced in the session. Keep both copies in
sync when the listing logic changes.
dot init:
- refuses to run if
~/.dotfilesalready exists (no re-init support) - clones the bare repo from
--url(default: the hardcoded Gitea remote) — if the clone fails, it errors out; it never falls back togit init - backs up any pre-existing file that checkout would clobber into
~/.dotfiles-backup/<timestamp>/, then retries the checkout - explicitly sets
status.showUntrackedFiles=noafter cloning — this is a local-only git setting, so a freshgit clonenever carries it over
Adding a subcommand
Beyond init, dot looks for ~/.config/dot/commands/<name>.fish, sources
it, and calls _dot_<name>. These files are deliberately kept out of
~/.config/fish/functions/ (fish's autoload path) so they never become
independently invokable top-level commands or clutter tab-completion outside
of dot itself.
- Create
~/.config/dot/commands/<name>.fishdefining a_dot_<name>function. - Confirm
dot <name>dispatches to it. No other wiring is needed —~/.config/fish/completions/dot.fishand__dot_helpboth discover new command files by globbing that directory, and--wraps=gitstill covers raw git subcommands. - Add a case to
~/.config/dot/tests/dot.fishcovering it and runfishtape ~/.config/dot/tests/dot.fishuntil it passes.
Testing
Tests live at ~/.config/dot/tests/dot.fish, run with
fishtape ~/.config/dot/tests/dot.fish. Fishtape is installed via Fisher
(fisher install jorgebucaran/fishtape) and tracked in
~/.config/fish/fish_plugins — a real, restorable dependency for developing
dot, but never required just to use it.
- Each scenario overrides
$HOME(set -gx HOME (mktemp -d)) before callingdot, so tests never touch the real~/.dotfiles. - Build a throwaway bare "remote" fixture with
git init --bareplus a seeded commit, and explicitly set itsHEAD(git --git-dir=$remote symbolic-ref HEAD refs/heads/main). Pushing withgit push origin HEAD:maindoes not update the bare repo'sHEADsymref — skip this and a clone of the fixture can end up "on a branch yet to be born." - Don't use
.gitconfigas a fake pre-existing "conflict" file in a fixture — git parses$HOME/.gitconfigas its own global config on every invocation, and garbage content there spams "key does not contain a section" errors that drown out the real assertion. Use a harmless file like.bashrcinstead. @test "description" <expr> <op> <expected>mirrors fish'stestbuiltin (-eq,-ne,=,-e,-f,-d,-n,-z);-a/-ocombinators aren't supported.