@topmindspace/tms-skills 0.1.19 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/README.en.md +5 -2
- package/README.md +4 -1
- package/package.json +5 -2
- package/topmind-cover/README.md +40 -0
- package/topmind-cover/SKILL.md +66 -0
- package/topmind-cover/package-lock.json +16 -0
- package/topmind-cover/package.json +16 -0
- package/topmind-cover/references/cover-styles.md +40 -0
- package/topmind-cover/scripts/audit_css.py +39 -0
- package/topmind-cover/scripts/audit_docs.py +58 -0
- package/topmind-cover/scripts/audit_skill.py +60 -0
- package/topmind-cover/scripts/audit_styles.py +54 -0
- package/topmind-cover/scripts/crop-cover.py +69 -0
- package/topmind-cover/scripts/negative_tests.py +71 -0
- package/topmind-cover/scripts/package_skill.py +188 -0
- package/topmind-wechat-post/README.md +49 -0
- package/topmind-wechat-post/SKILL.md +208 -0
- package/topmind-wechat-post/assets/themes/amber-review.json +35 -0
- package/topmind-wechat-post/assets/themes/graphite.json +35 -0
- package/topmind-wechat-post/assets/themes/minimal-ink.json +35 -0
- package/topmind-wechat-post/assets/themes/newsprint.json +35 -0
- package/topmind-wechat-post/assets/themes/tech-blue.json +35 -0
- package/topmind-wechat-post/package-lock.json +16 -0
- package/topmind-wechat-post/package.json +16 -0
- package/topmind-wechat-post/references/design.md +50 -0
- package/topmind-wechat-post/references/element-spec.md +162 -0
- package/topmind-wechat-post/references/known-pits.md +86 -0
- package/topmind-wechat-post/references/theme-map.md +139 -0
- package/topmind-wechat-post/references/typography-rules.md +62 -0
- package/topmind-wechat-post/references/wechat-constraints.md +106 -0
- package/topmind-wechat-post/references/workflow.md +102 -0
- package/topmind-wechat-post/references/writing-quality.md +129 -0
- package/topmind-wechat-post/scripts/audit_css.py +39 -0
- package/topmind-wechat-post/scripts/audit_docs.py +58 -0
- package/topmind-wechat-post/scripts/audit_skill.py +60 -0
- package/topmind-wechat-post/scripts/audit_styles.py +54 -0
- package/topmind-wechat-post/scripts/lint-wechat.py +446 -0
- package/topmind-wechat-post/scripts/md2wechat.py +1602 -0
- package/topmind-wechat-post/scripts/negative_tests.py +65 -0
- package/topmind-wechat-post/scripts/new-article.py +183 -0
- package/topmind-wechat-post/scripts/package_skill.py +188 -0
- package/topmind-wechat-post/scripts/push-to-topstream.py +226 -0
- package/topmind-wechat-post/scripts/scan_ai_flavor.py +464 -0
- package/topmind-wechat-post/scripts/sync-mapping.py +220 -0
- package/topmind-wechat-post/scripts/sync-status.py +187 -0
- package/topmind-x-article/README.md +40 -0
- package/topmind-x-article/SKILL.md +62 -0
- package/topmind-x-article/package-lock.json +16 -0
- package/topmind-x-article/package.json +16 -0
- package/topmind-x-article/references/publish-checklist.md +24 -0
- package/topmind-x-article/references/x-format.md +24 -0
- package/topmind-x-article/scripts/audit_css.py +39 -0
- package/topmind-x-article/scripts/audit_docs.py +58 -0
- package/topmind-x-article/scripts/audit_skill.py +60 -0
- package/topmind-x-article/scripts/audit_styles.py +54 -0
- package/topmind-x-article/scripts/md2x.py +121 -0
- package/topmind-x-article/scripts/negative_tests.py +74 -0
- package/topmind-x-article/scripts/package_skill.py +188 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""md → X 长文可粘贴纯文本。
|
|
3
|
+
|
|
4
|
+
把 Markdown 原稿转成可直接粘贴进 X Article 编辑器的纯文本:
|
|
5
|
+
标题/正文保留,排版标记剥离,图片转为 [图N] 占位。
|
|
6
|
+
|
|
7
|
+
用法:
|
|
8
|
+
python3 md2x.py <输入.md> --out <输出/X发布稿.txt>
|
|
9
|
+
|
|
10
|
+
规则见 references/x-format.md。
|
|
11
|
+
"""
|
|
12
|
+
import argparse
|
|
13
|
+
import re
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def convert(md: str):
|
|
19
|
+
images = []
|
|
20
|
+
out_lines = []
|
|
21
|
+
in_code = False
|
|
22
|
+
|
|
23
|
+
def img_repl(m):
|
|
24
|
+
alt = m.group(1).strip()
|
|
25
|
+
images.append(alt)
|
|
26
|
+
return f"[图{len(images)}]"
|
|
27
|
+
|
|
28
|
+
for raw in md.splitlines():
|
|
29
|
+
line = raw.rstrip()
|
|
30
|
+
|
|
31
|
+
# frontmatter 直接跳过
|
|
32
|
+
if line.strip() == "---":
|
|
33
|
+
continue
|
|
34
|
+
|
|
35
|
+
# 代码块:去围栏,内容缩进保留
|
|
36
|
+
if line.strip().startswith("```"):
|
|
37
|
+
in_code = not in_code
|
|
38
|
+
continue
|
|
39
|
+
if in_code:
|
|
40
|
+
out_lines.append(" " + line)
|
|
41
|
+
continue
|
|
42
|
+
|
|
43
|
+
# 图片占位
|
|
44
|
+
line = re.sub(r"!\[([^\]]*)\]\([^)]*\)", img_repl, line)
|
|
45
|
+
|
|
46
|
+
# 标题 → 纯文本行
|
|
47
|
+
m = re.match(r"^(#{1,6})\s+(.*)$", line)
|
|
48
|
+
if m:
|
|
49
|
+
out_lines.append("")
|
|
50
|
+
out_lines.append(m.group(2).strip())
|
|
51
|
+
out_lines.append("")
|
|
52
|
+
continue
|
|
53
|
+
|
|
54
|
+
# 引用 → 纯文本
|
|
55
|
+
line = re.sub(r"^>\s?", "", line)
|
|
56
|
+
|
|
57
|
+
# 分割线 → 空行
|
|
58
|
+
if re.match(r"^(\*{3,}|-{3,}|_{3,})\s*$", line):
|
|
59
|
+
out_lines.append("")
|
|
60
|
+
continue
|
|
61
|
+
|
|
62
|
+
# 表格行 → "项:值" 列表
|
|
63
|
+
if "|" in line and line.strip().startswith("|"):
|
|
64
|
+
cells = [c.strip() for c in line.strip().strip("|").split("|")]
|
|
65
|
+
if all(re.match(r"^:?-+:?$", c) for c in cells):
|
|
66
|
+
continue
|
|
67
|
+
label = cells[0]
|
|
68
|
+
rest = ",".join(c for c in cells[1:] if c)
|
|
69
|
+
out_lines.append(f"{label}:{rest}" if rest else label)
|
|
70
|
+
continue
|
|
71
|
+
|
|
72
|
+
# 行内标记剥离
|
|
73
|
+
line = re.sub(r"\*\*(.+?)\*\*", r"\1", line) # 加粗
|
|
74
|
+
line = re.sub(r"__(.+?)__", r"\1", line)
|
|
75
|
+
line = re.sub(r"\*(.+?)\*", r"\1", line) # 斜体
|
|
76
|
+
line = re.sub(r"`(.+?)`", r"\1", line) # 行内代码
|
|
77
|
+
line = re.sub(r"~~(.+?)~~", r"\1", line) # 删除线
|
|
78
|
+
line = re.sub(r"\[([^\]]+)\]\(([^)]+)\)", r"\1(\2)", line) # 链接
|
|
79
|
+
line = re.sub(r"^(\s*)- \[ \]\s+", r"\1- ", line) # 任务列表
|
|
80
|
+
line = re.sub(r"^(\s*)- \[x\]\s+", r"\1- ", line, flags=re.I)
|
|
81
|
+
|
|
82
|
+
out_lines.append(line)
|
|
83
|
+
|
|
84
|
+
# 压缩 3+ 空行为 2 个
|
|
85
|
+
text = "\n".join(out_lines)
|
|
86
|
+
text = re.sub(r"\n{3,}", "\n\n", text).strip() + "\n"
|
|
87
|
+
|
|
88
|
+
# 文末附图注清单
|
|
89
|
+
if images:
|
|
90
|
+
text += "\n—— 配图清单 ——\n"
|
|
91
|
+
for i, alt in enumerate(images, 1):
|
|
92
|
+
text += f"[图{i}] {alt or '(未写图注)'}\n"
|
|
93
|
+
|
|
94
|
+
return text, images
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def main() -> None:
|
|
98
|
+
ap = argparse.ArgumentParser()
|
|
99
|
+
ap.add_argument("input", help="输入 Markdown")
|
|
100
|
+
ap.add_argument("--out", required=True, help="输出 X发布稿.txt")
|
|
101
|
+
args = ap.parse_args()
|
|
102
|
+
|
|
103
|
+
inp = Path(args.input)
|
|
104
|
+
if not inp.is_file():
|
|
105
|
+
sys.exit(f"错误:找不到输入文件 {inp}")
|
|
106
|
+
md = inp.read_text(encoding="utf-8")
|
|
107
|
+
# 去掉首尾 frontmatter 块
|
|
108
|
+
md = re.sub(r"^---\n.*?\n---\n", "", md, flags=re.S)
|
|
109
|
+
text, images = convert(md)
|
|
110
|
+
|
|
111
|
+
out = Path(args.out)
|
|
112
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
113
|
+
out.write_text(text, encoding="utf-8")
|
|
114
|
+
|
|
115
|
+
chars = len(text.replace("\n", ""))
|
|
116
|
+
print(f"OK {out}")
|
|
117
|
+
print(f"正文约 {chars} 字,配图 {len(images)} 张")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
if __name__ == "__main__":
|
|
121
|
+
main()
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""topmind-x-article 异常输入测试:md2x.py 在各种坏输入下不崩溃(任一失败 → 退出码 1)。"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
import tempfile
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
13
|
+
except Exception:
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
17
|
+
MD2X = ROOT / "scripts" / "md2x.py"
|
|
18
|
+
PY = sys.executable
|
|
19
|
+
fails: list[str] = []
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def run(args: list[str]) -> subprocess.CompletedProcess:
|
|
23
|
+
return subprocess.run([PY, str(MD2X), *args], capture_output=True, text=True, timeout=30)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def check(name: str, cond: bool, detail: str = "") -> None:
|
|
27
|
+
print((" ✓ " if cond else " ✗ ") + name + (f" — {detail}" if detail and not cond else ""))
|
|
28
|
+
if not cond:
|
|
29
|
+
fails.append(name)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def main() -> None:
|
|
33
|
+
print("[negative_tests] topmind-x-article")
|
|
34
|
+
with tempfile.TemporaryDirectory() as td:
|
|
35
|
+
td = Path(td)
|
|
36
|
+
|
|
37
|
+
# 1. 空文件
|
|
38
|
+
(td / "empty.md").write_text("", encoding="utf-8")
|
|
39
|
+
r = run([str(td / "empty.md"), "--out", str(td / "o1.txt")])
|
|
40
|
+
check("空文件不崩溃", r.returncode == 0 and "Traceback" not in r.stderr)
|
|
41
|
+
|
|
42
|
+
# 2. 只有 frontmatter
|
|
43
|
+
(td / "fm.md").write_text("---\ntitle: t\n---\n", encoding="utf-8")
|
|
44
|
+
r = run([str(td / "fm.md"), "--out", str(td / "o2.txt")])
|
|
45
|
+
check("纯 frontmatter 不崩溃", r.returncode == 0 and "Traceback" not in r.stderr)
|
|
46
|
+
|
|
47
|
+
# 3. 未闭合代码围栏
|
|
48
|
+
(td / "fence.md").write_text("# t\n\n```python\nprint(1)\n", encoding="utf-8")
|
|
49
|
+
r = run([str(td / "fence.md"), "--out", str(td / "o3.txt")])
|
|
50
|
+
check("未闭合围栏不崩溃", r.returncode == 0 and "Traceback" not in r.stderr)
|
|
51
|
+
|
|
52
|
+
# 4. 无分隔行的表格
|
|
53
|
+
(td / "tbl.md").write_text("# t\n\n| a | b |\n| 1 | 2 |\n", encoding="utf-8")
|
|
54
|
+
r = run([str(td / "tbl.md"), "--out", str(td / "o4.txt")])
|
|
55
|
+
check("异常表格不崩溃", r.returncode == 0 and "Traceback" not in r.stderr)
|
|
56
|
+
|
|
57
|
+
# 5. 不存在的输入 → 非零退出且无 Traceback
|
|
58
|
+
r = run([str(td / "nope.md"), "--out", str(td / "o5.txt")])
|
|
59
|
+
check("缺失输入干净报错", r.returncode != 0 and "Traceback" not in r.stderr)
|
|
60
|
+
|
|
61
|
+
# 6. 正常输入产出配图清单
|
|
62
|
+
(td / "ok.md").write_text("# t\n\n\n", encoding="utf-8")
|
|
63
|
+
r = run([str(td / "ok.md"), "--out", str(td / "o6.txt")])
|
|
64
|
+
out = (td / "o6.txt").read_text(encoding="utf-8") if (td / "o6.txt").exists() else ""
|
|
65
|
+
check("正常输入含配图清单", r.returncode == 0 and "[图1]" in out)
|
|
66
|
+
|
|
67
|
+
if fails:
|
|
68
|
+
print(f"\n[negative_tests] {len(fails)} 项失败")
|
|
69
|
+
sys.exit(1)
|
|
70
|
+
print("\n[negative_tests] 全部通过")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
if __name__ == "__main__":
|
|
74
|
+
main()
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""技能分发包打包器(发布前完整性校验 + 发布清单)。
|
|
4
|
+
|
|
5
|
+
用法:
|
|
6
|
+
python scripts/package_skill.py # 校验 → 打包 → 写发布清单
|
|
7
|
+
python scripts/package_skill.py --check # 只校验不打包(CI / 提交前自检)
|
|
8
|
+
|
|
9
|
+
产物:
|
|
10
|
+
dist/<skill>.zip 分发包(解压目录名 = 技能名)
|
|
11
|
+
dist/<skill>.manifest.json 发布清单(版本 / 条目 / 大小 / SHA-256)
|
|
12
|
+
|
|
13
|
+
校验(任一不过 → 退出码 1):
|
|
14
|
+
· SKILL.md frontmatter 含 name / description;name 与目录名一致;
|
|
15
|
+
description ≤ 1024 字符(平台截断阈值)
|
|
16
|
+
· README.md / package.json 存在;package.json name 与技能名一致
|
|
17
|
+
· scripts/*.py 全部可编译;references/*.md 非空
|
|
18
|
+
· SKILL.md 引用的 references/<file> 全部存在
|
|
19
|
+
· 无 scripts/_* / __pycache__ / .env 等不应进包的文件
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import hashlib
|
|
24
|
+
import json
|
|
25
|
+
import re
|
|
26
|
+
import sys
|
|
27
|
+
import zipfile
|
|
28
|
+
from datetime import datetime, timezone
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
33
|
+
except Exception:
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
37
|
+
NAME = ROOT.name
|
|
38
|
+
OUT = ROOT / "dist" / f"{NAME}.zip"
|
|
39
|
+
MANIFEST = ROOT / "dist" / f"{NAME}.manifest.json"
|
|
40
|
+
DESC_LIMIT = 1024
|
|
41
|
+
|
|
42
|
+
INCLUDE = ["SKILL.md", "README.md", "package.json", "assets/", "references/", "scripts/", "evals/", "agents/"]
|
|
43
|
+
EXCLUDE_RES = [
|
|
44
|
+
re.compile(r"(^|/)scripts/_"),
|
|
45
|
+
re.compile(r"(^|/)\.env($|\.)"),
|
|
46
|
+
re.compile(r"(^|/)__pycache__(/|$)"),
|
|
47
|
+
re.compile(r"(^|/)node_modules(/|$)"),
|
|
48
|
+
re.compile(r"(^|/)dist(/|$)"),
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
errors: list[str] = []
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def fail(msg: str) -> None:
|
|
55
|
+
errors.append(msg)
|
|
56
|
+
print(f" ✗ {msg}")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def ok(msg: str) -> None:
|
|
60
|
+
print(f" ✓ {msg}")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def read_frontmatter() -> dict:
|
|
64
|
+
text = (ROOT / "SKILL.md").read_text(encoding="utf-8")
|
|
65
|
+
m = re.match(r"^---\r?\n([\s\S]*?)\r?\n---", text)
|
|
66
|
+
if not m:
|
|
67
|
+
fail("SKILL.md 缺 frontmatter")
|
|
68
|
+
return {}
|
|
69
|
+
fm = m.group(1)
|
|
70
|
+
name = re.search(r"^name:\s*(\S+)\s*$", fm, re.M)
|
|
71
|
+
desc = ""
|
|
72
|
+
dm = re.search(r'^description:\s*"([^"]*)"\s*$', fm, re.M)
|
|
73
|
+
if dm:
|
|
74
|
+
desc = dm.group(1).strip()
|
|
75
|
+
else:
|
|
76
|
+
dm = re.search(r"^description:\s*>\-\s*\n((?:[ \t]+.*\n?)+)", fm, re.M)
|
|
77
|
+
if dm:
|
|
78
|
+
desc = " ".join(line.strip() for line in dm.group(1).splitlines() if line.strip())
|
|
79
|
+
return {"name": name.group(1) if name else "", "description": desc}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def check() -> bool:
|
|
83
|
+
print(f"[check] {NAME}")
|
|
84
|
+
fm = read_frontmatter()
|
|
85
|
+
if fm.get("name") != NAME:
|
|
86
|
+
fail(f"frontmatter name={fm.get('name')!r} 与目录名 {NAME!r} 不一致")
|
|
87
|
+
else:
|
|
88
|
+
ok("name 与目录名一致")
|
|
89
|
+
desc = fm.get("description", "")
|
|
90
|
+
if not desc:
|
|
91
|
+
fail("description 为空")
|
|
92
|
+
elif len(desc) > DESC_LIMIT:
|
|
93
|
+
fail(f"description {len(desc)} 字符,超过 {DESC_LIMIT}")
|
|
94
|
+
else:
|
|
95
|
+
ok(f"description {len(desc)} 字符")
|
|
96
|
+
|
|
97
|
+
for f in ("README.md", "package.json"):
|
|
98
|
+
if not (ROOT / f).is_file():
|
|
99
|
+
fail(f"缺 {f}")
|
|
100
|
+
else:
|
|
101
|
+
ok(f"有 {f}")
|
|
102
|
+
try:
|
|
103
|
+
pkg = json.loads((ROOT / "package.json").read_text(encoding="utf-8"))
|
|
104
|
+
if pkg.get("name") != NAME:
|
|
105
|
+
fail(f"package.json name={pkg.get('name')!r} 与技能名不一致")
|
|
106
|
+
if not re.match(r"^\d+\.\d+\.\d+", str(pkg.get("version", ""))):
|
|
107
|
+
fail("package.json version 非 semver")
|
|
108
|
+
except (OSError, json.JSONDecodeError) as e:
|
|
109
|
+
fail(f"package.json 解析失败: {e}")
|
|
110
|
+
|
|
111
|
+
for p in ROOT.rglob("*"):
|
|
112
|
+
if not p.is_file() or ".git" in p.parts:
|
|
113
|
+
continue
|
|
114
|
+
rel = p.relative_to(ROOT).as_posix()
|
|
115
|
+
for rx in EXCLUDE_RES:
|
|
116
|
+
if rx.search(rel):
|
|
117
|
+
fail(f"不应进包的文件: {rel}")
|
|
118
|
+
break
|
|
119
|
+
ok("无禁用文件")
|
|
120
|
+
|
|
121
|
+
for py in sorted((ROOT / "scripts").glob("*.py")):
|
|
122
|
+
try:
|
|
123
|
+
compile(py.read_text(encoding="utf-8"), str(py), "exec")
|
|
124
|
+
except SyntaxError as e:
|
|
125
|
+
fail(f"{py.name} 编译失败: {e}")
|
|
126
|
+
ok("scripts/*.py 编译通过")
|
|
127
|
+
|
|
128
|
+
refdir = ROOT / "references"
|
|
129
|
+
if refdir.is_dir():
|
|
130
|
+
for md in sorted(refdir.glob("*.md")):
|
|
131
|
+
if md.stat().st_size == 0:
|
|
132
|
+
fail(f"references/{md.name} 为空")
|
|
133
|
+
skill_text = (ROOT / "SKILL.md").read_text(encoding="utf-8")
|
|
134
|
+
for ref in sorted(set(re.findall(r"references/([\w\-.]+\.md)", skill_text))):
|
|
135
|
+
if not (refdir / ref).is_file():
|
|
136
|
+
fail(f"SKILL.md 引用的 references/{ref} 不存在")
|
|
137
|
+
ok("references 完整性通过")
|
|
138
|
+
|
|
139
|
+
if errors:
|
|
140
|
+
print(f"\n[check] {NAME}: {len(errors)} 项失败")
|
|
141
|
+
return False
|
|
142
|
+
print(f"\n[check] {NAME}: 全部通过")
|
|
143
|
+
return True
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def package() -> None:
|
|
147
|
+
if not check():
|
|
148
|
+
sys.exit(1)
|
|
149
|
+
(ROOT / "dist").mkdir(exist_ok=True)
|
|
150
|
+
entries: list[str] = []
|
|
151
|
+
with zipfile.ZipFile(OUT, "w", zipfile.ZIP_DEFLATED) as z:
|
|
152
|
+
for p in sorted(ROOT.rglob("*")):
|
|
153
|
+
if not p.is_file() or ".git" in p.parts:
|
|
154
|
+
continue
|
|
155
|
+
rel = p.relative_to(ROOT)
|
|
156
|
+
rel_posix = rel.as_posix()
|
|
157
|
+
if any(rx.search(rel_posix) for rx in EXCLUDE_RES):
|
|
158
|
+
continue
|
|
159
|
+
if not any(
|
|
160
|
+
rel_posix == inc.rstrip("/") or rel_posix.startswith(inc)
|
|
161
|
+
for inc in INCLUDE
|
|
162
|
+
if inc.endswith("/")
|
|
163
|
+
) and rel_posix not in INCLUDE:
|
|
164
|
+
continue
|
|
165
|
+
z.write(p, arcname=f"{NAME}/{rel_posix}")
|
|
166
|
+
entries.append(rel_posix)
|
|
167
|
+
sha = hashlib.sha256(OUT.read_bytes()).hexdigest()
|
|
168
|
+
pkg = json.loads((ROOT / "package.json").read_text(encoding="utf-8"))
|
|
169
|
+
manifest = {
|
|
170
|
+
"name": NAME,
|
|
171
|
+
"version": pkg.get("version"),
|
|
172
|
+
"built_at": datetime.now(timezone.utc).isoformat(),
|
|
173
|
+
"files": len(entries),
|
|
174
|
+
"sha256": sha,
|
|
175
|
+
"entries": entries,
|
|
176
|
+
}
|
|
177
|
+
MANIFEST.write_text(json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
178
|
+
print(f"\n[package] {OUT} ({len(entries)} 文件, sha256={sha[:12]}…)")
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def main() -> None:
|
|
182
|
+
if "--check" in sys.argv[1:]:
|
|
183
|
+
sys.exit(0 if check() else 1)
|
|
184
|
+
package()
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
if __name__ == "__main__":
|
|
188
|
+
main()
|