#!/usr/bin/env bash # # A wizard — walks a human through a manual procedure step by step. # Generated by the /wizard skill. # # Author the per-step stages below the "STAGES" marker. Adapt and test the # persistence helpers when the target consumer needs a different format. 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 these two at the top of the stages section. TOTAL_STAGES=0 TOTAL_MINUTES=0 _STAGE_INDEX=0 _MINUTES_ELAPSED=0 ENV_FILE="${ENV_FILE:-.env}" WRITTEN_ENV=() # KEYs written to ENV_FILE this run SKIPPED=() # manual steps still outstanding # _clear — wipe 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" — opening frame: what this wizard does and how long it takes. banner() { _clear printf '\n%s%s %s%s\n' "$BOLD" "$BLUE" "$1" "$RESET" printf '%s %s stages · about %s minutes%s\n\n' \ "$DIM" "$TOTAL_STAGES" "$TOTAL_MINUTES" "$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 — it remembers values already saved.%s\n' "$RESET" pause "Ready to start?" } # stage "Name" — clear the screen, then announce a stage and show # progress + time remaining. Clearing keeps only the current step on screen. stage() { _clear _STAGE_INDEX=$((_STAGE_INDEX + 1)) local remaining=$((TOTAL_MINUTES - _MINUTES_ELAPSED)) (( remaining < 0 )) && remaining=0 _MINUTES_ELAPSED=$((_MINUTES_ELAPSED + ${2:-0})) printf '\n%s%s▸ Stage %s/%s · %s%s %s(~%s min left)%s\n' \ "$BOLD" "$BLUE" "$_STAGE_INDEX" "$TOTAL_STAGES" "$1" "$RESET" "$DIM" "$remaining" "$RESET" } # say "..." — a plain instruction line. say() { printf ' %s\n' "$1"; } # step "..." — 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 — open 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 — visit it manually: $url" } # pause "msg" — wait 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 _ || _cancel } _cancel() { printf '\nCancelled; no further steps were run.\n' >&2; exit 1; } _valid_key() { [[ "$1" =~ ^[A-Z_][A-Z0-9_]*$ && "$1" != HOME && "$1" != CODEX_HOME && "$1" != PATH && "$1" != ENV_FILE ]] || { printf 'Invalid or reserved variable name.\n' >&2; return 1; } } # confirm "question" — y/N gate; returns success on yes. confirm() { local reply="" printf ' %s? %s [y/N] ' "$YELLOW" "$1" read -r reply || _cancel [[ "$reply" == y || "$reply" == Y || "$reply" == yes || "$reply" == YES ]] } # _existing KEY — current value of KEY in ENV_FILE, if any. _existing() { [[ -e "$ENV_FILE" ]] || return 0 local line value="" while IFS= read -r line || [[ -n "$line" ]]; do [[ "$line" =~ ^[[:space:]]*(export[[:space:]]+)?${1}[[:space:]]*= ]] || continue value="${line#*=}" # ponytail: only reuse the single-line format we emit; adapt for another dotenv dialect. if [[ "$value" == \"* || "$value" == \`* || "$value" == *$'\r'* ]]; then printf 'Existing value uses an unsupported dotenv format; adapt the helper first.\n' >&2; return 1 elif [[ "$value" == "'"* ]]; then [[ "$value" == *"'" && ${#value} -ge 2 ]] || return 1 value="${value:1:${#value}-2}" [[ "$value" != *"'"* ]] || return 1 else [[ "$value" != *[[:space:]]* && "$value" != *[\'\"\`\\]* ]] || return 1 value="${value%%#*}" fi done < "$ENV_FILE" printf '%s' "$value" } # ask KEY "Prompt" — read 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 _valid_key "$key" || return 1 current=$(_existing "$key") || return 1 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 || _cancel [[ -z "$input" && -n "$current" ]] && input="$current" [[ -n "$input" ]] || { printf 'A value is required.\n' >&2; return 1; } printf -v "$key" '%s' "$input" } # ask_secret KEY "Prompt" — like ask, but input is hidden. ask_secret() { local key="$1" prompt="$2" current input _valid_key "$key" || return 1 current=$(_existing "$key") || return 1 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 || _cancel printf '\n' [[ -z "$input" && -n "$current" ]] && input="$current" [[ -n "$input" ]] || { printf 'A value is required.\n' >&2; return 1; } printf -v "$key" '%s' "$input" } # write_env KEY VALUE — atomic, private single-line dotenv upsert. # Single quotes preserve #, spaces, dollars and backslashes literally. write_env() { local key="$1" value="$2" tmp line _valid_key "$key" || return 1 [[ -n "$value" && "$value" != *"'"* && "$value" != *$'\n'* && "$value" != *$'\r'* ]] || { printf 'Cannot save an empty value, apostrophe, or newline in this dotenv format.\n' >&2; return 1; } _existing "$key" >/dev/null || return 1 tmp=$(mktemp "${ENV_FILE}.XXXXXX") || return 1 if [[ -e "$ENV_FILE" ]] && ! awk -v key="$key" '$0 !~ "^[[:space:]]*(export[[:space:]]+)?" key "[[:space:]]*="' "$ENV_FILE" > "$tmp"; then rm -- "$tmp" return 1 fi if printf "%s='%s'\n" "$key" "$value" >> "$tmp" && mv -- "$tmp" "$ENV_FILE"; then : else rm -- "$tmp" printf 'Could not save dotenv value.\n' >&2; return 1 fi WRITTEN_ENV+=("$key") printf ' %s✓ wrote%s %s → %s\n' "$GREEN" "$RESET" "$key" "$ENV_FILE" } # finish — clear, then a closing summary of everything configured. finish() { _clear if (( ${#SKIPPED[@]} )); then warn 'Setup incomplete' else printf '\n%s%s ✓ Setup complete%s\n' "$BOLD" "$GREEN" "$RESET" fi (( ${#WRITTEN_ENV[@]} )) && note "wrote ${#WRITTEN_ENV[@]} value(s) to $ENV_FILE: ${WRITTEN_ENV[*]}" 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 the two totals to match the stages you write. # ────────────────────────────────────────────────────────────────────────── # Sourcing exposes helpers for isolated checks without opening browsers. if [[ "${BASH_SOURCE[0]}" != "$0" ]]; then return 0; fi TOTAL_STAGES=1 TOTAL_MINUTES=5 banner "Stripe setup" # ── Example stage: replace with your real steps ─────────────────────────── stage "Stripe — API keys" 5 say "We'll grab your Stripe test keys and store them for local development." 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" # ────────────────────────────────────────────────────────────────────────── finish