diff --git a/.chezmoi.toml.tmpl b/.chezmoi.toml.tmpl index 24df80a..2f661a8 100644 --- a/.chezmoi.toml.tmpl +++ b/.chezmoi.toml.tmpl @@ -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" -}} @@ -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 -}} @@ -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-". Single derived - value used by .chezmoiscripts/* — replaces nested if-elif on - .chezmoi.osRelease.id. Pattern from twpayne/dotfiles. ────── */ -}} +{{- /* .osid composite key (darwin / linux-) — used by .chezmoiscripts/* */ -}} {{- $osid := .chezmoi.os -}} {{- if eq .chezmoi.os "linux" -}} {{- if hasKey .chezmoi.osRelease "id" -}} diff --git a/.chezmoidata/packages.yaml b/.chezmoidata/packages.yaml index 7902665..e07bb77 100644 --- a/.chezmoidata/packages.yaml +++ b/.chezmoidata/packages.yaml @@ -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 @@ -49,36 +31,33 @@ 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 @@ -86,30 +65,22 @@ packages: - 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 diff --git a/.chezmoiscripts/run_once_after_99-post-install-hint.sh.tmpl b/.chezmoiscripts/run_once_after_99-post-install-hint.sh.tmpl new file mode 100644 index 0000000..778fbee --- /dev/null +++ b/.chezmoiscripts/run_once_after_99-post-install-hint.sh.tmpl @@ -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= + 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 diff --git a/.chezmoiscripts/run_onchange_after_50-install-packages.sh.tmpl b/.chezmoiscripts/run_onchange_after_50-install-packages.sh.tmpl index 1fe43b3..97dd66c 100644 --- a/.chezmoiscripts/run_onchange_after_50-install-packages.sh.tmpl +++ b/.chezmoiscripts/run_onchange_after_50-install-packages.sh.tmpl @@ -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 }} @@ -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" diff --git a/.chezmoiscripts/run_onchange_after_60-install-rtk.sh.tmpl b/.chezmoiscripts/run_onchange_after_60-install-rtk.sh.tmpl deleted file mode 100644 index 7c627df..0000000 --- a/.chezmoiscripts/run_onchange_after_60-install-rtk.sh.tmpl +++ /dev/null @@ -1,91 +0,0 @@ -{{- /* - rtk install + init — only at profile=dev or workstation (where claude/codex - are installed). At profile=core the script exits early since rtk has no - consumer (Claude Code + Codex CLIs ship via dev tier's npm globals). -*/ -}} -{{- $profile := .profile | default "core" -}} -{{- $isDev := or (eq $profile "dev") (eq $profile "workstation") -}} -#!/bin/sh -# Install rtk (token-saving CLI proxy) and wire it into Claude Code + Codex. -# rtk init -g writes ~/.claude/RTK.md, adds @RTK.md reference to ~/.claude/CLAUDE.md, -# adds hook entry to ~/.claude/settings.json. Our modify_settings.json preserves -# that hook entry across applies. -# -# Idempotent: rtk init detects already-wired state. -set -eu - -{{- if not $isDev }} -echo "[install-rtk] profile=core — skipping (no claude/codex consumer at this tier)." -exit 0 -{{- end }} - -{{ if eq .chezmoi.os "darwin" -}} -# On fresh Mac (first-time `chezmoi init --apply` via the curl one-liner), -# the parent shell may not yet have /opt/homebrew/bin on PATH — chezmoi -# inherits that bare PATH and scripts can't find brew-installed binaries -# (rtk, claude, codex) even though script 50 just installed them. Re-eval -# brew shellenv per Mac script to set PATH deterministically. -if [ -x /opt/homebrew/bin/brew ]; then - eval "$(/opt/homebrew/bin/brew shellenv)" -elif [ -x /usr/local/bin/brew ]; then - eval "$(/usr/local/bin/brew shellenv)" -fi -{{- end }} - -# Ensure binary is on PATH. -# On macOS rtk lives in packages.yaml under packages.dev.brews and the -# install-packages script (50-install-packages) runs first because 50 < 60 -# alphabetically. Binary should be present here at dev/workstation profile. -# On Linux the install-packages script doesn't bundle rtk (not in apt/dnf), -# so the Linux fallback below is the install path. -if ! command -v rtk >/dev/null 2>&1; then -{{ if ne .chezmoi.os "darwin" -}} - curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/refs/heads/master/install.sh | sh - export PATH="$HOME/.local/bin:$PATH" -{{- else -}} - echo "rtk not found on PATH after install-packages." >&2 - echo "Common cause: brew bundle aborted (e.g. VSCode or Docker cask" >&2 - echo "adoption needs sudo, blocking non-interactive apply)." >&2 - echo "Fix: rerun 'chezmoi apply' from a TTY, or 'sudo -v' beforehand" >&2 - echo "to cache credentials, then retry." >&2 - exit 1 -{{- end }} -fi - -# rtk's Linux arm64 build is dynamically linked against glibc >=2.39 -# (Ubuntu 24.04+); on Ubuntu 22.04 jammy (glibc 2.35) it fails with -# "version `GLIBC_2.39' not found". No musl/static alternative is shipped -# upstream as of 2026-05. Skip rtk init gracefully on too-old glibc rather -# than fail the whole apply — claude/codex still work without the rtk hook. -if ! rtk --version >/dev/null 2>&1; then - echo "rtk binary fails to execute on this system (likely glibc too old)." >&2 - echo "Upstream ships glibc-2.39 build only for linux-arm64; needs Ubuntu 24.04+." >&2 - echo "Skipping rtk init. claude/codex still usable without rtk hook." >&2 - exit 0 -fi - -# Init for Claude Code (only if claude binary is present). -# --auto-patch is Claude-specific: writes the PreToolUse hook entry into -# ~/.claude/settings.json without prompting. Codex doesn't have an equivalent -# settings hook (rtk wires Codex via @RTK.md in AGENTS.md instead), so the -# flag is intentionally absent below. -if command -v claude >/dev/null 2>&1; then - rtk init -g --auto-patch -fi - -# Init for Codex (only if codex binary is present). -# `-g --codex` writes to ~/.codex/{AGENTS.md, RTK.md} — GLOBAL Codex config. -# Without `-g`, rtk init --codex would write to cwd (which is $HOME when run -# from chezmoi apply), polluting $HOME with stray AGENTS.md and RTK.md. -# Verified in rtk source: src/hooks/init.rs::run_codex_mode() — when global=true, -# paths resolve to resolve_codex_dir() which uses $CODEX_HOME or ~/.codex/. -if command -v codex >/dev/null 2>&1; then - rtk init -g --codex -fi - -echo "rtk: $(rtk --version) — initialized" - -# Script content hash already triggers re-run on changes. No need to embed -# `output "rtk" "--version"` here — that would call rtk binary at chezmoi -# template-render time, which aborts apply on systems where rtk can't -# execute (e.g. jammy arm64 — rtk requires glibc 2.39 not in Ubuntu 22.04). diff --git a/.chezmoiscripts/run_onchange_after_70-install-plugins.sh.tmpl b/.chezmoiscripts/run_onchange_after_70-install-plugins.sh.tmpl index 39da8e6..8e0f408 100644 --- a/.chezmoiscripts/run_onchange_after_70-install-plugins.sh.tmpl +++ b/.chezmoiscripts/run_onchange_after_70-install-plugins.sh.tmpl @@ -1,41 +1,16 @@ {{- /* - Install + update Claude Code and OpenAI Codex plugins from - .chezmoidata/packages.yaml (.plugins.{claude, claude_marketplaces, - codex_marketplaces}). Re-runs when the list changes (chezmoi hashes - rendered script content). - - Claude: - - `claude plugin marketplace add` for each non-default marketplace. - Defensively adds `anthropics/claude-plugins-official` first — that - marketplace is normally auto-registered on first interactive - `claude` launch, but a fresh box where claude was npm-installed - but never run won't have it. - - `claude plugin install @ --scope user` for each - .plugins.claude entry. `claude plugin update` bumps to latest. - Both idempotent. - - Caveman is just a row in .plugins.claude + .plugins.claude_marketplaces - (canonical install per its INSTALL.md per-agent table). The plugin - manages its own hooks via plugin.json — no settings.json mutation. - - Codex: - - `openai-curated` marketplace is reserved (built into the codex - binary); enable flags via [plugins."x@y"] in config.toml are - sufficient. Those flags come from - .chezmoitemplates/codex-config-base.toml (rendered through - modify_config.toml). - - Non-reserved marketplaces need `codex plugin marketplace add`. - - Skill-based plugins (e.g. caveman for Codex) install via `npx - skills add ... -a codex` — but the skills CLI doesn't currently - wire the per-agent symlinks Codex needs for discovery, so they're - a manual step, not part of this script. + Install Claude Code + Codex plugins from .chezmoidata/packages.yaml. + Claude: `plugin marketplace add` (defensively re-adds claude-plugins-official + in case claude was never launched interactively) + `plugin install`/`update`. + Codex: only non-reserved marketplaces need `plugin marketplace add`; enable + flags live in .chezmoitemplates/codex-config-base.toml. + Caveman-for-Codex is manual (skills CLI doesn't wire per-agent symlinks yet). */ -}} #!/bin/sh set -eu {{ if eq .chezmoi.os "darwin" -}} -# Same as script 60: ensure /opt/homebrew/bin is on PATH so command -v claude -# / codex find the freshly-installed binaries from script 50, even on first -# apply where the parent shell didn't have brew on PATH yet. +# Defensive PATH on first apply (mise activate may not have hit parent shell yet). if [ -x /opt/homebrew/bin/brew ]; then eval "$(/opt/homebrew/bin/brew shellenv)" elif [ -x /usr/local/bin/brew ]; then diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index deabb94..0e13ef1 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -41,23 +41,117 @@ jobs: sudo apt-get update -qq sudo apt-get install -y --no-install-recommends bats jq - - name: Install chezmoi + - name: Install chezmoi + put mise shims on PATH ahead of time run: | sh -c "$(curl -fsLS get.chezmoi.io)" -- -b "$HOME/.local/bin" echo "$HOME/.local/bin" >> "$GITHUB_PATH" + # mise shims aren't installed yet, but adding the path to + # $GITHUB_PATH now ensures every subsequent step sees the dir + # once mise install populates it. Without this, the 2nd + # `chezmoi apply` re-renders dot_gitconfig.tmpl with + # `{{ lookPath "delta" }}` still false (delta exists at + # ~/.local/share/mise/installs/... but its shim at + # ~/.local/share/mise/shims/delta isn't reachable). + echo "$HOME/.local/share/mise/shims" >> "$GITHUB_PATH" - - name: chezmoi init + apply + - name: chezmoi init + apply (twice — second converges modify_ + lookPath drift) run: | # Symlink workspace to chezmoi's default source dir so every # subsequent `chezmoi *` invocation (e.g. `chezmoi diff` in bats) - # finds the source without needing --source flag. `chezmoi init - # --source=PATH` only takes effect for the current invocation; - # it does NOT persist sourceDir to ~/.config/chezmoi/chezmoi.toml. + # finds the source without needing --source flag. mkdir -p "$HOME/.local/share" ln -s "$GITHUB_WORKSPACE" "$HOME/.local/share/chezmoi" chezmoi init --apply --promptDefaults echo "--- chezmoi data ---" chezmoi data + # Second apply: re-rendering settles two convergence cases — + # (a) modify_settings.json regenerates against the settings.json + # the rtk init wrote in pass 1 (alphabetised vs insertion-order) + # (b) dot_gitconfig.tmpl `{{ lookPath "delta" }}` block becomes + # truthy now that mise installed delta in pass 1. + chezmoi apply - name: Verify post-apply state - run: bats tests/files/common.bats + run: | + bats tests/files/common.bats + bats tests/files/core.bats + + apply-dev: + name: apply (profile=dev, Ubuntu) + runs-on: ubuntu-latest + needs: unit + env: + # Lifts mise's 60/hr anonymous GitHub API rate-limit to 1000/hr. + # Needed for the 25-tool aqua-backend fetch (~30-50 API calls). + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + DEBIAN_FRONTEND: noninteractive + + steps: + - uses: actions/checkout@v4 + + - name: Cache mise installs + uses: actions/cache@v4 + with: + # mise extracts every aqua / language toolchain into per-tool dirs + # under ~/.local/share/mise/installs/. Cache keyed by hash of the + # mise config so adding/removing a tool busts the cache cleanly. + path: ~/.local/share/mise/installs + key: mise-${{ runner.os }}-${{ hashFiles('dot_config/mise/config.toml.tmpl') }} + restore-keys: mise-${{ runner.os }}- + + - name: Install bats + jq + run: | + sudo apt-get update -qq + sudo apt-get install -y --no-install-recommends bats jq + + - name: Install chezmoi + put mise shims on PATH ahead of time + run: | + sh -c "$(curl -fsLS get.chezmoi.io)" -- -b "$HOME/.local/bin" + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + # mise shims aren't installed yet, but adding the path to + # $GITHUB_PATH now ensures every subsequent step sees the dir + # once mise install populates it. Without this, the 2nd + # `chezmoi apply` re-renders dot_gitconfig.tmpl with + # `{{ lookPath "delta" }}` still false (delta exists at + # ~/.local/share/mise/installs/... but its shim at + # ~/.local/share/mise/shims/delta isn't reachable). + echo "$HOME/.local/share/mise/shims" >> "$GITHUB_PATH" + + - name: chezmoi init + apply (twice — second converges modify_ + lookPath drift) + run: | + mkdir -p "$HOME/.config/chezmoi" "$HOME/.local/share" + ln -s "$GITHUB_WORKSPACE" "$HOME/.local/share/chezmoi" + # Seed chezmoi.toml with profile=dev BEFORE init. + # promptChoiceOnce takes a default arg ("core"); --promptDefaults + # makes it return that LITERAL default, NOT the computed + # $defaultProfile that the template's auto-detect would resolve to. + # So to test dev tier on CI we need hasKey .profile == true at + # init time → promptChoiceOnce short-circuits to the cached + # value (no prompt, no default fallthrough). + # `chezmoi init` then re-renders chezmoi.toml (writes same content + # back, since hasKey passes for every prompted field) AND caches + # the template hash in configState. Without that hash cache, + # subsequent applies emit "config file template has changed" + # warning, which bats captures as drift. + cat > "$HOME/.config/chezmoi/chezmoi.toml" <` → use the aqua slug → add line to `dot_config/mise/config.toml.tmpl` (inside the `$isDev` block unless every tier needs it) → `chezmoi apply` (or `mise install`). @@ -120,20 +205,38 @@ Defined declaratively in `dot_config/mise/config.toml.tmpl` (profile-aware — r ## OS-native packages (via brew / apt / dnf) -Tools NOT in mise's aqua registry — C apps with libpcap/curses/PAM deps, system services, or platform-specific. Defined in `.chezmoidata/packages.yaml` under `packages.{core,dev,mac}` sections, installed cascade-style by `run_onchange_after_50-install-packages.sh.tmpl`. +Tools NOT in mise's aqua registry — C apps with libpcap/curses/PAM deps, system services, or platform-specific. Defined in `.chezmoidata/packages.yaml` under `packages.{core,dev,gui}` sections, installed cascade-style by `run_onchange_after_50-install-packages.sh.tmpl`. | Tier | OS | Packages | |---|---|---| | core | Linux apt/dnf | `zsh`, `vim`, `tmux`, `git`, `curl`, `ca-certificates`, `ufw`, `tcpdump` | | core | Mac brew | (none — Apple bundles zsh/vim/tmux/git/curl + system `/usr/sbin/tcpdump`; no Mac equivalent for ufw — use `pfctl`) | -| dev | Mac brew | `htop`, `tree`, `wget`, `nmap`, `telnet`, `libpq` (psql, force-linked), `wireguard-tools`, `rtk` | -| dev | Linux apt | `build-essential`, `xsel`, `wl-clipboard`, `docker.io`, `htop`, `tree`, `wget`, `nmap`, `inetutils-telnet`, `wireguard-tools`, `postgresql-client` | +| dev | Mac brew | `htop`, `tree`, `wget`, `nmap`, `telnet`, `libpq` (psql, force-linked), `wireguard-tools` | +| dev | Linux apt | `build-essential`, `xsel`, `wl-clipboard`, `htop`, `tree`, `wget`, `nmap`, `inetutils-telnet`, `wireguard-tools`, `postgresql-client` | | dev | Linux dnf | (same as apt, modulo Fedora naming) | -| mac | Mac casks | `iterm2`, `docker-desktop`, `visual-studio-code`, `ngrok`, `font-meslo-lg-nerd-font`, `font-monaspace` | +| workstation | Mac casks | `iterm2`, `docker-desktop`, `visual-studio-code`, `ngrok`, `1password` (desktop), `font-meslo-lg-nerd-font`, `font-monaspace` | + +**Docker is intentionally NOT in the `dev` tier**: `docker.io` (Ubuntu repo) and `docker-ce` (Docker's official apt repo) can't coexist, and a pre-existing install would break `apt-get install`. Pick the flavour that fits the machine: `sudo apt install docker.io` (simple), Docker's official repo for `docker-ce` (production), or `workstation` profile on Mac (Docker Desktop cask). **Ad-hoc installs**: brew/apt/dnf still primary on each platform. Want `mongosh`? `brew install mongosh` (Mac) or follow MongoDB's apt repo (Linux). Want `kubectl` debug version? `mise use kubectl@1.34.2` (per-project) or edit the global config. -**First-apply latency** at `dev` or `workstation`: cold install downloads ~1.9 GB (4 language toolchains × ~300 MB + 25 aqua tools). Realistic ranges: 5-10 min on fast link, 15-30 min on residential, 30-60+ min on slow links / jetson. `core` profile only pulls fzf+zoxide (~5 MB). +**First-apply latency** at `dev` or `workstation`: cold install downloads ~1.9 GB (4 language toolchains × ~300 MB + ~30 binaries via mise). Realistic ranges: 5-10 min on fast link, 15-30 min on residential, 30-60+ min on slow links / jetson. `core` profile only pulls fzf+zoxide (~5 MB). + +**GitHub API rate limit on fresh bootstrap**: mise's aqua + github backends hit `api.github.com` once per tool (~29 tools × 1-2 calls ≈ 30-50 requests). Anonymous limit is 60/hr — easy to exhaust on a slow link or shared IP. Two ways to lift it: + +```sh +# Option 1 — set token directly before apply (one-off) +export GITHUB_TOKEN=ghp_yourPersonalAccessToken +chezmoi apply + +# Option 2 — `gh auth login` once, install-packages auto-picks it up on +# subsequent applies via `gh auth token` (works after first apply when +# gh is installed by mise). +gh auth login # interactive, browser flow +chezmoi apply # token auto-detected, ~5000/hr limit +``` + +Anonymous bootstrap on a fast link usually fits inside 60/hr — the auto-detect is a safety net for jetson / VPS / re-runs. **Troubleshooting slow zsh prompt**: `MISE_TIMINGS=1 exec zsh` — total < 50ms per prompt is healthy. If consistently above, switch to mise shim mode in `dot_zshrc`: ```zsh @@ -156,7 +259,7 @@ Source files in this repo use chezmoi's naming convention. The mapping is mechan | `symlink_X.tmpl` | symbolic link (body = target) | | `modify_X` | script: stdin = existing file, stdout = new contents | | `modify_X` + `#chezmoi:modify-template` | template: `.chezmoi.stdin` = existing; output = new contents | -| `.chezmoiscripts/run_*` | scripts that don't create `$HOME` files (per-OS subdirs `darwin/` + `linux/`; root scripts use `50/60/70/80-` numerical prefix for ordering) | +| `.chezmoiscripts/run_*` | scripts that don't create `$HOME` files (per-OS subdirs `darwin/` + `linux/`; root scripts use `50/70/99-` numerical prefix for ordering — 50 installs packages, 70 installs plugins, 99 prints post-install hint) | | `.chezmoihooks/*` | hook scripts registered in `.chezmoi.toml.tmpl` (e.g. `read-source-state.pre`) | | `.chezmoiexternal.toml.tmpl` | vendored externals (archives, file downloads) | | `.chezmoiignore` | exclude target paths from apply | @@ -236,7 +339,7 @@ Global, cross-machine config for both CLIs. **Only user-curated settings are tra | Source path | Becomes in `$HOME` | Pattern | Purpose | |---|---|---|---| | `dot_claude/CLAUDE.md` | `~/.claude/CLAUDE.md` | plain | global instructions (bootstrap + TL;DR rule index) | -| `dot_claude/modify_settings.json.tmpl` | `~/.claude/settings.json` | `modify_` (jq merge) | global settings, preserves tool-added keys | +| `dot_claude/modify_settings.json` | `~/.claude/settings.json` | `modify_` (jq merge) | global settings, preserves tool-added keys | | `.chezmoitemplates/claude-settings-base.json` | (not applied) | template partial | curated base, loaded via `includeTemplate` from `modify_` | | `dot_claude/executable_statusline-command.sh` | `~/.claude/statusline-command.sh` | plain +x | custom status-line renderer | | `dot_claude/agents/` | `~/.claude/agents/` | plain dir | custom subagents | @@ -302,11 +405,11 @@ Mechanics: `caveman` is just another entry in `plugins.claude` (caveman@caveman) + `plugins.claude_marketplaces` (caveman:JuliusBrussee/caveman). Per its [INSTALL.md](https://github.com/JuliusBrussee/caveman/blob/main/INSTALL.md) per-agent table, that's the canonical Claude install path. The plugin self-registers its `SessionStart` + `UserPromptSubmit` hooks via `plugin.json` (no settings.json mutation). Caveman for Codex is a manual step (`npx -y skills add JuliusBrussee/caveman -a codex`) — the skills CLI doesn't currently create the per-agent symlinks Codex needs. -#### rtk auto-refresh on version change +#### rtk install + init -`brew bundle` upgrades the rtk binary on each `chezmoi apply` (Homebrew's default). `RTK.md` awareness files (`~/.claude/RTK.md`, `~/.codex/RTK.md`) are bundled inside the binary and re-written by `rtk init`. The install script tracks `rtk --version` output in a trailing hash comment (`# {{ if lookPath "rtk" }}{{ output "rtk" "--version" | trim }}{{ else }}not-installed{{ end }}`), so a version bump changes the script's effective content and chezmoi re-runs it automatically — no manual `rtk init` needed. +`rtk` is installed by mise as `github:rtk-ai/rtk = "latest"` — cross-platform, single source. On every `chezmoi apply`, mise refreshes its registry and pulls a newer rtk if upstream released one. The `post_install_rtk_init` function in `lib/install-packages.sh` runs `rtk init -g --auto-patch` (Claude PreToolUse hook) and `rtk init -g --codex` (Codex AGENTS.md reference) — both idempotent. `RTK.md` awareness files (`~/.claude/RTK.md`, `~/.codex/RTK.md`) are bundled inside the binary and rewritten on each init. -On a fresh machine the script will run twice across the first two applies (install rtk → hash populates → next apply re-runs `rtk init`). Both runs are idempotent. +Ubuntu 22.04 jammy (glibc 2.35) skips the init step gracefully — upstream ships glibc-2.39 builds only for linux-arm64 (Ubuntu 24.04+); claude/codex still work without the rtk hook. ### Open VSCode from any terminal @@ -330,7 +433,7 @@ export VSCODE_REMOTE_HOST=my-ssh-alias Top-level dotfiles (`dot_zshrc`, `dot_vimrc`, etc.) are plain — they handle platform differences via runtime feature-detection (`command -v X`, `case "$(uname -s)"` in shell). Where chezmoi templates are used: - `.chezmoiexternal.toml.tmpl` is templated for headless detection (skip fonts) + per-OS font destination -- `.chezmoiscripts/{darwin,linux}/` hold per-OS scripts; root holds cross-platform scripts with numerical prefix (`50-`, `60-`, `70-`, `80-`) for ordering within the `run_onchange_after_` group +- `.chezmoiscripts/{darwin,linux}/` hold per-OS scripts; root holds cross-platform scripts with numerical prefix (`50-`, `70-`, `99-`) for ordering within the `run_onchange_after_` / `run_once_after_` groups - `.chezmoiscripts/run_onchange_after_50-install-packages.sh.tmpl` branches on `.chezmoi.os` (Mac brew bundle vs Linux apt/dnf) + `.chezmoi.osRelease.id` (Debian vs Fedora) - `.chezmoihooks/ensure-prereqs.sh` is NOT a template (chezmoi doesn't template hooks) — it detects OS inline via `case "$(uname -s)"` @@ -384,8 +487,6 @@ Cache lives in `~/Library/Caches/chezmoi/` (Mac) — outside the source repo, so ### Soft hardening (review-flagged, not blocking) -- Pin rtk install URL to a tagged release (currently `refs/heads/master` — - needs upstream tagging first). - Renovate config for `dot_config/mise/config.toml` — auto-PR bumps for pinned tool versions (replaces "track latest" with deterministic upgrades). diff --git a/dot_claude/skills/save-to-dotfiles/SKILL.md b/dot_claude/skills/save-to-dotfiles/SKILL.md index a0e61e8..85305f8 100644 --- a/dot_claude/skills/save-to-dotfiles/SKILL.md +++ b/dot_claude/skills/save-to-dotfiles/SKILL.md @@ -93,7 +93,7 @@ Map intent → source file (in the chezmoi source dir; `chezmoi cd` to navigate) | p10k major theme change | run `p10k configure` (rewrites `dot_p10k.zsh`) | (done by p10k itself) | | Brew formula or cask (Mac) / apt/dnf package (Linux) | `.chezmoidata/packages.yaml` under `common.brews` / `darwin.casks` / `linux.{apt,dnf}` | `chezmoi apply` runs install-packages script | | Claude global rule | `dot_claude/rules/.md` with frontmatter | next Claude session | -| Claude setting / statusline / plugin enable | `.chezmoitemplates/claude-settings-base.json` (curated base; `dot_claude/modify_settings.json.tmpl` merges tool-added keys) | next Claude session | +| Claude setting / statusline / plugin enable | `.chezmoitemplates/claude-settings-base.json` (curated base; `dot_claude/modify_settings.json` merges tool-added keys) | next Claude session | | Claude custom agent | `dot_claude/agents//...` | next Claude session | | Claude custom skill | `dot_claude/skills//SKILL.md` | next Claude session | | Codex behavioural rule | `dot_codex/AGENTS.md` (inline section) | next Codex session | diff --git a/dot_config/mise/config.toml.tmpl b/dot_config/mise/config.toml.tmpl index b1818d8..577bad5 100644 --- a/dot_config/mise/config.toml.tmpl +++ b/dot_config/mise/config.toml.tmpl @@ -1,44 +1,36 @@ {{- /* - Global mise config — managed by chezmoi (rendered from .tmpl). - Profile-aware: at profile=core, only the dotfile-essential tools are - registered (fzf+zoxide power zshrc bindings). At profile=dev|workstation, - the full dev toolchain registers (languages + 25 aqua tools). - - Per-project pins still live in /.mise.toml or .tool-versions. - Per-machine opt-out: untracked ~/.config/mise/config.local.toml — mise - auto-merges later files over this one. - - Adding a tool: `mise registry | grep -i ` → use the aqua slug shown. + Profile-aware mise config. core: fzf+zoxide only. dev/workstation: full toolchain. + Per-machine opt-out: ~/.config/mise/config.local.toml (auto-merged, untracked). + Adding a tool: `mise registry | grep -i ` → use the slug shown. */ -}} {{- $profile := .profile | default "core" -}} {{- $isDev := or (eq $profile "dev") (eq $profile "workstation") -}} [settings] -# Enable idiomatic version files (.nvmrc, .python-version, .go-version). -# Disabled by default since mise 2025.10.0; we want them on for back-compat -# with existing projects that pre-date our move to .mise.toml. +# Idiomatic version files (.nvmrc, .python-version, .go-version) — disabled +# upstream since 2025.10.0; re-enabled here for projects predating .mise.toml. idiomatic_version_file_enable_tools = ["node", "python", "go"] [tools] -# --- Core tier — minimum to make dotfile integrations work -------------- +# --- Core: required for zshrc bindings ---------------------------------- "aqua:junegunn/fzf" = "latest" # zshrc Ctrl-R, Ctrl-T "aqua:ajeetdsouza/zoxide" = "latest" # zshrc z, zi {{- if $isDev }} -# --- Languages (core backends) ------------------------------------------ +# --- Languages ---------------------------------------------------------- node = "lts" go = "latest" python = "3.12" rust = "stable" # --- CLI utilities ------------------------------------------------------ -"aqua:jqlang/jq" = "latest" # JSON CLI -"aqua:cli/cli" = "latest" # gh (GitHub CLI) -"aqua:dandavison/delta" = "latest" # git pager -"aqua:koalaman/shellcheck" = "latest" # POSIX linter -"aqua:sharkdp/fd" = "latest" # better find -"aqua:gitleaks/gitleaks" = "latest" # pre-push secret scan -"aqua:mikefarah/yq" = "latest" # YAML CLI (Go yq, NOT Python) +"aqua:jqlang/jq" = "latest" +"aqua:cli/cli" = "latest" # gh +"aqua:dandavison/delta" = "latest" +"aqua:koalaman/shellcheck" = "latest" +"aqua:sharkdp/fd" = "latest" +"aqua:gitleaks/gitleaks" = "latest" +"aqua:mikefarah/yq" = "latest" # Go yq, NOT Python # --- Cloud + Kubernetes ------------------------------------------------- "aqua:helm/helm" = "latest" @@ -55,7 +47,7 @@ rust = "stable" # --- Networking --------------------------------------------------------- "aqua:vi/websocat" = "latest" -# --- Go ecosystem (binaries — Go runtime itself is above) --------------- +# --- Go ecosystem ------------------------------------------------------- "aqua:bufbuild/buf" = "latest" "aqua:golangci/golangci-lint" = "latest" "aqua:goreleaser/goreleaser" = "latest" @@ -66,11 +58,23 @@ rust = "stable" "aqua:pnpm/pnpm" = "latest" "aqua:astral-sh/uv" = "latest" -# Not in mise: -# - goimports → `go install golang.org/x/tools/cmd/goimports@latest` after Go. -# - ssh-audit → `uv tool install ssh-audit` after uv. -# - rtk → mise registry has wrong project; stays on its own curl-pipe. -# - mongosh → heavy (~80MB) JS bundle; install ad-hoc when needed. -# - htop/tree/nmap/wireguard-tools/telnet/postgresql-client → OS-native -# (apt/dnf on Linux, brew on Mac) for C-library + system-integration reasons. +# --- AI CLIs (native binaries, no Node coupling) ------------------------ +# Fallback if aqua-registry breaks: swap to npm:@anthropic-ai/claude-code +# / npm:@openai/codex (mise's npm backend resolves identically). +"aqua:anthropics/claude-code" = "latest" +"aqua:openai/codex" = "latest" + +# --- Auth + secrets ----------------------------------------------------- +# Unqualified `op` shortname → mise direct-URL backend (1Password CDN). +# Do NOT use `aqua:1password/cli` — that aqua entry is broken (missing +# repo_owner/repo_name). Cross-platform: Mac + Linux arm64/amd64. +op = "latest" + +# rtk: Linux arm64 needs glibc ≥2.39 (Ubuntu 24.04+); init skips gracefully on older. +"github:rtk-ai/rtk" = "latest" + +# Not in mise (installed elsewhere): +# goimports — `go install` (lib post-install) +# ssh-audit — `uv tool install` (lib post-install) +# htop/tree/nmap/wireguard-tools/telnet/postgresql-client — OS-native (C deps) {{- end }} diff --git a/hooks/ensure-prereqs.sh b/hooks/ensure-prereqs.sh index 0dd2d82..c27dac6 100755 --- a/hooks/ensure-prereqs.sh +++ b/hooks/ensure-prereqs.sh @@ -1,21 +1,13 @@ #!/bin/sh -# Hook: ensure minimum prereqs are present BEFORE chezmoi reads source state. -# Registered in .chezmoi.toml.tmpl as `hooks.read-source-state.pre.command`. +# Pre-source-state hook (registered in .chezmoi.toml.tmpl). Installs minimum +# prereqs so the rest of chezmoi apply can proceed. Plain shell, not a template +# (chezmoi doesn't render hooks). Idempotent. # -# Per chezmoi docs: hooks are PLAIN shell scripts (not templates). Detect OS -# inline. Idempotent: skips when everything is present, skips with warning if -# missing tools would require interactive sudo on a non-TTY apply. -# -# Installs (per OS): -# - macOS: Xcode Command Line Tools + Homebrew (Apple bundles git/zsh/vim/ -# tmux/curl, so no base-tool install needed) + mise (via brew). -# - Linux: base tools (git/zsh/vim/tmux/curl/ca-certificates) via apt/dnf, -# plus mise via `curl https://mise.run | sh` into ~/.local/bin. -# - No Alpine/Arch support (documented limitation). +# Mac: brew (+ Xcode CLT as side effect) + mise via brew. +# Linux (Debian/Ubuntu/Fedora): git/zsh/vim/tmux/curl/ca-certificates via +# apt/dnf + mise via curl-pipe. No Alpine/Arch support. set -eu -# Force apt non-interactive — even from a TTY apply, downstream scripts may -# run from a non-TTY chezmoi child, and consistency matters. export DEBIAN_FRONTEND=noninteractive export TZ=Etc/UTC @@ -24,41 +16,26 @@ tty -s && is_tty=1 case "$(uname -s)" in Darwin) - # Homebrew install (if missing) — needs Xcode CLT, which `brew install` - # script handles. Interactive on first run. if ! command -v brew >/dev/null 2>&1; then if [ "$is_tty" -eq 0 ]; then - # Fail loudly: skipping brew install silently leads to all - # downstream scripts breaking with "brew: command not found". - # Halt the apply so the user runs the visible install command. echo "ERROR: Homebrew not installed; chezmoi apply needs an interactive run first." >&2 echo " Run: /bin/bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"" >&2 echo " Then re-run: chezmoi apply" >&2 exit 1 fi - echo "Installing Homebrew (Xcode CLT will install as a side effect)..." + echo "Installing Homebrew (Xcode CLT installs as side effect)..." /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" fi - - # mise — install via brew (primary dev-tool installer). Idempotent. - # Must be on PATH before chezmoi reads source state, since the - # install-packages script runs `mise install` for ~28 tools. if ! command -v mise >/dev/null 2>&1; then brew install mise 2>&1 || echo "brew install mise failed" >&2 fi ;; Linux) - # --- 1. Base tools ------------------------------------------------- - # git/zsh/vim/tmux/curl: editor + shell + version control baseline. - # Each is a CLI binary — `command -v` is the right check. missing="" for tool in git zsh vim tmux curl; do command -v "$tool" >/dev/null 2>&1 || missing="$missing $tool" done - # ca-certificates is a system PACKAGE, not a binary — `command -v` - # always returns false for it. Check via the canonical cert-bundle - # file location instead. Linux distros put the bundle in different - # places; check both Debian-family and RHEL-family paths. + # ca-certificates: system pkg, not a CLI — check cert bundle file. if [ ! -f /etc/ssl/certs/ca-certificates.crt ] \ && [ ! -f /etc/pki/tls/certs/ca-bundle.crt ]; then missing="$missing ca-certificates" @@ -72,7 +49,6 @@ case "$(uname -s)" in sudo_cmd="" fi - # Decide whether sudo path is usable (passwordless or TTY). sudo_usable=1 if [ -n "$sudo_cmd" ] && [ "$is_tty" -eq 0 ]; then if ! sudo -n true 2>/dev/null; then @@ -85,14 +61,11 @@ case "$(uname -s)" in if [ -n "$missing" ]; then if [ -z "$sudo_cmd" ] && [ "$(id -u)" -ne 0 ]; then echo "WARNING: missing tools:$missing — neither root nor sudo available." >&2 - echo " Install manually as root." >&2 elif [ "$sudo_usable" -eq 0 ]; then echo "WARNING: missing tools:$missing — non-interactive apply, sudo prompt would block." >&2 echo " Install manually:$sudo_cmd apt-get install -y$missing (or dnf install -y$missing)" >&2 elif command -v apt-get >/dev/null 2>&1; then - # Redirect stdout → /dev/null so the hook stays silent during - # chezmoi diff / verify (their stdout becomes chezmoi's). apt - # errors still surface on stderr and via $?. + # stdout → /dev/null so hook stays silent during chezmoi diff/verify. $sudo_cmd apt-get update -qq >/dev/null # shellcheck disable=SC2086 $sudo_cmd apt-get install -y --no-install-recommends $missing >/dev/null @@ -105,13 +78,6 @@ case "$(uname -s)" in fi fi - # --- 2. mise — install via curl-pipe to ~/.local/bin ---------------- - # mise must be on PATH before chezmoi reads source state (the install- - # packages script calls `mise install` for ~28 tools). - # - # Why curl-pipe and not signed apt/dnf repo: simplicity. mise has - # `mise self-update` for ongoing upgrades, so we don't need apt's - # tracking. Trade-off: one curl-pipe = ~5 LOC vs ~35 LOC repo logic. if ! command -v mise >/dev/null 2>&1; then curl -fsSL https://mise.run | sh export PATH="$HOME/.local/bin:$PATH" diff --git a/lib/install-packages.sh b/lib/install-packages.sh index 52af825..3088193 100644 --- a/lib/install-packages.sh +++ b/lib/install-packages.sh @@ -1,31 +1,16 @@ #!/bin/sh -# Library of package-install functions. Pure POSIX shell, no chezmoi -# templating. Sourced by `.chezmoiscripts/run_onchange_after_50-install- -# packages.sh.tmpl` (which renders chezmoi facts into DOTFILES_* env vars -# before sourcing). Sourceable standalone by bats unit tests with mocked -# external commands. +# Package-install library. Sourced by run_onchange_after_50-install-packages.sh.tmpl +# (renders chezmoi facts into DOTFILES_* env vars first). Bats-sourceable +# standalone — the guard at the bottom only runs main when INSTALL_PACKAGES_INVOKE=1. # -# Entry point: `main`. The wrapper sets INSTALL_PACKAGES_INVOKE=1 and -# sources this file; the guard at the bottom calls main only when invoked. -# Bats `setup()` sources without setting the flag → no auto-run. -# -# Required env (set by wrapper): +# Env required: # DOTFILES_PROFILE core | dev | workstation # DOTFILES_OS darwin | linux -# DOTFILES_OSID darwin | linux- (e.g. linux-ubuntu) -# DOTFILES_OSRELEASE_IDLIKE comma-list, e.g. "debian" (Linux only) -# DOTFILES_CORE_BREWS space-separated formula list -# DOTFILES_DEV_BREWS space-separated formula list -# DOTFILES_CORE_APT space-separated package list -# DOTFILES_DEV_APT space-separated package list -# DOTFILES_CORE_DNF space-separated package list -# DOTFILES_DEV_DNF space-separated package list -# DOTFILES_GUI_MAC_CASKS space-separated cask list (Mac workstation only) -# DOTFILES_GUI_LINUX_APT space-separated package list (Linux workstation only) -# DOTFILES_GUI_LINUX_DNF space-separated package list (Linux workstation only) -# DOTFILES_DEV_NPM_GLOBAL space-separated npm package list - -# Cascade: dev⊂workstation. workstation gets dev tools too. +# DOTFILES_OSID darwin | linux- +# DOTFILES_OSRELEASE_IDLIKE comma-list (Linux only) +# DOTFILES_{CORE,DEV}_{BREWS,APT,DNF} +# DOTFILES_GUI_{MAC_CASKS,LINUX_APT,LINUX_DNF} + is_dev() { [ "$DOTFILES_PROFILE" = "dev" ] || [ "$DOTFILES_PROFILE" = "workstation" ] } @@ -63,8 +48,6 @@ _ensure_brew_path() { brew_bundle_install() { _ensure_brew_path || return 0 - # Compose Brewfile from env-supplied package lists. - # workstation on Mac → adds casks from packages.gui.mac_casks. { for f in $DOTFILES_CORE_BREWS; do echo "brew \"$f\""; done if is_dev; then @@ -73,15 +56,10 @@ brew_bundle_install() { if is_workstation; then for c in $DOTFILES_GUI_MAC_CASKS; do echo "cask \"$c\""; done fi - } | brew bundle --file=/dev/stdin || { - echo "brew bundle had failures — some packages may need manual install." >&2 - echo "Most common: a cask trying to adopt a pre-existing /Applications/" >&2 - echo " needs sudo (non-interactive apply can't supply password). Fix: remove" >&2 - echo " the app first, run 'chezmoi apply' from a TTY, or 'sudo -v' beforehand." >&2 - } + } | brew bundle --file=/dev/stdin || \ + echo "brew bundle had failures — most often a cask needing sudo (non-interactive apply). Re-run from a TTY." >&2 - # libpq is keg-only (conflicts with `postgresql`). Force-link to expose - # psql, pg_dump, pg_restore, etc. + # libpq is keg-only — force-link to expose psql et al. if brew list libpq >/dev/null 2>&1; then brew link --force libpq >/dev/null 2>&1 || true fi @@ -153,7 +131,17 @@ mise_install_tools() { return 0 fi mise trust "$HOME/.config/mise/config.toml" >/dev/null 2>&1 || true - mise install --yes || echo "mise install had failures — re-run interactively." >&2 + mise install --yes || echo "mise install had failures — see warnings above." >&2 + + missing_count=$(mise ls 2>/dev/null | grep -c '(missing)' || true) + if [ "${missing_count:-0}" -gt 0 ]; then + echo "" >&2 + echo "[install-packages] WARNING: $missing_count mise tool(s) missing." >&2 + echo " Likely cause: GitHub API rate-limit (60/hr anonymous)." >&2 + echo " Fix: gh auth login or op item create 'op://Personal/GitHub API Token/credential'" >&2 + echo " Then: chezmoi apply" >&2 + echo "" >&2 + fi } post_install_goimports() { @@ -169,42 +157,24 @@ post_install_ssh_audit() { fi } -_resolve_npm() { - # Prefer mise's npm — avoids stale fnm/nvm shims. - if command -v mise >/dev/null 2>&1; then - mise_node_root="$(mise where node 2>/dev/null || true)" - if [ -n "$mise_node_root" ] && [ -x "$mise_node_root/bin/npm" ]; then - echo "$mise_node_root/bin/npm" - return 0 - fi +post_install_rtk_init() { + if ! command -v rtk >/dev/null 2>&1; then + echo "rtk not on PATH — check 'mise ls'; if shims dir missing, 'exec zsh' then re-apply." >&2 + return 0 fi - command -v npm 2>/dev/null && return 0 - return 1 -} - -npm_install_ai_globals() { - npm_bin=$(_resolve_npm) || return 0 - - # Defang any stale prefix from a decommissioned version manager. - "$npm_bin" config delete prefix --global 2>/dev/null || true - "$npm_bin" config delete prefix 2>/dev/null || true - - if [ "$DOTFILES_OS" = "linux" ]; then - "$npm_bin" config set prefix "$HOME/.local" + # rtk's linux-arm64 build needs glibc >=2.39 (Ubuntu 24.04+); no musl variant shipped. + if ! rtk --version >/dev/null 2>&1; then + echo "rtk binary fails to execute (likely glibc too old). Skipping init." >&2 + return 0 fi - for pkg in $DOTFILES_DEV_NPM_GLOBAL; do - "$npm_bin" install -g "$pkg" || echo "npm -g $pkg failed" >&2 - done -} - -linux_fd_symlink_fallback() { - # Distro fd-find → fd symlink. Relevant when mise's fd hasn't installed - # yet (first apply mid-download) or at core profile (no fd in mise.toml). - [ "$DOTFILES_OS" = "linux" ] || return 0 - if command -v fdfind >/dev/null 2>&1 && ! command -v fd >/dev/null 2>&1; then - mkdir -p "$HOME/.local/bin" - ln -sf "$(command -v fdfind)" "$HOME/.local/bin/fd" + if command -v claude >/dev/null 2>&1; then + rtk init -g --auto-patch fi + # -g pins write path to ~/.codex/ (not cwd, which is $HOME under chezmoi apply). + if command -v codex >/dev/null 2>&1; then + rtk init -g --codex + fi + echo "rtk: $(rtk --version) — initialized" } main() { @@ -220,14 +190,10 @@ main() { if is_dev; then post_install_goimports post_install_ssh_audit - npm_install_ai_globals + post_install_rtk_init fi - - linux_fd_symlink_fallback } -# Auto-run when sourced by the chezmoi wrapper (which sets the flag). -# Bats tests source without setting it → functions defined, main not called. if [ "${INSTALL_PACKAGES_INVOKE:-0}" = "1" ]; then main "$@" fi diff --git a/tests/files/common.bats b/tests/files/common.bats index 5f6e3f2..5c983dc 100644 --- a/tests/files/common.bats +++ b/tests/files/common.bats @@ -1,23 +1,18 @@ #!/usr/bin/env bats -# Post-apply asserts for `profile=core` on Linux. Runs in CI after -# `chezmoi apply` against a clean devcontainer (Codespaces-style): -# verifies dotfile presence, content sanity, OS-package install, and -# mise-managed core tools (fzf + zoxide). +# Profile-AGNOSTIC post-apply asserts. Runs from BOTH apply-core and apply-dev +# CI jobs (and locally on any profile). Verifies things that should be true on +# every machine after chezmoi apply, regardless of which profile is active. # -# Profile-aware: stricter asserts (dev/mac tools) would go in -# tests/files/dev.bats / tests/files/mac.bats — out of scope for the -# minimal Phase-1 smoke harness. +# Profile-SPECIFIC asserts live in: +# tests/files/core.bats — core-only (no rtk, no dev tools) +# tests/files/dev.bats — dev tier asserts (mise tools, claude CLI, etc.) setup() { export PATH="$HOME/.local/bin:$HOME/.local/share/mise/shims:$PATH" hash -r 2>/dev/null || true } -# --- chezmoi config + profile ---------------------------------------------- - -@test "chezmoi config profile is core" { - grep -qE '^\s*profile\s*=\s*"core"' "$HOME/.config/chezmoi/chezmoi.toml" -} +# --- chezmoi state --------------------------------------------------------- @test "chezmoi diff is empty post-apply (idempotent)" { run chezmoi diff @@ -78,8 +73,6 @@ setup() { } @test "~/.zshrc sources ~/.zshrc_local LAST (after p10k + SSH tints, so local wins)" { - # The source line must come after the SSH tint block that sets - # tmux_conf_theme_*. Grep both anchors; assert line order. src_line=$(grep -n 'source ~/.zshrc_local' "$HOME/.zshrc" | tail -1 | cut -d: -f1) ssh_line=$(grep -n 'tmux_conf_theme_status_bg' "$HOME/.zshrc" | tail -1 | cut -d: -f1) [ -n "$src_line" ] @@ -110,22 +103,17 @@ setup() { [ -f "$HOME/.codex/config.toml" ] } -# --- mise config + tools (profile=core: only fzf + zoxide) ----------------- +# --- mise itself + core tools (fzf + zoxide installed at every tier) ------ @test "~/.config/mise/config.toml exists" { [ -f "$HOME/.config/mise/config.toml" ] } -@test "~/.config/mise/config.toml has core tools" { +@test "~/.config/mise/config.toml has core tools (fzf + zoxide)" { grep -q 'aqua:junegunn/fzf' "$HOME/.config/mise/config.toml" grep -q 'aqua:ajeetdsouza/zoxide' "$HOME/.config/mise/config.toml" } -@test "~/.config/mise/config.toml does NOT have dev tools at core profile" { - ! grep -q 'aqua:kubernetes/kubernetes/kubectl' "$HOME/.config/mise/config.toml" - ! grep -q '^node ' "$HOME/.config/mise/config.toml" -} - @test "mise binary on PATH" { command -v mise } @@ -138,7 +126,7 @@ setup() { command -v zoxide } -# --- OS-native core packages (Linux apt) ----------------------------------- +# --- OS-native core packages (Linux apt — always installed at every tier) - @test "apt: zsh installed" { command -v zsh @@ -173,11 +161,3 @@ setup() { @test "no stale pyenv directory" { [ ! -e "$HOME/.pyenv" ] } - -@test "no rtk binary at core profile (gated to dev|workstation)" { - ! command -v rtk -} - -@test "no claude CLI at core profile (npm globals gated to dev|workstation)" { - ! command -v claude -} diff --git a/tests/files/core.bats b/tests/files/core.bats new file mode 100644 index 0000000..0822ff8 --- /dev/null +++ b/tests/files/core.bats @@ -0,0 +1,33 @@ +#!/usr/bin/env bats +# CORE-tier-only post-apply asserts. Runs after `chezmoi apply` against +# profile=core. Verifies the negative-space asserts: things that should +# NOT be present at the core tier (dev tools, rtk, claude CLI). +# +# Profile-agnostic asserts live in tests/files/common.bats. +# Dev-tier asserts live in tests/files/dev.bats. + +setup() { + export PATH="$HOME/.local/bin:$HOME/.local/share/mise/shims:$PATH" + hash -r 2>/dev/null || true +} + +@test "chezmoi config profile=core" { + grep -qE '^\s*profile\s*=\s*"core"' "$HOME/.config/chezmoi/chezmoi.toml" +} + +@test "~/.config/mise/config.toml does NOT have dev tools at core profile" { + ! grep -q 'aqua:kubernetes/kubernetes/kubectl' "$HOME/.config/mise/config.toml" + ! grep -q '^node ' "$HOME/.config/mise/config.toml" +} + +@test "no rtk binary at core profile (install-rtk gated to dev|workstation)" { + ! command -v rtk +} + +@test "no claude CLI at core profile (npm globals gated to dev|workstation)" { + ! command -v claude +} + +@test "no codex CLI at core profile" { + ! command -v codex +} diff --git a/tests/files/dev.bats b/tests/files/dev.bats new file mode 100644 index 0000000..9f30a71 --- /dev/null +++ b/tests/files/dev.bats @@ -0,0 +1,111 @@ +#!/usr/bin/env bats +# Dev-tier post-apply asserts. Runs only by CI's `apply-dev` job (after +# `chezmoi apply` against profile=dev on Ubuntu). Verifies the full mise +# toolchain landed + post-install steps succeeded + AI CLIs are reachable. +# +# Counterpart of tests/files/common.bats which covers the profile-agnostic +# baseline (dotfiles present, mise itself + fzf/zoxide, core apt packages). +# +# Run locally on a dev-profile machine: bats tests/files/dev.bats + +setup() { + export PATH="$HOME/.local/bin:$HOME/.local/share/mise/shims:$PATH" + hash -r 2>/dev/null || true +} + +# --- profile sanity -------------------------------------------------------- + +@test "chezmoi profile is dev" { + grep -qE '^\s*profile\s*=\s*"dev"' "$HOME/.config/chezmoi/chezmoi.toml" +} + +# --- languages (mise core backends) ---------------------------------------- + +@test "node on PATH (mise lts)" { command -v node; } +@test "go on PATH (mise latest)" { command -v go; } +@test "python on PATH (mise 3.12)" { command -v python; } +@test "rustc on PATH (mise stable)" { command -v rustc; } + +# --- CLI utilities (mise aqua) -------------------------------------------- + +@test "jq" { command -v jq; } +@test "gh" { command -v gh; } +@test "delta" { command -v delta; } +@test "shellcheck" { command -v shellcheck; } +@test "fd" { command -v fd; } +@test "yq" { command -v yq; } +@test "gitleaks" { command -v gitleaks; } + +# --- cloud + Kubernetes (mise aqua) --------------------------------------- + +@test "kubectl" { command -v kubectl; } +@test "helm" { command -v helm; } +@test "k9s" { command -v k9s; } +@test "kustomize" { command -v kustomize; } +@test "stern" { command -v stern; } +@test "argocd" { command -v argocd; } +@test "tofu (opentofu)" { command -v tofu; } +@test "aws (awscli)" { command -v aws; } +@test "rclone" { command -v rclone; } +@test "cloudflared" { command -v cloudflared; } + +# --- networking + Go ecosystem + pkg managers (mise aqua) ----------------- + +@test "websocat" { command -v websocat; } +@test "buf" { command -v buf; } +@test "golangci-lint" { command -v golangci-lint; } +@test "goreleaser" { command -v goreleaser; } +@test "gotestsum" { command -v gotestsum; } +@test "protoc-gen-go" { command -v protoc-gen-go; } +@test "pnpm" { command -v pnpm; } +@test "uv" { command -v uv; } + +# --- post-installs (need mise's runtimes) --------------------------------- + +@test "goimports (go install)" { command -v goimports; } +@test "ssh-audit (uv tool install)" { command -v ssh-audit; } + +# --- AI CLIs (mise aqua: native binaries, no Node coupling) ---------------- + +@test "claude" { command -v claude; } +@test "codex" { command -v codex; } + +# --- execute-checks: actually run --version on critical binaries ---------- +# command -v above resolves PATH but doesn't catch broken binary (corrupt +# aqua download, glibc mismatch on Linux, version_prefix filter mismatch +# after upstream naming change). These tests catch that. + +@test "claude --version executes" { run claude --version; [ "$status" -eq 0 ]; } +@test "codex --version executes" { run codex --version; [ "$status" -eq 0 ]; } +@test "op --version executes" { run op --version; [ "$status" -eq 0 ]; } +@test "gh --version executes" { run gh --version; [ "$status" -eq 0 ]; } +@test "delta --version executes" { run delta --version; [ "$status" -eq 0 ]; } +@test "rtk --version executes (skip on glibc < 2.39)" { + run rtk --version + [ "$status" -eq 0 ] || skip "rtk fails (likely glibc < 2.39 on this runner)" +} + +# --- no partial install ---------------------------------------------------- + +@test "no (missing) mise tools post-install" { + ! mise ls 2>/dev/null | grep -q '(missing)' +} + +# --- idempotency ----------------------------------------------------------- + +@test "chezmoi diff is empty post-apply" { + run chezmoi diff + if [ "$status" -ne 0 ] || [ -n "$output" ]; then + echo "chezmoi diff exit=$status output:" >&2 + echo "$output" >&2 + fi + [ "$status" -eq 0 ] + [ -z "$output" ] +} + +# --- apt dev packages (sanity check beyond common.bats core list) --------- + +@test "apt: htop" { command -v htop; } +@test "apt: tree" { command -v tree; } +@test "apt: wget" { command -v wget; } +@test "apt: nmap" { command -v nmap; } diff --git a/tests/unit/install-packages.bats b/tests/unit/install-packages.bats index 6931a75..f10c9b6 100644 --- a/tests/unit/install-packages.bats +++ b/tests/unit/install-packages.bats @@ -23,7 +23,6 @@ setup() { export DOTFILES_DEV_APT="" export DOTFILES_CORE_DNF="curl git zsh" export DOTFILES_DEV_DNF="" - export DOTFILES_DEV_NPM_GLOBAL="" # Wipe INSTALL_PACKAGES_INVOKE so sourcing the lib doesn't auto-run main. unset INSTALL_PACKAGES_INVOKE @@ -237,6 +236,45 @@ setup() { grep -q 'mise install --yes' "$mise_log" } +@test "mise_install_tools: emits WARNING when (missing) tools post-install" { + command() { + case "$2" in mise) return 0 ;; *) builtin command "$@" ;; esac + } + # mise install → silent success. mise ls → returns one (missing) line. + mise() { + case "$1" in + install) return 0 ;; + trust) return 0 ;; + ls) echo "aqua:foo/bar 1.0 ~/.config/mise/config.toml latest (missing)" ;; + *) :; ;; + esac + } + + run mise_install_tools + [ "$status" -eq 0 ] + [[ "$output" =~ "WARNING: 1 mise tool" ]] + [[ "$output" =~ "gh auth login" ]] + [[ "$output" =~ "op://Personal/GitHub API Token" ]] +} + +@test "mise_install_tools: NO warning when mise ls is clean" { + command() { + case "$2" in mise) return 0 ;; *) builtin command "$@" ;; esac + } + mise() { + case "$1" in + install) return 0 ;; + trust) return 0 ;; + ls) echo "aqua:foo/bar 1.0 ~/.config/mise/config.toml latest" ;; + *) :; ;; + esac + } + + run mise_install_tools + [ "$status" -eq 0 ] + [[ ! "$output" =~ "WARNING" ]] +} + # --- main dispatcher ------------------------------------------------------- @test "main: core profile → no post-installs run" { @@ -247,15 +285,14 @@ setup() { brew_bundle_install() { :; } linux_pkg_install() { :; } mise_install_tools() { :; } - linux_fd_symlink_fallback() { :; } post_install_goimports() { echo "GO_CALLED"; } post_install_ssh_audit() { echo "AUDIT_CALLED"; } - npm_install_ai_globals() { echo "NPM_CALLED"; } + post_install_rtk_init() { echo "RTK_CALLED"; } run main [[ ! "$output" =~ "GO_CALLED" ]] [[ ! "$output" =~ "AUDIT_CALLED" ]] - [[ ! "$output" =~ "NPM_CALLED" ]] + [[ ! "$output" =~ "RTK_CALLED" ]] } @test "main: dev profile → all post-installs run" { @@ -265,15 +302,14 @@ setup() { brew_bundle_install() { :; } linux_pkg_install() { :; } mise_install_tools() { :; } - linux_fd_symlink_fallback() { :; } post_install_goimports() { echo "GO_CALLED"; } post_install_ssh_audit() { echo "AUDIT_CALLED"; } - npm_install_ai_globals() { echo "NPM_CALLED"; } + post_install_rtk_init() { echo "RTK_CALLED"; } run main [[ "$output" =~ "GO_CALLED" ]] [[ "$output" =~ "AUDIT_CALLED" ]] - [[ "$output" =~ "NPM_CALLED" ]] + [[ "$output" =~ "RTK_CALLED" ]] } @test "main: darwin OS → brew path, not linux" { @@ -281,7 +317,6 @@ setup() { brew_bundle_install() { echo "BREW_CALLED"; } linux_pkg_install() { echo "LINUX_CALLED"; } mise_install_tools() { :; } - linux_fd_symlink_fallback() { :; } run main [[ "$output" =~ "BREW_CALLED" ]] @@ -294,13 +329,89 @@ setup() { brew_bundle_install() { echo "BREW_CALLED"; } linux_pkg_install() { echo "LINUX_CALLED"; } mise_install_tools() { :; } - linux_fd_symlink_fallback() { :; } run main [[ "$output" =~ "LINUX_CALLED" ]] [[ ! "$output" =~ "BREW_CALLED" ]] } +# --- post_install_rtk_init ------------------------------------------------- + +@test "post_install_rtk_init: rtk not on PATH → skip + warning" { + command() { + case "$2" in rtk) return 1 ;; *) builtin command "$@" ;; esac + } + + run post_install_rtk_init + [ "$status" -eq 0 ] + [[ "$output" =~ "rtk not on PATH" ]] +} + +@test "post_install_rtk_init: rtk --version fails (glibc too old) → graceful skip" { + command() { + case "$2" in rtk) return 0 ;; *) builtin command "$@" ;; esac + } + rtk() { + case "$1" in --version) return 1 ;; *) return 0 ;; esac + } + + run post_install_rtk_init + [ "$status" -eq 0 ] + [[ "$output" =~ "glibc too old" ]] + [[ ! "$output" =~ "initialized" ]] +} + +@test "post_install_rtk_init: claude present → calls --auto-patch" { + rtk_log="$BATS_TEST_TMPDIR/rtk.log" + command() { + case "$2" in rtk|claude) return 0 ;; codex) return 1 ;; *) builtin command "$@" ;; esac + } + rtk() { + echo "rtk $*" >>"$rtk_log" + case "$1" in --version) echo "rtk 0.40.0" ;; esac + return 0 + } + + run post_install_rtk_init + [ "$status" -eq 0 ] + grep -q 'rtk init -g --auto-patch' "$rtk_log" + [[ "$output" =~ "initialized" ]] +} + +@test "post_install_rtk_init: codex present → calls --codex" { + rtk_log="$BATS_TEST_TMPDIR/rtk.log" + command() { + case "$2" in rtk|codex) return 0 ;; claude) return 1 ;; *) builtin command "$@" ;; esac + } + rtk() { + echo "rtk $*" >>"$rtk_log" + case "$1" in --version) echo "rtk 0.40.0" ;; esac + return 0 + } + + run post_install_rtk_init + [ "$status" -eq 0 ] + grep -q 'rtk init -g --codex' "$rtk_log" + ! grep -q 'rtk init -g --auto-patch' "$rtk_log" +} + +@test "post_install_rtk_init: neither claude nor codex → still prints initialized banner (idempotent no-op-ish)" { + rtk_log="$BATS_TEST_TMPDIR/rtk.log" + command() { + case "$2" in rtk) return 0 ;; claude|codex) return 1 ;; *) builtin command "$@" ;; esac + } + rtk() { + echo "rtk $*" >>"$rtk_log" + case "$1" in --version) echo "rtk 0.40.0" ;; esac + return 0 + } + + run post_install_rtk_init + [ "$status" -eq 0 ] + ! grep -q 'rtk init' "$rtk_log" + [[ "$output" =~ "initialized" ]] +} + # --- guard sanity ---------------------------------------------------------- @test "INSTALL_PACKAGES_INVOKE guard: sourcing without flag does not run main" {