CLI-driven dotfiles manager. Bun + TypeScript, no framework.
bun install
# check what's managed
dotfiles status
# deploy all modules to $HOME
dotfiles deploy
# import existing config into management
dotfiles import git ~/.gitconfigShell registration (add to ~/.zshrc.local):
function dotfiles() { bun run ~/toy/dotfiles/src/cli.ts "$@"; }
eval "$(dotfiles _completion zsh)"| Command | Description |
|---|---|
status |
Show sync status of all modules |
diff [name] |
Show diff for one or all modules |
deploy [name] |
Deploy modules to $HOME (auto-backups overwritten files) |
pull [name] |
Pull changes from $HOME back into mods (tracked files only) |
import <mod> <file> [file...] |
Import existing file(s) into a module |
uninstall <name> |
Remove deployed files from $HOME (with backup) |
backup |
Backup all local files to ~/backups/dotfiles/ |
restore [file] |
List backups or restore from a backup |
dotfiles/
├── src/cli.ts # CLI entry point (single file)
├── mods/ # Module directory
│ ├── zsh/
│ │ ├── .zshrc # → ~/.zshrc (git tracked)
│ │ └── .zshrc.local # → ~/.zshrc.local (gitignored)
│ ├── ssh/
│ │ └── .ssh/
│ │ ├── config # → ~/.ssh/config (git tracked, Include config.local)
│ │ └── config.local # → ~/.ssh/config.local (gitignored)
│ ├── fish/
│ │ └── .config/fish/config.fish
│ ├── starship/
│ │ └── .config/starship.toml
│ └── hammerspoon/
│ ├── mod.yaml # Module config (optional)
│ └── .hammerspoon/init.lua
├── package.json
└── tsconfig.json
Each subdirectory under mods/ is a module. The file tree inside mirrors $HOME:
mods/zsh/.zshrc → ~/.zshrc
mods/starship/.config/starship.toml → ~/.config/starship.toml
No configuration needed for the common case.
| Mode | Description | When |
|---|---|---|
| copy (default) | Diff-check then copy to $HOME |
Most configs |
| custom | Run deploy.sh in module dir |
Special handling needed |
Optional per-module configuration:
mode: copy # copy | custom
pre: "brew list foo" # Run before deploy, skip module on failure
post: "open -g hammerspoon://reload" # Run after deploy
depends: # Deploy order dependencies
- starship
ignore: # Exclude files (regex)
- "*.bak"Machine-specific configs live alongside tracked files but are gitignored:
| Pattern | Example |
|---|---|
*.local |
.zshrc.local |
*.local.* |
config.local.yaml |
*.custom |
env.custom |
*.custom.* |
secrets.custom.toml |
These files are deployed normally but not committed. The tracked config loads them at runtime:
# .zshrc — source at end
[[ -f ~/.zshrc.local ]] && source ~/.zshrc.local
# .ssh/config — Include at top
Include ~/.ssh/config.local
# config.fish — source at end
test -f ~/.config/fish/config.fish.local && source ~/.config/fish/config.fish.localFile permissions are preserved across deploy/pull/import (important for .ssh).
| Operation | Direction | Scope |
|---|---|---|
| deploy | mods → $HOME |
All files (tracked + local) |
| pull | $HOME → mods |
Tracked files only |
| import | $HOME → mods |
Explicitly specified files |
| backup/restore | mods ↔ ~/backups/ |
Local files only |
New machine workflow:
- Clone repo,
bun install dotfiles deploy— deploy tracked configs- Create
~/.zshrc.localwith machine-specific content dotfiles import zsh ~/.zshrc.local— bring it under managementdotfiles backup— save local files
| Prefix | Created by | Restores to |
|---|---|---|
local-* |
backup |
mods/ |
pre-deploy-* |
deploy (auto) |
$HOME |
uninstall-* |
uninstall (auto) |
$HOME |
| Module | Contents |
|---|---|
| zsh | zinit + plugins + starship + zoxide, .zshrc.local for machine-specific config |
| ssh | SSH client config, config.local for private Host entries |
| fish | Minimal config + starship |
| starship | Two-line prompt, lambda character |
| hammerspoon | Window management (copy mode + reload hook) |