Compare commits
14
Commits
db3a3e6e82
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41a90a10fb | ||
|
|
5f0dec5270 | ||
|
|
fe8b9a3ec0 | ||
|
|
72d85eb9ec | ||
|
|
7bbd911365 | ||
|
|
b6d36ba113 | ||
|
|
c0f1fc21b7 | ||
|
|
86b8e51646 | ||
|
|
ffedc84cfb | ||
|
|
cb7ab17769 | ||
|
|
5bc8a27f60 | ||
|
|
8ec3269f4a | ||
|
|
46052cf573 | ||
|
|
d3393edfdd |
@@ -44,9 +44,9 @@ Every module's `install.sh` satisfies all of these. Copy the shape from
|
||||
## Wiring it up
|
||||
|
||||
**`bootstrap.sh`** — add the call. Order is load-bearing and each existing
|
||||
out-of-order entry carries a comment explaining why (`docker/` before `bin/`
|
||||
so kind's gate sees the daemon; `docker-rootless-cgroup/` last because it
|
||||
needs the rootless daemon already up). If your module has an ordering
|
||||
out-of-order entry carries a comment explaining why (`secrets/` after `bin/`
|
||||
because it needs `op`; `docker-rootless-cgroup/` last because it needs the
|
||||
rootless daemon already up). If your module has an ordering
|
||||
constraint, add it *with* a comment; if it doesn't, append it near related
|
||||
modules.
|
||||
|
||||
@@ -61,7 +61,7 @@ Never commit the value itself.
|
||||
## Non-systemd hosts
|
||||
|
||||
Kasm has no systemd. Anything that would `systemctl enable` must degrade
|
||||
gracefully — see `docker/install.sh`, which detects the situation and no-ops
|
||||
gracefully — see `docker-rootless-cgroup/install.sh`, which detects the situation and no-ops
|
||||
rather than failing the whole bootstrap run.
|
||||
|
||||
## Before you're done
|
||||
|
||||
@@ -14,7 +14,7 @@ Everything in this repo is version-pinned on purpose. Nothing floats.
|
||||
| CLI tools | `bin/install.sh` | `ver=v1.2.3` inside each `install_<name>()` |
|
||||
| Node.js | `node/install.sh` | `NODE_VERSION="v24.18.0"` |
|
||||
| MCP servers | `mcp-config/opencode.gateway.json` | `["npx", "-y", "pkg@0.0.10"]` |
|
||||
| oh-my-zsh, Docker | `zsh/`, `docker/` | installed from upstream scripts — not pinned |
|
||||
| oh-my-zsh | `zsh/` | installed from upstream script — not pinned |
|
||||
|
||||
Grep before assuming: `grep -rnE 'ver=|_VERSION=|@[0-9]+\.[0-9]+' --include='*.sh' --include='*.json'`
|
||||
|
||||
|
||||
@@ -63,8 +63,8 @@ faithful rehearsal.
|
||||
|
||||
Kasm has no systemd and no rootful Docker. Anything that would `systemctl` must
|
||||
no-op rather than abort — you can't fully rehearse this in a container, so read
|
||||
the code path and confirm the guard exists (`docker/install.sh` and
|
||||
`docker-rootless-cgroup/install.sh` are the references).
|
||||
the code path and confirm the guard exists (`docker-rootless-cgroup/install.sh`
|
||||
is the reference).
|
||||
|
||||
## Full run
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
@@ -32,16 +32,9 @@ Already have a clone? Just run it directly:
|
||||
- `git/` — sets git `user.name`/`user.email` and configures
|
||||
`credential.helper store` (plaintext `~/.git-credentials`, 0600) for
|
||||
HTTPS auth.
|
||||
- `docker/` — installs full (rootful) Docker Engine from Docker's upstream
|
||||
package repo (get.docker.com on Debian-family; the docker-ce repo wired up
|
||||
directly on RHEL-family, since the convenience script rejects EL rebuilds
|
||||
like Oracle Linux by name), enables the service (systemd hosts), and adds
|
||||
the invoking user to the
|
||||
`docker` group (takes effect at next login). No-ops where a rootless
|
||||
Docker daemon is already running (e.g. Kasm) so it never fights that
|
||||
setup. Uses sudo.
|
||||
- `bin/` — installs userland devops CLIs as self-contained, statically
|
||||
linked release binaries into `~/.local/bin`: jq, kubectl, helm, k9s,
|
||||
linked release binaries into `~/.local/bin`: jq, kubectl, kubelogin (the
|
||||
`kubectl oidc-login` plugin, for OIDC auth to Omni clusters), helm, k9s,
|
||||
kubectx/kubens, stern, yq, OpenTofu, terragrunt, task (go-task, the
|
||||
Taskfile runner), the 1Password CLI `op` for secrets storage, the
|
||||
opencode coding agent, zarf (DevSecOps for Offline and Airgapped
|
||||
@@ -49,6 +42,8 @@ Already have a clone? Just run it directly:
|
||||
coder CLI for Coder workspaces.
|
||||
kind is installed only when full (non-rootless) Docker is available —
|
||||
it doesn't work against rootless Docker, so it is skipped on Kasm.
|
||||
Dotfiles does not install Docker itself; install it on the host (or bake
|
||||
it into the image) where you want it.
|
||||
No sudo, nothing outside `$HOME`, so tools persist across Kasm
|
||||
redeploys with no reinstall. Idempotent: each tool records its version
|
||||
under `~/.local/bin/.versions` and is skipped when the pinned version
|
||||
@@ -58,8 +53,12 @@ Already have a clone? Just run it directly:
|
||||
sources, so opening a shell never calls `op`. One vault per user and one
|
||||
item in it, both named after your login by default; each field's label is
|
||||
the variable name (`GITEA_KEY`, `LITELLM_KEY`, `PROXMOX_TOKEN_SECRET`, …).
|
||||
The first run prompts for the service-account token (and vault/item), and
|
||||
keeps them in `~/.config/dotfiles/op.env` (0600, not loaded into shells).
|
||||
The first run prompts for the service-account token and checks it with
|
||||
1Password straight away, re-prompting on a bad paste. It then picks the vault
|
||||
and item: the only one the token can see, or a prompt that lists them. It keeps
|
||||
all three in `~/.config/dotfiles/op.env` (0600, not loaded into shells). If
|
||||
that fails, bootstrap skips mcp-config/ and hermes/ with an error rather
|
||||
than asking for the LiteLLM key by hand.
|
||||
Runs after `bin/` (needs `op` and `jq`); bootstrap loads the result so later
|
||||
modules get the keys without prompting. Day to day: `opr NAME` reads a
|
||||
field, `opc NAME [VALUE]` creates or updates one (prompts, hidden, without
|
||||
@@ -79,6 +78,14 @@ Already have a clone? Just run it directly:
|
||||
`~/.local/bin`. Persists across redeploys; only rebuilt when the venv is
|
||||
missing or broken. Building it may install `python3`/`python3-venv` via
|
||||
the system package manager (the one place this module uses sudo).
|
||||
- `vscode/` — installs Visual Studio Code from upstream's portable tarball into
|
||||
`~/.local/opt/vscode`, symlinks `code` into `~/.local/bin`, and writes a
|
||||
user-level `.desktop` entry so it shows up in the application menu. **No
|
||||
sudo, nothing outside `$HOME`** — the same contract as `bin/`, so it survives
|
||||
a Kasm redeploy. Pinned via `VSCODE_VERSION` in the module; bump it to upgrade.
|
||||
Also sets `password-store: basic` in `~/.vscode/argv.json` so KDE's kwallet
|
||||
does not prompt for an unlock on every launch (see below). **No-ops on
|
||||
headless hosts** — it's a ~400MB GUI app — override with `DOTFILES_VSCODE`.
|
||||
- `docker-rootless-cgroup/` — on systemd-less hosts (e.g. Kasm), hand-delegates
|
||||
cgroup v2 controllers to the rootless Docker daemon so per-container cgroups
|
||||
work (rootless Docker otherwise comes up with
|
||||
@@ -112,6 +119,18 @@ item instead of `~/.zshrc.local`:
|
||||
don't set this by hand: run `mcp-config/install.sh` once and its shell
|
||||
hook exports it from `~/.config/mcp-sync/key`. Set it in `~/.zshrc.local`
|
||||
only on a box without mcp-sync.
|
||||
- `DOTFILES_VSCODE` — controls the `vscode/` module. Unset (default) installs
|
||||
VS Code only when a graphical session is detected; `1` forces the install on
|
||||
a headless host (e.g. for `code tunnel`); `0` skips it everywhere. A plain
|
||||
shell/env var, not a vault secret.
|
||||
|
||||
On the keyring: VS Code's secret storage reaches for the OS keyring through
|
||||
libsecret, which on KDE makes kwalletd prompt for an unlock before the editor
|
||||
is usable — every session, since `kwallet-pam` is not installed. The module
|
||||
sets `"password-store": "basic"` in `~/.vscode/argv.json` to stop that, which
|
||||
means VS Code's stored tokens are obfuscated on disk rather than sealed
|
||||
behind a wallet password. To use the wallet instead, install `kwallet-pam` so
|
||||
it unlocks at login, then change that value to `"kwallet5"`.
|
||||
- `OP_SERVICE_ACCOUNT_TOKEN` — 1Password service-account token. Not a shell
|
||||
variable any more: `secrets/install.sh` asks for it once and stores it in
|
||||
`~/.config/dotfiles/op.env`. Pass it in the env only for an unattended run.
|
||||
|
||||
@@ -49,6 +49,18 @@ install_kubectl() {
|
||||
mark_version kubectl "$ver"
|
||||
}
|
||||
|
||||
# kubelogin — kubectl OIDC plugin, so omnictl-generated kubeconfigs can
|
||||
# authenticate (they exec `kubectl oidc-login get-token`). Installed as
|
||||
# kubectl-oidc_login, the name kubectl's plugin lookup expects.
|
||||
install_oidc_login() {
|
||||
ver=v1.36.4
|
||||
have_version kubectl-oidc_login "$ver" && { fsay "kubectl-oidc_login $ver already installed."; return 0; }
|
||||
install_zip_bin \
|
||||
"https://github.com/int128/kubelogin/releases/download/$ver/kubelogin_linux_$DEB.zip" \
|
||||
kubelogin kubectl-oidc_login
|
||||
mark_version kubectl-oidc_login "$ver"
|
||||
}
|
||||
|
||||
# helm — Kubernetes package manager (from get.helm.sh).
|
||||
install_helm() {
|
||||
ver=v4.2.2
|
||||
@@ -235,6 +247,7 @@ try() {
|
||||
|
||||
try install_jq
|
||||
try install_kubectl
|
||||
try install_oidc_login
|
||||
try install_helm
|
||||
try install_k9s
|
||||
if docker_full_available; then
|
||||
|
||||
+27
-7
@@ -66,7 +66,7 @@ fi
|
||||
#
|
||||
# WHY: these used to be bare calls, so under `set -eu` the FIRST failing module
|
||||
# aborted bootstrap and every later module silently never ran — one unsupported
|
||||
# distro in docker/ was enough to leave a box with no CLIs, no npx and no
|
||||
# distro in one module was enough to leave a box with no CLIs, no npx and no
|
||||
# ansible. The modules are independent, so a failure in one is not a reason to
|
||||
# skip the rest. (bin/install.sh does the same for individual tools; this is
|
||||
# that guard one level up.)
|
||||
@@ -79,8 +79,6 @@ run_module() {
|
||||
|
||||
run_module zsh
|
||||
run_module git
|
||||
# Runs before bin/ so the kind install gate can see the rootful daemon.
|
||||
run_module docker
|
||||
run_module bin
|
||||
# Needs op and jq from bin/. Runs before everything that consumes a secret, and
|
||||
# its output is loaded into THIS process so later modules see the vault's
|
||||
@@ -103,11 +101,33 @@ run_module node
|
||||
# be repaired by re-running bootstrap. Losing ~/.zshrc.local (or the hook line
|
||||
# in it) left opencode with no covert-gpt provider and no way to get it back
|
||||
# short of curl-ing this module by hand.
|
||||
run_module mcp-config
|
||||
# Hermes Agent (Nous Research) is distributed as an npm package with a Python
|
||||
# dependency. Runs after node/ ensures npx is available.
|
||||
run_module hermes
|
||||
#
|
||||
# Both it and hermes/ are SKIPPED when the vault did not deliver LITELLM_KEY
|
||||
# (and no box-local copy exists): mcp-config/ would otherwise fall back to a
|
||||
# "paste your LiteLLM key" prompt, which reads like a LiteLLM problem and hides
|
||||
# the real one — a bad 1Password token, vault or item, which also means every
|
||||
# other vault key is missing.
|
||||
if [ -z "${LITELLM_KEY:-}" ] && [ ! -s "$HOME/.config/mcp-sync/key" ]; then
|
||||
echo "" >&2
|
||||
echo "bootstrap: ERROR: no LITELLM_KEY from 1Password — skipping mcp-config and hermes." >&2
|
||||
case " $FAILED " in
|
||||
*" secrets "*) echo "bootstrap: the secrets module failed (see its messages above)." >&2 ;;
|
||||
*) echo "bootstrap: the vault item has no LITELLM_KEY field; add it with: opc LITELLM_KEY" >&2 ;;
|
||||
esac
|
||||
echo "bootstrap: fix it with $SCRIPT_DIR/secrets/install.sh, then re-run bootstrap." >&2
|
||||
echo "" >&2
|
||||
FAILED="$FAILED mcp-config hermes"
|
||||
else
|
||||
run_module mcp-config
|
||||
# Hermes Agent (Nous Research) is distributed as an npm package with a Python
|
||||
# dependency. Runs after node/ ensures npx is available.
|
||||
run_module hermes
|
||||
fi
|
||||
run_module ansible
|
||||
# GUI editor, installed from upstream's tarball into ~/.local/opt (no sudo, same
|
||||
# contract as bin/). No ordering constraints — it needs nothing from bin/,
|
||||
# secrets/ or node/ — and no-ops on headless hosts.
|
||||
run_module vscode
|
||||
# Runs last: needs rootless Docker already up (started at boot) so it can
|
||||
# delegate cgroups to it. No-ops where that does not apply.
|
||||
run_module docker-rootless-cgroup
|
||||
|
||||
Binary file not shown.
@@ -1,114 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
# docker/install.sh — install full (rootful) Docker Engine and let the
|
||||
# invoking user talk to it without sudo.
|
||||
#
|
||||
# What it does:
|
||||
# 1. Installs Docker Engine from Docker's upstream package repo. Skipped when
|
||||
# `docker` is already installed. Debian-family hosts go through Docker's
|
||||
# convenience script (get.docker.com); RHEL-family hosts wire up the
|
||||
# docker-ce repo directly — see install_rhel() for why.
|
||||
# 2. Enables and starts the service (systemd hosts only).
|
||||
# 3. Adds the invoking user to the `docker` group so the daemon socket is
|
||||
# usable without sudo. Takes effect at next login.
|
||||
#
|
||||
# Deliberately no-ops when a rootless Docker daemon is detected (e.g. Kasm
|
||||
# ships its own rootless setup) — installing rootful Docker there would fight
|
||||
# the existing runtime. See docker-rootless-cgroup/ for the Kasm-side config.
|
||||
#
|
||||
# Idempotent and safe to re-run. Uses sudo.
|
||||
set -eu
|
||||
|
||||
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
|
||||
REPO_ROOT="$(dirname -- "$SCRIPT_DIR")"
|
||||
|
||||
# shellcheck source=../lib/os-detect.sh
|
||||
. "$REPO_ROOT/lib/os-detect.sh"
|
||||
|
||||
say() { printf '%s\n' "$*" >&2; }
|
||||
|
||||
DOCKER_PKGS="docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin"
|
||||
|
||||
# install_rhel — install docker-ce from Docker's EL repo.
|
||||
#
|
||||
# WHY NOT get.docker.com: the convenience script allowlists distros by
|
||||
# /etc/os-release ID and aborts on anything it does not recognize —
|
||||
# "ERROR: Unsupported distribution 'ol'" on Oracle Linux, and the same for
|
||||
# other EL rebuilds it has not enumerated. On a match it does nothing more
|
||||
# than what this function does: add the repo, then install the packages.
|
||||
# Wiring the repo up directly drops the allowlist, so every EL rebuild
|
||||
# (Oracle, Rocky, Alma, CentOS, RHEL) installs from one code path.
|
||||
#
|
||||
# The repo URL is keyed on $releasever/$basearch, which dnf expands per host,
|
||||
# so EL9 and EL10 both resolve without a version table here.
|
||||
install_rhel() {
|
||||
say "docker: adding Docker CE repo (EL \$releasever/\$basearch)..."
|
||||
sudo tee /etc/yum.repos.d/docker-ce.repo >/dev/null <<'REPO'
|
||||
[docker-ce-stable]
|
||||
name=Docker CE Stable - $basearch
|
||||
baseurl=https://download.docker.com/linux/centos/$releasever/$basearch/stable
|
||||
enabled=1
|
||||
gpgcheck=1
|
||||
gpgkey=https://download.docker.com/linux/centos/gpg
|
||||
REPO
|
||||
# --allowerasing: containerd.io conflicts with the distro's own runc/containerd
|
||||
# on EL rebuilds that ship them (Oracle Linux does). Let dnf swap them out
|
||||
# rather than dead-end on the conflict.
|
||||
if command -v dnf >/dev/null 2>&1; then
|
||||
# shellcheck disable=SC2086
|
||||
sudo dnf install -y --allowerasing $DOCKER_PKGS
|
||||
else
|
||||
# shellcheck disable=SC2086
|
||||
sudo yum install -y --allowerasing $DOCKER_PKGS
|
||||
fi
|
||||
}
|
||||
|
||||
# install_debian — Docker's convenience script; it supports every Debian-family
|
||||
# distro this repo targets and sets up the apt repo and keyring.
|
||||
install_debian() {
|
||||
say "docker: installing Docker Engine via get.docker.com..."
|
||||
tmp="$(mktemp)"
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
curl -fsSL https://get.docker.com -o "$tmp"
|
||||
elif command -v wget >/dev/null 2>&1; then
|
||||
wget -qO "$tmp" https://get.docker.com
|
||||
else
|
||||
say "docker: need curl or wget"; rm -f "$tmp"; return 1
|
||||
fi
|
||||
sudo sh "$tmp"
|
||||
rm -f "$tmp"
|
||||
}
|
||||
|
||||
# --- skip on rootless-Docker hosts -------------------------------------------
|
||||
if command -v docker >/dev/null 2>&1 &&
|
||||
docker info -f '{{println .SecurityOptions}}' 2>/dev/null | grep -q 'name=rootless'; then
|
||||
say "docker: rootless Docker daemon detected; leaving it alone."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# --- 1. install engine -------------------------------------------------------
|
||||
if command -v docker >/dev/null 2>&1; then
|
||||
say "docker: engine already installed ($(docker --version 2>/dev/null || echo 'version unknown'))."
|
||||
else
|
||||
case "$(detect_pkg_family)" in
|
||||
rhel) install_rhel ;;
|
||||
debian) install_debian ;;
|
||||
*) say "docker: unsupported/undetected OS family; install Docker manually."; exit 1 ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# --- 2. enable + start the service (systemd only) ----------------------------
|
||||
if [ -d /run/systemd/system ] && command -v systemctl >/dev/null 2>&1; then
|
||||
sudo systemctl enable --now docker
|
||||
else
|
||||
say "docker: no systemd; not managing the service (start dockerd yourself)."
|
||||
fi
|
||||
|
||||
# --- 3. group membership for the invoking user -------------------------------
|
||||
user="$(id -un)"
|
||||
if id -nG "$user" | tr ' ' '\n' | grep -qx docker; then
|
||||
say "docker: $user already in the docker group."
|
||||
else
|
||||
sudo usermod -aG docker "$user"
|
||||
say "docker: added $user to the docker group."
|
||||
say " Log out and back in (or run 'newgrp docker') for it to take effect."
|
||||
fi
|
||||
+9
-1
@@ -26,6 +26,12 @@ REPO_ROOT="$(dirname -- "$SCRIPT_DIR")"
|
||||
|
||||
say() { printf '%s\n' "$*" >&2; }
|
||||
|
||||
# The installer links the CLI into ~/.local/bin, which is only on PATH in
|
||||
# shells that sourced the managed ~/.zshrc. bootstrap run by a non-shell
|
||||
# parent (the Coder agent's dotfiles script) has no such PATH, so the
|
||||
# post-install `command -v hermes` failed on a successful install.
|
||||
PATH="$HOME/.local/bin:$PATH"
|
||||
|
||||
# Hermes Agent version (synced with PyPI/npm).
|
||||
HERMES_VERSION="v0.18.2"
|
||||
|
||||
@@ -74,7 +80,9 @@ if [ "$KEY_CHANGED" -eq 1 ] && [ -f "$HOME/.config/mcp-sync/key" ]; then
|
||||
fi
|
||||
say "hermes: Updated LITELLM_KEY in ~/.hermes/.env"
|
||||
else
|
||||
# Create .env if it doesn't exist
|
||||
# Create .env if it doesn't exist. On a fresh home this runs BEFORE the
|
||||
# installer below has created ~/.hermes, so make the directory first.
|
||||
mkdir -p "$HOME/.hermes"
|
||||
printf 'LITELLM_KEY=%s\n' "$NEW_KEY" >"$HOME/.hermes/.env"
|
||||
say "hermes: Created ~/.hermes/.env with LITELLM_KEY"
|
||||
fi
|
||||
|
||||
+30
-2
@@ -8,8 +8,8 @@
|
||||
# reset.
|
||||
#
|
||||
# Source this (callers already run `set -eu`), then use fetch_arch /
|
||||
# fetch_arch_deb, have_version + mark_version, install_tarball_bin, and
|
||||
# install_raw_bin.
|
||||
# fetch_arch_deb, have_version + mark_version, install_tarball_bin,
|
||||
# install_tarball_tree, and install_raw_bin.
|
||||
|
||||
BIN_DIR="${BIN_DIR:-$HOME/.local/bin}"
|
||||
VERSION_DIR="$BIN_DIR/.versions"
|
||||
@@ -93,6 +93,34 @@ install_zip_bin() {
|
||||
fsay "installed $BIN_DIR/$3"
|
||||
}
|
||||
|
||||
# install_tarball_tree <url> <root-within-archive> <dest-dir> — download a
|
||||
# .tar.gz that holds a whole application directory rather than one binary, and
|
||||
# swap it into <dest-dir>. install_tarball_bin cannot be reused here: VS Code
|
||||
# is an Electron bundle of thousands of files that only runs from an intact
|
||||
# tree, not a single binary that can be picked out of the archive.
|
||||
#
|
||||
# The new tree is staged beside the target and moved into place only once the
|
||||
# download and extraction have both succeeded, so an interrupted run leaves the
|
||||
# previously installed version intact instead of a half-extracted directory.
|
||||
install_tarball_tree() {
|
||||
_dest="$3"
|
||||
_tmp="$(mktemp -d)"
|
||||
_fetch_dl "$1" "$_tmp/archive.tar.gz"
|
||||
tar -xzf "$_tmp/archive.tar.gz" -C "$_tmp"
|
||||
if [ ! -d "$_tmp/$2" ]; then
|
||||
fsay "fetch: '$2' not found in archive"; rm -rf "$_tmp"; return 1
|
||||
fi
|
||||
mkdir -p "$(dirname "$_dest")"
|
||||
rm -rf "$_dest.new" "$_dest.old"
|
||||
mv "$_tmp/$2" "$_dest.new"
|
||||
if [ -e "$_dest" ]; then
|
||||
mv "$_dest" "$_dest.old"
|
||||
fi
|
||||
mv "$_dest.new" "$_dest"
|
||||
rm -rf "$_dest.old" "$_tmp"
|
||||
fsay "installed $_dest"
|
||||
}
|
||||
|
||||
# install_raw_bin <url> <dest-name> — download a single raw binary.
|
||||
install_raw_bin() {
|
||||
mkdir -p "$BIN_DIR"
|
||||
|
||||
+85
-16
@@ -48,8 +48,17 @@ for _tool in op jq; do
|
||||
done
|
||||
|
||||
# --- 1. resolve the service account, vault and item -------------------------
|
||||
# Precedence: the environment, then op.env, then a prompt (token) / defaults.
|
||||
# Precedence: the environment, then op.env, then a prompt / auto-detection.
|
||||
#
|
||||
# WHY each piece is checked on its own: the vault holds every key later
|
||||
# modules need (LITELLM_KEY, GITEA_KEY, ...). A bad token paste used to fall
|
||||
# through as a generic "could not read item", bootstrap carried on, and the
|
||||
# first visible symptom was mcp-config/ asking for a LiteLLM key by hand —
|
||||
# which hid the real problem. Now a rejected token, a wrong vault and a wrong
|
||||
# item each get their own message, and on a TTY a re-prompt.
|
||||
_env_token="${OP_SERVICE_ACCOUNT_TOKEN:-}"
|
||||
_token_src="from OP_SERVICE_ACCOUNT_TOKEN"
|
||||
[ -n "$_env_token" ] || _token_src="stored in $OP_ENV"
|
||||
_env_vault="${OP_VAULT:-}"
|
||||
_env_item="${OP_ITEM:-}"
|
||||
if [ -f "$OP_ENV" ]; then
|
||||
@@ -61,6 +70,10 @@ OP_VAULT="${_env_vault:-${OP_VAULT:-}}"
|
||||
OP_ITEM="${_env_item:-${OP_ITEM:-}}"
|
||||
_user="$(id -un)"
|
||||
|
||||
# Probe in a subshell: in POSIX sh a failed redirect on a special builtin
|
||||
# like `:` exits the whole script.
|
||||
have_tty() { ( : < /dev/tty ) 2>/dev/null; }
|
||||
|
||||
# ask <prompt> <default> — read one line from the TTY; echoes the default on empty.
|
||||
ask() {
|
||||
printf '%s [%s]: ' "$1" "$2" >&2
|
||||
@@ -68,31 +81,85 @@ ask() {
|
||||
printf '%s' "${_ans:-$2}"
|
||||
}
|
||||
|
||||
if [ -z "$OP_SERVICE_ACCOUNT_TOKEN" ]; then
|
||||
if [ -f "$SECRETS_ENV" ]; then
|
||||
say "secrets: no service-account token; keeping existing $SECRETS_ENV."
|
||||
exit 0
|
||||
# mask <secret> — first 10 and last 10 characters plus the length, so a
|
||||
# truncated or doubled paste is visible without printing the whole token.
|
||||
mask() {
|
||||
printf '%s' "$1" | awk '{ n = length($0)
|
||||
if (n <= 20) printf "%s (%d chars)", "(too short)", n
|
||||
else printf "%s**********%s (first 10 + last 10 of %d chars)", substr($0, 1, 10), substr($0, n - 9), n }'
|
||||
}
|
||||
|
||||
if [ -z "$OP_SERVICE_ACCOUNT_TOKEN" ] && [ -f "$SECRETS_ENV" ]; then
|
||||
say "secrets: no service-account token; keeping existing $SECRETS_ENV."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# The token: `op whoami` is the cheapest call that fails on a bad token and
|
||||
# nothing else. Up to three pastes before giving up.
|
||||
_tries=0
|
||||
while :; do
|
||||
if [ -n "$OP_SERVICE_ACCOUNT_TOKEN" ]; then
|
||||
export OP_SERVICE_ACCOUNT_TOKEN
|
||||
op whoami >/dev/null 2>&1 && break
|
||||
if [ "$_tries" -eq 0 ]; then
|
||||
say "secrets: 1Password REJECTED the service-account token $_token_src (bad paste, revoked or expired?)."
|
||||
say " token: $(mask "$OP_SERVICE_ACCOUNT_TOKEN")"
|
||||
else
|
||||
say "secrets: 1Password REJECTED that token — most likely a truncated or bad paste."
|
||||
say " token: $(mask "$OP_SERVICE_ACCOUNT_TOKEN")"
|
||||
fi
|
||||
fi
|
||||
# Probe in a subshell: in POSIX sh a failed redirect on a special builtin
|
||||
# like `:` exits the whole script, which would skip the message below.
|
||||
if ! ( : < /dev/tty ) 2>/dev/null; then
|
||||
say "secrets: no OP_SERVICE_ACCOUNT_TOKEN and no TTY to prompt."
|
||||
if ! have_tty; then
|
||||
say "secrets: no valid OP_SERVICE_ACCOUNT_TOKEN and no TTY to prompt."
|
||||
say " re-run as: OP_SERVICE_ACCOUNT_TOKEN=ops_... $0"
|
||||
exit 1
|
||||
fi
|
||||
if [ "$_tries" -ge 3 ]; then
|
||||
say "secrets: giving up after $_tries attempts. Every key in the vault (LITELLM_KEY,"
|
||||
say " GITEA_KEY, ...) is missing until this works: re-run $0"
|
||||
exit 1
|
||||
fi
|
||||
_tries=$((_tries + 1))
|
||||
printf '1Password service-account token (ops_..., input hidden): ' >&2
|
||||
stty -echo < /dev/tty 2>/dev/null || true
|
||||
IFS= read -r OP_SERVICE_ACCOUNT_TOKEN < /dev/tty || true
|
||||
stty echo < /dev/tty 2>/dev/null || true
|
||||
printf '\n' >&2
|
||||
OP_SERVICE_ACCOUNT_TOKEN="$(printf '%s' "$OP_SERVICE_ACCOUNT_TOKEN" | tr -d ' \t\r\n')"
|
||||
[ -n "$OP_SERVICE_ACCOUNT_TOKEN" ] || { say "secrets: no token entered."; exit 1; }
|
||||
[ -n "$OP_VAULT" ] || OP_VAULT="$(ask 'Vault' "$_user")"
|
||||
[ -n "$OP_ITEM" ] || OP_ITEM="$(ask 'Item' "$_user")"
|
||||
fi
|
||||
OP_VAULT="${OP_VAULT:-$_user}"
|
||||
OP_ITEM="${OP_ITEM:-$_user}"
|
||||
export OP_SERVICE_ACCOUNT_TOKEN
|
||||
[ -n "$OP_SERVICE_ACCOUNT_TOKEN" ] || say "secrets: no token entered."
|
||||
done
|
||||
say "secrets: service-account token OK: $(mask "$OP_SERVICE_ACCOUNT_TOKEN")"
|
||||
|
||||
# pick <kind> <current> <default> <check-fn> <list-cmd...>
|
||||
# Resolve a vault or item name: keep <current> if it checks out; otherwise use
|
||||
# the only one the token can see, or ask (TTY) showing what it can see.
|
||||
pick() {
|
||||
_kind="$1" _cur="$2" _def="$3"; shift 3
|
||||
_check="$1"; shift
|
||||
_names="$("$@" --format json 2>/dev/null | jq -r '.[] | .name // .title')"
|
||||
if [ -n "$_cur" ]; then
|
||||
$_check "$_cur" && { printf '%s' "$_cur"; return 0; }
|
||||
say "secrets: $_kind '$_cur' not found. This token can see:"
|
||||
printf '%s\n' "$_names" | sed 's/^/ - /' >&2
|
||||
fi
|
||||
if [ "$(printf '%s\n' "$_names" | grep -c .)" -eq 1 ]; then
|
||||
say "secrets: using the only $_kind this token can see: $_names"
|
||||
printf '%s' "$_names"; return 0
|
||||
fi
|
||||
have_tty || { say "secrets: set OP_$(printf '%s' "$_kind" | tr a-z A-Z) and re-run."; return 1; }
|
||||
printf '%s\n' "$_names" | grep -Fxq -- "$_def" || _def="$(printf '%s\n' "$_names" | head -n 1)"
|
||||
while :; do
|
||||
_cur="$(ask "$_kind" "$_def")"
|
||||
$_check "$_cur" && { printf '%s' "$_cur"; return 0; }
|
||||
say "secrets: $_kind '$_cur' not found. This token can see:"
|
||||
printf '%s\n' "$_names" | sed 's/^/ - /' >&2
|
||||
done
|
||||
}
|
||||
vault_ok() { op vault get "$1" >/dev/null 2>&1; }
|
||||
item_ok() { op item get "$1" --vault "$OP_VAULT" >/dev/null 2>&1; }
|
||||
|
||||
OP_VAULT="$(pick vault "$OP_VAULT" "$_user" vault_ok op vault list)" || exit 1
|
||||
OP_ITEM="$(pick item "$OP_ITEM" "$_user" item_ok op item list --vault "$OP_VAULT")" || exit 1
|
||||
|
||||
# --- 2. fetch the item once and render it -----------------------------------
|
||||
# One `op item get` instead of one `op read` per field. Labels that are not
|
||||
@@ -132,6 +199,8 @@ fi
|
||||
mv -f "$_tmp" "$SECRETS_ENV"
|
||||
trap - EXIT
|
||||
say "secrets: wrote $_count variables to $SECRETS_ENV (0600)."
|
||||
grep -q '^export LITELLM_KEY=' "$SECRETS_ENV" \
|
||||
|| say "secrets: WARNING: op://$OP_VAULT/$OP_ITEM has no LITELLM_KEY field; mcp-config/ and hermes/ need it."
|
||||
|
||||
# Remember the account only after it has actually worked.
|
||||
{
|
||||
|
||||
Executable
+216
@@ -0,0 +1,216 @@
|
||||
#!/usr/bin/env sh
|
||||
# vscode/install.sh — install Visual Studio Code into ~/.local/opt/vscode.
|
||||
#
|
||||
# WHY: VS Code is the one GUI app this repo installs, and the obvious way to get
|
||||
# it — Microsoft's rpm/deb repo — needs root, writes outside $HOME, and is wiped
|
||||
# by a Kasm redeploy, so the editor quietly disappears on every rebuild. The box
|
||||
# this was written for had exactly that half-state: /etc/yum.repos.d/vscode.repo
|
||||
# present, `code` never installed, and nothing in this repo recording that VS
|
||||
# Code was meant to be there at all. Upstream's portable tarball is the same
|
||||
# build with none of that: everything lands under $HOME, no sudo, and it
|
||||
# survives a redeploy. The only thing given up is dnf/apt-driven updates, which
|
||||
# is what the pinned VSCODE_VERSION below replaces.
|
||||
#
|
||||
# It is not in bin/install.sh because that script installs single self-contained
|
||||
# binaries; VS Code is an Electron tree of thousands of files plus a menu entry
|
||||
# and an icon. It follows node/ and ansible/ instead: unpack under ~/.local/opt
|
||||
# and symlink the CLI into ~/.local/bin.
|
||||
#
|
||||
# The Chromium sandbox still works from a userland install: the bundled
|
||||
# chrome-sandbox helper is not setuid here, so Electron falls back to the
|
||||
# unprivileged user-namespace sandbox. That needs user namespaces enabled
|
||||
# (user.max_user_namespaces > 0, the default on every distro this repo targets).
|
||||
# Verified by launching without --no-sandbox. Nothing is given up for dropping
|
||||
# root; if a host ever has userns disabled, VS Code says so on startup.
|
||||
#
|
||||
# Idempotent and safe to re-run. To upgrade, bump VSCODE_VERSION.
|
||||
set -eu
|
||||
|
||||
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
|
||||
REPO_ROOT="$(dirname -- "$SCRIPT_DIR")"
|
||||
|
||||
# shellcheck source=../lib/fetch.sh
|
||||
. "$REPO_ROOT/lib/fetch.sh"
|
||||
|
||||
say() { printf '%s\n' "$*" >&2; }
|
||||
|
||||
# Pinned deliberately. VS Code auto-updates itself on other platforms; a tarball
|
||||
# install does not, which is the point — the editor runs your code and holds
|
||||
# your Git and extension credentials, so the version is a supply-chain decision
|
||||
# rather than whatever upstream served today. `code --version` prints this
|
||||
# string verbatim, so it must match with no leading "v".
|
||||
VSCODE_VERSION="1.139.0"
|
||||
|
||||
PREFIX="$HOME/.local/opt/vscode"
|
||||
DESKTOP_DIR="$HOME/.local/share/applications"
|
||||
ARGV_JSON="$HOME/.vscode/argv.json"
|
||||
|
||||
# has_desktop — true when this host has a graphical session to run a GUI editor
|
||||
# in. VS Code is ~400MB unpacked and $HOME is the thing that persists across
|
||||
# redeploys, so it is not worth carrying on a box that will never display it.
|
||||
# Desktop images ship session files under /usr/share/{x,wayland-}sessions.
|
||||
has_desktop() {
|
||||
[ -n "${XDG_CURRENT_DESKTOP:-}" ] && return 0
|
||||
for d in /usr/share/xsessions /usr/share/wayland-sessions; do
|
||||
[ -d "$d" ] && [ -n "$(ls -A "$d" 2>/dev/null)" ] && return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# --- skip on headless hosts --------------------------------------------------
|
||||
# DOTFILES_VSCODE=1 forces the install anyway (e.g. for `code tunnel` on a
|
||||
# server); DOTFILES_VSCODE=0 skips it even on a desktop.
|
||||
case "${DOTFILES_VSCODE:-auto}" in
|
||||
0) say "vscode: DOTFILES_VSCODE=0; skipping."; exit 0 ;;
|
||||
1) ;;
|
||||
*) if ! has_desktop; then
|
||||
say "vscode: no graphical session detected; skipping (DOTFILES_VSCODE=1 to force)."
|
||||
exit 0
|
||||
fi ;;
|
||||
esac
|
||||
|
||||
# Upstream keys its download URLs on x64/arm64 rather than amd64/arm64.
|
||||
case "$(fetch_arch)" in
|
||||
x86_64) VSCODE_ARCH=x64 ;;
|
||||
aarch64) VSCODE_ARCH=arm64 ;;
|
||||
*) say "vscode: unsupported arch $(fetch_arch)"; exit 1 ;;
|
||||
esac
|
||||
|
||||
# --- install the app tree ----------------------------------------------------
|
||||
# Verify it RUNS, not just that the tree exists: a Kasm redeploy can leave a
|
||||
# present-but-broken directory behind (the same failure node/ and ansible/
|
||||
# guard against).
|
||||
#
|
||||
# The root fallback is not optional. Run as root, `code --version` refuses --
|
||||
# it prints "You are trying to start Visual Studio Code as a super user"
|
||||
# instead of the version, and still exits 0 -- so a bare run-check never
|
||||
# matches and would re-download 325MB on every single bootstrap, silently.
|
||||
# That is the forced-install path (DOTFILES_VSCODE=1 in a container or on a
|
||||
# server), so for root only, fall back to the recorded version stamp.
|
||||
installed_version="$("$PREFIX/bin/code" --version 2>/dev/null | head -1 || true)"
|
||||
if [ "$installed_version" = "$VSCODE_VERSION" ]; then
|
||||
say "vscode: $VSCODE_VERSION already present at $PREFIX."
|
||||
elif [ "$(id -u)" = "0" ] && have_version code "$VSCODE_VERSION"; then
|
||||
say "vscode: $VSCODE_VERSION already present at $PREFIX (stamped; root cannot query --version)."
|
||||
else
|
||||
say "vscode: installing $VSCODE_VERSION into $PREFIX (this is a ~325MB download)..."
|
||||
install_tarball_tree \
|
||||
"https://update.code.visualstudio.com/$VSCODE_VERSION/linux-$VSCODE_ARCH/stable" \
|
||||
"VSCode-linux-$VSCODE_ARCH" "$PREFIX"
|
||||
|
||||
# bin/code is upstream's CLI wrapper (`code .`, `code --install-extension`);
|
||||
# the Electron binary at the tree root is what the menu entry launches.
|
||||
mkdir -p "$BIN_DIR"
|
||||
ln -sf "$PREFIX/bin/code" "$BIN_DIR/code"
|
||||
mark_version code "$VSCODE_VERSION"
|
||||
say "vscode: installed $VSCODE_VERSION and linked $BIN_DIR/code."
|
||||
fi
|
||||
|
||||
# --- menu entry --------------------------------------------------------------
|
||||
# Icon= is an absolute path rather than a theme name: the bundled icon is a
|
||||
# single 1024x1024 PNG and hicolor tops out at 512x512, so there is no correct
|
||||
# themed directory to drop it in. The spec allows an absolute path, and it needs
|
||||
# no icon-cache rebuild to take effect.
|
||||
mkdir -p "$DESKTOP_DIR"
|
||||
cat >"$DESKTOP_DIR/code.desktop" <<DESKTOP
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=Visual Studio Code
|
||||
GenericName=Text Editor
|
||||
Comment=Code Editing. Redefined.
|
||||
Exec=$PREFIX/code %F
|
||||
Icon=$PREFIX/resources/app/resources/linux/code.png
|
||||
Terminal=false
|
||||
StartupNotify=true
|
||||
StartupWMClass=Code
|
||||
Categories=TextEditor;Development;IDE;
|
||||
MimeType=text/plain;inode/directory;
|
||||
Keywords=vscode;editor;
|
||||
Actions=new-window;
|
||||
|
||||
[Desktop Action new-window]
|
||||
Name=New Empty Window
|
||||
Exec=$PREFIX/code --new-window %F
|
||||
Icon=$PREFIX/resources/app/resources/linux/code.png
|
||||
DESKTOP
|
||||
say "vscode: wrote $DESKTOP_DIR/code.desktop"
|
||||
|
||||
# Refresh the per-user menu caches so the entry appears without a re-login.
|
||||
if command -v update-desktop-database >/dev/null 2>&1; then
|
||||
update-desktop-database "$DESKTOP_DIR" 2>/dev/null || true
|
||||
fi
|
||||
if command -v kbuildsycoca5 >/dev/null 2>&1; then
|
||||
kbuildsycoca5 --noincremental >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
# --- keyring prompt suppression ----------------------------------------------
|
||||
# WHY: on first launch Electron's safeStorage reaches for the OS keyring through
|
||||
# libsecret. On a KDE session that wakes kwalletd, which pops an unlock/create
|
||||
# dialog before the editor is usable -- and since kwallet-pam is not installed
|
||||
# on these hosts, nothing unlocks the wallet at login, so it prompts every
|
||||
# session. "basic" keeps VS Code's secrets in its own store and never reaches
|
||||
# for the wallet at all. Verified with dbus-monitor: no org.kde.KWallet traffic
|
||||
# on launch.
|
||||
#
|
||||
# TRADE-OFF: basic is weaker than a real keyring. Secrets VS Code stores (Git
|
||||
# and GitHub auth tokens, extension credentials) end up obfuscated on disk
|
||||
# rather than sealed behind a wallet password. To move to the wallet later,
|
||||
# install kwallet-pam so it unlocks at login, then change the value to
|
||||
# "kwallet5" and restart VS Code.
|
||||
#
|
||||
# The path comes from product.json's dataFolderName (".vscode"), NOT the
|
||||
# user-data-dir: argv.json lives at ~/.vscode/argv.json and a copy written
|
||||
# anywhere else is read by nothing and silently ignored.
|
||||
if [ ! -e "$ARGV_JSON" ]; then
|
||||
mkdir -p "$(dirname "$ARGV_JSON")"
|
||||
cat >"$ARGV_JSON" <<'ARGV'
|
||||
// Runtime arguments for VS Code. See dotfiles vscode/install.sh.
|
||||
{
|
||||
// Keep secrets in VS Code's own store rather than the OS keyring, so KDE's
|
||||
// kwallet does not prompt for an unlock on every launch.
|
||||
"password-store": "basic"
|
||||
}
|
||||
ARGV
|
||||
say "vscode: wrote $ARGV_JSON (password-store=basic)"
|
||||
elif grep -q '"password-store"' "$ARGV_JSON"; then
|
||||
say "vscode: $ARGV_JSON already sets password-store; leaving it alone."
|
||||
else
|
||||
# Merge rather than overwrite: this file is JSON-with-comments that VS Code
|
||||
# and the user both own, and it carries a crash-reporter-id that should
|
||||
# survive. Insert the key after the opening brace and keep the rest byte for
|
||||
# byte.
|
||||
cp -p "$ARGV_JSON" "$ARGV_JSON.bak"
|
||||
if awk '
|
||||
BEGIN { done = 0 }
|
||||
{
|
||||
print
|
||||
if (!done && $0 ~ /^[[:space:]]*\{[[:space:]]*$/) {
|
||||
print "\t// Added by dotfiles vscode/install.sh: keep secrets in VS Code'"'"'s"
|
||||
print "\t// own store so kwallet does not prompt for an unlock on every launch."
|
||||
print "\t\"password-store\": \"basic\","
|
||||
done = 1
|
||||
}
|
||||
}
|
||||
END { exit(done ? 0 : 1) }
|
||||
' "$ARGV_JSON.bak" >"$ARGV_JSON.new"; then
|
||||
mv "$ARGV_JSON.new" "$ARGV_JSON"
|
||||
say "vscode: set password-store=basic in $ARGV_JSON (backup: $ARGV_JSON.bak)"
|
||||
else
|
||||
rm -f "$ARGV_JSON.new"
|
||||
say "vscode: WARNING: could not find the object opening in $ARGV_JSON."
|
||||
say " Add '\"password-store\": \"basic\"' by hand to stop the keyring prompt."
|
||||
fi
|
||||
fi
|
||||
|
||||
# A system-wide VS Code (from Microsoft's rpm/deb repo) shadows this one in the
|
||||
# menu and shows a duplicate entry. Flag it rather than touching it -- removing
|
||||
# it needs the root this module deliberately avoids.
|
||||
if [ -e /usr/share/applications/com.microsoft.VSCode.desktop ]; then
|
||||
say ""
|
||||
say "vscode: NOTE: a system-wide VS Code package is also installed."
|
||||
say " You will see two menu entries until it is removed:"
|
||||
say " sudo dnf remove code # or: sudo apt remove code"
|
||||
say ""
|
||||
fi
|
||||
|
||||
say "vscode: ready."
|
||||
@@ -2,19 +2,6 @@
|
||||
# overwritten on the next bootstrap run. Edit zsh/zshrc in the dotfiles repo
|
||||
# instead.
|
||||
|
||||
# If this account was added to the docker group (dotfiles/docker/install.sh)
|
||||
# but this shell's session predates that, re-exec via newgrp so docker works
|
||||
# without a full logout. Guarded: fires only when the group is on the account
|
||||
# yet missing from the running shell's groups, so the newgrp'd shell (which
|
||||
# has the group) cannot loop.
|
||||
# Only for real terminals ([ -t 0 ]): newgrp replaces the shell and would
|
||||
# swallow the command in scripted `zsh -i -c ...` invocations.
|
||||
if [ -t 0 ] && [ -S /var/run/docker.sock ] \
|
||||
&& id -nG "${USER:-$(id -un)}" 2>/dev/null | grep -qw docker \
|
||||
&& ! id -Gn 2>/dev/null | grep -qw docker; then
|
||||
exec newgrp docker
|
||||
fi
|
||||
|
||||
# Userland tools installed by dotfiles/bin/install.sh live here.
|
||||
case ":$PATH:" in
|
||||
*":$HOME/.local/bin:"*) ;;
|
||||
@@ -82,9 +69,10 @@ _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 (prompts, hidden, when VALUE is
|
||||
# omitted, which keeps the secret out of shell history),
|
||||
# then refresh and reload this shell
|
||||
# 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)
|
||||
@@ -107,19 +95,47 @@ opr() {
|
||||
opc() {
|
||||
emulate -L zsh
|
||||
local name=$1 value
|
||||
_dotfiles_env_name "$name" opc || return
|
||||
if (( $# >= 2 )); then
|
||||
if [[ $1 == *=* ]]; then
|
||||
# NAME=VALUE: split on the first '=' so values may contain '='
|
||||
name=${1%%=*} value=${1#*=}
|
||||
elif (( $# >= 2 )); then
|
||||
value=$2
|
||||
else
|
||||
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.
|
||||
_dotfiles_op sh -c 'op item edit "$OP_ITEM" --vault "$OP_VAULT" "$1[concealed]=$2" >/dev/null' \
|
||||
opc "$name" "$value" && ops
|
||||
# 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
|
||||
_dotfiles_op sh -c 'op item edit "$OP_ITEM" --vault "$OP_VAULT" "$1[delete]" >/dev/null' opd "$1" \
|
||||
# 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() {
|
||||
|
||||
Reference in New Issue
Block a user