Rebuild dotfiles as one branch with per-host layers
Replaces the previous repo, which had split into two histories that never met (mainframe on a dead GitLab remote, the laptops on Gitea) with 146 dirty files across three machines and the NAS never enrolled at all. Branch-per-machine is gone. One main, with host differences expressed as small files under hosts/<hostname>/ rather than as branches, so there is nothing to merge. The reconciled zsh layer reduces 15-33 line forks to 1-7 effective lines per host; distro differences (oh-my-zsh prefix, syntax-highlighting path, fd vs fdfind) are probed in common/ instead. Fresh history: the old one carried six plaintext credentials, 45 MB of mail caches, browser caches and vendored binaries. 5,096 tracked files and 144 MB become 462 files and 2.6 MB. The .gitignore is now an allowlist, which is what keeps that true. Root cause of the rot: ~/.local/bin was a symlink to scripts/ with GOPATH inside it, so every go install wrote into version control (2.2 GB on the work laptop). PATH now points at the repo instead of the reverse. Also: Hyprland replaces sway and is sourced in two halves so $browser is defined before use; singleton automations carry ConditionHost= alongside host-layer-only placement; ddns moves from cron to a guarded timer; package manifests and pkg-snapshot/pkg-restore replace the X11-era install_scripts/; networkmanager-dmenu added to system76 (the binding always existed, the package never did).
This commit is contained in:
commit
e6644d0616
462 files changed
+23524
No files matched your search
@@ -0,0 +1,15 @@
|
||||
alias rm="rm -I"
|
||||
alias szsh="source ~/.zshrc"
|
||||
alias p="paru"
|
||||
alias ls="exa"
|
||||
alias la="ls -la"
|
||||
alias grep="rg"
|
||||
alias ssh="TERM=xterm-256color ssh"
|
||||
alias sshome="TERM=xterm-256color ssh -Y connor@home.johnstone.software"
|
||||
alias rofi="wofi"
|
||||
alias icat="kitten icat"
|
||||
|
||||
# Guarded rather than split per host: present on the laptops, absent elsewhere.
|
||||
command -v advcp >/dev/null 2>&1 && alias cp="advcp -g"
|
||||
command -v advmv >/dev/null 2>&1 && alias mv="advmv -g"
|
||||
command -v fdfind >/dev/null 2>&1 && alias fd="fdfind"
|
||||
@@ -0,0 +1,162 @@
|
||||
# ---------------------------------------------------------------------------
|
||||
# OpenCode profile launchers (Phase 5)
|
||||
#
|
||||
# Four profiles, one LiteLLM proxy. The profile decides WHICH KEY is used, and
|
||||
# the key decides which models are reachable -- enforcement lives in LiteLLM
|
||||
# (Phase 4 access groups), not here. This file only picks a key and a config.
|
||||
#
|
||||
# opencode / oc personal anthropic + openai + local
|
||||
# oc-work work openai + local NO anthropic
|
||||
# oc-work-strict work-strict openai + local NO anthropic, PRC never
|
||||
# oc-local local local only, no egress
|
||||
#
|
||||
# NO SECRET IS STORED HERE. Keys are read from rbw at launch and passed to the
|
||||
# process as LITELLM_KEY, which the config files reference as {env:LITELLM_KEY}.
|
||||
# Nothing is written to a project repo -- there is deliberately no
|
||||
# opencode.json or .envrc anywhere but ~/.config/opencode.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Install prefix is ~/.local/opt/opencode, NOT ~/.local/share/opencode --
|
||||
# the latter is opencode's OWN data directory (opencode.db, log/, repos/) and
|
||||
# `opencode uninstall` claims to remove all related files there. Keeping the
|
||||
# binary out of it means an uninstall or a reinstall cannot take the session
|
||||
# database with it. Also NOT ~/.local/bin: that is a symlink to
|
||||
# ~/.dotfiles/scripts, so npm's relative bin symlink resolved to
|
||||
# ~/.dotfiles/lib/... (broken) and wrote its entry into the dotfiles git repo.
|
||||
export OPENCODE_BIN="$HOME/.local/opt/opencode/bin"
|
||||
[[ ":$PATH:" == *":$OPENCODE_BIN:"* ]] || path=("$OPENCODE_BIN" $path)
|
||||
|
||||
_oc_launch() {
|
||||
local profile="$1" entry="$2" cfg="$3"; shift 3
|
||||
local key
|
||||
|
||||
# rbw must be UNLOCKED, and the entry must be non-empty. Both matter: rbw
|
||||
# needs a TTY to prompt, and a missing entry returns empty rather than
|
||||
# failing, which would surface later as an opaque 401 from the proxy.
|
||||
if ! rbw unlocked >/dev/null 2>&1; then
|
||||
print -u2 "oc: rbw is locked -- run 'rbw unlock' first."
|
||||
return 1
|
||||
fi
|
||||
key=$(rbw get "$entry" 2>/dev/null)
|
||||
if [[ -z "$key" ]]; then
|
||||
print -u2 "oc: rbw entry '$entry' is missing or empty."
|
||||
print -u2 " Populate it with /tmp/store-litellm-keys-rbw.sh (needs a TTY)."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# ---- where the proxy lives -------------------------------------------
|
||||
# Default is mainframe talking to itself. Any other machine (the laptop)
|
||||
# exports LITELLM_BASE=https://llm.rcjohnstone.com before calling these.
|
||||
# That name is NOT public: no A record, internal CA, and Caddy's (internal)
|
||||
# snippet source-IP gates it to 10.0.0.0/24, 10.0.1.0/24 and the podman
|
||||
# subnets. The router's DNS override resolves it to 10.0.0.2, so on the LAN
|
||||
# the traffic never leaves the network and the gate passes.
|
||||
#
|
||||
# The opencode profiles read this as {env:LITELLM_BASE}, so it MUST be
|
||||
# exported before opencode starts -- there is no default inside the JSON and
|
||||
# an unset value interpolates to empty, which fails as a confusing URL error
|
||||
# rather than a missing-variable one.
|
||||
: ${LITELLM_BASE:=http://127.0.0.1:4000}
|
||||
export LITELLM_BASE
|
||||
|
||||
# Preflight the proxy. Without this the first request fails somewhere inside
|
||||
# the TUI and reads like a model problem instead of a dead backend.
|
||||
if ! curl -fsS -m 3 -o /dev/null "$LITELLM_BASE/health/liveliness" 2>/dev/null; then
|
||||
print -u2 "oc: LiteLLM proxy is not answering on $LITELLM_BASE."
|
||||
if [[ "$LITELLM_BASE" == *127.0.0.1* ]]; then
|
||||
print -u2 " Try: sudo podman restart connor_litellm_1"
|
||||
else
|
||||
print -u2 " Check: on the LAN? DNS resolving to 10.0.0.2? Caddy's"
|
||||
print -u2 " internal CA trusted by this machine? (a TLS failure looks"
|
||||
print -u2 " identical to a dead backend here)"
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
|
||||
# ---- install the profile's OMO roster --------------------------------
|
||||
# WHY THIS EXISTS. OMO reads exactly ONE config, resolveHomeDir($HOME) +
|
||||
# "/.omo/omo.jsonc". There is no path override: `profiles`/OMO_PROFILE are in
|
||||
# the new schema but the OLD loader rejects them, and a config the loaders
|
||||
# reject fails SILENTLY (agent model bindings just stop applying). Setting
|
||||
# HOME per profile would relocate opencode's own data and cache dirs too.
|
||||
#
|
||||
# It matters because OMO picks its per-model prompt by string-matching the
|
||||
# LAST PATH SEGMENT of the model name, so the alias has to name the model
|
||||
# family (see guides/llm/robots.md 5.1). Personal runs Kimi on the thinking
|
||||
# and worker roles; work may not (PRC exclusion). One shared alias name
|
||||
# therefore cannot be correct for both -- work was left running Kimi prompts
|
||||
# on GPT models, 6 of 11 agents mismatched.
|
||||
#
|
||||
# The opencode-side fix does NOT work and was tested before this was written:
|
||||
# OMO builds protectedBuiltinAgentNames from its own agent keys and runs
|
||||
# filterProtectedAgentOverrides over opencode's `agent` config, so an
|
||||
# `agent.sisyphus.model` in a profile config is silently DROPPED.
|
||||
#
|
||||
# So the roster is swapped here instead. Only omo.jsonc is touched;
|
||||
# codegraph/, lsp-daemon/ and runtime/ under ~/.omo stay shared.
|
||||
#
|
||||
# RACE: two sessions in different profiles started within the same instant
|
||||
# could read the other's roster. The window is startup-only (OMO reads the
|
||||
# file once), and the failure is BOUNDED AND LOUD -- the LiteLLM virtual key,
|
||||
# not this file, is the policy boundary, so a work session that ends up with
|
||||
# the personal roster gets a 403 on the Kimi models rather than using them.
|
||||
local roster
|
||||
case "$profile" in
|
||||
personal) roster=personal ;;
|
||||
work|work-strict) roster=work ;;
|
||||
local) roster= ;; # local.json loads no OMO plugin
|
||||
esac
|
||||
if [[ -n "$roster" ]]; then
|
||||
local src="$HOME/.omo/profiles/${roster}.jsonc"
|
||||
if [[ ! -r "$src" ]]; then
|
||||
print -u2 "oc: OMO roster '$src' is missing."
|
||||
print -u2 " Refusing to launch -- the wrong roster silently mis-prompts every agent."
|
||||
return 1
|
||||
fi
|
||||
# Atomic: never leave a half-written omo.jsonc if this is interrupted.
|
||||
if ! { cp -- "$src" "$HOME/.omo/.omo.jsonc.$$" &&
|
||||
mv -f -- "$HOME/.omo/.omo.jsonc.$$" "$HOME/.omo/omo.jsonc" }; then
|
||||
rm -f -- "$HOME/.omo/.omo.jsonc.$$"
|
||||
print -u2 "oc: could not install the $roster OMO roster."
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Make the active mode obvious: banner, terminal title, and a prompt marker
|
||||
# that survives for the rest of the shell if the profile is exported.
|
||||
local colour
|
||||
case "$profile" in
|
||||
personal) colour=$'\e[1;36m' ;; # cyan
|
||||
work) colour=$'\e[1;33m' ;; # yellow
|
||||
work-strict) colour=$'\e[1;31m' ;; # red
|
||||
local) colour=$'\e[1;32m' ;; # green
|
||||
esac
|
||||
print -u2 "${colour}┌─ opencode profile: ${(U)profile} ─┐\e[0m"
|
||||
printf '\e]0;opencode [%s]\a' "$profile"
|
||||
|
||||
LITELLM_KEY="$key" \
|
||||
OPENCODE_PROFILE="$profile" \
|
||||
OPENCODE_CONFIG="$cfg" \
|
||||
command opencode "$@"
|
||||
}
|
||||
|
||||
# NO PROFILE IS NAMED opencode.json, AND THAT IS LOAD-BEARING. Whatever sits at
|
||||
# ~/.config/opencode/opencode.json is a GLOBAL BASE that opencode merges into
|
||||
# every other config -- OPENCODE_CONFIG adds to it, it does not replace it, and
|
||||
# neither does OPENCODE_CONFIG_DIR. Verified: with the personal profile parked
|
||||
# at that path, `oc-local models` listed the Anthropic models too. So personal
|
||||
# lives at personal.json like every other profile, and the file at the default
|
||||
# path is a two-line guard holding only `enabled_providers: ["litellm"]`.
|
||||
#
|
||||
# That guard is a safety net, not decoration: without it a bare `opencode` run
|
||||
# outside these functions falls back to opencode's BUILT-IN providers, which
|
||||
# include OpenCode Zen -- the account currently under a ToS block. With it,
|
||||
# a bare `opencode` lists nothing at all and cannot reach Zen.
|
||||
#
|
||||
# Personal is still wrapped under the bare name so `opencode` does the
|
||||
# expected thing and gets a key.
|
||||
opencode() { _oc_launch personal litellm-key-personal "$HOME/.config/opencode/personal.json" "$@" }
|
||||
oc() { _oc_launch personal litellm-key-personal "$HOME/.config/opencode/personal.json" "$@" }
|
||||
oc-work() { _oc_launch work litellm-key-work "$HOME/.config/opencode/work.json" "$@" }
|
||||
oc-work-strict() { _oc_launch work-strict litellm-key-work-strict "$HOME/.config/opencode/work-strict.json" "$@" }
|
||||
oc-local() { _oc_launch local litellm-key-local "$HOME/.config/opencode/local.json" "$@" }
|
||||
@@ -0,0 +1,12 @@
|
||||
# Copy to ~/.secrets.zsh and fill in. chmod 600. Never committed.
|
||||
# Sourced at the end of zshenv, so non-interactive shells get these too.
|
||||
#
|
||||
# Back up edits by hand as ~/.secrets.zsh.pre-<thing>, matching the convention
|
||||
# used across ~/config.
|
||||
|
||||
export LASTFM_API_KEY=""
|
||||
export LASTFM_API_SECRET=""
|
||||
|
||||
# Read by the opencode profiles as {env:LITELLM_KEY}. opencode has no fallback
|
||||
# syntax, so an unset value becomes an empty string and every request 401s.
|
||||
export LITELLM_KEY=""
|
||||
@@ -0,0 +1,51 @@
|
||||
# Read by EVERY zsh, including non-interactive ones (scripts, `ssh host cmd`).
|
||||
# Keep it cheap: no subshells in the hot path, no vault lookups, no network.
|
||||
|
||||
export DOTFILES="${DOTFILES:-$HOME/.dotfiles}"
|
||||
: ${DOTFILES_HOST:=${(L)${HOST:-$(hostname)}}} # lowercased; SCTFW004 -> sctfw004
|
||||
export DOTFILES_HOST
|
||||
|
||||
export EDITOR="nvim"
|
||||
export BROWSER="qutebrowser"
|
||||
export GTK_THEME="Arc:dark"
|
||||
export XDG_DOWNLOAD_DIR="$HOME/downloads"
|
||||
export QUTEWAL_DYNAMIC_LOADING=True
|
||||
export NEOVIDE_FORK=1
|
||||
export RUSTFLAGS="-L/usr/local/lib"
|
||||
|
||||
# `col -bx` strips the backspace-overstrike bold that otherwise reaches bat as
|
||||
# literal ^H sequences. MANROFFOPT=-c is required alongside it.
|
||||
export MANPAGER="sh -c 'col -bx | bat -l man -p'"
|
||||
export MANROFFOPT="-c"
|
||||
|
||||
# NOT $HOME/.local/bin. That used to be a symlink to this repo's scripts/, so
|
||||
# `go install` wrote 1.6 GB into version control. See README.
|
||||
export GOPATH="$HOME/.local/share/go"
|
||||
export ANDROID_HOME="$HOME/Android/Sdk"
|
||||
|
||||
# opencode's {env:} interpolation has no fallback syntax: an unset value becomes
|
||||
# an empty string and every request fails with an opaque URL parse error. Set
|
||||
# here rather than in .zshrc so non-interactive shells resolve it too.
|
||||
export LITELLM_BASE="${LITELLM_BASE:-http://127.0.0.1:4000}"
|
||||
|
||||
# Repo scripts come first; host-specific ones shadow the shared ones.
|
||||
path=(
|
||||
"$DOTFILES/hosts/$DOTFILES_HOST/bin"
|
||||
"$DOTFILES/common/bin"
|
||||
"$HOME/.local/bin"
|
||||
"$GOPATH/bin"
|
||||
"$HOME/.cargo/bin"
|
||||
/usr/lib/jvm/default/bin
|
||||
$path
|
||||
)
|
||||
export PATH
|
||||
|
||||
[[ -r "$HOME/.local/share/bob/env/env.sh" ]] && . "$HOME/.local/share/bob/env/env.sh"
|
||||
|
||||
for _f in "$DOTFILES/hosts/$DOTFILES_HOST"/zsh/*.zshenv(N); do source "$_f"; done
|
||||
unset _f
|
||||
|
||||
# Untracked, mode 600. See common/zsh/secrets.zsh.example.
|
||||
[[ -r "$HOME/.secrets.zsh" ]] && source "$HOME/.secrets.zsh"
|
||||
|
||||
true
|
||||
@@ -0,0 +1,47 @@
|
||||
# Interactive shells only.
|
||||
|
||||
# Arch packages oh-my-zsh system-wide; Debian/Ubuntu installs it per-user.
|
||||
for _d in /usr/share/oh-my-zsh "$HOME/.oh-my-zsh"; do
|
||||
[[ -d $_d ]] && { export ZSH=$_d; break }
|
||||
done
|
||||
unset _d
|
||||
|
||||
ZSH_THEME="avit"
|
||||
HYPHEN_INSENSITIVE="true"
|
||||
DISABLE_UPDATE_PROMPT="true"
|
||||
ENABLE_CORRECTION="true"
|
||||
COMPLETION_WAITING_DOTS="true"
|
||||
plugins=(git)
|
||||
[[ -n $ZSH && -r $ZSH/oh-my-zsh.sh ]] && source $ZSH/oh-my-zsh.sh
|
||||
|
||||
fpath+=~/.zsh
|
||||
|
||||
unsetopt correct_all
|
||||
unsetopt HIST_VERIFY
|
||||
unsetopt multios
|
||||
|
||||
# Arch nests this under plugins/; Debian/Ubuntu does not.
|
||||
for _d in \
|
||||
/usr/share/zsh/plugins/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh \
|
||||
/usr/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh
|
||||
do
|
||||
[[ -r $_d ]] && { source $_d; break }
|
||||
done
|
||||
unset _d
|
||||
|
||||
source "$DOTFILES/common/zsh/aliases.zsh"
|
||||
|
||||
for _f in "$DOTFILES/hosts/$DOTFILES_HOST"/zsh/*.zsh(N); do source "$_f"; done
|
||||
unset _f
|
||||
|
||||
command -v pyenv >/dev/null 2>&1 && eval "$(pyenv init -)"
|
||||
|
||||
# zoxide's cd override confuses Claude Code's shell snapshotting.
|
||||
[[ "$CLAUDECODE" != "1" ]] && command -v zoxide >/dev/null 2>&1 && eval "$(zoxide init --cmd cd zsh)"
|
||||
|
||||
command -v starship >/dev/null 2>&1 && eval "$(starship init zsh)"
|
||||
|
||||
source "$DOTFILES/common/zsh/opencode.zsh"
|
||||
|
||||
# Nudge when the repo has drifted. Silent when clean, so it means something.
|
||||
command -v dots >/dev/null 2>&1 && dots --nag
|
||||
Reference in new issue
Block a user