{ pkgs, lib }: # Single `hive-forge ` CLI wrapping common Forgejo API operations. # Reads credentials from the environment: # # HIVE_FORGE_URL -- base URL, e.g. http://localhost:3000 # HIVE_FORGE_REPO -- default repo, e.g. hyperhive/hyperhive # HYPERHIVE_STATE_DIR -- state dir; forge-token lives here # # Usage: hive-forge [args...] # Verbs: view, issue, pr, comment, assign, close, labels, pr-reviews, # branches, tree-sha pkgs.writeShellApplication { name = "hive-forge"; runtimeInputs = [ pkgs.curl pkgs.jq ]; text = '' : "''${HIVE_FORGE_URL:=http://localhost:3000}" : "''${HIVE_FORGE_REPO:=hyperhive/hyperhive}" _state_dir="''${HYPERHIVE_STATE_DIR:-}" _token_file="''${_state_dir:+$_state_dir/}forge-token" if [ ! -f "$_token_file" ]; then echo "hive-forge: no forge-token at $_token_file" >&2 exit 1 fi _token=$(cat "$_token_file") FORGE_API="$HIVE_FORGE_URL/api/v1" forge_get() { ${pkgs.curl}/bin/curl -sf \ -H "Authorization: token $_token" \ -H "Accept: application/json" \ "$1" } forge_post() { ${pkgs.curl}/bin/curl -sf -X POST \ -H "Authorization: token $_token" \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ -d "$2" "$1" } forge_patch() { ${pkgs.curl}/bin/curl -sf -X PATCH \ -H "Authorization: token $_token" \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ -d "$2" "$1" } forge_delete() { local _url="$1" local _body="''${2:-}" if [ -n "$_body" ]; then ${pkgs.curl}/bin/curl -sf -X DELETE \ -H "Authorization: token $_token" \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ -d "$_body" "$_url" else ${pkgs.curl}/bin/curl -sf -X DELETE \ -H "Authorization: token $_token" \ -H "Accept: application/json" \ "$_url" fi } cmd_view() { # view [repo] # Dump title + body + all comments for an issue or PR. if [ $# -lt 1 ]; then echo "usage: hive-forge view [repo]" >&2; exit 1; fi local _n="$1" _repo="''${2:-$HIVE_FORGE_REPO}" local _issue _title _body _state _user _is_pr _kind _issue=$(forge_get "$FORGE_API/repos/$_repo/issues/$_n") _title=$(printf '%s' "$_issue" | jq -r '.title') _body=$(printf '%s' "$_issue" | jq -r '.body // ""') _state=$(printf '%s' "$_issue" | jq -r '.state') _user=$(printf '%s' "$_issue" | jq -r '.user.login') _is_pr=$(printf '%s' "$_issue" | jq -r '.pull_request != null') _kind="issue" if [ "$_is_pr" = "true" ]; then _kind="PR"; fi printf '# %s #%s [%s] by %s\n' "$_kind" "$_n" "$_state" "$_user" printf '%s\n' "$_title" if [ -n "$_body" ]; then printf '\n%s\n' "$_body"; fi local _comments _count _comments=$(forge_get "$FORGE_API/repos/$_repo/issues/$_n/comments?limit=50") _count=$(printf '%s' "$_comments" | jq 'length') if [ "$_count" -gt 0 ]; then printf '\n---\n## Comments (%s)\n\n' "$_count" printf '%s' "$_comments" | jq -r '.[] | "**\(.user.login)**: \(.body)\n"' fi } cmd_issue() { # issue [repo] # Print key fields of an issue as JSON. if [ $# -lt 1 ]; then echo "usage: hive-forge issue [repo]" >&2; exit 1; fi local _n="$1" _repo="''${2:-$HIVE_FORGE_REPO}" forge_get "$FORGE_API/repos/$_repo/issues/$_n" \ | jq '{number,title,state,user:.user.login,assignees:[.assignees[]?.login],labels:[.labels[]?.name],body}' } cmd_pr() { # pr [repo] # Print key fields of a PR as JSON. if [ $# -lt 1 ]; then echo "usage: hive-forge pr [repo]" >&2; exit 1; fi local _n="$1" _repo="''${2:-$HIVE_FORGE_REPO}" forge_get "$FORGE_API/repos/$_repo/pulls/$_n" \ | jq '{number,title,state,merged,user:.user.login,head_sha:.head.sha,head_branch:.head.label,base_branch:.base.label}' } cmd_comment() { # comment [body | -f | - for stdin] # Post a comment on an issue or PR. if [ $# -lt 1 ]; then echo "usage: hive-forge comment [body | -f | -]" >&2; exit 1; fi local _n="$1"; shift local _body if [ $# -eq 0 ] || [ "''${1:-}" = "-" ]; then _body=$(cat) elif [ "''${1:-}" = "-f" ]; then _body=$(cat "$2") else _body="$*" fi local _payload _payload=$(jq -n --arg body "$_body" '{body:$body}') forge_post "$FORGE_API/repos/$HIVE_FORGE_REPO/issues/$_n/comments" "$_payload" \ | jq '{id,url:.html_url}' } cmd_assign() { # assign [--remove] # Assign or unassign a user on an issue or PR. if [ $# -lt 2 ]; then echo "usage: hive-forge assign [--remove]" >&2; exit 1; fi local _n="$1" _user="$2" _remove="''${3:-}" local _payload _payload=$(jq -n --arg u "$_user" '{assignees:[$u]}') if [ "$_remove" = "--remove" ]; then forge_delete "$FORGE_API/repos/$HIVE_FORGE_REPO/issues/$_n/assignees" "$_payload" \ | jq '{number,assignees:[.assignees[]?.login]}' else forge_post "$FORGE_API/repos/$HIVE_FORGE_REPO/issues/$_n/assignees" "$_payload" \ | jq '{number,assignees:[.assignees[]?.login]}' fi } cmd_close() { # close # Close an issue or PR. if [ $# -lt 1 ]; then echo "usage: hive-forge close " >&2; exit 1; fi forge_patch "$FORGE_API/repos/$HIVE_FORGE_REPO/issues/$1" '{"state":"closed"}' \ | jq '{number,state}' } cmd_labels() { # labels [list|add|remove] [labels...] # Manage labels on an issue or PR. Default action is list. if [ $# -lt 1 ]; then echo "usage: hive-forge labels [list|add|remove] [labels...]" >&2; exit 1; fi local _n="$1" _action="''${2:-list}" shift; shift || true local _all _ids _id _label case "$_action" in list) forge_get "$FORGE_API/repos/$HIVE_FORGE_REPO/issues/$_n/labels" | jq '[.[].name]' ;; add) _all=$(forge_get "$FORGE_API/repos/$HIVE_FORGE_REPO/labels?limit=100") _ids=$(printf '%s' "$_all" | jq -r --args '[.[] | select(.name == ($ARGS.positional[])) | .id]' -- "$@") forge_post "$FORGE_API/repos/$HIVE_FORGE_REPO/issues/$_n/labels" "{\"labels\":$_ids}" \ | jq '[.[].name]' ;; remove) _all=$(forge_get "$FORGE_API/repos/$HIVE_FORGE_REPO/labels?limit=100") for _label in "$@"; do _id=$(printf '%s' "$_all" | jq -r --arg n "$_label" '.[] | select(.name==$n) | .id') if [ -n "$_id" ]; then forge_delete "$FORGE_API/repos/$HIVE_FORGE_REPO/issues/$_n/labels/$_id" fi done forge_get "$FORGE_API/repos/$HIVE_FORGE_REPO/issues/$_n/labels" | jq '[.[].name]' ;; *) echo "unknown action: $_action (use list, add, remove)" >&2; exit 1 ;; esac } cmd_pr_reviews() { # pr-reviews [repo] # List reviews on a PR with id/state/user/body. if [ $# -lt 1 ]; then echo "usage: hive-forge pr-reviews [repo]" >&2; exit 1; fi local _n="$1" _repo="''${2:-$HIVE_FORGE_REPO}" forge_get "$FORGE_API/repos/$_repo/pulls/$_n/reviews" \ | jq '[.[] | {id,state,user:.user.login,body,comments_count}]' } cmd_branches() { # branches [pattern] [repo] # List branches, optionally filtered by a grep pattern. local _pattern="''${1:-}" _repo="''${2:-$HIVE_FORGE_REPO}" local _result _result=$(forge_get "$FORGE_API/repos/$_repo/branches?limit=100" | jq -r '.[].name') if [ -n "$_pattern" ]; then printf '%s\n' "$_result" | grep "$_pattern" || true else printf '%s\n' "$_result" fi } cmd_tree_sha() { # tree-sha [repo] # Print the tree SHA of the commit at the given branch or commit SHA. if [ $# -lt 1 ]; then echo "usage: hive-forge tree-sha [repo]" >&2; exit 1; fi local _ref="$1" _repo="''${2:-$HIVE_FORGE_REPO}" local _commit_sha _commit_sha=$(forge_get "$FORGE_API/repos/$_repo/branches/$_ref" 2>/dev/null \ | jq -r '.commit.id // empty') || true if [ -z "$_commit_sha" ]; then _commit_sha="$_ref" fi forge_get "$FORGE_API/repos/$_repo/git/commits/$_commit_sha" \ | jq -r '.tree.sha // .sha' } cmd_attach_issue() { # attach-issue [repo] # Upload a file as an attachment to an issue. Prints the download URL. if [ $# -lt 2 ]; then echo "usage: hive-forge attach-issue [repo]" >&2; exit 1; fi local _n="$1" _file="$2" _repo="''${3:-$HIVE_FORGE_REPO}" if [ ! -f "$_file" ]; then echo "hive-forge attach-issue: file not found: $_file" >&2; exit 1; fi ${pkgs.curl}/bin/curl -sf -X POST \ -H "Authorization: token $_token" \ -F "attachment=@$_file" \ "$FORGE_API/repos/$_repo/issues/$_n/assets" \ | jq -r '.browser_download_url' } cmd_attach_comment() { # attach-comment [repo] # Upload a file as an attachment to an issue comment. Prints the download URL. if [ $# -lt 2 ]; then echo "usage: hive-forge attach-comment [repo]" >&2; exit 1; fi local _id="$1" _file="$2" _repo="''${3:-$HIVE_FORGE_REPO}" if [ ! -f "$_file" ]; then echo "hive-forge attach-comment: file not found: $_file" >&2; exit 1; fi ${pkgs.curl}/bin/curl -sf -X POST \ -H "Authorization: token $_token" \ -F "attachment=@$_file" \ "$FORGE_API/repos/$_repo/issues/comments/$_id/assets" \ | jq -r '.browser_download_url' } VERB="''${1:-}" if [ -z "$VERB" ]; then echo "usage: hive-forge [args...]" >&2 echo "verbs: view, issue, pr, comment, assign, close, labels, pr-reviews, branches, tree-sha, attach-issue, attach-comment" >&2 exit 1 fi shift case "$VERB" in view) cmd_view "$@" ;; issue) cmd_issue "$@" ;; pr) cmd_pr "$@" ;; comment) cmd_comment "$@" ;; assign) cmd_assign "$@" ;; close) cmd_close "$@" ;; labels) cmd_labels "$@" ;; pr-reviews) cmd_pr_reviews "$@" ;; branches) cmd_branches "$@" ;; tree-sha) cmd_tree_sha "$@" ;; attach-issue) cmd_attach_issue "$@" ;; attach-comment) cmd_attach_comment "$@" ;; *) echo "hive-forge: unknown verb '$VERB'" >&2 echo "verbs: view, issue, pr, comment, assign, close, labels, pr-reviews, branches, tree-sha, attach-issue, attach-comment" >&2 exit 1 ;; esac ''; }