Initial commit: add all skills
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1
gitea-tea-skill/.gitattributes
vendored
Normal file
1
gitea-tea-skill/.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
* text=auto eol=lf
|
||||
4
gitea-tea-skill/.gitignore
vendored
Normal file
4
gitea-tea-skill/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
.venv/
|
||||
venv/
|
||||
232
gitea-tea-skill/SKILL.md
Normal file
232
gitea-tea-skill/SKILL.md
Normal file
@@ -0,0 +1,232 @@
|
||||
---
|
||||
name: gitea-tea-skill
|
||||
description: 使用 `tea` CLI 或直接调用 Gitea API 与 https://git.shumengya.top 交互。支持仓库、Issue、PR、Release 管理。默认脚本语言为 Python,辅助脚本零外部依赖。
|
||||
---
|
||||
|
||||
# 萌芽 Gitea 操作指南
|
||||
|
||||
`tea` 是 Gitea 官方 CLI,同时本技能提供纯 Python 辅助脚本(零依赖),两者互补使用。
|
||||
|
||||
---
|
||||
|
||||
## 萌芽 Gitea 服务器信息
|
||||
|
||||
| 项目 | 值 |
|
||||
|------|-----|
|
||||
| **地址** | https://git.shumengya.top |
|
||||
| **用户名** | `shumengya` |
|
||||
| **默认令牌** | `dbc5dfe55b19e9a32f685a97f126cafc2ef76449` |
|
||||
| **仓库基础路径** | `https://git.shumengya.top/shumengya` |
|
||||
| **SSH 地址** | `git@git.shumengya.top:shumengya/<仓库名>.git` |
|
||||
| **SSH 端口** | `8022` |
|
||||
| **API 基础路径** | `https://git.shumengya.top/api/v1` |
|
||||
|
||||
---
|
||||
|
||||
## 方式一:tea CLI
|
||||
|
||||
### 前置检查
|
||||
|
||||
1. **检查 tea 是否已安装**:`tea --version`
|
||||
- 不存在则安装最新版:
|
||||
- Linux:`wget -qO- https://gitea.com/gitea/tea/releases/latest/download/tea-linux-amd64.tar.gz | tar -xz -C /usr/local/bin/ tea`
|
||||
- macOS:`brew install tea`
|
||||
- 如果 tea 版本太旧(如 0.1.0-dev),建议升级:
|
||||
```bash
|
||||
# 备份旧 config
|
||||
cp ~/.tea/tea.yml ~/.tea/tea.yml.bak
|
||||
# 下载新版覆盖
|
||||
wget -qO- https://gitea.com/gitea/tea/releases/latest/download/tea-linux-amd64.tar.gz | tar -xz -C /usr/local/bin/ tea
|
||||
```
|
||||
|
||||
2. **登录**
|
||||
```bash
|
||||
tea login add --url https://git.shumengya.top --token dbc5dfe55b19e9a32f685a97f126cafc2ef76449 --name gitea-shumengya
|
||||
```
|
||||
配置文件路径:`~/.tea/tea.yml`(Linux/macOS)
|
||||
|
||||
3. **已知问题与解决方案**
|
||||
- 旧版 tea(0.1.0-dev)的 `-l` flag 可能无效
|
||||
- **如果 tea 命令异常,直接改用方式二(API)或方式三(Python 脚本)**
|
||||
|
||||
### 常用命令
|
||||
|
||||
```bash
|
||||
# 仓库
|
||||
tea repos ls # 列出仓库
|
||||
tea repos create --name <仓库名> # 创建仓库
|
||||
tea repos delete shumengya/<仓库名> # 删除仓库
|
||||
|
||||
# Issue
|
||||
tea issues list --state all -o json # 列出所有 Issue(JSON 格式)
|
||||
tea issues create -t "标题" -d "描述"
|
||||
tea issues close <编号>
|
||||
tea issues reopen <编号>
|
||||
|
||||
# Pull Request
|
||||
tea pulls list # 列出 PR
|
||||
tea pulls create -t "标题" -b main -d "描述"
|
||||
tea pulls approve <编号>
|
||||
tea pulls merge <编号>
|
||||
tea pulls close <编号>
|
||||
|
||||
# Release
|
||||
tea releases list
|
||||
tea releases create --tag v1.0.0 --title "v1.0.0" --note "更新说明"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 方式二:Gitea REST API(curl)
|
||||
|
||||
当 tea 不可用时,直接用 curl 操作。令牌已配置:
|
||||
|
||||
```bash
|
||||
TOKEN="dbc5dfe55b19e9a32f685a97f126cafc2ef76449"
|
||||
API="https://git.shumengya.top/api/v1"
|
||||
```
|
||||
|
||||
### 仓库操作
|
||||
|
||||
```bash
|
||||
# 列出所有仓库
|
||||
curl -s -H "Authorization: token $TOKEN" "$API/user/repos" | python3 -m json.tool
|
||||
|
||||
# 列出指定用户的仓库
|
||||
curl -s -H "Authorization: token $TOKEN" "$API/users/shumengya/repos" \
|
||||
| python3 -c "import sys,json; [print(r['name'], r['html_url']) for r in json.load(sys.stdin)]"
|
||||
|
||||
# 创建仓库(公开)
|
||||
curl -s -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
|
||||
-X POST "$API/user/repos" \
|
||||
-d '{"name":"<仓库名>","description":"描述","private":false}'
|
||||
|
||||
# 创建仓库(私有)
|
||||
curl -s -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
|
||||
-X POST "$API/user/repos" \
|
||||
-d '{"name":"<仓库名>","description":"描述","private":true}'
|
||||
|
||||
# 删除仓库
|
||||
curl -s -H "Authorization: token $TOKEN" -X DELETE "$API/repos/shumengya/<仓库名>"
|
||||
|
||||
# 检查仓库是否存在
|
||||
curl -s -o /dev/null -w "%{http_code}" -H "Authorization: token $TOKEN" \
|
||||
"$API/repos/shumengya/<仓库名>"
|
||||
# 200=存在, 404=不存在
|
||||
```
|
||||
|
||||
### 推送代码到新仓库
|
||||
|
||||
```bash
|
||||
# 添加远程仓库
|
||||
git remote add gitea https://git.shumengya.top/shumengya/<仓库名>.git
|
||||
# 或使用 SSH(推荐,免输令牌)
|
||||
git remote add gitea ssh://git@git.shumengya.top:8022/shumengya/<仓库名>.git
|
||||
|
||||
# 推送到 Gitea
|
||||
git push gitea main
|
||||
```
|
||||
|
||||
### Issue & PR 操作
|
||||
|
||||
```bash
|
||||
# 列出仓库的所有 Issue
|
||||
curl -s -H "Authorization: token $TOKEN" "$API/repos/shumengya/<仓库名>/issues"
|
||||
|
||||
# 创建 Issue
|
||||
curl -s -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
|
||||
-X POST "$API/repos/shumengya/<仓库名>/issues" \
|
||||
-d '{"title":"标题","body":"描述内容"}'
|
||||
|
||||
# 关闭 Issue(编号 42)
|
||||
curl -s -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
|
||||
-X PATCH "$API/repos/shumengya/<仓库名>/issues/42" \
|
||||
-d '{"state":"closed"}'
|
||||
|
||||
# 创建 PR
|
||||
curl -s -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
|
||||
-X POST "$API/repos/shumengya/<仓库名>/pulls" \
|
||||
-d '{"title":"标题","head":"feature-branch","base":"main","body":"描述"}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 方式三:Python 辅助脚本(零依赖)
|
||||
|
||||
纯 Python 标准库,无需 `pip install`,直接运行。
|
||||
|
||||
### gitea_api.py — API 直连脚本
|
||||
|
||||
```bash
|
||||
# 列出仓库
|
||||
python scripts/gitea_api.py repos list
|
||||
|
||||
# 创建仓库
|
||||
python scripts/gitea_api.py repos create sproutclaw --desc "项目描述"
|
||||
|
||||
# 删除仓库
|
||||
python scripts/gitea_api.py repos delete sproutclaw
|
||||
|
||||
# 列出 Issue
|
||||
python scripts/gitea_api.py issues list --repo shumengya/sproutclaw
|
||||
|
||||
# 创建 Issue
|
||||
python scripts/gitea_api.py issues create --repo shumengya/sproutclaw --title "标题" --body "描述"
|
||||
```
|
||||
|
||||
### tea_helper.py — tea CLI 包装脚本
|
||||
|
||||
```bash
|
||||
# 依赖 tea CLI 本身
|
||||
python scripts/tea_helper.py issues # 列出 Issue
|
||||
python scripts/tea_helper.py prs # 列出 PR
|
||||
python scripts/tea_helper.py releases # 列出 Release
|
||||
python scripts/tea_helper.py close-issue 42 # 关闭 Issue
|
||||
python scripts/tea_helper.py triage # 按标签分组 Issue
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 快速工作流示例
|
||||
|
||||
### 场景:将本地项目上传到 Gitea(基于 sproutclaw 实战)
|
||||
|
||||
```bash
|
||||
# 1. 在 Gitea 上创建仓库
|
||||
curl -s -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
|
||||
-X POST "$API/user/repos" \
|
||||
-d '{"name":"sproutclaw","description":"项目描述","private":false}'
|
||||
|
||||
# 2. 添加 Gitea 远程(SSH 方式)
|
||||
git remote add gitea ssh://git@git.shumengya.top:8022/shumengya/sproutclaw.git
|
||||
|
||||
# 3. 提交本地变更并推送
|
||||
git add -A
|
||||
git commit -m "chore: sync to gitea"
|
||||
git push gitea main
|
||||
```
|
||||
|
||||
### 场景:从零创建并推送
|
||||
|
||||
```bash
|
||||
# 使用 Python 脚本一键创建仓库
|
||||
python scripts/gitea_api.py repos create <仓库名> --desc "<描述>"
|
||||
|
||||
# 添加远程
|
||||
git remote add gitea ssh://git@git.shumengya.top:8022/shumengya/<仓库名>.git
|
||||
|
||||
# 推送
|
||||
git push -u gitea main
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 补充说明
|
||||
|
||||
- **令牌变更**:更新令牌时只需修改 `~/.tea/tea.yml` 中的 `token` 字段,或重新执行 `tea login add`
|
||||
- **双远程工作流**:项目同时有 GitHub(`origin`)和 Gitea(`gitea`)远程时,推送时需指定:
|
||||
```bash
|
||||
git push origin main # 推送到 GitHub
|
||||
git push gitea main # 推送到 Gitea
|
||||
```
|
||||
- **参考文档**:[reference.md](reference.md) | [Gitea API 官方文档](https://docs.gitea.com/api/1.20/)
|
||||
244
gitea-tea-skill/reference.md
Normal file
244
gitea-tea-skill/reference.md
Normal file
@@ -0,0 +1,244 @@
|
||||
# tea CLI Full Command Reference
|
||||
|
||||
## Global Flags
|
||||
|
||||
| Flag | Short | Description |
|
||||
|------|-------|-------------|
|
||||
| `--login` | `-l` | Use a specific configured login |
|
||||
| `--repo` | `-r` | Override repo as `owner/repo` slug |
|
||||
| `--remote` | `-R` | Discover login from a git remote name |
|
||||
| `--output` | `-o` | Output format: `simple`, `table`, `csv`, `tsv`, `yaml`, `json` |
|
||||
|
||||
---
|
||||
|
||||
## logins / login
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `tea login list` | List all configured logins |
|
||||
| `tea login add` | Add a Gitea instance |
|
||||
| `tea login edit <name>` | Edit a login entry |
|
||||
| `tea login delete <name>` | Remove a login |
|
||||
| `tea login default [name]` | Get or set default login |
|
||||
| `tea login oauth-refresh` | Refresh OAuth token |
|
||||
| `tea logout` | Log out from a server |
|
||||
| `tea whoami` | Show current logged-in user |
|
||||
|
||||
### `tea login add` flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--url, -u` | Server URL (default: https://gitea.com) |
|
||||
| `--token, -t` | Access token (Settings > Applications) |
|
||||
| `--user` | Username for basic auth |
|
||||
| `--password, --pwd` | Password for basic auth |
|
||||
| `--oauth, -o` | Interactive OAuth2 flow |
|
||||
| `--ssh-key, -s` | Path to SSH key |
|
||||
| `--ssh-agent-key, -a` | SSH public key / fingerprint via agent |
|
||||
| `--otp` | OTP token |
|
||||
| `--name, -n` | Login alias name |
|
||||
| `--insecure, -i` | Disable TLS verification |
|
||||
| `--no-version-check, --nv` | Skip Gitea version check |
|
||||
|
||||
---
|
||||
|
||||
## issues / issue / i
|
||||
|
||||
### List
|
||||
```
|
||||
tea issues list [--state open|closed|all] [--keyword STR] [--labels L1,L2]
|
||||
[--assignee USER] [--author USER] [--milestone M]
|
||||
[--limit N] [--page N]
|
||||
[--fields index,state,kind,author,url,title,body,created,updated,
|
||||
deadline,assignees,milestone,labels,comments,owner,repo]
|
||||
```
|
||||
|
||||
### Create
|
||||
```
|
||||
tea issues create --title TITLE --description DESC
|
||||
[--labels L1,L2] [--assignees U1,U2]
|
||||
[--milestone M] [--deadline DATE]
|
||||
```
|
||||
|
||||
### Edit
|
||||
```
|
||||
tea issues edit INDEX --title T --description D
|
||||
--add-labels L1 --remove-labels L2
|
||||
--add-assignees U1 --milestone M
|
||||
```
|
||||
|
||||
### Other
|
||||
```
|
||||
tea issues close INDEX [INDEX...]
|
||||
tea issues reopen INDEX [INDEX...]
|
||||
tea issues comment INDEX # Add a comment (interactive)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## pulls / pull / pr
|
||||
|
||||
### List
|
||||
```
|
||||
tea pulls list [--state open|closed|all]
|
||||
[--fields index,state,author,url,title,body,mergeable,base,
|
||||
base-commit,head,diff,patch,created,updated,deadline,
|
||||
assignees,milestone,labels,comments,ci]
|
||||
```
|
||||
|
||||
### Create
|
||||
```
|
||||
tea pulls create --title T --description D --base BRANCH
|
||||
[--head BRANCH] [--labels L] [--assignees U]
|
||||
[--milestone M] [--allow-maintainer-edits]
|
||||
```
|
||||
|
||||
### Review workflow
|
||||
```
|
||||
tea pulls checkout 99 # Check out PR locally
|
||||
tea pulls review 99 # Interactive review
|
||||
tea pulls approve 99 # Approve (LGTM)
|
||||
tea pulls reject 99 # Request changes
|
||||
tea pulls merge 99 # Merge the PR
|
||||
tea pulls clean 99 # Delete merged branch
|
||||
```
|
||||
|
||||
### Edit
|
||||
```
|
||||
tea pulls edit 99 --title T --description D
|
||||
--add-labels L --remove-labels L
|
||||
--add-reviewers U1,U2 --remove-reviewers U3
|
||||
--add-assignees U1 --milestone M
|
||||
```
|
||||
|
||||
### review-comments
|
||||
```
|
||||
tea pulls review-comments 99 # List review comments
|
||||
tea pulls resolve COMMENT_ID # Resolve a review comment
|
||||
tea pulls unresolve COMMENT_ID
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## repos / repo
|
||||
|
||||
```
|
||||
tea repos list [--owner ORG] [--limit N]
|
||||
tea repos create --name NAME [--description D] [--private] [--init]
|
||||
[--default-branch BRANCH] [--template OWNER/REPO]
|
||||
tea repos fork OWNER/REPO [--organization ORG]
|
||||
tea repos delete OWNER/REPO
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## releases / release
|
||||
|
||||
```
|
||||
tea releases list [--limit N]
|
||||
tea releases create --tag TAG --title TITLE --note "Notes"
|
||||
[--draft] [--prerelease] [--target BRANCH_OR_SHA]
|
||||
tea releases edit TAG --title T --note N [--draft] [--prerelease]
|
||||
tea releases delete TAG
|
||||
tea releases assets list TAG
|
||||
tea releases assets upload TAG --asset FILE
|
||||
tea releases assets delete TAG ASSET_ID
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## milestones / milestone
|
||||
|
||||
```
|
||||
tea milestones list
|
||||
tea milestones create --title T [--description D] [--deadline DATE]
|
||||
tea milestones edit INDEX --title T
|
||||
tea milestones close INDEX
|
||||
tea milestones reopen INDEX
|
||||
tea milestones delete INDEX
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## labels / label
|
||||
|
||||
```
|
||||
tea labels list
|
||||
tea labels create --name NAME --color "#HEX" [--description D]
|
||||
tea labels delete INDEX
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## notifications / notification / n
|
||||
|
||||
```
|
||||
tea notifications list [--all] [--mine] [--limit N]
|
||||
tea notifications markread [INDEX] # Mark as read
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## admin (server admin only)
|
||||
|
||||
```
|
||||
tea admin users list
|
||||
tea admin orgs list
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Output Fields Quick Reference
|
||||
|
||||
### Issues `--fields`
|
||||
`index`, `state`, `kind`, `author`, `author-id`, `url`, `title`, `body`, `created`, `updated`, `deadline`, `assignees`, `milestone`, `labels`, `comments`, `owner`, `repo`
|
||||
|
||||
### PRs `--fields`
|
||||
`index`, `state`, `author`, `author-id`, `url`, `title`, `body`, `mergeable`, `base`, `base-commit`, `head`, `diff`, `patch`, `created`, `updated`, `deadline`, `assignees`, `milestone`, `labels`, `comments`, `ci`
|
||||
|
||||
---
|
||||
|
||||
## Python Automation Patterns
|
||||
|
||||
### Get all open issues as structured data
|
||||
```python
|
||||
import subprocess, json
|
||||
|
||||
def get_issues(repo=None, state="open"):
|
||||
cmd = ["tea", "issues", "list", "--state", state, "-o", "json"]
|
||||
if repo:
|
||||
cmd += ["-r", repo]
|
||||
out = subprocess.check_output(cmd, text=True)
|
||||
return json.loads(out)
|
||||
```
|
||||
|
||||
### Create issue from Python
|
||||
```python
|
||||
def create_issue(title, body="", labels=None, repo=None):
|
||||
cmd = ["tea", "issues", "create", "--title", title]
|
||||
if body:
|
||||
cmd += ["--description", body]
|
||||
if labels:
|
||||
cmd += ["--labels", ",".join(labels)]
|
||||
if repo:
|
||||
cmd += ["-r", repo]
|
||||
subprocess.run(cmd, check=True)
|
||||
```
|
||||
|
||||
### Bulk close stale issues
|
||||
```python
|
||||
issues = get_issues(state="open")
|
||||
stale = [i for i in issues if "stale" in [l["name"] for l in (i.get("labels") or [])]]
|
||||
for issue in stale:
|
||||
idx = issue.get("number") or issue.get("index")
|
||||
subprocess.run(["tea", "issues", "close", str(idx)], check=True)
|
||||
print(f"Closed #{idx}")
|
||||
```
|
||||
|
||||
### Export releases to JSON file
|
||||
```python
|
||||
import subprocess, json, pathlib
|
||||
|
||||
out = subprocess.check_output(["tea", "releases", "list", "-o", "json"], text=True)
|
||||
pathlib.Path("releases.json").write_text(out)
|
||||
```
|
||||
260
gitea-tea-skill/scripts/gitea_api.py
Normal file
260
gitea-tea-skill/scripts/gitea_api.py
Normal file
@@ -0,0 +1,260 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
gitea_api.py — Pure stdlib Gitea API helper for https://git.shumengya.top
|
||||
|
||||
Usage:
|
||||
python scripts/gitea_api.py repos list
|
||||
python scripts/gitea_api.py repos create <name> [--desc DESC] [--private]
|
||||
python scripts/gitea_api.py repos delete <name>
|
||||
python scripts/gitea_api.py repos exists <name>
|
||||
python scripts/gitea_api.py issues list [--repo owner/name]
|
||||
python scripts/gitea_api.py issues create --repo owner/name --title TITLE [--body BODY]
|
||||
|
||||
Zero external dependencies (stdlib only: urllib, json, argparse, sys, os).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from typing import Any
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Configuration
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
DEFAULT_URL = "https://git.shumengya.top"
|
||||
DEFAULT_TOKEN = "dbc5dfe55b19e9a32f685a97f126cafc2ef76449"
|
||||
DEFAULT_OWNER = "shumengya"
|
||||
|
||||
# Allow override via environment variables
|
||||
GITEA_URL = os.environ.get("GITEA_URL", DEFAULT_URL).rstrip("/")
|
||||
GITEA_TOKEN = os.environ.get("GITEA_TOKEN", DEFAULT_TOKEN)
|
||||
API_BASE = f"{GITEA_URL}/api/v1"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# HTTP helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _headers() -> dict[str, str]:
|
||||
return {
|
||||
"Authorization": f"token {GITEA_TOKEN}",
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
}
|
||||
|
||||
|
||||
def _request(
|
||||
method: str,
|
||||
path: str,
|
||||
data: dict[str, Any] | None = None,
|
||||
) -> tuple[int, Any]:
|
||||
"""Send HTTP request to Gitea API. Returns (status_code, parsed_json)."""
|
||||
url = f"{API_BASE}{path}"
|
||||
body = json.dumps(data).encode("utf-8") if data else None
|
||||
req = urllib.request.Request(
|
||||
url,
|
||||
data=body,
|
||||
headers=_headers(),
|
||||
method=method,
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req) as resp:
|
||||
raw = resp.read()
|
||||
status = resp.status
|
||||
if raw.strip():
|
||||
return status, json.loads(raw.decode("utf-8"))
|
||||
return status, None
|
||||
except urllib.error.HTTPError as e:
|
||||
try:
|
||||
detail = json.loads(e.read().decode("utf-8"))
|
||||
except Exception:
|
||||
detail = {"message": str(e)}
|
||||
return e.code, detail
|
||||
except urllib.error.URLError as e:
|
||||
print(f"[ERROR] Network error: {e.reason}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Repository commands
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def cmd_repos_list(_args: argparse.Namespace) -> None:
|
||||
status, data = _request("GET", f"/users/{DEFAULT_OWNER}/repos?limit=100")
|
||||
if status != 200:
|
||||
print(f"[ERROR] Failed to list repos (HTTP {status}): {data}")
|
||||
sys.exit(1)
|
||||
if not data:
|
||||
print("No repositories found.")
|
||||
return
|
||||
print(f"{'Name':<30} {'Private':<8} {'Description'}")
|
||||
print("-" * 80)
|
||||
for r in sorted(data, key=lambda x: x.get("name", "")):
|
||||
name = r.get("name", "?")
|
||||
private = "yes" if r.get("private") else "no"
|
||||
desc = (r.get("description") or "")[:45]
|
||||
print(f"{name:<30} {private:<8} {desc}")
|
||||
|
||||
|
||||
def cmd_repos_create(args: argparse.Namespace) -> None:
|
||||
payload: dict[str, Any] = {
|
||||
"name": args.name,
|
||||
"description": args.desc or "",
|
||||
"private": args.private,
|
||||
"auto_init": False,
|
||||
}
|
||||
status, data = _request("POST", "/user/repos", payload)
|
||||
if status in (201, 200):
|
||||
print(f"[OK] Repository created: {GITEA_URL}/{DEFAULT_OWNER}/{args.name}")
|
||||
print(f" HTTPS: {GITEA_URL}/{DEFAULT_OWNER}/{args.name}.git")
|
||||
print(f" SSH: ssh://git@{GITEA_URL.split('://')[1]}:8022/{DEFAULT_OWNER}/{args.name}.git")
|
||||
elif status == 409:
|
||||
print(f"[WARN] Repository '{args.name}' already exists.", file=sys.stderr)
|
||||
else:
|
||||
print(f"[ERROR] HTTP {status}: {data}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def cmd_repos_delete(args: argparse.Namespace) -> None:
|
||||
print(f"Deleting repository {DEFAULT_OWNER}/{args.name}...", end=" ")
|
||||
status, data = _request("DELETE", f"/repos/{DEFAULT_OWNER}/{args.name}")
|
||||
if status in (204, 200):
|
||||
print("[OK] Deleted.")
|
||||
else:
|
||||
print(f"\n[ERROR] HTTP {status}: {data}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def cmd_repos_exists(args: argparse.Namespace) -> None:
|
||||
status, _data = _request("GET", f"/repos/{DEFAULT_OWNER}/{args.name}")
|
||||
if status == 200:
|
||||
print(f"[YES] Repository '{args.name}' exists.")
|
||||
elif status == 404:
|
||||
print(f"[NO] Repository '{args.name}' does not exist.")
|
||||
else:
|
||||
print(f"[UNKNOWN] HTTP {status}", file=sys.stderr)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Issue commands
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def cmd_issues_list(args: argparse.Namespace) -> None:
|
||||
repo = args.repo or f"{DEFAULT_OWNER}/sproutclaw"
|
||||
status, data = _request("GET", f"/repos/{repo}/issues?state={args.state}&limit=50")
|
||||
if status != 200:
|
||||
print(f"[ERROR] HTTP {status}: {data}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
if not data:
|
||||
print(f"No {args.state} issues in {repo}.")
|
||||
return
|
||||
print(f"{'#':<6} {'State':<8} {'Title'}")
|
||||
print("-" * 70)
|
||||
for issue in data:
|
||||
# PRs also appear in issues endpoint; skip them
|
||||
if issue.get("pull_request"):
|
||||
continue
|
||||
num = issue.get("number", "?")
|
||||
state = issue.get("state", "?")
|
||||
title = issue.get("title", "?")
|
||||
print(f"#{num:<5} {state:<8} {title}")
|
||||
|
||||
|
||||
def cmd_issues_create(args: argparse.Namespace) -> None:
|
||||
if not args.repo:
|
||||
print("[ERROR] --repo is required (e.g. shumengya/sproutclaw)", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
payload: dict[str, Any] = {"title": args.title}
|
||||
if args.body:
|
||||
payload["body"] = args.body
|
||||
status, data = _request("POST", f"/repos/{args.repo}/issues", payload)
|
||||
if status in (201, 200):
|
||||
url = data.get("html_url", "?")
|
||||
print(f"[OK] Issue created: {url}")
|
||||
else:
|
||||
print(f"[ERROR] HTTP {status}: {data}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CLI
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Gitea API helper — 纯 Python 标准库,零外部依赖。"
|
||||
)
|
||||
sub = parser.add_subparsers(dest="resource", required=True)
|
||||
|
||||
# --- repos ---
|
||||
repos_parser = sub.add_parser("repos", help="仓库操作")
|
||||
repos_commands = repos_parser.add_subparsers(dest="action", required=True)
|
||||
|
||||
# repos list
|
||||
repos_commands.add_parser("list", help="列出所有仓库")
|
||||
|
||||
# repos create
|
||||
rc = repos_commands.add_parser("create", help="创建新仓库")
|
||||
rc.add_argument("name", help="仓库名称")
|
||||
rc.add_argument("--desc", "-d", default="", help="仓库描述")
|
||||
rc.add_argument("--private", action="store_true", help="创建私有仓库")
|
||||
|
||||
# repos delete
|
||||
rd = repos_commands.add_parser("delete", help="删除仓库")
|
||||
rd.add_argument("name", help="仓库名称")
|
||||
|
||||
# repos exists
|
||||
re = repos_commands.add_parser("exists", help="检查仓库是否存在")
|
||||
re.add_argument("name", help="仓库名称")
|
||||
|
||||
# --- issues ---
|
||||
issues_parser = sub.add_parser("issues", help="Issue 操作")
|
||||
issues_commands = issues_parser.add_subparsers(dest="action", required=True)
|
||||
|
||||
# issues list
|
||||
il = issues_commands.add_parser("list", help="列出 Issue")
|
||||
il.add_argument("--repo", "-r", help="仓库路径 (owner/name)")
|
||||
il.add_argument("--state", default="open", choices=["open", "closed", "all"])
|
||||
|
||||
# issues create
|
||||
ic = issues_commands.add_parser("create", help="创建 Issue")
|
||||
ic.add_argument("--repo", "-r", required=True, help="仓库路径 (owner/name)")
|
||||
ic.add_argument("--title", "-t", required=True, help="Issue 标题")
|
||||
ic.add_argument("--body", "-b", default="", help="Issue 描述")
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = build_parser()
|
||||
args = parser.parse_args()
|
||||
|
||||
if not GITEA_TOKEN:
|
||||
print("[ERROR] GITEA_TOKEN environment variable is not set and no default available.")
|
||||
sys.exit(1)
|
||||
|
||||
dispatch = {
|
||||
("repos", "list"): cmd_repos_list,
|
||||
("repos", "create"): cmd_repos_create,
|
||||
("repos", "delete"): cmd_repos_delete,
|
||||
("repos", "exists"): cmd_repos_exists,
|
||||
("issues", "list"): cmd_issues_list,
|
||||
("issues", "create"): cmd_issues_create,
|
||||
}
|
||||
|
||||
key = (args.resource, args.action)
|
||||
if key in dispatch:
|
||||
dispatch[key](args)
|
||||
else:
|
||||
parser.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
190
gitea-tea-skill/scripts/tea_helper.py
Normal file
190
gitea-tea-skill/scripts/tea_helper.py
Normal file
@@ -0,0 +1,190 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
tea_helper.py — Python utility for Gitea tea CLI automation.
|
||||
|
||||
Usage:
|
||||
python tea_helper.py issues [--state open|closed|all] [--repo owner/repo]
|
||||
python tea_helper.py prs [--state open|closed|all] [--repo owner/repo]
|
||||
python tea_helper.py releases [--repo owner/repo]
|
||||
python tea_helper.py close-issue <index> [--repo owner/repo]
|
||||
python tea_helper.py close-pr <index> [--repo owner/repo]
|
||||
python tea_helper.py create-issue --title TITLE [--desc DESC] [--labels L1,L2]
|
||||
python tea_helper.py triage [--repo owner/repo]
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Core helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def run_tea(*args: str, capture: bool = True) -> subprocess.CompletedProcess:
|
||||
cmd = ["tea", *args]
|
||||
result = subprocess.run(cmd, capture_output=capture, text=True)
|
||||
if result.returncode != 0 and capture:
|
||||
print(f"[tea error] {result.stderr.strip()}", file=sys.stderr)
|
||||
return result
|
||||
|
||||
|
||||
def tea_json(*args: str) -> Any:
|
||||
result = run_tea(*args, "-o", "json")
|
||||
if result.returncode != 0:
|
||||
return []
|
||||
try:
|
||||
return json.loads(result.stdout)
|
||||
except json.JSONDecodeError:
|
||||
return []
|
||||
|
||||
|
||||
def repo_flags(repo: str | None) -> list[str]:
|
||||
return ["-r", repo] if repo else []
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Commands
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def cmd_issues(args: argparse.Namespace) -> None:
|
||||
flags = ["issues", "list", "--state", args.state, *repo_flags(args.repo)]
|
||||
issues = tea_json(*flags)
|
||||
if not issues:
|
||||
print("No issues found.")
|
||||
return
|
||||
print(f"{'#':<6} {'State':<8} {'Title'}")
|
||||
print("-" * 60)
|
||||
for i in issues:
|
||||
print(f"#{i.get('number', i.get('index','?')):<5} {i.get('state',''):<8} {i.get('title','')}")
|
||||
|
||||
|
||||
def cmd_prs(args: argparse.Namespace) -> None:
|
||||
flags = ["pulls", "list", "--state", args.state,
|
||||
"--fields", "index,title,state,author,milestone,updated,labels,ci",
|
||||
*repo_flags(args.repo)]
|
||||
prs = tea_json(*flags)
|
||||
if not prs:
|
||||
print("No pull requests found.")
|
||||
return
|
||||
print(f"{'#':<6} {'State':<8} {'Title'}")
|
||||
print("-" * 60)
|
||||
for pr in prs:
|
||||
print(f"#{pr.get('number', pr.get('index','?')):<5} {pr.get('state',''):<8} {pr.get('title','')}")
|
||||
|
||||
|
||||
def cmd_releases(args: argparse.Namespace) -> None:
|
||||
flags = ["releases", "list", *repo_flags(args.repo)]
|
||||
releases = tea_json(*flags)
|
||||
if not releases:
|
||||
print("No releases found.")
|
||||
return
|
||||
print(f"{'Tag':<20} {'Name':<30} {'Draft':<6} {'Pre':<5}")
|
||||
print("-" * 65)
|
||||
for r in releases:
|
||||
print(f"{r.get('tag_name',''):<20} {r.get('name',''):<30} "
|
||||
f"{str(r.get('draft', False)):<6} {str(r.get('prerelease', False)):<5}")
|
||||
|
||||
|
||||
def cmd_close_issue(args: argparse.Namespace) -> None:
|
||||
result = run_tea("issues", "close", str(args.index), *repo_flags(args.repo))
|
||||
if result.returncode == 0:
|
||||
print(f"Issue #{args.index} closed.")
|
||||
|
||||
|
||||
def cmd_close_pr(args: argparse.Namespace) -> None:
|
||||
result = run_tea("pulls", "close", str(args.index), *repo_flags(args.repo))
|
||||
if result.returncode == 0:
|
||||
print(f"PR #{args.index} closed.")
|
||||
|
||||
|
||||
def cmd_create_issue(args: argparse.Namespace) -> None:
|
||||
flags = ["issues", "create", "--title", args.title, *repo_flags(args.repo)]
|
||||
if args.desc:
|
||||
flags += ["--description", args.desc]
|
||||
if args.labels:
|
||||
flags += ["--labels", args.labels]
|
||||
result = run_tea(*flags, capture=False)
|
||||
sys.exit(result.returncode)
|
||||
|
||||
|
||||
def cmd_triage(args: argparse.Namespace) -> None:
|
||||
"""Group open issues by their first label."""
|
||||
flags = ["issues", "list", "--state", "open", *repo_flags(args.repo)]
|
||||
issues = tea_json(*flags)
|
||||
if not issues:
|
||||
print("No open issues.")
|
||||
return
|
||||
|
||||
groups: dict[str, list] = {}
|
||||
for issue in issues:
|
||||
labels = issue.get("labels") or []
|
||||
label = labels[0].get("name", "unlabeled") if labels else "unlabeled"
|
||||
groups.setdefault(label, []).append(issue)
|
||||
|
||||
for label, items in sorted(groups.items()):
|
||||
print(f"\n[{label}] ({len(items)})")
|
||||
for i in items:
|
||||
print(f" #{i.get('number', i.get('index','?'))} {i.get('title','')}")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CLI
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
p = argparse.ArgumentParser(description="tea CLI helper (Python wrapper)")
|
||||
p.add_argument("--repo", "-r", help="owner/repo slug (overrides auto-detect)")
|
||||
sub = p.add_subparsers(dest="command", required=True)
|
||||
|
||||
# issues
|
||||
pi = sub.add_parser("issues", help="List issues")
|
||||
pi.add_argument("--state", default="open", choices=["open", "closed", "all"])
|
||||
|
||||
# prs
|
||||
pp = sub.add_parser("prs", help="List pull requests")
|
||||
pp.add_argument("--state", default="open", choices=["open", "closed", "all"])
|
||||
|
||||
# releases
|
||||
sub.add_parser("releases", help="List releases")
|
||||
|
||||
# close-issue
|
||||
pci = sub.add_parser("close-issue", help="Close an issue")
|
||||
pci.add_argument("index", type=int, help="Issue index/number")
|
||||
|
||||
# close-pr
|
||||
pcp = sub.add_parser("close-pr", help="Close a pull request")
|
||||
pcp.add_argument("index", type=int, help="PR index/number")
|
||||
|
||||
# create-issue
|
||||
pcr = sub.add_parser("create-issue", help="Create a new issue")
|
||||
pcr.add_argument("--title", "-t", required=True)
|
||||
pcr.add_argument("--desc", "-d", default="")
|
||||
pcr.add_argument("--labels", "-L", default="", help="Comma-separated labels")
|
||||
|
||||
# triage
|
||||
sub.add_parser("triage", help="Group open issues by label")
|
||||
|
||||
return p
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = build_parser()
|
||||
args = parser.parse_args()
|
||||
|
||||
dispatch = {
|
||||
"issues": cmd_issues,
|
||||
"prs": cmd_prs,
|
||||
"releases": cmd_releases,
|
||||
"close-issue": cmd_close_issue,
|
||||
"close-pr": cmd_close_pr,
|
||||
"create-issue": cmd_create_issue,
|
||||
"triage": cmd_triage,
|
||||
}
|
||||
dispatch[args.command](args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user