10 KiB
gitea-cli 擴充設計
日期:2026-04-21 狀態:待實作
背景
gitea-cli 目前只有一支 scripts/publish.sh,負責在自架 Gitea 建立 repo 並把目前工作目錄推上去。本 spec 定義將它擴充為完整的 Gitea 操作工具集。
目標
- 新增三大類功能:repo 管理、取用既有 repo、推送進階(release / tag / 自訂 commit 訊息)
- 抽出共用 lib(
scripts/lib/common.sh),新舊腳本都使用 - 完全不破壞 現有
publish.sh的外部行為:既有呼叫方式仍然有效 - 維持既有安全不變量:token 絕不寫進
.git/config/ remote URL / log
非目標(刻意 YAGNI)
以下情境不在本次範圍,留待真正需要時再加:
- 批次 clone 所有 repo(
clone.sh --all) - Fork / mirror / webhook / collaborator 管理
- 多站台切換 config(
GITEA_PROFILE等) - 互動式 UI / 問答精靈(Claude 會代問,腳本維持純 CLI)
- GitHub / GitLab 相容
架構
組織方式:多支獨立腳本 + 共用 lib
選擇理由:腳本風格延續既有、每支單一職責、最不破壞現況;相對於單一子命令 CLI,shell subcommand 分派需要自己寫會讓單檔肥大。
檔案結構
gitea-cli/
├── SKILL.md
├── README.md
├── CLAUDE.md
├── config.example.env
├── config.env (gitignored)
├── .gitignore
├── docs/superpowers/specs/ (本 spec 所在)
└── scripts/
├── lib/
│ └── common.sh (所有腳本 source 此檔)
├── publish.sh (重構後 source lib;外部行為不變 + 新增 -m)
├── list.sh
├── clone.sh
├── rename.sh
├── visibility.sh
├── archive.sh
├── delete.sh
├── meta.sh
├── release.sh
└── tag.sh
scripts/lib/common.sh 介面
必須放進來(跨腳本重複且短小):
| Function | 職責 |
|---|---|
load_config |
找 config.env → source → 檢查 GITEA_URL / GITEA_USER / GITEA_TOKEN 非空且非預設值;失敗時統一錯誤訊息 |
gitea_api <METHOD> <path> [json-body] |
封裝 curl;回傳 HTTP 狀態碼、把 body 寫入 $GITEA_API_RESP_FILE;呼叫端自行判斷狀態碼。所有新腳本只透過這支呼叫 API,不自己寫 curl |
git_push_with_token <branch> |
封裝一次性 credential.helper 推送 |
git_clone_with_token <clean-url> <target-dir> |
同上,用於 clone;clone 完自動把 origin 重設為乾淨 URL |
infer_repo_from_cwd |
從 git remote get-url origin 推斷 repo 名;解析不到回非零 |
confirm_destructive <prompt> |
互動 y/N;若 --yes flag 或 GITEA_YES=1 則直接通過 |
confirm_exact_match <expected> |
要求使用者完整輸入指定字串才通過(給 delete.sh 用);同樣支援 --yes 略過 |
log_info / log_warn / log_err |
統一輸出前綴(▶ / ⚠ / ❌ / ✅ / ✓),全部寫到 stderr |
刻意不放(留在各腳本):
- 各腳本的 arg 解析(幾行而已,抽共用反而難讀)
- 各 API endpoint 的 JSON body 組字串(放在呼叫處比較直觀)
publish.sh的三種 git 狀態分支判斷(是 publish 獨有)
不變量(review 時必守)
- Token 絕不落地:token 僅允許出現在
config.env、環境變數、gitea_api/git_*_with_token函式內部;不得寫進.git/config、remote URL、log、stderr、stdout - origin URL 永遠乾淨:
publish.sh和clone.sh結束後 origin 都是不含 token 的 URL publish.sh外部行為:publish.sh [repo] [public|private]原有參數位置與預設值完全不變;-m "msg"是新增選項,不影響舊呼叫方式set -euo pipefail+trap清臨時檔:每支腳本都要- stdout 只有結構化結果;stderr 放進度訊息
腳本詳細設計
既有腳本(微調)
publish.sh
用法:publish.sh [repo] [public|private] [-m "msg"]
- 新增
-m "msg":若指定,取代預設 commit 訊息(原本是Initial commit/Update) - 重構:
load_config/gitea_api/git_push_with_token/log_*改用 lib - 三種 git 狀態分支保留在腳本內
- stdout:最後只印 repo web URL(方便
URL=$(publish.sh)) - stderr:進度訊息
新腳本(方向 1:repo 管理)
rename.sh
用法:rename.sh <old-name> <new-name>
PATCH /repos/{GITEA_USER}/{old}body{"name": "<new>"}- 若 cwd 的
git remote get-url origin指向舊 repo,順手git remote set-url origin <new-clean-url> - stdout:新 repo web URL
- 不需二次確認(重命名非破壞性)
visibility.sh
用法:visibility.sh <repo> <public|private>
PATCH /repos/{GITEA_USER}/{repo}body{"private": true|false}- 不需二次確認
archive.sh
用法:archive.sh <repo> [--unarchive] [--yes|-y]
PATCH /repos/{GITEA_USER}/{repo}body{"archived": true}(--unarchive則false)- 互動模式預設需 y/N 確認;
--yes/-y/GITEA_YES=1略過
delete.sh
用法:delete.sh <repo> [--yes|-y]
DELETE /repos/{GITEA_USER}/{repo}- 互動模式:用
confirm_exact_match <repo>要求使用者重新輸入完整 repo 名稱才動(最高保險) --yes/-y/GITEA_YES=1略過
meta.sh
用法:meta.sh <repo> [--desc "..."] [--topics "a,b,c"]
--desc:PATCH /repos/{owner}/{repo}body{"description": "..."}--topics:PUT /repos/{owner}/{repo}/topicsbody{"topics": ["a","b","c"]}(覆蓋式,非增量)- 兩者可同時給;都不給時印用法並非零退出
新腳本(方向 2:取用既有 repo)
list.sh
用法:list.sh [--private|--public] [--format table|tsv|name]
GET /user/repos(自動翻頁,每頁 50)--private/--public:過濾可見性,預設顯示全部--format:table(預設):對齊欄位NAME VISIBILITY DESCRIPTIONtsv:name\tvisibility\tdescription,給管線 / Claude 用name:每行一個 repo 名,給xargs用
- stdout:清單內容
- stderr:進度(抓到幾頁、共幾個)
clone.sh
用法:clone.sh <repo> [target-dir]
- 預設 clone 到
./<repo>,可指定 target - 目標目錄已存在且非空 → 失敗(不覆蓋)
- 用
git_clone_with_token做 clone,完成後把 origin 重設為乾淨 URL - stdout:clone 到的絕對路徑
新腳本(方向 3:推送進階)
release.sh
用法:release.sh <tag> [--name "..."] [--notes "..."] [--target <branch>] [--draft] [--prerelease] [--asset <path>]...
- 從 cwd 的 origin 推斷 repo;推不到強制要
--repo <name> - 兩段 API:
POST /repos/{owner}/{repo}/releasesbody 組合參數 → 取回id- 對每個
--asset做POST /repos/{owner}/{repo}/releases/{id}/assets?name=<basename>(multipart/form-data)
- Tag 處理:Gitea release API 的
tag_name若不存在會自動建(指到target_commitish,預設 HEAD)。不強制先tag.sh - 錯誤處理:
- 第 1 步失敗 → 直接結束
- 第 2 步某個 asset 失敗 → 繼續其他 asset,最後非零退出並列出失敗的
- stdout:第一行 release web URL,其餘每行一個 asset 下載 URL
tag.sh
用法:tag.sh <tag> [-m "msg"]
- 本地
git tag -a <tag> -m "<msg>"(預設 msg ="Tag <tag>") git_push_with_token推這個 tag(git push origin <tag>)- 定位:只打 tag 不發 release 的情境;與
release.sh獨立
Repo 名稱解析原則(跨腳本)
所有需要 repo 名的腳本,若參數省略,呼叫 infer_repo_from_cwd 嘗試從 git remote get-url origin 推斷。推斷不到就要求使用者帶參數。這樣在 repo 裡直接跑 rename.sh newname、release.sh v1.0.0 都能用。
錯誤處理與輸出格式
退出碼
0成功- 非零 失敗(不細分子類;錯誤訊息本身會講清楚原因)
stdout / stderr 分流
- stderr:所有
▶ / ✓ / ⚠ / ❌ / ✅進度訊息與錯誤 - stdout:僅最終結構化輸出
publish.sh/rename.sh→ repo web URLlist.sh→ repo 清單clone.sh→ 絕對路徑release.sh→ release URL + 每個 asset URL- 其他 → 空
既有 publish.sh 變更:原本進度訊息是 stdout,會改到 stderr。在終端機看起來一樣(兩者都會顯示),但若有人 pipe 其 stdout 會只看到最後的 URL。這是刻意設計,不視為 regression。
測試策略
無自動化測試。驗收方式:
- 既有
publish.sh使用情境(三種 git 狀態、public/private、已存在 repo)全部手動跑過,確認行為不變 - 每支新腳本在測試用的 Gitea repo 上跑 happy path 一次
- 破壞性操作(
delete.sh/archive.sh)特別驗證:- 不帶
-y時會提示 delete.sh輸入錯 repo 名會被拒- 帶
-y時直接執行
- 不帶
- Token 不落地驗收:跑完
publish.sh後grep -r "$GITEA_TOKEN" .git/應為空
文件同步
SKILL.md
description:擴寫以涵蓋所有擴充後的觸發情境(push / clone / list / rename / release / archive / delete / tag…)- 正文改以「常見情境 → 對應腳本」方式列全部腳本,保留
config.env一次性設定段落
README.md
- 新增「腳本總覽」表格:每支腳本一行用法與一句說明
- 每支新腳本 1-2 段詳細說明 + 範例
- 「安全設計」段落補上「clone 完 origin 也是乾淨 URL」
CLAUDE.md
- 補充「擴展為多腳本」的架構敘述
- 不變量改寫為所有 push / clone 腳本都必須守 token 不落地
- 新增「
scripts/lib/common.sh的用途與邊界」段落 - 新增「新腳本的檔案骨架」小範本(source lib → parse args → 呼叫 lib → 印結果),讓未來加新腳本的人照抄
config.example.env
不變,仍是三個變數。
實作順序建議
scripts/lib/common.sh(所有後續腳本的底)- 重構
publish.sh改用 lib,新增-m;手動驗證外部行為不變 list.sh/clone.sh(方向 2:最常用)rename.sh/visibility.sh/meta.sh(方向 1:非破壞)archive.sh/delete.sh(方向 1:破壞性,需測確認機制)tag.sh/release.sh(方向 3)- 更新
SKILL.md/README.md/CLAUDE.md - 整套驗收(上節「測試策略」)