From 7b041af68b2e13ee4c1281a6be3a4463def14297 Mon Sep 17 00:00:00 2001 From: Brian Majewski Date: Mon, 8 Jun 2026 16:23:50 -0700 Subject: [PATCH] feat: thin install.sh onboarding bootstrap (EGB-671) Ships in the repo (clone already done). Checks age + jq + git, then PRINTS the PATH line, onboarding next-steps, upgrade one-liner, and key-transfer hint. Never edits shell rc, never runs sudo (prints the command). Exits non-zero with an install hint when a dependency is missing. --- install.sh | 113 ++++++++++++++++++++++++++++++++++++++++++++++ test/install.bats | 72 +++++++++++++++++++++++++++++ 2 files changed, 185 insertions(+) create mode 100755 install.sh create mode 100644 test/install.bats diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..695fc59 --- /dev/null +++ b/install.sh @@ -0,0 +1,113 @@ +#!/usr/bin/env bash +# +# secrets — thin onboarding bootstrap (EGB-671). +# +# This script ships INSIDE the repo: you already cloned the repo to get it, so +# its only jobs are (1) verify the dependencies the tool needs and (2) print the +# exact commands to finish setup. It deliberately does NOT: +# - edit your shell rc files (it prints the PATH line for you to paste) +# - invoke sudo or install packages behind your back (it prints the command) +# - re-implement any of the tool's security logic +# +# This is a security tool whose whole pitch is "verify, don't trust" — so the +# installer holds itself to a higher bar than convenience, not a lower one. +# +# Usage: +# ./install.sh # check deps, print setup + next steps +# ./install.sh --help + +set -euo pipefail + +# Resolve the directory this script lives in (the cloned tool repo). Uses bash +# builtins only so it works under a minimal PATH. +_src="${BASH_SOURCE[0]}" +TOOL_DIR="$(cd "${_src%/*}" 2>/dev/null && pwd)" + +usage() { + cat < + Other machine: secrets join --remote --key +EOF +} + +# Print the install command for a package, using whatever package manager is +# present. For sudo-requiring managers we PRINT the line for you to run — the +# installer never escalates on its own. +install_hint() { + local pkg="$1" + if command -v brew >/dev/null 2>&1; then + echo "brew install $pkg" + elif command -v apt-get >/dev/null 2>&1; then + echo "sudo apt-get install -y $pkg" + elif command -v dnf >/dev/null 2>&1; then + echo "sudo dnf install -y $pkg" + else + echo "install '$pkg' with your system package manager" + fi +} + +case "${1:-}" in + --help|-h) usage; exit 0 ;; + "") ;; + *) echo "Unknown option: $1" >&2; usage >&2; exit 2 ;; +esac + +echo "secrets — bootstrap check (tool dir: $TOOL_DIR)" +echo "" + +# Dependency check. age + jq + git are all load-bearing on the cold-start path: +# jq became required once .secrets.json (manifest) is JSON, so it must be present +# BEFORE the first manifest read. +missing=0 +for dep in git age jq; do + if command -v "$dep" >/dev/null 2>&1; then + echo " ok $dep" + else + echo " MISSING $dep — install it with:" + echo " $(install_hint "$dep")" + missing=1 + fi +done +echo "" + +if [ "$missing" -ne 0 ]; then + echo "Install the missing dependencies above, then re-run ./install.sh." >&2 + exit 1 +fi + +cat < + # then transfer key.txt to your other machines (AirDrop / scp / USB): + # scp :$HOME/.secrets/key.txt ~/.secrets/key.txt + + Other machine (join an existing vault): + secrets join --remote --key + # 'join' clones the vault, installs the key, and VERIFIES it decrypts + # before declaring success — a mis-copied key fails loudly, not silently. + +To update the tool later: + git -C "$TOOL_DIR" pull +EOF diff --git a/test/install.bats b/test/install.bats new file mode 100644 index 0000000..0ce7593 --- /dev/null +++ b/test/install.bats @@ -0,0 +1,72 @@ +#!/usr/bin/env bats +# EGB-671: install.sh thin bootstrap. It ships IN the repo (you clone the repo +# to get it), so its job is: verify deps (age + jq + git), PRINT the PATH line +# and next-step commands — never edit dotfiles, never invoke sudo. Security-rail +# concerns are operator-local (.ship-policy.json); these are functional checks. + +load test_helper + +INSTALL_SH="$(cd "$(dirname "${BATS_TEST_FILENAME}")/.." && pwd)/install.sh" + +@test "install.sh exists and is executable" { + [ -f "$INSTALL_SH" ] + [ -x "$INSTALL_SH" ] +} + +@test "install.sh --help prints usage and exits 0" { + run "$INSTALL_SH" --help + [ "$status" -eq 0 ] + [[ "$output" == *"install.sh"* ]] || false + [[ "$output" == *"join"* ]] || false +} + +@test "install.sh prints the PATH export line for the tool dir (does not edit rc)" { + local tool_dir + tool_dir="$(cd "$(dirname "$INSTALL_SH")" && pwd)" + run "$INSTALL_SH" + [ "$status" -eq 0 ] + [[ "$output" == *"export PATH="* ]] || false + [[ "$output" == *"$tool_dir"* ]] || false + # It must NOT have written to any shell rc in the isolated HOME. + [ ! -f "$HOME/.zshrc" ] + [ ! -f "$HOME/.bashrc" ] +} + +@test "install.sh prints both onboarding next-steps (init --remote and join)" { + run "$INSTALL_SH" + [ "$status" -eq 0 ] + [[ "$output" == *"secrets init --remote"* ]] || false + [[ "$output" == *"secrets join --remote"* ]] || false +} + +@test "install.sh prints the upgrade one-liner" { + run "$INSTALL_SH" + [ "$status" -eq 0 ] + [[ "$output" == *"git -C"* ]] || false + [[ "$output" == *"pull"* ]] || false +} + +@test "install.sh prints a key-transfer hint" { + run "$INSTALL_SH" + [ "$status" -eq 0 ] + [[ "$output" == *"key.txt"* ]] || false +} + +@test "install.sh never invokes sudo (prints it for the user instead)" { + # No executed 'sudo' — any sudo reference must be quoted guidance text. + run grep -nE '^[[:space:]]*sudo ' "$INSTALL_SH" + [ "$status" -ne 0 ] +} + +@test "install.sh reports a missing dependency with an install hint and non-zero exit" { + # Build a minimal PATH that has the tools install.sh needs but NOT jq. + local fake="$TEST_TMPDIR/fakebin" + mkdir -p "$fake" + for t in bash uname env cat grep sed tr dirname command age git printf; do + src="$(command -v "$t" 2>/dev/null || true)" + [ -n "$src" ] && ln -sf "$src" "$fake/$t" 2>/dev/null || true + done + run env PATH="$fake" "$INSTALL_SH" + [ "$status" -ne 0 ] + [[ "$output" == *"jq"* ]] || false +}