#!/usr/bin/env python3 """ Skill 发布脚本 使用方式: uv run python publish.py """ import os import sys import shutil import subprocess import tempfile TARGET_REPO_URL = "https://github.com/lanyuanxiaoyao/skills.git" TARGET_PATH = "skills/lyxy-document-reader" def check_build_dir(build_dir: str) -> None: """ 检查 build/ 目录是否存在 Args: build_dir: build 目录路径 Raises: SystemExit: 目录不存在时退出 """ if not os.path.exists(build_dir): print(""" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 错误: build/ 目录不存在 请先运行 build.py: uv run python build.py ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ """) sys.exit(1) def check_build_skill_md(build_skill_md_path: str) -> None: """ 检查 build/SKILL.md 是否存在 Args: build_skill_md_path: build/SKILL.md 路径 Raises: SystemExit: 文件不存在时退出 """ if not os.path.exists(build_skill_md_path): print(""" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 错误: build/SKILL.md 不存在 请先运行 build.py: uv run python build.py ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ """) sys.exit(1) def parse_version_from_skill_md(skill_md_path: str) -> str: """ 从 SKILL.md 解析出版本号 Args: skill_md_path: SKILL.md 路径 Returns: 版本号字符串 Raises: SystemExit: 解析失败时退出 """ with open(skill_md_path, "r", encoding="utf-8") as f: content = f.read() # 简单解析 YAML frontmatter 中的 version lines = content.split("\n") in_frontmatter = False in_metadata = False for line in lines: stripped = line.strip() if stripped == "---": if not in_frontmatter: in_frontmatter = True else: break continue if in_frontmatter: if stripped == "metadata:": in_metadata = True elif in_metadata and stripped.startswith("version:"): # 提取版本号,去掉引号 version_part = stripped.split(":", 1)[1].strip() version = version_part.strip('"').strip("'") return version elif in_metadata and stripped and not stripped.startswith(" "): # metadata 块结束 in_metadata = False print(""" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 错误: 无法从 build/SKILL.md 解析版本号 请检查 build/SKILL.md 是否包含 metadata.version 字段 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ """) sys.exit(1) def run_git_command(repo_dir: str, args: list[str]) -> subprocess.CompletedProcess: """ 在指定目录运行 git 命令 Args: repo_dir: 仓库目录 args: git 命令参数列表 Returns: subprocess.CompletedProcess Raises: subprocess.CalledProcessError: 命令失败时 """ cmd = ["git"] + args return subprocess.run( cmd, cwd=repo_dir, check=True, capture_output=True, text=True ) def clone_repo(temp_dir: str) -> str: """ 在临时目录 clone 目标仓库 Args: temp_dir: 临时目录路径 Returns: 仓库目录路径 Raises: SystemExit: clone 失败时退出 """ repo_dir = os.path.join(temp_dir, "skills-repo") print(f"Clone 仓库: {TARGET_REPO_URL}") print(f" 到: {repo_dir}") try: run_git_command(temp_dir, ["clone", "--depth", "1", TARGET_REPO_URL, "skills-repo"]) except subprocess.CalledProcessError as e: print(f""" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 错误: Clone 仓库失败 返回码: {e.returncode} 标准输出: {e.stdout} 错误输出: {e.stderr} ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ """) sys.exit(1) return repo_dir def clear_target_dir(repo_dir: str) -> str: """ 清空目标路径目录 Args: repo_dir: 仓库目录 Returns: 目标目录路径 """ target_dir = os.path.join(repo_dir, TARGET_PATH) if os.path.exists(target_dir): print(f"清空目标目录: {target_dir}") shutil.rmtree(target_dir) os.makedirs(target_dir, exist_ok=True) return target_dir def copy_build_contents(build_dir: str, target_dir: str) -> None: """ 复制 build/ 内容到目标目录 Args: build_dir: build 源目录 target_dir: 目标目录 """ print(f"复制 build/ 内容 -> {target_dir}") for item in os.listdir(build_dir): src = os.path.join(build_dir, item) dst = os.path.join(target_dir, item) if os.path.isdir(src): shutil.copytree(src, dst) print(f" 目录: {item}") else: shutil.copy2(src, dst) print(f" 文件: {item}") def git_commit_and_push(repo_dir: str, version: str) -> None: """ 执行 git add / commit / push Args: repo_dir: 仓库目录 version: 版本号 Raises: SystemExit: git 操作失败时退出 """ commit_message = f"publish: lyxy-document-reader {version}" print(f"Git 提交: {commit_message}") try: run_git_command(repo_dir, ["add", "."]) run_git_command(repo_dir, ["commit", "-m", commit_message]) print(" 推送中...") run_git_command(repo_dir, ["push"]) except subprocess.CalledProcessError as e: print(f""" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 错误: Git 操作失败 返回码: {e.returncode} 标准输出: {e.stdout} 错误输出: {e.stderr} ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ """) sys.exit(1) def main() -> None: """ 主函数:执行完整的发布流程 """ print("=" * 60) print("Skill 发布") print("=" * 60) # 路径配置 project_root = os.path.dirname(os.path.abspath(__file__)) build_dir = os.path.join(project_root, "build") build_skill_md_path = os.path.join(build_dir, "SKILL.md") # 检查 build/ 目录 check_build_dir(build_dir) check_build_skill_md(build_skill_md_path) # 解析版本号 version = parse_version_from_skill_md(build_skill_md_path) print(f"版本号: {version}") print() # 使用临时目录 with tempfile.TemporaryDirectory(prefix="lyxy-publish-") as temp_dir: print(f"临时目录: {temp_dir}") print() # Clone 仓库 repo_dir = clone_repo(temp_dir) print() # 清空目标路径 target_dir = clear_target_dir(repo_dir) print() # 复制内容 copy_build_contents(build_dir, target_dir) print() # Git 提交并推送 git_commit_and_push(repo_dir, version) print() # 完成信息 print("=" * 60) print("发布完成!") print(f"版本号: {version}") print(f"目标仓库: {TARGET_REPO_URL}") print(f"目标路径: {TARGET_PATH}") print("=" * 60) if __name__ == "__main__": main()