chore: commit pending workspace updates
Some checks failed
CI / build-check-test (push) Has been cancelled

This commit is contained in:
2026-06-14 20:36:01 +08:00
parent 62e2866228
commit ad62015fe3
152 changed files with 338 additions and 23015 deletions

View File

@@ -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/` 之类的位置使用。
- 如果用户指定了固定命令名,按用户要求命名。

View File

@@ -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."

View File

@@ -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.

View File

@@ -1 +0,0 @@
* text=auto eol=lf

View File

@@ -1,4 +0,0 @@
__pycache__/
*.py[cod]
.venv/
venv/

View File

@@ -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. **已知问题与解决方案**
- 旧版 tea0.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 # 列出所有 IssueJSON 格式)
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 APIcurl
当 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/)

View File

@@ -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)
```

View File

@@ -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()

View File

@@ -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()

View File

@@ -1 +0,0 @@
* text=auto eol=lf

View File

@@ -1,4 +0,0 @@
__pycache__/
*.py[cod]
.venv/
venv/

View File

@@ -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 CLIgh技能
## 何时启用
用户提到:`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 Actionsworkflow / 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)

View File

@@ -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` 版本文档为准。

View File

@@ -1,84 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
模块GitHub CLIgh跨平台环境检测脚本。
说明:跨平台单文件,仅依赖 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()

View File

@@ -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.

View File

@@ -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 安全建议、常见命令、主机指纹处理与故障排查说明。

View File

@@ -1,3 +0,0 @@
interface:
display_name: "Linux SSH Operator"
short_description: "Operate Linux servers, SSH aliases, and passwordless login"

View File

@@ -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).

View File

@@ -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

View File

@@ -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[@]}"

View File

@@ -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[@]}"