Files
dotfiles/dot_local/bin/executable_gh-sonr-org-migrate
Prad Nukala 6406208b35 shell: fish functions -> ~/.local/bin executables + bash/zsh ports; manage ~/.agents
- 16 cross-shell utilities converted to sh/bash chezmoi executables:
  ai c2p cp-appid extract_agents fab gdc gh-sonr-org-migrate ghg-clone
  ghn gho git-summary launch_omp_livediff_overlay lc-elir mdcp mdp pj
  (ghg-clone not named ghg: unrelated cargo binary owns that name)
- cwd/history helpers stay per-shell: yy c cd clip-last + ghg/ghn/gho
  cd-wrappers in bash/functions/ and zsh/functions/, loaded by rc files
- fish keeps only shell-state functions; converted ones now hit PATH
- cp-appid ignore gating moved to .local/bin path
- dot_agents/skills: 82 skills materialized from dotstate storage so
  dot_claude skill symlinks resolve on fresh bootstrap; dotstate
  profile/symlinks.json released .agents/skills .bashrc .zshrc
2026-08-10 15:15:06 -04:00

164 lines
5.7 KiB
Bash
Executable File

#!/usr/bin/env bash
# gh-sonr-org-migrate
# Migrates all repos from orgs matching "sonr" into sonr-io,
# prompting via gum for renames on collision, then deletes emptied orgs.
#
# Requires: gh (authenticated as org owner on all orgs), gum, jq
TARGET_ORG=sonr-io
red() { printf '\033[31m%s\033[0m\n' "$*"; }
green() { printf '\033[32m%s\033[0m\n' "$*"; }
yellow() { printf '\033[33m%s\033[0m\n' "$*"; }
cyan() { printf '\033[36m%s\033[0m\n' "$*"; }
bcyan() { printf '\033[1;36m%s\033[0m\n' "$*"; }
bgreen() { printf '\033[1;32m%s\033[0m\n' "$*"; }
# ── Preflight ────────────────────────────────────────────────────────────────
for bin in gh gum jq; do
if ! command -v "$bin" >/dev/null 2>&1; then
red "missing dependency: $bin"
exit 1
fi
done
if ! gh auth status >/dev/null 2>&1; then
red "gh is not authenticated"
exit 1
fi
# ── Discover source orgs ─────────────────────────────────────────────────────
# All orgs the user belongs to whose login contains "sonr", excluding target.
mapfile -t source_orgs < <(gh api user/orgs --paginate --jq '.[].login' \
| grep 'sonr' | grep -vx "$TARGET_ORG")
if [ "${#source_orgs[@]}" -eq 0 ]; then
echo "No source orgs found. Nothing to do."
exit 0
fi
cyan "Source orgs:"
printf ' %s\n' "${source_orgs[@]}"
cyan "Target org:"
echo " $TARGET_ORG"
echo
# Let the user confirm / trim the list interactively
mapfile -t selected_orgs < <(printf '%s\n' "${source_orgs[@]}" | gum choose --no-limit \
--header "Select orgs to migrate into $TARGET_ORG (space to toggle, enter to confirm)")
if [ "${#selected_orgs[@]}" -eq 0 ]; then
echo "Nothing selected. Exiting."
exit 0
fi
# ── Helpers ──────────────────────────────────────────────────────────────────
repo_exists_in_target() {
gh api "repos/$TARGET_ORG/$1" --silent >/dev/null 2>&1
}
transfer_repo() {
# $1 = source org, $2 = repo name, $3 = (optional) new name
local src=$1 repo=$2 new_name=${3:-}
local args=(-f "new_owner=$TARGET_ORG")
if [ -n "$new_name" ]; then
args+=(-f "new_name=$new_name")
fi
gh api -X POST "repos/$src/$repo/transfer" "${args[@]}" --silent
}
# ── Migration ────────────────────────────────────────────────────────────────
failed_transfers=()
for org in "${selected_orgs[@]}"; do
echo
bcyan "── $org ──"
mapfile -t repos < <(gh api "orgs/$org/repos" --paginate --jq '.[].name')
if [ "${#repos[@]}" -eq 0 ]; then
echo " (no repos)"
continue
fi
for repo in "${repos[@]}"; do
new_name=""
if repo_exists_in_target "$repo"; then
yellow " ⚠ $repo already exists in $TARGET_ORG"
while true; do
new_name=$(gum input \
--header "New name for $org/$repo (exists in $TARGET_ORG)" \
--placeholder "$org-$repo" \
--value "$org-$repo")
if [ -z "$new_name" ]; then
echo " skipped $org/$repo"
new_name=__SKIP__
break
fi
if repo_exists_in_target "$new_name"; then
yellow " $new_name also exists — pick another"
else
break
fi
done
[ "$new_name" = __SKIP__ ] && continue
fi
label="$org/$repo → $TARGET_ORG/${new_name:-$repo}"
if transfer_repo "$org" "$repo" "$new_name"; then
green " ✓ $label"
else
red " ✗ failed: $label"
failed_transfers+=("$org/$repo")
fi
done
done
if [ "${#failed_transfers[@]}" -gt 0 ]; then
echo
red "Failed transfers (orgs containing these will NOT be deleted):"
printf ' %s\n' "${failed_transfers[@]}"
fi
# ── Wait for transfers to settle ─────────────────────────────────────────────
# GitHub repo transfers are asynchronous; give them time to drain.
echo
gum spin --title "Waiting 30s for transfers to settle..." -- sleep 30
# ── Delete emptied orgs ──────────────────────────────────────────────────────
echo
for org in "${selected_orgs[@]}"; do
# Skip orgs with failed transfers
org_had_failure=0
for f in ${failed_transfers[@]+"${failed_transfers[@]}"}; do
case "$f" in
"$org"/*) org_had_failure=1; break ;;
esac
done
if [ "$org_had_failure" -eq 1 ]; then
yellow "skipping delete: $org (had failures)"
continue
fi
remaining=$(gh api "orgs/$org/repos" --paginate --jq '. | length' | jq -s 'add // 0')
if [ "$remaining" -gt 0 ]; then
yellow "skipping delete: $org still has $remaining repo(s) (transfer pending?)"
continue
fi
if gum confirm "Delete empty org $org?"; then
if gh api -X DELETE "orgs/$org" --silent; then
green "✓ deleted $org"
else
red "✗ could not delete $org (API delete requires org owner; may need manual deletion in Settings)"
fi
else
echo "kept $org"
fi
done
echo
bgreen "Done."