- requeue <job>: move a failed job back to inbox/ and drop stale meta/body so it re-runs cleanly - clean [--keep N]: archive finished jobs' logs+meta beyond the newest N (default 50) into queue/.archive/<ts>/; running jobs + .md records untouched - document both in usage + bytelyst-cli subcommand list
292 lines
11 KiB
Bash
292 lines
11 KiB
Bash
#!/bin/bash
|
|
|
|
# bytelyst-cli.sh: Unified CLI for Bytelyst GitHub DevOps Tools
|
|
#
|
|
# Usage examples:
|
|
# ./bytelyst-cli.sh list-public-repos --user <username>
|
|
# ./bytelyst-cli.sh list-private-repos --org <orgname>
|
|
# ./bytelyst-cli.sh check-collaborators --input input.json
|
|
# ./bytelyst-cli.sh export --type repos --output repos.json
|
|
#
|
|
# If no arguments are given, an interactive menu is shown.
|
|
|
|
RED=$(tput setaf 1)
|
|
GREEN=$(tput setaf 2)
|
|
YELLOW=$(tput setaf 3)
|
|
BLUE=$(tput setaf 4)
|
|
RESET=$(tput sgr0)
|
|
|
|
CLI_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
|
# agent-queue delegates to the standalone tool (no GitHub token / jq / curl needed),
|
|
# so handle it BEFORE the GITHUB_TOKEN + required-tools gates below.
|
|
if [[ "${1:-}" == "agent-queue" || "${1:-}" == "aq" ]]; then
|
|
shift
|
|
exec "$CLI_DIR/agent-queue/agent-queue.sh" "$@"
|
|
fi
|
|
|
|
REQUIRED_TOOLS=(jq curl)
|
|
|
|
# Check for required tools
|
|
for tool in "${REQUIRED_TOOLS[@]}"; do
|
|
if ! command -v $tool &>/dev/null; then
|
|
echo "${RED}❌ Error: Required tool '$tool' is not installed. Please install it and try again.${RESET}"
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
# Load .env if present. `set -a` exports everything sourced; this safely handles
|
|
# quoted values and spaces, unlike `export $(grep ... | xargs)`.
|
|
if [[ -f .env ]]; then
|
|
set -a
|
|
# shellcheck disable=SC1091
|
|
. ./.env
|
|
set +a
|
|
fi
|
|
|
|
# Validate GITHUB_TOKEN (printf so the newline renders, unlike echo "...\n...")
|
|
if [[ -z "${GITHUB_TOKEN:-}" ]]; then
|
|
printf '%s❌ Error: GITHUB_TOKEN is not set.\nSet it in your environment (e.g. export GITHUB_TOKEN=... in ~/.zshrc, ~/.bashrc, or .env).%s\n' "$RED" "$RESET" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# gh_get_all <url> -> echo one JSON array combining ALL pages (per_page=100).
|
|
# Verifies HTTP 200 on every page before parsing; returns non-zero on API error.
|
|
gh_get_all() {
|
|
local base="$1" page=1 combined="[]"
|
|
local joiner='&'; [[ "$base" == *'?'* ]] || joiner='?'
|
|
while :; do
|
|
local resp http body n
|
|
resp=$(curl -sS -w $'\n%{http_code}' \
|
|
-H "Authorization: token $GITHUB_TOKEN" \
|
|
-H "Accept: application/vnd.github+json" \
|
|
"${base}${joiner}per_page=100&page=${page}")
|
|
http="${resp##*$'\n'}"
|
|
body="${resp%$'\n'*}"
|
|
if [[ "$http" != "200" ]]; then
|
|
printf '%s❌ GitHub API error (HTTP %s) for %s%s\n' "$RED" "$http" "$base" "$RESET" >&2
|
|
printf '%s' "$body" | jq -r '.message? // empty' >&2 2>/dev/null || true
|
|
return 1
|
|
fi
|
|
n=$(printf '%s' "$body" | jq 'length' 2>/dev/null || echo 0)
|
|
[[ "$n" -eq 0 ]] && break
|
|
combined=$(jq -s 'add' <(printf '%s' "$combined") <(printf '%s' "$body"))
|
|
[[ "$n" -lt 100 ]] && break
|
|
page=$((page+1))
|
|
[[ "$page" -gt 100 ]] && break
|
|
done
|
|
printf '%s' "$combined"
|
|
}
|
|
|
|
usage() {
|
|
echo "${BLUE}Bytelyst CLI - Unified GitHub DevOps Tool${RESET}"
|
|
echo ""
|
|
echo "Usage: $0 <command> [options]"
|
|
echo "Commands:"
|
|
echo " list-public-repos --user <username>"
|
|
echo " list-private-repos --org <orgname>"
|
|
echo " check-collaborators --input <input.json>"
|
|
echo " export --type <repos|users> --output <file.json>"
|
|
echo " remove-user-from-all-repos --user <username> [--input <file.json>]"
|
|
echo " agent-queue (aq) <init|add|run|status|watch|dash|stop|logs|requeue|clean> — agent prompt queue runner"
|
|
echo " help Show this help message"
|
|
echo ""
|
|
echo "If no command is given, an interactive menu will be shown."
|
|
}
|
|
|
|
list_public_repos() {
|
|
local user=""
|
|
while [[ $# -gt 0 ]]; do
|
|
case $1 in
|
|
--user)
|
|
user="$2"; shift 2;;
|
|
*) shift;;
|
|
esac
|
|
done
|
|
if [[ -z "$user" ]]; then
|
|
echo "${RED}❌ Please provide --user <username>.${RESET}"; exit 1
|
|
fi
|
|
echo "${BLUE}🔍 Fetching all public repositories for user: $user...${RESET}"
|
|
local json repos
|
|
json=$(gh_get_all "https://api.github.com/users/$user/repos?type=public") || exit 1
|
|
repos=$(printf '%s' "$json" | jq -r '.[].full_name')
|
|
if [[ -z "$repos" ]]; then
|
|
echo "${YELLOW}🚫 No public repositories found for user.${RESET}"
|
|
else
|
|
echo "$repos"
|
|
fi
|
|
}
|
|
|
|
list_private_repos() {
|
|
local org=""
|
|
while [[ $# -gt 0 ]]; do
|
|
case $1 in
|
|
--org)
|
|
org="$2"; shift 2;;
|
|
*) shift;;
|
|
esac
|
|
done
|
|
if [[ -z "$org" ]]; then
|
|
echo "${RED}❌ Please provide --org <orgname>.${RESET}"; exit 1
|
|
fi
|
|
echo "${BLUE}🔍 Fetching all private repositories for org: $org...${RESET}"
|
|
local json repos
|
|
json=$(gh_get_all "https://api.github.com/orgs/$org/repos?type=private") || exit 1
|
|
repos=$(printf '%s' "$json" | jq -r '.[].full_name')
|
|
if [[ -z "$repos" ]]; then
|
|
echo "${YELLOW}🚫 No private repositories found for org.${RESET}"
|
|
else
|
|
echo "$repos"
|
|
fi
|
|
}
|
|
|
|
check_collaborators() {
|
|
local input=""
|
|
while [[ $# -gt 0 ]]; do
|
|
case $1 in
|
|
--input)
|
|
input="$2"; shift 2;;
|
|
*) shift;;
|
|
esac
|
|
done
|
|
if [[ -z "$input" || ! -f "$input" ]]; then
|
|
echo "${RED}❌ Please provide --input <input.json> (file must exist).${RESET}"; exit 1
|
|
fi
|
|
local org=$(jq -r '.org' "$input")
|
|
local user=$(jq -r '.user' "$input")
|
|
local whitelist=($(jq -r '.whitelist[]' "$input"))
|
|
local repos=($(jq -r '.repos[]' "$input"))
|
|
if [[ -z "$org" || -z "$user" || ${#whitelist[@]} -eq 0 || ${#repos[@]} -eq 0 ]]; then
|
|
echo "${RED}❌ input.json must contain 'org', 'user', 'whitelist', and 'repos'.${RESET}"; exit 1
|
|
fi
|
|
for repo in "${repos[@]}"; do
|
|
echo "${BLUE}🔍 Checking repo: $repo${RESET}"
|
|
local cjson collaborators
|
|
cjson=$(gh_get_all "https://api.github.com/repos/$org/$repo/collaborators") \
|
|
|| { echo "${YELLOW}⚠️ Skipping $repo (API error).${RESET}"; continue; }
|
|
collaborators=$(printf '%s' "$cjson" | jq -r '.[].login')
|
|
local non_whitelisted=()
|
|
for collab in $collaborators; do
|
|
# explicit membership test (avoids the array-concatenation pitfall of
|
|
# [[ " ${whitelist[@]} " =~ " $collab " ]], which false-matches substrings)
|
|
local is_white=false w
|
|
for w in "${whitelist[@]}"; do
|
|
[[ "$w" == "$collab" ]] && { is_white=true; break; }
|
|
done
|
|
$is_white || non_whitelisted+=("$collab")
|
|
done
|
|
if [[ ${#non_whitelisted[@]} -gt 0 ]]; then
|
|
echo "${YELLOW}🚨 Repository: $repo${RESET}"
|
|
echo "${RED}❌ Non-Whitelisted Collaborators:${RESET}"
|
|
printf '%s\n' "${non_whitelisted[@]}"
|
|
for user in "${non_whitelisted[@]}"; do
|
|
read -p "Do you want to remove collaborator '$user' from '$repo'? (yes/no): " confirm
|
|
if [[ "$confirm" == "yes" ]]; then
|
|
response=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE -H "Authorization: token $GITHUB_TOKEN" "https://api.github.com/repos/$org/$repo/collaborators/$user")
|
|
if [[ "$response" -eq 204 ]]; then
|
|
echo "${GREEN}✅ Successfully removed $user from repository $repo.${RESET}"
|
|
else
|
|
echo "${YELLOW}⚠️ Failed to remove $user from repository $repo (HTTP Status: $response).${RESET}"
|
|
fi
|
|
else
|
|
echo "${YELLOW}🚫 Skipped removal of $user from $repo.${RESET}"
|
|
fi
|
|
done
|
|
echo "--------------------------------------------"
|
|
fi
|
|
done
|
|
}
|
|
|
|
export_json() {
|
|
local type=""; local output=""
|
|
while [[ $# -gt 0 ]]; do
|
|
case $1 in
|
|
--type)
|
|
type="$2"; shift 2;;
|
|
--output)
|
|
output="$2"; shift 2;;
|
|
*) shift;;
|
|
esac
|
|
done
|
|
if [[ -z "$type" || -z "$output" ]]; then
|
|
echo "${RED}❌ Please provide --type <repos|users> and --output <file.json>.${RESET}"; exit 1
|
|
fi
|
|
if [[ "$type" == "repos" ]]; then
|
|
jq . repos.json > "$output"
|
|
echo "${GREEN}✅ Exported repos to $output${RESET}"
|
|
elif [[ "$type" == "users" ]]; then
|
|
jq . users.json > "$output"
|
|
echo "${GREEN}✅ Exported users to $output${RESET}"
|
|
else
|
|
echo "${RED}❌ Unknown export type: $type${RESET}"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
remove_user_from_all_repos() {
|
|
local user=""; local input="github_repos.json"
|
|
while [[ $# -gt 0 ]]; do
|
|
case $1 in
|
|
--user)
|
|
user="$2"; shift 2;;
|
|
--input)
|
|
input="$2"; shift 2;;
|
|
*) shift;;
|
|
esac
|
|
done
|
|
if [[ -z "$user" ]]; then
|
|
echo "${RED}❌ Please provide --user <username>.${RESET}"; exit 1
|
|
fi
|
|
if [[ ! -f "$input" ]]; then
|
|
echo "${RED}❌ Input file '$input' not found.${RESET}"; exit 1
|
|
fi
|
|
local org=$(jq -r '.org' "$input")
|
|
local repos=($(jq -r '.repos[]' "$input"))
|
|
if [[ -z "$org" || ${#repos[@]} -eq 0 ]]; then
|
|
echo "${RED}❌ Input file must contain 'org' and 'repos'.${RESET}"; exit 1
|
|
fi
|
|
for repo in "${repos[@]}"; do
|
|
echo "${BLUE}🔗 Removing user '$user' from repo: $repo${RESET}"
|
|
response=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE -H "Authorization: token $GITHUB_TOKEN" "https://api.github.com/repos/$org/$repo/collaborators/$user")
|
|
if [[ "$response" -eq 204 ]]; then
|
|
echo "${GREEN}✅ Successfully removed $user from $repo.${RESET}"
|
|
elif [[ "$response" -eq 404 ]]; then
|
|
echo "${YELLOW}⚠️ $user is not a collaborator on $repo or repo not found.${RESET}"
|
|
else
|
|
echo "${RED}❌ Failed to remove $user from $repo (HTTP Status: $response).${RESET}"
|
|
fi
|
|
done
|
|
}
|
|
|
|
interactive_menu() {
|
|
echo "${BLUE}Bytelyst CLI Interactive Menu${RESET}"
|
|
select opt in "List Public Repos" "List Private Repos" "Check Collaborators" "Export JSON" "Remove User from All Repos" "Agent Queue Status" "Exit"; do
|
|
case $REPLY in
|
|
1) read -p "Enter GitHub username: " user; list_public_repos --user "$user";;
|
|
2) read -p "Enter GitHub org: " org; list_private_repos --org "$org";;
|
|
3) read -p "Enter path to input.json: " input; check_collaborators --input "$input";;
|
|
4) read -p "Export type (repos/users): " type; read -p "Output file: " output; export_json --type "$type" --output "$output";;
|
|
5) read -p "Enter GitHub username: " user; remove_user_from_all_repos --user "$user";;
|
|
6) "$CLI_DIR/agent-queue/agent-queue.sh" status;;
|
|
7) exit 0;;
|
|
*) echo "Invalid option.";;
|
|
esac
|
|
done
|
|
}
|
|
|
|
# Main CLI dispatcher
|
|
if [[ $# -eq 0 ]]; then
|
|
interactive_menu
|
|
exit 0
|
|
fi
|
|
|
|
case $1 in
|
|
list-public-repos) shift; list_public_repos "$@";;
|
|
list-private-repos) shift; list_private_repos "$@";;
|
|
check-collaborators) shift; check_collaborators "$@";;
|
|
export) shift; export_json "$@";;
|
|
remove-user-from-all-repos) shift; remove_user_from_all_repos "$@";;
|
|
agent-queue|aq) shift; exec "$CLI_DIR/agent-queue/agent-queue.sh" "$@";;
|
|
help|--help|-h) usage;;
|
|
*) echo "${RED}Unknown command: $1${RESET}"; usage; exit 1;;
|
|
esac |