Files
gitea-cli/docs/superpowers/specs/2026-04-21-gitea-cli-expansion-design.md
timmy faee15bc78 Initial commit
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 13:04:09 +08:00

260 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
選擇理由:腳本風格延續既有、每支單一職責、最不破壞現況;相對於單一子命令 CLIshell 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>` | 同上,用於 cloneclone 完自動把 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**:進度訊息
### 新腳本(方向 1repo 管理)
#### `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. 整套驗收(上節「測試策略」)