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
4 changes: 3 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ jobs:
ca-certificates \
curl \
gnupg \
shellcheck
shellcheck \
python3 \
zsh

- name: Cache chezmoi binary
id: cache-chezmoi
Expand Down
73 changes: 70 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
[![Chezmoi](https://img.shields.io/badge/managed%20with-chezmoi-6e4c1e)](https://www.chezmoi.io/)
![Platform](https://img.shields.io/badge/platform-macOS%20%2B%20Linux-lightgrey)

Personal macOS development environment managed with Chezmoi and Homebrew.
Shell, Git, editor, and XDG configuration also work on Linux where supported.
Personal development environment managed with Chezmoi: Homebrew on macOS,
native apt packages on Ubuntu 26.04. Shell, Git, editor, and XDG configuration
are shared across both systems.

## Overview

Expand All @@ -15,7 +16,7 @@ Shell, Git, editor, and XDG configuration also work on Linux where supported.
| Shell | Zsh, Oh My Zsh, Starship, fzf, mise, and direnv |
| Editors | Zed, VS Code, Vim, and Neovim |
| Shortcuts | Raycast Caps Lock Hyper translated by Ghostty into tmux commands |
| Packages | Homebrew bundle with personal and work profiles |
| Packages | Homebrew profiles on macOS; common native apt packages on Ubuntu |
| SSH | Portable client config; keys stay local to each machine |
| macOS | Dock, Finder, keyboard, Safari, and general defaults |

Expand All @@ -35,6 +36,72 @@ settings in `~/.gitconfig.local`. On new machines, the root config includes
repository defaults from the managed `~/.config/git/dotfiles.gitconfig`. Omit
`--macos` to apply the dotfiles without changing macOS defaults.

### Ubuntu 26.04

Install the bootstrap prerequisites and Chezmoi, then preview before applying:

```bash
sudo apt-get update
sudo apt-get install -y git curl ca-certificates
sh -c "$(curl -fsLS get.chezmoi.io)" -- -b "$HOME/.local/bin"
export PATH="$HOME/.local/bin:$PATH"
git clone https://github.com/froppa/dotfiles.git ~/.local/share/chezmoi
cd ~/.local/share/chezmoi
chezmoi init --source="$PWD" --promptString 'name=sdf'
chezmoi diff
chezmoi apply -v
```

If already cloned, use that checkout instead of cloning again. Ubuntu setup
installs missing packages with apt and never installs or updates Linuxbrew.
An existing Linuxbrew installation is left on disk, but managed Zsh startup
no longer adds it to PATH. Personal and work profiles currently share the same
Ubuntu package set. Raycast, Hammerspoon, and iTerm2 configuration is macOS-only.

The native set covers shell, editor, Git, and common build tools. It does not
add third-party repositories or install mise, Codex, Claude Code, Docker,
OpenTofu, AWS CLI, lazygit, Ghostty, GUI editors, or Nerd Fonts. These remain
separate bootstrap steps; Ubuntu's `yq` is also not assumed equivalent to the
Homebrew formula. Once mise is installed, `mise install` installs the declared
runtimes; copying its configuration alone does not install them. Some Neovim
plugins need those runtimes before their first launch.

After applying, choose Zsh as your login shell if desired:

```bash
chsh -s "$(command -v zsh)"
```

Log out and back in. `fd` and `bat` aliases handle Ubuntu's executable names,
and fzf uses an executable path that also works in its subprocesses. The Linux
`update` alias uses apt. Mac-specific Hyper shortcuts require separate desktop
key bindings on Ubuntu.

Authenticate GitHub, Codex, and Claude freshly on Ubuntu as your normal user.
Do not copy tokens, private keys, or provider credential stores from another
machine. SSH access and the SSH server belong to the machine's infrastructure
setup; these dotfiles manage only portable client configuration. A remote
tmux session continues when you disconnect, but not when the Ubuntu VM shuts
down.

### Ubuntu AI tools

After applying the native package configuration, install Claude Code and mise
through their signed apt repositories, and Codex through its official standalone
installer:

```bash
./scripts/bootstrap-ubuntu-ai.sh
codex login
claude auth login
gh auth login --web
```

This does not authenticate automatically or copy credentials from your Mac.
Codex is pinned to the validated CLI version in the script. Runtime installation
is separate; do not run source-building plugin installers until the required
Depot build wrapper is available on the machine.

### SSH

SSH keys are never managed by this repository. Generate a new key on each
Expand Down
48 changes: 48 additions & 0 deletions home/.chezmoidata/40-packages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,54 @@ packages:
- chezmoi
- shellcheck

# Ubuntu package names are explicit: Brew formula names are not portable.
# Vendor tools and language runtimes are documented separately in README.md.
ubuntu:
apt:
- ca-certificates
- curl
- unzip
- coreutils
- findutils
- sed
- grep
- moreutils
- tree
- build-essential
- ripgrep
- zsh
- starship
- tmux
- fzf
- direnv
- ncdu
- gnupg
- openssh-client
- openssl
- nmap
- age
- bat
- fd-find
- jq
- htop
- gh
- git
- git-lfs
- pipx
- eza
- httpie
- vim
- neovim
- cmake
- m4
- autoconf
- automake
- libtool
- libboost-dev
- pkgconf
- sccache
- shellcheck

casks:
- ghostty
- raycast
Expand Down
6 changes: 6 additions & 0 deletions home/.chezmoiignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,9 @@
# stray one read as a file to delete on the next apply.
**/.DS_Store
.oh-my-zsh/cache/**

{{ if ne .chezmoi.os "darwin" -}}
.hammerspoon
.config/raycast
.config/iterm2
{{ end -}}
3 changes: 3 additions & 0 deletions home/.chezmoiscripts/run_once_10-install-homebrew.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
#!/usr/bin/env bash
set -euo pipefail

# Ubuntu uses apt; never install or update Linuxbrew during bootstrap.
[[ "$(uname -s)" == Darwin ]] || exit 0

if ! command -v brew &>/dev/null; then
echo "=> Homebrew installing..."
bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
Expand Down
23 changes: 23 additions & 0 deletions home/.chezmoiscripts/run_onchange_20-install-pkgs.sh.tmpl
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail

{{ if eq .chezmoi.os "darwin" -}}
command -v brew >/dev/null 2>&1 || {
echo "=> brew not found, skipping package install"
exit 0
Expand Down Expand Up @@ -43,3 +44,25 @@ cask "{{ . }}"
BREWFILE

brew cleanup
{{ else if and (eq .chezmoi.os "linux") (eq .chezmoi.osRelease.id "ubuntu") -}}
packages=(
{{ range .packages.ubuntu.apt }} "{{ . }}"
{{ end -}}
)
missing=()
for package in "${packages[@]}"; do
if [[ "$(dpkg-query -W -f='${Status}' "$package" 2>/dev/null || true)" != "install ok installed" ]]; then
missing+=("$package")
fi
done

if [[ ${#missing[@]} -gt 0 ]]; then
sudo apt-get update
sudo apt-get install -y --no-install-recommends "${missing[@]}"
else
echo "=> Ubuntu packages already installed"
fi
{{ else -}}
echo "Package bootstrap supports macOS and Ubuntu; install packages manually on this OS." >&2
exit 1
{{ end -}}
7 changes: 6 additions & 1 deletion home/dot_aliases
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,12 @@ if [[ "$OSTYPE" == darwin* ]]; then
alias afk='/System/Library/CoreServices/Menu\ Extras/User.menu/Contents/Resources/CGSession -suspend'
fi

alias brewup='brew update && brew upgrade && brew cleanup'
if [[ "$OSTYPE" == linux* ]] && command -v apt-get >/dev/null 2>&1; then
alias update='sudo apt-get update && sudo apt-get upgrade'
fi
if [[ "$OSTYPE" == darwin* ]]; then
alias brewup='brew update && brew upgrade && brew cleanup'
fi
alias cleanup="find . -type f -name '*.DS_Store' -ls -delete"

[[ -f "$HOME/.aliases.local" ]] && source "$HOME/.aliases.local"
13 changes: 0 additions & 13 deletions home/dot_config/mise/config.toml

This file was deleted.

27 changes: 27 additions & 0 deletions home/dot_config/mise/config.toml.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{{- $ubuntu := and (eq .chezmoi.os "linux") (eq .chezmoi.osRelease.id "ubuntu") -}}
# ~/.config/mise/config.toml
# Managed by chezmoi — global default runtimes (replaces nvm/pyenv/brew runtimes)
[tools]
node = "lts"
python = "3.12"
go = "latest"
ruby = "latest"
{{ if $ubuntu -}}
"aqua:pnpm/pnpm" = "latest"
rust = "stable"
bun = "latest"
{{ else -}}
pnpm = "latest"
yarn = "latest"
{{ end -}}
java = "temurin-21"

[settings.ruby]
compile = false
{{ if $ubuntu }}
[settings.node]
compile = false

[settings.python]
compile = false
{{ end -}}
26 changes: 13 additions & 13 deletions home/dot_zprofile
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,20 @@
# Managed by chezmoi
# Login shell initialization only

# Homebrew
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)"
elif [[ -x /home/linuxbrew/.linuxbrew/bin/brew ]]; then
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
fi
# Homebrew is the macOS package manager; Ubuntu uses apt.
if [[ "$OSTYPE" == darwin* ]]; then
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

# Homebrew zsh completions
if (( $+commands[brew] )); then
brew_prefix="$(brew --prefix 2>/dev/null)"
if [[ -n "$brew_prefix" && -d "$brew_prefix/share/zsh/site-functions" ]]; then
fpath=("$brew_prefix/share/zsh/site-functions" $fpath)
# Homebrew zsh completions
if (( $+commands[brew] )); then
brew_prefix="$(brew --prefix 2>/dev/null)"
if [[ -n "$brew_prefix" && -d "$brew_prefix/share/zsh/site-functions" ]]; then
fpath=("$brew_prefix/share/zsh/site-functions" $fpath)
fi
fi
fi

Expand Down
13 changes: 9 additions & 4 deletions home/dot_zshrc
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,9 @@ fi
# =======================================
alias g="git"

command -v fdfind >/dev/null 2>&1 && alias fd="fdfind"
# Ubuntu exposes these programs under different executable names.
(( ! $+commands[fd] && $+commands[fdfind] )) && alias fd="fdfind"
(( ! $+commands[bat] && $+commands[batcat] )) && alias bat="batcat"

gpgkeys() {
gpg -K --with-colons 2>/dev/null | awk -F: '$1 == "sec" { print $5 }'
Expand Down Expand Up @@ -138,11 +140,14 @@ bindkey "^[[1;5D" backward-word
# =======================================
if command -v fzf >/dev/null 2>&1; then
source <(fzf --zsh)
if command -v fd >/dev/null 2>&1; then
export FZF_DEFAULT_COMMAND='fd --type f --hidden --follow --exclude .git'
# fzf launches a child shell, so a Zsh alias alone cannot provide fd.
fd_command="${commands[fd]:-${commands[fdfind]:-}}"
if [[ -n "$fd_command" ]]; then
export FZF_DEFAULT_COMMAND="${(q)fd_command} --type f --hidden --follow --exclude .git"
export FZF_CTRL_T_COMMAND="$FZF_DEFAULT_COMMAND"
export FZF_ALT_C_COMMAND='fd --type d --hidden --follow --exclude .git'
export FZF_ALT_C_COMMAND="${(q)fd_command} --type d --hidden --follow --exclude .git"
fi
unset fd_command
fi

# =======================================
Expand Down
37 changes: 37 additions & 0 deletions scripts/bootstrap-ubuntu-ai.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#!/usr/bin/env bash
# Native Ubuntu agent tools; authenticate as the normal user after installation.
set -euo pipefail

[[ $EUID -ne 0 ]] || { echo "Run as your normal Ubuntu user, not root." >&2; exit 1; }
[[ -r /etc/os-release ]] || { echo "Ubuntu is required." >&2; exit 1; }
# shellcheck disable=SC1091
source /etc/os-release
[[ "$ID" == ubuntu && "$VERSION_ID" == 26.04 ]] || {
echo "This bootstrap is validated for Ubuntu 26.04." >&2; exit 1;
}

sudo apt-get update
sudo apt-get install -y --no-install-recommends curl gnupg ca-certificates software-properties-common
scratch=$(mktemp -d)
trap 'rm -rf "$scratch"' EXIT
curl -fsSL https://downloads.claude.ai/keys/claude-code.asc -o "$scratch/claude-code.asc"
fingerprint=$(gpg --show-keys --with-colons "$scratch/claude-code.asc" 2>/dev/null | awk -F: '$1 == "fpr" {print $10; exit}')
[[ "$fingerprint" == 31DDDE24DDFAB679F42D7BD2BAA929FF1A7ECACE ]] || {
echo "Unexpected Claude signing key; no repository was trusted." >&2; exit 1;
}
sudo install -d -m 0755 /etc/apt/keyrings
sudo install -m 0644 "$scratch/claude-code.asc" /etc/apt/keyrings/claude-code.asc
printf '%s\n' 'deb [signed-by=/etc/apt/keyrings/claude-code.asc] https://downloads.claude.ai/claude-code/apt/stable stable main' |
sudo tee /etc/apt/sources.list.d/claude-code.list >/dev/null
sudo add-apt-repository -y --no-update ppa:jdxcode/mise
sudo apt-get update
sudo apt-get install -y --no-install-recommends claude-code mise

# Codex publishes a standalone Linux installer, not an apt package.
export PATH="$HOME/.local/bin:$PATH"
curl -fsSL https://chatgpt.com/codex/install.sh -o "$scratch/install-codex.sh"
CODEX_NON_INTERACTIVE=1 sh "$scratch/install-codex.sh" --release 0.154.0
codex --version
claude --version
mise --version
printf '%s\n' 'Authenticate freshly: codex login; claude auth login; gh auth login --web'
6 changes: 5 additions & 1 deletion test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ section "Checking required tools"

command -v shellcheck >/dev/null 2>&1 || fail "shellcheck is required"
command -v chezmoi >/dev/null 2>&1 || fail "chezmoi is required"
command -v python3 >/dev/null 2>&1 || fail "python3 is required"
command -v zsh >/dev/null 2>&1 || fail "zsh is required"

section "Running shellcheck"

Expand Down Expand Up @@ -111,7 +113,9 @@ fi
! grep -Fq -- '--exclude encrypted' README.md || fail "the documented install must apply cleanly without an age identity"
sed -n '/^ darwin:/,/^ personal:/p' home/.chezmoidata/40-packages.yml | grep -Fq -- '- xcode-build-server' || fail "xcode-build-server must be macOS-only"
grep -Fq '/usr/local/bin/brew' home/.chezmoiscripts/run_once_10-install-homebrew.sh || fail "Homebrew setup must support Intel macOS"
grep -Fq '/home/linuxbrew/.linuxbrew/bin/brew' home/.chezmoiscripts/run_once_10-install-homebrew.sh || fail "Homebrew setup must support Linux"

section "Testing Ubuntu and macOS bootstrap behavior"
PYTHONDONTWRITEBYTECODE=1 python3 -m unittest discover -s tests -p 'test_*.py' -v

section "Testing fresh-home chezmoi dry-run apply"

Expand Down
Loading