op item edit can exit non-zero with "(404) Not Found" after an edit it did save (seen with op 2.34.1 and 2.39.0 on an existing item), which made opc report failure and skip the ops refresh. opc now reads the field back and opd re-checks the item's labels, then prints either the op:// reference it changed or what failed, op's message without its timestamp prefix, and where to look next. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
164 lines
6.6 KiB
Bash
164 lines
6.6 KiB
Bash
# Managed by dotfiles/zsh/install.sh — edits made directly to ~/.zshrc will be
|
|
# overwritten on the next bootstrap run. Edit zsh/zshrc in the dotfiles repo
|
|
# instead.
|
|
|
|
# Userland tools installed by dotfiles/bin/install.sh live here.
|
|
case ":$PATH:" in
|
|
*":$HOME/.local/bin:"*) ;;
|
|
*) export PATH="$HOME/.local/bin:$PATH" ;;
|
|
esac
|
|
|
|
export ZSH="$HOME/.oh-my-zsh"
|
|
|
|
ZSH_THEME="robbyrussell"
|
|
|
|
plugins=(git)
|
|
|
|
source "$ZSH/oh-my-zsh.sh"
|
|
|
|
HISTSIZE=10000
|
|
SAVEHIST=10000
|
|
setopt SHARE_HISTORY
|
|
setopt HIST_IGNORE_DUPS
|
|
|
|
# Delete remote branches without fighting tab-completion.
|
|
#
|
|
# `git push origin --delete <TAB>` completes to `origin/foo`, but `origin/foo`
|
|
# is a LOCAL remote-tracking name — the ref on the server is just `foo`. So the
|
|
# command zsh completes for you is the one git rejects, with "remote ref does
|
|
# not exist", which reads like the branch is missing rather than misnamed.
|
|
#
|
|
# grmb takes either form. It fetches with --prune first so branches already
|
|
# deleted on the server stop being offered at all (that is the other half of
|
|
# this annoyance, and `fetch.prune = true` from dotfiles/git/install.sh makes it
|
|
# automatic everywhere else).
|
|
#
|
|
# Named grmb, not gbd: oh-my-zsh's git plugin already binds gbd to
|
|
# `git branch --delete`, aliases resolve before functions, and the collision
|
|
# fails in the worst possible way — `gbd origin/foo` would quietly try to
|
|
# delete a LOCAL branch instead of the remote one. Check with `type <name>`
|
|
# before adding any g* helper here.
|
|
#
|
|
# grmb origin/my-branch # completed form, works
|
|
# grmb my-branch other # bare form, several at once
|
|
grmb() {
|
|
emulate -L zsh
|
|
local remote=origin b rc=0
|
|
if [ "$#" -eq 0 ]; then
|
|
print -u2 "grmb: usage: grmb <branch>... (origin/ prefix optional)"
|
|
return 2
|
|
fi
|
|
git fetch --prune --quiet "$remote" || return 1
|
|
for b in "$@"; do
|
|
b=${b#${remote}/}
|
|
if git push "$remote" --delete "$b"; then
|
|
print "deleted ${remote}/${b}"
|
|
else
|
|
print -u2 "grmb: could not delete ${b} (already gone? check: git ls-remote --heads ${remote})"
|
|
rc=1
|
|
fi
|
|
done
|
|
return $rc
|
|
}
|
|
|
|
# Secrets from this user's 1Password vault, written by dotfiles/secrets/install.sh.
|
|
# Reading a local 0600 file keeps shell startup off the network: nothing here
|
|
# calls op. opc/opd refresh the file themselves; after editing the vault
|
|
# anywhere else (the 1Password app, another box), run `ops`.
|
|
_DOTFILES_CONF="$HOME/.config/dotfiles"
|
|
[ -f "$_DOTFILES_CONF/secrets.env" ] && source "$_DOTFILES_CONF/secrets.env"
|
|
|
|
# opr NAME read one field straight from the vault
|
|
# opc NAME=VALUE create or update a field, then refresh and reload this
|
|
# opc NAME shell; with no VALUE (NAME or NAME=) it prompts, hidden,
|
|
# which keeps the secret out of shell history.
|
|
# `opc NAME VALUE` also still works.
|
|
# opd NAME delete a field, refresh, and unset it in this shell
|
|
# opl [VAULT [ITEM]] show every field and its current value (default: your
|
|
# vault and item from op.env)
|
|
# ops re-fetch the vault into secrets.env and reload this shell
|
|
#
|
|
# The service-account token lives in op.env and is loaded only inside these
|
|
# subshells, so it never lands in the environment of everything you run.
|
|
_dotfiles_op() {
|
|
[ -f "$_DOTFILES_CONF/op.env" ] || { print -u2 "no $_DOTFILES_CONF/op.env; run: ops"; return 1; }
|
|
( set -a; source "$_DOTFILES_CONF/op.env"; set +a; "$@" )
|
|
}
|
|
_dotfiles_env_name() {
|
|
[[ "$1" =~ '^[A-Za-z_][A-Za-z0-9_]*$' ]] && return 0
|
|
print -u2 "$2: '$1' is not a valid variable name"; return 2
|
|
}
|
|
opr() {
|
|
_dotfiles_env_name "$1" opr || return
|
|
_dotfiles_op sh -c 'op read "op://$OP_VAULT/$OP_ITEM/$1"' opr "$1"
|
|
}
|
|
opc() {
|
|
emulate -L zsh
|
|
local name=$1 value
|
|
if [[ $1 == *=* ]]; then
|
|
# NAME=VALUE: split on the first '=' so values may contain '='
|
|
name=${1%%=*} value=${1#*=}
|
|
elif (( $# >= 2 )); then
|
|
value=$2
|
|
fi
|
|
_dotfiles_env_name "$name" opc || return
|
|
if [[ -z $value ]]; then
|
|
read -rs "value?$name: " && print
|
|
[[ -n $value ]] || { print -u2 "opc: empty value for $name; nothing changed"; return 1; }
|
|
fi
|
|
# [concealed] makes it a password field; an existing field of that name is updated.
|
|
# op can exit non-zero with "(404) Not Found" after an edit it did save (seen with
|
|
# op 2.34.1 and 2.39.0), so its exit code is ignored: the field is read back instead.
|
|
_dotfiles_op sh -c '
|
|
clean() { printf "%s" "${1:-(no output)}" | sed "s/^\[ERROR\] [0-9/]* [0-9:]* //"; }
|
|
ref="op://$OP_VAULT/$OP_ITEM/$1"
|
|
err=$(op item edit "$OP_ITEM" --vault "$OP_VAULT" "$1[concealed]=$2" 2>&1 >/dev/null)
|
|
if [ "$(op read "$ref" 2>/dev/null)" = "$2" ]; then
|
|
printf "opc: saved %s\n" "$ref" >&2
|
|
else
|
|
printf "opc: could not save %s\n op said: %s\n check vault, item and service-account write access with: opl\n" \
|
|
"$ref" "$(clean "$err")" >&2
|
|
exit 1
|
|
fi' opc "$name" "$value" && ops
|
|
}
|
|
opd() {
|
|
_dotfiles_env_name "$1" opd || return
|
|
# As in opc, op's exit code is unreliable here: check the item's field labels instead.
|
|
_dotfiles_op sh -c '
|
|
clean() { printf "%s" "${1:-(no output)}" | sed "s/^\[ERROR\] [0-9/]* [0-9:]* //"; }
|
|
ref="op://$OP_VAULT/$OP_ITEM/$1"
|
|
err=$(op item edit "$OP_ITEM" --vault "$OP_VAULT" "$1[delete]" 2>&1 >/dev/null)
|
|
if ! item=$(op item get "$OP_ITEM" --vault "$OP_VAULT" --format json 2>&1); then
|
|
printf "opd: could not re-read %s to confirm the delete\n op said: %s\n" "$ref" "$(clean "$item")" >&2
|
|
exit 1
|
|
elif printf "%s" "$item" | jq -r ".fields[].label" | grep -qxF "$1"; then
|
|
printf "opd: could not delete %s\n op said: %s\n" "$ref" "$(clean "$err")" >&2
|
|
exit 1
|
|
fi
|
|
printf "opd: deleted %s\n" "$ref" >&2' opd "$1" \
|
|
&& unset "$1" && ops
|
|
}
|
|
opl() {
|
|
_dotfiles_op sh -c '
|
|
vault=${1:-$OP_VAULT} item=${2:-$OP_ITEM}
|
|
printf "op://%s/%s\n" "$vault" "$item" >&2
|
|
op item get "$item" --vault "$vault" --reveal --format json' opl "$@" \
|
|
| jq -r '
|
|
[.fields[] | select(.purpose != "NOTES")] as $f
|
|
| ([$f[] | .label // "" | length] | max // 0) as $w
|
|
| $f[]
|
|
| ((.label // "") + " " * ($w - ((.label // "") | length))) as $name
|
|
| ((.value // "") | split("\n")) as $lines
|
|
| "\($name) \($lines[0] // "")",
|
|
($lines[1:][] | (" " * ($w + 2)) + .)'
|
|
}
|
|
ops() {
|
|
"${DOTFILES_DIR:-$HOME/.dotfiles}/secrets/install.sh" \
|
|
&& source "$_DOTFILES_CONF/secrets.env"
|
|
}
|
|
|
|
# Per-machine customizations (secrets, keys, local overrides) live in
|
|
# ~/.zshrc.local, which bootstrap never touches. See zsh/zshrc.local.example
|
|
# for the variables modules look for. Loaded last so it can override anything.
|
|
[ -f "$HOME/.zshrc.local" ] && source "$HOME/.zshrc.local"
|