#!/usr/bin/env bash # cleanup-claude-dirs.sh — Remove .claude/ directories from all repos # # .claude/ is local workspace config (MCP settings, worktrees) and should # never be committed. This script: # 1. Checks if .claude/ exists in the repo via Gitea API # 2. Deletes all files in .claude/ via API # 3. Ensures .claude/ is in .gitignore # # Usage: # cleanup-claude-dirs.sh # all repos # cleanup-claude-dirs.sh MokoOnyx # one repo # set -euo pipefail GITEA_URL="${GITEA_URL:-https://git.mokoconsulting.tech}" GITEA_TOKEN="${GITEA_TOKEN:-$(cat ~/.gitea-token 2>/dev/null || echo "")}" if [[ -z "$GITEA_TOKEN" ]]; then echo "ERROR: GITEA_TOKEN not set" exit 1 fi FILTER="${1:-}" CLEANED=0 SKIPPED=0 log() { echo "[$(date '+%H:%M:%S')] $*"; } # Get all repos across orgs get_repos() { for ORG in MokoConsulting ClarksvilleFurs; do PAGE=1 while true; do REPOS=$(curl -sf -H "Authorization: token ${GITEA_TOKEN}" \ "${GITEA_URL}/api/v1/orgs/${ORG}/repos?page=${PAGE}&limit=50" 2>/dev/null) [[ -z "$REPOS" || "$REPOS" == "[]" ]] && break echo "$REPOS" | python3 -c " import sys, json for r in json.load(sys.stdin): if not r.get('archived'): print(f'{r[\"owner\"][\"login\"]}/{r[\"name\"]}|{r[\"default_branch\"]}') " 2>/dev/null COUNT=$(echo "$REPOS" | python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null) [[ "$COUNT" -lt 50 ]] && break PAGE=$((PAGE + 1)) done done } while IFS='|' read -r FULL_NAME BRANCH; do [[ -z "$FULL_NAME" ]] && continue REPO_NAME="${FULL_NAME#*/}" # Filter if specified if [[ -n "$FILTER" && "$REPO_NAME" != "$FILTER" && "$FULL_NAME" != *"$FILTER"* ]]; then continue fi # Check if .claude/ exists in the repo CLAUDE_DIR=$(curl -sf -H "Authorization: token ${GITEA_TOKEN}" \ "${GITEA_URL}/api/v1/repos/${FULL_NAME}/contents/.claude?ref=${BRANCH}" 2>/dev/null) if [[ -z "$CLAUDE_DIR" || "$CLAUDE_DIR" == "null" ]]; then SKIPPED=$((SKIPPED + 1)) continue fi log "Cleaning ${FULL_NAME}..." # Get all files in .claude/ FILES=$(echo "$CLAUDE_DIR" | python3 -c " import sys, json data = json.load(sys.stdin) if isinstance(data, list): for f in data: if f.get('type') == 'file': print(f'{f[\"path\"]}|{f[\"sha\"]}') elif isinstance(data, dict) and data.get('type') == 'file': print(f'{data[\"path\"]}|{data[\"sha\"]}') " 2>/dev/null) # Delete each file while IFS='|' read -r FILE_PATH FILE_SHA; do [[ -z "$FILE_PATH" ]] && continue ENCODED_PATH=$(python3 -c "import urllib.parse; print(urllib.parse.quote('${FILE_PATH}', safe='/'))" 2>/dev/null) RESULT=$(curl -sf -X DELETE \ -H "Authorization: token ${GITEA_TOKEN}" \ -H "Content-Type: application/json" \ "${GITEA_URL}/api/v1/repos/${FULL_NAME}/contents/${ENCODED_PATH}" \ -d "{\"sha\": \"${FILE_SHA}\", \"message\": \"chore: remove .claude/ from version control [skip ci]\", \"branch\": \"${BRANCH}\"}" \ -w "%{http_code}" -o /dev/null 2>&1) if [[ "$RESULT" == "200" ]]; then log " Deleted: ${FILE_PATH}" else log " FAIL: ${FILE_PATH} (HTTP ${RESULT})" fi done <<< "$FILES" # Also check for .mcp.json MCP_JSON=$(curl -sf -H "Authorization: token ${GITEA_TOKEN}" \ "${GITEA_URL}/api/v1/repos/${FULL_NAME}/contents/.mcp.json?ref=${BRANCH}" 2>/dev/null) if [[ -n "$MCP_JSON" && "$MCP_JSON" != "null" ]]; then MCP_SHA=$(echo "$MCP_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null) if [[ -n "$MCP_SHA" ]]; then RESULT=$(curl -sf -X DELETE \ -H "Authorization: token ${GITEA_TOKEN}" \ -H "Content-Type: application/json" \ "${GITEA_URL}/api/v1/repos/${FULL_NAME}/contents/.mcp.json" \ -d "{\"sha\": \"${MCP_SHA}\", \"message\": \"chore: remove .mcp.json from version control [skip ci]\", \"branch\": \"${BRANCH}\"}" \ -w "%{http_code}" -o /dev/null 2>&1) [[ "$RESULT" == "200" ]] && log " Deleted: .mcp.json" fi fi CLEANED=$((CLEANED + 1)) done < <(get_repos) log "Done. Cleaned: ${CLEANED}, Skipped: ${SKIPPED}"