Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
8a173a4
fix(install): resilient docker + npm + GitHub-token bootstrap
vaintrub May 16, 2026
6f5aead
feat(install): 1Password token cascade + add op CLI + 1Password cask
vaintrub May 16, 2026
3f767f9
test(ci): add dev profile matrix + tests/files/dev.bats
vaintrub May 16, 2026
8db612a
feat(install): mise partial-failure warning + first-apply hint + docs
vaintrub May 16, 2026
5da5641
fix(ci): split common.bats per profile + two-apply convergence pattern
vaintrub May 16, 2026
e0db7a6
fix(ci): add ~/.local/share/mise/shims to GITHUB_PATH
vaintrub May 16, 2026
5cf7c62
fix(ci): replace pre-seed with chezmoi init for apply-dev (caches tem…
vaintrub May 16, 2026
5a96fbc
fix(ci): seed chezmoi.toml profile=dev BEFORE init (hasKey short-circ…
vaintrub May 16, 2026
828589a
feat(install): unify op + rtk via mise (cross-platform)
vaintrub May 17, 2026
17e2922
fix(install): drop 1password-cli cask (now via mise)
vaintrub May 17, 2026
2d6ac28
refactor(install): merge rtk init into lib + drop dead fd fallback
vaintrub May 17, 2026
84bc57b
feat(install): migrate claude+codex to mise aqua backend
vaintrub May 17, 2026
c7a860e
chore: comment+test cleanup after recent migrations
vaintrub May 17, 2026
d733bef
docs: sync README + AGENTS with post-migration repo state
vaintrub May 17, 2026
ad604b9
chore: drop legacy profile="mac" → "workstation" translation
vaintrub May 17, 2026
0965183
docs: fix tool count drift + stale script prefix list
vaintrub May 17, 2026
5140097
chore: trim verbose comments across scripts + lib + templates
vaintrub May 17, 2026
155aa76
docs: post-trim audit — unify mise tool counts + fix stale yaml key ref
vaintrub May 17, 2026
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
59 changes: 11 additions & 48 deletions .chezmoi.toml.tmpl
Original file line number Diff line number Diff line change
@@ -1,41 +1,13 @@
{{- /*
Rendered into ~/.config/chezmoi/chezmoi.toml on `chezmoi init`. Cached
[data] values short-circuit prompts on subsequent init runs.

Install profile (3 tiers, cascade):
core minimum to make these dotfiles functional. Installs:
- zsh, vim, tmux, git, curl, ca-certificates (Linux apt/dnf)
- Linux: ufw + tcpdump (network safety baseline)
- MesloLGS NF + Monaspace Neon fonts (chezmoi externals)
- mise + zoxide + fzf (zshrc integrations depend on these)
Use case: Hetzner VPS, DO droplet, jetson via SSH, Codespace,
devcontainer, recovery. ~50 MB.
dev core + CLI dev toolchain. Adds:
- 25 mise aqua tools (kubectl, helm, jq, gh, k9s, ...)
- Languages: Go, Python, Node, Rust (via mise core)
- Linux: docker.io, htop, tree, wget, nmap, telnet, libpq,
wireguard-tools, build-essential, xsel/wl-clipboard
- Mac brews: htop, tree, wget, nmap, telnet, libpq,
wireguard-tools, rtk (no docker on Mac dev — pick
workstation for Docker Desktop)
- goimports, ssh-audit (post-installs)
- AI npm globals: claude-code, codex
Use case: headless dev box (jetson, Ubuntu VM, Linux VPS for
actual work, Mac SSH'd into headless). ~1.9 GB first apply.
workstation dev + GUI apps. Adds:
- Mac: casks (iterm2, docker-desktop, vscode, ngrok, fonts)
- Linux: placeholder (empty for now)
Use case: primary GUI machine (Mac laptop, Linux desktop).

Environment is auto-detected and shown as a HINT in the prompt — you
always pick explicitly. No silent auto-decision. Default = "core" (safe
fallback for CI / non-interactive / accidental enter).

To re-prompt later: `chezmoi init --prompt`.
Skip prompt non-interactively (CI): default kicks in via --promptDefaults.
Rendered to ~/.config/chezmoi/chezmoi.toml on `chezmoi init`. Cached
[data] values short-circuit prompts on subsequent runs.
Profile tiers (cascade core ⊂ dev ⊂ workstation) — see README for details.
Env hint is informational only; user always picks. Default "core" is the
safe fallback for CI / non-interactive / accidental enter.
Re-prompt: `chezmoi init --prompt`.
*/ -}}

{{- /* ────── env detection (HINT ONLY — not a decision) ────── */ -}}
{{- /* env detection (hint only) */ -}}
{{- $isSSH := or (ne (env "SSH_CONNECTION") "") (ne (env "SSH_CLIENT") "") (ne (env "SSH_TTY") "") -}}
{{- $hasGUI := false -}}
{{- if eq .chezmoi.os "darwin" -}}
Expand All @@ -51,7 +23,7 @@
{{- else if not $ephemeral -}}{{- $detected = "dev" -}}
{{- end -}}

{{- /* ────── identity prompts (cached after first init) ────── */ -}}
{{- /* identity prompts (cached after first init) */ -}}
{{- $interactive := stdinIsATTY -}}
{{- if $ephemeral -}}{{- $interactive = false -}}{{- end -}}

Expand All @@ -69,20 +41,11 @@
{{- $email = promptString "Email" $email -}}
{{- end -}}

{{- /* ────── profile prompt (ALWAYS fires on first init via promptChoiceOnce;
default = "core" so CI / non-TTY get safe fallback) ────── */ -}}
{{- /* profile prompt — promptChoiceOnce caches after first run; default "core" */ -}}
{{- $hint := printf "Install profile [detected env: %s (GUI=%t, SSH=%t, ephemeral=%t)] — you pick" $detected $hasGUI $isSSH $ephemeral -}}
{{- $rawProfile := promptChoiceOnce . "profile" $hint (list "core" "dev" "workstation") "core" -}}

{{- /* ────── legacy translation: previous design used profile="mac" for
workstation Mac. Map silently on next init so existing machines
don't need manual cleanup. ────── */ -}}
{{- $profile := $rawProfile -}}
{{- if eq $rawProfile "mac" -}}{{- $profile = "workstation" -}}{{- end -}}
{{- $profile := promptChoiceOnce . "profile" $hint (list "core" "dev" "workstation") "core" -}}

{{- /* ────── .osid composite key: "darwin" / "linux-<id>". Single derived
value used by .chezmoiscripts/* — replaces nested if-elif on
.chezmoi.osRelease.id. Pattern from twpayne/dotfiles. ────── */ -}}
{{- /* .osid composite key (darwin / linux-<id>) — used by .chezmoiscripts/* */ -}}
{{- $osid := .chezmoi.os -}}
{{- if eq .chezmoi.os "linux" -}}
{{- if hasKey .chezmoi.osRelease "id" -}}
Expand Down
113 changes: 42 additions & 71 deletions .chezmoidata/packages.yaml
Original file line number Diff line number Diff line change
@@ -1,44 +1,26 @@
# Single source of truth for OS-package installation.
# Consumed by .chezmoiscripts/run_onchange_after_50-install-packages.sh.tmpl
# (renders into env vars) + lib/install-packages.sh (consumes env, installs).
# OS-package source of truth. Consumed by run_onchange_after_50-install-
# packages.sh.tmpl (renders into env) + lib/install-packages.sh (installs).
#
# Install profile tiers (set in chezmoi.toml [data].profile):
# core minimum to make dotfiles functional. Installs zsh+vim+tmux+
# git+curl+ca-certs via apt/dnf (on Mac these come from Apple
# base). Network safety baseline: ufw+tcpdump (Linux); Mac
# tcpdump system-provided, ufw has no equivalent (use App
# Firewall via System Settings → Privacy & Security).
# dev core + dev toolchain: htop/tree/wget/nmap/telnet/wireguard/
# psql/build-essential (Linux+Mac symmetric), docker engine
# (Linux only — Mac users wanting Docker pick `workstation`
# which installs Docker Desktop), AI CLIs (claude-code, codex).
# workstation dev + GUI apps. Installs differ by OS:
# - Mac: casks (iterm2, docker-desktop, vscode, ngrok, fonts)
# - Linux: placeholder (empty for now — populated when I run
# Linux desktop machine)
# Tier cascade: core ⊂ dev ⊂ workstation.
# core dotfile baseline + Linux network safety (ufw, tcpdump).
# dev + CLI dev toolchain. Docker NOT here (distro-conflict-prone).
# workstation + GUI casks (Mac); Linux placeholder.
#
# Cascade: dev includes core. workstation includes dev (+ core).
#
# Note: mise tools (kubectl, helm, k9s, jq, gh, fd, fzf, ...) live in
# dot_config/mise/config.toml.tmpl (cross-platform aqua backend), gated
# inside that file by `$isDev` (cascades dev+workstation). Not in this YAML.
# Cross-platform dev binaries (kubectl, helm, op, rtk, claude-code, codex, …)
# live in dot_config/mise/config.toml.tmpl, gated by $isDev.

packages:
# === core tier — always installed ===
core:
# Mac: zsh/vim/tmux/git/curl ship with macOS. No brew install at this
# tier. Fonts handled by .chezmoiexternal.toml.tmpl externals.
brews: []
# Linux: bootstrap + safety baseline.
brews: [] # Mac ships zsh/vim/tmux/git/curl from base
apt:
- zsh # shell (chsh switches login shell)
- vim # $EDITOR default
- tmux # dot_tmux.conf.local
- git # antidote clone + chezmoi update
- curl # curl-pipes (mise install, etc.)
- ca-certificates # HTTPS verification
- ufw # firewall (disabled by default; Mac uses App Firewall)
- tcpdump # packet capture (Mac tcpdump = /usr/sbin/tcpdump system)
- zsh
- vim
- tmux
- git
- curl
- ca-certificates
- ufw # disabled by default; Mac uses App Firewall
- tcpdump
dnf:
- zsh
- vim
Expand All @@ -49,67 +31,56 @@ packages:
- ufw
- tcpdump

# === dev tier — adds CLI dev toolchain ===
dev:
# Mac brew formulae (tools not in mise's aqua registry).
# Tools without a mise registry entry (C-deps, system integration).
brews:
- htop # process monitor
- tree # directory tree
- wget # alt to curl
- nmap # network scanner
- telnet # legacy net tool
- libpq # psql/pg_dump (keg-only, force-linked)
- wireguard-tools # VPN CLI
- rtk # token-saving Claude/Codex proxy
# Linux apt: OS-native dev tools (symmetric to brews above + docker engine).
- htop
- tree
- wget
- nmap
- telnet
- libpq # keg-only; lib force-links it
- wireguard-tools
# Docker NOT here: docker.io (Ubuntu repo) + docker-ce (Docker repo)
# cannot coexist. Pick the right one per machine, or use workstation
# profile on Mac for Docker Desktop.
apt:
- build-essential # compilers (Xcode CLT equivalent — Mac gets via brew side-effect)
- xsel # tmux yank → clipboard (X11; Mac uses pbcopy built-in)
- wl-clipboard # tmux yank → clipboard (Wayland; Mac uses pbcopy built-in)
- docker.io # Docker engine (Mac dev tier gets nothing here — pick `workstation` for Docker Desktop)
- build-essential
- xsel # tmux yank → X11 clipboard
- wl-clipboard # tmux yank → Wayland clipboard
- htop
- tree
- wget
- nmap
- inetutils-telnet # telnet (Debian/Ubuntu package name)
- inetutils-telnet # Debian/Ubuntu telnet package name
- wireguard-tools
- postgresql-client # psql
# Linux dnf (Fedora-family).
- postgresql-client
dnf:
- xsel
- wl-clipboard
- docker
- htop
- tree
- wget
- nmap
- telnet
- wireguard-tools
- postgresql
# AI CLI globals — installed via mise's npm after mise install. Available
# at dev tier and above (cascades into workstation). Cross-platform.
npm_global:
- "@anthropic-ai/claude-code"
- "@openai/codex"

# === workstation tier — adds GUI apps (per-OS lists) ===
gui:
# Mac casks. Installed when profile=workstation AND OS=darwin.
# Installed when profile=workstation AND OS=darwin.
mac_casks:
- iterm2 # terminal
- docker-desktop # Docker engine + UI (Mac equivalent of Linux docker.io)
- iterm2
- docker-desktop
- visual-studio-code
- ngrok # tunnel UI
- font-meslo-lg-nerd-font # MesloLGS — backstop alongside .chezmoiexternal font drop
- font-monaspace # GitHub Monaspace family
# Linux GUI packages. Installed when profile=workstation AND OS=linux.
# Placeholder — populate when I actually run a Linux desktop (Ubuntu/Pop/
# Fedora workstation). Candidates: code, firefox, vlc, gimp, ...
- ngrok
- 1password # desktop app — Touch ID unlock for `op` CLI
- font-meslo-lg-nerd-font
- font-monaspace
# Linux desktop placeholder.
linux_apt: []
linux_dnf: []

# Claude Code + Codex plugins — unchanged by tier system. Installed when
# claude/codex CLI is on PATH (dev tier and above).
# Plugins (Claude + Codex) — installed when respective CLI on PATH.
plugins:
claude:
- gopls-lsp@claude-plugins-official
Expand Down
37 changes: 37 additions & 0 deletions .chezmoiscripts/run_once_after_99-post-install-hint.sh.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{{- /*
First-apply hint. Fires once per machine (run_once_after_, chezmoi state
bucket tracks). Re-trigger: `chezmoi state delete-bucket --bucket=scriptState`.
*/ -}}
#!/bin/sh
set -eu

cat <<'EOF'

[install] Setup complete. Next steps:

exec zsh # Reload current shell so mise shims are on PATH.
# After this: aws --version kubectl version --client etc.

mise ls # Inspect installed tools. Any (missing) row → re-fetch needed.

If any tools are missing (anonymous GitHub API rate-limit on slow links):

gh auth login # interactive auth — token auto-detected next apply
# OR — if you use 1Password, create the item once:
# op item create --vault Personal --title 'GitHub API Token' \
# credential=<your-PAT>
chezmoi apply # picks up the new token and retries missing tools
mise reshim # only if shim symlinks are stale (rare)

Per-machine override (skip rust on a small VPS, pin a different python):

cat > ~/.config/mise/config.local.toml <<'OVERRIDE'
[tools]
rust = "skip"
python = "3.11"
OVERRIDE

# Re-apply to pick up the override:
chezmoi apply

EOF
44 changes: 26 additions & 18 deletions .chezmoiscripts/run_onchange_after_50-install-packages.sh.tmpl
Original file line number Diff line number Diff line change
@@ -1,29 +1,19 @@
{{- /*
Tier-cascade package installer (3 profiles: core / dev / workstation).
This script is a thin wrapper that renders chezmoi facts (profile,
osid, package lists from .chezmoidata/packages.yaml) into env vars,
then sources lib/install-packages.sh and calls main.

All logic lives in lib/install-packages.sh — testable via bats unit
tests with mocked external commands.

Re-runs whenever rendered output changes — chezmoi hashes rendered
script. packages.yaml, mise.toml, profile, or lib edits all trigger
a re-render.
Thin wrapper: renders chezmoi facts (profile, osid, package lists) into
DOTFILES_* env vars, then sources lib/install-packages.sh::main. All
install logic lives in the lib — bats-testable.
*/ -}}
#!/bin/sh
set -eu

# Force apt non-interactive (chezmoi child sh has no TTY — tzdata would prompt).
# Non-interactive apt (chezmoi child sh has no TTY).
export DEBIAN_FRONTEND=noninteractive
export TZ=Etc/UTC

# Make ~/.local/bin and mise shims discoverable for this script and post-
# installs. Debian's ~/.profile adds ~/.local/bin only for LOGIN shells;
# chezmoi exec()s without sourcing profile.
# Debian's ~/.profile adds ~/.local/bin only for LOGIN shells; chezmoi exec()s
# without sourcing profile. Ensure mise shims are reachable too.
export PATH="$HOME/.local/bin:$HOME/.local/share/mise/shims:$PATH"

# Render chezmoi facts → env. Lib reads only these, never .chezmoi.* directly.
export DOTFILES_PROFILE={{ .profile | quote }}
export DOTFILES_OS={{ .chezmoi.os | quote }}
export DOTFILES_OSID={{ .osid | quote }}
Expand All @@ -37,12 +27,30 @@ export DOTFILES_DEV_APT={{ (.packages.dev.apt | default (list)) | sortAlpha | un
export DOTFILES_CORE_DNF={{ (.packages.core.dnf | default (list)) | sortAlpha | uniq | join " " | quote }}
export DOTFILES_DEV_DNF={{ (.packages.dev.dnf | default (list)) | sortAlpha | uniq | join " " | quote }}

# Workstation tier (GUI apps). Per-OS lists from packages.gui.
export DOTFILES_GUI_MAC_CASKS={{ (.packages.gui.mac_casks | default (list)) | sortAlpha | uniq | join " " | quote }}
export DOTFILES_GUI_LINUX_APT={{ (.packages.gui.linux_apt | default (list)) | sortAlpha | uniq | join " " | quote }}
export DOTFILES_GUI_LINUX_DNF={{ (.packages.gui.linux_dnf | default (list)) | sortAlpha | uniq | join " " | quote }}

export DOTFILES_DEV_NPM_GLOBAL={{ (.packages.dev.npm_global | default (list)) | sortAlpha | uniq | join " " | quote }}
# GITHUB_TOKEN cascade: 1Password → gh → anon. Lifts mise's 60/hr anonymous
# GH API limit on fresh bootstrap. `op read` keeps token in process env only
# (chezmoi's native onepasswordRead would bake it into the rendered .sh under
# $TMPDIR). Template lookPath guards omit cascade steps for machines without
# the corresponding CLI.
if [ -z "${GITHUB_TOKEN:-}" ]; then
{{- if lookPath "op" }}
# --no-newline: trailing \n would corrupt Authorization: Bearer headers.
# $DOTFILES_OP_GITHUB_REF overrides the default item path.
GITHUB_TOKEN="$(op read --no-newline \
"${DOTFILES_OP_GITHUB_REF:-op://Personal/GitHub API Token/credential}" \
2>/dev/null || true)"
{{- end }}
{{- if lookPath "gh" }}
if [ -z "${GITHUB_TOKEN:-}" ]; then
GITHUB_TOKEN="$(gh auth token 2>/dev/null || true)"
fi
{{- end }}
[ -n "$GITHUB_TOKEN" ] && export GITHUB_TOKEN
fi

export INSTALL_PACKAGES_INVOKE=1
. "{{ .chezmoi.sourceDir }}/lib/install-packages.sh"
Loading
Loading