Skip to main content

WIZARD_TEMPLATE

Constant WIZARD_TEMPLATE 

Source
const WIZARD_TEMPLATE: &str = "#!/usr/bin/env bash\n#\n# A wizard walks a human through a manual procedure, step by step.\n# Generated by the wizard skill.\n#\n# The agent runs this in the seat\'s own terminal and hands the keyboard over\n# for hidden input; a person with no agent can still run it themselves.\n#\n# Everything above the \"STAGES\" marker is the wizard library: do not hand-edit\n# it. Author the per-step stages below the marker.\n\nset -euo pipefail\n\n# \u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\n# Wizard library: delightful, consistent UX, identical across every wizard.\n# \u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\n\n# **Every capability is probed, never assumed.** `tput dim` exits non-zero on\n# a TERM without it (ansi, rxvt) and `tput clear` on a TERM it does not know;\n# under `set -e` either would kill the wizard before its first line.\nif [[ -t 1 ]] && command -v tput >/dev/null 2>&1; then\n  BOLD=$(tput bold 2>/dev/null || true)\n  DIM=$(tput dim 2>/dev/null || true)\n  RESET=$(tput sgr0 2>/dev/null || true)\n  BLUE=$(tput setaf 4 2>/dev/null || true)\n  GREEN=$(tput setaf 2 2>/dev/null || true)\n  YELLOW=$(tput setaf 3 2>/dev/null || true)\n  CLEAR=$(tput clear 2>/dev/null || true)\nelse\n  BOLD=\"\"; DIM=\"\"; RESET=\"\"; BLUE=\"\"; GREEN=\"\"; YELLOW=\"\"; CLEAR=\"\"\nfi\n\n# Author sets this at the top of the stages section.\nTOTAL_STAGES=0\n\n_STAGE_INDEX=0\nENV_FILE=\"${ENV_FILE:-.env}\"\nWRITTEN_ENV=()    # KEYs written to ENV_FILE this run\nWRITTEN_SECRET=() # secret NAMEs set this run\n_ENV_TMP=\"\"       # the temp a write_env is staging\n\n_cleanup() {\n  if [[ -n \"$_ENV_TMP\" && -f \"$_ENV_TMP\" ]]; then rm -f \"$_ENV_TMP\"; fi\n  return 0\n}\ntrap _cleanup EXIT\n\n# _clear wipes the terminal so only the current step is on screen. No-op when\n# output isn\'t a terminal, so piped logs stay readable.\n_clear() {\n  [[ -t 1 ]] || return 0\n  if [[ -n \"$CLEAR\" ]]; then printf \'%s\' \"$CLEAR\"; else printf \'\\033[2J\\033[3J\\033[H\'; fi\n}\n\n# banner \"Title\" shows the opening frame: what this wizard does.\nbanner() {\n  _clear\n  printf \'\\n%s%s  %s%s\\n\' \"$BOLD\" \"$BLUE\" \"$1\" \"$RESET\"\n  printf \'%s  %s stages%s\\n\\n\' \"$DIM\" \"$TOTAL_STAGES\" \"$RESET\"\n  printf \'%s  This wizard tells you exactly what to do and captures the values you\\n\' \"$DIM\"\n  printf \'  copy back. Stop any time with Ctrl-C and re-run later, since it remembers\\n\'\n  printf \'  values already saved.%s\\n\' \"$RESET\"\n  pause \"Ready to start?\"\n}\n\n# stage \"Name\" clears the screen, then announces a stage and shows progress.\n# Clearing keeps only the current step on screen.\nstage() {\n  _clear\n  _STAGE_INDEX=$((_STAGE_INDEX + 1))\n  printf \'\\n%s%s\u{25b8} Stage %s/%s \u{b7} %s%s\\n\' \\\n    \"$BOLD\" \"$BLUE\" \"$_STAGE_INDEX\" \"$TOTAL_STAGES\" \"$1\" \"$RESET\"\n}\n\n# say \"...\" prints a plain instruction line.\nsay()  { printf \'  %s\\n\' \"$1\"; }\n# step \"...\" is a numbered-feeling action the human takes in the browser.\nstep() { printf \'  %s\u{2022}%s %s\\n\' \"$BLUE\" \"$RESET\" \"$1\"; }\nnote() { printf \'  %s%s%s\\n\' \"$DIM\" \"$1\" \"$RESET\"; }\nwarn() { printf \'  %s\u{26a0} %s%s\\n\' \"$YELLOW\" \"$1\" \"$RESET\"; }\n\n# _stop MESSAGE prints a failure and ends the run. A CI value has no other\n# home, so a failed write stops the wizard rather than recording a name whose\n# value is nowhere; a re-run asks for it again.\n_stop() {\n  printf \'  %s\u{2717} %s%s\\n\' \"$YELLOW\" \"$1\" \"$RESET\"\n  exit 1\n}\n\n# _valid_key KEY aborts unless KEY is a shell-assignment-shaped name. **It is\n# never interpolated into a pattern**: as a regex, `A.B` would match a\n# different line and `A[` would empty the file.\n_valid_key() {\n  [[ \"$1\" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] && return 0\n  _stop \"$1 is not a valid environment variable name: use letters, digits and _, not starting with a digit\"\n}\n\n# _no_newline KEY VALUE aborts when VALUE spans lines: an env file holds one\n# line per key.\n_no_newline() {\n  [[ \"$2\" != *$\'\\n\'* ]] && return 0\n  _stop \"$1 contains a newline, which $ENV_FILE cannot hold: put a multi-line value in the file its program reads, or in the vault\"\n}\n\n# open_url URL opens it in the human\'s browser, cross-platform incl. WSL.\n# This is the fallback for when no browser is available: an agent driving the\n# wizard opens the page in the seat\'s browser and hands the tab over instead.\nopen_url() {\n  local url=\"$1\" opener=\"\"\n  printf \'  %s\u{2197} opening%s %s\\n\' \"$GREEN\" \"$RESET\" \"$url\"\n  if   command -v wslview      >/dev/null 2>&1; then opener=\"wslview\"\n  elif command -v explorer.exe >/dev/null 2>&1; then opener=\"explorer.exe\"\n  elif command -v xdg-open     >/dev/null 2>&1; then opener=\"xdg-open\"\n  elif command -v open         >/dev/null 2>&1; then opener=\"open\"\n  fi\n  if [[ -z \"$opener\" ]]; then\n    warn \"couldn\'t open a browser; visit it manually: $url\"\n    return 0\n  fi\n  # **The warning is emitted outside the redirection.** Redirecting the\n  # whole block would swallow it along with the opener\'s own noise.\n  \"$opener\" \"$url\" >/dev/null 2>&1 || warn \"couldn\'t open a browser, so visit it manually: $url\"\n}\n\n# pause \"msg\" waits for the human to confirm they\'ve done the manual part.\npause() {\n  printf \'  %s%s%s \' \"$DIM\" \"${1:-Press Enter to continue}\" \"$RESET\"\n  IFS= read -r _ || true\n}\n\n# confirm \"question\" is a y/N gate; returns success on yes. **A bare call\n# ends the wizard under `set -e` when the answer is no**, so this says so and\n# the skill shows the if/else idiom.\nconfirm() {\n  local reply=\"\"\n  printf \'  %s? %s [y/N] \' \"$YELLOW\" \"$1\"\n  IFS= read -r reply || true\n  if [[ \"$reply\" =~ ^[Yy] ]]; then return 0; fi\n  printf \'  %s\u{b7} answered no: %s - nothing was changed%s\\n\' \"$DIM\" \"$1\" \"$RESET\"\n  return 1\n}\n\n# _existing KEY: current value of KEY in ENV_FILE. Returns 0 and prints the\n# value, 1 when there is no such line, 2 when the file could not be read.\n# **It never calls `_stop` itself**: ask/ask_secret run it in a command\n# substitution, where an exit only ends the subshell and its message would be\n# captured as the value. The caller turns 2 into the wizard\'s own message.\n_existing() {\n  [[ -f \"$ENV_FILE\" ]] || return 1\n  local key=\"$1\" line status=0\n  # awk\'s own error is 2; 1 is this program\'s \"no such line\", the only exit\n  # that means a normal miss.\n  line=$(awk -v key=\"$key\" \\\n    \'index($0, key \"=\") == 1 { found = $0 } END { if (found != \"\") { print found; exit 0 } exit 1 }\' \\\n    \"$ENV_FILE\") || status=$?\n  if (( status == 1 )); then return 1; fi\n  if (( status != 0 )); then return 2; fi\n  printf \'%s\' \"${line#*=}\"\n}\n\n# ask KEY \"Prompt\" reads a value into $KEY. Offers the existing .env value as\n# a default on re-runs (Enter keeps it). Visible input (non-secret).\nask() {\n  local key=\"$1\" prompt=\"$2\" current status=0 input\n  _valid_key \"$key\"\n  current=$(_existing \"$key\") || status=$?\n  if (( status > 1 )); then\n    _stop \"could not read $ENV_FILE; check that it is readable, then re-run\"\n  fi\n  if [[ -n \"$current\" ]]; then\n    printf \'  %s%s%s %s[Enter keeps current]%s \' \"$BOLD\" \"$prompt\" \"$RESET\" \"$DIM\" \"$RESET\"\n  else\n    printf \'  %s%s%s \' \"$BOLD\" \"$prompt\" \"$RESET\"\n  fi\n  IFS= read -r input || true\n  [[ -z \"$input\" && -n \"$current\" ]] && input=\"$current\"\n  printf -v \"$key\" \'%s\' \"$input\"\n}\n\n# ask_secret KEY \"Prompt\" is like ask, but input is hidden.\nask_secret() {\n  local key=\"$1\" prompt=\"$2\" current status=0 input\n  _valid_key \"$key\"\n  current=$(_existing \"$key\") || status=$?\n  if (( status > 1 )); then\n    _stop \"could not read $ENV_FILE; check that it is readable, then re-run\"\n  fi\n  if [[ -n \"$current\" ]]; then\n    printf \'  %s%s%s %s[Enter keeps current]%s \' \"$BOLD\" \"$prompt\" \"$RESET\" \"$DIM\" \"$RESET\"\n  else\n    printf \'  %s%s%s \' \"$BOLD\" \"$prompt\" \"$RESET\"\n  fi\n  IFS= read -rs input || true\n  printf \'\\n\'\n  [[ -z \"$input\" && -n \"$current\" ]] && input=\"$current\"\n  printf -v \"$key\" \'%s\' \"$input\"\n}\n\n# write_env KEY VALUE upserts KEY=VALUE into ENV_FILE (creates it; replaces\n# any existing line). Idempotent. **The temp sits beside ENV_FILE**, so the\n# rename is atomic and a secret never lands in $TMPDIR.\nwrite_env() {\n  local key=\"$1\" value=\"$2\" status=0\n  _valid_key \"$key\"\n  _no_newline \"$key\" \"$value\"\n  touch \"$ENV_FILE\" || _stop \"could not create $ENV_FILE\"\n  _ENV_TMP=$(mktemp \"${ENV_FILE}.XXXXXX\") \\\n    || _stop \"could not create a temp file beside $ENV_FILE\"\n  awk -v key=\"$key\" \'index($0, key \"=\") != 1\' \"$ENV_FILE\" > \"$_ENV_TMP\" || status=$?\n  if (( status != 0 )); then\n    rm -f \"$_ENV_TMP\"; _ENV_TMP=\"\"\n    _stop \"could not read $ENV_FILE (awk exit $status); nothing was written\"\n  fi\n  if ! printf \'%s=%s\\n\' \"$key\" \"$value\" >> \"$_ENV_TMP\"; then\n    rm -f \"$_ENV_TMP\"; _ENV_TMP=\"\"\n    _stop \"could not write the new value for $key; nothing was changed\"\n  fi\n  if ! mv \"$_ENV_TMP\" \"$ENV_FILE\"; then\n    rm -f \"$_ENV_TMP\"; _ENV_TMP=\"\"\n    _stop \"could not replace $ENV_FILE with the new value for $key\"\n  fi\n  _ENV_TMP=\"\"\n  if [[ \" ${WRITTEN_ENV[*]:-} \" != *\" $key \"* ]]; then WRITTEN_ENV+=(\"$key\"); fi\n  printf \'  %s\u{2713} wrote%s %s \u{2192} %s\\n\' \"$GREEN\" \"$RESET\" \"$key\" \"$ENV_FILE\"\n}\n\n# set_secret NAME VALUE sets a GitHub Actions repo secret via gh. **A CI-only\n# value has no other home**, so a failure stops the stage instead of leaving\n# a name whose value is nowhere; nothing recorded means a re-run asks again.\nset_secret() {\n  local name=\"$1\" value=\"$2\"\n  command -v gh >/dev/null 2>&1 \\\n    || _stop \"GitHub secret $name needs the gh CLI, which is not installed: install gh and re-run\"\n  gh auth status >/dev/null 2>&1 \\\n    || _stop \"GitHub secret $name needs gh signed in: run gh auth login, then re-run\"\n  printf \'%s\' \"$value\" | gh secret set \"$name\" >/dev/null 2>&1 \\\n    || _stop \"GitHub secret $name was not set: the gh call failed. Nothing was recorded, so a re-run asks for it again\"\n  WRITTEN_SECRET+=(\"$name\")\n  printf \'  %s\u{2713} set%s GitHub secret %s\\n\' \"$GREEN\" \"$RESET\" \"$name\"\n}\n\n# set_var NAME VALUE sets a GitHub Actions repo variable (non-secret).\nset_var() {\n  local name=\"$1\" value=\"$2\"\n  command -v gh >/dev/null 2>&1 \\\n    || _stop \"GitHub variable $name needs the gh CLI, which is not installed: install gh and re-run\"\n  gh auth status >/dev/null 2>&1 \\\n    || _stop \"GitHub variable $name needs gh signed in: run gh auth login, then re-run\"\n  gh variable set \"$name\" --body \"$value\" >/dev/null 2>&1 \\\n    || _stop \"GitHub variable $name was not set: the gh call failed. Nothing was recorded, so a re-run asks for it again\"\n  printf \'  %s\u{2713} set%s GitHub variable %s\\n\' \"$GREEN\" \"$RESET\" \"$name\"\n}\n\n# finish clears, then shows a closing summary of everything configured.\nfinish() {\n  _clear\n  printf \'\\n%s%s  \u{2713} Setup complete%s\\n\' \"$BOLD\" \"$GREEN\" \"$RESET\"\n  (( ${#WRITTEN_ENV[@]} ))    && note \"wrote ${#WRITTEN_ENV[@]} value(s) to $ENV_FILE: ${WRITTEN_ENV[*]}\"\n  (( ${#WRITTEN_SECRET[@]} )) && note \"set ${#WRITTEN_SECRET[@]} GitHub secret(s): ${WRITTEN_SECRET[*]}\"\n  printf \'\\n\'\n}\n\n# \u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\n# STAGES: author this section. One stage() per step the human takes.\n# Replace the example below. Set TOTAL_STAGES to match the stages you write.\n# ENV_FILE is relative: run the wizard from the repo root it should land in.\n# \u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\n\nTOTAL_STAGES=1\n\nbanner \"Stripe setup\"\n\n# \u{2500}\u{2500} Example stage: replace with your real steps \u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\nstage \"Stripe: API keys\"\nsay \"We\'ll grab your Stripe test keys and store them for local dev + CI.\"\n# The agent opens this page in the seat\'s browser and hands the tab over;\n# open_url is the fallback for when no browser is available.\nopen_url \"https://dashboard.stripe.com/test/apikeys\"\nstep \"On the API keys page, copy the Publishable key (starts pk_test_).\"\nask STRIPE_PUBLISHABLE_KEY \"Paste the publishable key:\"\nstep \"Click \'Reveal test key\' on the Secret key row, then copy it.\"\nask_secret STRIPE_SECRET_KEY \"Paste the secret key:\"\nwrite_env STRIPE_PUBLISHABLE_KEY \"$STRIPE_PUBLISHABLE_KEY\"\nwrite_env STRIPE_SECRET_KEY \"$STRIPE_SECRET_KEY\"\nset_secret STRIPE_SECRET_KEY \"$STRIPE_SECRET_KEY\"   # CI needs this one\n# \u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\n\nfinish\n";