diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..9cbb9b7 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,8 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + cooldown: + default-days: 7 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..8b19b89 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,161 @@ +name: CI + +on: + pull_request: + push: + branches: + - main + workflow_dispatch: + +permissions: {} + +concurrency: + group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +env: + UV_MALWARE_CHECK: "1" + +jobs: + static-checks: + name: Static checks + runs-on: ubuntu-24.04 + timeout-minutes: 10 + permissions: + contents: read + steps: + - name: Check out repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Install Zsh + run: | + sudo apt-get update + sudo apt-get install --yes zsh + - name: Check shell syntax + run: | + bash -n assimilate.sh bashrc bash_profile claude/statusline.sh scripts/ci/*.sh + zsh -n zshrc + - name: Test VPS container harness + run: scripts/ci/test-vps-container.sh + - name: Check Git configuration + run: git config --file gitconfig --list >/dev/null + - name: Check strict JSON + run: | + jq empty nvim/coc-settings.json + jq empty vscode/code_settings.json + jq empty zed/keymap.json + + pre-commit: + name: Pre-commit + runs-on: ubuntu-24.04 + timeout-minutes: 15 + permissions: + contents: read + steps: + - name: Check out repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Install pre-commit + run: python3 -m pip install --user pre-commit==4.6.2 + - name: Run repository hooks + run: python3 -m pre_commit run --all-files --show-diff-on-failure + + actions-security: + name: Actions security + runs-on: ubuntu-24.04 + timeout-minutes: 15 + permissions: + contents: read + steps: + - name: Check out repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Validate workflows with actionlint + run: go run github.com/rhysd/actionlint/cmd/actionlint@v1.7.12 + - name: Audit workflows with zizmor + run: pipx run zizmor==1.29.0 . + + secret-history: + name: Secret history + runs-on: ubuntu-24.04 + timeout-minutes: 15 + permissions: + contents: read + steps: + - name: Check out full history + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + - name: Install pinned Gitleaks + env: + GOBIN: ${{ runner.temp }}/bin + run: go install github.com/zricethezav/gitleaks/v8@v8.30.1 + - name: Scan Git history + run: '"${RUNNER_TEMP}/bin/gitleaks" git --redact --verbose' + + install: + name: Install (${{ matrix.os }}) + strategy: + fail-fast: false + matrix: + os: + - macos-26 + - ubuntu-24.04 + runs-on: ${{ matrix.os }} + timeout-minutes: 60 + permissions: + contents: read + steps: + - name: Check out repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Install Linux test prerequisites + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install --yes zsh + - name: Install dotfiles twice on hosted runner + env: + HOMEBREW_NO_ANALYTICS: "1" + run: | + # The hosted VM and its home directory are discarded after this job. + # Keep the runner's package-manager state while honoring assimilate's + # documented requirement that the repository live at $HOME/dotfiles. + test ! -e "$HOME/dotfiles" + ln -s "$GITHUB_WORKSPACE" "$HOME/dotfiles" + + ./assimilate.sh + DOTFILES="$HOME/dotfiles" scripts/ci/verify-assimilate.sh + backup_count="$(find "$HOME/backups" -mindepth 1 -maxdepth 1 | wc -l | tr -d ' ')" + + ./assimilate.sh + DOTFILES="$HOME/dotfiles" scripts/ci/verify-assimilate.sh + + test "$(find "$HOME/backups" -mindepth 1 -maxdepth 1 | wc -l | tr -d ' ')" = "$backup_count" + + vps-install: + name: VPS install (${{ matrix.target }}) + strategy: + fail-fast: false + matrix: + target: + - ubuntu + - amazon-linux + runs-on: ubuntu-24.04 + timeout-minutes: 60 + permissions: + contents: read + steps: + - name: Check out repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Install dotfiles twice in a minimal VPS image + env: + VPS_TARGET: ${{ matrix.target }} + run: scripts/ci/run-vps-container.sh run "$VPS_TARGET" diff --git a/.gitignore b/.gitignore index d713c1d..11fa49e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,17 @@ local .DS_Store -nvim/plugin/ gh/ .claude .wrangler +# Secrets and machine-local tool state +*.pem +*.key +.env +.env.* +credentials.json +*.secret +Brewfile.lock.json + +# XDG_CONFIG_HOME points at this repo, so tools sometimes drop machine-local +# state here. Homebrew writes its tap trust store (homebrew/trust.json). +homebrew/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..eef9bf0 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,9 @@ +# Pre-commit hooks for this dotfiles repo. +# Install once with: pre-commit install +# Run against all files: pre-commit run --all-files +repos: + - repo: https://github.com/gitleaks/gitleaks + rev: v8.30.1 + hooks: + # Scans staged changes for secrets before they can be committed. + - id: gitleaks diff --git a/Brewfile b/Brewfile index 91ef6d4..a63d208 100644 --- a/Brewfile +++ b/Brewfile @@ -6,17 +6,15 @@ brew "gh" brew "hashicorp/tap/terraform", trusted: true brew "htop" brew "jq" -brew "k9s" brew "lsd" brew "lua" brew "neovim" brew "mosh" -brew "node" brew "postgresql@17" -brew "py-spy" -brew "pyenv" +brew "pre-commit" brew "tmux" brew "tree" +brew "uv" brew "wget" brew "zsh-autosuggestions" brew "zsh-syntax-highlighting" diff --git a/assimilate.sh b/assimilate.sh index e445ec1..9c683ff 100755 --- a/assimilate.sh +++ b/assimilate.sh @@ -1,6 +1,9 @@ -#!/usr/bin/env sh +#!/usr/bin/env bash -set -ex +set -euxo pipefail + +OS="$(uname -s)" +ARCH="$(uname -m)" PREFIX="$HOME" DOTFILES="$PREFIX/dotfiles" @@ -13,9 +16,16 @@ function sym () { # Ensure parent directory exists mkdir -p "$(dirname "$dest")" + # An already-correct link is installed. Leaving it alone makes repeated + # assimilation safe and avoids filling the backup directory on every run. + if [ -L "$dest" ] && [ "$(readlink "$dest")" = "$src" ]; then + return + fi + # Save existing dotfiles (also matches dangling symlinks, where -e alone returns false) if [ -e "$dest" ] || [ -L "$dest" ]; then - backup="$BACKUPS/$(basename $dest)-$(date +%s)" + backup="$(mktemp -d "$BACKUPS/$(basename "$dest").XXXXXX")" + rmdir "$backup" mv "$dest" "$backup" echo "> Moved $dest to $backup" fi @@ -24,6 +34,19 @@ function sym () { ln -s "$src" "$dest" } +function clone_pinned () { + url="$1"; dir="$2"; sha="$3" + # Fetch only the pinned commit, no full history. Avoids `git clone --revision` + # (git >= 2.49) so this works on older git too, e.g. AL2023's 2.40 on stale AMIs. + # Fetch-by-SHA relies on the server allowing reachable-SHA1-in-want (GitHub does). + if [ ! -d "$dir" ]; then + git init -q "$dir" + git -C "$dir" remote add origin "$url" + fi + git -C "$dir" fetch --depth 1 origin "$sha" + git -C "$dir" checkout -q "$sha" +} + if [ ! -e "$DOTFILES" ]; then echo "error: dotfiles/ needs to reside in $PREFIX" exit 1 @@ -31,35 +54,61 @@ fi mkdir -p "$BACKUPS/vim_backups" -sym bashrc .bashrc +# Shared symlinks (work on macOS and Linux) sym bash_profile .bash_profile +sym shared.sh .shared.sh sym gitconfig .gitconfig sym tmux.conf .tmux.conf sym zshrc .zshrc sym nvim .config/nvim -sym hammerspoon .hammerspoon -sym vscode/code_settings.json .vscode/settings.json -sym zed/settings.json .config/zed/settings.json -sym ghostty/config Library/Application\ Support/com.mitchellh.ghostty/config -sym tmux-powerline/config.sh .config/tmux-powerline/config.sh +sym tmux-powerline/config.sh .config/tmux-powerline/config.sh sym tmux-powerline/themes/theme.sh .config/tmux-powerline/themes/theme.sh -sym claude/statusline.sh .claude/statusline.sh -sym zed/keymap.json .config/zed/keymap.json +sym claude/statusline.sh .claude/statusline.sh +sym ssh/config .ssh/config +sym uv/uv.toml .config/uv/uv.toml -brew bundle install +# Lock down sensitive symlink targets (chmod follows the symlink to the repo file). +chmod 600 "$HOME/.gitconfig" +chmod 700 "$HOME/.ssh" +chmod 600 "$HOME/.ssh/config" -function clone_pinned () { - url="$1"; dir="$2"; sha="$3" - if [ ! -d "$dir" ]; then - git clone --revision="$sha" "$url" "$dir" - else - # SHA may be missing locally — newer than the last fetch, or absent because - # the prior clone was shallow (--revision pulls only that one commit). - # Fetch it directly. - git -C "$dir" fetch origin "$sha" - git -C "$dir" checkout "$sha" +# bashrc: symlinked on macOS; sourced from a stub on Linux. +# On the EC2 dev box, user_data appends a secrets/region block to .bashrc after +# this script runs. If .bashrc were a symlink into the repo, those appends would +# write into the dotfiles repo's tracked file. So on Linux, leave .bashrc as a +# regular file and source the dotfiles bashrc from it. +if [ "$OS" = "Darwin" ]; then + sym bashrc .bashrc +else + if ! grep -Fq 'dotfiles/bashrc' "$PREFIX/.bashrc" 2>/dev/null; then + echo '[ -f "$HOME/dotfiles/bashrc" ] && . "$HOME/dotfiles/bashrc"' >> "$PREFIX/.bashrc" fi -} +fi + +# macOS-only symlinks and Homebrew (apps/paths don't exist on Linux) +if [ "$OS" = "Darwin" ]; then + sym hammerspoon .hammerspoon + sym vscode/code_settings.json .vscode/settings.json + sym zed/settings.json .config/zed/settings.json + sym zed/keymap.json .config/zed/keymap.json + sym ghostty/config Library/Application\ Support/com.mitchellh.ghostty/config + + # Homebrew 6 requires explicit trust before installing formulae from a tap. + brew trust hashicorp/tap + brew bundle install +fi + +# Let uv manage Python when it is available. The rolling release-age window in +# uv/uv.toml applies to package resolution, while this installs the interpreter. +if command -v uv >/dev/null 2>&1; then + uv python install +fi + +# Install repository security hooks when pre-commit is available (Homebrew +# supplies it on macOS). Linux users can install pre-commit independently. +if command -v pre-commit >/dev/null 2>&1; then + (cd "$DOTFILES" && pre-commit install --allow-missing-config) +fi # Install oh-my-zsh (clone repo directly; install.sh is just `git clone` once # its zshrc/runzsh/chsh side-effects are disabled) @@ -68,33 +117,140 @@ clone_pinned https://github.com/ohmyzsh/ohmyzsh "$HOME/.oh-my-zsh" e7aa0c56e6834 # Install rust via rustup-init.sh pinned to a specific GitHub commit (immutable), # with rustc toolchain version locked if [ ! -d "$HOME/.cargo" ]; then - RUSTUP_SHA=e10ffbdbb807c47fdd208119de99e7baae3e0dfe # rustup 1.29.0 + RUSTUP_SHA=28d1352dbcb436d3111c3594b9e1588e94950464 # rustup 1.29.0 tag's commit curl --proto '=https' --tlsv1.2 -sSf \ "https://raw.githubusercontent.com/rust-lang/rustup/$RUSTUP_SHA/rustup-init.sh" \ | sh -s -- -y --no-modify-path --default-toolchain 1.95.0 fi -# Install git-delta from a pinned GitHub release tarball (sidesteps brew bottle ABI drift) +# Install git-delta from a pinned GitHub release tarball when available. +# delta 0.19.2 has no Intel macOS archive, so that platform uses Homebrew. DELTA_VERSION=0.19.2 -DELTA_SHA256=9be36612a5a13e9e386dc498fb8e50dc87c72ee42b63db0ea05b32f99a72a69a DELTA_BIN="$HOME/.local/bin/delta" -if [ ! -x "$DELTA_BIN" ] || [ "$("$DELTA_BIN" --version 2>/dev/null | awk '{print $2}')" != "$DELTA_VERSION" ]; then + +case "$OS-$ARCH" in + Darwin-arm64) + DELTA_TARGET=aarch64-apple-darwin + DELTA_SHA256=9be36612a5a13e9e386dc498fb8e50dc87c72ee42b63db0ea05b32f99a72a69a + ;; + Linux-x86_64) + DELTA_TARGET=x86_64-unknown-linux-gnu + DELTA_SHA256=8e695c5f586a8c53d6c3b01be0b4a422ed218bfed2a56191caebe373a1c18ab2 + ;; + Darwin-x86_64) + DELTA_TARGET="" + if [ "$("$DELTA_BIN" --version 2>/dev/null | awk '{print $2}')" != "$DELTA_VERSION" ]; then + brew install git-delta + fi + ;; + *) + echo "WARN: no delta build pinned for $OS-$ARCH — skipping delta install" >&2 + DELTA_TARGET="" + ;; +esac + +if [ -n "$DELTA_TARGET" ] && { [ ! -x "$DELTA_BIN" ] || [ "$("$DELTA_BIN" --version 2>/dev/null | awk '{print $2}')" != "$DELTA_VERSION" ]; }; then tmp=$(mktemp -d) - curl -fsSL "https://github.com/dandavison/delta/releases/download/${DELTA_VERSION}/delta-${DELTA_VERSION}-aarch64-apple-darwin.tar.gz" -o "$tmp/delta.tar.gz" - echo "${DELTA_SHA256} $tmp/delta.tar.gz" | shasum -a 256 -c - + curl -fsSL "https://github.com/dandavison/delta/releases/download/${DELTA_VERSION}/delta-${DELTA_VERSION}-${DELTA_TARGET}.tar.gz" -o "$tmp/delta.tar.gz" + if command -v sha256sum >/dev/null; then + echo "${DELTA_SHA256} $tmp/delta.tar.gz" | sha256sum -c - + else + echo "${DELTA_SHA256} $tmp/delta.tar.gz" | shasum -a 256 -c - + fi mkdir -p "$HOME/.local/bin" tar -xzf "$tmp/delta.tar.gz" -C "$tmp" - install -m 755 "$tmp/delta-${DELTA_VERSION}-aarch64-apple-darwin/delta" "$DELTA_BIN" + install -m 755 "$tmp/delta-${DELTA_VERSION}-${DELTA_TARGET}/delta" "$DELTA_BIN" rm -rf "$tmp" fi +# Install a pinned Google Cloud CLI archive without modifying shell profiles. +# zshrc sources the SDK's path and completion files from this location. +GCLOUD_VERSION=577.0.0 +GCLOUD_DIR="$HOME/.local/share/google-cloud-sdk" + +if ! command -v gcloud >/dev/null 2>&1 && [ ! -x "$GCLOUD_DIR/bin/gcloud" ]; then + case "$OS-$ARCH" in + Darwin-arm64) + GCLOUD_TARGET=darwin-arm + GCLOUD_SHA256=5ed8f9176eca367c6849fea10fde8e27d9a7d00a221c67f895c3020b8cf45a42 + ;; + Darwin-x86_64) + GCLOUD_TARGET=darwin-x86_64 + GCLOUD_SHA256=6e54ae17d744fc8ca12ac69886c880bd2b164141179f6a8a9d3acc53e6b5d3e8 + ;; + Linux-aarch64|Linux-arm64) + GCLOUD_TARGET=linux-arm + GCLOUD_SHA256=dbac26bdf80d72b5d13538e3a215dcbfe2781edfd2d69723effbeef3839cffb8 + ;; + Linux-x86_64) + GCLOUD_TARGET=linux-x86_64 + GCLOUD_SHA256=0b32d330446ce7b0f57f253e7efab4636c18fb1f87a3ac31c6c3f2a2a697525e + ;; + *) + echo "WARN: no Google Cloud CLI archive pinned for $OS-$ARCH — skipping installation" >&2 + GCLOUD_TARGET="" + ;; + esac + + if [ -n "$GCLOUD_TARGET" ]; then + tmp=$(mktemp -d) + GCLOUD_ARCHIVE="google-cloud-cli-${GCLOUD_VERSION}-${GCLOUD_TARGET}.tar.gz" + curl -fsSL "https://storage.googleapis.com/cloud-sdk-release/$GCLOUD_ARCHIVE" -o "$tmp/$GCLOUD_ARCHIVE" + if command -v sha256sum >/dev/null; then + echo "${GCLOUD_SHA256} $tmp/$GCLOUD_ARCHIVE" | sha256sum -c - + else + echo "${GCLOUD_SHA256} $tmp/$GCLOUD_ARCHIVE" | shasum -a 256 -c - + fi + mkdir -p "$HOME/.local/share" + tar -xzf "$tmp/$GCLOUD_ARCHIVE" -C "$HOME/.local/share" + rm -rf "$tmp" + fi +fi + # Install tmux plugin manager and plugins declared in tmux.conf clone_pinned https://github.com/tmux-plugins/tpm "$HOME/.tmux/plugins/tpm" 7bdb7ca33c9cc6440a600202b50142f401b6fe21 # v3.1.0 clone_pinned https://github.com/erikw/tmux-powerline "$HOME/.tmux/plugins/tmux-powerline" 6079ace8d534a01d4d964b8b854b223f72edaf4b # v3.2.0 -# Install Packer (nvim plugin manager) and run PackerSync -clone_pinned https://github.com/wbthomason/packer.nvim "$HOME/.local/share/nvim/site/pack/packer/start/packer.nvim" ea0cc3c59f67c440c5ff0bbe4fb9420f4350b9a3 # 2023-08-24, matches plugins.lua pin +# Install neovim on Linux from a pinned upstream tarball (macOS gets it via Brewfile). +# AL2023 doesn't ship neovim in its default dnf repos. Lands in $HOME/.local so no +# root needed, and runs before the lazy.nvim sync below so plugin setup works. +NVIM_VERSION=0.12.4 +NVIM_SHA256=012bf3fcac5ade43914df3f174668bf64d05e049a4f032a388c027b1ebd78628 +NVIM_BIN="$HOME/.local/bin/nvim" +if [ "$OS" = "Linux" ] && [ "$ARCH" = "x86_64" ] && { [ ! -x "$NVIM_BIN" ] || [ "$("$NVIM_BIN" --version | head -n 1)" != "NVIM v${NVIM_VERSION}" ]; }; then + tmp=$(mktemp -d) + curl -fsSL "https://github.com/neovim/neovim/releases/download/v${NVIM_VERSION}/nvim-linux-x86_64.tar.gz" -o "$tmp/nvim.tar.gz" + if command -v sha256sum >/dev/null; then + echo "${NVIM_SHA256} $tmp/nvim.tar.gz" | sha256sum -c - + else + echo "${NVIM_SHA256} $tmp/nvim.tar.gz" | shasum -a 256 -c - + fi + mkdir -p "$HOME/.local/share" "$HOME/.local/bin" + tar -xzf "$tmp/nvim.tar.gz" -C "$HOME/.local/share" + ln -sf "$HOME/.local/share/nvim-linux-x86_64/bin/nvim" "$NVIM_BIN" + rm -rf "$tmp" +fi -nvim --headless -c 'autocmd User PackerComplete quitall' -c 'PackerSync' || true +if [ "$OS" = "Linux" ] && [ "$ARCH" = "x86_64" ] && [ -x "$NVIM_BIN" ]; then + # Prefer the pinned nvim for plugin sync, even when the host has another version. + export PATH="$HOME/.local/bin:$PATH" +fi + +# lazy.nvim self-bootstraps (clones itself, pinned in nvim/lua/config/plugins.lua) +# on first launch. Trigger a headless sync when nvim is available. +if command -v nvim >/dev/null; then + nvim --headless "+Lazy! sync" +qa || true +fi + +# Turn off Claude Code commit/PR attribution. Appends the "attribution" key only +# if it isn't already set, so existing local settings are never overridden. +CLAUDE_SETTINGS="$HOME/.claude/settings.json" +mkdir -p "$HOME/.claude" +if [ ! -f "$CLAUDE_SETTINGS" ]; then + printf '{\n "attribution": { "commit": "", "pr": "" }\n}\n' > "$CLAUDE_SETTINGS" +elif ! jq -e 'has("attribution")' "$CLAUDE_SETTINGS" >/dev/null 2>&1; then + tmp=$(mktemp) + jq '. + {attribution: {commit: "", pr: ""}}' "$CLAUDE_SETTINGS" > "$tmp" && mv "$tmp" "$CLAUDE_SETTINGS" +fi echo "> Assimilation successful!" diff --git a/bash_profile b/bash_profile index ed0d107..cddefc9 100644 --- a/bash_profile +++ b/bash_profile @@ -1,3 +1,3 @@ -if [ -f $HOME/.bashrc ]; then - source $HOME/.bashrc +if [ -f "$HOME/.bashrc" ]; then + source "$HOME/.bashrc" fi diff --git a/bashrc b/bashrc index 795a00c..4a3f82d 100644 --- a/bashrc +++ b/bashrc @@ -1,126 +1,13 @@ # ---- # Basics -set -o vi - -export EDITOR='nvim' -export TERM='xterm-256color' -export LANG='en_US.UTF-8' +# Shared config (aliases, exports, consolidated PATH) used by both bash and zsh +[ -f "$HOME/.shared.sh" ] && . "$HOME/.shared.sh" # ---- -# Aliases - -# general -alias config="/usr/bin/git --git-dir=$HOME/.cfg/ --work-tree=$HOME" -alias vi=nvim -alias vim=nvim -alias ls="lsd -al" - -# Docker -alias d='docker' -alias dc='docker compose' -alias dcup='docker compose up' -alias dsp='docker system prune --all --force' -alias docker_rmi_dangling='docker rmi $(docker images -qa -f 'dangling=true') -f' - -# python -alias python='python3.11' -alias python3='python3.11' -alias pip='python3.11 -m pip' -alias pip3='pip' -alias grepy='grep -r --include \*.py' -alias greps='grep -r --include \*.sql' -alias jpn='jupyter notebook' -function ver { - pip list | grep $1 -} - -# git -alias g='git status' -alias gs='git status' -alias gf='git fetch' -alias gm='git merge' -alias ga='git add' -alias gb='git branch' -alias gc='git commit' -alias gck='git checkout' -alias gckb='git checkout -b' -alias gd='git diff' -alias gg='git grep' -alias gl='git log --reverse -n 10' -alias gpl='git pull' -alias gp='git push' -alias gdm='git diff master' -alias gdnm='git diff --name-only origin/master' -alias gcap='git checkout main && git pull' - -# hg -alias hs='hg status' -alias hc='hg commit' -alias ha='hg amend' -alias hd='hg diff' -alias hl='hg log' -alias hu='hg update' -alias hp='hg prev' -alias hn='hg next' -alias ht='hg top' -alias hb='hg bottom' - -# kube -alias ku='kubectl' -alias kuc='kubectl config' - -# --- -# Applications - -# nvm -export NVM_DIR="$HOME/.nvm" -[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm -[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion - -# PYTHONPATH -export PATH="$PATH:/Library/Frameworks/Python.framework/Versions/3.11/bin" +# bash-specific -# sops -export PATH="$PATH:~/Downloads/sops-3.7.2" - -# Homebrew -# Intel Path -export PATH="/usr/local/bin:${PATH}" - -# ARM/M1 Path -export PATH="/opt/homebrew/bin:${PATH}" - -# Postgres -export PATH="/opt/homebrew/Cellar/postgresql@14/14.6_1/bin:${PATH}" - -# dbt -alias docs="dbt docs generate; dbt docs serve" -alias dbtb="dbt build" -alias dbtc="dbt compile" -alias dbtr="dbt run" - -# rust -alias cb="cargo build" -alias cr="cargo run" -alias ct="cargo test" -. "$HOME/.cargo/env" - -# modal -alias md="modal deploy" -alias mr="modal run" - -# GPG -export GPG_TTY=$(tty) - -# Terraform -alias tf="terraform" +# Terraform completion if [ -n "$BASH_VERSION" ]; then complete -C /opt/homebrew/bin/terraform terraform fi - -# Set $XDG_CONFIG_HOME for Zed and Ghostty -export XDG_CONFIG_HOME="$HOME/dotfiles" - -# Claude -alias claude="$HOME/.local/bin/claude" diff --git a/checklist.md b/checklist.md index 852ce84..e6892be 100644 --- a/checklist.md +++ b/checklist.md @@ -1,5 +1,10 @@ # Checklist +> **Most setup is automated by [`assimilate.sh`](./assimilate.sh).** It symlinks +> all dotfiles and installs oh-my-zsh, Homebrew packages (`brew bundle`), Rust, +> git-delta, tmux plugin manager + plugins, and bootstraps the Neovim plugin +> manager. The steps below are the manual bits that can't be scripted. + # Mac settings - General: Automatically Hide and Show the Menu Bar -> True - Keyboard: Key Repeat -> Fast @@ -12,14 +17,13 @@ - Spotlight: Set default shortcut to `cmd + shift + space` - Alfred: Set the default shortcut to `cmd + space` - Alfred Clipboard history: enable text + image + file lists -- Terminal: set default theme to Dark Background -- Install [ohmyzsh](https://github.com/ohmyzsh/ohmyzsh) -- Install zsh plugins - - [git](https://github.com/ohmyzsh/ohmyzsh/tree/master/plugins/git) - - [zsh-autosuggestions](https://github.com/zsh-users/zsh-autosuggestions/blob/master/INSTALL.md) -- Install [Homebrew](https://brew.sh/) -- Install all packages via `brew bundle` -- Install Hack Nerd Font ([v2.3.3](https://github.com/ryanoasis/nerd-fonts/releases/tag/v2.3.3)) -- Install tmux plugin manager [tpm](https://github.com/tmux-plugins/tpm). Run `tmux source tmux.conf`. -- Install plugins with `I` -- Install [Packer](https://github.com/wbthomason/packer.nvim#quickstart) and run `PackerSync`. +- Install [Homebrew](https://brew.sh/), then run `./assimilate.sh` +- Install [Ghostty](https://ghostty.org/) (terminal; config is symlinked from `ghostty/`) +- Install [Zed](https://zed.dev/) (editor; `settings.json` + `keymap.json` symlinked from `zed/`) +- Install [Claude Code](https://docs.anthropic.com/en/docs/claude-code) (`curl -fsSL https://claude.ai/install.sh | bash`) +- In tmux, install plugins with `I` + +> The Hack Nerd Font is installed automatically via `brew bundle` +> (`cask "font-hack-nerd-font"`) — no manual version pin needed. +> oh-my-zsh, zsh plugins, tmux plugin manager (tpm), and the Neovim plugin +> manager are all handled by `assimilate.sh`. diff --git a/gitconfig b/gitconfig index 9e772a9..66ceecf 100644 --- a/gitconfig +++ b/gitconfig @@ -18,9 +18,13 @@ # is currently exposed (macOS system agent, 1Password, gpg-agent, etc.) # rather than hardcoding ~/.ssh/id_ed25519.pub. Works with hardware- # resident keys (YubiKey ed25519-sk, 1Password) where there may be no - # .pub file on disk. The `key::` prefix is what git's source explicitly - # looks for in defaultKeyCommand output. - defaultKeyCommand = sh -c 'printf "key::%s\n" "$(ssh-add -L | head -n1)"' + # .pub file on disk. git takes the first line of this command's output + # as the signing key, so a bare `ssh-add -L` is all that's needed — no + # shell wrapper. (The previous printf wrapper broke signing entirely: + # git's config parser consumes the quotes and `\n` escapes before the + # shell ever sees them, so the key from the command substitution was + # *executed* as a command and the output reduced to a bare "key::".) + defaultKeyCommand = ssh-add -L [commit] gpgsign = true diff --git a/nvim/coc-settings.json b/nvim/coc-settings.json deleted file mode 100644 index c5d03b8..0000000 --- a/nvim/coc-settings.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "suggest.noselect": false, - "python.autoComplete.showAdvancedMembers": false, - "python.jediEnabled": true, - "python.linting.mypyEnabled": true -} diff --git a/nvim/lazy-lock.json b/nvim/lazy-lock.json new file mode 100644 index 0000000..8001fbc --- /dev/null +++ b/nvim/lazy-lock.json @@ -0,0 +1,23 @@ +{ + "Comment.nvim": { "branch": "master", "commit": "e30b7f2008e52442154b66f7c519bfd2f1e32acb" }, + "LuaSnip": { "branch": "master", "commit": "0abc8f390b278c3b4aabc4c004ac8a088b65cf24" }, + "bufferline.nvim": { "branch": "main", "commit": "655133c3b4c3e5e05ec549b9f8cc2894ac6f51b3" }, + "cmp-nvim-lsp": { "branch": "main", "commit": "cbc7b02bb99fae35cb42f514762b89b5126651ef" }, + "gruvbox.nvim": { "branch": "main", "commit": "61b0b3be2f0cfd521667403a0367298144d6c165" }, + "indent-blankline.nvim": { "branch": "master", "commit": "29be0919b91fb59eca9e90690d76014233392bef" }, + "lazy.nvim": { "branch": "main", "commit": "306a05526ada86a7b30af95c5cc81ffba93fef97" }, + "lsp-zero.nvim": { "branch": "v3.x", "commit": "77550f2f6cbf0959ef1583d845661af075f3442b" }, + "lualine.nvim": { "branch": "master", "commit": "2248ef254d0a1488a72041cfb45ca9caada6d994" }, + "markdown-preview.nvim": { "branch": "master", "commit": "a923f5fc5ba36a3b17e289dc35dc17f66d0548ee" }, + "mason-lspconfig.nvim": { "branch": "main", "commit": "47059d71b42d74b0a1e9f61c1d99d301039c3b5b" }, + "mason.nvim": { "branch": "main", "commit": "2a6940af80375532e5e9e7c1f2fc6319a1b7a69d" }, + "nvim-cmp": { "branch": "main", "commit": "a1d504892f2bc56c2e79b65c6faded2fd21f3eca" }, + "nvim-lspconfig": { "branch": "master", "commit": "d224a1920728ba129880efc700d4a0180ac4ecbb" }, + "nvim-tree.lua": { "branch": "master", "commit": "5e4475d8bf7a3646164e01d9b65ef68369b17e3c" }, + "nvim-treesitter": { "branch": "main", "commit": "42fc28ba918343ebfd5565147a42a26580579482" }, + "nvim-web-devicons": { "branch": "master", "commit": "21417212f640a1dad28a1408f04468819848f5e7" }, + "plenary.nvim": { "branch": "master", "commit": "74b06c6c75e4eeb3108ec01852001636d85a932b" }, + "telescope.nvim": { "branch": "master", "commit": "c1a2af0af69e80e14e6b226d3957a064cd080805" }, + "vim-fugitive": { "branch": "master", "commit": "46eaf8918b347906789df296143117774e827616" }, + "vim-gitgutter": { "branch": "main", "commit": "fe0e8a2630eef548e4122096e4e2241f42208fe3" } +} diff --git a/nvim/lua/config/plugins.lua b/nvim/lua/config/plugins.lua index 3932def..e49c221 100644 --- a/nvim/lua/config/plugins.lua +++ b/nvim/lua/config/plugins.lua @@ -1,80 +1,96 @@ --- Automatically run :PackerCompile whenever plugins.lua is updated with an autocommand: -vim.api.nvim_create_autocmd('BufWritePost', { - group = vim.api.nvim_create_augroup('PACKER', { clear = true }), - pattern = 'plugins.lua', - command = 'source | PackerCompile', -}) +-- Leader must be set before lazy.nvim loads so that lazy `keys` specs below +-- bind against the intended leader (also set in remaps.lua). +vim.g.mapleader = " " +vim.g.maplocalleader = " " -return require('packer').startup(function(use) - -- Packer can manage itself - use { - 'wbthomason/packer.nvim', - commit = 'ea0cc3c', - } +-- Bootstrap lazy.nvim (pinned to a stable tag; it self-installs on first launch, +-- so no separate clone step is needed in assimilate.sh). +local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim" +if not (vim.uv or vim.loop).fs_stat(lazypath) then + local out = vim.fn.system({ + "git", "clone", "--filter=blob:none", + "--branch=v11.17.5", + "https://github.com/folke/lazy.nvim.git", + lazypath, + }) + if vim.v.shell_error ~= 0 then + error("Error cloning lazy.nvim:\n" .. out) + end +end +vim.opt.rtp:prepend(lazypath) - -- Devicons - -- Required by nvim-tree, lualine, and bufferline - use { +require("lazy").setup({ + -- Devicons — required by nvim-tree, lualine, and bufferline + { 'nvim-tree/nvim-web-devicons', tag = 'nerd-v3.2-compat', - } + }, -- Nvim file tree - use { + { 'nvim-tree/nvim-tree.lua', commit = '5e4475d', + dependencies = { 'nvim-tree/nvim-web-devicons' }, config = function() require("config.plugins.nvim-tree") - end - } + end, + }, - -- Markdown preview - use({ + -- Markdown preview (load only for markdown files) + { "iamcco/markdown-preview.nvim", commit = 'a923f5f', - run = function() vim.fn["mkdp#util#install"]() end, - }) + ft = "markdown", + build = function() vim.fn["mkdp#util#install"]() end, + }, -- Syntax highlighting - use { + { 'nvim-treesitter/nvim-treesitter', tag = 'v0.10.0', - run = function() + build = function() local ts_update = require('nvim-treesitter.install').update({ with_sync = true }) ts_update() end, config = function() require("config.plugins.nvim-treesitter") - end - } + end, + }, - -- Fuzzyfinder - use { + -- Fuzzyfinder (load on its keys / :Telescope) + { 'nvim-telescope/telescope.nvim', tag = '0.1.1', - requires = { { 'nvim-lua/plenary.nvim' } }, + dependencies = { 'nvim-lua/plenary.nvim' }, + cmd = 'Telescope', + keys = { '', 'pf', 'ps', 'vh' }, config = function() require("config.plugins.telescope") - end - } + end, + }, - -- Git - use { + -- Git (load on :Git / gs) + { 'tpope/vim-fugitive', commit = '46eaf89', - } + cmd = { 'Git', 'G' }, + keys = { { 'gs', 'Git', desc = 'Git status' } }, + config = function() + require("config.plugins.fugitive") + end, + }, -- LSP - use { + { 'VonHeikemen/lsp-zero.nvim', branch = 'v3.x', - requires = { + dependencies = { -- LSP Support { 'neovim/nvim-lspconfig' }, -- Required { -- Optional 'williamboman/mason.nvim', - run = function() + build = function() pcall(vim.cmd, 'MasonUpdate') end, }, @@ -87,57 +103,60 @@ return require('packer').startup(function(use) }, config = function() require("config.plugins.lsp") - end - } + end, + }, - -- Gruvbox - use { + -- Gruvbox (colorscheme — load first, before other UI plugins) + { "ellisonleao/gruvbox.nvim", tag = '2.0.0', + priority = 1000, + lazy = false, config = function() require("config.plugins.colors") - end - } + end, + }, -- GitGutter - use { + { 'airblade/vim-gitgutter', commit = 'fe0e8a2', config = function() require("config.plugins.gitgutter") - end - } + end, + }, -- Lualine - use { + { 'nvim-lualine/lualine.nvim', commit = '2248ef2', + dependencies = { 'nvim-tree/nvim-web-devicons' }, config = function() require("config.plugins.lualine") - end - } + end, + }, -- Vertical indent lines - use { + { "lukas-reineke/indent-blankline.nvim", tag = 'v3.3.7', - } + }, -- bufferline - use { + { 'akinsho/bufferline.nvim', tag = "v4.9.1", - config = function () + dependencies = { 'nvim-tree/nvim-web-devicons' }, + config = function() require("config.plugins.bufferline") - end - } + end, + }, -- Comments - -- add this to your lua/plugins.lua, lua/plugins/init.lua, or the file you keep your other plugins: - use { + { 'numToStr/Comment.nvim', config = function() require('Comment').setup() - end - } -end) + end, + }, +}) diff --git a/nvim/lua/config/plugins/nvim-treesitter.lua b/nvim/lua/config/plugins/nvim-treesitter.lua index 978f385..bd091bc 100644 --- a/nvim/lua/config/plugins/nvim-treesitter.lua +++ b/nvim/lua/config/plugins/nvim-treesitter.lua @@ -1,6 +1,6 @@ require("nvim-treesitter").setup { ensure_installed = { - "help", + "vimdoc", "lua", "javascript", "python", diff --git a/scripts/ci/run-vps-container.sh b/scripts/ci/run-vps-container.sh new file mode 100755 index 0000000..484b884 --- /dev/null +++ b/scripts/ci/run-vps-container.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash + +set -euo pipefail + +usage() { + echo "usage: $0 image|run ubuntu|amazon-linux" >&2 + exit 2 +} + +image_for() { + case "$1" in + ubuntu) + echo "ubuntu:24.04@sha256:561618e2c15bf2397621dd04f96926663a3b5616c189cf7e38db7e82f5c538ea" + ;; + amazon-linux) + echo "amazonlinux:2023@sha256:694092ae18877ed4e3cb9b643759ba95df1f12af12528fefa18f60f79d4c1568" + ;; + *) + echo "unsupported VPS target: $1" >&2 + return 2 + ;; + esac +} + +command="${1:-}" +target="${2:-}" +[ "$#" -eq 2 ] || usage + +image="$(image_for "$target")" + +if [ "$command" = image ]; then + echo "$image" + exit 0 +fi + +[ "$command" = run ] || usage + +repo_root="$(git rev-parse --show-toplevel)" + +docker run --rm --interactive \ + --platform linux/amd64 \ + --env VPS_TARGET="$target" \ + --volume "$repo_root:/workspace:ro" \ + "$image" \ + bash -s <<'CONTAINER' +set -euxo pipefail + +case "$VPS_TARGET" in + ubuntu) + export DEBIAN_FRONTEND=noninteractive + apt-get update + apt-get install --yes ca-certificates curl findutils git gzip passwd tar zsh + ;; + amazon-linux) + dnf install --assumeyes ca-certificates curl-minimal findutils git gzip shadow-utils tar util-linux zsh + ;; + *) + echo "unsupported VPS target: $VPS_TARGET" >&2 + exit 2 + ;; +esac + +useradd --create-home --shell /bin/bash dotfiles +cp -a /workspace /home/dotfiles/dotfiles +chown -R dotfiles:dotfiles /home/dotfiles/dotfiles + +runuser --user dotfiles -- env \ + HOME=/home/dotfiles \ + PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \ + bash -c ' + set -euo pipefail + cd "$HOME/dotfiles" + + ./assimilate.sh + DOTFILES="$HOME/dotfiles" scripts/ci/verify-assimilate.sh + backup_count="$(find "$HOME/backups" -mindepth 1 -maxdepth 1 | wc -l | tr -d " ")" + + ./assimilate.sh + DOTFILES="$HOME/dotfiles" scripts/ci/verify-assimilate.sh + test "$(find "$HOME/backups" -mindepth 1 -maxdepth 1 | wc -l | tr -d " ")" = "$backup_count" + ' +CONTAINER diff --git a/scripts/ci/test-vps-container.sh b/scripts/ci/test-vps-container.sh new file mode 100755 index 0000000..8ed261a --- /dev/null +++ b/scripts/ci/test-vps-container.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash + +set -euo pipefail + +runner="scripts/ci/run-vps-container.sh" + +test -x "$runner" +test "$("$runner" image ubuntu)" = "ubuntu:24.04@sha256:561618e2c15bf2397621dd04f96926663a3b5616c189cf7e38db7e82f5c538ea" +test "$("$runner" image amazon-linux)" = "amazonlinux:2023@sha256:694092ae18877ed4e3cb9b643759ba95df1f12af12528fefa18f60f79d4c1568" +grep -Fq -- '--interactive' "$runner" +grep -Fq 'curl-minimal' "$runner" + +grep -Fq 'set -euxo pipefail' assimilate.sh +grep -Fq '28d1352dbcb436d3111c3594b9e1588e94950464' assimilate.sh +grep -Fq 'NVIM_VERSION=0.12.4' assimilate.sh +grep -Fq 'set -euxo pipefail' scripts/ci/verify-assimilate.sh +grep -Fq "bash --noprofile -ic 'test \"\$UV_MALWARE_CHECK\" = 1'" scripts/ci/verify-assimilate.sh +grep -Fq "zsh -ic 'test \"\$UV_MALWARE_CHECK\" = 1'" scripts/ci/verify-assimilate.sh +if grep -Fq -- '--norc' scripts/ci/verify-assimilate.sh; then + echo "VPS shell verification must not disable .bashrc" >&2 + exit 1 +fi + +if "$runner" image alpine >/dev/null 2>&1; then + echo "unsupported VPS target unexpectedly succeeded" >&2 + exit 1 +fi diff --git a/scripts/ci/verify-assimilate.sh b/scripts/ci/verify-assimilate.sh new file mode 100755 index 0000000..610d9e1 --- /dev/null +++ b/scripts/ci/verify-assimilate.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash + +set -euxo pipefail + +: "${DOTFILES:?DOTFILES must point to the checked-out repository}" +export PATH="$HOME/.local/bin:$PATH" + +assert_link() { + local source="$DOTFILES/$1" + local target="$HOME/$2" + + test -L "$target" + test "$(readlink "$target")" = "$source" +} + +assert_link bash_profile .bash_profile +assert_link gitconfig .gitconfig +assert_link tmux.conf .tmux.conf +assert_link zshrc .zshrc +assert_link nvim .config/nvim +assert_link tmux-powerline/config.sh .config/tmux-powerline/config.sh +assert_link tmux-powerline/themes/theme.sh .config/tmux-powerline/themes/theme.sh +assert_link claude/statusline.sh .claude/statusline.sh + +if [ "$(uname -s)" = Darwin ]; then + assert_link bashrc .bashrc + assert_link hammerspoon .hammerspoon + assert_link vscode/code_settings.json .vscode/settings.json + assert_link zed/settings.json .config/zed/settings.json + assert_link zed/keymap.json .config/zed/keymap.json + assert_link ghostty/config 'Library/Application Support/com.mitchellh.ghostty/config' +else + test ! -L "$HOME/.bashrc" + grep -Fq 'dotfiles/bashrc' "$HOME/.bashrc" +fi + +HOME="$HOME" bash --noprofile -ic 'test "$UV_MALWARE_CHECK" = 1' >/dev/null 2>&1 +HOME="$HOME" zsh -ic 'test "$UV_MALWARE_CHECK" = 1' >/dev/null 2>&1 +test "$("$HOME/.local/bin/delta" --version)" = "delta 0.19.2" +test -x "$HOME/.cargo/bin/rustc" +test -d "$HOME/.oh-my-zsh/.git" +test -d "$HOME/.tmux/plugins/tpm/.git" +test -d "$HOME/.tmux/plugins/tmux-powerline/.git" +test -d "$HOME/.local/share/nvim/site/pack/packer/start/packer.nvim/.git" + +nvim_log="$(mktemp)" +if ! HOME="$HOME" nvim --headless -c 'quitall' >"$nvim_log" 2>&1; then + cat "$nvim_log" >&2 + rm -f "$nvim_log" + exit 1 +fi +if grep -Fq 'Error detected while processing' "$nvim_log"; then + cat "$nvim_log" >&2 + rm -f "$nvim_log" + exit 1 +fi +rm -f "$nvim_log" + +git -C "$DOTFILES" diff --exit-code +git -C "$DOTFILES" diff --cached --exit-code diff --git a/shared.sh b/shared.sh new file mode 100644 index 0000000..5ef6018 --- /dev/null +++ b/shared.sh @@ -0,0 +1,112 @@ +# shared.sh — configuration common to both bash and zsh. +# +# Sourced from both bashrc and zshrc (via ~/.shared.sh, symlinked by assimilate.sh) +# so there is a single source of truth for aliases, exports, and PATH. +# Keep this POSIX-sh compatible — it must parse under both shells. + +# ---- +# Exports + +export LANG='en_US.UTF-8' +export GPG_TTY=$(tty) +export UV_MALWARE_CHECK=1 +# $XDG_CONFIG_HOME drives Zed and Ghostty config discovery +export XDG_CONFIG_HOME="$HOME/dotfiles" + +# ---- +# PATH +# +# Single consolidated block with a dedup helper. Prepending through this guard +# keeps PATH from ballooning with duplicates in nested shells (e.g. tmux), where +# the rc files are re-sourced. Last prepend wins (ends up first on PATH), so +# pinned tools in ~/.local/bin take priority over brew bottles with ABI drift. + +_prepend_path() { + case ":$PATH:" in + *":$1:"*) ;; # already present — skip + *) PATH="$1:$PATH" ;; + esac +} + +_prepend_path "/usr/local/bin" # Homebrew (Intel) +_prepend_path "/opt/homebrew/bin" # Homebrew (ARM/M1) +# Postgres — guarded so shells without Homebrew (e.g. the Linux dev box) don't +# error on every startup +if command -v brew >/dev/null 2>&1; then + _prepend_path "$(brew --prefix postgresql@17)/bin" +fi +_prepend_path "$HOME/.yarn/bin" # Yarn global bins +_prepend_path "$HOME/.config/yarn/global/node_modules/.bin" +export BUN_INSTALL="$HOME/.bun" +_prepend_path "$BUN_INSTALL/bin" # Bun +_prepend_path "$HOME/.local/bin" # pinned tools (delta, claude, uv) +export PATH + +# ---- +# Aliases + +# general +alias vi=nvim +alias vim=nvim +command -v lsd >/dev/null 2>&1 && alias ls="lsd -al" +alias claude="$HOME/.local/bin/claude" + +# Docker +alias d='docker' +alias dc='docker compose' +alias dcup='docker compose up' +alias dsp='docker system prune --all --force' +alias docker_rmi_dangling='docker rmi $(docker images -qa -f "dangling=true") -f' + +# python +alias python='python3.11' +alias python3='python3.11' +alias pip='python3.11 -m pip' +alias pip3='pip' +alias grepy='grep -r --include \*.py' +alias greps='grep -r --include \*.sql' +alias jpn='jupyter notebook' +ver() { + pip list | grep "$1" +} + +# git +alias g='git status' +alias gs='git status' +alias gf='git fetch' +alias gm='git merge' +alias ga='git add' +alias gb='git branch' +alias gc='git commit' +alias gck='git checkout' +alias gckb='git checkout -b' +alias gd='git diff' +alias gg='git grep' +alias gl='git log --reverse -n 10' +alias gpl='git pull' +alias gp='git push' +alias gdm='git diff main' +alias gdnm='git diff --name-only origin/main' +alias gcap='git checkout main && git pull' + +# kube +alias ku='kubectl' +alias kuc='kubectl config' + +# dbt +alias docs="dbt docs generate; dbt docs serve" +alias dbtb="dbt build" +alias dbtc="dbt compile" +alias dbtr="dbt run" + +# rust +alias cb="cargo build" +alias cr="cargo run" +alias ct="cargo test" + +# modal +alias md="modal deploy" +alias mr="modal run" + +# terraform +alias tf="terraform" diff --git a/ssh/config b/ssh/config new file mode 100644 index 0000000..f2a4dc7 --- /dev/null +++ b/ssh/config @@ -0,0 +1,29 @@ +# SSH client configuration (symlinked to ~/.ssh/config by assimilate.sh) + +# Machine-local hosts live in ~/.ssh/config.local (not tracked — this repo is +# public). ssh silently skips a missing Include, and entries there are read +# first so they take precedence for their hosts. +Include config.local + +Host * + # Hash hostnames in known_hosts so a leaked file doesn't reveal every + # server you've connected to. + HashKnownHosts yes + # Store/read key passphrases from the macOS keychain. IgnoreUnknown keeps + # Linux ssh (which has no UseKeychain option) from erroring on it. + IgnoreUnknown UseKeychain + UseKeychain yes + # Load a key into the agent on first use so you aren't re-prompted. + AddKeysToAgent yes + # Only offer keys explicitly configured for a host — don't spray every + # loaded key at every server (avoids leaking fingerprints / auth failures). + IdentitiesOnly yes + # Show an ASCII-art fingerprint on connect to help spot MITM on first use. + VisualHostKey yes + # Never silently auto-accept unknown host keys. + StrictHostKeyChecking ask + +Host github.com + HostName github.com + User git + IdentityFile ~/.ssh/id_ed25519 diff --git a/uv/uv.toml b/uv/uv.toml new file mode 100644 index 0000000..d7f534b --- /dev/null +++ b/uv/uv.toml @@ -0,0 +1,16 @@ +# uv configuration — https://docs.astral.sh/uv/reference/settings/ +# +# Symlinked to ~/.config/uv/uv.toml by assimilate.sh (and also discovered via +# $XDG_CONFIG_HOME, which points at this repo). Applies to every uv invocation: +# uv lock / uv sync / uv pip install / uv run. + +# Supply-chain hardening: never resolve a package release younger than 7 days. +# This is a *rolling* window (a duration, not a fixed date) that moves with the +# clock — most malicious/compromised uploads are detected and yanked within a +# week, so waiting buys time without much practical cost. +exclude-newer = "7 days" + +# Let uv own Python version management: prefer uv-managed CPython interpreters +# over system/pyenv ones. Install versions with `uv python install ` and +# pin per-project with a `.python-version` file. +python-preference = "managed" diff --git a/zed/keymap.json b/zed/keymap.json index 2606a56..50e0967 100644 --- a/zed/keymap.json +++ b/zed/keymap.json @@ -7,8 +7,5 @@ "ctrl-a j": "workspace::ActivatePaneDown", "ctrl-a k": "workspace::ActivatePaneUp" } - }, - { - "context": "Editor && vim_mode == insert" } ] diff --git a/zed/settings.json b/zed/settings.json index b46a8f0..f3a1182 100644 --- a/zed/settings.json +++ b/zed/settings.json @@ -1,4 +1,7 @@ { + "project_panel": { + "dock": "left" + }, "cursor_blink": false, "terminal": { "cursor_shape": "bar" diff --git a/zshrc b/zshrc index b4cbca2..93f3b53 100644 --- a/zshrc +++ b/zshrc @@ -1,5 +1,6 @@ # Basics -source ~/.bashrc +# Shared config (aliases, exports, consolidated PATH) used by both bash and zsh +[ -f "$HOME/.shared.sh" ] && . "$HOME/.shared.sh" export ZSH="$HOME/.oh-my-zsh" export UPDATE_ZSH_DAYS=13 @@ -25,9 +26,9 @@ HIST_STAMPS="mm/dd/yyyy" COMPLETION_WAITING_DOTS="false" # Plugins -plugins=( - git -) +# (OMZ git plugin removed — it loads after the custom git aliases above and was +# shadowing them, e.g. its gl='git pull' clobbered gl='git log --reverse -n 10') +plugins=() bindkey -e bindkey "^[begin" backward-word @@ -46,27 +47,24 @@ else export EDITOR='nvim' fi -export PATH="$HOME/.yarn/bin:$HOME/.config/yarn/global/node_modules/.bin:$PATH" -source $(brew --prefix)/share/zsh-autosuggestions/zsh-autosuggestions.zsh +# zsh-autosuggestions (installed via brew on macOS, may be missing elsewhere) +if command -v brew >/dev/null 2>&1; then + _zsh_autosug="$(brew --prefix)/share/zsh-autosuggestions/zsh-autosuggestions.zsh" + [ -f "$_zsh_autosug" ] && source "$_zsh_autosug" + unset _zsh_autosug +fi export NVM_DIR="$HOME/.nvm" -[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm +[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" --no-use # Load nvm without auto-use (auto-use errors with "N/A" when no .nvmrc is set) [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion -. "$HOME/.cargo/env" +[ -f "$HOME/.cargo/env" ] && . "$HOME/.cargo/env" alias snowsql=/Applications/SnowSQL.app/Contents/MacOS/snowsql -# The next line updates PATH for the Google Cloud SDK. -if [ -f "$HOME/Downloads/google-cloud-sdk/path.zsh.inc" ]; then . "$HOME/Downloads/google-cloud-sdk/path.zsh.inc"; fi - -# The next line enables shell command completion for gcloud. -if [ -f "$HOME/Downloads/google-cloud-sdk/completion.zsh.inc" ]; then . "$HOME/Downloads/google-cloud-sdk/completion.zsh.inc"; fi - -# .zsh syntax highlighting -source /opt/homebrew/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh - -# Prepend so pinned overrides in assimilate.sh (e.g. delta) win over brew bottles with ABI drift -export PATH="$HOME/.local/bin:$PATH" +# .zsh syntax highlighting (installed via brew on macOS, may be missing elsewhere) +if [ -f /opt/homebrew/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh ]; then + source /opt/homebrew/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh +fi autoload -U +X bashcompinit && bashcompinit complete -o nospace -C /opt/homebrew/bin/terraform terraform @@ -80,9 +78,16 @@ _reset_cursor() { printf '\e[6 q' } precmd_functions+=(_reset_cursor) -# bun completions +# bun completions ($BUN_INSTALL and its PATH entry live in shared.sh) [ -s "$HOME/.bun/_bun" ] && source "$HOME/.bun/_bun" -# bun -export BUN_INSTALL="$HOME/.bun" -export PATH="$BUN_INSTALL/bin:$PATH" +# Load the Google Cloud SDK installed by assimilate.sh, or an existing SDK. +_gcloud_sdk_root="$HOME/.local/share/google-cloud-sdk" +if [ ! -d "$_gcloud_sdk_root" ] && (( $+commands[gcloud] )); then + _gcloud_sdk_root="${commands[gcloud]:A:h:h}" +elif [ ! -d "$_gcloud_sdk_root" ] && [ -d "$HOME/work/dev/google-cloud-sdk" ]; then + _gcloud_sdk_root="$HOME/work/dev/google-cloud-sdk" +fi +[ -f "$_gcloud_sdk_root/path.zsh.inc" ] && source "$_gcloud_sdk_root/path.zsh.inc" +[ -f "$_gcloud_sdk_root/completion.zsh.inc" ] && source "$_gcloud_sdk_root/completion.zsh.inc" +unset _gcloud_sdk_root