
Skill Publisher
- 8 installs
- 33 repo stars
- Updated June 11, 2026
- joeseesun/qiaomu-skill-publisher
Publish Claude Code skills with packaging, versioning, and distribution to npm and skill registries.
About
Automates skill publication workflow including packaging, version management, and registry upload. Handles both local development and production publishing.
- Automated packaging and versioning for skill releases
- Distribution to npm and Claude Code registries
Skill Publisher by the numbers
- 8 all-time installs (skills.sh)
- Ranked #545 of 782 Skill Development skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/joeseesun/qiaomu-skill-publisher --skill skill-publisherAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 8 |
|---|---|
| repo stars | ★ 33 |
| Last updated | June 11, 2026 |
| Repository | joeseesun/qiaomu-skill-publisher ↗ |
What it does
Publish Claude Code skills with packaging, versioning, and distribution to npm and skill registries.
Files
Skill Publisher
一键将 agent skill 发布到 GitHub,自动完成验证、README 质量检查、补全、推送和真实安装验证。
前置条件
ghCLI 已安装且已登录(gh auth status)- Skill 目录包含有效的
SKILL.md(含 YAML frontmattername+description) - 优先使用
/Users/joe/.agents/skills/<name>作为正式源目录;.codex/.claude可作为兼容入口。
发布流程
当用户要求发布 skill 时,运行发布脚本:
python3 ~/.agents/skills/qiaomu-skill-publisher/scripts/publish_skill.py <skill_dir>确定 skill 目录
- 如果用户说"发布这个 skill"且当前在某个 skill 目录 → 用当前目录
- 如果用户指定了 skill 名称 → 优先在
~/.agents/skills/下查找 - 如果不确定 → 问用户要发布哪个 skill
脚本自动完成的步骤
1. 验证 SKILL.md 的 YAML frontmatter(name + description) 2. 检查 gh CLI 就绪状态 3. 创建 LICENSE(MIT,如果缺少) 4. 生成或检查 README.md:生成时不再写 TODO 模板;已存在时拦截明显占位符 5. 识别仓库名:优先使用当前 origin 仓库名,避免把 skill-publisher 误发到错误 repo 6. 初始化 git(如果需要) 7. 创建或更新 GitHub 公开仓库并推送 8. 验证 npx skills add --list 可发现,并在临时目录真实安装
参数选项
| 参数 | 说明 |
|---|---|
--private | 创建私有仓库(默认公开) |
--dry-run | 仅检查,不实际发布 |
--skip-verify | 跳过 npx skills 验证 |
--github-user USER | 指定 GitHub 用户名(默认自动获取) |
--repo-name NAME | 指定 GitHub 仓库名;默认优先使用当前 origin 仓库名,否则使用 skill name |
--no-symlink | 跳过同步 ~/.agents/skills/ 实体目录 |
自动同步 ~/.agents/skills/ 实体目录
发布成功后,脚本可自动把 skill 同步到 ~/.agents/skills/<name> 作为实体目录。
这个目录是通用 Agent Skills 标准目录,以下工具会自动读取: OpenCode、Codex CLI、Cursor、Gemini CLI、GitHub Copilot、Amp、Cline、Warp 等。
一次发布,多工具共享,无需重复配置。
如果当前发布源已经是 ~/.agents/skills/<name>,脚本会跳过同步,避免误删自己的源目录。若 skill name 与 repo name 不一致,或者你不想产生本地副本,发布时加 --no-symlink。
⚠️ SKILL.md YAML 安全规则(发布前必查)
npx skills 使用严格 YAML 解析器,以下写法会导致安装失败(报 "No valid skills found"):
| ❌ 错误写法 | ✅ 正确写法 |
|---|---|
description: 含有 "引号" 的文字 | 改用 `\ |
description: 含单引号'的文字 | 改用 `\ |
description: 含冒号: 的文字 | 改用 `\ |
最安全的 description 写法:
description: |
描述放这里,可以随意包含 "双引号"、'单引号'、冒号: 等特殊字符
触发词: 用户说...时触发脚本已内置 YAML 严格校验(pyyaml),会在发布前捕获这类错误并给出修复提示。
更新已发布的 skill
对已有 GitHub 仓库的 skill 再次运行同一命令,脚本会检测到仓库已存在,自动 commit + push 更新。
使用示例
用户:发布 yt-search-download 这个 skill
执行:python3 ~/.agents/skills/qiaomu-skill-publisher/scripts/publish_skill.py ~/.agents/skills/yt-search-download
用户:把当前 skill 发到 GitHub
执行:python3 ~/.agents/skills/qiaomu-skill-publisher/scripts/publish_skill.py .
用户:先检查一下能不能发布
执行:python3 ~/.agents/skills/qiaomu-skill-publisher/scripts/publish_skill.py <dir> --dry-run
用户:当前 skill name 与仓库名不同,明确发布到 qiaomu-skill-publisher
执行:python3 ~/.agents/skills/qiaomu-skill-publisher/scripts/publish_skill.py <dir> --repo-name qiaomu-skill-publisherREADME 质量检查(发布前必做)
脚本只在 README 不存在时自动生成一个发布页模板。发布前,必须人工检查/撰写 README,确保它对陌生用户有价值。
最新标准:自动生成的 README 不应包含 TODO、特性 1、[问题 1] 这类占位符;已有 README 如果包含明显占位内容,发布会失败。
README 必须包含的 7 个要素
1. 价值主张(Hook):第一段就让用户明白"这能解决我什么问题",用具体场景描述,避免抽象描述 2. 首屏证据:产品截图、输出样例、真实生成物或短 Demo 必须出现在 README 第一屏附近;Web 项目优先放完整产品截图 3. 动态可信信号:GitHub stars、forks、issues、last commit、license、deploy/install 按钮等徽章要放在顶部;适合公开传播的项目加 Star History 4. 前置条件清单(checkbox 格式):用 - [ ] 列出所有依赖,让用户逐一确认。每条都要写清楚怎么装,不能只说"需要 xxx" 5. 完整安装步骤:编号步骤,细到小白能跟着做。每步都提供验证命令(如 --version) 6. 自然语言使用示例:展示 2-3 个用户真实会说的句子,让人一眼看懂怎么触发 7. 致谢原作者:如果 skill 基于第三方工具/库,必须注明原项目链接和作者 8. 必要的风险/限制说明:写操作、账号相关、费用相关的风险需明确告知 9. 常见问题/Troubleshooting:至少列出 3 个常见报错和解决方法,降低用户放弃率
Web 项目 README 发布标准
参考 nexu-io/open-design 这类高转化 README,Web 项目不能只写安装命令,必须像产品发布页一样给用户一个可判断的首屏。
首屏顺序建议:
1. 项目名 + 一句话 Hook 2. 主要 CTA:Deploy with Vercel / Live Demo / Install,按钮要能直接用真实仓库 URL 3. GitHub 动态徽章:stars、forks、issues、last commit、license、build 状态(如果有) 4. 产品截图:docs/assets/product-screenshot.png,alt 写清楚截图内容 5. 3-6 个核心功能点,不写泛泛的"高效/现代/优雅"
Web 项目必须优先补齐:
docs/assets/product-screenshot.png:首屏或核心工作流截图docs/assets/下的样例输出:如图片生成网站放 3-6 个代表性生成图- README 中的样例表格:展示输入、输出、关键差异或使用场景
scripts/capture-screenshots.*或等价命令:能本地启动网站后自动刷新 README 截图- 部署说明:Vercel/Netlify/Docker 等入口必须使用真实仓库 URL 和最少步骤
推荐 README 模块:
# Project Name
> 一句话说明用户会得到什么。
[Deploy with Vercel] [Live Demo] [Stars] [Forks] [Last commit] [License]
<img src="docs/assets/product-screenshot.png" alt="..." />
## 为什么值得用
## 样例输出
## 一键部署
## 本地开发
## 自动更新截图
## 数据/生成流程
## Star History
## Troubleshooting自动截图要求:
- 有前端 UI 的项目,优先用 Playwright/Puppeteer 生成 README 截图,不手工截屏
- 截图脚本应允许通过环境变量指定 URL,例如
SCREENSHOT_URL=http://127.0.0.1:3000 npm run capture:screenshots - 截图文件放进
docs/assets/,README 使用相对路径引用 - 截图脚本是发布资产生成,不等同于功能验收;除非用户要求,否则不要把它包装成完整 E2E 测试
语言原则
默认写中英双语 README,但中文在前。 中文用户是主要受众,英文是给国际用户的补充。
双语结构(中文在前):
# skill-name
> 中文一行价值主张
> One-line English hook
**[中文](#中文) | [English](#english)**
---
<a name="中文"></a>
## 中文
[完整中文内容]
---
<a name="english"></a>
## English
[完整英文内容]README 吸引力写法(最重要)
目标不是"让人看懂",而是"让人想装"。 每一段都要回答用户心里那个问题:这和我有什么关系?
高吸引力 README 的结构:
1. 第一句话抓痛点:描述用户现在的痛,不是你的方案。一句话,精准,不废话。
- ✅ "你问 AI 一个难问题,它给你一个答案,一个角度。"
- ❌ "这是一个多视角推理工具,基于 arXiv:2605.02396..."
2. 一句话翻转:立刻给出"用了这个之后"的对比。
- ✅ "HeavySkill 让 K 个独立 AI 并行思考同一个问题,然后让 Codex 主持讨论,综合出一个比任何单一答案都更扎实的结论。"
3. 具体输出预览:告诉用户他会得到什么。不要说"生成报告",要说"生成一个 Medium 杂志风格的 HTML 报告,有每个视角的推理过程,有讨论中发现的盲点,有一个加粗的最终判断"。
4. 首屏截图或样例:让用户先看到真实结果,再决定是否继续读。Web 项目放产品截图,生成类项目放代表性输出,CLI/Skill 放实际终端输出或生成文档片段。
5. 一行安装:最低摩擦。不要说"先 clone 再 cd 再 npm install"。
npx skills add username/skill-name6. 自然语言触发示例:让用户知道怎么用。不是命令,是他会真实说出的话。
- "讨论一下 React vs Vue"
- "think harder: 微服务还是单体架构"
7. 前置条件清单(checkbox 格式):放在安装之后,不要放开头吓人。用 - [ ] 格式让用户逐项确认。
8. 动态项目状态:公开 GitHub 项目顶部放 stars/forks/issues/last commit/license 徽章;有增长叙事时补 Star History。
9. Troubleshooting 表格:至少 3 条,解决用户放弃的最后一公里。
让人想装的写作要领:
- 短句。每个 idea 一行,不要堆在同一段。
- 具体胜于抽象。"36k+ GitHub Stars、900 万/月 npm 下载" 比 "流行的开源项目" 有说服力。
- 让用户想象"用了之后"的场景,而不是描述技术实现。
- 风险或限制放最后,不要一开始就说"注意:这个需要..."。
README 小白友好度检查
发布前逐项核对:
- [ ] 第一句话是痛点,不是功能描述
- [ ] 有"用了之后"的具体场景描述
- [ ] 安装命令一行搞定,放在显眼位置
- [ ] Web 项目有首屏产品截图,生成类项目有代表性输出样例
- [ ] GitHub 项目顶部有动态徽章,公开项目有 Star History 或 repo 活跃度入口
- [ ] Web 项目有自动截图命令,截图在
docs/assets/ - [ ] 前置条件用 checkbox 格式,每条说明怎么安装
- [ ] 使用示例是用户真实会说的话,不是技术命令
- [ ] 有 Troubleshooting 表格(问题 → 解决方法)
README 常见坏味道(避免)
- ❌ 直接把 SKILL.md 的 AI 指令(Output Formatting Rules、Rule:...)放进 README
- ❌ 第一段全是技术描述,没有用户能感受到的价值
- ❌ "基于 arXiv:XXXX..." 开头 — 用户不在乎论文,在乎结果
- ❌ 前置要求放开头,吓跑用户
- ❌ 安装步骤超过 3 步(如果超过了,说明你没想清楚怎么简化)
- ❌ 没有 Troubleshooting,用户遇到问题只能自己摸索
- ❌ Web 项目没有截图,或者截图过期、无法复现
- ❌ Deploy 按钮仍指向
your-org/your-repo这类占位 URL
发布工作流
1. 读取 SKILL.md,理解 skill 的功能和目标用户
2. 判断语言:默认中文,只在明确国际化需求时写双语
3. 检查 README.md 是否存在,若存在则评估吸引力(不只是完整度)
4. 若 README 不够吸引人,先重写再发布
5. README 确认后,再运行发布脚本
6. 如果当前仓库已有 `origin`,确认脚本使用的是 `origin` 仓库名;否则用 `--repo-name` 明确指定
7. 发布后必须验证 `npx skills add <user>/<repo> --list`,并尽量做临时目录真实安装发布完成后
向用户展示:
- GitHub 仓库 URL
- 安装命令:
npx skills add <user>/<skill-name> - 验证结果
MIT License
Copyright (c) 2026 joeseesun
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
qiaomu-skill-publisher
你已经把一个工作流沉淀成 skill,下一步最容易卡住的不是代码,而是发布:README 太像内部说明、YAML 一点点写错、仓库名发错、最后还没人验证能不能安装。
qiaomu-skill-publisher turns a local agent skill into a public GitHub repo with a product-style README and real npx skills add verification.<p align="center"> <a href="https://github.com/joeseesun/qiaomu-skill-publisher/stargazers"><img alt="Stars" src="https://img.shields.io/github/stars/joeseesun/qiaomu-skill-publisher?style=for-the-badge&logo=github" /></a> <a href="https://github.com/joeseesun/qiaomu-skill-publisher/network/members"><img alt="Forks" src="https://img.shields.io/github/forks/joeseesun/qiaomu-skill-publisher?style=for-the-badge&logo=github" /></a> <a href="https://github.com/joeseesun/qiaomu-skill-publisher/issues"><img alt="Issues" src="https://img.shields.io/github/issues/joeseesun/qiaomu-skill-publisher?style=for-the-badge&logo=github" /></a> <a href="https://github.com/joeseesun/qiaomu-skill-publisher/commits/main"><img alt="Last commit" src="https://img.shields.io/github/last-commit/joeseesun/qiaomu-skill-publisher?style=for-the-badge&logo=git" /></a> <a href="LICENSE"><img alt="License" src="https://img.shields.io/badge/license-MIT-blue.svg?style=for-the-badge" /></a> </p>
中文 | English
npx skills add joeseesun/qiaomu-skill-publisher为什么值得用
一个 skill 真正可复用,不是本地能跑就结束。
它还需要:
- GitHub 仓库能被别人找到
- README 第一眼让人想装
SKILL.mdfrontmatter 能被严格 YAML 解析- 安装命令一行可用
- 发布后真的能通过
npx skills add发现和安装 - 本地
.agents、.codex、.claude入口别互相打架
qiaomu-skill-publisher 把这些发布步骤打包成一个可验证流程。
它会做什么
- 验证
SKILL.md的name和description - 用 PyYAML 做严格 frontmatter 检查
- 创建 MIT LICENSE
- 生成不带 TODO 占位符的 README 发布页
- 检查已有 README 是否还残留占位内容
- 区分
skill name和 GitHubrepo name - 优先使用当前 git
origin仓库名,避免推错 repo - 创建或更新 GitHub 公开仓库
- 跑
npx skills add <user>/<repo> --list - 在临时目录真实安装一次,确认
SKILL.md落盘 - 可把 skill 同步到
~/.agents/skills/<name>实体目录
快速开始
发布一个 skill:
python3 ~/.agents/skills/qiaomu-skill-publisher/scripts/publish_skill.py ~/.agents/skills/my-skill先检查,不发布:
python3 ~/.agents/skills/qiaomu-skill-publisher/scripts/publish_skill.py ~/.agents/skills/my-skill --dry-runskill name 和仓库名不一致时,明确仓库名:
python3 ~/.agents/skills/qiaomu-skill-publisher/scripts/publish_skill.py ~/.agents/skills/qiaomu-skill-publisher --repo-name qiaomu-skill-publisher --no-symlink发布后用户安装:
npx skills add joeseesun/my-skill你可以这样说
- “把这个 skill 发布到 GitHub。”
- “先检查这个 skill 能不能发布。”
- “重写 README,让它更吸引人,然后发布。”
- “更新 qiaomu-goal-meta-skill 到 GitHub,并验证可安装。”
- “这个 skill name 和 repo name 不一样,发布到 qiaomu-skill-publisher。”
发布流程
flowchart LR
A["本地 skill 目录"] --> B["验证 SKILL.md"]
B --> C["检查 README 质量"]
C --> D["识别 repo name"]
D --> E["提交并推送 GitHub"]
E --> F["npx --list 发现验证"]
F --> G["临时目录真实安装"]
G --> H["输出仓库和安装命令"]README 发布页标准
这个 publisher 的经验来自多次 qiaomu skill 发布,尤其是 qiaomu-goal-meta-skill 的 README 重写。
好 README 不只是“说明功能”。
它要让陌生人愿意安装。
推荐首屏:
1. 痛点:用户现在为什么难受 2. 翻转:用了这个 skill 之后有什么不同 3. 一行安装命令 4. 真实输出样例或工作流片段 5. 3-6 个具体能力点 6. 前置要求和 Troubleshooting 7. 作者、版权、风险边界
脚本会拦截这些坏味道:
TODO特性 1[问题 1][解决方案]your-orgyour-repo- 未替换的
product-screenshot.png
参数
| 参数 | 说明 |
|---|---|
--github-user USER | 指定 GitHub 用户名;默认优先使用当前 origin owner,否则使用 gh api user |
--repo-name NAME | 指定 GitHub 仓库名;默认优先使用当前 origin repo,否则使用 skill name |
--private | 创建私有仓库,默认公开 |
--dry-run | 只检查,不发布 |
--skip-verify | 跳过 npx skills 验证 |
--no-symlink | 跳过同步 ~/.agents/skills 实体目录 |
前置要求
- [ ] 已安装 GitHub CLI:
brew install gh - [ ] 已登录 GitHub CLI:
gh auth status - [ ] 已安装 Python 3.9+
- [ ] 已安装 Node.js 和
npx - [ ] skill 目录包含有效
SKILL.md - [ ] 发布前已检查 README 中没有密钥、私有路径、账号信息或未替换占位符
关键细节
repo name 和 skill name 可以不同
例如这个仓库是 qiaomu-skill-publisher,但 SKILL.md 里的 name 是 skill-publisher。
脚本会优先读取当前 git origin,避免把更新误推到 joeseesun/skill-publisher。
必要时使用:
--repo-name qiaomu-skill-publisher验证不是只看 --list
脚本会先跑:
npx skills add <user>/<repo> --list然后在临时目录真实安装:
npx skills add <user>/<repo> --skill <skill-name>确认 .agents/skills/<skill-name>/SKILL.md 真实落盘后才算过。
本地同步不会自删
如果发布源目录已经是 ~/.agents/skills/<name>,脚本会跳过同步,避免删除自己的源目录。
如果你从一个仓库名和 skill name 不一致的目录发布,并且不想产生本地副本,用:
--no-symlinkTroubleshooting
| 问题 | 原因 | 解决方法 |
|---|---|---|
gh: command not found | 没装 GitHub CLI | 运行 brew install gh && gh auth login |
No valid skills found | SKILL.md frontmatter 不是严格 YAML | 用 `description: |
| 发布到了错误仓库 | skill name 和 repo name 混用 | 检查 git remote -v,或传 --repo-name |
| README 质量检查失败 | 还残留 TODO 或占位符 | 把 README 改成真实痛点、样例和安装说明 |
| npx 真实安装失败 | repo 可见但 skill 未正确解析或路径不对 | 先跑 --list 看 skill name,再检查 SKILL.md |
| 本地出现重复 skill | 自动同步创建了实体副本 | 下次发布用 --no-symlink,或清理不需要的副本 |
License
MIT
Copyright (c) 向阳乔木 X: https://x.com/vista8 GitHub: https://github.com/joeseesun/
<a name="english"></a>
English
qiaomu-skill-publisher publishes a local agent skill to GitHub and verifies that it can be discovered and installed through npx skills add.
Install:
npx skills add joeseesun/qiaomu-skill-publisherIt focuses on:
- strict
SKILL.mdYAML validation - attractive product-page README generation
- README placeholder checks
- repo-name and skill-name separation
- GitHub repo creation or update
npx skills add --listdiscovery verification- real install verification in a temporary directory
- safe local
~/.agents/skillssync
Author:
Copyright (c) 向阳乔木 X: https://x.com/vista8 GitHub: https://github.com/joeseesun/
#!/usr/bin/env python3
"""
Skill Publisher — 检查并发布 Claude Code Skill 到 GitHub
用法:
python3 publish_skill.py <skill_dir> [--github-user USER] [--public/--private] [--dry-run]
流程:
1. 验证 SKILL.md (YAML frontmatter)
2. 检查/创建 LICENSE
3. 生成 README.md
4. 初始化 git (如需)
5. 创建 GitHub repo + push
6. 验证 npx skills 可发现
"""
import os
import sys
import re
import subprocess
import argparse
import json
import datetime
import shutil
import tempfile
def run(cmd, capture=True, check=True, cwd=None):
"""Run a shell command and return stdout."""
if isinstance(cmd, list):
r = subprocess.run(cmd, capture_output=capture, text=True, cwd=cwd)
else:
r = subprocess.run(cmd, shell=True, capture_output=capture, text=True, cwd=cwd)
if check and r.returncode != 0:
return None
return r.stdout.strip() if capture else ""
def check_prerequisites():
"""Check gh CLI is available and authenticated."""
if not run("which gh"):
print("[错误] 未找到 gh CLI。安装方式: brew install gh", file=sys.stderr)
return False
auth = run("gh auth status 2>&1", check=False)
if auth is None or "not logged" in (auth or ""):
print("[错误] gh 未登录。运行: gh auth login", file=sys.stderr)
return False
return True
def parse_yaml_frontmatter(skill_md_path):
"""Extract name and description from SKILL.md YAML frontmatter.
Returns (name, desc, yaml_error). yaml_error is None if parsing succeeded.
Uses pyyaml for strict validation (same parser family as npx skills CLI).
Falls back to regex if pyyaml is unavailable.
"""
with open(skill_md_path, "r") as f:
content = f.read()
m = re.match(r"^---\s*\n(.*?)\n---", content, re.DOTALL)
if not m:
return None, None, "找不到 YAML frontmatter(需要 --- ... --- 包裹)"
yaml_block = m.group(1)
try:
import yaml
try:
data = yaml.safe_load(yaml_block)
except yaml.YAMLError as e:
err_line = str(e).split("\n")[0]
return None, None, (
f"YAML 语法错误: {err_line}\n"
" 常见原因: description 含未转义的引号或特殊字符\n"
" 修复方法: 改用 | 块标量格式:\n"
" description: |\n"
" 描述文字,可随意包含 \"引号\"、'单引号'、冒号: 等"
)
if not isinstance(data, dict):
return None, None, "frontmatter 解析结果不是 dict"
name = data.get("name")
desc = data.get("description")
if isinstance(desc, str):
desc = " ".join(desc.split()) # normalize whitespace
return name, desc, None
except ImportError:
pass # pyyaml not installed, fall back to regex
# Regex fallback (less strict, may miss YAML errors)
name_m = re.search(r"^name:\s*(.+)$", yaml_block, re.MULTILINE)
name = name_m.group(1).strip().strip("'\"") if name_m else None
desc = None
desc_m = re.search(r"^description:\s*[|>]\s*\n((?:[ \t]+.+\n?)+)", yaml_block, re.MULTILINE)
if desc_m:
lines = desc_m.group(1).split("\n")
desc = " ".join(line.strip() for line in lines if line.strip())
else:
desc_m = re.search(r"^description:\s*(.+)$", yaml_block, re.MULTILINE)
if desc_m:
desc = desc_m.group(1).strip().strip("'\"")
return name, desc, None
def get_github_user():
"""Get current GitHub username."""
return run("gh api user --jq '.login'")
def get_origin_repo(skill_dir):
"""Return (owner, repo) parsed from git origin, if present."""
remote = run("git remote get-url origin 2>/dev/null", cwd=skill_dir, check=False)
if not remote:
return None, None
patterns = [
r"github\.com[:/](?P<owner>[^/]+)/(?P<repo>[^/]+?)(?:\.git)?$",
r"https://github\.com/(?P<owner>[^/]+)/(?P<repo>[^/]+?)(?:\.git)?$",
]
for pattern in patterns:
match = re.search(pattern, remote)
if match:
return match.group("owner"), match.group("repo")
return None, None
def validate_skill(skill_dir):
"""Validate skill directory structure."""
errors = []
skill_md = os.path.join(skill_dir, "SKILL.md")
if not os.path.exists(skill_md):
errors.append("缺少 SKILL.md")
return errors, None, None
name, desc, yaml_error = parse_yaml_frontmatter(skill_md)
if yaml_error:
errors.append(yaml_error)
return errors, None, None
if not name:
errors.append("SKILL.md 缺少 YAML frontmatter 中的 name 字段")
if not desc:
errors.append("SKILL.md 缺少 YAML frontmatter 中的 description 字段")
if desc and len(desc) < 20:
errors.append(f"description 太短 ({len(desc)} 字符),建议至少 50 字符")
return errors, name, desc
def ensure_license(skill_dir, github_user):
"""Create MIT LICENSE if missing."""
license_path = os.path.join(skill_dir, "LICENSE")
if os.path.exists(license_path):
return False
year = datetime.datetime.now().year
# Try to get full name from git config
full_name = run("git config user.name") or github_user
content = f"""MIT License
Copyright (c) {year} {full_name}
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
with open(license_path, "w") as f:
f.write(content)
return True
def extract_user_facing_sections(body):
"""
Extract user-facing sections from SKILL.md body.
Skips AI-only sections like output formatting rules, internal rules, etc.
AI-only sections to skip (case-insensitive patterns):
- "Output Formatting Rules"
- Lines starting with "Rule:" or "**Rule:"
- Sections about internal AI behavior
User-facing sections to keep:
- Quick Examples / 快速示例 / 示例
- Commands / 命令
- Usage / 使用方法
- Features / 功能
- 支持的平台 / Supported sites
"""
AI_SECTION_PATTERNS = [
r"output formatting rules",
r"requirements", # usually "Chrome open with..." which IS user-facing, keep it
]
# Actually let's keep requirements — it's important for users.
# Only skip pure AI-instruction sections:
SKIP_SECTION_TITLES = {
"output formatting rules",
"output formatting",
"formatting rules",
}
lines = body.split("\n")
result_lines = []
skip_section = False
current_h2 = ""
for line in lines:
# Detect h2/h3 headings
h2_match = re.match(r"^##\s+(.+)$", line)
if h2_match:
current_h2 = h2_match.group(1).strip().lower()
skip_section = current_h2 in SKIP_SECTION_TITLES
if skip_section:
continue
# Skip lines starting with "Rule:" (AI-facing rules)
if re.match(r"^\*?\*?Rule:", line):
continue
if skip_section:
continue
result_lines.append(line)
return "\n".join(result_lines).strip()
def generate_readme(skill_dir, name, repo_name, desc, github_user):
"""Generate README.md from SKILL.md content."""
readme_path = os.path.join(skill_dir, "README.md")
if os.path.exists(readme_path):
return False
# Build tagline from description (first sentence)
if "。" in desc:
tagline = desc.split("。")[0] + "。"
elif ". " in desc:
tagline = desc.split(". ")[0] + "."
else:
tagline = desc[:120]
readme = f"""# {repo_name}
<p align="center">
<a href="https://github.com/{github_user}/{repo_name}/stargazers"><img alt="Stars" src="https://img.shields.io/github/stars/{github_user}/{repo_name}?style=for-the-badge&logo=github" /></a>
<a href="https://github.com/{github_user}/{repo_name}/network/members"><img alt="Forks" src="https://img.shields.io/github/forks/{github_user}/{repo_name}?style=for-the-badge&logo=github" /></a>
<a href="https://github.com/{github_user}/{repo_name}/issues"><img alt="Issues" src="https://img.shields.io/github/issues/{github_user}/{repo_name}?style=for-the-badge&logo=github" /></a>
<a href="https://github.com/{github_user}/{repo_name}/commits/main"><img alt="Last commit" src="https://img.shields.io/github/last-commit/{github_user}/{repo_name}?style=for-the-badge&logo=git" /></a>
<a href="LICENSE"><img alt="License" src="https://img.shields.io/badge/license-MIT-blue.svg?style=for-the-badge" /></a>
</p>
> {tagline}
> Install this agent skill with one command, then trigger it with natural language in Codex, Claude Code, or another skills-compatible agent.
```bash
npx skills add {github_user}/{repo_name}
```
**中文** | [English](#english)
## 为什么值得用
很多 skill 发布失败,不是功能不够好。
而是 README 太像内部说明、安装路径不清、YAML frontmatter 有坑,或者发布后没有验证能不能被 `npx skills add` 发现。
这个 skill 会把可复用工作流包装成一个公开可安装的 agent skill,并把发布结果验证到可复制安装命令为止。
## 你会得到
- 一个带 `SKILL.md`、README、LICENSE 的 GitHub 仓库
- 严格 YAML frontmatter 校验
- README 发布页,不是内部指令转储
- `npx skills add` 可发现性和真实安装验证
- 本地 `~/.agents/skills` 兼容同步
- 清楚的仓库 URL、安装命令和验证结果
## 安装
```bash
npx skills add {github_user}/{repo_name}
```
安装后确认:
```bash
test -f ~/.agents/skills/{name}/SKILL.md
```
## 你可以这样说
- "把这个 skill 发布到 GitHub。"
- "先检查一下这个 skill 能不能发布。"
- "重写 README,让它更像公开产品页,然后发布。"
- "更新已发布的 skill,并验证 npx skills add 可安装。"
## 前置要求
- [ ] 已安装 GitHub CLI:`brew install gh`
- [ ] 已登录 GitHub CLI:`gh auth status`
- [ ] 已安装 Python 3.9+
- [ ] skill 目录包含有效的 `SKILL.md`
- [ ] 公开发布前已检查 README 中没有密钥、私有路径或内部账号信息
## Skill 摘要
{desc}
## 发布质量检查
发布前至少确认:
- README 第一屏说清楚用户痛点和安装命令
- README 有真实使用示例,不只是参数说明
- README 没有 `TODO`、`特性 1`、`[问题 1]` 这类占位符
- 如果 repo 已经有 `origin`,发布目标使用现有仓库名,而不是误用 skill name
- 发布后通过 `npx skills add {github_user}/{repo_name} --list`
- 最好再做一次临时目录真实安装
## Troubleshooting
| 问题 | 原因 | 解决方法 |
|---|---|---|
| `gh` 不可用 | GitHub CLI 没装或没登录 | 运行 `brew install gh && gh auth login` |
| `npx skills add` 找不到 skill | `SKILL.md` frontmatter 无效或 repo/path 错误 | 先运行发布脚本 dry-run,修复 YAML |
| 发布到了错误仓库 | skill name 和 repo name 混用 | 使用现有 `origin`,或传 `--repo-name` |
| README 看起来像内部文档 | 直接复制了 `SKILL.md` | 重写成痛点、样例、安装、风险和排障结构 |
| 本地 `.agents` 被重复复制 | 从非规范目录发布但自动同步开启 | 使用 `--no-symlink` 或先确认目标目录 |
## License
MIT
Copyright (c) 向阳乔木
X: https://x.com/vista8
GitHub: https://github.com/joeseesun/
<a name="english"></a>
## English
{repo_name} packages and publishes an agent skill to GitHub, then verifies that it can be discovered and installed through `npx skills add`.
Install:
```bash
npx skills add {github_user}/{repo_name}
```
It focuses on:
- strict `SKILL.md` YAML validation
- GitHub repository creation or update
- product-page README guidance
- repo-name and skill-name separation
- `npx skills add` discovery and install verification
- safe local agent-skill sync
Copyright (c) 向阳乔木
X: https://x.com/vista8
GitHub: https://github.com/joeseesun/
"""
with open(readme_path, "w") as f:
f.write(readme)
return True
def check_readme_quality(skill_dir):
"""Block obvious placeholder READMEs before publishing."""
readme_path = os.path.join(skill_dir, "README.md")
if not os.path.exists(readme_path):
return []
with open(readme_path, "r") as f:
content = f.read()
placeholder_patterns = [
r"<!--\s*TODO",
r"特性\s*1[::]描述",
r"场景\s*1[::]\[场景名称\]",
r"你说[::]\"\[用户的自然语言输入\]\"",
r"AI 做[::]\[AI 的具体操作步骤\]",
r"Q:\s*\[问题\s*\d+\]",
r"\*\*A:\*\*\s*\[解决方案\]",
r"your-org/your-repo",
r"docs/assets/product-screenshot\.png",
r"(在此补充",
]
errors = []
for pattern in placeholder_patterns:
if re.search(pattern, content, flags=re.IGNORECASE):
errors.append(f"README.md 仍包含占位内容: {pattern}")
return errors
def init_git(skill_dir):
"""Initialize git repo if needed."""
git_dir = os.path.join(skill_dir, ".git")
if os.path.isdir(git_dir):
return False
run(f"git init", cwd=skill_dir)
return True
def create_and_push(skill_dir, repo_name, desc, github_user, public=True):
"""Create GitHub repo and push."""
visibility = "--public" if public else "--private"
# Check if repo already exists
existing = run(f"gh repo view {github_user}/{repo_name} --json url --jq '.url' 2>/dev/null", check=False)
if existing and "github.com" in existing:
print(f"[信息] 仓库已存在: {existing}")
# Just push updates
run("git add -A", cwd=skill_dir)
status = run("git status --porcelain", cwd=skill_dir)
if status:
run('git commit -m "Update skill"', cwd=skill_dir)
# Check if remote exists
remote = run("git remote get-url origin 2>/dev/null", cwd=skill_dir, check=False)
if not remote:
run(f"git remote add origin https://github.com/{github_user}/{repo_name}.git", cwd=skill_dir)
run("git push -u origin main 2>&1", cwd=skill_dir, check=False)
run("git push -u origin HEAD:main 2>&1", cwd=skill_dir, check=False)
return existing
# Short description for GitHub (max 350 chars)
gh_desc = desc[:150] if len(desc) > 150 else desc
# Commit all files
run("git add -A", cwd=skill_dir)
run(f'git commit -m "Initial release: {repo_name}"', cwd=skill_dir)
# Create repo and push
result = run(
["gh", "repo", "create", f"{github_user}/{repo_name}", visibility,
"--description", gh_desc, "--source", ".", "--push"],
cwd=skill_dir, check=False
)
if result and "github.com" in result:
url = result.strip().split("\n")[0]
return url
print(f"[错误] 创建仓库失败: {result}", file=sys.stderr)
return None
def verify_skill(github_user, repo_name, skill_name):
"""Verify skill is installable via npx skills.
Runs both --list discovery and a real install into a temporary directory.
"""
source = f"{github_user}/{repo_name}"
result = run(f"npx skills add {source} --list 2>&1", check=False)
if not result:
return False, "npx skills 命令执行失败或超时"
# Must see both "Found N skill" and the skill name — confirms YAML was parsed OK
if "No valid skills found" in result:
return False, "YAML 解析失败(npx skills 找不到有效 skill)— 检查 SKILL.md frontmatter"
if not ("Found" in result and skill_name in result):
return False, f"skill 名称 '{skill_name}' 未出现在 --list 输出中"
tmpdir = tempfile.mkdtemp(prefix="skill-publisher-verify-")
try:
install = run(
f"npx skills add {source} --skill {skill_name} 2>&1",
cwd=tmpdir,
check=False,
)
installed_skill = os.path.join(tmpdir, ".agents", "skills", skill_name, "SKILL.md")
if not install or not os.path.exists(installed_skill):
return False, "真实安装失败:临时目录中没有生成 .agents/skills/<name>/SKILL.md"
return True, None
finally:
shutil.rmtree(tmpdir, ignore_errors=True)
def sync_agent_skill(skill_dir, name):
"""Copy/update a skill into ~/.agents/skills/<name> as real files.
This keeps the local agent skill tree as the canonical on-disk mirror instead of
a symlink, so future edits can live there as实体 files.
"""
agents_dir = os.path.expanduser("~/.agents/skills")
os.makedirs(agents_dir, exist_ok=True)
target_path = os.path.join(agents_dir, name)
target = os.path.abspath(skill_dir)
backup_path = None
if os.path.abspath(target_path) == target:
return "skipped", f"{target_path} 已经是当前发布源目录,跳过同步以避免自删"
if os.path.islink(target_path):
backup_path = os.readlink(target_path)
os.unlink(target_path)
elif os.path.isdir(target_path):
shutil.rmtree(target_path)
elif os.path.exists(target_path):
os.remove(target_path)
shutil.copytree(
target,
target_path,
ignore=shutil.ignore_patterns(".git", ".DS_Store"),
dirs_exist_ok=False,
)
if backup_path:
return "updated", f"{target_path} 已从 symlink 迁移为实体目录,来源: {target}"
return "created", f"{target_path} ← {target}"
def main():
parser = argparse.ArgumentParser(description="发布 Claude Code Skill 到 GitHub")
parser.add_argument("skill_dir", help="Skill 目录路径")
parser.add_argument("--github-user", help="GitHub 用户名 (默认自动获取)")
parser.add_argument("--repo-name", help="GitHub 仓库名 (默认优先使用当前 origin 仓库名,否则使用 skill name)")
parser.add_argument("--private", action="store_true", help="创建私有仓库 (默认公开)")
parser.add_argument("--dry-run", action="store_true", help="仅检查,不实际发布")
parser.add_argument("--skip-verify", action="store_true", help="跳过 npx skills 验证")
parser.add_argument("--no-symlink", action="store_true", help="跳过同步 ~/.agents/skills/ 实体目录")
args = parser.parse_args()
skill_dir = os.path.abspath(args.skill_dir)
if not os.path.isdir(skill_dir):
print(f"[错误] 目录不存在: {skill_dir}", file=sys.stderr)
sys.exit(1)
print(f"\n🔍 检查 Skill: {skill_dir}\n")
# Step 1: Validate
errors, name, desc = validate_skill(skill_dir)
if errors:
print("❌ 验证失败:")
for e in errors:
print(f" - {e}")
sys.exit(1)
print(f"✅ SKILL.md 验证通过 (name: {name})")
# Step 2: Prerequisites
if not check_prerequisites():
sys.exit(1)
print("✅ gh CLI 已就绪")
origin_owner, origin_repo = get_origin_repo(skill_dir)
github_user = args.github_user or origin_owner or get_github_user()
if not github_user:
print("[错误] 无法获取 GitHub 用户名", file=sys.stderr)
sys.exit(1)
print(f"✅ GitHub 用户: {github_user}")
repo_name = args.repo_name or origin_repo or name
print(f"✅ GitHub 仓库名: {repo_name}")
if repo_name != name:
print(f"ℹ️ Skill name 与仓库名不同: skill={name}, repo={repo_name}")
# Step 3: Ensure LICENSE
if ensure_license(skill_dir, github_user):
print("📄 已创建 LICENSE (MIT)")
else:
print("✅ LICENSE 已存在")
# Step 4: Generate README
if generate_readme(skill_dir, name, repo_name, desc, github_user):
print("📄 已生成 README.md")
else:
print("✅ README.md 已存在")
readme_errors = check_readme_quality(skill_dir)
if readme_errors:
print("❌ README 质量检查失败:")
for e in readme_errors:
print(f" - {e}")
sys.exit(1)
print("✅ README 质量检查通过")
if args.dry_run:
print(f"\n🏁 Dry run 完成。实际发布命令:")
print(f" python3 {__file__} {skill_dir} --github-user {github_user} --repo-name {repo_name}")
return
# Step 5: Git init
if init_git(skill_dir):
print("📦 已初始化 git 仓库")
else:
print("✅ git 仓库已存在")
# Step 6: Create repo and push
public = not args.private
print(f"\n🚀 发布到 GitHub ({'公开' if public else '私有'})...")
url = create_and_push(skill_dir, repo_name, desc, github_user, public=public)
if not url:
print("❌ 发布失败", file=sys.stderr)
sys.exit(1)
print(f"✅ GitHub: {url}")
# Step 7: Verify
if not args.skip_verify:
print("\n🔎 验证 npx skills 可安装...")
ok, verify_err = verify_skill(github_user, repo_name, name)
if ok:
print("✅ 验证通过(可发现,并已在临时目录真实安装)")
else:
print(f"❌ 验证失败: {verify_err}", file=sys.stderr)
print(" 请检查 SKILL.md frontmatter,修复后重新运行脚本更新", file=sys.stderr)
# Step 8: Sync ~/.agents/skills/实体目录
if not args.no_symlink:
status, msg = sync_agent_skill(skill_dir, name)
if status == "created":
print(f"\n📁 已同步 Agent skill: {msg}")
elif status == "updated":
print(f"\n📁 已更新 Agent skill: {msg}")
else:
print(f"\nℹ️ Agent skill: {msg}")
# Summary
print(f"\n{'='*60}")
print(f"🎉 发布成功!")
print(f" 仓库: {url}")
print(f" 安装: npx skills add {github_user}/{repo_name}")
print(f"{'='*60}\n")
if __name__ == "__main__":
main()