register_skills.py 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. # -*- coding: utf-8 -*-
  2. """
  3. register_skills.py — 把 30-技能/ 下的技能注册给 WorkBuddy
  4. 优先用目录联接(junction),改源即生效;失败则回退为复制 + 同步模式。
  5. 用法:
  6. python register_skills.py # 注册(link 优先)
  7. python register_skills.py --copy # 强制复制模式
  8. python register_skills.py --sync # 复制模式下,把源文件同步到目标
  9. python register_skills.py --status # 查看当前注册状态
  10. python register_skills.py --uninstall
  11. """
  12. import sys, os, shutil, subprocess, argparse
  13. from pathlib import Path
  14. if hasattr(sys.stdout, "reconfigure"):
  15. sys.stdout.reconfigure(encoding="utf-8", errors="replace")
  16. ROOT = Path(__file__).resolve().parent.parent
  17. SRC = ROOT / "30-技能"
  18. DEST = Path(os.environ.get("USERPROFILE", Path.home())) / ".workbuddy" / "skills"
  19. SKILLS = ["course-factory", "kb-sync", "kb-ingest", "kb-query",
  20. "kb-lint", "kb-index", "course-outline", "course-slides"]
  21. def is_link(p: Path) -> bool:
  22. if p.is_symlink():
  23. return True
  24. try:
  25. return bool(p.exists() and os.readlink(str(p)))
  26. except (OSError, ValueError):
  27. pass
  28. # Windows junction 无 readlink,用 FILE_ATTRIBUTE_REPARSE_POINT 判断
  29. try:
  30. return bool(os.stat(str(p)).st_file_attributes & 0x400)
  31. except (AttributeError, OSError):
  32. return False
  33. def make_link(src: Path, dst: Path) -> bool:
  34. if os.name == "nt":
  35. r = subprocess.run(["cmd", "/c", "mklink", "/J", str(dst), str(src)],
  36. capture_output=True, text=True,
  37. encoding="gbk", errors="replace")
  38. return r.returncode == 0
  39. try:
  40. dst.symlink_to(src, target_is_directory=True)
  41. return True
  42. except OSError:
  43. return False
  44. def register(mode: str):
  45. DEST.mkdir(parents=True, exist_ok=True)
  46. ok_link, ok_copy = [], []
  47. for name in SKILLS:
  48. s = SRC / name
  49. d = DEST / name
  50. if not (s / "SKILL.md").exists():
  51. print(f" [SKIP] {name}:源目录缺少 SKILL.md")
  52. continue
  53. if d.exists() or is_link(d):
  54. if is_link(d):
  55. print(f" [OK] {name}(已是联接,跳过)")
  56. ok_link.append(name)
  57. continue
  58. shutil.rmtree(d, ignore_errors=True)
  59. if mode == "link" and make_link(s, d):
  60. print(f" [LINK] {name}")
  61. ok_link.append(name)
  62. else:
  63. shutil.copytree(s, d)
  64. print(f" [COPY] {name}")
  65. ok_copy.append(name)
  66. print(f"\n注册完成:联接 {len(ok_link)} 个,复制 {len(ok_copy)} 个 -> {DEST}")
  67. if ok_copy:
  68. print("提示:复制模式改了源文件后需运行 python register_skills.py --sync")
  69. def sync():
  70. n = 0
  71. for name in SKILLS:
  72. s, d = SRC / name, DEST / name
  73. if is_link(d) or not d.exists():
  74. continue
  75. for f in s.rglob("*"):
  76. if f.is_file():
  77. t = d / f.relative_to(s)
  78. t.parent.mkdir(parents=True, exist_ok=True)
  79. shutil.copy2(f, t)
  80. n += 1
  81. print(f"已同步 {n} 个文件到 {DEST}")
  82. def status():
  83. print(f"技能源目录:{SRC}")
  84. print(f"注册目标: {DEST}\n")
  85. for name in SKILLS:
  86. d = DEST / name
  87. if not d.exists():
  88. st = "未注册"
  89. elif is_link(d):
  90. st = "联接"
  91. else:
  92. st = "复制(需 sync)"
  93. print(f" {name:<18} {st}")
  94. def uninstall():
  95. for name in SKILLS:
  96. d = DEST / name
  97. if is_link(d):
  98. subprocess.run(["cmd", "/c", "rmdir", str(d)], capture_output=True,
  99. text=True, encoding="gbk", errors="replace")
  100. print(f" [RM] {name}")
  101. elif d.exists():
  102. shutil.rmtree(d, ignore_errors=True)
  103. print(f" [RM] {name}")
  104. if __name__ == "__main__":
  105. ap = argparse.ArgumentParser()
  106. ap.add_argument("--copy", action="store_true")
  107. ap.add_argument("--sync", action="store_true")
  108. ap.add_argument("--status", action="store_true")
  109. ap.add_argument("--uninstall", action="store_true")
  110. a = ap.parse_args()
  111. if a.status:
  112. status()
  113. elif a.sync:
  114. sync()
  115. elif a.uninstall:
  116. uninstall()
  117. else:
  118. register("copy" if a.copy else "link")