项目级skills 同步小脚本

2026-04-11 10:571阅读0评论SEO基础
  • 内容介绍
  • 文章标签
  • 相关推荐
问题描述:

全局的skills 我用的是cc-switch 管理同步的,目前它不支持项目级别的同步,有一些skills 又只需要在单个项目下使用。就让codex 糊了个简单的脚本。

  1. 目前只有codex、cc、Antigravity 的,需要啥的自己加在DEFAULT_TOOLS里面
  2. 只是个简单的扫描skills、软连接创建,不包括格式处理等功能
  3. 仅Windows

直接新建个py 文件,把下面代码copy 进去。丢到需要同步的路径下跑起来就完事了

#!/usr/bin/env python3 from __future__ import annotations import argparse import os import sys from dataclasses import dataclass, field from pathlib import Path from typing import Dict, Iterable, List, Sequence DEFAULT_TOOLS: Sequence[str] = (".codex", ".claude", ".agent") @dataclass(frozen=True) class SkillSource: tool_name: str skill_name: str path: Path @dataclass class SyncResult: discovered_skills_dirs: List[Path] = field(default_factory=list) selected_sources: Dict[str, SkillSource] = field(default_factory=dict) created_links: List[Path] = field(default_factory=list) skipped: List[str] = field(default_factory=list) errors: List[str] = field(default_factory=list) expected_skill_names: List[str] = field(default_factory=list) consistency_report: List[str] = field(default_factory=list) def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( description=( "扫描当前目录下常见开发工具的 skills 目录,并将每个 skill " "同步为其他工具 skills 目录下的软链接。" ) ) parser.add_argument( "--root", default=".", help="扫描起点目录,默认当前目录。", ) parser.add_argument( "--tools", nargs="+", default=list(DEFAULT_TOOLS), help="需要识别的工具目录名,默认: %(default)s", ) parser.add_argument( "--dry-run", action="store_true", help="仅输出计划动作,不实际创建软链接。", ) parser.add_argument( "--verbose", action="store_true", help="输出更详细的扫描和跳过信息。", ) return parser.parse_args() def normalize_tools(tools: Iterable[str]) -> List[str]: normalized: List[str] = [] for tool in tools: item = tool.strip() if not item: continue if not item.startswith("."): item = f".{item}" if item not in normalized: normalized.append(item) if not normalized: raise ValueError("工具目录列表不能为空。") return normalized def find_skills_dirs(root: Path, tools: Sequence[str]) -> List[Path]: tool_set = set(tools) found: List[Path] = [] seen: set[Path] = set() for current_root, dirnames, _ in os.walk(root): current_path = Path(current_root) if current_path.name in tool_set: skills_dir = current_path / "skills" if skills_dir.is_dir(): resolved = skills_dir.resolve() if resolved not in seen: found.append(skills_dir) seen.add(resolved) dirnames[:] = [] return sorted(found, key=lambda item: str(item)) def collect_sources(skills_dirs: Sequence[Path], tools: Sequence[str]) -> Dict[str, SkillSource]: priority = {tool: index for index, tool in enumerate(tools)} selected: Dict[str, SkillSource] = {} for skills_dir in skills_dirs: tool_name = skills_dir.parent.name for child in sorted(skills_dir.iterdir(), key=lambda item: item.name): if not child.is_dir(): continue candidate = SkillSource(tool_name=tool_name, skill_name=child.name, path=child) existing = selected.get(child.name) if existing is None: selected[child.name] = candidate continue if priority[candidate.tool_name] < priority[existing.tool_name]: selected[child.name] = candidate return selected def target_already_points_to(target: Path, source: Path) -> bool: if not target.is_symlink(): return False try: return target.resolve() == source.resolve() except OSError: return False def create_symlink(target: Path, source: Path, dry_run: bool) -> None: if dry_run: return target.symlink_to(source, target_is_directory=True) def ensure_skills_dirs(root: Path, tools: Sequence[str], existing_skills_dirs: Sequence[Path], dry_run: bool) -> List[Path]: discovered_tools = {skills_dir.parent.resolve() for skills_dir in existing_skills_dirs} ensured: List[Path] = [] for current_root, dirnames, _ in os.walk(root): current_path = Path(current_root) if current_path.name not in tools: continue tool_dir = current_path.resolve() if tool_dir in discovered_tools: dirnames[:] = [] continue skills_dir = current_path / "skills" if not dry_run: skills_dir.mkdir(parents=True, exist_ok=True) ensured.append(skills_dir) discovered_tools.add(tool_dir) dirnames[:] = [] return sorted(ensured, key=lambda item: str(item)) def sync_skills(root: Path, tools: Sequence[str], dry_run: bool, verbose: bool) -> SyncResult: result = SyncResult() existing_skills_dirs = find_skills_dirs(root, tools) ensured_skills_dirs = ensure_skills_dirs(root, tools, existing_skills_dirs, dry_run) all_skills_dirs = sorted(existing_skills_dirs + ensured_skills_dirs, key=lambda item: str(item)) result.discovered_skills_dirs.extend(all_skills_dirs) selected_sources = collect_sources(existing_skills_dirs, tools) result.selected_sources.update(selected_sources) expected_skill_names = sorted(selected_sources.keys()) result.expected_skill_names.extend(expected_skill_names) for skills_dir in all_skills_dirs: tool_name = skills_dir.parent.name for skill_name, source in selected_sources.items(): if tool_name == source.tool_name: continue target = skills_dir / skill_name if target.exists() or target.is_symlink(): if verbose: if target_already_points_to(target, source.path): result.skipped.append( f"跳过 {target},已是正确链接 -> {source.path}" ) else: result.skipped.append( f"跳过 {target},目标已存在。" ) continue try: create_symlink(target, source.path, dry_run) result.created_links.append(target) except OSError as exc: message = f"创建链接失败 {target} -> {source.path}: {exc}" if os.name == "nt": message += ";Windows 下请确认已启用开发者模式或以具备权限的终端运行。" result.errors.append(message) expected_set = set(result.expected_skill_names) planned_by_dir: Dict[Path, set[str]] = {} if dry_run: for link in result.created_links: planned_by_dir.setdefault(link.parent, set()).add(link.name) for skills_dir in all_skills_dirs: current_names: set[str] = set() if skills_dir.is_dir(): for child in skills_dir.iterdir(): if child.is_dir(): current_names.add(child.name) final_names = current_names | planned_by_dir.get(skills_dir, set()) missing = sorted(expected_set - final_names) if missing: result.consistency_report.append( f"{skills_dir}: {len(final_names)}/{len(expected_set)},缺失 {', '.join(missing)}" ) else: result.consistency_report.append( f"{skills_dir}: {len(final_names)}/{len(expected_set)},已按并集补齐" ) return result def print_summary(result: SyncResult, dry_run: bool) -> None: print("发现的 skills 目录:") if result.discovered_skills_dirs: for path in result.discovered_skills_dirs: print(f" - {path}") else: print(" - 无") print("\n选定的 skill 源目录:") if result.selected_sources: for skill_name in sorted(result.selected_sources): source = result.selected_sources[skill_name] print(f" - {skill_name}: {source.path} ({source.tool_name})") else: print(" - 无") action_title = "计划创建的软链接:" if dry_run else "已创建的软链接:" print(f"\n{action_title}") if result.created_links: for path in result.created_links: print(f" - {path}") else: print(" - 无") print("\n跳过项:") if result.skipped: for item in result.skipped: print(f" - {item}") else: print(" - 无") print("\n错误:") if result.errors: for item in result.errors: print(f" - {item}") else: print(" - 无") print("\n一致性检查:") if result.consistency_report: for item in result.consistency_report: print(f" - {item}") else: print(" - 无") def main() -> int: args = parse_args() root = Path(args.root).expanduser().resolve() if not root.exists(): print(f"扫描起点不存在: {root}", file=sys.stderr) return 2 if not root.is_dir(): print(f"扫描起点不是目录: {root}", file=sys.stderr) return 2 try: tools = normalize_tools(args.tools) except ValueError as exc: print(str(exc), file=sys.stderr) return 2 result = sync_skills(root=root, tools=tools, dry_run=args.dry_run, verbose=args.verbose) print_summary(result, dry_run=args.dry_run) return 1 if result.errors else 0 if __name__ == "__main__": raise SystemExit(main()) 网友解答:


--【壹】--:

skillshare

你值得拥有


--【贰】--:

这个看上去也是同步全局skill 的


--【叁】--:

这很难啊 这要看要做什么才知道有什么 skill 啊。


--【肆】--:

谢谢分享,但是看github的介绍,好像也是管理全局skills 的


--【伍】--:

试试skills-manager


--【陆】--:

可以自选,-g 参数就全局 -p 就是项目级


--【柒】--:

让codex 去查codex,让claude 去查claude?


--【捌】--:
  1. 这个是给单项目多个工具用的,全局的我用cc-switch 管理了
  2. 没有python 环境的…只能说比较少

这个主要是我自己用,顺手分享一下而已


--【玖】--:

找到了,谢谢分享,藏得好深

skillshare.runkids.cc

Options​ - enable / disable | skillshare

Temporarily enable or disable skills without removing them.


--【拾】--:

cc-switch 可以webdav 同步


--【拾壹】--:

话说有跨设备同步skill的方案吗


--【拾贰】--:

感谢佬友分享


--【拾叁】--:

有哪些好用的skill 求推荐 谢谢佬友


--【拾肆】--:

我想说明明 README 里就有写。。。


--【拾伍】--:

感谢大佬


--【拾陆】--:
  1. 既然不是单一项目里的skill, 那为什么不放在全局里?
  2. 不是所有开发都有python环境, 这个脚本还是有局限性的