bin/: swap seed tools for devops CLIs; add ansible/ module

Replace the placeholder ripgrep/fd/bat/fzf set with devops tooling, keeping
jq: kubectl, helm, k9s, kubectx/kubens, stern, yq, OpenTofu, and terragrunt
— all self-contained, statically linked release binaries in ~/.local/bin,
so they persist across Kasm redeploys with no reinstall.

Switch idempotency from parsing each tool's --version (kubectl/helm/k9s have
no uniform flag) to a version stamp under ~/.local/bin/.versions, and add
fetch_arch_deb() for the amd64/arm64 naming most devops tools use.

Ansible is Python, not a single binary, so it gets its own module: a venv at
~/.local/opt/ansible with its CLIs symlinked into ~/.local/bin. The guard
verifies ansible actually runs (a redeploy can wipe the system python the
venv links to) and rebuilds if not.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-08 10:34:01 -04:00
co-authored by Claude Opus 4.8
parent fd88d8bba2
commit ebdc213444
5 changed files with 206 additions and 75 deletions
+11 -4
View File
@@ -30,11 +30,18 @@ Already have a clone? Just run it directly:
- `git/` — sets git `user.name`/`user.email` and configures - `git/` — sets git `user.name`/`user.email` and configures
`credential.helper store` (plaintext `~/.git-credentials`, 0600) for `credential.helper store` (plaintext `~/.git-credentials`, 0600) for
HTTPS auth. HTTPS auth.
- `bin/` — installs userland CLI tools (ripgrep, fd, bat, fzf, jq) as - `bin/` — installs userland devops CLIs (jq, kubectl, helm, k9s,
self-contained release binaries into `~/.local/bin`. No sudo, nothing kubectx/kubens, stern, yq, OpenTofu, terragrunt) as self-contained,
statically linked release binaries into `~/.local/bin`. No sudo, nothing
outside `$HOME`, so they persist across Kasm redeploys with no reinstall. outside `$HOME`, so they persist across Kasm redeploys with no reinstall.
Idempotent: pinned versions already present are skipped. `zsh/zshrc` puts Idempotent: each tool records its version under `~/.local/bin/.versions`
`~/.local/bin` on `PATH`. and is skipped when the pinned version is already present. `zsh/zshrc`
puts `~/.local/bin` on `PATH`.
- `ansible/` — installs Ansible into a virtualenv at `~/.local/opt/ansible`
(it's Python, not a single binary) and symlinks its CLIs into
`~/.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).
- `mcp-config/` — shared MCP gateway config sync for laptops (LiteLLM / - `mcp-config/` — shared MCP gateway config sync for laptops (LiteLLM /
opencode). Independent of `bootstrap.sh`; see its own README. opencode). Independent of `bootstrap.sh`; see its own README.
+58
View File
@@ -0,0 +1,58 @@
#!/usr/bin/env sh
# ansible/install.sh — install Ansible into a userland virtualenv and expose
# its CLIs on PATH. Ansible is Python, not a single binary, so it can't live
# in bin/ alongside the downloaded release binaries.
#
# The venv lives at ~/.local/opt/ansible and its CLIs are symlinked into
# ~/.local/bin. Both are under $HOME, so they persist across Kasm redeploys;
# the venv is only rebuilt when missing. Building it needs python3 + venv,
# which may require a system package on a fresh box (the one place this
# module uses sudo, via pkg_install).
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; }
VENV="$HOME/.local/opt/ansible"
BIN_DIR="$HOME/.local/bin"
ANSIBLE_CLIS="ansible ansible-playbook ansible-galaxy ansible-vault ansible-config ansible-doc ansible-inventory ansible-console ansible-pull"
# Verify it actually runs, not just that the file exists: a Kasm redeploy can
# wipe the system python3 the venv links to, leaving a present-but-broken
# venv that must be rebuilt.
if "$VENV/bin/ansible" --version >/dev/null 2>&1; then
say "ansible venv already present at $VENV."
else
rm -rf "$VENV"
command -v python3 >/dev/null 2>&1 || pkg_install python3
say "Creating ansible venv at $VENV..."
if ! python3 -m venv "$VENV" 2>/dev/null; then
# Debian splits venv/ensurepip into python3-venv; install it and retry.
say "python venv module missing; installing it..."
case "$(detect_pkg_family)" in
debian) pkg_install python3-venv python3-pip ;;
rhel) pkg_install python3 python3-pip ;;
*) say "install python3 venv support manually"; exit 1 ;;
esac
rm -rf "$VENV"
python3 -m venv "$VENV"
fi
"$VENV/bin/pip" install --quiet --upgrade pip
say "Installing ansible (this pulls in a lot of collections)..."
"$VENV/bin/pip" install --quiet ansible
say "Installed ansible into $VENV."
fi
# Symlink the CLIs into ~/.local/bin (which is on PATH via zsh/zshrc).
mkdir -p "$BIN_DIR"
for cli in $ANSIBLE_CLIS; do
if [ -x "$VENV/bin/$cli" ]; then
ln -sf "$VENV/bin/$cli" "$BIN_DIR/$cli"
fi
done
say "Linked ansible CLIs into $BIN_DIR."
+106 -60
View File
@@ -1,11 +1,14 @@
#!/usr/bin/env sh #!/usr/bin/env sh
# bin/install.sh — install userland CLI tools into ~/.local/bin. # bin/install.sh — install userland devops CLIs into ~/.local/bin.
# #
# Every tool is a self-contained release binary downloaded from upstream and # Every tool is a self-contained release binary downloaded from upstream and
# dropped into ~/.local/bin (no sudo, nothing outside $HOME), so it persists # dropped into ~/.local/bin (no sudo, nothing outside $HOME), so it persists
# across Kasm redeploys. Idempotent: each tool is skipped when the pinned # across Kasm redeploys. Idempotent: each tool records its version under
# version is already installed. To add a tool, pin a version, add an # ~/.local/bin/.versions and is skipped when the pinned version is already
# install_<name> function, and call it at the bottom. # installed. To add a tool, pin a version, add an install_<name> function,
# and call it at the bottom.
#
# Ansible is not a single binary; it is handled separately in ansible/.
set -eu set -eu
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
@@ -14,70 +17,113 @@ REPO_ROOT="$(dirname -- "$SCRIPT_DIR")"
# shellcheck source=../lib/fetch.sh # shellcheck source=../lib/fetch.sh
. "$REPO_ROOT/lib/fetch.sh" . "$REPO_ROOT/lib/fetch.sh"
ARCH="$(fetch_arch)" ARCH="$(fetch_arch)" # x86_64 / aarch64
DEB="$(fetch_arch_deb)" # amd64 / arm64
# ripgrep — fast recursive search. x86_64 ships musl (static); aarch64 gnu.
install_ripgrep() {
ver=15.1.0
have_version rg "$ver" && { fsay "ripgrep $ver already installed."; return 0; }
case "$ARCH" in
x86_64) triple=x86_64-unknown-linux-musl ;;
aarch64) triple=aarch64-unknown-linux-gnu ;;
esac
install_tarball_bin \
"https://github.com/BurntSushi/ripgrep/releases/download/$ver/ripgrep-$ver-$triple.tar.gz" \
"ripgrep-$ver-$triple/rg" rg
}
# fd — fast, user-friendly find. musl (static) on both arches.
install_fd() {
ver=10.4.2
have_version fd "$ver" && { fsay "fd $ver already installed."; return 0; }
triple="$ARCH-unknown-linux-musl"
install_tarball_bin \
"https://github.com/sharkdp/fd/releases/download/v$ver/fd-v$ver-$triple.tar.gz" \
"fd-v$ver-$triple/fd" fd
}
# bat — cat with syntax highlighting. musl (static) on both arches.
install_bat() {
ver=0.26.1
have_version bat "$ver" && { fsay "bat $ver already installed."; return 0; }
triple="$ARCH-unknown-linux-musl"
install_tarball_bin \
"https://github.com/sharkdp/bat/releases/download/v$ver/bat-v$ver-$triple.tar.gz" \
"bat-v$ver-$triple/bat" bat
}
# fzf — general-purpose fuzzy finder (static Go binary).
install_fzf() {
ver=0.74.0
have_version fzf "$ver" && { fsay "fzf $ver already installed."; return 0; }
case "$ARCH" in
x86_64) a=amd64 ;;
aarch64) a=arm64 ;;
esac
install_tarball_bin \
"https://github.com/junegunn/fzf/releases/download/v$ver/fzf-$ver-linux_$a.tar.gz" \
fzf fzf
}
# jq — command-line JSON processor (single static binary). # jq — command-line JSON processor (single static binary).
install_jq() { install_jq() {
ver=1.8.2 ver=1.8.2
have_version jq "$ver" && { fsay "jq $ver already installed."; return 0; } have_version jq "$ver" && { fsay "jq $ver already installed."; return 0; }
case "$ARCH" in
x86_64) a=amd64 ;;
aarch64) a=arm64 ;;
esac
install_raw_bin \ install_raw_bin \
"https://github.com/jqlang/jq/releases/download/jq-$ver/jq-linux-$a" jq "https://github.com/jqlang/jq/releases/download/jq-$ver/jq-linux-$DEB" jq
mark_version jq "$ver"
}
# kubectl — Kubernetes CLI (single static binary from dl.k8s.io).
install_kubectl() {
ver=v1.36.2
have_version kubectl "$ver" && { fsay "kubectl $ver already installed."; return 0; }
install_raw_bin \
"https://dl.k8s.io/release/$ver/bin/linux/$DEB/kubectl" kubectl
mark_version kubectl "$ver"
}
# helm — Kubernetes package manager (from get.helm.sh).
install_helm() {
ver=v4.2.2
have_version helm "$ver" && { fsay "helm $ver already installed."; return 0; }
install_tarball_bin \
"https://get.helm.sh/helm-$ver-linux-$DEB.tar.gz" \
"linux-$DEB/helm" helm
mark_version helm "$ver"
}
# k9s — terminal UI for Kubernetes clusters. Asset uses capital "Linux".
install_k9s() {
ver=v0.51.0
have_version k9s "$ver" && { fsay "k9s $ver already installed."; return 0; }
install_tarball_bin \
"https://github.com/derailed/k9s/releases/download/$ver/k9s_Linux_$DEB.tar.gz" \
k9s k9s
mark_version k9s "$ver"
}
# kubectx / kubens — fast context and namespace switching. Uses x86_64 token.
install_kubectx() {
ver=v0.11.0
have_version kubectx "$ver" && { fsay "kubectx $ver already installed."; return 0; }
install_tarball_bin \
"https://github.com/ahmetb/kubectx/releases/download/$ver/kubectx_${ver}_linux_$ARCH.tar.gz" \
kubectx kubectx
mark_version kubectx "$ver"
}
install_kubens() {
ver=v0.11.0
have_version kubens "$ver" && { fsay "kubens $ver already installed."; return 0; }
install_tarball_bin \
"https://github.com/ahmetb/kubectx/releases/download/$ver/kubens_${ver}_linux_$ARCH.tar.gz" \
kubens kubens
mark_version kubens "$ver"
}
# stern — multi-pod log tailing. Release tag is vX but asset drops the v.
install_stern() {
ver=1.34.0
have_version stern "$ver" && { fsay "stern $ver already installed."; return 0; }
install_tarball_bin \
"https://github.com/stern/stern/releases/download/v$ver/stern_${ver}_linux_$DEB.tar.gz" \
stern stern
mark_version stern "$ver"
}
# yq — YAML/JSON/XML processor (single static binary).
install_yq() {
ver=v4.53.3
have_version yq "$ver" && { fsay "yq $ver already installed."; return 0; }
install_raw_bin \
"https://github.com/mikefarah/yq/releases/download/$ver/yq_linux_$DEB" yq
mark_version yq "$ver"
}
# OpenTofu — open-source Terraform. Asset drops the leading v.
install_tofu() {
ver=1.12.3
have_version tofu "$ver" && { fsay "tofu $ver already installed."; return 0; }
install_tarball_bin \
"https://github.com/opentofu/opentofu/releases/download/v$ver/tofu_${ver}_linux_$DEB.tar.gz" \
tofu tofu
mark_version tofu "$ver"
}
# terragrunt — thin wrapper around tofu/terraform (single static binary).
install_terragrunt() {
ver=v1.1.0
have_version terragrunt "$ver" && { fsay "terragrunt $ver already installed."; return 0; }
install_raw_bin \
"https://github.com/gruntwork-io/terragrunt/releases/download/$ver/terragrunt_linux_$DEB" \
terragrunt
mark_version terragrunt "$ver"
} }
install_ripgrep
install_fd
install_bat
install_fzf
install_jq install_jq
install_kubectl
install_helm
install_k9s
install_kubectx
install_kubens
install_stern
install_yq
install_tofu
install_terragrunt
fsay "Done. Tools in $BIN_DIR (added to PATH by zsh/zshrc)." fsay "Done. Tools in $BIN_DIR (added to PATH by zsh/zshrc)."
+1
View File
@@ -32,3 +32,4 @@ fi
"$SCRIPT_DIR/zsh/install.sh" "$SCRIPT_DIR/zsh/install.sh"
"$SCRIPT_DIR/git/install.sh" "$SCRIPT_DIR/git/install.sh"
"$SCRIPT_DIR/bin/install.sh" "$SCRIPT_DIR/bin/install.sh"
"$SCRIPT_DIR/ansible/install.sh"
+30 -11
View File
@@ -3,18 +3,20 @@
# #
# Everything lands under $HOME, so installed tools survive Kasm redeploys # Everything lands under $HOME, so installed tools survive Kasm redeploys
# (which wipe the root filesystem but keep $HOME) with no reinstall needed. # (which wipe the root filesystem but keep $HOME) with no reinstall needed.
# No sudo, no system packages. Prefer statically linked (musl) release # No sudo, no system packages. Prefer statically linked release binaries so
# binaries so they keep working even after the root fs — and its shared # they keep working even after the root fs — and its shared libraries — is
# libraries — is reset. # reset.
# #
# Source this (callers already run `set -eu`), then use fetch_arch, # Source this (callers already run `set -eu`), then use fetch_arch /
# have_version, install_tarball_bin, and install_raw_bin. # fetch_arch_deb, have_version + mark_version, install_tarball_bin, and
# install_raw_bin.
BIN_DIR="${BIN_DIR:-$HOME/.local/bin}" BIN_DIR="${BIN_DIR:-$HOME/.local/bin}"
VERSION_DIR="$BIN_DIR/.versions"
fsay() { printf '%s\n' "$*" >&2; } fsay() { printf '%s\n' "$*" >&2; }
# Normalize `uname -m` to the canonical arch tokens used below. # Normalize `uname -m` to the tokens rust-style release names use.
fetch_arch() { fetch_arch() {
case "$(uname -m)" in case "$(uname -m)" in
x86_64|amd64) printf 'x86_64\n' ;; x86_64|amd64) printf 'x86_64\n' ;;
@@ -23,6 +25,15 @@ fetch_arch() {
esac esac
} }
# Same, but in the Go/Debian token style most devops tools use.
fetch_arch_deb() {
case "$(uname -m)" in
x86_64|amd64) printf 'amd64\n' ;;
aarch64|arm64) printf 'arm64\n' ;;
*) fsay "fetch: unsupported arch $(uname -m)"; return 1 ;;
esac
}
# Download <url> to <dest-file> using whatever downloader is available. # Download <url> to <dest-file> using whatever downloader is available.
_fetch_dl() { _fetch_dl() {
if command -v curl >/dev/null 2>&1; then if command -v curl >/dev/null 2>&1; then
@@ -34,12 +45,20 @@ _fetch_dl() {
fi fi
} }
# have_version <cmd> <version-substring> — true if ~/.local/bin/<cmd> exists # have_version <name> <version> — true if <name> is installed and was
# and reports the given version, so bootstrap can skip a re-download. # stamped at <version>, so bootstrap can skip a re-download. Versions are
# recorded in ~/.local/bin/.versions rather than parsed from each tool's
# `--version` output, which varies wildly (kubectl/helm/k9s have no uniform
# flag).
have_version() { have_version() {
_bin="$BIN_DIR/$1" [ -x "$BIN_DIR/$1" ] && [ -f "$VERSION_DIR/$1" ] &&
[ -x "$_bin" ] || return 1 [ "$(cat "$VERSION_DIR/$1")" = "$2" ]
"$_bin" --version 2>/dev/null | grep -qF "$2" }
# mark_version <name> <version> — record the installed version.
mark_version() {
mkdir -p "$VERSION_DIR"
printf '%s\n' "$2" >"$VERSION_DIR/$1"
} }
# install_tarball_bin <url> <path-within-archive> <dest-name> — download a # install_tarball_bin <url> <path-within-archive> <dest-name> — download a