260 lines
10 KiB
Markdown
260 lines
10 KiB
Markdown
# 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 時必守)
|
||
|
||
1. **Token 絕不落地**:token 僅允許出現在 `config.env`、環境變數、`gitea_api` / `git_*_with_token` 函式內部;不得寫進 `.git/config`、remote URL、log、`stderr`、`stdout`
|
||
2. **origin URL 永遠乾淨**:`publish.sh` 和 `clone.sh` 結束後 origin 都是不含 token 的 URL
|
||
3. **`publish.sh` 外部行為**:`publish.sh [repo] [public|private]` 原有參數位置與預設值完全不變;`-m "msg"` 是**新增**選項,不影響舊呼叫方式
|
||
4. **`set -euo pipefail` + `trap` 清臨時檔**:每支腳本都要
|
||
5. **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}/topics` body `{"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 DESCRIPTION`
|
||
- `tsv`:`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**:
|
||
1. `POST /repos/{owner}/{repo}/releases` body 組合參數 → 取回 `id`
|
||
2. 對每個 `--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 URL
|
||
- `list.sh` → repo 清單
|
||
- `clone.sh` → 絕對路徑
|
||
- `release.sh` → release URL + 每個 asset URL
|
||
- 其他 → 空
|
||
|
||
**既有 `publish.sh` 變更**:原本進度訊息是 stdout,會改到 stderr。在終端機看起來一樣(兩者都會顯示),但若有人 pipe 其 stdout 會只看到最後的 URL。這是刻意設計,不視為 regression。
|
||
|
||
## 測試策略
|
||
|
||
無自動化測試。驗收方式:
|
||
|
||
1. 既有 `publish.sh` 使用情境(三種 git 狀態、public/private、已存在 repo)全部手動跑過,確認行為不變
|
||
2. 每支新腳本在測試用的 Gitea repo 上跑 happy path 一次
|
||
3. 破壞性操作(`delete.sh` / `archive.sh`)特別驗證:
|
||
- 不帶 `-y` 時會提示
|
||
- `delete.sh` 輸入錯 repo 名會被拒
|
||
- 帶 `-y` 時直接執行
|
||
4. 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`
|
||
|
||
不變,仍是三個變數。
|
||
|
||
## 實作順序建議
|
||
|
||
1. `scripts/lib/common.sh`(所有後續腳本的底)
|
||
2. 重構 `publish.sh` 改用 lib,新增 `-m`;手動驗證外部行為不變
|
||
3. `list.sh` / `clone.sh`(方向 2:最常用)
|
||
4. `rename.sh` / `visibility.sh` / `meta.sh`(方向 1:非破壞)
|
||
5. `archive.sh` / `delete.sh`(方向 1:破壞性,需測確認機制)
|
||
6. `tag.sh` / `release.sh`(方向 3)
|
||
7. 更新 `SKILL.md` / `README.md` / `CLAUDE.md`
|
||
8. 整套驗收(上節「測試策略」)
|