| #!/usr/bin/env bash |
| set -euo pipefail |
|
|
| |
| |
| |
|
|
| cd "$(dirname "$0")" |
|
|
| BOLD=$'\033[1m' |
| ACCENT=$'\033[38;2;251;191;36m' |
| MUTED=$'\033[38;2;90;100;128m' |
| SUCCESS=$'\033[38;2;0;229;204m' |
| ERROR=$'\033[38;2;230;57;70m' |
| NC=$'\033[0m' |
|
|
| |
| |
| COMMANDS=( |
| "build:π¨:Build the application" |
| "dev:π:Build & run" |
| "run:π:Run the application" |
| "check:π§ͺ:All checks" |
| "test unit:π¬:Go unit tests" |
| "test dashboard:π¬:Dashboard unit tests" |
| "e2e:π³:E2E tests" |
| " recent:π³:E2E Recent tests" |
| " orchestrator:π³:E2E Orchestrator tests" |
| " curl:π³:E2E Curl tests" |
| " cli:π³:E2E CLI tests" |
| "doctor:π©Ί:Setup dev environment" |
| "check go:π§ͺ:Go only" |
| "check dashboard:π§ͺ:Dashboard only" |
| "check security:π§ͺ:Gosec security scan" |
| "check docs:π§ͺ:Validate docs JSON" |
| "binary:π¦:Build release-style binary into dist/" |
| " all:π¦:Build the full release binary matrix into dist/" |
| "format dashboard:π¨:Run Prettier on dashboard sources" |
| ) |
|
|
| |
|
|
| build_e2e_cli_binary() { |
| echo " ${MUTED}Building static binary for E2E CLI tests...${NC}" |
| CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o tests/e2e/runner-cli/pinchtab ./cmd/pinchtab |
| echo " ${SUCCESS}β${NC} Binary built" |
| echo "" |
| } |
|
|
| show_help() { |
| echo "" |
| echo " ${ACCENT}${BOLD}π¦ Pinchtab Dev${NC}" |
| echo "" |
| for cmd in "${COMMANDS[@]}"; do |
| IFS=':' read -r name emoji desc <<< "$cmd" |
| if [[ "$name" == " "* ]]; then |
| local trimmed="${name#"${name%%[![:space:]]*}"}" |
| printf " ${BOLD}%-18s${NC} ${MUTED}%s${NC}\n" "$trimmed" "$desc" |
| else |
| printf " ${SUCCESS}%s${NC} ${BOLD}%-18s${NC} ${MUTED}%s${NC}\n" "$emoji" "$name" "$desc" |
| fi |
| done |
| echo "" |
| echo " ${MUTED}Usage: dev [command] [sub]${NC}" |
| echo "" |
| } |
|
|
| run_command() { |
| local target="$1" |
|
|
| echo "" |
|
|
| case "$target" in |
| "check") |
| echo " ${ACCENT}${BOLD}π§ͺ Running all checks (Go + Dashboard)${NC}" |
| echo "" |
| bash scripts/check.sh |
| local go_exit=$? |
| echo "" |
| echo " ${ACCENT}${BOLD}π§ͺ Dashboard checks${NC}" |
| echo "" |
| bash scripts/check-dashboard.sh |
| local dash_exit=$? |
| if [ $go_exit -ne 0 ] || [ $dash_exit -ne 0 ]; then exit 1; fi |
| exit 0 |
| ;; |
| "check go") |
| echo " ${ACCENT}${BOLD}π§ͺ Go checks${NC}" |
| echo "" |
| exec bash scripts/check.sh |
| ;; |
| "check dashboard") |
| echo " ${ACCENT}${BOLD}π§ͺ Dashboard checks${NC}" |
| echo "" |
| exec bash scripts/check-dashboard.sh |
| ;; |
| "check security") |
| echo " ${ACCENT}${BOLD}π Security scan${NC}" |
| echo "" |
| exec bash scripts/check-gosec.sh |
| ;; |
| "check docs") |
| echo " ${ACCENT}${BOLD}π Docs validation${NC}" |
| echo "" |
| exec bash scripts/check-docs-json.sh |
| ;; |
| "format dashboard") |
| echo " ${ACCENT}${BOLD}π¨ Dashboard formatting${NC}" |
| echo "" |
| exec bash -lc 'cd dashboard && if command -v bun >/dev/null 2>&1; then bun run format; else npx prettier --write '"'"'src/**/*.{ts,tsx,css}'"'"'; fi' |
| ;; |
| "test") |
| echo " ${ACCENT}${BOLD}π¬ All tests${NC}" |
| echo "" |
| exec bash scripts/test.sh all |
| ;; |
| "test unit") |
| echo " ${ACCENT}${BOLD}π¬ Go unit tests${NC}" |
| echo "" |
| exec bash scripts/test.sh unit |
| ;; |
| "test dashboard") |
| echo " ${ACCENT}${BOLD}π¬ Dashboard tests${NC}" |
| echo "" |
| exec bash scripts/test.sh dashboard |
| ;; |
| "unit") |
| echo " ${ACCENT}${BOLD}π¬ Unit tests${NC}" |
| echo "" |
| exec bash scripts/test.sh unit |
| ;; |
| "system") |
| echo " ${ACCENT}${BOLD}π¬ System tests${NC}" |
| echo "" |
| exec bash scripts/test.sh system |
| ;; |
| "e2e"*) |
| |
| |
| chmod -R 755 tests/e2e/fixtures/test-extension* 2>/dev/null || true |
| |
| case "$target" in |
| "e2e") |
| echo " ${ACCENT}${BOLD}π³ E2E Recent tests (Docker)${NC}" |
| echo "" |
| docker compose -f tests/e2e/docker-compose.yml run --build --rm runner /scenarios-recent/run.sh |
| local recent_exit=$? |
| if [ $recent_exit -ne 0 ]; then |
| echo -e "${ERROR} Recent tests failed. Showing pinchtab logs:${NC}" |
| docker compose -f tests/e2e/docker-compose.yml logs pinchtab | tail -n 50 |
| docker compose -f tests/e2e/docker-compose.yml down -v 2>/dev/null |
| exit 1 |
| fi |
| docker compose -f tests/e2e/docker-compose.yml down -v 2>/dev/null |
|
|
| echo "" |
| echo " ${ACCENT}${BOLD}π³ E2E Full Curl tests (Docker)${NC}" |
| echo "" |
| docker compose -f tests/e2e/docker-compose.yml up --abort-on-container-exit |
| local curl_exit=$? |
| if [ $curl_exit -ne 0 ]; then |
| docker compose -f tests/e2e/docker-compose.yml logs pinchtab | tail -n 50 |
| fi |
| docker compose -f tests/e2e/docker-compose.yml down -v 2>/dev/null |
|
|
| echo "" |
| echo " ${ACCENT}${BOLD}π³ E2E Orchestrator tests (Docker)${NC}" |
| echo "" |
| docker compose -f tests/e2e/docker-compose-orchestrator.yml run --build --rm runner |
| local orch_exit=$? |
| if [ $orch_exit -ne 0 ]; then |
| docker compose -f tests/e2e/docker-compose-orchestrator.yml logs pinchtab | tail -n 50 || true |
| docker compose -f tests/e2e/docker-compose-orchestrator.yml logs pinchtab-bridge | tail -n 50 || true |
| fi |
| docker compose -f tests/e2e/docker-compose-orchestrator.yml down -v 2>/dev/null |
|
|
| echo "" |
| echo " ${ACCENT}${BOLD}π³ E2E CLI tests (Docker)${NC}" |
| echo "" |
| build_e2e_cli_binary |
| docker compose -f tests/e2e/docker-compose-cli.yml up --build --abort-on-container-exit |
| local cli_exit=$? |
| if [ $cli_exit -ne 0 ]; then |
| docker compose -f tests/e2e/docker-compose-cli.yml logs pinchtab | tail -n 50 |
| fi |
| docker compose -f tests/e2e/docker-compose-cli.yml down -v 2>/dev/null |
|
|
| echo "" |
| if [ $curl_exit -ne 0 ] || [ $orch_exit -ne 0 ] || [ $cli_exit -ne 0 ]; then |
| echo " ${ERROR}Some E2E tests failed${NC}" |
| exit 1 |
| fi |
| echo " ${SUCCESS}All E2E tests passed${NC}" |
| exit 0 |
| ;; |
| "e2e curl") |
| echo " ${ACCENT}${BOLD}π³ E2E Curl tests (Docker)${NC}" |
| echo "" |
| docker compose -f tests/e2e/docker-compose.yml up --build --abort-on-container-exit |
| local curl_exit=$? |
| if [ $curl_exit -ne 0 ]; then |
| docker compose -f tests/e2e/docker-compose.yml logs pinchtab | tail -n 50 |
| fi |
| docker compose -f tests/e2e/docker-compose.yml down -v 2>/dev/null |
| exit $curl_exit |
| ;; |
| "e2e cli") |
| echo " ${ACCENT}${BOLD}π³ E2E CLI tests (Docker)${NC}" |
| echo "" |
| build_e2e_cli_binary |
| docker compose -f tests/e2e/docker-compose-cli.yml up --build --abort-on-container-exit |
| local cli_exit=$? |
| if [ $cli_exit -ne 0 ]; then |
| docker compose -f tests/e2e/docker-compose-cli.yml logs pinchtab | tail -n 50 |
| fi |
| docker compose -f tests/e2e/docker-compose-cli.yml down -v 2>/dev/null |
| exit $cli_exit |
| ;; |
| "e2e orchestrator") |
| echo " ${ACCENT}${BOLD}π³ E2E Orchestrator tests (Docker)${NC}" |
| echo "" |
| docker compose -f tests/e2e/docker-compose-orchestrator.yml run --build --rm runner |
| local orch_exit=$? |
| if [ $orch_exit -ne 0 ]; then |
| docker compose -f tests/e2e/docker-compose-orchestrator.yml logs pinchtab | tail -n 50 || true |
| docker compose -f tests/e2e/docker-compose-orchestrator.yml logs pinchtab-bridge | tail -n 50 || true |
| fi |
| docker compose -f tests/e2e/docker-compose-orchestrator.yml down -v 2>/dev/null |
| exit $orch_exit |
| ;; |
| "e2e recent") |
| echo " ${ACCENT}${BOLD}π³ E2E Recent tests (Docker)${NC}" |
| echo "" |
| docker compose -f tests/e2e/docker-compose.yml run --build --rm runner /scenarios-recent/run.sh |
| local recent_exit=$? |
| if [ $recent_exit -ne 0 ]; then |
| echo "" |
| echo -e "${ERROR} Recent tests failed. Dumping pinchtab logs:${NC}" |
| docker compose -f tests/e2e/docker-compose.yml logs pinchtab || true |
| echo "" |
| echo -e "${ERROR} Filtered Chrome/extension lines:${NC}" |
| docker compose -f tests/e2e/docker-compose.yml logs pinchtab 2>/dev/null | grep -Ei 'chrome|extension|devtools|load-extension|disable-extensions|headless|warning|error' || true |
| echo "" |
| echo -e "${ERROR} Instance inventory:${NC}" |
| local instances_json |
| instances_json=$(curl -sf http://localhost:9999/instances || true) |
| if [ -n "$instances_json" ]; then |
| echo "$instances_json" |
| if command -v jq >/dev/null 2>&1; then |
| while IFS= read -r inst_id; do |
| [ -z "$inst_id" ] && continue |
| echo "" |
| echo -e "${ERROR} Instance logs for ${inst_id}:${NC}" |
| curl -sf "http://localhost:9999/instances/${inst_id}/logs" || true |
| done < <(echo "$instances_json" | jq -r '.[].id') |
| fi |
| else |
| echo " unable to fetch /instances from localhost:9999" |
| fi |
| fi |
| docker compose -f tests/e2e/docker-compose.yml down -v 2>/dev/null |
| exit $recent_exit |
| ;; |
| esac |
| ;; |
| "dev") exec bash scripts/dev.sh ;; |
| "build") exec bash scripts/build.sh ;; |
| "binary") exec bash scripts/binary.sh ;; |
| "binary all"|"all") exec bash scripts/binary.sh all ;; |
| "run") exec bash scripts/run.sh ;; |
| "doctor") exec bash scripts/doctor.sh ;; |
| "hooks") exec bash scripts/install-hooks.sh ;; |
| *) |
| echo " ${ERROR}Unknown command: $target${NC}" |
| show_help |
| exit 1 |
| ;; |
| esac |
| } |
|
|
| pick_with_gum() { |
| local options=() |
| local commands_map=() |
| local parent="" |
| for cmd in "${COMMANDS[@]}"; do |
| IFS=':' read -r name emoji desc <<< "$cmd" |
| local trimmed="${name#"${name%%[![:space:]]*}"}" |
| if [[ "$name" == " "* ]]; then |
| |
| options+=("$(printf ' %-18s %s' "$trimmed" "$desc")") |
| commands_map+=("$parent $trimmed") |
| else |
| parent="$trimmed" |
| options+=("$(printf '%s %-18s %s' "$emoji" "$trimmed" "$desc")") |
| commands_map+=("$trimmed") |
| fi |
| done |
|
|
| local choice |
| choice=$(printf '%s\n' "${options[@]}" | gum choose \ |
| --header "π¦ Pinchtab Dev" \ |
| --header.foreground "#fbbf24" \ |
| --cursor.foreground "#00e5cc" \ |
| --selected.foreground "#00e5cc" \ |
| --item.foreground "#8892b0") |
|
|
| |
| local picked="" |
| for i in "${!options[@]}"; do |
| if [[ "${options[$i]}" == "$choice" ]]; then |
| picked="${commands_map[$i]}" |
| break |
| fi |
| done |
|
|
| |
| printf '\033[2J\033[H' |
| run_command "$picked" |
| } |
|
|
| pick_with_select() { |
| echo "" |
| echo " ${ACCENT}${BOLD}π¦ Pinchtab Dev${NC}" |
| echo "" |
|
|
| local names=() |
| local i=1 |
| for cmd in "${COMMANDS[@]}"; do |
| IFS=':' read -r name emoji desc <<< "$cmd" |
| local trimmed="${name#"${name%%[![:space:]]*}"}" |
| if [[ "$name" == " "* ]]; then |
| printf " ${MUTED}[%3d]${NC} ${BOLD}%-18s${NC} ${MUTED}%s${NC}\n" "$i" "$trimmed" "$desc" |
| else |
| printf " ${MUTED}[%3d]${NC} ${SUCCESS}%s${NC} ${BOLD}%-18s${NC} ${MUTED}%s${NC}\n" "$i" "$emoji" "$trimmed" "$desc" |
| fi |
| names+=("$trimmed") |
| i=$((i + 1)) |
| done |
|
|
| echo "" |
| echo -ne " ${BOLD}Pick [1-${#COMMANDS[@]}]:${NC} " |
| read -r choice |
|
|
| |
| if [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -le ${#COMMANDS[@]} ]; then |
| run_command "${names[$((choice - 1))]}" |
| else |
| run_command "$choice" |
| fi |
| } |
|
|
| |
|
|
| |
| if [ $# -gt 0 ]; then |
| case "$1" in |
| -h|--help|help) show_help; exit 0 ;; |
| -i|--interactive) |
| if command -v gum &>/dev/null; then pick_with_gum; else pick_with_select; fi |
| exit 0 |
| ;; |
| check) |
| if [ $# -gt 1 ]; then |
| run_command "check $2" |
| else |
| run_command "check" |
| fi |
| ;; |
| test) |
| if [ $# -gt 1 ]; then |
| run_command "test $2" |
| else |
| run_command "test" |
| fi |
| ;; |
| format) |
| if [ $# -gt 1 ]; then |
| run_command "format $2" |
| else |
| run_command "format" |
| fi |
| ;; |
| e2e) |
| if [ $# -gt 1 ]; then |
| run_command "e2e $2" |
| else |
| run_command "e2e" |
| fi |
| ;; |
| binary) |
| if [ $# -gt 1 ]; then |
| run_command "binary $2" |
| else |
| run_command "binary" |
| fi |
| ;; |
| *) run_command "$1" ;; |
| esac |
| fi |
|
|
| |
| if command -v gum &>/dev/null; then |
| pick_with_gum |
| else |
| pick_with_select |
| fi |
|
|