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.
265 lines
10 KiB
Bash
Executable File
265 lines
10 KiB
Bash
Executable File
#!/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
|