gitlab-connect
Rama main · réplica de lectura del repositorio original.
#!/usr/bin/env bash
# Robot Dev Team Project
# File: gitlab-connect
# Description: GitLab CLI wrapper for agent-aware workflows.
# License: MIT
# SPDX-License-Identifier: MIT
# Copyright (c) 2025 MCKNLY LLC
set -euo pipefail
HOST="${GLAB_HOST:-gitlab.example.com}"
# Loud-fail when GLAB_HOST resolves to the placeholder so we never silently
# target a non-existent server. Intent-based: trips even when explicitly
# exported, and matches ported variants (e.g. gitlab.example.com:8080).
# The hint deliberately names no example host: this file ships in the published
# image, and the one example a reader would find obvious is the placeholder the
# guard just rejected.
case "${HOST%%:*}" in
gitlab.example.com)
printf 'Error: GLAB_HOST is not configured (resolves to placeholder "%s").\n' "$HOST" >&2
printf ' Export GLAB_HOST=<your-gitlab-host> and retry.\n' >&2
exit 1
;;
esac
# Reject scheme in GLAB_HOST so we do not double-encode the protocol when
# constructing credential URLs and glab auth flags.
case "$HOST" in
*://*)
printf 'Error: GLAB_HOST must be a bare host (no scheme); got "%s".\n' "$HOST" >&2
printf ' Use GLAB_HOST=<host>[:port] and set GLAB_PROTOCOL=http when needed.\n' >&2
exit 1
;;
esac
CONFIG_FILE="${XDG_CONFIG_HOME:-$HOME/.config}/glab-cli/config.yml"
SCRIPT_NAME="$(basename "$0")"
timestamp() {
date -u +"%Y-%m-%dT%H:%M:%SZ"
}
current_glab_user() {
[[ -f "$CONFIG_FILE" ]] || return 0
awk -v host="$HOST" '
$1 == "hosts:" { in_hosts = 1; next }
in_hosts && $1 ~ /^[^:]+:$/ {
current = $1
sub(/:$/, "", current)
in_target = (current == host)
next
}
in_target && $1 == "user:" { print $2; exit }
' "$CONFIG_FILE"
}
resolve_agent_name() {
if [[ -n "${CURRENT_AGENT:-}" ]]; then
printf '%s' "$CURRENT_AGENT"
return
fi
if [[ -n "${ROBOT_AGENT_NAME:-}" ]]; then
printf '%s' "$ROBOT_AGENT_NAME"
return
fi
local glab_user
glab_user=$(current_glab_user)
if [[ -n "$glab_user" ]]; then
printf '%s' "$glab_user"
else
printf 'unknown'
fi
}
log_tool_call() {
local command="$1"
printf '%s | INFO | tool-call | tool=gitlab-connect agent=%s command=%s\n' \
"$(timestamp)" "$(resolve_agent_name)" "$command" >&2
}
format_args() {
local result=""
local arg
for arg in "$@"; do
result+=" $(printf '%q' "$arg")"
done
printf '%s' "$result"
}
die() {
printf 'Error: %s\n' "$1" >&2
exit 1
}
# --- read-only repo detection and --repo injection ---
#
# When the project directory is mounted read-only, glab cannot mutate
# .git/config and may struggle to auto-detect the repo from the working
# tree. Detect that case, derive the "owner/project" from origin, and
# inject --repo <owner/project> into every downstream glab invocation so
# the API call still targets the right repo.
#
# In the read-only case we also skip re-authentication on the assumption
# that the router has already authenticated this process (see the AUTO_AUTH
# gate near the bottom of the script). Writes would fail anyway, and
# re-running glab-usr would attempt .git/config mutations that error on a
# read-only mount.
REPO_FLAG=()
READONLY_REPO=0
detect_readonly_repo() {
local repo_root
repo_root=$(git rev-parse --show-toplevel 2>/dev/null) || return 1
if [[ -e "$repo_root/.git" ]]; then
# Root reports chmod(0555) paths as writable, which made this check depend
# on the caller's UID. Inspect mode bits as well so CI and local execution
# agree; the access check still catches read-only mounts with writable bits.
local mode
mode=$(stat -c '%a' "$repo_root/.git" 2>/dev/null || stat -f '%Lp' "$repo_root/.git" 2>/dev/null || true)
if [[ "$mode" =~ ^[0-7]+$ ]] && (( (8#$mode & 0222) == 0 )); then
return 0
fi
if ! test -w "$repo_root/.git"; then
return 0
fi
fi
return 1
}
extract_repo_path() {
# Returns the "owner/project" portion of the origin URL, with no host
# prefix. glab --repo resolves against the currently-authenticated host,
# so omitting the host makes injection robust to container-internal
# remotes (service names, ports) that do not match GLAB_HOST.
local url
url=$(git remote get-url origin 2>/dev/null) || {
echo "[gitlab-connect] WARN: could not resolve origin remote URL" >&2
return 1
}
local repo_path="$url"
if [[ "$repo_path" =~ ^[a-zA-Z0-9_.-]+@ ]]; then
# SSH scp-style: git@host:ns/proj.git -> ns/proj.git
repo_path="${repo_path#*@}"
repo_path="${repo_path#*:}"
elif [[ "$repo_path" == ssh://* ]]; then
# SSH protocol: ssh://[user@]host[:port]/ns/proj.git -> ns/proj.git
repo_path="${repo_path#*://}"
repo_path="${repo_path#*@}"
repo_path="${repo_path#*/}"
else
# HTTPS or other protocol with scheme: strip scheme, userinfo, host
repo_path="${repo_path#*://}"
repo_path="${repo_path#*@}"
repo_path="${repo_path#*/}"
fi
# Strip .git suffix
repo_path="${repo_path%.git}"
if [[ -z "$repo_path" || "$repo_path" == "$url" || "$repo_path" != */* ]]; then
echo "[gitlab-connect] WARN: could not parse repo path from origin URL: $url" >&2
return 1
fi
printf '%s' "$repo_path"
}
resolve_repo_context() {
if ! detect_readonly_repo; then
return
fi
READONLY_REPO=1
echo "[gitlab-connect] INFO: read-only repo detected, resolving repo context for glab" >&2
local repo_path
if repo_path=$(extract_repo_path); then
REPO_FLAG=("--repo" "$repo_path")
echo "[gitlab-connect] INFO: using --repo $repo_path for glab commands" >&2
else
echo "[gitlab-connect] WARN: falling back to glab auto-detection (may fail in RO mount)" >&2
fi
}
print_main_usage() {
cat <<'USAGE'
Usage: gitlab-connect [--agent NAME] [--no-auth] <command> [<args>]
Commands:
auth Authenticate the GitLab CLI as the specified agent
issue Create, edit, comment on, or view issues
mr Create, edit, comment on, or view merge requests
help Show this help text
Global options:
-a, --agent NAME Agent account to operate as (any configured agent name); defaults to $CURRENT_AGENT or $ROBOT_AGENT_NAME when unset
--no-auth Skip automatic authentication (advanced)
-h, --help Show this help text
Examples:
gitlab-connect issue create --title "Bug" --file body.md
gitlab-connect issue view 42
gitlab-connect issue comment 42 --file reply.md
gitlab-connect mr comment 17 --file note.md
gitlab-connect auth --agent claude
Authentication:
Tokens follow a naming convention: for agent "foo", set FOO_AGENT_GITLAB_TOKEN.
The container entrypoint mirrors each token into ~/.<agent>/glab-token (mode 0600).
gitlab-connect invokes glab-usr under the hood, which reads the env var or token
file and runs `glab auth login` before executing the requested command.
See docs/ADDING_AN_AGENT.md for details on onboarding custom agents.
USAGE
}
print_issue_usage() {
cat <<'USAGE'
Usage: gitlab-connect issue <action> [ID] [options] [-- GLAB_ARGS...]
Actions:
create Create a new issue (requires --title and --file)
edit Update an issue description (requires ID and --file)
comment Add a note to an issue (requires ID and --file)
view Display an issue with comments (requires ID)
list List issues (pass-through to `glab issue list`; flags
such as --state, --label, --author are forwarded)
Options:
-f, --file PATH Markdown file for create/edit/comment
-t, --title TITLE Title for create/edit
-h, --help Show this help text
Additional flags for the underlying `glab issue` command can be passed after `--`.
USAGE
}
print_mr_usage() {
cat <<'USAGE'
Usage: gitlab-connect mr <action> [ID] [options] [-- GLAB_ARGS...]
Actions:
create Create a new merge request (requires --title and --file)
edit Update an MR description (requires ID and --file)
comment Add a note to an MR (requires ID and --file)
view Display an MR with comments (requires ID)
list List merge requests (pass-through to `glab mr list`;
flags such as --source-branch, --state, --author
are forwarded)
Options:
-f, --file PATH Markdown file for create/edit/comment
-t, --title TITLE Title for create/edit
-h, --help Show this help text
Additional flags for the underlying `glab mr` command can be passed after `--`.
USAGE
}
normalize_issue_action() {
case "$1" in
create|edit|comment|view|list) printf '%s' "$1" ;;
show|get) printf 'view' ;;
ls) printf 'list' ;;
*) die "Invalid issue action: $1" ;;
esac
}
normalize_mr_action() {
case "$1" in
create|edit|comment|view|list) printf '%s' "$1" ;;
show|get) printf 'view' ;;
ls) printf 'list' ;;
*) die "Invalid merge request action: $1" ;;
esac
}
read_file_contents() {
local path="$1"
[[ -f "$path" ]] || die "File not found: $path"
[[ -r "$path" ]] || die "File is not readable: $path"
cat "$path"
}
authenticate_agent() {
local agent="$1"
[[ -n "$agent" ]] || die "No agent specified; set CURRENT_AGENT or pass --agent"
log_tool_call "glab-usr $(printf '%q' "$agent")"
glab-usr "$agent"
}
issue_create() {
local title=""
local body_file=""
local extra=()
while [[ $# -gt 0 ]]; do
case "$1" in
-t|--title)
[[ $# -ge 2 ]] || die "Missing value for $1"
title="$2"
shift 2
;;
-f|--file)
[[ $# -ge 2 ]] || die "Missing value for $1"
body_file="$2"
shift 2
;;
-h|--help)
print_issue_usage
exit 0
;;
--)
shift
extra+=("$@")
break
;;
-*)
die "Unknown option for issue create: $1"
;;
*)
die "Unexpected argument: $1"
;;
esac
done
[[ -n "$title" ]] || die "--title is required for issue create"
[[ -n "$body_file" ]] || die "--file is required for issue create"
local body_contents
body_contents="$(read_file_contents "$body_file")"
local -a cmd=(glab issue create --title "$title" --description "$body_contents")
[[ ${#REPO_FLAG[@]} -gt 0 ]] && cmd+=("${REPO_FLAG[@]}")
if [[ ${#extra[@]} -gt 0 ]]; then
cmd+=("${extra[@]}")
fi
local display="glab issue create --title $(printf '%q' "$title") --description @${body_file}"
if [[ ${#extra[@]} -gt 0 ]]; then
display+="$(format_args "${extra[@]}")"
fi
log_tool_call "$display"
"${cmd[@]}"
}
issue_edit() {
local issue_id=""
local title=""
local body_file=""
local extra=()
while [[ $# -gt 0 ]]; do
case "$1" in
-t|--title)
[[ $# -ge 2 ]] || die "Missing value for $1"
title="$2"
shift 2
;;
-f|--file)
[[ $# -ge 2 ]] || die "Missing value for $1"
body_file="$2"
shift 2
;;
-h|--help)
print_issue_usage
exit 0
;;
--)
shift
extra+=("$@")
break
;;
-*)
die "Unknown option for issue edit: $1"
;;
*)
if [[ -z "$issue_id" ]]; then
issue_id="$1"
shift
else
extra+=("$1")
shift
fi
;;
esac
done
[[ -n "$issue_id" ]] || die "Issue ID is required for issue edit"
[[ -n "$body_file" ]] || die "--file is required for issue edit"
local body_contents
body_contents="$(read_file_contents "$body_file")"
local -a cmd=(glab issue update "$issue_id" --description "$body_contents")
[[ ${#REPO_FLAG[@]} -gt 0 ]] && cmd+=("${REPO_FLAG[@]}")
local display="glab issue update $(printf '%q' "$issue_id") --description @${body_file}"
if [[ -n "$title" ]]; then
cmd+=(--title "$title")
display+=" --title $(printf '%q' "$title")"
fi
if [[ ${#extra[@]} -gt 0 ]]; then
cmd+=("${extra[@]}")
display+="$(format_args "${extra[@]}")"
fi
log_tool_call "$display"
"${cmd[@]}"
}
issue_comment() {
local issue_id=""
local body_file=""
local extra=()
while [[ $# -gt 0 ]]; do
case "$1" in
-f|--file)
[[ $# -ge 2 ]] || die "Missing value for $1"
body_file="$2"
shift 2
;;
-h|--help)
print_issue_usage
exit 0
;;
--)
shift
extra+=("$@")
break
;;
-*)
die "Unknown option for issue comment: $1"
;;
*)
if [[ -z "$issue_id" ]]; then
issue_id="$1"
shift
else
extra+=("$1")
shift
fi
;;
esac
done
[[ -n "$issue_id" ]] || die "Issue ID is required for issue comment"
[[ -n "$body_file" ]] || die "--file is required for issue comment"
local body_contents
body_contents="$(read_file_contents "$body_file")"
local -a cmd=(glab issue note "$issue_id" --message "$body_contents")
[[ ${#REPO_FLAG[@]} -gt 0 ]] && cmd+=("${REPO_FLAG[@]}")
local display="glab issue note $(printf '%q' "$issue_id") --message @${body_file}"
if [[ ${#extra[@]} -gt 0 ]]; then
cmd+=("${extra[@]}")
display+="$(format_args "${extra[@]}")"
fi
log_tool_call "$display"
"${cmd[@]}"
}
issue_view() {
local issue_id=""
local extra=()
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help)
print_issue_usage
exit 0
;;
--)
shift
extra+=("$@")
break
;;
-*)
extra+=("$1")
shift
;;
*)
if [[ -z "$issue_id" ]]; then
issue_id="$1"
shift
else
extra+=("$1")
shift
fi
;;
esac
done
[[ -n "$issue_id" ]] || die "Issue ID is required for issue view"
local -a cmd=(glab issue view "$issue_id" --comments --per-page 100)
[[ ${#REPO_FLAG[@]} -gt 0 ]] && cmd+=("${REPO_FLAG[@]}")
if [[ ${#extra[@]} -gt 0 ]]; then
cmd+=("${extra[@]}")
fi
local display="glab issue view $(printf '%q' "$issue_id") --comments --per-page 100"
if [[ ${#extra[@]} -gt 0 ]]; then
display+="$(format_args "${extra[@]}")"
fi
log_tool_call "$display"
"${cmd[@]}"
}
issue_list() {
# Thin pass-through to `glab issue list`. All args (including flags such
# as --state, --label, --author, --assignee) are forwarded verbatim;
# --repo injection is preserved in read-only mounts.
local -a forwarded=()
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help)
print_issue_usage
exit 0
;;
--)
shift
forwarded+=("$@")
break
;;
*)
forwarded+=("$1")
shift
;;
esac
done
local -a cmd=(glab issue list)
[[ ${#REPO_FLAG[@]} -gt 0 ]] && cmd+=("${REPO_FLAG[@]}")
if [[ ${#forwarded[@]} -gt 0 ]]; then
cmd+=("${forwarded[@]}")
fi
local display="glab issue list"
if [[ ${#forwarded[@]} -gt 0 ]]; then
display+="$(format_args "${forwarded[@]}")"
fi
log_tool_call "$display"
"${cmd[@]}"
}
mr_create() {
local title=""
local body_file=""
local extra=()
while [[ $# -gt 0 ]]; do
case "$1" in
-t|--title)
[[ $# -ge 2 ]] || die "Missing value for $1"
title="$2"
shift 2
;;
-f|--file)
[[ $# -ge 2 ]] || die "Missing value for $1"
body_file="$2"
shift 2
;;
-h|--help)
print_mr_usage
exit 0
;;
--)
shift
extra+=("$@")
break
;;
-*)
die "Unknown option for mr create: $1"
;;
*)
die "Unexpected argument: $1"
;;
esac
done
[[ -n "$title" ]] || die "--title is required for mr create"
[[ -n "$body_file" ]] || die "--file is required for mr create"
local body_contents
body_contents="$(read_file_contents "$body_file")"
local -a cmd=(glab mr create --title "$title" --description "$body_contents")
[[ ${#REPO_FLAG[@]} -gt 0 ]] && cmd+=("${REPO_FLAG[@]}")
if [[ ${#extra[@]} -gt 0 ]]; then
cmd+=("${extra[@]}")
fi
local display="glab mr create --title $(printf '%q' "$title") --description @${body_file}"
if [[ ${#extra[@]} -gt 0 ]]; then
display+="$(format_args "${extra[@]}")"
fi
log_tool_call "$display"
"${cmd[@]}"
}
mr_edit() {
local mr_ref=""
local title=""
local body_file=""
local extra=()
while [[ $# -gt 0 ]]; do
case "$1" in
-t|--title)
[[ $# -ge 2 ]] || die "Missing value for $1"
title="$2"
shift 2
;;
-f|--file)
[[ $# -ge 2 ]] || die "Missing value for $1"
body_file="$2"
shift 2
;;
-h|--help)
print_mr_usage
exit 0
;;
--)
shift
extra+=("$@")
break
;;
-*)
die "Unknown option for mr edit: $1"
;;
*)
if [[ -z "$mr_ref" ]]; then
mr_ref="$1"
shift
else
extra+=("$1")
shift
fi
;;
esac
done
[[ -n "$mr_ref" ]] || die "Merge request ID is required for mr edit"
[[ -n "$body_file" ]] || die "--file is required for mr edit"
local body_contents
body_contents="$(read_file_contents "$body_file")"
local -a cmd=(glab mr update "$mr_ref" --description "$body_contents")
[[ ${#REPO_FLAG[@]} -gt 0 ]] && cmd+=("${REPO_FLAG[@]}")
local display="glab mr update $(printf '%q' "$mr_ref") --description @${body_file}"
if [[ -n "$title" ]]; then
cmd+=(--title "$title")
display+=" --title $(printf '%q' "$title")"
fi
if [[ ${#extra[@]} -gt 0 ]]; then
cmd+=("${extra[@]}")
display+="$(format_args "${extra[@]}")"
fi
log_tool_call "$display"
"${cmd[@]}"
}
mr_comment() {
local mr_ref=""
local body_file=""
local extra=()
while [[ $# -gt 0 ]]; do
case "$1" in
-f|--file)
[[ $# -ge 2 ]] || die "Missing value for $1"
body_file="$2"
shift 2
;;
-h|--help)
print_mr_usage
exit 0
;;
--)
shift
extra+=("$@")
break
;;
-*)
die "Unknown option for mr comment: $1"
;;
*)
if [[ -z "$mr_ref" ]]; then
mr_ref="$1"
shift
else
extra+=("$1")
shift
fi
;;
esac
done
[[ -n "$mr_ref" ]] || die "Merge request ID is required for mr comment"
[[ -n "$body_file" ]] || die "--file is required for mr comment"
local body_contents
body_contents="$(read_file_contents "$body_file")"
local -a cmd=(glab mr note "$mr_ref" --message "$body_contents")
[[ ${#REPO_FLAG[@]} -gt 0 ]] && cmd+=("${REPO_FLAG[@]}")
local display="glab mr note $(printf '%q' "$mr_ref") --message @${body_file}"
if [[ ${#extra[@]} -gt 0 ]]; then
cmd+=("${extra[@]}")
display+="$(format_args "${extra[@]}")"
fi
log_tool_call "$display"
"${cmd[@]}"
}
mr_view() {
local mr_ref=""
local extra=()
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help)
print_mr_usage
exit 0
;;
--)
shift
extra+=("$@")
break
;;
-*)
extra+=("$1")
shift
;;
*)
if [[ -z "$mr_ref" ]]; then
mr_ref="$1"
shift
else
extra+=("$1")
shift
fi
;;
esac
done
[[ -n "$mr_ref" ]] || die "Merge request ID is required for mr view"
local -a cmd=(glab mr view "$mr_ref" --comments --per-page 100)
[[ ${#REPO_FLAG[@]} -gt 0 ]] && cmd+=("${REPO_FLAG[@]}")
if [[ ${#extra[@]} -gt 0 ]]; then
cmd+=("${extra[@]}")
fi
local display="glab mr view $(printf '%q' "$mr_ref") --comments --per-page 100"
if [[ ${#extra[@]} -gt 0 ]]; then
display+="$(format_args "${extra[@]}")"
fi
log_tool_call "$display"
"${cmd[@]}"
}
mr_list() {
# Thin pass-through to `glab mr list`. All args (including flags such
# as --source-branch, --state, --author) are forwarded verbatim; --repo
# injection is preserved in read-only mounts. Used by the "check for
# existing MR before opening" step in the GitLab workflow.
local -a forwarded=()
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help)
print_mr_usage
exit 0
;;
--)
shift
forwarded+=("$@")
break
;;
*)
forwarded+=("$1")
shift
;;
esac
done
local -a cmd=(glab mr list)
[[ ${#REPO_FLAG[@]} -gt 0 ]] && cmd+=("${REPO_FLAG[@]}")
if [[ ${#forwarded[@]} -gt 0 ]]; then
cmd+=("${forwarded[@]}")
fi
local display="glab mr list"
if [[ ${#forwarded[@]} -gt 0 ]]; then
display+="$(format_args "${forwarded[@]}")"
fi
log_tool_call "$display"
"${cmd[@]}"
}
handle_issue() {
local action=""
local -a rest=()
if [[ $# -eq 0 ]]; then
print_issue_usage >&2
exit 1
fi
while [[ $# -gt 0 ]]; do
case "$1" in
create|edit|comment|view|get|show|list|ls)
if [[ -z "$action" ]]; then
action="$(normalize_issue_action "$1")"
else
rest+=("$1")
fi
shift
;;
-m|--mode)
[[ $# -ge 2 ]] || die "Missing value for $1"
action="$(normalize_issue_action "$2")"
shift 2
;;
-h|--help)
print_issue_usage
exit 0
;;
*)
rest+=("$1")
shift
;;
esac
done
[[ -n "$action" ]] || { print_issue_usage >&2; exit 1; }
case "$action" in
create) issue_create "${rest[@]}" ;;
edit) issue_edit "${rest[@]}" ;;
comment) issue_comment "${rest[@]}" ;;
view) issue_view "${rest[@]}" ;;
list) issue_list "${rest[@]}" ;;
*) die "Unsupported issue action: $action" ;;
esac
}
handle_mr() {
local action=""
local -a rest=()
if [[ $# -eq 0 ]]; then
print_mr_usage >&2
exit 1
fi
while [[ $# -gt 0 ]]; do
case "$1" in
create|edit|comment|view|get|show|list|ls)
if [[ -z "$action" ]]; then
action="$(normalize_mr_action "$1")"
else
rest+=("$1")
fi
shift
;;
-m|--mode)
[[ $# -ge 2 ]] || die "Missing value for $1"
action="$(normalize_mr_action "$2")"
shift 2
;;
-h|--help)
print_mr_usage
exit 0
;;
*)
rest+=("$1")
shift
;;
esac
done
[[ -n "$action" ]] || { print_mr_usage >&2; exit 1; }
case "$action" in
create) mr_create "${rest[@]}" ;;
edit) mr_edit "${rest[@]}" ;;
comment) mr_comment "${rest[@]}" ;;
view) mr_view "${rest[@]}" ;;
list) mr_list "${rest[@]}" ;;
*) die "Unsupported merge request action: $action" ;;
esac
}
handle_auth() {
local agent_override="${AGENT_OVERRIDE:-}"
while [[ $# -gt 0 ]]; do
case "$1" in
-a|--agent)
[[ $# -ge 2 ]] || die "Missing value for $1"
agent_override="$2"
shift 2
;;
-h|--help)
cat <<'USAGE'
Usage: gitlab-connect auth [--agent NAME]
Authenticate the GitLab CLI for the requested agent account.
If no agent is provided, the command falls back to --agent, CURRENT_AGENT, or ROBOT_AGENT_NAME.
USAGE
exit 0
;;
--)
shift
break
;;
*)
if [[ -z "$agent_override" ]]; then
agent_override="$1"
shift
else
die "Unexpected argument: $1"
fi
;;
esac
done
if [[ -z "$agent_override" ]]; then
agent_override="$(determine_agent)"
fi
authenticate_agent "$agent_override"
}
determine_agent() {
if [[ -n "${AGENT_OVERRIDE:-}" ]]; then
printf '%s' "$AGENT_OVERRIDE"
return
fi
if [[ -n "${CURRENT_AGENT:-}" ]]; then
printf '%s' "$CURRENT_AGENT"
return
fi
if [[ -n "${ROBOT_AGENT_NAME:-}" ]]; then
printf '%s' "$ROBOT_AGENT_NAME"
return
fi
printf ''
}
# --- main ---
AGENT_OVERRIDE=""
AUTO_AUTH=1
while [[ $# -gt 0 ]]; do
case "$1" in
-a|--agent)
[[ $# -ge 2 ]] || die "Missing value for $1"
AGENT_OVERRIDE="$2"
shift 2
;;
--no-auth)
AUTO_AUTH=0
shift
;;
-h|--help)
print_main_usage
exit 0
;;
help)
print_main_usage
exit 0
;;
auth|issue|mr)
COMMAND="$1"
shift
break
;;
*)
COMMAND="$1"
shift
break
;;
esac
done
if [[ -z "${COMMAND:-}" ]]; then
print_main_usage >&2
exit 1
fi
case "$COMMAND" in
auth)
handle_auth "$@"
exit $?
;;
issue|mr)
;;
*)
die "Unknown command: $COMMAND"
;;
esac
# Detect read-only repo and resolve context for glab commands
resolve_repo_context
# In read-only repos, skip redundant re-authentication to avoid
# glab-usr attempting writes to .git/config (the router pre-authenticates)
if [[ "$READONLY_REPO" -eq 1 && "$AUTO_AUTH" -eq 1 && -z "${GITLAB_CONNECT_FORCE_AUTH:-}" ]]; then
echo "[gitlab-connect] INFO: skipping re-authentication in read-only repo (pre-auth assumed)" >&2
AUTO_AUTH=0
fi
if [[ "$AUTO_AUTH" -eq 1 ]]; then
AGENT_SELECTED="$(determine_agent)"
[[ -n "$AGENT_SELECTED" ]] || die "Unable to determine agent; set CURRENT_AGENT or use --agent"
authenticate_agent "$AGENT_SELECTED"
fi
case "$COMMAND" in
issue)
handle_issue "$@"
;;
mr)
handle_mr "$@"
;;
esac