Ctrl K
ComunidadGuías…

gitlab-connect

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/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