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.
This commit is contained in:
Brian Majewski 2026-06-08 16:23:50 -07:00
parent 6319313ee4
commit 7b041af68b
2 changed files with 185 additions and 0 deletions

113
install.sh Executable file
View file

@ -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 <<EOF
install.sh — finish setting up the 'secrets' tool.
Run this once after cloning the repo. It verifies dependencies (age, jq, git)
and prints the commands to put 'secrets' on your PATH and onboard a machine.
Usage:
./install.sh Check dependencies and print setup + next steps
./install.sh --help Show this help
It never edits your shell config and never runs sudo — it prints the exact
commands so you stay in control (this is a secrets tool, after all).
Onboarding after setup:
First machine: secrets init --remote <your-private-repo-url>
Other machine: secrets join --remote <your-private-repo-url> --key <key.txt>
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 <<EOF
All dependencies present. Two steps to finish:
1) Put 'secrets' on your PATH. Add this line to your shell config
(~/.zshrc or ~/.bashrc), then restart your terminal:
export PATH="$TOOL_DIR:\$PATH"
2) Onboard this machine:
First machine (new vault):
secrets init --remote <your-private-repo-url>
# then transfer key.txt to your other machines (AirDrop / scp / USB):
# scp <this-host>:$HOME/.secrets/key.txt ~/.secrets/key.txt
Other machine (join an existing vault):
secrets join --remote <your-private-repo-url> --key <path-to-key.txt>
# '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

72
test/install.bats Normal file
View file

@ -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
}