Files
dotfiles/.claude/skills/dotfiles/DOT-CLI.md

3.7 KiB

The dot CLI

Architecture

dot is defined in one file: ~/.config/fish/functions/dot.fish. It holds three functions:

  • dot (--wraps=git) — dispatches init, help, and any file found under ~/.config/dot/commands/, otherwise forwards everything to git --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 ~/.dotfiles already 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 to git init
  • backs up any pre-existing file that checkout would clobber into ~/.dotfiles-backup/<timestamp>/, then retries the checkout
  • explicitly sets status.showUntrackedFiles=no after cloning — this is a local-only git setting, so a fresh git clone never 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.

  1. Create ~/.config/dot/commands/<name>.fish defining a _dot_<name> function.
  2. Confirm dot <name> dispatches to it. No other wiring is needed — ~/.config/fish/completions/dot.fish and __dot_help both discover new command files by globbing that directory, and --wraps=git still covers raw git subcommands.
  3. Add a case to ~/.config/dot/tests/dot.fish covering it and run fishtape ~/.config/dot/tests/dot.fish until 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 calling dot, so tests never touch the real ~/.dotfiles.
  • Build a throwaway bare "remote" fixture with git init --bare plus a seeded commit, and explicitly set its HEAD (git --git-dir=$remote symbolic-ref HEAD refs/heads/main). Pushing with git push origin HEAD:main does not update the bare repo's HEAD symref — skip this and a clone of the fixture can end up "on a branch yet to be born."
  • Don't use .gitconfig as a fake pre-existing "conflict" file in a fixture — git parses $HOME/.gitconfig as 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 .bashrc instead.
  • @test "description" <expr> <op> <expected> mirrors fish's test builtin (-eq, -ne, =, -e, -f, -d, -n, -z); -a/-o combinators aren't supported.