{ 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_show() { # comment-show [--json] [repo] # Print the body (or full JSON with --json) of a single comment by its id. if [ $# -lt 1 ]; then echo "usage: hive-forge comment-show [--json] [repo]" >&2; exit 1; fi local _id="$1"; shift local _json=false _repo="$HIVE_FORGE_REPO" while [ $# -gt 0 ]; do case "$1" in --json) _json=true; shift ;; *) _repo="$1"; shift ;; esac done local _resp _resp=$(forge_get "$FORGE_API/repos/$_repo/issues/comments/$_id") if [ "$_json" = true ]; then printf '%s\n' "$_resp" | jq '{id,user:.user.login,created_at,updated_at,body,url:.html_url}' else printf '%s\n' "$_resp" | jq -r '.body' fi } 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_pr_create() { # pr-create --title --head <branch> [--base <base>] [--body <body>] [--draft] [repo] # Create a pull request. Prints the PR URL on success. local _title="" _head="" _base="main" _body="" _draft="false" _repo="$HIVE_FORGE_REPO" while [ $# -gt 0 ]; do case "$1" in --title) _title="$2"; shift 2 ;; --head) _head="$2"; shift 2 ;; --base) _base="$2"; shift 2 ;; --body) _body="$2"; shift 2 ;; --draft) _draft="true"; shift ;; *) _repo="$1"; shift ;; esac done if [ -z "$_title" ] || [ -z "$_head" ]; then echo "usage: hive-forge pr-create --title <title> --head <branch> [--base <base>] [--body <body>] [--draft] [repo]" >&2 exit 1 fi local _payload _payload=$(jq -n \ --arg title "$_title" \ --arg head "$_head" \ --arg base "$_base" \ --arg body "$_body" \ --argjson draft "$_draft" \ '{title:$title,head:$head,base:$base,body:$body,draft:$draft}') forge_post "$FORGE_API/repos/$_repo/pulls" "$_payload" \ | jq -r '.html_url' } cmd_issue_create() { # issue-create --title <title> [--body <body>] [--assignee <user>] [repo] # Create an issue. Prints the issue URL on success. local _title="" _body="" _assignee="" _repo="$HIVE_FORGE_REPO" while [ $# -gt 0 ]; do case "$1" in --title) _title="$2"; shift 2 ;; --body) _body="$2"; shift 2 ;; --assignee) _assignee="$2"; shift 2 ;; *) _repo="$1"; shift ;; esac done if [ -z "$_title" ]; then echo "usage: hive-forge issue-create --title <title> [--body <body>] [--assignee <user>] [repo]" >&2 exit 1 fi local _payload if [ -n "$_assignee" ]; then _payload=$(jq -n --arg t "$_title" --arg b "$_body" --arg a "$_assignee" \ '{title:$t,body:$b,assignees:[$a]}') else _payload=$(jq -n --arg t "$_title" --arg b "$_body" '{title:$t,body:$b}') fi forge_post "$FORGE_API/repos/$_repo/issues" "$_payload" \ | jq -r '.html_url' } cmd_issue_edit() { # issue-edit <number> [--title <title>] [--body <body>] [--state open|closed] [repo] # Edit an issue's title, body, or state. Only provided fields are changed. if [ $# -lt 1 ]; then echo "usage: hive-forge issue-edit <number> [--title <title>] [--body <body>] [--state open|closed] [repo]" >&2; exit 1; fi local _n="$1"; shift local _title="" _body="" _state="" _repo="$HIVE_FORGE_REPO" while [ $# -gt 0 ]; do case "$1" in --title) _title="$2"; shift 2 ;; --body) _body="$2"; shift 2 ;; --state) _state="$2"; shift 2 ;; *) _repo="$1"; shift ;; esac done local _payload _payload=$(jq -n \ --arg title "$_title" \ --arg body "$_body" \ --arg state "$_state" \ '{} | if $title != "" then . + {title:$title} else . end | if $body != "" then . + {body:$body} else . end | if $state != "" then . + {state:$state} else . end') forge_patch "$FORGE_API/repos/$_repo/issues/$_n" "$_payload" \ | jq '{number,title,state}' } cmd_attach_issue() { # attach-issue <number> <file> [repo] # Upload a file as an attachment to an issue. Prints the download URL. if [ $# -lt 2 ]; then echo "usage: hive-forge attach-issue <number> <file> [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 <comment-id> <file> [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 <comment-id> <file> [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' } cmd_diff() { # diff <pr> [repo] # Print the unified diff for a PR. if [ $# -lt 1 ]; then echo "usage: hive-forge diff <pr> [repo]" >&2; exit 1; fi local _n="$1" _repo="''${2:-$HIVE_FORGE_REPO}" ${pkgs.curl}/bin/curl -sf \ -H "Authorization: token $_token" \ -H "Accept: text/plain" \ "$FORGE_API/repos/$_repo/pulls/$_n.diff" } cmd_subscription() { # subscription [--watch|--ignore|--unwatch] [repo] # Get or set the current user's watch subscription for a repo. # No flag: print current subscription status as JSON. # --watch: subscribe; --ignore: ignore; --unwatch: unsubscribe. local _action="" _repo="$HIVE_FORGE_REPO" while [ $# -gt 0 ]; do case "$1" in --watch) _action="watch"; shift ;; --ignore) _action="ignore"; shift ;; --unwatch) _action="unwatch"; shift ;; *) _repo="$1"; shift ;; esac done if [ -z "$_action" ]; then forge_get "$FORGE_API/repos/$_repo/subscription" \ | jq '{subscribed,ignored}' elif [ "$_action" = "unwatch" ]; then forge_delete "$FORGE_API/repos/$_repo/subscription" > /dev/null echo "unsubscribed" else local _subscribed="true" _ignored="false" if [ "$_action" = "ignore" ]; then _subscribed="false"; _ignored="true"; fi forge_post "$FORGE_API/repos/$_repo/subscription" \ "{\"subscribed\":$_subscribed,\"ignored\":$_ignored}" \ | jq '{subscribed,ignored}' fi } cmd_milestone() { # milestone [list|create|close] [args...] [repo] # Manage milestones. Default action: list. # list [repo] -- list open milestones as JSON # create --title <title> [--desc <desc>] [--due YYYY-MM-DD] [repo] -- create milestone, print id+title # close <id> [repo] -- close a milestone local _action="''${1:-list}" shift || true local _repo="$HIVE_FORGE_REPO" case "$_action" in list) if [ $# -gt 0 ] && [[ "$1" != --* ]]; then _repo="$1"; shift; fi forge_get "$FORGE_API/repos/$_repo/milestones?state=open&limit=50" \ | jq '[.[] | {id,title,open_issues,closed_issues,due_on,description}]' ;; create) local _title="" _desc="" _due="" while [ $# -gt 0 ]; do case "$1" in --title) _title="$2"; shift 2 ;; --desc) _desc="$2"; shift 2 ;; --due) _due="$2"; shift 2 ;; *) _repo="$1"; shift ;; esac done if [ -z "$_title" ]; then echo "usage: hive-forge milestone create --title <title> [--desc <desc>] [--due YYYY-MM-DD] [repo]" >&2; exit 1 fi local _payload _payload=$(jq -n --arg t "$_title" --arg d "$_desc" --arg due "$_due" \ '{title:$t} | if $d != "" then . + {description:$d} else . end | if $due != "" then . + {due_on:($due+"T00:00:00Z")} else . end') forge_post "$FORGE_API/repos/$_repo/milestones" "$_payload" \ | jq '{id,title}' ;; close) if [ $# -lt 1 ]; then echo "usage: hive-forge milestone close <id> [repo]" >&2; exit 1; fi local _id="$1"; shift if [ $# -gt 0 ]; then _repo="$1"; fi forge_patch "$FORGE_API/repos/$_repo/milestones/$_id" '{"state":"closed"}' \ | jq '{id,title,state}' ;; *) echo "unknown milestone action: $_action (use list, create, close)" >&2; exit 1 ;; esac } VERB="''${1:-}" if [ -z "$VERB" ]; then echo "usage: hive-forge <verb> [args...]" >&2 echo "verbs: view, issue, issue-create, issue-edit, pr, pr-create, comment, comment-show," >&2 echo " assign, close, labels, milestone, pr-reviews, branches, tree-sha, diff," >&2 echo " subscription, attach-issue, attach-comment" >&2 exit 1 fi shift case "$VERB" in view) cmd_view "$@" ;; issue) cmd_issue "$@" ;; issue-create) cmd_issue_create "$@" ;; issue-edit) cmd_issue_edit "$@" ;; pr) cmd_pr "$@" ;; pr-create) cmd_pr_create "$@" ;; comment) cmd_comment "$@" ;; comment-show) cmd_comment_show "$@" ;; assign) cmd_assign "$@" ;; close) cmd_close "$@" ;; labels) cmd_labels "$@" ;; milestone) cmd_milestone "$@" ;; pr-reviews) cmd_pr_reviews "$@" ;; branches) cmd_branches "$@" ;; tree-sha) cmd_tree_sha "$@" ;; diff) cmd_diff "$@" ;; subscription) cmd_subscription "$@" ;; attach-issue) cmd_attach_issue "$@" ;; attach-comment) cmd_attach_comment "$@" ;; *) echo "hive-forge: unknown verb '$VERB'" >&2 echo "verbs: view, issue, issue-create, issue-edit, pr, pr-create, comment, comment-show," >&2 echo " assign, close, labels, milestone, pr-reviews, branches, tree-sha, diff," >&2 echo " subscription, attach-issue, attach-comment" >&2 exit 1 ;; esac ''; }