项目级skills 同步小脚本
- 内容介绍
- 文章标签
- 相关推荐
全局的skills 我用的是cc-switch 管理同步的,目前它不支持项目级别的同步,有一些skills 又只需要在单个项目下使用。就让codex 糊了个简单的脚本。
- 目前只有codex、cc、Antigravity 的,需要啥的自己加在
DEFAULT_TOOLS里面 - 只是个简单的扫描skills、软连接创建,不包括格式处理等功能
- 仅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?
--【捌】--:
- 这个是给单项目多个工具用的,全局的我用cc-switch 管理了
- 没有python 环境的…只能说比较少
这个主要是我自己用,顺手分享一下而已
--【玖】--:
找到了,谢谢分享,藏得好深
Options - enable / disable | skillshare
Temporarily enable or disable skills without removing them.
--【拾】--:
cc-switch 可以webdav 同步
--【拾壹】--:
话说有跨设备同步skill的方案吗
--【拾贰】--:
感谢佬友分享
--【拾叁】--:
有哪些好用的skill 求推荐 谢谢佬友
--【拾肆】--:
我想说明明 README 里就有写。。。
--【拾伍】--:
感谢大佬
--【拾陆】--:
- 既然不是单一项目里的skill, 那为什么不放在全局里?
- 不是所有开发都有python环境, 这个脚本还是有局限性的
全局的skills 我用的是cc-switch 管理同步的,目前它不支持项目级别的同步,有一些skills 又只需要在单个项目下使用。就让codex 糊了个简单的脚本。
- 目前只有codex、cc、Antigravity 的,需要啥的自己加在
DEFAULT_TOOLS里面 - 只是个简单的扫描skills、软连接创建,不包括格式处理等功能
- 仅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?
--【捌】--:
- 这个是给单项目多个工具用的,全局的我用cc-switch 管理了
- 没有python 环境的…只能说比较少
这个主要是我自己用,顺手分享一下而已
--【玖】--:
找到了,谢谢分享,藏得好深
Options - enable / disable | skillshare
Temporarily enable or disable skills without removing them.
--【拾】--:
cc-switch 可以webdav 同步
--【拾壹】--:
话说有跨设备同步skill的方案吗
--【拾贰】--:
感谢佬友分享
--【拾叁】--:
有哪些好用的skill 求推荐 谢谢佬友
--【拾肆】--:
我想说明明 README 里就有写。。。
--【拾伍】--:
感谢大佬
--【拾陆】--:
- 既然不是单一项目里的skill, 那为什么不放在全局里?
- 不是所有开发都有python环境, 这个脚本还是有局限性的

