1#!/usr/bin/env bash
2#
3# A wizard — walks a human through a manual procedure step by step.
4# Generated by the /wizard skill.
5#
6# Everything above the "STAGES" marker is the wizard library: do not hand-edit
7# it. Author the per-step stages below the marker.
8
9set -euo pipefail
10
11# ──────────────────────────────────────────────────────────────────────────
12# Wizard library — delightful, consistent UX. Identical across every wizard.
13# ──────────────────────────────────────────────────────────────────────────
14
15if [[ -t 1 ]] && command -v tput >/dev/null 2>&1 && [[ "$(tput colors 2>/dev/null || echo 0)" -ge 8 ]]; then
16 BOLD=$(tput bold); DIM=$(tput dim); RESET=$(tput sgr0)
17 BLUE=$(tput setaf 4); GREEN=$(tput setaf 2); YELLOW=$(tput setaf 3); RED=$(tput setaf 1)
18else
19 BOLD=""; DIM=""; RESET=""; BLUE=""; GREEN=""; YELLOW=""; RED=""
20fi
21
22# Author sets this at the top of the stages section.
23TOTAL_STAGES=0
24
25_STAGE_INDEX=0
26ENV_FILE="${ENV_FILE:-.env}"
27WRITTEN_ENV=() # KEYs written to ENV_FILE this run
28WRITTEN_SECRET=() # secret NAMEs set this run
29SKIPPED=() # things we couldn't do (e.g. gh missing)
30
31# _clear — wipe the terminal so only the current step is on screen. No-op when
32# output isn't a terminal, so piped logs stay readable.
33_clear() {
34 [[ -t 1 ]] || return 0
35 if command -v tput >/dev/null 2>&1; then tput clear; else printf '\033[2J\033[3J\033[H'; fi
36}
37
38# banner "Title" — opening frame: what this wizard does.
39banner() {
40 _clear
41 printf '\n%s%s %s%s\n' "$BOLD" "$BLUE" "$1" "$RESET"
42 printf '%s %s stages%s\n\n' "$DIM" "$TOTAL_STAGES" "$RESET"
43 printf '%s You drive the browser; this wizard tells you exactly what to do and\n' "$DIM"
44 printf ' captures the values you copy back. Stop any time with Ctrl-C and re-run\n'
45 printf ' later — it remembers values already saved.%s\n' "$RESET"
46 pause "Ready to start?"
47}
48
49# stage "Name" — clear the screen, then announce a stage and show progress.
50# Clearing keeps only the current step on screen.
51stage() {
52 _clear
53 _STAGE_INDEX=$((_STAGE_INDEX + 1))
54 printf '\n%s%s▸ Stage %s/%s · %s%s\n' \
55 "$BOLD" "$BLUE" "$_STAGE_INDEX" "$TOTAL_STAGES" "$1" "$RESET"
56}
57
58# say "..." — a plain instruction line.
59say() { printf ' %s\n' "$1"; }
60# step "..." — a numbered-feeling action the human takes in the browser.
61step() { printf ' %s•%s %s\n' "$BLUE" "$RESET" "$1"; }
62note() { printf ' %s%s%s\n' "$DIM" "$1" "$RESET"; }
63warn() { printf ' %s⚠ %s%s\n' "$YELLOW" "$1" "$RESET"; }
64
65# open_url URL — open in the human's browser, cross-platform incl. WSL.
66open_url() {
67 local url="$1"
68 printf ' %s↗ opening%s %s\n' "$GREEN" "$RESET" "$url"
69 { if command -v wslview >/dev/null 2>&1; then wslview "$url"
70 elif command -v explorer.exe >/dev/null 2>&1; then explorer.exe "$url"
71 elif command -v xdg-open >/dev/null 2>&1; then xdg-open "$url"
72 elif command -v open >/dev/null 2>&1; then open "$url"
73 else warn "couldn't open a browser — visit it manually: $url"; fi
74 } >/dev/null 2>&1 || warn "couldn't open a browser — visit it manually: $url"
75}
76
77# pause "msg" — wait for the human to confirm they've done the manual part.
78pause() {
79 printf ' %s%s%s ' "$DIM" "${1:-Press Enter to continue}" "$RESET"
80 read -r _ || true
81}
82
83# confirm "question" — y/N gate; returns success on yes.
84confirm() {
85 local reply=""
86 printf ' %s? %s [y/N] ' "$YELLOW" "$1"
87 read -r reply || true
88 [[ "$reply" =~ ^[Yy] ]]
89}
90
91# _existing KEY — current value of KEY in ENV_FILE, if any.
92_existing() {
93 [[ -f "$ENV_FILE" ]] || return 1
94 local line; line=$(grep -E "^${1}=" "$ENV_FILE" | tail -n1) || return 1
95 printf '%s' "${line#*=}"
96}
97
98# ask KEY "Prompt" — read a value into $KEY. Offers the existing .env value as
99# a default on re-runs (Enter keeps it). Visible input (non-secret).
100ask() {
101 local key="$1" prompt="$2" current input
102 current=$(_existing "$key" || true)
103 if [[ -n "$current" ]]; then
104 printf ' %s%s%s %s[Enter keeps current]%s ' "$BOLD" "$prompt" "$RESET" "$DIM" "$RESET"
105 else
106 printf ' %s%s%s ' "$BOLD" "$prompt" "$RESET"
107 fi
108 read -r input || true
109 [[ -z "$input" && -n "$current" ]] && input="$current"
110 printf -v "$key" '%s' "$input"
111}
112
113# ask_secret KEY "Prompt" — like ask, but input is hidden.
114ask_secret() {
115 local key="$1" prompt="$2" current input
116 current=$(_existing "$key" || true)
117 if [[ -n "$current" ]]; then
118 printf ' %s%s%s %s[Enter keeps current]%s ' "$BOLD" "$prompt" "$RESET" "$DIM" "$RESET"
119 else
120 printf ' %s%s%s ' "$BOLD" "$prompt" "$RESET"
121 fi
122 read -rs input || true
123 printf '\n'
124 [[ -z "$input" && -n "$current" ]] && input="$current"
125 printf -v "$key" '%s' "$input"
126}
127
128# write_env KEY VALUE — upsert KEY=VALUE into ENV_FILE (creates it; replaces
129# any existing line). Idempotent.
130write_env() {
131 local key="$1" value="$2" tmp
132 touch "$ENV_FILE"
133 tmp=$(mktemp)
134 grep -vE "^${key}=" "$ENV_FILE" > "$tmp" || true
135 printf '%s=%s\n' "$key" "$value" >> "$tmp"
136 mv "$tmp" "$ENV_FILE"
137 WRITTEN_ENV+=("$key")
138 printf ' %s✓ wrote%s %s → %s\n' "$GREEN" "$RESET" "$key" "$ENV_FILE"
139}
140
141# set_secret NAME VALUE — set a GitHub Actions repo secret via gh. Falls back
142# to a warning (and records it) if gh is unavailable or unauthenticated.
143set_secret() {
144 local name="$1" value="$2"
145 if command -v gh >/dev/null 2>&1 && gh auth status >/dev/null 2>&1; then
146 if printf '%s' "$value" | gh secret set "$name" >/dev/null 2>&1; then
147 WRITTEN_SECRET+=("$name")
148 printf ' %s✓ set%s GitHub secret %s\n' "$GREEN" "$RESET" "$name"
149 return
150 fi
151 fi
152 SKIPPED+=("GitHub secret $name (set it manually: gh secret set $name)")
153 warn "skipped GitHub secret $name — gh not ready; set it later"
154}
155
156# set_var NAME VALUE — set a GitHub Actions repo variable (non-secret).
157set_var() {
158 local name="$1" value="$2"
159 if command -v gh >/dev/null 2>&1 && gh auth status >/dev/null 2>&1; then
160 if gh variable set "$name" --body "$value" >/dev/null 2>&1; then
161 printf ' %s✓ set%s GitHub variable %s\n' "$GREEN" "$RESET" "$name"
162 return
163 fi
164 fi
165 SKIPPED+=("GitHub variable $name")
166 warn "skipped GitHub variable $name — gh not ready; set it later"
167}
168
169# finish — clear, then a closing summary of everything configured.
170finish() {
171 _clear
172 printf '\n%s%s ✓ Setup complete%s\n' "$BOLD" "$GREEN" "$RESET"
173 (( ${#WRITTEN_ENV[@]} )) && note "wrote ${#WRITTEN_ENV[@]} value(s) to $ENV_FILE: ${WRITTEN_ENV[*]}"
174 (( ${#WRITTEN_SECRET[@]} )) && note "set ${#WRITTEN_SECRET[@]} GitHub secret(s): ${WRITTEN_SECRET[*]}"
175 if (( ${#SKIPPED[@]} )); then
176 printf '\n'; warn "still to do by hand:"
177 for s in "${SKIPPED[@]}"; do note " - $s"; done
178 fi
179 printf '\n'
180}
181
182# ──────────────────────────────────────────────────────────────────────────
183# STAGES — author this section. One stage() per step the human takes.
184# Replace the example below. Set TOTAL_STAGES to match the stages you write.
185# ──────────────────────────────────────────────────────────────────────────
186
187TOTAL_STAGES=9
188
189REPO_ROOT="$(git -C "$(dirname "${BASH_SOURCE[0]}")" rev-parse --show-toplevel 2>/dev/null || echo "$HOME/instagram-apify-scraper")"
190
191banner "Publish instagram-post-reel-scraper to the Apify Store"
192
193# ── Stage 1: Apify CLI install + login ─────────────────────────────────────
194stage "Apify CLI — install & log in"
195say "Nothing has pushed this Actor to Apify's platform yet — no CLI, no linked project."
196if command -v apify >/dev/null 2>&1; then
197 say "apify CLI already installed ($(apify --version 2>/dev/null))."
198else
199 if confirm "apify CLI not found — install it now via 'npm install -g apify-cli'?"; then
200 npm install -g apify-cli
201 else
202 warn "can't continue without the CLI — install it manually, then re-run this wizard."
203 exit 1
204 fi
205fi
206step "Run 'apify login' below. It opens a browser to authenticate, or you can paste an API token"
207step "from Console → Settings → Integrations → API token."
208( cd "$REPO_ROOT" && apify login )
209pause "Logged in? Press Enter to continue."
210
211# ── Stage 2: Push the Actor ─────────────────────────────────────────────────
212stage "Push the Actor to your account"
213say "This uploads the current code as an Actor on your account. It is NOT public or listed yet —"
214say "just present on the platform so the Console has something to configure."
215step "If this is the first push, the CLI will ask whether to create a new Actor — say yes and"
216step "accept the name from .actor/actor.json (instagram-post-reel-scraper)."
217( cd "$REPO_ROOT" && apify push )
218pause "Push finished successfully? Press Enter to continue."
219
220# ── Stage 3: Competitor pricing research ────────────────────────────────────
221stage "Research competitor pricing"
222say "The spec requires pricing below the lowest current comparable competitor — this has to be"
223say "checked live, not assumed from memory."
224open_url "https://apify.com/store?search=instagram+scraper"
225step "Skim the pay-per-event Instagram scrapers listed. Note the lowest per-event price you see"
226step "among actors comparable to this one (single post/reel or profile post/reel scraping)."
227ask LOWEST_COMPETITOR_PRICE "Lowest comparable competitor price you found (e.g. \$0.01 per item):"
228note "Stage 4 will ask you to price both events below this."
229
230# ── Stage 4: Configure pay-per-event pricing ────────────────────────────────
231stage "Configure pay-per-event pricing"
232open_url "https://console.apify.com/actors"
233step "Open the Actor you just pushed (instagram-post-reel-scraper) → Publication tab → Monetization."
234step "Add TWO events, matching these names exactly (the code charges these via Actor.pushData(item, eventName)):"
235say " • name: post-scraped — title: e.g. \"Post scraped\" — description: one post pushed to the Dataset"
236say " • name: reel-scraped — title: e.g. \"Reel scraped\" — description: one reel pushed to the Dataset"
237step "Price BOTH below the competitor price from Stage 3 ($LOWEST_COMPETITOR_PRICE)."
238ask POST_SCRAPED_PRICE "Price you set for post-scraped:"
239ask REEL_SCRAPED_PRICE "Price you set for reel-scraped:"
240pause "Both events saved in Console? Press Enter to continue."
241
242# ── Stage 5: Display information ────────────────────────────────────────────
243stage "Display information"
244say "Still on the Publication tab:"
245step "Add an Actor logo (any square image works for now — can be swapped later)."
246step "Confirm the description pulls in cleanly and reads well."
247step "Confirm the README renders correctly as the Store detail page — it now carries the"
248step "Pricing, Maintenance, and Disclaimer sections written for ticket 05. Check the"
249step "Disclaimer section is visible and unmodified by any Console-side formatting."
250pause "Display Information section marked complete? Press Enter to continue."
251
252# ── Stage 6: Sample output & schemas ────────────────────────────────────────
253stage "Sample output & schemas"
254note "Apify's docs don't spell out exact fields here — follow whatever the Publication tab's"
255note "\"Sample Output\" and \"Schemas\" sections prompt you for on the page itself."
256step "Sample Output: if not auto-populated, run the Actor once (single postOrReelUrl input against"
257step "a real public post) and use that run's dataset item as the sample."
258step "Schemas: accept the auto-generated output schema unless the Console flags something missing."
259pause "Both sections marked complete? Press Enter to continue."
260
261# ── Stage 7: Actor permissions ──────────────────────────────────────────────
262stage "Actor permissions"
263note "Again, no fixed field list from the docs — follow the Console page's own prompts."
264step "Set whatever permission level the Publication tab asks for (this determines what a user"
265step "running the Actor from the Store can see/access) — the default/minimal option is fine for v1."
266pause "Actor Permissions section marked complete? Press Enter to continue."
267
268# ── Stage 8: Publish ─────────────────────────────────────────────────────────
269stage "Publish on Store"
270warn "This is the irreversible step — the Actor becomes publicly listed and runnable by anyone."
271say "All Publication tab sections should now show as complete (Display Information, Monetization,"
272say "Sample Output, Schemas, Actor Permissions)."
273if confirm "All sections complete and ready to go public — click 'Publish on Store' now?"; then
274 step "Click 'Publish on Store' in the Console."
275 pause "Published? Press Enter to continue."
276else
277 warn "stopping before publish — re-run this wizard (or just do Stage 8 by hand) when ready."
278 SKIPPED+=("Publish on Store — deferred, run was stopped before the irreversible step")
279fi
280
281# ── Stage 9: Verify ──────────────────────────────────────────────────────────
282stage "Verify the listing is live"
283open_url "https://apify.com/store?search=instagram+post+reel+scraper"
284step "Confirm your Actor appears in the Store search results with the two priced events showing."
285pause "Confirmed live? Press Enter to continue."
286
287finish
288note "Recorded this run: competitor floor $LOWEST_COMPETITOR_PRICE, post-scraped $POST_SCRAPED_PRICE, reel-scraped $REEL_SCRAPED_PRICE."
289# ──────────────────────────────────────────────────────────────────────────