Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
102 changes: 102 additions & 0 deletions .chezmoi.toml.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
{{- /*
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.
*/ -}}

{{- /* ────── env detection (HINT ONLY — not a decision) ────── */ -}}
{{- $isSSH := or (ne (env "SSH_CONNECTION") "") (ne (env "SSH_CLIENT") "") (ne (env "SSH_TTY") "") -}}
{{- $hasGUI := false -}}
{{- if eq .chezmoi.os "darwin" -}}
{{- $hasGUI = ne (env "TERM_PROGRAM") "" -}}
{{- else if eq .chezmoi.os "linux" -}}
{{- $hasGUI = or (ne (env "DISPLAY") "") (ne (env "WAYLAND_DISPLAY") "") -}}
{{- end -}}
{{- $ephemeral := or (ne (env "CODESPACES") "") (ne (env "REMOTE_CONTAINERS") "") -}}

{{- $detected := "core" -}}
{{- if and (not $ephemeral) $hasGUI -}}{{- $detected = "workstation" -}}
{{- else if and (not $ephemeral) (not $isSSH) -}}{{- $detected = "workstation" -}}
{{- else if not $ephemeral -}}{{- $detected = "dev" -}}
{{- end -}}

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

{{- $name := "Your Name" -}}
{{- if hasKey . "name" -}}
{{- $name = .name -}}
{{- else if $interactive -}}
{{- $name = promptString "Full name" $name -}}
{{- end -}}

{{- $email := "you@example.com" -}}
{{- if hasKey . "email" -}}
{{- $email = .email -}}
{{- else if $interactive -}}
{{- $email = promptString "Email" $email -}}
{{- end -}}

{{- /* ────── profile prompt (ALWAYS fires on first init via promptChoiceOnce;
default = "core" so CI / non-TTY get safe fallback) ────── */ -}}
{{- $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 -}}

{{- /* ────── .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 := .chezmoi.os -}}
{{- if eq .chezmoi.os "linux" -}}
{{- if hasKey .chezmoi.osRelease "id" -}}
{{- $osid = printf "linux-%s" .chezmoi.osRelease.id -}}
{{- end -}}
{{- end -}}

[data]
name = {{ $name | quote }}
email = {{ $email | quote }}
profile = {{ $profile | quote }}
osid = {{ $osid | quote }}

[hooks]
[hooks.read-source-state]
[hooks.read-source-state.pre]
command = "{{ .chezmoi.workingTree }}/hooks/ensure-prereqs.sh"
125 changes: 125 additions & 0 deletions .chezmoidata/packages.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# 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).
#
# 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)
#
# 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.

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.
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)
dnf:
- zsh
- vim
- tmux
- git
- curl
- ca-certificates
- ufw
- tcpdump

# === dev tier — adds CLI dev toolchain ===
dev:
# Mac brew formulae (tools not in mise's aqua registry).
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).
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)
- htop
- tree
- wget
- nmap
- inetutils-telnet # telnet (Debian/Ubuntu package name)
- wireguard-tools
- postgresql-client # psql
# Linux dnf (Fedora-family).
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.
mac_casks:
- iterm2 # terminal
- docker-desktop # Docker engine + UI (Mac equivalent of Linux docker.io)
- 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, ...
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:
- gopls-lsp@claude-plugins-official
- figma@claude-plugins-official
- frontend-design@claude-plugins-official
- caveman@caveman
claude_marketplaces:
- caveman:JuliusBrussee/caveman
codex:
- github@openai-curated
- google-drive@openai-curated
codex_marketplaces:
[]
55 changes: 55 additions & 0 deletions .chezmoiexternal.toml.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{{- /*
Vendored external dependencies.
- gpakosz/.tmux: cross-platform, SHA-pinned via URL (URL = lockfile).
- MesloLGS NF + Monaspace Neon fonts: cross-platform, downloaded into the
OS-native font dir on machines with a graphical session. Skipped on
headless boxes (SSH session with no DISPLAY/WAYLAND_DISPLAY) — saves
~2 MB of needless download and avoids fc-cache on systems where
fontconfig may not even be installed.
*/ -}}
{{- $monaspaceVer := "v1.400" -}}
{{- $headless := false -}}
{{- if and (ne (env "SSH_CONNECTION") "") (eq (env "DISPLAY") "") (eq (env "WAYLAND_DISPLAY") "") -}}
{{- $headless = true -}}
{{- end -}}

[".config/tmux"]
type = "archive"
# Pinned to commit af33f07 (same as previous git-submodule HEAD)
url = "https://github.com/gpakosz/.tmux/archive/af33f07134b76134acca9d01eacbdecca9c9cda6.tar.gz"
exact = true
stripComponents = 1
refreshPeriod = "720h" # 30d safety net; SHA pinning is the real control

{{ if not $headless -}}
{{- $fontDir := "" -}}
{{- if eq .chezmoi.os "darwin" -}}{{ $fontDir = "Library/Fonts" -}}{{- end -}}
{{- if eq .chezmoi.os "linux" -}}{{ $fontDir = ".local/share/fonts" -}}{{- end -}}
{{- if $fontDir }}

# MesloLGS NF — the font Powerlevel10k expects. Glyphs include Powerline
# arrows used by tmux Powerline separators (U+E0B0–E0B3) and all p10k icons.
# Refresh weekly; the upstream repo updates infrequently.
{{- range $font := list "MesloLGS NF Regular.ttf" "MesloLGS NF Bold.ttf" "MesloLGS NF Italic.ttf" "MesloLGS NF Bold Italic.ttf" }}

["{{ $fontDir }}/{{ $font }}"]
type = "file"
url = "https://github.com/romkatv/powerlevel10k-media/raw/master/{{ $font | replace " " "%20" }}"
refreshPeriod = "168h"
{{- end }}

# Monaspace Neon — GitHub's monospace family (sans-serif neo-grotesque),
# optional alternative to MesloLGS for editors / iTerm2. Distributed only
# in release zips (no raw OTFs in main); extract the four canonical weights
# via archive-file. chezmoi caches the zip by URL — all 4 declarations
# trigger only one download.
{{- range $font := list "MonaspaceNeon-Regular.otf" "MonaspaceNeon-Bold.otf" "MonaspaceNeon-Italic.otf" "MonaspaceNeon-BoldItalic.otf" }}

["{{ $fontDir }}/{{ $font }}"]
type = "archive-file"
url = "https://github.com/githubnext/monaspace/releases/download/{{ $monaspaceVer }}/monaspace-static-{{ $monaspaceVer }}.zip"
path = "Static Fonts/Monaspace Neon/{{ $font }}"
refreshPeriod = "720h"
{{- end }}
{{- end }}
{{- end }}
45 changes: 45 additions & 0 deletions .chezmoiignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Patterns are TARGET paths (relative to destDir, default $HOME).
# Use `.foo` style — what the file is in $HOME — not source-state names.

# Repo-level docs and project-level AI guides — not applied to $HOME
README.md
LICENSE
AGENTS.md
CLAUDE.md

# iTerm2 plist lives at source root `iterm/` (outside dot_*) so chezmoi doesn't
# materialise it into $HOME. iTerm2 reads + writes the plist there directly,
# driven by `defaults write com.googlecode.iterm2 PrefsCustomFolder` (set by
# .chezmoiscripts/run_once_after_configure-iterm2.sh.tmpl). No symlink layer.
iterm

# Custom hooks dir at source root (we don't use `.chezmoihooks/` because
# chezmoi doctor flags any `.chezmoi*` dir not in its hardcoded whitelist
# as 'suspicious-entries'). Scripts here are invoked via .chezmoi.toml.tmpl
# [hooks.read-source-state.pre.command] = "{{ .chezmoi.workingTree }}/hooks/..."
# Not for $HOME materialization — ignore the target path.
hooks

# Bats post-apply smoke tests (CI runs them after `chezmoi apply` in a
# devcontainer). Never materialised into $HOME — only used from CI or
# locally via `bats tests/files/*.bats`.
tests

# GitHub Actions workflows. Not part of $HOME state — only relevant to
# remote CI runners.
.github

# Shell libraries sourced by .chezmoiscripts/ wrappers via {{ .chezmoi.sourceDir }}.
# Never materialised into $HOME — lib functions only callable from chezmoi
# scripts or bats unit tests, both of which know the source-dir path.
lib

# Per-machine files written by external tools (rtk init writes RTK.md fresh
# each install — bundled rtk version's content, not ours to track)
.claude/RTK.md
.codex/RTK.md

# rtk init writes RTK.md into cwd when run from project dir. We don't want it
# tracked at source root — the per-machine RTK.md lives at ~/.claude/RTK.md
# and ~/.codex/RTK.md (both in .chezmoiignore above).
RTK.md
11 changes: 11 additions & 0 deletions .chezmoiremove.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{{- /*
Lists target-state paths (relative to $HOME) that chezmoi should remove on
next `chezmoi apply`. Use when deprecating a managed file — list it here
so chezmoi cleans it up on every machine, then remove from this file once
all machines have been applied.

Empty by default. Add entries one per line, e.g.:

.config/old-tool/config
.some-removed-dotfile
*/ -}}
37 changes: 37 additions & 0 deletions .chezmoiscripts/darwin/run_once_after_configure-iterm2.sh.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{{ if eq .chezmoi.os "darwin" -}}
#!/bin/sh
# Point iTerm2 at the chezmoi source dir directly. iTerm2 reads + writes
# preferences in this folder, so GUI edits flow straight to `git status`
# in the source repo (use `chezmoi cd` to navigate there).
#
# Why directly at sourceDir/iterm/ instead of ~/.config/iterm/? Because
# iTerm2 ALSO creates its own ~/.config/iterm2/ (for AppSupport + sockets) —
# having both `iterm/` and `iterm2/` in ~/.config/ was confusing.
#
# Runs once per machine (after first apply).
set -eu

iterm_dir="{{ .chezmoi.sourceDir }}/iterm"

# Sanity check: chezmoi sourceDir should contain the iterm/ plist directory.
# Fail loudly if it's missing — typically only happens during a manual
# misconfig of sourceDir.
if [ ! -d "$iterm_dir" ]; then
echo "ERROR: $iterm_dir does not exist" >&2
echo "Verify chezmoi sourceDir resolves to this repo (try 'chezmoi cd')." >&2
exit 1
fi

current=$(defaults read com.googlecode.iterm2 PrefsCustomFolder 2>/dev/null || true)
load=$(defaults read com.googlecode.iterm2 LoadPrefsFromCustomFolder 2>/dev/null || true)

if [ "$current" = "$iterm_dir" ] && [ "$load" = "1" ]; then
echo "iTerm2 already loading prefs from $iterm_dir"
exit 0
fi

defaults write com.googlecode.iterm2 PrefsCustomFolder -string "$iterm_dir"
defaults write com.googlecode.iterm2 LoadPrefsFromCustomFolder -bool true
echo "iTerm2 configured to read prefs from $iterm_dir."
echo "Quit and relaunch iTerm2 (Don't Save if prompted) for changes to apply."
{{- end }}
Loading
Loading