Keep the personal half of the desktop in one place, and ask before installing it

Agent instructions, skills, SSH host aliases and expansion triggers are worth
having identical on every machine one person owns, and belong in none of the
shared configuration. They live in user/ now, with a manifest saying where each
piece goes and a link-user stage that puts it there.

That stage does nothing unless the machine said yes. Somebody who clones Panama
to try the desktop keeps their own ~/.claude/CLAUDE.md exactly where it was;
the question names the destinations and defaults to no. Anything displaced goes
to config/old rather than being deleted.

~/.claude/CLAUDE.md and ~/.codex/AGENTS.md were byte-identical copies of one
file, which is the drift this exists to prevent.

Also adds the vitals toggles for the battery and Claude usage readouts, which
had preferences and no way to reach them.
This commit is contained in:
Gabriel Brown
2026-08-22 08:54:43 -04:00
parent 8b96d907a1
commit 89761a7da3
156 changed files with 16439 additions and 6 deletions
+101
View File
@@ -0,0 +1,101 @@
---
name: wizard
description: Use when a procedure needs a human at the keyboard, meaning provisioning infrastructure, setting up credentials or CI secrets, clicking through an unfamiliar third-party dashboard, or running a one-off migration or cutover. Not for steps you can perform yourself.
---
# Wizard
A **wizard** is a bash script that walks a human, step by step, through a manual procedure that is
tedious to do by hand and tedious to re-explain to an agent every time. It opens each URL, says
exactly what to click and copy, captures the values, writes them where they belong, confirms before
anything irreversible, and shows how many stages are left.
The UX is already solved by [template.sh](template.sh): stage-by-stage progress, confirmation gates,
cross-platform URL opening, hidden secret entry, idempotent `.env` upserts, CI secret writes, and a
closing summary of what was set and what still needs doing by hand.
**Your job is only to scope the procedure and author its stages.** The library above the `STAGES`
marker is identical in every wizard, and that sameness is the point. Never hand-edit it.
A wizard is ephemeral by default: built for one run, saved to a scratch path or `scripts/`, deleted
when the job is done. Commit it only where the user wants a repeatable setup path living in the repo.
**Where you could just do it yourself, do it yourself.** This is for the steps where a human is
genuinely in the loop.
## 1. Scope the procedure
Work out every manual step the human must take and every value captured along the way. Read the repo
first rather than asking cold:
- **For setup**: `.env`, `.env.example`, `.env.*`, the README, `docker-compose*` or `Containerfile`,
framework config, and CI workflow files. Every `secrets.*` or `$CI_` reference is a value the wizard
must produce.
- **For a migration or cutover**: the current state, the target state, and every irreversible action
in between.
Then show the ordered list of stages and the values each produces, and confirm. They may add, drop,
or reorder.
**Done when**, for every captured value, you know where the human gets it, where it is written
(`.env`, a CI secret, Infisical, several of those, or nowhere for a pure action stage), and whether it
is secret and so needs hidden entry.
## 2. Map each stage's journey
For each stage, write the precise path a human follows: which URL, what to do there, where the value
is shown, which variable it fills.
> Dashboard → Developers → API keys → Reveal test key → copy
Where you do not actually know the current UI or the exact command, **say so and ask, or check the
docs.** Never invent steps that may not exist: a wizard confidently naming a button that is not there
is worse than no wizard, because the human trusts it.
**Done when** every stage traces to concrete instructions a stranger could follow.
## 3. Author it
Copy `template.sh` to the target path. Replace the example stage with one `stage` per step in
dependency order, and set `TOTAL_STAGES` to match.
Use the library helpers:
| Helper | Does |
|---|---|
| `stage "Name"` | Clears the screen, announces the stage, shows progress |
| `say` / `step` / `note` / `warn` | Instruction lines |
| `open_url URL` | Opens it in the human's browser |
| `ask KEY "Prompt"` / `ask_secret KEY "Prompt"` | Reads a value, hidden for secrets, offering the existing `.env` value on a re-run |
| `write_env KEY VALUE` | Idempotent upsert into `.env` |
| `set_secret` / `set_var` | Writes a CI secret or variable, detecting GitHub or GitLab from the git remote |
| `set_infisical KEY VALUE` | Writes to Infisical, honouring `INFISICAL_ENV` (default `dev`) |
| `pause` / `confirm` | Waits, or gates an irreversible action behind y/N |
Hold the bar the template sets: open the URL **before** asking for its value, `ask_secret` anything
secret, `write_env` every persisted value, `set_secret` only what CI actually needs, and `confirm`
before anything irreversible.
Each `stage` clears the screen, so keep a stage to one focused task and nothing the human still needs
scrolls away.
**Verify the CLI invocations you rely on.** `set_secret` degrades to a warning rather than failing,
so a wrong flag silently becomes a manual to-do. Where a stage depends on a `glab`, `gh`, or
`infisical` command, confirm the syntax against its `--help` before shipping.
## 4. Verify and hand off
- `bash -n <script>`, and `shellcheck` where it is installed
- `chmod +x <script>`
- **Do not run it end to end yourself.** It opens browsers and blocks on human input. Trace it
statically instead: every value from step 1 is captured, lands where step 1 said it would, and
every `set_secret` name exactly matches the reference in CI.
- Tell the user how to run it. Where it is a repeatable setup path, commit it and link it from the
README so the next person runs the script instead of asking an agent.
## Done when
- Every stage is a step only a human can take.
- Every captured value has a known source, a known destination, and correct secrecy.
- No step names a button or command you have not verified exists.
- The script passes `bash -n`, is executable, and was traced statically rather than run.
@@ -0,0 +1,3 @@
interface:
display_name: Wizard
short_description: Generate an interactive bash wizard for steps only a human can perform.
+264
View File
@@ -0,0 +1,264 @@
#!/usr/bin/env bash
#
# A wizard walks a human through a manual procedure, step by step.
# Generated by the /wizard skill.
#
# Everything above the "STAGES" marker is the wizard library: do not hand-edit
# it. Author the per-step stages below the marker.
set -euo pipefail
# ──────────────────────────────────────────────────────────────────────────
# Wizard library: delightful, consistent UX, identical across every wizard.
# ──────────────────────────────────────────────────────────────────────────
if [[ -t 1 ]] && command -v tput >/dev/null 2>&1 && [[ "$(tput colors 2>/dev/null || echo 0)" -ge 8 ]]; then
BOLD=$(tput bold); DIM=$(tput dim); RESET=$(tput sgr0)
BLUE=$(tput setaf 4); GREEN=$(tput setaf 2); YELLOW=$(tput setaf 3); RED=$(tput setaf 1)
else
BOLD=""; DIM=""; RESET=""; BLUE=""; GREEN=""; YELLOW=""; RED=""
fi
# Author sets this at the top of the stages section.
TOTAL_STAGES=0
_STAGE_INDEX=0
ENV_FILE="${ENV_FILE:-.env}"
WRITTEN_ENV=() # KEYs written to ENV_FILE this run
WRITTEN_SECRET=() # secret NAMEs set this run
SKIPPED=() # things we couldn't do (e.g. gh missing)
# _clear wipes the terminal so only the current step is on screen. No-op when
# output isn't a terminal, so piped logs stay readable.
_clear() {
[[ -t 1 ]] || return 0
if command -v tput >/dev/null 2>&1; then tput clear; else printf '\033[2J\033[3J\033[H'; fi
}
# banner "Title" shows the opening frame: what this wizard does.
banner() {
_clear
printf '\n%s%s %s%s\n' "$BOLD" "$BLUE" "$1" "$RESET"
printf '%s %s stages%s\n\n' "$DIM" "$TOTAL_STAGES" "$RESET"
printf '%s You drive the browser; this wizard tells you exactly what to do and\n' "$DIM"
printf ' captures the values you copy back. Stop any time with Ctrl-C and re-run\n'
printf ' later, since it remembers values already saved.%s\n' "$RESET"
pause "Ready to start?"
}
# stage "Name" clears the screen, then announces a stage and shows progress.
# Clearing keeps only the current step on screen.
stage() {
_clear
_STAGE_INDEX=$((_STAGE_INDEX + 1))
printf '\n%s%s▸ Stage %s/%s · %s%s\n' \
"$BOLD" "$BLUE" "$_STAGE_INDEX" "$TOTAL_STAGES" "$1" "$RESET"
}
# say "..." prints a plain instruction line.
say() { printf ' %s\n' "$1"; }
# step "..." is a numbered-feeling action the human takes in the browser.
step() { printf ' %s•%s %s\n' "$BLUE" "$RESET" "$1"; }
note() { printf ' %s%s%s\n' "$DIM" "$1" "$RESET"; }
warn() { printf ' %s⚠ %s%s\n' "$YELLOW" "$1" "$RESET"; }
# open_url URL opens it in the human's browser, cross-platform incl. WSL.
open_url() {
local url="$1"
printf ' %s↗ opening%s %s\n' "$GREEN" "$RESET" "$url"
{ if command -v wslview >/dev/null 2>&1; then wslview "$url"
elif command -v explorer.exe >/dev/null 2>&1; then explorer.exe "$url"
elif command -v xdg-open >/dev/null 2>&1; then xdg-open "$url"
elif command -v open >/dev/null 2>&1; then open "$url"
else warn "couldn't open a browser; visit it manually: $url"; fi
} >/dev/null 2>&1 || warn "couldn't open a browser, so visit it manually: $url"
}
# pause "msg" waits for the human to confirm they've done the manual part.
pause() {
printf ' %s%s%s ' "$DIM" "${1:-Press Enter to continue}" "$RESET"
read -r _ || true
}
# confirm "question" is a y/N gate; returns success on yes.
confirm() {
local reply=""
printf ' %s? %s [y/N] ' "$YELLOW" "$1"
read -r reply || true
[[ "$reply" =~ ^[Yy] ]]
}
# _existing KEY: current value of KEY in ENV_FILE, if any.
_existing() {
[[ -f "$ENV_FILE" ]] || return 1
local line; line=$(grep -E "^${1}=" "$ENV_FILE" | tail -n1) || return 1
printf '%s' "${line#*=}"
}
# ask KEY "Prompt" reads a value into $KEY. Offers the existing .env value as
# a default on re-runs (Enter keeps it). Visible input (non-secret).
ask() {
local key="$1" prompt="$2" current input
current=$(_existing "$key" || true)
if [[ -n "$current" ]]; then
printf ' %s%s%s %s[Enter keeps current]%s ' "$BOLD" "$prompt" "$RESET" "$DIM" "$RESET"
else
printf ' %s%s%s ' "$BOLD" "$prompt" "$RESET"
fi
read -r input || true
[[ -z "$input" && -n "$current" ]] && input="$current"
printf -v "$key" '%s' "$input"
}
# ask_secret KEY "Prompt" is like ask, but input is hidden.
ask_secret() {
local key="$1" prompt="$2" current input
current=$(_existing "$key" || true)
if [[ -n "$current" ]]; then
printf ' %s%s%s %s[Enter keeps current]%s ' "$BOLD" "$prompt" "$RESET" "$DIM" "$RESET"
else
printf ' %s%s%s ' "$BOLD" "$prompt" "$RESET"
fi
read -rs input || true
printf '\n'
[[ -z "$input" && -n "$current" ]] && input="$current"
printf -v "$key" '%s' "$input"
}
# write_env KEY VALUE upserts KEY=VALUE into ENV_FILE (creates it; replaces
# any existing line). Idempotent.
write_env() {
local key="$1" value="$2" tmp
touch "$ENV_FILE"
tmp=$(mktemp)
grep -vE "^${key}=" "$ENV_FILE" > "$tmp" || true
printf '%s=%s\n' "$key" "$value" >> "$tmp"
mv "$tmp" "$ENV_FILE"
WRITTEN_ENV+=("$key")
printf ' %s✓ wrote%s %s → %s\n' "$GREEN" "$RESET" "$key" "$ENV_FILE"
}
# _forge detects the CI host from the git remote: "github", "gitlab", or "".
_forge() {
local remote
remote=$(git remote get-url origin 2>/dev/null) || return 1
case "$remote" in
*github.com*) printf 'github' ;;
*gitlab*) printf 'gitlab' ;;
*) return 1 ;;
esac
}
# set_secret NAME VALUE sets a masked CI secret on whichever forge this repo
# lives on. Falls back to recording it as a manual to-do.
set_secret() {
local name="$1" value="$2" forge
forge=$(_forge || true)
case "$forge" in
github)
if command -v gh >/dev/null 2>&1 && gh auth status >/dev/null 2>&1; then
if printf '%s' "$value" | gh secret set "$name" >/dev/null 2>&1; then
WRITTEN_SECRET+=("$name")
printf ' %s✓ set%s GitHub secret %s\n' "$GREEN" "$RESET" "$name"
return
fi
fi
SKIPPED+=("GitHub secret $name (set it manually: gh secret set $name)")
warn "skipped GitHub secret $name: gh not ready; set it later"
;;
gitlab)
if command -v glab >/dev/null 2>&1 && glab auth status >/dev/null 2>&1; then
if glab variable set "$name" "$value" --masked >/dev/null 2>&1; then
WRITTEN_SECRET+=("$name")
printf ' %s✓ set%s GitLab masked variable %s\n' "$GREEN" "$RESET" "$name"
return
fi
fi
SKIPPED+=("GitLab masked variable $name (set it manually: glab variable set $name --masked)")
warn "skipped GitLab variable $name: glab not ready; set it later"
;;
*)
SKIPPED+=("CI secret $name (no GitHub/GitLab remote detected)")
warn "no recognised git remote; set CI secret $name by hand"
;;
esac
}
# set_var NAME VALUE sets a non-secret CI variable on the detected forge.
set_var() {
local name="$1" value="$2" forge
forge=$(_forge || true)
case "$forge" in
github)
if command -v gh >/dev/null 2>&1 && gh auth status >/dev/null 2>&1; then
if gh variable set "$name" --body "$value" >/dev/null 2>&1; then
printf ' %s✓ set%s GitHub variable %s\n' "$GREEN" "$RESET" "$name"
return
fi
fi
;;
gitlab)
if command -v glab >/dev/null 2>&1 && glab auth status >/dev/null 2>&1; then
if glab variable set "$name" "$value" >/dev/null 2>&1; then
printf ' %s✓ set%s GitLab variable %s\n' "$GREEN" "$RESET" "$name"
return
fi
fi
;;
esac
SKIPPED+=("CI variable $name")
warn "skipped CI variable $name; set it later"
}
# set_infisical KEY VALUE stores a secret in Infisical for the current
# project/environment. Requires `infisical login` and an initialised project.
# INFISICAL_ENV defaults to dev; override before calling.
set_infisical() {
local key="$1" value="$2" env="${INFISICAL_ENV:-dev}"
if command -v infisical >/dev/null 2>&1; then
if infisical secrets set "$key=$value" --env="$env" >/dev/null 2>&1; then
WRITTEN_SECRET+=("$key (infisical/$env)")
printf ' %s✓ set%s Infisical secret %s in %s\n' "$GREEN" "$RESET" "$key" "$env"
return
fi
fi
SKIPPED+=("Infisical secret $key (set it manually: infisical secrets set $key=... --env=$env)")
warn "skipped Infisical secret $key; set it later"
}
# finish clears, then shows a closing summary of everything configured.
finish() {
_clear
printf '\n%s%s ✓ Setup complete%s\n' "$BOLD" "$GREEN" "$RESET"
(( ${#WRITTEN_ENV[@]} )) && note "wrote ${#WRITTEN_ENV[@]} value(s) to $ENV_FILE: ${WRITTEN_ENV[*]}"
(( ${#WRITTEN_SECRET[@]} )) && note "set ${#WRITTEN_SECRET[@]} GitHub secret(s): ${WRITTEN_SECRET[*]}"
if (( ${#SKIPPED[@]} )); then
printf '\n'; warn "still to do by hand:"
for s in "${SKIPPED[@]}"; do note " - $s"; done
fi
printf '\n'
}
# ──────────────────────────────────────────────────────────────────────────
# STAGES: author this section. One stage() per step the human takes.
# Replace the example below. Set TOTAL_STAGES to match the stages you write.
# ──────────────────────────────────────────────────────────────────────────
TOTAL_STAGES=1
banner "Stripe setup"
# ── Example stage: replace with your real steps ───────────────────────────
stage "Stripe: API keys"
say "We'll grab your Stripe test keys and store them for local dev + CI."
open_url "https://dashboard.stripe.com/test/apikeys"
step "On the API keys page, copy the Publishable key (starts pk_test_)."
ask STRIPE_PUBLISHABLE_KEY "Paste the publishable key:"
step "Click 'Reveal test key' on the Secret key row, then copy it."
ask_secret STRIPE_SECRET_KEY "Paste the secret key:"
write_env STRIPE_PUBLISHABLE_KEY "$STRIPE_PUBLISHABLE_KEY"
write_env STRIPE_SECRET_KEY "$STRIPE_SECRET_KEY"
set_secret STRIPE_SECRET_KEY "$STRIPE_SECRET_KEY" # CI needs this one
# ──────────────────────────────────────────────────────────────────────────
finish