chore: commit pending workspace updates
Some checks failed
CI / build-check-test (push) Has been cancelled
Some checks failed
CI / build-check-test (push) Has been cancelled
This commit is contained in:
@@ -1,48 +0,0 @@
|
||||
---
|
||||
name: command-help-zh-skill
|
||||
description: 为任意 CLI 命令创建独立、无依赖的 Bash 包装脚本,将帮助文本翻译成中文,并只把说明性描述高亮为黄色。适用于用户希望获得本地化的 `--help` 体验,同时保持命令名称和参数不着色的场景。
|
||||
---
|
||||
|
||||
# 命令帮助中文高亮
|
||||
|
||||
## 适用场景
|
||||
|
||||
用户要把任意命令的 `--help` / `-h` 帮助页做成中文版本,并且要求:
|
||||
|
||||
- 只高亮“介绍 / 描述”文字
|
||||
- 命令名、参数名、选项名保持普通颜色
|
||||
- 脚本必须是单文件 `bash`
|
||||
- 不能依赖复杂工具链,优先只用 `printf`、`case`、`cat`
|
||||
|
||||
## 处理流程
|
||||
|
||||
1. 先确认目标命令名,例如 `codex`、`git`、`tea`、`gh`,或由用户提供 `--help` 输出。
|
||||
2. 如果没有原始帮助内容,就先运行 `<command> --help` 获取输出。
|
||||
3. 翻译成自然、简洁的中文。
|
||||
4. 保留原始结构:
|
||||
- 标题
|
||||
- 简介
|
||||
- 用法
|
||||
- 命令
|
||||
- 参数
|
||||
- 选项
|
||||
5. 输出时只让说明文字使用 ANSI 黄色。
|
||||
- 例如 `\033[1;33m`
|
||||
- 结束后恢复默认颜色
|
||||
6. 命令名、参数、选项、括号内容保持普通颜色,不要整行染色。
|
||||
7. 生成可直接放到全局 PATH 的脚本,默认命名为 `<command>-help`;如果用户要求或命名冲突,再用 `<command>-help-zh`。
|
||||
|
||||
## 实现要求
|
||||
|
||||
- 必须是独立脚本,不能依赖仓库内外的其他文件。
|
||||
- 脚本开头应固定目标命令名,执行时再调用原始命令的 `--help`。
|
||||
- 推荐使用 `printf` 逐行输出,避免把整段文本一起染色。
|
||||
- 如果需要分隔“左侧标签”和“右侧说明”,先打印标签,再单独打印黄色说明。
|
||||
- 保持输出整洁,避免多余前导空格。
|
||||
|
||||
## 输出标准
|
||||
|
||||
- 中文表达自然,优先清晰准确。
|
||||
- 说明部分高亮黄色。
|
||||
- 可以直接复制到 `/usr/local/bin/` 之类的位置使用。
|
||||
- 如果用户指定了固定命令名,按用户要求命名。
|
||||
@@ -1,4 +0,0 @@
|
||||
interface:
|
||||
display_name: "Command Help Chinese Highlight"
|
||||
short_description: "Translate any CLI help text into Chinese with yellow-highlighted descriptions only"
|
||||
default_prompt: "Create a standalone no-dependency Bash help wrapper for the specified command. Translate its help text into Chinese, keep command names and arguments uncolored, and highlight only descriptions in yellow."
|
||||
@@ -1,42 +0,0 @@
|
||||
---
|
||||
name: frontend-design
|
||||
description: Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
|
||||
license: Complete terms in LICENSE.txt
|
||||
---
|
||||
|
||||
This skill guides creation of distinctive, production-grade frontend interfaces that avoid generic "AI slop" aesthetics. Implement real working code with exceptional attention to aesthetic details and creative choices.
|
||||
|
||||
The user provides frontend requirements: a component, page, application, or interface to build. They may include context about the purpose, audience, or technical constraints.
|
||||
|
||||
## Design Thinking
|
||||
|
||||
Before coding, understand the context and commit to a BOLD aesthetic direction:
|
||||
- **Purpose**: What problem does this interface solve? Who uses it?
|
||||
- **Tone**: Pick an extreme: brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian, etc. There are so many flavors to choose from. Use these for inspiration but design one that is true to the aesthetic direction.
|
||||
- **Constraints**: Technical requirements (framework, performance, accessibility).
|
||||
- **Differentiation**: What makes this UNFORGETTABLE? What's the one thing someone will remember?
|
||||
|
||||
**CRITICAL**: Choose a clear conceptual direction and execute it with precision. Bold maximalism and refined minimalism both work - the key is intentionality, not intensity.
|
||||
|
||||
Then implement working code (HTML/CSS/JS, React, Vue, etc.) that is:
|
||||
- Production-grade and functional
|
||||
- Visually striking and memorable
|
||||
- Cohesive with a clear aesthetic point-of-view
|
||||
- Meticulously refined in every detail
|
||||
|
||||
## Frontend Aesthetics Guidelines
|
||||
|
||||
Focus on:
|
||||
- **Typography**: Choose fonts that are beautiful, unique, and interesting. Avoid generic fonts like Arial and Inter; opt instead for distinctive choices that elevate the frontend's aesthetics; unexpected, characterful font choices. Pair a distinctive display font with a refined body font.
|
||||
- **Color & Theme**: Commit to a cohesive aesthetic. Use CSS variables for consistency. Dominant colors with sharp accents outperform timid, evenly-distributed palettes.
|
||||
- **Motion**: Use animations for effects and micro-interactions. Prioritize CSS-only solutions for HTML. Use Motion library for React when available. Focus on high-impact moments: one well-orchestrated page load with staggered reveals (animation-delay) creates more delight than scattered micro-interactions. Use scroll-triggering and hover states that surprise.
|
||||
- **Spatial Composition**: Unexpected layouts. Asymmetry. Overlap. Diagonal flow. Grid-breaking elements. Generous negative space OR controlled density.
|
||||
- **Backgrounds & Visual Details**: Create atmosphere and depth rather than defaulting to solid colors. Add contextual effects and textures that match the overall aesthetic. Apply creative forms like gradient meshes, noise textures, geometric patterns, layered transparencies, dramatic shadows, decorative borders, custom cursors, and grain overlays.
|
||||
|
||||
NEVER use generic AI-generated aesthetics like overused font families (Inter, Roboto, Arial, system fonts), cliched color schemes (particularly purple gradients on white backgrounds), predictable layouts and component patterns, and cookie-cutter design that lacks context-specific character.
|
||||
|
||||
Interpret creatively and make unexpected choices that feel genuinely designed for the context. No design should be the same. Vary between light and dark themes, different fonts, different aesthetics. NEVER converge on common choices (Space Grotesk, for example) across generations.
|
||||
|
||||
**IMPORTANT**: Match implementation complexity to the aesthetic vision. Maximalist designs need elaborate code with extensive animations and effects. Minimalist or refined designs need restraint, precision, and careful attention to spacing, typography, and subtle details. Elegance comes from executing the vision well.
|
||||
|
||||
Remember: Claude is capable of extraordinary creative work. Don't hold back, show what can truly be created when thinking outside the box and committing fully to a distinctive vision.
|
||||
@@ -1 +0,0 @@
|
||||
* text=auto eol=lf
|
||||
4
.pi/agent/skills/gitea-tea-skill/.gitignore
vendored
4
.pi/agent/skills/gitea-tea-skill/.gitignore
vendored
@@ -1,4 +0,0 @@
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
.venv/
|
||||
venv/
|
||||
@@ -1,232 +0,0 @@
|
||||
---
|
||||
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/)
|
||||
@@ -1,244 +0,0 @@
|
||||
# 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)
|
||||
```
|
||||
@@ -1,260 +0,0 @@
|
||||
#!/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()
|
||||
@@ -1,190 +0,0 @@
|
||||
#!/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()
|
||||
@@ -1 +0,0 @@
|
||||
* text=auto eol=lf
|
||||
4
.pi/agent/skills/github-gh-skill/.gitignore
vendored
4
.pi/agent/skills/github-gh-skill/.gitignore
vendored
@@ -1,4 +0,0 @@
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
.venv/
|
||||
venv/
|
||||
@@ -1,86 +0,0 @@
|
||||
---
|
||||
name: github-gh-skill
|
||||
description: 使用 GitHub CLI(`gh`)处理认证、仓库、Issue、Pull Request、Actions、Release,以及通过 JSON/JQ 调用 API 查询数据。适用于用户提到 `gh`、GitHub CLI、终端里的 GitHub、PR、Issue、Actions、Release,或需要不打开浏览器直接获取 GitHub 数据的场景。
|
||||
---
|
||||
|
||||
# GitHub CLI(gh)技能
|
||||
|
||||
## 何时启用
|
||||
|
||||
用户提到:`gh`、GitHub CLI、命令行操作 GitHub、PR/Issue/Release/Actions、或需要从终端拉取 GitHub 信息时,优先按本技能执行。
|
||||
|
||||
## 前置检查(一次性)
|
||||
|
||||
1. **检查 `gh` 是否已安装**:执行 `gh --version`。
|
||||
- 若命令不存在,**自动安装最新版**:根据系统包管理器执行安装(如 `apt install gh -y`、`yum install gh -y`、`brew install gh`)。
|
||||
- 安装失败再**提示用户手动安装**(参考官网 <https://cli.github.com/>)。
|
||||
2. 若需私有资源,执行 `gh auth status`;未登录则 `gh auth login`(非 CI 场景由用户完成交互)。
|
||||
3. 需要脚本化检测时,运行跨平台脚本:`python scripts/check_gh.py`(或 `python3 ...`,仅需 Python 3.8+ 与标准库)。
|
||||
|
||||
## 高效原则(给 Agent)
|
||||
|
||||
- **优先机器可读**:能用 `--json` 就用;需要筛选时用 `--jq`(避免解析人类表格输出)。
|
||||
- **减少交互**:批量操作用 `--yes` / `-y`;需要合并且非交互时用环境变量或文档中的标志位(见 [reference.md](reference.md))。
|
||||
- **先定目标再选子命令**:issue / pr / repo / workflow / release / api —— 不要猜,缺参数时 `gh <cmd> --help`。
|
||||
- **API 兜底**:`gh` 子命令缺能力时用 `gh api`,路径以官方 REST 为准。
|
||||
|
||||
## 模块索引(按场景跳转)
|
||||
|
||||
| 模块 | 内容 |
|
||||
|------|------|
|
||||
| A. 认证与上下文 | `gh auth`,默认仓库 |
|
||||
| B. 仓库 | `gh repo clone/view/create` |
|
||||
| C. Issue | `gh issue list/create/view/close` |
|
||||
| D. PR | `gh pr list/create/view/diff/merge/checkout` |
|
||||
| E. Actions | `gh run list/view/watch/rerun` |
|
||||
| F. Release | `gh release list/create` |
|
||||
| G. 高级 | `gh api`,GraphQL |
|
||||
|
||||
**详细命令与模板**见 [reference.md](reference.md)。
|
||||
|
||||
## A. 认证与上下文
|
||||
|
||||
- 查看登录:`gh auth status`
|
||||
- 设置默认仓库(减少重复 `-R`):在仓库目录内 `gh repo set-default`
|
||||
|
||||
## B. 仓库
|
||||
|
||||
- 克隆:`gh repo clone owner/name`
|
||||
- 当前仓库信息(JSON):`gh repo view --json name,owner,url,defaultBranchRef`
|
||||
|
||||
## C. Issue
|
||||
|
||||
- 列表(JSON):`gh issue list --json number,title,state,author,labels`
|
||||
- 创建:`gh issue create --title "..." --body "..."`
|
||||
|
||||
## D. Pull Request
|
||||
|
||||
- 列表:`gh pr list --json number,title,state,headRefName`
|
||||
- 检出本地分支:`gh pr checkout <number>`
|
||||
- 查看差异:`gh pr diff <number>`
|
||||
|
||||
## E. GitHub Actions(workflow / run)
|
||||
|
||||
- 运行列表:`gh run list --limit 20`
|
||||
- 单次运行详情 + 日志:`gh run view <run-id> --log`
|
||||
|
||||
## F. Release 与标签
|
||||
|
||||
- 列表:`gh release list --limit 20`
|
||||
- 创建(需明确 tag 与说明):`gh release create <tag> --notes "..."`
|
||||
|
||||
## G. `gh api`(REST)
|
||||
|
||||
- GET 示例:`gh api repos/{owner}/{repo}/issues --jq '.[0].title'`
|
||||
- 占位符:`{owner}` `{repo}` 在已关联 git remote 时常可自动解析;不确定时显式传 `-R owner/repo`。
|
||||
|
||||
## 错误处理
|
||||
|
||||
- **401/403**:检查 `gh auth status`、token 权限(repo、workflow 等)。
|
||||
- **未找到命令**:升级 `gh` 或改用 `gh api`。
|
||||
- **交互阻塞**:CI/自动化中避免会打开编辑器的命令;为 `pr merge` 等加 `--merge`/`--squash`/`--rebase` 等非交互选项。
|
||||
|
||||
## 附加资源
|
||||
|
||||
- 完整命令表与 JSON 字段提示:[reference.md](reference.md)
|
||||
- 环境检查脚本(模块化,中文注释;跨平台,仅标准库):[scripts/check_gh.py](scripts/check_gh.py)
|
||||
@@ -1,95 +0,0 @@
|
||||
# gh 参考手册(渐进披露)
|
||||
|
||||
主说明见 [SKILL.md](SKILL.md)。此处补充常用命令与非交互参数。
|
||||
|
||||
## 全局
|
||||
|
||||
| 用途 | 示例 |
|
||||
|------|------|
|
||||
| 帮助 | `gh <command> --help` |
|
||||
| 指定仓库 | `-R owner/repo` 或在仓库目录执行 |
|
||||
| JSON 输出 | `--json field1,field2` |
|
||||
| jq 过滤 | `--jq 'select(.state=="OPEN")'` |
|
||||
|
||||
## auth
|
||||
|
||||
```bash
|
||||
gh auth status
|
||||
gh auth login
|
||||
gh auth token
|
||||
```
|
||||
|
||||
## repo
|
||||
|
||||
```bash
|
||||
gh repo view --json name,owner,url,description
|
||||
gh repo clone owner/name
|
||||
gh repo create name --public --source=. --remote=origin --push
|
||||
```
|
||||
|
||||
## issue
|
||||
|
||||
```bash
|
||||
gh issue list --state open --limit 50
|
||||
gh issue list --json number,title,labels,assignees
|
||||
gh issue view 123 --json body,author,comments
|
||||
gh issue create --title "t" --body "b" --label bug
|
||||
gh issue close 123
|
||||
```
|
||||
|
||||
## pr
|
||||
|
||||
```bash
|
||||
gh pr list --state open
|
||||
gh pr view 42 --json title,body,commits,files
|
||||
gh pr checkout 42
|
||||
gh pr diff 42
|
||||
gh pr merge 42 --squash --delete-branch
|
||||
```
|
||||
|
||||
合并时若需避免编辑器:使用 `--title`/`--body` 与明确的合并策略标志。
|
||||
|
||||
## workflow / run
|
||||
|
||||
```bash
|
||||
gh workflow list
|
||||
gh run list --workflow "ci.yml" --limit 10
|
||||
gh run view <run-id>
|
||||
gh run watch <run-id>
|
||||
gh run rerun <run-id>
|
||||
```
|
||||
|
||||
## release
|
||||
|
||||
```bash
|
||||
gh release list
|
||||
gh release view v1.2.3
|
||||
gh release create v1.2.3 ./dist/*.zip --notes "..."
|
||||
```
|
||||
|
||||
## api
|
||||
|
||||
```bash
|
||||
# GET
|
||||
gh api user
|
||||
|
||||
# 带查询参数
|
||||
gh api repos/{owner}/{repo}/pulls --paginate
|
||||
|
||||
# POST(示例)
|
||||
gh api repos/{owner}/{repo}/issues -f title="x" -f body="y"
|
||||
```
|
||||
|
||||
GraphQL 使用 `gh api graphql -f query='...'`(复杂查询优先查 GitHub 文档)。
|
||||
|
||||
## JSON 字段提示
|
||||
|
||||
不同子命令可用字段以 `gh <cmd> list --json` 的帮助为准。常见:
|
||||
|
||||
- `gh pr list --json`: `number,title,state,headRefName,baseRefName,author,url`
|
||||
- `gh issue list --json`: `number,title,state,labels,assignees,author`
|
||||
|
||||
## CI 环境变量
|
||||
|
||||
- `GH_TOKEN`:非交互调用时常用(注意最小权限原则)。
|
||||
- 具体行为以当前 `gh` 版本文档为准。
|
||||
@@ -1,84 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
模块:GitHub CLI(gh)跨平台环境检测脚本。
|
||||
|
||||
说明:跨平台单文件,仅依赖 Python 标准库;
|
||||
供 Agent 或本地一键验证 gh 是否可用、版本与登录用户(不打印 token)。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
# 区域:配置常量
|
||||
GH_SKILL_PREFIX = "github-gh-skill:"
|
||||
|
||||
|
||||
def resolve_gh_executable() -> str:
|
||||
"""检测 gh 是否在 PATH 中,返回可执行路径。"""
|
||||
path = shutil.which("gh")
|
||||
if not path:
|
||||
print("未找到 gh,请先安装:https://cli.github.com/", file=sys.stderr)
|
||||
sys.exit(127)
|
||||
return path
|
||||
|
||||
|
||||
def get_gh_version_line(gh: str) -> str:
|
||||
"""获取 gh --version 的首行输出。"""
|
||||
proc = subprocess.run(
|
||||
[gh, "--version"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
)
|
||||
if proc.returncode != 0:
|
||||
print("gh --version 执行失败", file=sys.stderr)
|
||||
sys.exit(proc.returncode)
|
||||
first = (proc.stdout or "").splitlines()
|
||||
return first[0].strip() if first else ""
|
||||
|
||||
|
||||
def get_github_login(gh: str) -> str | None:
|
||||
"""
|
||||
仅查询当前登录用户名(gh api),避免打印 gh auth status 中的 token 行。
|
||||
未登录或失败时返回 None。
|
||||
"""
|
||||
proc = subprocess.run(
|
||||
[gh, "api", "user", "--jq", ".login"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
)
|
||||
if proc.returncode != 0:
|
||||
return None
|
||||
login = (proc.stdout or "").strip()
|
||||
return login or None
|
||||
|
||||
|
||||
def print_auth_summary(gh: str) -> None:
|
||||
"""输出登录摘要或警告到 stderr。"""
|
||||
login = get_github_login(gh)
|
||||
if login:
|
||||
print(f"{GH_SKILL_PREFIX} 已登录 GitHub 用户: {login}")
|
||||
else:
|
||||
print(
|
||||
"WARNING: gh 未登录或 token 无效,请执行 gh auth login",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""入口:路径、版本、登录状态(不泄露 token)。"""
|
||||
gh_path = resolve_gh_executable()
|
||||
print(f"{GH_SKILL_PREFIX} gh 路径: {gh_path}")
|
||||
print(f"{GH_SKILL_PREFIX} {get_gh_version_line(gh_path)}")
|
||||
print_auth_summary(gh_path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,140 +0,0 @@
|
||||
---
|
||||
name: linux-ssh-operator-skill
|
||||
description: 通过 SSH 连接并操作 Linux 服务器:执行远程命令、查看日志、管理 systemd 服务、传输文件(scp/rsync)、配置 SSH 别名、安装公钥启用免密登录、排障。用户提到 ssh/scp/rsync、远程服务器 IP:端口、配置 ssh 别名、ssh-copy-id、免密登录、systemctl/journalctl、部署到服务器、在服务器上运行命令、远程拷贝文件 等场景时使用。
|
||||
---
|
||||
|
||||
# Linux SSH Operator
|
||||
|
||||
## Overview
|
||||
|
||||
Use SSH to connect to Linux servers and perform safe, repeatable remote operations (commands, logs, services, file transfer, alias bootstrap, passwordless login).
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Confirm authorization and the target (host, port, user).
|
||||
2. Prefer SSH keys (recommended) for non-interactive runs; avoid storing passwords in files or chat logs.
|
||||
3. Start with read-only checks, then apply changes, then verify.
|
||||
4. If a password prompt or interactive tool is required, run the SSH command in a real terminal/TTY (or enable TTY in your runner).
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Set up SSH keys (recommended)
|
||||
|
||||
Generate a key (ed25519):
|
||||
|
||||
```bash
|
||||
ssh-keygen -t ed25519 -C "codex" -f ~/.ssh/id_ed25519
|
||||
```
|
||||
|
||||
Install the public key on the server (example uses port 22):
|
||||
|
||||
```bash
|
||||
ssh-copy-id -i ~/.ssh/id_ed25519.pub -p 22 USER@SERVER_IP
|
||||
```
|
||||
|
||||
Optional: create an alias in `~/.ssh/config`:
|
||||
|
||||
```sshconfig
|
||||
Host my-server
|
||||
HostName SERVER_IP
|
||||
Port 22
|
||||
User USER
|
||||
IdentityFile ~/.ssh/id_ed25519
|
||||
IdentitiesOnly yes
|
||||
```
|
||||
|
||||
Then connect:
|
||||
|
||||
```bash
|
||||
ssh my-server
|
||||
```
|
||||
|
||||
### Bootstrap alias + passwordless login
|
||||
|
||||
When the user provides an alias, host/IP, and password and wants `ssh alias` to work immediately:
|
||||
|
||||
1. Write or update the alias with the helper script:
|
||||
|
||||
```bash
|
||||
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_alias_setup.sh my-server 192.168.1.10 --user root --install-key
|
||||
```
|
||||
|
||||
2. If `ssh-copy-id` prompts for a password, run in a TTY and enter the password once.
|
||||
3. Verify passwordless login:
|
||||
|
||||
```bash
|
||||
ssh -o BatchMode=yes my-server 'echo SSH_OK; id -un 2>/dev/null || echo root'
|
||||
```
|
||||
|
||||
4. Remind the user that `scp` and `rsync` can reuse the same alias:
|
||||
|
||||
```bash
|
||||
scp ./file.txt my-server:/root/
|
||||
rsync -av ./dir/ my-server:/root/dir/
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- `scripts/ssh_alias_setup.sh` auto-generates `~/.ssh/id_ed25519` if it does not exist.
|
||||
- It updates `~/.ssh/config` idempotently and defaults to `root`, port `22`, and `StrictHostKeyChecking=accept-new`.
|
||||
- For tests, set `SSH_CONFIG_FILE=/path/to/temp-config`.
|
||||
|
||||
### Run remote commands
|
||||
|
||||
- Direct:
|
||||
|
||||
```bash
|
||||
ssh my-server uname -a
|
||||
```
|
||||
|
||||
- With sudo (often needs a TTY):
|
||||
|
||||
```bash
|
||||
ssh -tt my-server sudo systemctl status nginx --no-pager
|
||||
```
|
||||
|
||||
- Via wrapper script (consistent options; supports env defaults like `REMOTE_USER`, `REMOTE_PORT`, `REMOTE_KEY`):
|
||||
- If you installed this Skill globally in `~/.claude/skills/`, use the absolute paths below (recommended so both Claude Code + OpenCode can find it).
|
||||
|
||||
```bash
|
||||
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_run.sh my-server -- uname -a
|
||||
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_run.sh --tty --sudo my-server -- systemctl restart nginx
|
||||
```
|
||||
|
||||
### Transfer files
|
||||
|
||||
Upload:
|
||||
|
||||
```bash
|
||||
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_copy.sh push my-server ./local.txt /tmp/local.txt
|
||||
```
|
||||
|
||||
Download:
|
||||
|
||||
```bash
|
||||
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_copy.sh pull my-server /var/log/syslog ./syslog
|
||||
```
|
||||
|
||||
## Common Ops (snippets)
|
||||
|
||||
- Disk: `df -h`, `du -sh /path/* | sort -h`
|
||||
- Memory/CPU: `free -h`, `top`, `ps aux --sort=-%mem | head`
|
||||
- Logs: `journalctl -u SERVICE -n 200 --no-pager`, `tail -n 200 -f /path/log`
|
||||
- Services: `systemctl status|restart|stop SERVICE`
|
||||
- Networking: `ss -lntp`, `ip a`, `ip r`, `curl -v http://127.0.0.1:PORT/`
|
||||
|
||||
## Safety
|
||||
|
||||
- Never store or paste passwords in repo files or chat logs.
|
||||
- Avoid `StrictHostKeyChecking=no`; prefer verifying host keys (or use `accept-new` only when appropriate).
|
||||
- For destructive commands (rm, shutdown, firewall changes), ask for explicit user confirmation and show the exact command first.
|
||||
|
||||
## References
|
||||
|
||||
- SSH security + troubleshooting: `references/ssh-playbook.md`
|
||||
|
||||
## Scripts
|
||||
|
||||
- `scripts/ssh_alias_setup.sh`: create/update SSH aliases and optionally run `ssh-copy-id`.
|
||||
- `scripts/ssh_run.sh`: run remote commands with consistent options.
|
||||
- `scripts/ssh_copy.sh`: push/pull files via scp with consistent options.
|
||||
@@ -1,204 +0,0 @@
|
||||
---
|
||||
name: linux-ssh-operator-skill
|
||||
description: 通过 SSH 连接并操作 Linux 服务器:执行远程命令、查看日志、管理 systemd 服务、传输文件(scp/rsync)、配置 SSH 别名、安装公钥启用免密登录、排障。用户提到 ssh/scp/rsync、远程服务器 IP:端口、配置 ssh 别名、ssh-copy-id、免密登录、systemctl/journalctl、部署到服务器、在服务器上运行命令、远程拷贝文件 等场景时使用。
|
||||
---
|
||||
|
||||
# Linux SSH 运维助手(中文说明)
|
||||
|
||||
> 本文件是 `SKILL.md` 的中文说明版,便于阅读与维护;技能实际触发仍以 `SKILL.md` 为准。
|
||||
|
||||
## 概述
|
||||
|
||||
这个 Skill 用于通过 SSH 安全地连接 Linux 服务器,并执行可重复、可验证的远程操作,包括:
|
||||
|
||||
- 执行远程命令
|
||||
- 查看系统与服务日志
|
||||
- 管理 `systemd` 服务
|
||||
- 上传和下载文件
|
||||
- 配置 SSH 别名
|
||||
- 安装公钥并启用免密登录
|
||||
- 进行常见运维排障
|
||||
|
||||
适合以下场景:
|
||||
|
||||
- 登录远程 Linux 服务器
|
||||
- 在服务器上运行命令
|
||||
- 查看 `journalctl` / 应用日志
|
||||
- 启动、停止、重启服务
|
||||
- 把文件部署到服务器
|
||||
- 从服务器拉取日志、配置或产物文件
|
||||
- 按“别名 + IP + 密码”快速配置一键 SSH 免密登录
|
||||
|
||||
## 工作流程
|
||||
|
||||
1. 先确认授权,以及目标信息:主机、端口、用户名。
|
||||
2. 优先使用 SSH 密钥进行非交互连接,避免在聊天记录或文件里保存密码。
|
||||
3. 优先先做只读检查,再执行修改,最后进行验证。
|
||||
4. 如果需要密码提示或交互式工具,使用真实终端或启用 TTY。
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 配置 SSH 密钥(推荐)
|
||||
|
||||
生成一把 `ed25519` 密钥:
|
||||
|
||||
```bash
|
||||
ssh-keygen -t ed25519 -C "codex" -f ~/.ssh/id_ed25519
|
||||
```
|
||||
|
||||
把公钥安装到目标服务器(示例端口为 `22`):
|
||||
|
||||
```bash
|
||||
ssh-copy-id -i ~/.ssh/id_ed25519.pub -p 22 USER@SERVER_IP
|
||||
```
|
||||
|
||||
可选:在 `~/.ssh/config` 中配置主机别名:
|
||||
|
||||
```sshconfig
|
||||
Host my-server
|
||||
HostName SERVER_IP
|
||||
Port 22
|
||||
User USER
|
||||
IdentityFile ~/.ssh/id_ed25519
|
||||
IdentitiesOnly yes
|
||||
```
|
||||
|
||||
之后可直接连接:
|
||||
|
||||
```bash
|
||||
ssh my-server
|
||||
```
|
||||
|
||||
### 一键配置别名 + 免密登录
|
||||
|
||||
当用户提供“别名、主机/IP、密码”,并希望直接通过 `ssh 别名` 登录时,优先使用下面的流程:
|
||||
|
||||
1. 使用辅助脚本写入或更新 SSH 别名:
|
||||
|
||||
```bash
|
||||
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_alias_setup.sh my-server 192.168.1.10 --user root --install-key
|
||||
```
|
||||
|
||||
2. 如果 `ssh-copy-id` 提示输入密码,请在 TTY 中输入一次密码。
|
||||
3. 安装完成后验证免密登录:
|
||||
|
||||
```bash
|
||||
ssh -o BatchMode=yes my-server 'echo SSH_OK; id -un 2>/dev/null || echo root'
|
||||
```
|
||||
|
||||
4. 告诉用户 `scp` 和 `rsync` 也可以直接复用这个别名:
|
||||
|
||||
```bash
|
||||
scp ./file.txt my-server:/root/
|
||||
rsync -av ./dir/ my-server:/root/dir/
|
||||
```
|
||||
|
||||
补充说明:
|
||||
|
||||
- `scripts/ssh_alias_setup.sh` 在本机没有 `~/.ssh/id_ed25519` 时会自动生成。
|
||||
- 它会幂等更新 `~/.ssh/config`,默认使用 `root`、端口 `22`、`StrictHostKeyChecking=accept-new`。
|
||||
- 如果要做临时测试,可以设置 `SSH_CONFIG_FILE=/path/to/temp-config`。
|
||||
|
||||
### 运行远程命令
|
||||
|
||||
直接执行:
|
||||
|
||||
```bash
|
||||
ssh my-server uname -a
|
||||
```
|
||||
|
||||
带 `sudo` 执行(通常需要 TTY):
|
||||
|
||||
```bash
|
||||
ssh -tt my-server sudo systemctl status nginx --no-pager
|
||||
```
|
||||
|
||||
使用封装脚本执行(统一参数风格,支持环境变量默认值如 `REMOTE_USER`、`REMOTE_PORT`、`REMOTE_KEY`):
|
||||
|
||||
```bash
|
||||
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_run.sh my-server -- uname -a
|
||||
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_run.sh --tty --sudo my-server -- systemctl restart nginx
|
||||
```
|
||||
|
||||
### 传输文件
|
||||
|
||||
上传文件:
|
||||
|
||||
```bash
|
||||
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_copy.sh push my-server ./local.txt /tmp/local.txt
|
||||
```
|
||||
|
||||
下载文件:
|
||||
|
||||
```bash
|
||||
~/.claude/skills/linux-ssh-operator-skill/scripts/ssh_copy.sh pull my-server /var/log/syslog ./syslog
|
||||
```
|
||||
|
||||
## 常见运维操作
|
||||
|
||||
- 磁盘空间:`df -h`
|
||||
- 目录占用:`du -sh /path/* | sort -h`
|
||||
- 内存查看:`free -h`
|
||||
- 进程查看:`ps aux --sort=-%mem | head`
|
||||
- 日志查看:`journalctl -u SERVICE -n 200 --no-pager`
|
||||
- 日志跟踪:`tail -n 200 -f /path/log`
|
||||
- 服务管理:`systemctl status|restart|stop SERVICE`
|
||||
- 网络排查:`ss -lntp`、`ip a`、`ip r`、`curl -v http://127.0.0.1:PORT/`
|
||||
|
||||
## 安全注意事项
|
||||
|
||||
- 不要把密码保存到仓库文件、脚本或聊天记录中。
|
||||
- 尽量避免使用 `StrictHostKeyChecking=no`。
|
||||
- 首次连接建议核验主机指纹;只有在临时环境自动化场景下,才考虑 `accept-new`。
|
||||
- 对于破坏性命令(如删除、关机、防火墙调整),必须先确认再执行,并展示将要运行的确切命令。
|
||||
|
||||
## 附带脚本
|
||||
|
||||
### `scripts/ssh_alias_setup.sh`
|
||||
|
||||
用于创建或更新 SSH 别名,并可选执行 `ssh-copy-id`,支持:
|
||||
|
||||
- 指定别名、主机、用户、端口、私钥
|
||||
- 自动生成默认的 `~/.ssh/id_ed25519`
|
||||
- 幂等更新 `~/.ssh/config`
|
||||
- 可选直接运行 `ssh-copy-id` 安装公钥
|
||||
- 使用 `--dry-run` 预览即将执行的动作
|
||||
|
||||
### `scripts/ssh_run.sh`
|
||||
|
||||
用于通过 SSH 执行远程命令,支持:
|
||||
|
||||
- 指定用户、端口、私钥
|
||||
- 设置连接超时
|
||||
- 启用 TTY
|
||||
- 使用 `sudo` 或 `sudo -n`
|
||||
- 使用 `--accept-new` 处理首次连接主机
|
||||
- 使用 `--dry-run` 预览实际将执行的命令
|
||||
|
||||
默认可读取这些环境变量:
|
||||
|
||||
- `REMOTE_USER`
|
||||
- `REMOTE_PORT`
|
||||
- `REMOTE_KEY`
|
||||
- `REMOTE_CONNECT_TIMEOUT`
|
||||
|
||||
### `scripts/ssh_copy.sh`
|
||||
|
||||
用于通过 `scp` 上传或下载文件,支持:
|
||||
|
||||
- `push` 上传文件到远程服务器
|
||||
- `pull` 从远程服务器下载文件
|
||||
- 递归复制目录
|
||||
- 指定用户、端口、私钥
|
||||
- 使用 `--accept-new`
|
||||
- 使用 `--dry-run` 预览命令
|
||||
|
||||
默认可读取这些环境变量:
|
||||
|
||||
- `REMOTE_USER`
|
||||
- `REMOTE_PORT`
|
||||
- `REMOTE_KEY`
|
||||
|
||||
## 参考资料
|
||||
|
||||
- `references/ssh-playbook.md`:SSH 安全建议、常见命令、主机指纹处理与故障排查说明。
|
||||
@@ -1,3 +0,0 @@
|
||||
interface:
|
||||
display_name: "Linux SSH Operator"
|
||||
short_description: "Operate Linux servers, SSH aliases, and passwordless login"
|
||||
@@ -1,103 +0,0 @@
|
||||
# SSH playbook (Linux server ops)
|
||||
|
||||
## Defaults and conventions
|
||||
|
||||
- Prefer SSH keys (ed25519) and `~/.ssh/config` aliases for repeatable runs.
|
||||
- Avoid putting passwords in files, prompts, or chat logs. If password auth is required, use an interactive terminal/TTY.
|
||||
- Start with read-only inspection, then apply changes, then verify.
|
||||
|
||||
Recommended env vars for wrappers:
|
||||
|
||||
- `REMOTE_USER`: default SSH user
|
||||
- `REMOTE_PORT`: default SSH port (usually 22)
|
||||
- `REMOTE_KEY`: path to identity file (private key)
|
||||
- `REMOTE_CONNECT_TIMEOUT`: connect timeout seconds
|
||||
|
||||
## SSH key setup (recommended)
|
||||
|
||||
Generate a new key:
|
||||
|
||||
```bash
|
||||
ssh-keygen -t ed25519 -C "codex" -f ~/.ssh/id_ed25519
|
||||
```
|
||||
|
||||
Copy the public key to the server:
|
||||
|
||||
```bash
|
||||
ssh-copy-id -i ~/.ssh/id_ed25519.pub -p 22 USER@SERVER_IP
|
||||
```
|
||||
|
||||
Add a host alias:
|
||||
|
||||
```sshconfig
|
||||
Host my-server
|
||||
HostName SERVER_IP
|
||||
Port 22
|
||||
User USER
|
||||
IdentityFile ~/.ssh/id_ed25519
|
||||
IdentitiesOnly yes
|
||||
```
|
||||
|
||||
## Common tasks
|
||||
|
||||
### Connectivity and OS info
|
||||
|
||||
```bash
|
||||
ssh my-server "whoami && hostname && uname -a"
|
||||
ssh my-server "cat /etc/os-release"
|
||||
```
|
||||
|
||||
### Disk and memory
|
||||
|
||||
```bash
|
||||
ssh my-server "df -h"
|
||||
ssh my-server "free -h"
|
||||
ssh my-server "du -sh /var/log/* | sort -h | tail"
|
||||
```
|
||||
|
||||
### Processes and ports
|
||||
|
||||
```bash
|
||||
ssh my-server "ps aux --sort=-%mem | head"
|
||||
ssh my-server "ss -lntp"
|
||||
```
|
||||
|
||||
### Logs (systemd)
|
||||
|
||||
```bash
|
||||
ssh my-server "journalctl -u SERVICE -n 200 --no-pager"
|
||||
ssh my-server "journalctl -u SERVICE -f --no-pager"
|
||||
```
|
||||
|
||||
### Services (systemd)
|
||||
|
||||
Status:
|
||||
|
||||
```bash
|
||||
ssh my-server "systemctl status SERVICE --no-pager"
|
||||
```
|
||||
|
||||
Restart (often needs sudo and TTY):
|
||||
|
||||
```bash
|
||||
ssh -tt my-server "sudo systemctl restart SERVICE"
|
||||
```
|
||||
|
||||
Non-interactive sudo (fails if a password prompt would be required):
|
||||
|
||||
```bash
|
||||
ssh my-server "sudo -n systemctl restart SERVICE"
|
||||
```
|
||||
|
||||
## Safer host key handling
|
||||
|
||||
- Prefer verifying the host key fingerprint out-of-band on first connect.
|
||||
- If you must automate first-connect for ephemeral hosts, use `StrictHostKeyChecking=accept-new` (OpenSSH 7.6+).
|
||||
- If you see a "host key changed" warning, treat it as a potential security incident until you confirm the change is expected.
|
||||
|
||||
## Troubleshooting quick hits
|
||||
|
||||
- `Permission denied (publickey)`: wrong user, wrong key, server missing your public key, or `sshd` settings.
|
||||
- `Connection timed out`: routing/firewall/security group, wrong port, server down.
|
||||
- `No route to host`: network path missing (VPN, subnet, ACL).
|
||||
|
||||
@@ -1,208 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
Configure an SSH alias and optionally install the current public key for passwordless login.
|
||||
|
||||
Usage:
|
||||
ssh_alias_setup.sh [options] ALIAS HOST
|
||||
|
||||
Options:
|
||||
-u, --user USER SSH user (default: root)
|
||||
-p, --port PORT SSH port (default: 22)
|
||||
-i, --key PATH Identity file (default: ~/.ssh/id_ed25519)
|
||||
--install-key Run ssh-copy-id after writing the alias
|
||||
--no-accept-new Disable StrictHostKeyChecking=accept-new
|
||||
--dry-run Print planned actions without changing files
|
||||
-h, --help Show help
|
||||
|
||||
Environment:
|
||||
SSH_CONFIG_FILE Override SSH config path for testing
|
||||
REMOTE_KEY Default key path
|
||||
REMOTE_PORT Default SSH port
|
||||
|
||||
Examples:
|
||||
ssh_alias_setup.sh bigmengya 192.168.1.233 --install-key
|
||||
ssh_alias_setup.sh -u ubuntu -p 2222 devbox 10.0.0.8
|
||||
USAGE
|
||||
}
|
||||
|
||||
expand_path() {
|
||||
local value="$1"
|
||||
case "$value" in
|
||||
~) printf '%s\n' "$HOME" ;;
|
||||
~/*) printf '%s/%s\n' "$HOME" "${value#~/}" ;;
|
||||
*) printf '%s\n' "$value" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
user="root"
|
||||
port="${REMOTE_PORT:-22}"
|
||||
key="${REMOTE_KEY:-$HOME/.ssh/id_ed25519}"
|
||||
accept_new=true
|
||||
install_key=false
|
||||
dry_run=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-u|--user)
|
||||
user="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
-p|--port)
|
||||
port="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
-i|--key)
|
||||
key="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--install-key)
|
||||
install_key=true
|
||||
shift
|
||||
;;
|
||||
--no-accept-new)
|
||||
accept_new=false
|
||||
shift
|
||||
;;
|
||||
--dry-run)
|
||||
dry_run=true
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
-*)
|
||||
echo "Unknown option: $1" >&2
|
||||
usage >&2
|
||||
exit 2
|
||||
;;
|
||||
*)
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ $# -lt 2 ]]; then
|
||||
usage >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
alias_name="$1"
|
||||
host="$2"
|
||||
key="$(expand_path "$key")"
|
||||
config_file="$(expand_path "${SSH_CONFIG_FILE:-$HOME/.ssh/config}")"
|
||||
ssh_dir="$(dirname "$config_file")"
|
||||
key_pub="${key}.pub"
|
||||
key_dir="$(dirname "$key")"
|
||||
|
||||
if $dry_run; then
|
||||
echo "Would ensure directory: $ssh_dir"
|
||||
echo "Would ensure key directory: $key_dir"
|
||||
else
|
||||
mkdir -p "$ssh_dir" "$key_dir"
|
||||
chmod 700 "$ssh_dir" "$key_dir"
|
||||
fi
|
||||
|
||||
if [[ ! -f "$key" ]]; then
|
||||
if $dry_run; then
|
||||
printf 'Would generate key: ssh-keygen -t ed25519 -C %q -f %q -N %q\n' "codex" "$key" ""
|
||||
else
|
||||
ssh-keygen -t ed25519 -C "codex" -f "$key" -N ""
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ ! -f "$key_pub" ]]; then
|
||||
echo "Public key not found: $key_pub" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if $dry_run; then
|
||||
cat <<DRYRUN
|
||||
Would write/update SSH alias:
|
||||
Host $alias_name
|
||||
HostName $host
|
||||
User $user
|
||||
Port $port
|
||||
ServerAliveInterval 30
|
||||
ServerAliveCountMax 3
|
||||
ConnectTimeout 10
|
||||
StrictHostKeyChecking $( $accept_new && printf 'accept-new' || printf 'ask' )
|
||||
IdentityFile $key
|
||||
IdentitiesOnly yes
|
||||
DRYRUN
|
||||
else
|
||||
python3 - "$config_file" "$alias_name" "$host" "$user" "$port" "$key" "$accept_new" <<'PY'
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
config_path = Path(sys.argv[1])
|
||||
alias_name = sys.argv[2]
|
||||
host = sys.argv[3]
|
||||
user = sys.argv[4]
|
||||
port = sys.argv[5]
|
||||
key = sys.argv[6]
|
||||
accept_new = sys.argv[7].lower() == 'true'
|
||||
|
||||
existing = config_path.read_text(encoding='utf-8') if config_path.exists() else ''
|
||||
lines = existing.splitlines()
|
||||
out: list[str] = []
|
||||
skip = False
|
||||
for line in lines:
|
||||
stripped = line.strip()
|
||||
if stripped.lower().startswith('host '):
|
||||
host_names = stripped[5:].split()
|
||||
if alias_name in host_names:
|
||||
skip = True
|
||||
continue
|
||||
skip = False
|
||||
out.append(line)
|
||||
continue
|
||||
if skip:
|
||||
continue
|
||||
out.append(line)
|
||||
|
||||
block = [
|
||||
f'Host {alias_name}',
|
||||
f' HostName {host}',
|
||||
f' User {user}',
|
||||
f' Port {port}',
|
||||
' ServerAliveInterval 30',
|
||||
' ServerAliveCountMax 3',
|
||||
' ConnectTimeout 10',
|
||||
f' StrictHostKeyChecking {"accept-new" if accept_new else "ask"}',
|
||||
f' IdentityFile {key}',
|
||||
' IdentitiesOnly yes',
|
||||
]
|
||||
text = '\n'.join(out).strip()
|
||||
if text:
|
||||
text += '\n\n'
|
||||
text += '\n'.join(block) + '\n'
|
||||
config_path.write_text(text, encoding='utf-8')
|
||||
PY
|
||||
chmod 600 "$config_file"
|
||||
fi
|
||||
|
||||
printf 'Alias ready: ssh %s\n' "$alias_name"
|
||||
printf 'Config file: %s\n' "$config_file"
|
||||
|
||||
if $install_key; then
|
||||
if ! command -v ssh-copy-id >/dev/null 2>&1; then
|
||||
echo 'ssh-copy-id is required but not installed.' >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! [ -t 0 ]; then
|
||||
echo 'Warning: no TTY detected; password prompt may fail. Prefer running with a terminal/TTY.' >&2
|
||||
fi
|
||||
if $dry_run; then
|
||||
printf 'Would run: ssh-copy-id -i %q %q\n' "$key_pub" "$alias_name"
|
||||
else
|
||||
ssh-copy-id -i "$key_pub" "$alias_name"
|
||||
fi
|
||||
fi
|
||||
@@ -1,131 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
Copy files via scp with consistent options.
|
||||
|
||||
Usage:
|
||||
ssh_copy.sh [options] push HOST LOCAL_PATH REMOTE_PATH
|
||||
ssh_copy.sh [options] pull HOST REMOTE_PATH LOCAL_PATH
|
||||
|
||||
Options:
|
||||
-u, --user USER Override SSH user (or set REMOTE_USER)
|
||||
-p, --port PORT SSH port (default: REMOTE_PORT or 22)
|
||||
-i, --key PATH Identity file (default: REMOTE_KEY)
|
||||
-r, --recursive Copy directories recursively
|
||||
--accept-new Set StrictHostKeyChecking=accept-new
|
||||
--dry-run Print the scp command that would run
|
||||
-h, --help Show help
|
||||
|
||||
Environment defaults:
|
||||
REMOTE_USER, REMOTE_PORT, REMOTE_KEY
|
||||
|
||||
Examples:
|
||||
ssh_copy.sh push my-server ./app.tar.gz /tmp/app.tar.gz
|
||||
ssh_copy.sh --user ubuntu pull 10.0.0.1 /var/log/syslog ./syslog
|
||||
USAGE
|
||||
}
|
||||
|
||||
port="${REMOTE_PORT:-22}"
|
||||
user="${REMOTE_USER:-}"
|
||||
key="${REMOTE_KEY:-}"
|
||||
|
||||
recursive=false
|
||||
accept_new=false
|
||||
dry_run=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-u|--user)
|
||||
user="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
-p|--port)
|
||||
port="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
-i|--key)
|
||||
key="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
-r|--recursive)
|
||||
recursive=true
|
||||
shift
|
||||
;;
|
||||
--accept-new)
|
||||
accept_new=true
|
||||
shift
|
||||
;;
|
||||
--dry-run)
|
||||
dry_run=true
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
-*)
|
||||
echo "Unknown option: $1" >&2
|
||||
usage >&2
|
||||
exit 2
|
||||
;;
|
||||
*)
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ $# -lt 4 ]]; then
|
||||
usage >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
direction="$1"
|
||||
shift
|
||||
|
||||
host="$1"
|
||||
shift
|
||||
|
||||
dest_host="$host"
|
||||
if [[ -n "$user" ]]; then
|
||||
host_no_user="${host#*@}"
|
||||
dest_host="${user}@${host_no_user}"
|
||||
fi
|
||||
|
||||
scp_opts=(-P "$port" -p)
|
||||
if [[ -n "$key" ]]; then
|
||||
scp_opts+=(-i "$key" -o "IdentitiesOnly=yes")
|
||||
fi
|
||||
if $recursive; then
|
||||
scp_opts+=(-r)
|
||||
fi
|
||||
if $accept_new; then
|
||||
scp_opts+=(-o "StrictHostKeyChecking=accept-new")
|
||||
fi
|
||||
|
||||
case "$direction" in
|
||||
push)
|
||||
local_path="$1"
|
||||
remote_path="$2"
|
||||
full_cmd=(scp "${scp_opts[@]}" "$local_path" "${dest_host}:${remote_path}")
|
||||
;;
|
||||
pull)
|
||||
remote_path="$1"
|
||||
local_path="$2"
|
||||
full_cmd=(scp "${scp_opts[@]}" "${dest_host}:${remote_path}" "$local_path")
|
||||
;;
|
||||
*)
|
||||
echo "Unknown direction: $direction (expected: push|pull)" >&2
|
||||
usage >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
if $dry_run; then
|
||||
printf '%q ' "${full_cmd[@]}"
|
||||
printf '\n'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
"${full_cmd[@]}"
|
||||
@@ -1,156 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
Run a remote command over SSH with consistent, script-friendly options.
|
||||
|
||||
Usage:
|
||||
ssh_run.sh [options] HOST -- COMMAND [ARG...]
|
||||
ssh_run.sh [options] HOST # interactive shell
|
||||
|
||||
Options:
|
||||
-u, --user USER Override SSH user (or set REMOTE_USER)
|
||||
-p, --port PORT SSH port (default: REMOTE_PORT or 22)
|
||||
-i, --key PATH Identity file (default: REMOTE_KEY)
|
||||
-t, --tty Force pseudo-tty allocation (useful for sudo prompts)
|
||||
--accept-new Set StrictHostKeyChecking=accept-new
|
||||
--sudo Prefix command with sudo --
|
||||
--sudo-non-interactive Prefix command with sudo -n -- (fails if password needed)
|
||||
--connect-timeout SEC Connect timeout (default: REMOTE_CONNECT_TIMEOUT or 10)
|
||||
--dry-run Print the ssh command that would run
|
||||
-h, --help Show help
|
||||
|
||||
Environment defaults:
|
||||
REMOTE_USER, REMOTE_PORT, REMOTE_KEY, REMOTE_CONNECT_TIMEOUT
|
||||
|
||||
Examples:
|
||||
ssh_run.sh --user ubuntu 10.0.0.1 -- uname -a
|
||||
ssh_run.sh --tty --sudo my-server -- systemctl restart nginx
|
||||
USAGE
|
||||
}
|
||||
|
||||
port="${REMOTE_PORT:-22}"
|
||||
user="${REMOTE_USER:-}"
|
||||
key="${REMOTE_KEY:-}"
|
||||
connect_timeout="${REMOTE_CONNECT_TIMEOUT:-10}"
|
||||
|
||||
tty=false
|
||||
accept_new=false
|
||||
sudo_mode=""
|
||||
dry_run=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-u|--user)
|
||||
user="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
-p|--port)
|
||||
port="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
-i|--key)
|
||||
key="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
-t|--tty)
|
||||
tty=true
|
||||
shift
|
||||
;;
|
||||
--accept-new)
|
||||
accept_new=true
|
||||
shift
|
||||
;;
|
||||
--sudo)
|
||||
sudo_mode="sudo"
|
||||
shift
|
||||
;;
|
||||
--sudo-non-interactive)
|
||||
sudo_mode="sudo-n"
|
||||
shift
|
||||
;;
|
||||
--connect-timeout)
|
||||
connect_timeout="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--dry-run)
|
||||
dry_run=true
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
-*)
|
||||
echo "Unknown option: $1" >&2
|
||||
usage >&2
|
||||
exit 2
|
||||
;;
|
||||
*)
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ $# -lt 1 ]]; then
|
||||
usage >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
host="$1"
|
||||
shift
|
||||
|
||||
dest="$host"
|
||||
if [[ -n "$user" ]]; then
|
||||
host_no_user="${host#*@}"
|
||||
dest="${user}@${host_no_user}"
|
||||
fi
|
||||
|
||||
ssh_opts=(
|
||||
-p "$port"
|
||||
-o "ConnectTimeout=${connect_timeout}"
|
||||
-o "ServerAliveInterval=30"
|
||||
-o "ServerAliveCountMax=3"
|
||||
)
|
||||
|
||||
if [[ -n "$key" ]]; then
|
||||
ssh_opts+=(-i "$key" -o "IdentitiesOnly=yes")
|
||||
fi
|
||||
|
||||
if $accept_new; then
|
||||
ssh_opts+=(-o "StrictHostKeyChecking=accept-new")
|
||||
fi
|
||||
|
||||
if $tty; then
|
||||
ssh_opts+=(-tt)
|
||||
fi
|
||||
|
||||
cmd=("$@")
|
||||
if [[ ${#cmd[@]} -gt 0 && "${cmd[0]}" == "--" ]]; then
|
||||
cmd=("${cmd[@]:1}")
|
||||
fi
|
||||
if [[ -n "$sudo_mode" && ${#cmd[@]} -gt 0 ]]; then
|
||||
if [[ "$sudo_mode" == "sudo-n" ]]; then
|
||||
cmd=("sudo" "-n" "--" "${cmd[@]}")
|
||||
else
|
||||
cmd=("sudo" "--" "${cmd[@]}")
|
||||
fi
|
||||
fi
|
||||
|
||||
full_cmd=(ssh "${ssh_opts[@]}" "$dest")
|
||||
if [[ ${#cmd[@]} -gt 0 ]]; then
|
||||
full_cmd+=("${cmd[@]}")
|
||||
fi
|
||||
|
||||
if $dry_run; then
|
||||
printf '%q ' "${full_cmd[@]}"
|
||||
printf '\n'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
"${full_cmd[@]}"
|
||||
Reference in New Issue
Block a user