Repository navigation
Expand file tree
/
Copy pathpackage_skill.py
More file actions
124 lines (108 loc) · 4.78 KB
/
Copy pathpackage_skill.py
File metadata and controls
124 lines (108 loc) · 4.78 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
package_skill.py —— 把 html-explainer 打包成可带到任意终端的 zip。
设计取舍:
· **排除 node/node_modules/**(14MB,且平台相关)—— setup_env.sh --install 会重装。
这样 zip 只有几十 KB,纯文本 + 一份 GSAP。
· **保留 assets/gsap.min.js**(72KB,离线必需,体积可接受)。
· **保留 README / LICENSE / THIRD_PARTY_NOTICES.md / licenses/** —— 再分发时必须随包
带上这些文件(Apache-2.0 §4 要求收到衍生作品的人同时收到许可证副本)。
· **排除仓库卫生文件**(.github/、.gitignore、.gitattributes)—— 它们属于 GitHub
仓库,不属于可移植的技能包。
· 排除运行时产物(out/、audio/、*.beats.js、__pycache__、诊断脚本)。
用法:
python package_skill.py # 产出 dist/html-explainer-vX.Y.zip
python package_skill.py --with-deps # 连 node_modules 一起打(完全离线可跑,~14MB)
"""
from __future__ import annotations
import argparse
import os
import re
import sys
import zipfile
from datetime import date
SKILL_ROOT = os.path.dirname(os.path.abspath(__file__))
# 目录名匹配即整棵排除
EXCLUDE_DIRS = {
"node_modules", "__pycache__", "dist", "out", "audio", ".git",
".pytest_cache", "qc_frames",
".github", # 仓库卫生:CI / issue 模板不属于可移植技能包
"render", "research", "script", # 项目级产物(防在技能目录内建过 demo)
}
# 文件名 / 后缀排除
EXCLUDE_SUFFIX = (".pyc", ".pyo", ".beats.js", ".mp4", ".mp3", ".wav", ".log",
".env", ".key", ".pem") # 密钥类:绝不进可分发包
EXCLUDE_NAMES = {
"package-lock.json", ".DS_Store", "Thumbs.db", "qc_sheet.jpg", "qc_report.md",
".gitignore", ".gitattributes",
# ★ 密钥文件。tts.env 是火山 TTS 的 API Key 唯一落点,打包进 zip 等于把
# 用户的密钥随技能一起发出去。模板 tts.env.example 是入库的,不在此列。
"tts.env", "secrets",
}
def version() -> str:
md = os.path.join(SKILL_ROOT, "SKILL.md")
try:
txt = open(md, encoding="utf-8").read()
m = re.search(r"^version:\s*(\S+)", txt, re.M)
if m:
return m.group(1).strip().strip('"\'')
except OSError:
pass
return "1.0.0"
def should_skip(rel: str, with_deps: bool) -> bool:
parts = rel.replace("\\", "/").split("/")
for p in parts[:-1]:
if p in EXCLUDE_DIRS:
if with_deps and p == "node_modules":
continue
return True
name = parts[-1]
if name in EXCLUDE_NAMES:
return True
if name.endswith(EXCLUDE_SUFFIX):
return True
# 临时诊断脚本(下划线开头 .js/.mjs/.py,非技能正式脚本)
if parts[0] == "scripts" and name.startswith("_"):
return True
return False
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--with-deps", action="store_true",
help="把 node/node_modules 一起打包(离线可跑,体积 ~14MB)")
ap.add_argument("--out", default=None)
args = ap.parse_args()
ver = version()
dist = os.path.join(SKILL_ROOT, "dist")
os.makedirs(dist, exist_ok=True)
tag = "-full" if args.with_deps else ""
out = args.out or os.path.join(dist, f"html-explainer-v{ver}{tag}.zip")
count = 0
total = 0
with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED, compresslevel=9) as z:
for root, dirs, files in os.walk(SKILL_ROOT):
dirs[:] = [d for d in dirs if d not in EXCLUDE_DIRS or
(args.with_deps and d == "node_modules")]
for f in files:
full = os.path.join(root, f)
rel = os.path.relpath(full, SKILL_ROOT)
if should_skip(rel, args.with_deps):
continue
arc = os.path.join("html-explainer", rel.replace("\\", "/"))
z.write(full, arc)
count += 1
total += os.path.getsize(full)
size = os.path.getsize(out)
print(f"✓ 打包完成:{out}")
print(f" 文件 {count} 个 · 源 {total / 1024:.0f}KB · zip {size / 1024:.0f}KB"
f"(压缩率 {100 - size * 100 / max(total, 1):.0f}%)")
if not args.with_deps:
print(" 注:未含 node/node_modules —— 目标机首次跑 `bash setup_env.sh --install` 装齐")
print()
print(" 带到新终端后:")
print(" 1. 解压到 ~/.workbuddy/skills/ (或任意位置,脚本按自身路径定位)")
print(" 2. bash setup_env.sh --install")
print(" 3. python scripts/new_project.py <name> && 编辑 narration.json …")
return 0
if __name__ == "__main__":
raise SystemExit(main())