Files
noconnorandClaude Opus 5 72d85eb9ec vscode: install VS Code into ~/.local/opt from upstream's tarball
The box this was written on had a half-finished install: vscode.repo
present under /etc/yum.repos.d, `code` never installed, and nothing in
this repo recording that VS Code was meant to be there. Microsoft's
rpm/deb would also need root, write outside $HOME, and be wiped by a
Kasm redeploy. Upstream's portable tarball is the same build with none
of that, so the module keeps bin/'s contract: no sudo, nothing outside
$HOME, pinned rather than floating.

The Chromium sandbox survives a userland install — the bundled
chrome-sandbox is not setuid, so Electron falls back to the
unprivileged user-namespace sandbox. Verified by launching without
--no-sandbox, so dropping root costs nothing.

Adds install_tarball_tree to lib/fetch.sh: install_tarball_bin picks a
single binary out of an archive, and VS Code only runs from an intact
Electron tree. It stages beside the target and swaps in after
extraction succeeds, so an interrupted run leaves the old version
intact.

Two things the verification caught rather than the design:

  - argv.json lives at ~/.vscode/argv.json (product.json's
    dataFolderName), not under the user-data-dir. A copy written
    anywhere else is read by nothing, so the first attempt at
    suppressing the keyring prompt silently did nothing.

  - Run as root, `code --version` prints a "super user" warning
    instead of the version and still exits 0, so a bare run-check
    never matches and would re-download 325MB on every bootstrap.
    Root falls back to the .versions stamp.

Sets password-store=basic in argv.json so Electron's safeStorage stops
waking kwalletd, which prompts for an unlock on every launch because
kwallet-pam is not installed. Confirmed with dbus-monitor: no
org.kde.KWallet traffic on launch. The trade-off — tokens obfuscated
on disk rather than sealed behind a wallet password — and the route
back to the wallet are documented in the module and the README.

No-ops on headless hosts; DOTFILES_VSCODE overrides either way.

Verified per verify-module: shellcheck -s sh clean, idempotent re-run
on both paths, and fresh install plus re-run as root on oraclelinux:9
and debian:13.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-28 13:08:49 +00:00

142 lines
6.3 KiB
Bash
Executable File

#!/usr/bin/env sh
# bootstrap.sh — entrypoint for configuring a fresh VM (RHEL-family or
# Debian-family) with this repo's dotfiles. Safe to re-run.
#
# curl -fsSL https://git.nic-oconnor.com/public/dotfiles/raw/branch/main/bootstrap.sh | sh
# (or, from a checked-out clone: ./bootstrap.sh)
#
# If the repo is private, export GITEA_KEY first — the self-clone below uses it.
# Without a key the clone falls back to git's credential.helper (set up by
# git/), which prompts once and caches, so the interactive path still works.
set -eu
REPO_URL="${DOTFILES_REPO:-https://git.nic-oconnor.com/public/dotfiles.git}"
DOTFILES_DIR="${DOTFILES_DIR:-$HOME/.dotfiles}"
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
# When curl-piped into `sh`, there is no checkout on disk: $0 is not a real
# path, so the module scripts alongside this file are missing. Clone (or
# update) the repo, then re-exec bootstrap.sh from the checkout.
if [ ! -f "$SCRIPT_DIR/zsh/install.sh" ]; then
if ! command -v git >/dev/null 2>&1; then
echo "bootstrap: git is required to self-clone; install git first" >&2
exit 1
fi
# Authenticated when we have a Gitea PAT, so a PRIVATE dotfiles repo
# bootstraps without the repo having to be world-readable. The token goes
# through the environment rather than `git -c ...` because argv shows up in
# `ps`; it is also not written into .git/config, so the clone leaves no
# credential behind on disk.
git_auth() {
if [ -n "${GITEA_KEY:-}" ]; then
GIT_CONFIG_COUNT=1 \
GIT_CONFIG_KEY_0=http.extraHeader \
GIT_CONFIG_VALUE_0="Authorization: token $GITEA_KEY" \
git "$@"
else
git "$@"
fi
}
if [ -d "$DOTFILES_DIR/.git" ]; then
echo "bootstrap: updating existing checkout at $DOTFILES_DIR"
git_auth -C "$DOTFILES_DIR" pull --ff-only
else
echo "bootstrap: cloning $REPO_URL into $DOTFILES_DIR"
git_auth clone "$REPO_URL" "$DOTFILES_DIR"
fi
exec "$DOTFILES_DIR/bootstrap.sh"
fi
# python3 is a hard dependency of this setup, not an optional extra:
# - lib/fetch.sh needs python3 (or unzip) to extract .zip release assets;
# without either, the 1Password CLI install fails.
# - mcp-config/sync.sh needs it to inject the gateway's discovered models;
# without it opencode comes up with an EMPTY model list.
# Minimal Debian/RHEL images ship neither python3 nor unzip, so install it
# before bin/ runs. Non-fatal: an unsupported OS should degrade, not abort.
if ! command -v python3 >/dev/null 2>&1 && ! command -v unzip >/dev/null 2>&1; then
# shellcheck source=lib/os-detect.sh
. "$SCRIPT_DIR/lib/os-detect.sh"
echo "bootstrap: installing python3 (needed for zip assets and model discovery)"
pkg_install python3 || echo "bootstrap: warning: could not install python3" >&2
fi
# run_module <path> — run one module; on failure, warn and keep going.
#
# 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 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.)
FAILED=""
run_module() {
if ( set -e; "$SCRIPT_DIR/$1/install.sh" ); then return 0; fi
FAILED="$FAILED $1"
echo "bootstrap: WARNING: $1 module failed; continuing." >&2
}
run_module zsh
run_module git
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
# values: mcp-config/ takes LITELLM_KEY from the env instead of prompting for
# it, and GITEA_KEY is there for anything that reads the repo over the API.
run_module secrets
if [ -f "$HOME/.config/dotfiles/secrets.env" ]; then
# shellcheck source=/dev/null
. "$HOME/.config/dotfiles/secrets.env"
fi
# Provides npx, which opencode needs to spawn the npm-based MCP servers in
# mcp-config/. Without it every MCP server silently fails to start.
run_module node
# Deploys the mcp-sync hook and the LiteLLM key. Runs after node/ (the MCP
# servers it configures are npx packages) and BEFORE hermes/, which needs
# LITELLM_KEY exported to point its provider at the gateway.
#
# WHY IT IS IN THIS LIST: it was the one module bootstrap never ran, so the
# shell hook it installs — the thing that exports OPENCODE_CONFIG — could not
# 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.
#
# 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
if [ -n "$FAILED" ]; then
echo "" >&2
echo "bootstrap: WARNING: these modules did NOT complete:$FAILED" >&2
echo "bootstrap: everything else was applied. Fix the cause and re-run." >&2
exit 1
fi
echo "bootstrap: all modules applied." >&2