From a60686c7969401ccfef794acb142b5a31672d399 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Fri, 21 Aug 2026 13:08:43 +0800 Subject: [PATCH] =?UTF-8?q?feat(gitea):=20=E5=8C=AF=E5=85=A5=20jsc-gitea?= =?UTF-8?q?=20=E6=8A=80=E8=83=BD=E7=B5=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- skills/repo-sync/SKILL.md | 17 ++++++ skills/wiki/SKILL.md | 29 +++++++++ tools/gitea.sh | 125 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 171 insertions(+) create mode 100644 skills/repo-sync/SKILL.md create mode 100644 skills/wiki/SKILL.md create mode 100755 tools/gitea.sh diff --git a/skills/repo-sync/SKILL.md b/skills/repo-sync/SKILL.md new file mode 100644 index 0000000..b17a9dd --- /dev/null +++ b/skills/repo-sync/SKILL.md @@ -0,0 +1,17 @@ +--- +name: repo-sync +description: Batch-sync all readable repos of a chosen Gitea owner into the working directory. List owners, let the user pick, then clone or update each repo; local changes become a branch, commit, push, and PR to develop or master. Use for workspace bootstrap or bulk refresh; not for a single repo. +--- + +# repo-sync — 存取庫批次同步 + +## 步驟 + +1. `tools/gitea.sh owners` 列出使用者擁有讀取權限的所有 `{owner}`。 +2. 依 `jsc-ask:ask` 規則讓使用者選擇要同步的 `{owner}`(選項需標明各 owner 的 repo 數量與影響範圍)。 +3. `tools/gitea.sh repos {owner}` 列出該 owner 底下可讀取的所有 `{repo}`。 +4. 逐一同步每個 `{repo}`,此步驟**必須以 sub agent 執行**(每個 repo 一個 sub agent): + 1. 不存在 → `git clone` 到工作目錄(clone URL 取自 `tools/gitea.sh clone-url`)。 + 2. 已存在 → 切換到 `develop`,沒有就切 `master`(或 `tools/gitea.sh default-branch` 的結果),再 `git pull`。 + 3. 有檔案變更 → 從 develop/master 新建分支,依 `jsc-git:commit` 認可、push,再依 `jsc-git:pr` 建立 PR 回 develop/master。 +5. 回報每個 repo 的同步結果(clone / 更新 / 建立 PR / 失敗原因)。 diff --git a/skills/wiki/SKILL.md b/skills/wiki/SKILL.md new file mode 100644 index 0000000..3124810 --- /dev/null +++ b/skills/wiki/SKILL.md @@ -0,0 +1,29 @@ +--- +name: wiki +description: Read or write a Gitea wiki page through tools/gitea.sh with GITEA_TOKEN. Resolve the wiki repo per page type (QUESTION/PLAN/ANALYZE/MAINTAIN/REPO/LOG) via gitea.sh wiki-repo, falling back to JSC_WIKI_REPO or a decision-tree question. Used by jsc-ask, jsc-sdlc, and jsc-log for all wiki pages; not for repo code files. +--- + +# wiki — Gitea Wiki 頁讀寫 + +所有 jsc 技能的 wiki 操作都必須經由本技能,統一入口與權限。 + +## 決定 wiki 位置 + +不同類型的 wiki 頁可以放在不同的 `{owner}/{repo}`,以頁名前綴分類:`QUESTION`、`PLAN`、`ANALYZE`、`MAINTAIN`、`REPO`、`LOG`。 + +1. 執行 `tools/gitea.sh wiki-repo {TYPE}`(TYPE = 頁名前綴)。解析順序:`JSC_WIKI_REPO_{TYPE}` > `JSC_WIKI_REPO`;已設定就直接使用,不再詢問。 +2. exit 3(都未設定)時依 `jsc-ask:ask` 規則詢問使用者該類型頁面所在的 `{owner}/{repo}`,並建議使用者設定 `JSC_WIKI_REPO_{TYPE}`(各類型可不同)或 `JSC_WIKI_REPO`(共用預設)。 + +## 操作 + +| 動作 | 指令 | +| --- | --- | +| 列頁 | `tools/gitea.sh wiki-list {owner}/{repo}` | +| 讀頁 | `tools/gitea.sh wiki-get {owner}/{repo} {page}`(不存在 exit 4) | +| 寫頁 | 內容先寫入暫存檔,再 `tools/gitea.sh wiki-put {owner}/{repo} {page} {file}`(自動判斷新建或更新) | + +## 規則 + +1. 頁面命名必須符合技能準則的「Wiki 頁命名總表」(見 `jsc-meta/references/guidelines.md`)。 +2. 更新目錄頁(`*_CONTENTS`)時先 `wiki-get` 取回、套用範本附加或修改後整頁 `wiki-put`,不可覆蓋他人條目。 +3. 內容一律 UTF-8 繁體中文。 diff --git a/tools/gitea.sh b/tools/gitea.sh new file mode 100755 index 0000000..c9c8767 --- /dev/null +++ b/tools/gitea.sh @@ -0,0 +1,125 @@ +#!/usr/bin/env sh +# gitea.sh — Gitea API 工具(curl + GITEA_TOKEN)。 +# 用法: +# gitea.sh owners # 列出可讀取的 owner(自己 + 組織) +# gitea.sh repos # 列出 owner 底下可讀取的 repo(全名) +# gitea.sh default-branch / # 印出預設分支 +# gitea.sh clone-url / # 印出 clone URL +# gitea.sh wiki-repo # 解析頁面類型的 wiki 位置: +# TYPE = QUESTION|PLAN|ANALYZE|MAINTAIN|REPO|LOG(即頁名前綴) +# 依序取 JSC_WIKI_REPO_{TYPE} > JSC_WIKI_REPO;都未設定 exit 3(呼叫端須詢問使用者) +# gitea.sh wiki-list / # 列出 wiki 頁名 +# gitea.sh wiki-get / # 印出 wiki 頁 markdown;不存在時 exit 4 +# gitea.sh wiki-put / # 建立或更新 wiki 頁(內容取自檔案) +# gitea.sh pr-create / <body-file> # 建立 PR,印出 PR URL +# gitea.sh api <METHOD> <path> [json-file] # 原始 API 呼叫(path 以 /repos/... 起始) +# 環境變數: GITEA_HOST(例 https://gitea.jsc.idv.tw)、GITEA_TOKEN +set -eu + +: "${GITEA_HOST:?GITEA_HOST is required}" +: "${GITEA_TOKEN:?GITEA_TOKEN is required}" +case "$GITEA_HOST" in http://*|https://*) HOST="$GITEA_HOST" ;; *) HOST="https://$GITEA_HOST" ;; esac +API="${HOST%/}/api/v1" + +req() { # METHOD path [json-file] -> body(HTTP >= 400 時 exit 4) + method="$1"; path="$2"; body_file="${3:-}" + if [ -n "$body_file" ]; then + out=$(curl -sS -w '\n%{http_code}' -X "$method" \ + -H "Authorization: token $GITEA_TOKEN" -H "Content-Type: application/json" \ + --data-binary "@$body_file" "$API$path") + else + out=$(curl -sS -w '\n%{http_code}' -X "$method" \ + -H "Authorization: token $GITEA_TOKEN" "$API$path") + fi + code=$(printf '%s' "$out" | tail -n1) + printf '%s\n' "$out" | sed '$d' + [ "$code" -lt 400 ] +} + +json_field() { # stdin JSON -> 每個物件的指定欄位一行(陣列或單一物件皆可) + python3 -c ' +import json,sys +d=json.load(sys.stdin); k=sys.argv[1] +items=d if isinstance(d,list) else [d] +for i in items: + v=i.get(k,"") + if v!="" and v is not None: print(v) +' "$1" +} + +cmd="${1:?usage: gitea.sh <command> ...}"; shift +case "$cmd" in + owners) + { req GET "/user" | json_field login + req GET "/user/orgs?limit=50" | json_field username; } | sort -u ;; + repos) + owner="${1:?owner required}" + # 分頁抓 owner 的 repo(org 與 user 端點擇一成功) + page=1 + while :; do + out=$(req GET "/orgs/$owner/repos?limit=50&page=$page" 2>/dev/null) \ + || out=$(req GET "/users/$owner/repos?limit=50&page=$page") + names=$(printf '%s' "$out" | json_field full_name) + [ -n "$names" ] || break + printf '%s\n' "$names" + page=$((page+1)) + done ;; + default-branch) + or="${1:?owner/repo required}" + req GET "/repos/$or" | json_field default_branch ;; + clone-url) + or="${1:?owner/repo required}" + req GET "/repos/$or" | json_field clone_url ;; + wiki-repo) + type=$(printf '%s' "${1:?TYPE required}" | tr a-z A-Z) + eval "v=\${JSC_WIKI_REPO_$type:-}" + [ -n "$v" ] || v="${JSC_WIKI_REPO:-}" + if [ -n "$v" ]; then printf '%s\n' "$v"; else + echo "no wiki repo configured for $type (set JSC_WIKI_REPO_$type or JSC_WIKI_REPO)" >&2; exit 3 + fi ;; + wiki-list) + or="${1:?owner/repo required}" + req GET "/repos/$or/wiki/pages?limit=200" | json_field title ;; + wiki-get) + or="${1:?owner/repo required}"; page="${2:?page required}" + if ! out=$(req GET "/repos/$or/wiki/page/$page" 2>/dev/null); then + echo "wiki page not found: $page" >&2; exit 4 + fi + printf '%s' "$out" | python3 -c ' +import json,sys,base64 +d=json.load(sys.stdin) +sys.stdout.write(base64.b64decode(d.get("content_base64","")).decode("utf-8")) +' ;; + wiki-put) + or="${1:?owner/repo required}"; page="${2:?page required}"; file="${3:?content file required}" + tmp=$(mktemp) + python3 -c ' +import json,sys,base64 +title,path=sys.argv[1],sys.argv[2] +content=open(path,"rb").read() +print(json.dumps({"title":title,"content_base64":base64.b64encode(content).decode()})) +' "$page" "$file" > "$tmp" + if req GET "/repos/$or/wiki/page/$page" >/dev/null 2>&1; then + req PATCH "/repos/$or/wiki/page/$page" "$tmp" >/dev/null + else + req POST "/repos/$or/wiki/new" "$tmp" >/dev/null + fi + rm -f "$tmp" + echo "OK $page" ;; + pr-create) + or="${1:?owner/repo required}"; head="${2:?head required}"; base="${3:?base required}" + title="${4:?title required}"; body_file="${5:?body file required}" + tmp=$(mktemp) + python3 -c ' +import json,sys +print(json.dumps({"head":sys.argv[1],"base":sys.argv[2],"title":sys.argv[3], + "body":open(sys.argv[4],encoding="utf-8").read()})) +' "$head" "$base" "$title" "$body_file" > "$tmp" + req POST "/repos/$or/pulls" "$tmp" | json_field html_url + rm -f "$tmp" ;; + api) + method="${1:?METHOD}"; path="${2:?path}"; body="${3:-}" + req "$method" "$path" $body ;; + *) + echo "unknown command: $cmd" >&2; exit 2 ;; +esac