feat(gitea): 匯入 jsc-gitea 技能組並統一 marketplace 為 jsc #2

Merged
admin merged 11 commits from develop into master 2026-08-21 06:40:25 +00:00
3 changed files with 171 additions and 0 deletions
Showing only changes of commit a60686c796 - Show all commits
+17
View File
@@ -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 / 失敗原因)。
+29
View File
@@ -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 繁體中文。
Executable
+125
View File
@@ -0,0 +1,125 @@
#!/usr/bin/env sh
# gitea.sh — Gitea API 工具(curl + GITEA_TOKEN)。
# 用法:
# gitea.sh owners # 列出可讀取的 owner(自己 + 組織)
# gitea.sh repos <owner> # 列出 owner 底下可讀取的 repo(全名)
# gitea.sh default-branch <owner>/<repo> # 印出預設分支
# gitea.sh clone-url <owner>/<repo> # 印出 clone URL
# gitea.sh wiki-repo <TYPE> # 解析頁面類型的 wiki 位置:
# TYPE = QUESTION|PLAN|ANALYZE|MAINTAIN|REPO|LOG(即頁名前綴)
# 依序取 JSC_WIKI_REPO_{TYPE} > JSC_WIKI_REPO;都未設定 exit 3(呼叫端須詢問使用者)
# gitea.sh wiki-list <owner>/<repo> # 列出 wiki 頁名
# gitea.sh wiki-get <owner>/<repo> <page> # 印出 wiki 頁 markdown;不存在時 exit 4
# gitea.sh wiki-put <owner>/<repo> <page> <file> # 建立或更新 wiki 頁(內容取自檔案)
# gitea.sh pr-create <owner>/<repo> <head> <base> <title> <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