From ba7b2663e85ef2fc8af1a4a600ebcc230f173da7 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Fri, 24 Apr 2026 00:11:43 -0700 Subject: [PATCH] feat(setup-gbrain): add list-orphans + delete-project subcommands (D20) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Powers /setup-gbrain --cleanup-orphans. list-orphans filters the authenticated user's Supabase projects by name prefix (default "gbrain") and excludes the project the local ~/.gbrain/config.json currently points at, so only unclaimed gbrain-shaped projects come back. Active-ref detection parses the pooler URL's user portion (postgres.:@...). delete-project is a thin DELETE /v1/projects/{ref} wrapper with no confirmation of its own — the skill's UI layer owns the per-project confirm AskUserQuestion loop. Keeps responsibilities clean: the bin manages HTTP; the skill manages user intent. Both subcommands reuse the existing api_call retry+backoff and the same PAT discipline (env only, never argv). Co-Authored-By: Claude Opus 4.7 (1M context) --- bin/gstack-gbrain-supabase-provision | 95 ++++++++++++++++++++++++++-- 1 file changed, 89 insertions(+), 6 deletions(-) diff --git a/bin/gstack-gbrain-supabase-provision b/bin/gstack-gbrain-supabase-provision index 7bc6c9a0..3f3128e9 100755 --- a/bin/gstack-gbrain-supabase-provision +++ b/bin/gstack-gbrain-supabase-provision @@ -27,6 +27,20 @@ # real value — we build from db_user/db_host/db_port/db_name instead). # Output: {"ref","pooler_url"}. # +# list-orphans [--name-prefix ] +# GET /v1/projects. Filter to projects whose name starts with --name-prefix +# (default "gbrain") AND whose ref does NOT match the one in the local +# active ~/.gbrain/config.json pooler URL. Those are the gbrain-shaped +# projects that aren't pointed at by a working local config — candidates +# for /setup-gbrain --cleanup-orphans. +# Output: {"active_ref","orphans":[{"ref","name","created_at","region"}, ...]}. +# +# delete-project +# DELETE /v1/projects/{ref}. Destructive, one-way — callers must +# double-confirm before invoking. This bin performs NO confirmation +# prompt; the skill's UI layer owns that responsibility. +# Output: {"deleted_ref"}. +# # Secrets discipline (D8, D10, D11): # - SUPABASE_ACCESS_TOKEN is read from env; never accepted as argv. # - DB_PASS (for `create` and `pooler-url`) is read from env; never argv. @@ -353,12 +367,81 @@ cmd_pooler_url() { fi } +cmd_list_orphans() { + local name_prefix="gbrain" + local json_mode=false + while [ $# -gt 0 ]; do + case "$1" in + --name-prefix) name_prefix="$2"; shift 2 ;; + --json) json_mode=true; shift ;; + --*) die "list-orphans: unknown flag: $1" ;; + *) die "list-orphans: unexpected arg: $1" ;; + esac + done + + require_jq; require_curl; require_pat + local all + all=$(api_call GET projects) + + # Extract the active brain's ref from ~/.gbrain/config.json if present. + # Pooler URL format: postgresql://postgres.:@... + local active_ref="null" + local gbrain_cfg="$HOME/.gbrain/config.json" + if [ -f "$gbrain_cfg" ]; then + local url + url=$(jq -r '.database_url // empty' "$gbrain_cfg" 2>/dev/null || true) + if [ -n "$url" ]; then + # Extract user portion before the colon: postgresql://USER:pw@... + local user + user=$(printf '%s' "$url" | sed -E 's|^[a-z]+://([^:]+):.*$|\1|') + # User format: postgres. — pull ref suffix + case "$user" in + postgres.*) + local ref="${user#postgres.}" + active_ref=$(jq -Rn --arg r "$ref" '$r') + ;; + esac + fi + fi + + local orphans + orphans=$(printf '%s' "$all" | jq \ + --arg prefix "$name_prefix" \ + --argjson active "$active_ref" \ + '[.[] + | select(.name | startswith($prefix)) + | select(.ref != $active) + | {ref: .ref, name: .name, created_at: .created_at, region: .region}]') + + jq -n --argjson active "$active_ref" --argjson orphans "$orphans" \ + '{active_ref: $active, orphans: $orphans}' +} + +cmd_delete_project() { + local ref="" + local json_mode=false + while [ $# -gt 0 ]; do + case "$1" in + --json) json_mode=true; shift ;; + --*) die "delete-project: unknown flag: $1" ;; + *) ref="$1"; shift ;; + esac + done + [ -z "$ref" ] && die "delete-project: missing " + + require_jq; require_curl; require_pat + api_call DELETE "projects/$ref" >/dev/null + jq -n --arg ref "$ref" '{deleted_ref: $ref}' +} + case "${1:-}" in - list-orgs) shift; cmd_list_orgs "$@" ;; - create) shift; cmd_create "$@" ;; - wait) shift; cmd_wait "$@" ;; - pooler-url) shift; cmd_pooler_url "$@" ;; - --help|-h|help) sed -n '2,60p' "$0" | sed 's/^# \{0,1\}//' ;; - "") die "usage: gstack-gbrain-supabase-provision {list-orgs|create|wait|pooler-url|--help}" ;; + list-orgs) shift; cmd_list_orgs "$@" ;; + create) shift; cmd_create "$@" ;; + wait) shift; cmd_wait "$@" ;; + pooler-url) shift; cmd_pooler_url "$@" ;; + list-orphans) shift; cmd_list_orphans "$@" ;; + delete-project) shift; cmd_delete_project "$@" ;; + --help|-h|help) sed -n '2,80p' "$0" | sed 's/^# \{0,1\}//' ;; + "") die "usage: gstack-gbrain-supabase-provision {list-orgs|create|wait|pooler-url|list-orphans|delete-project|--help}" ;; *) die "unknown subcommand: $1" ;; esac