diff --git a/.changeset/wizard-template-fixes.md b/.changeset/wizard-template-fixes.md new file mode 100644 index 0000000..d7e4dce --- /dev/null +++ b/.changeset/wizard-template-fixes.md @@ -0,0 +1,5 @@ +--- +"mattpocock-skills": patch +--- + +`wizard` template fixes: `ask` uses Readline so arrow keys move the cursor (#741); `write_env` single-quotes values so spaces, `#`, `$` and quotes survive `source` and dotenv (#770); `open_url` prints the open-it-yourself warning when no browser opener exists (#774); `write_env` writes through a symlinked `.env` and keeps an existing file's mode (#811); `ask` and `ask_secret` fail at EOF instead of looping (#852). diff --git a/docs/engineering/wizard.md b/docs/engineering/wizard.md index fa7c0f4..b4e76b6 100644 --- a/docs/engineering/wizard.md +++ b/docs/engineering/wizard.md @@ -62,8 +62,6 @@ No. The agent writes a script; it doesn't run it. You run the script yourself, a Not mid-run. There is no back button. The stages run forward, and a wrong answer on stage 3 means Ctrl-C and re-run. Re-running is cheap, because any value already written to `.env` is offered back as a default, so you press Enter through the stages you got right and retype only the wrong one. Users asked for this in launch week, and it is still open: "loved it! One thing though, is there a way to go back and correct what you've entered?" -There's a related open bug. Arrow keys in an `ask` prompt insert `^[[D` / `^[[C` instead of moving the cursor, because the prompt uses `read -r` rather than Readline ([issue #741](https://github.com/mattpocock/skills/issues/741)). Backspace works; arrow keys don't. Delete back to the mistake rather than moving the cursor into it. - **Does it know what I've already set up?** Partly, and less than the launch reactions assumed. It reads the repo before it asks (your `.env` files, `docker-compose`, framework config, the `secrets.*` references in CI), so it scopes to values that are missing rather than starting from zero the way a README does. What it doesn't do is check the third-party service. If a key exists in your `.env` the wizard offers it back and Enter keeps it; if you already created the Stripe account but never saved the key, the wizard still sends you to the dashboard for it. diff --git a/skills/engineering/wizard/template.sh b/skills/engineering/wizard/template.sh index 1b3cdf4..2d0de38 100644 --- a/skills/engineering/wizard/template.sh +++ b/skills/engineering/wizard/template.sh @@ -70,7 +70,7 @@ open_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 + else false; fi } >/dev/null 2>&1 || warn "couldn't open a browser, so visit it manually: $url" } @@ -88,52 +88,57 @@ confirm() { [[ "$reply" =~ ^[Yy] ]] } -# _existing KEY: current value of KEY in ENV_FILE, if any. +# _existing KEY: current value of KEY in ENV_FILE, if any, with write_env's quoting undone. _existing() { [[ -f "$ENV_FILE" ]] || return 1 - local line; line=$(grep -E "^${1}=" "$ENV_FILE" | tail -n1) || return 1 - printf '%s' "${line#*=}" + local line value sq="'\\''"; line=$(grep -E "^${1}=" "$ENV_FILE" | tail -n1) || return 1 + value="${line#*=}" + if [[ "$value" == \'*\' ]]; then value="${value:1:${#value}-2}"; value="${value//"$sq"/\'}"; fi + printf '%s' "$value" } # 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). +# a default on re-runs (Enter keeps it). Visible input (non-secret), edited +# with Readline. Fails at EOF with no input. ask() { - local key="$1" prompt="$2" current input + local key="$1" prompt="$2" current input s=$'\001' e=$'\002' p rc=0 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 + # Readline needs the colour codes wrapped in \001..\002 to place the cursor. + p=" $s$BOLD$e$prompt$s$RESET$e " + [[ -n "$current" ]] && p+="$s$DIM$e[Enter keeps current]$s$RESET$e " + read -e -r -p "$p" input || rc=1 + if (( rc )) && [[ -z "$input" ]]; then return 1; fi [[ -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 + local key="$1" prompt="$2" current input rc=0 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 + read -rs input || rc=1 printf '\n' + if (( rc )) && [[ -z "$input" ]]; then return 1; fi [[ -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 KEY VALUE upserts KEY='VALUE' into ENV_FILE (replaces any existing +# line). Idempotent. Creates ENV_FILE at mode 0600; writes through a symlink +# and keeps an existing file's mode. write_env() { - local key="$1" value="$2" tmp - touch "$ENV_FILE" + local key="$1" value="$2" tmp sq="'\\''" + [[ -e "$ENV_FILE" ]] || (umask 077 && : > "$ENV_FILE") tmp=$(mktemp) grep -vE "^${key}=" "$ENV_FILE" > "$tmp" || true - printf '%s=%s\n' "$key" "$value" >> "$tmp" - mv "$tmp" "$ENV_FILE" + printf "%s='%s'\n" "$key" "${value//\'/$sq}" >> "$tmp" + cat "$tmp" > "$ENV_FILE" + rm -f "$tmp" WRITTEN_ENV+=("$key") printf ' %sāœ“ wrote%s %s → %s\n' "$GREEN" "$RESET" "$key" "$ENV_FILE" }