#!/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" } # set_secret NAME VALUE sets a GitHub Actions repo secret via gh. Falls back # to a warning (and records it) if gh is unavailable or unauthenticated. set_secret() { local name="$1" value="$2" 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" } # set_var NAME VALUE sets a GitHub Actions repo variable (non-secret). set_var() { local name="$1" value="$2" 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 SKIPPED+=("GitHub variable $name") warn "skipped GitHub variable $name, gh not ready; 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=2 banner "Google Sign-In setup" BETTER_AUTH_URL_VALUE=$(_existing BETTER_AUTH_URL || true) BETTER_AUTH_URL_VALUE="${BETTER_AUTH_URL_VALUE:-http://localhost:3000}" REDIRECT_URI="${BETTER_AUTH_URL_VALUE}/api/auth/callback/google" # ── Stage 1: OAuth consent screen ────────────────────────────────────────── stage "Google Cloud: OAuth consent screen" say "Google won't let you create an OAuth client until a consent screen exists" say "for the project. Skip straight through if yours already has one." open_url "https://console.cloud.google.com/apis/credentials/consent" step "Select or create a Google Cloud project for this app." step "User type: 'External' (or 'Internal' if you're on a Workspace org and" step " only your org's users will ever sign in)." step "Fill in App name, User support email, and Developer contact email," step " then click through Save on the remaining screens (scopes, test users)." pause "Done configuring the consent screen?" # ── Stage 2: OAuth client ID ──────────────────────────────────────────────── stage "Google Cloud: create OAuth client ID" open_url "https://console.cloud.google.com/apis/credentials" step "Click '+ Create Credentials' → 'OAuth client ID'." step "Application type: 'Web application'." say "Authorized redirect URI to add:" note " $REDIRECT_URI" warn "If BETTER_AUTH_URL changes later (e.g. you deploy this), add that" warn "domain's /api/auth/callback/google URI here too." step "Paste the redirect URI above into 'Authorized redirect URIs', then click 'Create'." step "A dialog shows the Client ID and Client secret — copy both." ask GOOGLE_CLIENT_ID "Paste the Client ID:" ask_secret GOOGLE_CLIENT_SECRET "Paste the Client secret:" write_env GOOGLE_CLIENT_ID "$GOOGLE_CLIENT_ID" write_env GOOGLE_CLIENT_SECRET "$GOOGLE_CLIENT_SECRET" # ────────────────────────────────────────────────────────────────────────── finish note "Restart 'npm run dev' so the server picks up the new env vars." note "Apple SSO needs a real HTTPS domain (no localhost) — re-run /wizard for" note "that once you have one to test against."