initd bootstraps a fresh development machine from one repo and one shell script. It supports macOS, Fedora Workstation Linux, and headless Ubuntu VMs (a remote dev box with the toolchain and no desktop) from the same codebase.
It owns both:
- machine setup: Homebrew (mac) / dnf (linux), apps, runtimes, OS defaults, Hyprland desktop
- user config: Neovim, Ghostty, Fish, tmux, Git, Hyprland/Quickshell/rofi/dunst, and other dotfiles via managed symlinks
Runtime code lives in four directories: shared/, macos/, linux/, and vm/. Behavior tests live in tests/; documentation lives in docs/. The platform directories are decoupled — rm -rf macos/, linux/ or vm/ and the others keep working.
initd/
├── bootstrap.sh # dispatcher: bootstrap.sh [macos|linux|vm]; detects the OS when none is named
├── tests/ # behavior tests and isolated integration tests
├── docs/ # configuration guides and testing conventions
├── shared/ # cross-platform
│ ├── lib/ # logging, fs, link, cleanup, git-profile, fonts
│ ├── managed-links.sh # MANAGED_LINKS for shared configs + git helpers
│ └── configs/ # colima, fish, git, ghostty, kitty, mise, nvim, starship, tmux
├── macos/ # self-contained macOS bootstrap
│ ├── bootstrap.sh
│ ├── Brewfile
│ ├── brewinstall # add a formula/cask to the Brewfile and install it
│ ├── defaults.sh # macOS defaults write …
│ ├── managed-links.sh
│ └── update.sh
├── linux/ # self-contained Linux bootstrap
│ ├── bootstrap.sh # targets Fedora Workstation 44+ (dnf5)
│ ├── packages.txt # dnf package list (some via COPR — see below)
│ ├── setup.sh # fonts/theme/Firefox profile glue, system fixes
│ ├── managed-links.sh
│ ├── update.sh
│ ├── scripts/ # session scripts invoked by hyprland.lua/Quickshell
│ └── configs/ # hypr, hyprmoncfg, quickshell, rofi, dunst, fontconfig,
│ # systemd, gtk-3.0, gtk-4.0, ghostty, firefox
└── vm/ # self-contained headless VM bootstrap (Ubuntu 24.04+, apt)
├── bootstrap.sh # bash bootstrap.sh vm
├── packages.txt # apt package list: CLI tools + build deps, no desktop
├── managed-links.sh # drops the desktop-side shared links (terminals, Colima)
└── update.sh
git clone https://github.com/vishnukg/initd ~/.config/initd
bash ~/.config/initd/bootstrap.sh macos # or: linux, vmThe dispatcher runs <platform>/bootstrap.sh for the platform you name, and refuses a
platform that doesn't match the OS (macos on Linux, say) before installing anything.
With no platform it picks macos on macOS and linux on Linux, so a headless VM
must always be named. The macOS and Linux desktop bootstraps both:
- install required packages (Homebrew or dnf, enabling COPRs on Linux where Fedora's own repos don't carry a package)
- install mise and gh (dnf repo on Linux, brew on macOS)
- link managed configs into
$HOME, backing up unmanaged files to~/.config/initd-backups/<timestamp>/ - set fish as the login shell (
dsclon macOS,chshon Linux), then sync fisher plugins - run
mise installfor shared runtimes and LSP tooling - prompt for
personal/workGit identity on first run (both write this machine's email intolocal.gitconfig)
Re-running is safe and idempotent.
The macOS bootstrap targets Apple Silicon, with Homebrew at /opt/homebrew.
On a headless Ubuntu VM (e.g. an exe.dev box), name vm — with no platform, every
Linux host gets the Fedora desktop bootstrap:
git clone https://github.com/vishnukg/initd ~/.config/initd
bash ~/.config/initd/bootstrap.sh vmOn macOS, run it from a native arm64 shell. If Command Line Tools are missing, it launches their installer and asks you to rerun after installation completes.
| Action | Command |
|---|---|
| Full bootstrap | bash bootstrap.sh <macos|linux|vm> |
| Re-apply links only | shared/lib/link.sh <macos|linux|vm> |
| Set Git identity | node shared/lib/git-profile.mjs personal / work |
| Remove managed symlinks | node shared/lib/cleanup.mjs <macos|linux|vm> --dry-run |
| Add a brew formula/cask | macos/brewinstall <name> |
| Update tools | macos/update.sh, linux/update.sh or vm/update.sh |
| Run install behavior tests | node --test tests/shared/install.test.mjs |
| Run macOS + shared tests | ./macos/test.sh |
| Run Linux + shared tests | ./linux/test.sh |
| Run the full regression suite | INITD_TEST_TMUX=1 node --test 'tests/**/*.test.mjs' |
See Testing for test organization and conventions.
Runtime paths in $HOME are symlinks back into this repo. Editing happens inside initd, not at the live paths.
| Runtime path | Source |
|---|---|
~/.colima/_templates/default.yaml |
shared/configs/colima/.colima/_templates/default.yaml |
~/.config/fish |
shared/configs/fish/.config/fish |
~/.config/ghostty |
shared/configs/ghostty/.config/ghostty |
~/.config/kitty |
shared/configs/kitty/.config/kitty |
~/.config/mise |
shared/configs/mise/.config/mise |
~/.config/nvim |
shared/configs/nvim/.config/nvim |
~/.config/starship.toml |
shared/configs/starship/.config/starship.toml |
~/.config/tmux |
shared/configs/tmux/.config/tmux |
~/.ssh/config |
shared/configs/ssh/.ssh/config (exe.dev dev box; personal profile only, never on the VM) |
~/.config/ghostty-dev-box.conf |
shared/configs/ghostty/dev-box.conf (every Ghostty window runs ssh dev; personal profile only) |
~/.gitconfig |
shared/configs/git/gitconfig (per-machine email via local.gitconfig) |
| Runtime path | Source |
|---|---|
~/.config/hypr |
linux/configs/hypr |
~/.config/hyprmoncfg |
linux/configs/hyprmoncfg |
~/.config/quickshell |
linux/configs/quickshell |
~/.config/rofi |
linux/configs/rofi |
~/.config/dunst |
linux/configs/dunst |
~/.config/fontconfig |
linux/configs/fontconfig |
~/.config/gtk-3.0 |
linux/configs/gtk-3.0 |
~/.config/gtk-4.0 |
linux/configs/gtk-4.0 |
~/.config/systemd/user/initd-hyprland-session.service |
linux/configs/systemd/user/initd-hyprland-session.service |
~/.config/systemd/user/hyprmoncfgd.service.d |
linux/configs/systemd/user/hyprmoncfgd.service.d (Hyprland-only condition for the packaged daemon) |
~/.config/systemd/user/night-light.service, night-light-schedule.service, night-light-schedule.timer |
linux/configs/systemd/user/ (gammastep as a unit; warm 19:00–07:00) |
~/.gtkrc-2.0, ~/.icons/default/index.theme, Firefox profile glue |
linked individually by linux/setup.sh (paths are dynamic or outside ~/.config/) |
Gitignored paths — used if present, silently skipped if absent.
| Path | Purpose |
|---|---|
shared/configs/git/local.gitconfig |
This machine's Git email (personal or work) — git refuses to commit without it |
shared/configs/fish/.config/fish/local.env.fish |
Machine-specific environment variables, loaded by every Fish shell; set INITD_TMUX_AUTO_ATTACH=0 here to opt out of tmux |
shared/configs/fish/.config/fish/local.fish |
Interactive-only aliases and preferences |
shared/fonts/ |
Clone of the PRIVATE vishnukg/fonts repo (Berkeley Mono — paid, per-user licensed, so a public repo can't carry the OTFs). Synced by shared/lib/fonts.sh, which warns and skips without gh auth |
If a managed runtime path already exists and isn't an initd symlink, it is moved to:
~/.config/initd-backups/<timestamp>/<original-path>
before initd takes ownership. Nothing is deleted.
.gitattributes enforces LF for *.sh, *.fish, *.conf, *.ini, etc. so commits from any host (including Windows) land as LF — the only line-ending bash and /etc/-style configs accept on macOS/Linux.
Bootstrap and update leave manually installed apps in /Applications or
~/Applications under their existing owner's control. They exclude those apps
from a temporary Brewfile while retaining Homebrew-owned apps and all missing
apps. This covers 1Password, BetterDisplay, Chrome, Ghostty, kitty, and iTerm;
the repository's Brewfile stays unchanged. Such manually installed apps continue
to use their own update mechanism.
After linking configs, bootstrap trusts and installs the mise toolchain before
configuring hooks and syncing Fish plugins. Existing font symlinks are replaced
with real copies. Optional private fonts still require access through gh;
rerun bootstrap after authenticating to install them.
macos/brewinstall <name> detects whether the package is a formula or cask, appends it to macos/Brewfile, and runs brew bundle. This JavaScript command requires Node from the completed mise setup. Use --formula or --cask for ambiguous names. Existing entries, including lines with inline comments or options, are preserved; a failed install leaves the entry available for retry. brew bundle dump --force --file macos/Brewfile exports the current machine's state if you want a starting point — review carefully before committing.
Docker comes via Colima (no Docker Desktop): the colima, docker, docker-compose, and docker-credential-helper formulas in the Brewfile, a managed VM template at shared/configs/colima/, and an ensure_docker_config bootstrap step that points docker login at the macOS Keychain and wires the brew-installed compose plugin into the docker CLI. See docs/colima.md.
linux/packages.txt is one package per line, comments allowed. Add to it and re-run linux/bootstrap.sh. The packages list intentionally tracks the brew formulas where equivalents exist (fish, tmux, tig, git) plus Fedora -devel build deps for mise-managed Python/Node. Neovim is deliberately not among them — it is installed and pinned by mise, so the distro package would only shadow it. Fedora's own repos don't carry the Hyprland ecosystem, hyprmoncfg, or ghostty — queue_listed_packages() enables the sdegler/hyprland, paolino/hyprmoncfg, and scottames/ghostty COPRs first. Docker Engine is installed separately from Docker's official dnf repository, together with Buildx and the Compose v2 plugin; bootstrap adds the login user to the privileged docker group, so log out and back in once after its first installation. gh CLI and 1Password also come from their own official dnf repos.
Intel video decoding is configured by ensure_video_codecs() in linux/bootstrap.sh. It enables RPM Fusion free/nonfree repositories, replaces Fedora's restricted Intel VA-API driver with intel-media-driver, and installs ffmpeg-free, libavcodec-freeworld, and libva-utils for H.264/HEVC support. To apply just this step to an existing Fedora install, run bash -c 'source linux/bootstrap.sh; ensure_fedora; ensure_video_codecs; install_queued_packages' from the repo (bootstrap's package steps only queue what is missing, so the install needs that last call). Restart Firefox afterward, then check vainfo and the codec table in about:support; package installation alone does not prove that a playing video uses hardware decoding.
linux/setup.sh handles things that don't fit the symlink flow:
- system fixes requiring sudo (
videogroup membership for backlight keys; disabling idle daemons this machine doesn't use:ModemManager, Fedora'sabrtcrash reporter,rsyslog) - GTK theme + font/cursor gsettings (Fedora has no
fonts-ubuntuor DMZ-cursor package, so this uses Inter fromrsms-inter-fontsand theAdwaitacursor theme instead), plus matching keyboard gsettings so GNOME gets the samectrl:nocapsand repeat rate as Hyprland - session scripts symlinked to absolute
~/.config/paths that hyprland.lua/Quickshell invoke directly - the
hyprmoncfgduser service for automatic monitor profile switching - special-case paths outside
~/.config/:~/.gtkrc-2.0,~/.icons/default/, Firefox profile files (profile path is dynamic; Firefox itself is unmanaged by bootstrap.sh — install it however you like, this glue configures whatever it finds)
- Cross-platform: drop the source under
shared/configs/<name>/, add an entry toMANAGED_LINKSinshared/managed-links.sh. - macOS-only: drop under
macos/configs/<name>/, append toMANAGED_LINKSinmacos/managed-links.sh. - Linux-only: drop under
linux/configs/<name>/, append inlinux/managed-links.sh.
Then re-run node --test tests/shared/install.test.mjs.
docs/bash-primer.md— repo-specific Bash patternsdocs/javascript.md— JavaScript helpers, tmux update flow, and efficiency boundariesdocs/config-review.md— dated validation results and remaining platform checks
<
4F66
li>docs/nvim.md— Neovim setup with Lazy.nvimdocs/mise.md— mise tool managementdocs/colima.md— Colima (Docker without Docker Desktop) setup and daily usedocs/linux-monitors.md— monitor hotplug/lid switching, per-monitor scale and overridesdocs/git-branching-conflicts.md— Git branching and conflict resolutiondocs/tmux-nvim.md— tmux and Neovim workspace conceptsdocs/tmux-sessions.md— tmux session/window/pane workflow and keybindingsdocs/remote-dev-box.md— the exe.dev dev box (ssh dev): setup on a new personal machine, rebuilding the VM
docs/fish.md — fish/bash/zsh syntax comparison