create-dwy 0.12.1 → 0.12.2

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.
Files changed (22) hide show
  1. package/package.json +1 -1
  2. package/src/sync.js +13 -6
  3. package/templates/claude-global/rules/Python/dwy-python-core.md +105 -7
  4. package/templates/claude-global/skills//345/205/203/345/267/245/345/205/267/dwy-shared/SKILL.md +255 -0
  5. package/templates/claude-global/skills//345/256/211/345/205/250/dwy-sdk-spec/SKILL.md +232 -0
  6. package/templates/claude-global/skills//345/256/211/345/205/250/dwy-sdk-spec/assets/files-whitelist-snippet.json +22 -0
  7. package/templates/claude-global/skills//345/256/211/345/205/250/dwy-sdk-spec/assets/js-commercial.npmignore +76 -0
  8. package/templates/claude-global/skills//345/256/211/345/205/250/dwy-sdk-spec/assets/python-commercial-MANIFEST.in +37 -0
  9. package/templates/claude-global/skills//345/256/211/345/205/250/dwy-sdk-spec/assets/python-commercial.gitignore +62 -0
  10. package/templates/claude-global/skills//345/256/211/345/205/250/dwy-sdk-spec/references/js-commercial-guide.md +133 -0
  11. package/templates/claude-global/skills//345/256/211/345/205/250/dwy-sdk-spec/references/publish-blacklist.md +106 -0
  12. package/templates/claude-global/skills//345/256/211/345/205/250/dwy-sdk-spec/references/python-commercial-guide.md +147 -0
  13. package/templates/claude-global/skills//345/256/211/345/205/250/dwy-sdk-spec/references/sensitive-patterns.md +112 -0
  14. package/templates/claude-global/skills//345/256/211/345/205/250/dwy-sdk-spec/scripts/check_comments.py +260 -0
  15. package/templates/claude-global/skills//345/256/211/345/205/250/dwy-sdk-spec/scripts/check_js_publish.sh +110 -0
  16. package/templates/claude-global/skills//345/256/211/345/205/250/dwy-sdk-spec/scripts/check_python_publish.sh +171 -0
  17. package/templates/claude-global/skills//345/256/211/345/205/250/dwy-sdk-spec/scripts/detect_sdk.sh +71 -0
  18. package/templates/claude-global/skills//345/256/211/345/205/250/dwy-sdk-spec/scripts/init_config.sh +97 -0
  19. package/templates/claude-global/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-code-flow-docs/SKILL.md +170 -0
  20. package/templates/claude-global/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-code-flow-docs/examples//351/205/215/351/242/235/350/256/241/347/256/227.md +64 -0
  21. package/templates/claude-global/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-code-flow-docs/templates/INDEX.md +7 -0
  22. package/templates/claude-global/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-code-flow-docs/templates/logic-doc.md +49 -0
@@ -0,0 +1,147 @@
1
+ # Python 商业版 SDK 完整打包指南
2
+
3
+ 目标:发布到 PyPI 的 wheel 内**不含 `.py` 源文件**,只含编译后的 `.so` 与必要类型存根。
4
+
5
+ ## 决策树
6
+
7
+ ```
8
+ 模块类型
9
+ ├── 公开 API 入口(如 mypkg/__init__.py)→ 保留 .py(用户要 import)
10
+ ├── 内部实现(_ 开头,如 mypkg/_engine.py)→ 编译为 .so
11
+ └── 类型存根(.pyi)→ 保留(用户 IDE 提示)
12
+ ```
13
+
14
+ ## 工具选择
15
+
16
+ | 工具 | 优势 | 推荐场景 |
17
+ |---|---|---|
18
+ | **Cython** | 成熟、setuptools 集成无缝、编译快 | **默认选择**,纯 Python → .so |
19
+ | Nuitka | 优化更激进、支持整包编译 | 大型业务逻辑、想编译整个包 |
20
+
21
+ 下面以 Cython 为准。
22
+
23
+ ## 项目结构示例
24
+
25
+ ```
26
+ mypkg/ # 包根目录
27
+ ├── pyproject.toml
28
+ ├── MANIFEST.in
29
+ ├── .gitignore
30
+ ├── src/
31
+ │ └── mypkg/
32
+ │ ├── __init__.py # 公开入口(保留 .py,re-export)
33
+ │ ├── client.py # 公开 API(保留 .py)
34
+ │ ├── _engine.py # 内部核心(编译为 .so)
35
+ │ ├── _engine.pyi # 类型存根(保留)
36
+ │ ├── _utils.py # 内部工具(编译为 .so)
37
+ │ └── _utils.pyi # 类型存根(保留)
38
+ ```
39
+
40
+ ## pyproject.toml 配置
41
+
42
+ ```toml
43
+ [build-system]
44
+ requires = ["setuptools>=68.0", "Cython>=3.0"]
45
+ build-backend = "setuptools.build_meta"
46
+
47
+ [project]
48
+ name = "mypkg"
49
+ version = "1.0.0"
50
+ requires-python = ">=3.11"
51
+
52
+ [tool.setuptools]
53
+ include-package-data = true
54
+ zip-safe = false
55
+
56
+ [tool.setuptools.packages.find]
57
+ where = ["src"]
58
+
59
+ [tool.setuptools.package-data]
60
+ mypkg = ["*.pyi", "py.typed"]
61
+
62
+ [tool.setuptools.exclude-package-data]
63
+ mypkg = [
64
+ "_*.py", # 排除以 _ 开头的源文件(已编译为 .so)
65
+ "*.c", # Cython 中间产物
66
+ "*.pyx", # Cython 源
67
+ "*.pyc",
68
+ "__pycache__/*",
69
+ ]
70
+ ```
71
+
72
+ ## setup.py(Cython 入口)
73
+
74
+ ```python
75
+ from setuptools import setup
76
+ from Cython.Build import cythonize
77
+ from pathlib import Path
78
+
79
+ # 找所有 _ 开头的 .py 模块(命名约定:内部模块用 _ 前缀)
80
+ internal_modules = [
81
+ str(p) for p in Path("src/mypkg").glob("_*.py")
82
+ if p.name != "__init__.py"
83
+ ]
84
+
85
+ setup(
86
+ ext_modules=cythonize(
87
+ internal_modules,
88
+ compiler_directives={"language_level": "3"},
89
+ ),
90
+ )
91
+ ```
92
+
93
+ ## MANIFEST.in
94
+
95
+ ```
96
+ graft src
97
+ include README.md LICENSE py.typed
98
+ include src/mypkg/*.pyi
99
+
100
+ # 全局排除源码与中间产物
101
+ global-exclude _*.py
102
+ global-exclude *.pyx *.pxd *.c
103
+ global-exclude *.pyc *.pyo
104
+ global-exclude __pycache__ .DS_Store
105
+ ```
106
+
107
+ `graft src` 含整个 src/,然后 `global-exclude _*.py` 排掉内部源;公开 `.py` 保留(如 `__init__.py` / `client.py` 不以 `_` 开头)。
108
+
109
+ ## 构建命令
110
+
111
+ ```bash
112
+ # 清理旧产物
113
+ rm -rf build/ dist/ src/*.egg-info/
114
+
115
+ # 编译 + 打包
116
+ python -m build
117
+
118
+ # 验证 wheel 内不含 _ 开头的 .py
119
+ unzip -l dist/*.whl | grep -E '_.*\.py$' && echo "FAIL: source leaked" || echo "OK"
120
+ ```
121
+
122
+ ## 验收清单(流程 B 用)
123
+
124
+ | 检查项 | 命令 |
125
+ |---|---|
126
+ | pyproject.toml 含 Cython | `grep -E 'cython\|Cython' pyproject.toml` |
127
+ | `_*.so` 存在 | `find src -name '_*.so' -type f` 应该有输出 |
128
+ | MANIFEST.in 排除源码 | `grep -E 'global-exclude.*_\*\.py' MANIFEST.in` |
129
+ | wheel 内无 `_*.py` | `unzip -l dist/*.whl \| grep -E '_.*\.py$'` 应该为空 |
130
+ | .gitignore 含编译产物 | `grep -E '^\*\.so\b\|^build/\b\|^dist/\b' .gitignore` |
131
+
132
+ ## 常见踩坑
133
+
134
+ | 现象 | 原因 | 修复 |
135
+ |---|---|---|
136
+ | wheel 里有 `.py` 源 | `exclude-package-data` 没匹配到 / MANIFEST.in 缺 global-exclude | 同时配 setup 配置 + MANIFEST.in |
137
+ | 装完后 `ImportError: cannot import name '_engine'` | `__init__.py` 用了 `from . import _engine` 但 .so 命名空间不对 | 确保 `.so` 文件在包内,名字与 import 路径一致 |
138
+ | `pip install -e .` 后 import 慢 | 开发模式没编译 .so,跑了 .py | 开发用 `python setup.py build_ext --inplace` 触发编译 |
139
+ | 跨平台 wheel(manylinux / macOS) | 单平台编译产物只能装到对应平台 | 用 `cibuildwheel`(项目已有 dwy-cibuildwheel skill) |
140
+
141
+ ## 参考 dwy-shared 内已有 SDK
142
+
143
+ `backend/`(`dwyeapi`)当前是**开源版**(pyproject.toml 无 Cython 配置)。若要转商业版:
144
+ 1. 把 `src/dwyeapi/security.py` / `database.py` 重命名为 `_security.py` / `_database.py`
145
+ 2. `__init__.py` 加 re-export 维持公开 API
146
+ 3. 加 Cython 配置、MANIFEST.in、setup.py
147
+ 4. 重新发布 → 装完后用户 import 路径不变,但拿不到 .py 源
@@ -0,0 +1,112 @@
1
+ # SDK 接口注释敏感模式清单
2
+
3
+ `check_comments.py` 的规则源。SDK 对外注释(Python docstring、JSDoc、TSDoc)**只写**功能含义、入参格式、返回值、异常,下列模式一律视为泄露。
4
+
5
+ ## 模式表
6
+
7
+ | 模式 ID | 正则 | 风险 | 误报豁免(如有) |
8
+ |---|---|---|---|
9
+ | `internal-domain` | `[\w-]+\.(internal\|corp\|local\|intranet)\b` | 暴露内网域名结构 | 出现在 example.com / your-domain 等示例语 → 视为示例 |
10
+ | `private-ip` | `\b(10\.\d+\.\d+\.\d+\|192\.168\.\d+\.\d+\|172\.(1[6-9]\|2\d\|3[01])\.\d+\.\d+)\b` | 暴露内网 IP | — |
11
+ | `sql-statement` | `\b(SELECT\|UPDATE\|DELETE\|INSERT)\b[^.]*\b(FROM\|WHERE\|SET\|INTO)\b` | 暴露表结构、查询逻辑 | 行含 `example`/`e.g.` 视作示例语 |
12
+ | `redis-key-pattern` | `\b(cache\|redis\|fmt\|lock\|queue):[a-z_]+:` | 暴露缓存键 schema | — |
13
+ | `jira-ticket` | `\b[A-Z]{2,6}-\d{2,6}\b` | 暴露内部工单系统 | 行含「示例」「example」豁免 |
14
+ | `internal-service` | `\b[a-z_]+_(service\|server\|grpc\|rpc)_(v\d\|prod\|stg\|dev)\b` | 暴露内部服务命名 | — |
15
+ | `service-port` | `:(\d{4,5})\b` 且行内含 `connect\|server\|host\|endpoint\|address` | 暴露内网服务端口 | 默认端口 80/443/8080 豁免,example 示例豁免 |
16
+ | `internal-email` | `[\w.+-]+@(?!example\.com\|gmail\.com\|outlook\.com)[\w.-]+\.[a-z]{2,}` | 暴露员工邮箱 | 公开公司域名(github.io 等)豁免 |
17
+ | `aws-arn` | `arn:aws:[a-z0-9-]+:[a-z0-9-]*:\d+:[a-zA-Z0-9-_/:]+` | 暴露 AWS 账号 ID 与资源 | — |
18
+ | `cred-keyword` | `\b(API_KEY\|TOKEN\|SECRET\|PASSWORD\|PRIVATE_KEY)\s*[=:]\s*['"][\w-]+['"]` | 注释里硬编码凭证 | 占位 `xxx` / `<your-token>` 豁免 |
19
+ | `aws-key` | `\bAKIA[0-9A-Z]{16}\b` | AWS Access Key ID | — |
20
+
21
+ ## 正例 vs 反例
22
+
23
+ ### Python docstring
24
+
25
+ **反例**:
26
+
27
+ ```python
28
+ def get_user(user_id: str):
29
+ """
30
+ Connect to https://users.internal.corp.com/v2/profile via gRPC.
31
+ Query SELECT * FROM users_prod WHERE id = ? with cache key cache:user:{id}.
32
+ Owner: john.doe@anthropic.io. See PAY-1234 for context.
33
+ """
34
+ ```
35
+
36
+ **正例**:
37
+
38
+ ```python
39
+ def get_user(user_id: str):
40
+ """
41
+ 获取用户基本资料。
42
+
43
+ Args:
44
+ user_id: 用户唯一标识符(UUID 格式)。
45
+
46
+ Returns:
47
+ UserProfile,含 name / email / created_at 字段。
48
+
49
+ Raises:
50
+ UserNotFoundError: user_id 不存在时抛出。
51
+ AuthenticationError: 当前会话无权限读取该用户资料。
52
+
53
+ Example:
54
+ >>> user = get_user("user-123")
55
+ >>> user.name
56
+ 'Alice'
57
+ """
58
+ ```
59
+
60
+ ### TypeScript JSDoc
61
+
62
+ **反例**:
63
+
64
+ ```typescript
65
+ /**
66
+ * Fetch user from db.internal.corp:5432 (table users_prod).
67
+ * Uses Redis cache key fmt:user:{id} ttl 3600s.
68
+ * Fallback to backup_db at 10.0.0.5:5432 if primary down.
69
+ * See JIRA USR-456, contact #user-platform on internal Slack.
70
+ */
71
+ export async function fetchUser(id: string): Promise<UserProfile> { ... }
72
+ ```
73
+
74
+ **正例**:
75
+
76
+ ```typescript
77
+ /**
78
+ * 获取用户基本资料。
79
+ *
80
+ * @param id 用户唯一标识符(UUID 格式)
81
+ * @returns 用户资料对象,含 name / email / created_at 字段
82
+ * @throws {UserNotFoundError} id 不存在时抛出
83
+ * @throws {AuthenticationError} 当前会话无权限读取该用户资料
84
+ *
85
+ * @example
86
+ * ```ts
87
+ * const user = await fetchUser("user-123");
88
+ * console.log(user.name);
89
+ * ```
90
+ */
91
+ export async function fetchUser(id: string): Promise<UserProfile> { ... }
92
+ ```
93
+
94
+ ## 应该写什么 vs 不应该写什么
95
+
96
+ | 类别 | 应该写 | 不应该写 |
97
+ |---|---|---|
98
+ | 接口功能 | 业务语义(「获取用户资料」) | 实现路径(「调用 gRPC service xxx」) |
99
+ | 入参 | 类型、格式、约束(UUID、长度上限) | 上游表字段、内部 schema 字段名 |
100
+ | 返回 | 字段语义 | 来自哪张表、哪个缓存键 |
101
+ | 异常 | 业务异常类、触发条件 | 数据库连接错误细节、内部 RPC 失败码 |
102
+ | 示例 | 公开占位(`user-123` / `example.com`) | 真实用户 ID、真实邮箱、内网域名 |
103
+ | 链接 | 公开文档 URL、相对路径文档 | 内网 wiki、JIRA、Slack、Confluence 链接 |
104
+
105
+ ## 行内豁免约定
106
+
107
+ 误报场景在该行追加注释:
108
+
109
+ - Python:`# sdk-spec: ignore`
110
+ - TS/JS:`// sdk-spec: ignore`
111
+
112
+ 豁免**单行**,不豁免整段。要长期豁免某文件 → 在 `~/.config/dwy-sdk-spec/projects.json` 的对应项目下加 `ignore_files: ["path/to/file.py"]`(未来支持,当前手动用行内豁免)。
@@ -0,0 +1,260 @@
1
+ #!/usr/bin/env python3
2
+ """扫 Python docstring 与 JS/TS JSDoc 中的敏感模式。
3
+
4
+ 用法:
5
+ check_comments.py <root> [--sdk-paths <json>] [--format text|json]
6
+
7
+ 退出码:
8
+ 0 = 无违规
9
+ 1 = 有违规
10
+ 2 = 用法错误
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import argparse
15
+ import ast
16
+ import json
17
+ import re
18
+ import sys
19
+ from dataclasses import dataclass, asdict
20
+ from pathlib import Path
21
+ from typing import Iterable
22
+
23
+
24
+ # 敏感模式表,与 references/sensitive-patterns.md 对齐
25
+ PATTERNS: list[tuple[str, re.Pattern[str]]] = [
26
+ ("internal-domain", re.compile(r"\b[\w-]+\.(internal|corp|local|intranet)\b", re.I)),
27
+ ("private-ip", re.compile(
28
+ r"\b(?:10\.\d{1,3}\.\d{1,3}\.\d{1,3}"
29
+ r"|192\.168\.\d{1,3}\.\d{1,3}"
30
+ r"|172\.(?:1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3})\b"
31
+ )),
32
+ ("sql-statement", re.compile(
33
+ r"\b(SELECT|UPDATE|DELETE|INSERT)\b[^.\n]{0,80}\b(FROM|WHERE|SET|INTO)\b", re.I
34
+ )),
35
+ ("redis-key-pattern", re.compile(r"\b(cache|redis|fmt|lock|queue):[a-z_]+:")),
36
+ ("jira-ticket", re.compile(r"\b[A-Z]{2,6}-\d{2,6}\b")),
37
+ ("internal-service", re.compile(
38
+ r"\b[a-z_]+_(?:service|server|grpc|rpc)_(?:v\d|prod|stg|stage|dev)\b"
39
+ )),
40
+ ("aws-arn", re.compile(r"arn:aws:[a-z0-9-]+:[a-z0-9-]*:\d+:[a-zA-Z0-9-_/:]+")),
41
+ ("aws-key", re.compile(r"\bAKIA[0-9A-Z]{16}\b")),
42
+ ("cred-keyword", re.compile(
43
+ r"\b(API_KEY|TOKEN|SECRET|PASSWORD|PRIVATE_KEY)\s*[=:]\s*['\"][\w\-./+]{6,}['\"]"
44
+ )),
45
+ ]
46
+
47
+ # 弱模式(仅在行内有上下文关键字时命中,降误报)
48
+ PORT_PATTERN = re.compile(r":(\d{4,5})\b")
49
+ PORT_CONTEXT = re.compile(r"\b(connect|server|host|endpoint|address|listen|bind)\b", re.I)
50
+
51
+ EMAIL_PATTERN = re.compile(r"[\w.+-]+@[\w.-]+\.[a-z]{2,}")
52
+ PUBLIC_EMAIL_DOMAINS = {
53
+ "example.com", "example.org", "example.net",
54
+ "gmail.com", "outlook.com", "hotmail.com", "qq.com", "163.com",
55
+ "yourdomain.com", "your-domain.com", "domain.com",
56
+ }
57
+
58
+ # 行内示例 / 占位关键字 → 整行视为示例不告警
59
+ EXAMPLE_HINTS = re.compile(r"\b(example|示例|e\.g\.|placeholder|占位|your[- ]?domain|<.*>)\b", re.I)
60
+
61
+ # 行内豁免
62
+ IGNORE_MARKER = re.compile(r"#\s*sdk-spec:\s*ignore|//\s*sdk-spec:\s*ignore")
63
+
64
+
65
+ @dataclass
66
+ class Violation:
67
+ file: str
68
+ line: int
69
+ pattern: str
70
+ match: str
71
+ context: str
72
+
73
+
74
+ def scan_text(file_rel: str, text: str, start_line_offset: int = 0) -> list[Violation]:
75
+ """扫一段连续文本(docstring 或注释块),返回违规列表。
76
+
77
+ `text` 内部带换行,`start_line_offset` 是该文本第一行在文件中的实际行号。
78
+ """
79
+ out: list[Violation] = []
80
+ for i, line in enumerate(text.splitlines(), start=start_line_offset):
81
+ if IGNORE_MARKER.search(line):
82
+ continue
83
+ line_stripped = line.strip()
84
+ if not line_stripped:
85
+ continue
86
+
87
+ is_example = bool(EXAMPLE_HINTS.search(line))
88
+
89
+ # 主模式
90
+ for pat_name, pat in PATTERNS:
91
+ for m in pat.finditer(line):
92
+ if is_example and pat_name in {"sql-statement", "internal-domain", "jira-ticket"}:
93
+ continue
94
+ out.append(Violation(
95
+ file=file_rel, line=i,
96
+ pattern=pat_name, match=m.group(0),
97
+ context=line_stripped[:160],
98
+ ))
99
+
100
+ # 端口(弱模式)
101
+ if not is_example and PORT_CONTEXT.search(line):
102
+ for m in PORT_PATTERN.finditer(line):
103
+ port = int(m.group(1))
104
+ if port in {8080, 8000, 3000, 5000, 5173, 5432, 6379, 9000}:
105
+ # 常见示例端口,跳过
106
+ continue
107
+ out.append(Violation(
108
+ file=file_rel, line=i,
109
+ pattern="service-port", match=m.group(0),
110
+ context=line_stripped[:160],
111
+ ))
112
+
113
+ # 邮箱(弱模式)
114
+ if not is_example:
115
+ for m in EMAIL_PATTERN.finditer(line):
116
+ email = m.group(0)
117
+ domain = email.split("@", 1)[1].lower()
118
+ if domain in PUBLIC_EMAIL_DOMAINS:
119
+ continue
120
+ out.append(Violation(
121
+ file=file_rel, line=i,
122
+ pattern="internal-email", match=email,
123
+ context=line_stripped[:160],
124
+ ))
125
+ return out
126
+
127
+
128
+ def scan_python_file(path: Path, root: Path) -> list[Violation]:
129
+ rel = str(path.relative_to(root))
130
+ try:
131
+ source = path.read_text(encoding="utf-8")
132
+ except (UnicodeDecodeError, OSError):
133
+ return []
134
+ try:
135
+ tree = ast.parse(source)
136
+ except SyntaxError:
137
+ return []
138
+
139
+ out: list[Violation] = []
140
+ # 模块、函数、类、异步函数的 docstring
141
+ for node in ast.walk(tree):
142
+ if not isinstance(node, (ast.Module, ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
143
+ continue
144
+ doc = ast.get_docstring(node, clean=False)
145
+ if not doc:
146
+ continue
147
+ # docstring 首行在 node.body[0].lineno(第一条 Expr 是字符串)
148
+ first_stmt = node.body[0] if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)) else node.body[0]
149
+ start_line = getattr(first_stmt, "lineno", 1)
150
+ out.extend(scan_text(rel, doc, start_line_offset=start_line))
151
+ return out
152
+
153
+
154
+ # 匹配 JS/TS 的 /** ... */ JSDoc 块(不含其他 /* */ 注释)
155
+ JSDOC_BLOCK = re.compile(r"/\*\*(.*?)\*/", re.DOTALL)
156
+ # 行注释 //
157
+ LINE_COMMENT = re.compile(r"^\s*//\s?(.*)$")
158
+
159
+
160
+ def scan_js_file(path: Path, root: Path) -> list[Violation]:
161
+ rel = str(path.relative_to(root))
162
+ try:
163
+ source = path.read_text(encoding="utf-8")
164
+ except (UnicodeDecodeError, OSError):
165
+ return []
166
+
167
+ out: list[Violation] = []
168
+
169
+ # JSDoc 块
170
+ for m in JSDOC_BLOCK.finditer(source):
171
+ block_text = m.group(1)
172
+ # 起始行号
173
+ start_line = source.count("\n", 0, m.start()) + 1
174
+ # 去掉每行的 leading * 和空白
175
+ cleaned_lines = []
176
+ for line in block_text.splitlines():
177
+ cleaned_lines.append(re.sub(r"^\s*\*\s?", "", line))
178
+ out.extend(scan_text(rel, "\n".join(cleaned_lines), start_line_offset=start_line))
179
+
180
+ # 行内注释(只扫描,因为商业 SDK 内部实现的行注释虽然不打包但开发期能看到)
181
+ # 注:源文件其实是否打包取决于配置,行注释泄露主要看是否打包 src/
182
+ # 跨边界谨慎起见也扫
183
+ for i, line in enumerate(source.splitlines(), start=1):
184
+ lm = LINE_COMMENT.match(line)
185
+ if not lm:
186
+ continue
187
+ comment = lm.group(1)
188
+ out.extend(scan_text(rel, comment, start_line_offset=i))
189
+
190
+ return out
191
+
192
+
193
+ def iter_target_files(root: Path, sdk_paths: list[str] | None) -> Iterable[Path]:
194
+ """枚举要扫的文件。sdk_paths 为 None 时全扫;否则只扫指定路径。"""
195
+ excludes = {
196
+ "node_modules", ".venv", "venv", "dist", "build",
197
+ ".git", "__pycache__", ".mypy_cache", ".pytest_cache",
198
+ ".ruff_cache", ".tox", "egg-info", "site-packages",
199
+ }
200
+ bases = [root / p for p in sdk_paths] if sdk_paths else [root]
201
+ for base in bases:
202
+ if not base.exists():
203
+ continue
204
+ for p in base.rglob("*"):
205
+ if not p.is_file():
206
+ continue
207
+ if any(part in excludes or part.endswith(".egg-info") for part in p.parts):
208
+ continue
209
+ suf = p.suffix.lower()
210
+ if suf in {".py", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"}:
211
+ yield p
212
+
213
+
214
+ def main() -> int:
215
+ parser = argparse.ArgumentParser(description="Scan SDK comments for sensitive patterns.")
216
+ parser.add_argument("root", help="Project root directory (absolute path)")
217
+ parser.add_argument("--sdk-paths", default=None,
218
+ help='JSON {"python":[...], "js":[...]} relative to root. Omit to scan all.')
219
+ parser.add_argument("--format", choices=["text", "json"], default="text")
220
+ args = parser.parse_args()
221
+
222
+ root = Path(args.root).resolve()
223
+ if not root.is_dir():
224
+ print(f"ERROR: not a directory: {root}", file=sys.stderr)
225
+ return 2
226
+
227
+ target_paths: list[str] | None = None
228
+ if args.sdk_paths:
229
+ try:
230
+ sdk_spec = json.loads(args.sdk_paths)
231
+ except json.JSONDecodeError as e:
232
+ print(f"ERROR: invalid --sdk-paths JSON: {e}", file=sys.stderr)
233
+ return 2
234
+ target_paths = []
235
+ for key in ("python", "js"):
236
+ target_paths.extend(sdk_spec.get(key, []))
237
+
238
+ violations: list[Violation] = []
239
+ for fp in iter_target_files(root, target_paths):
240
+ if fp.suffix == ".py":
241
+ violations.extend(scan_python_file(fp, root))
242
+ else:
243
+ violations.extend(scan_js_file(fp, root))
244
+
245
+ if args.format == "json":
246
+ print(json.dumps([asdict(v) for v in violations], ensure_ascii=False, indent=2))
247
+ else:
248
+ if not violations:
249
+ print("OK: no sensitive patterns found")
250
+ else:
251
+ print(f"FAIL: {len(violations)} violation(s) found:\n")
252
+ for v in violations:
253
+ print(f" {v.file}:{v.line} [{v.pattern}] match={v.match!r}")
254
+ print(f" > {v.context}")
255
+
256
+ return 1 if violations else 0
257
+
258
+
259
+ if __name__ == "__main__":
260
+ sys.exit(main())
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env bash
2
+ # JS/TS SDK 发布前文件清单审计(npm pack --dry-run)
3
+ # 用法: check_js_publish.sh <js_sdk_root> --edition commercial|opensource
4
+
5
+ set -euo pipefail
6
+
7
+ if [[ $# -lt 3 ]]; then
8
+ echo "Usage: $0 <js_sdk_root> --edition commercial|opensource" >&2
9
+ exit 2
10
+ fi
11
+
12
+ PKG_ROOT="$1"
13
+ shift
14
+
15
+ EDITION=""
16
+ while [[ $# -gt 0 ]]; do
17
+ case "$1" in
18
+ --edition) EDITION="$2"; shift 2 ;;
19
+ *) echo "ERROR: unknown arg: $1" >&2; exit 2 ;;
20
+ esac
21
+ done
22
+
23
+ if [[ "$EDITION" != "commercial" && "$EDITION" != "opensource" ]]; then
24
+ echo "ERROR: --edition must be 'commercial' or 'opensource'" >&2
25
+ exit 2
26
+ fi
27
+
28
+ if [[ ! -d "$PKG_ROOT" ]]; then
29
+ echo "ERROR: not a directory: $PKG_ROOT" >&2
30
+ exit 2
31
+ fi
32
+
33
+ if ! command -v npm >/dev/null 2>&1; then
34
+ echo "ERROR: npm not found in PATH" >&2
35
+ exit 2
36
+ fi
37
+
38
+ echo "[1/2] Running npm pack --dry-run in $PKG_ROOT ..."
39
+
40
+ # npm pack --dry-run --json 输出包内文件列表
41
+ # 注:某些 npm 版本 --json 输出到 stderr,--silent 抑制非 JSON 输出
42
+ PACK_OUTPUT=$(cd "$PKG_ROOT" && npm pack --dry-run --json 2>/dev/null) || {
43
+ echo "ERROR: npm pack --dry-run failed; run 'npm install' first?" >&2
44
+ exit 2
45
+ }
46
+
47
+ # 提取文件路径列表
48
+ FILES=$(echo "$PACK_OUTPUT" | jq -r '.[0].files[].path' 2>/dev/null) || {
49
+ echo "ERROR: failed to parse npm pack output:" >&2
50
+ echo "$PACK_OUTPUT" | head -20 >&2
51
+ exit 2
52
+ }
53
+
54
+ echo "[2/2] Audit result (edition=$EDITION):"
55
+ echo "----------------------------------------"
56
+
57
+ # 致命级(任何版本都禁)
58
+ CRITICAL_REGEX='(^|/)(\.env(\..+)?|\.npmrc|\.pypirc|\.git-credentials|id_rsa|id_ed25519|secrets\.json|credentials\.json)$|\.(pem|key|p12)$|(^|/)\.aws/credentials$'
59
+
60
+ # 开源版黑名单
61
+ OPENSOURCE_DENY_REGEX='(^|/)(src|tests|__tests__|\.git|\.github|\.gitlab-ci\.yml|\.travis\.yml|\.circleci|\.vscode|\.idea|node_modules|\.husky)/|(^|/)(tsconfig.*\.json|vite\.config\.|vitest\.config\.|tsup\.config\.|rollup\.config\.|webpack\.config\.|jest\.config\.|\.eslintrc.*|\.prettierrc.*|eslint\.config\.|prettier\.config\.|commitlint\.config\.|\.gitignore|\.gitattributes|\.DS_Store|Thumbs\.db|pnpm-lock\.yaml|package-lock\.json|yarn\.lock)|\.(map|tsbuildinfo|swp|swo)$|\.(test|spec)\.(ts|tsx|js|jsx)$'
62
+
63
+ # 商业版白名单(其他视为违规)
64
+ COMMERCIAL_ALLOW_REGEX='^(dist/|bin/|package\.json$|README(\..+)?$|LICEN[SC]E(\..+)?$|CHANGELOG(\..+)?$)'
65
+
66
+ violations=()
67
+ critical=()
68
+
69
+ while IFS= read -r f; do
70
+ [[ -z "$f" ]] && continue
71
+
72
+ if [[ "$f" =~ $CRITICAL_REGEX ]]; then
73
+ critical+=("$f")
74
+ continue
75
+ fi
76
+
77
+ if [[ "$EDITION" == "opensource" ]]; then
78
+ if [[ "$f" =~ $OPENSOURCE_DENY_REGEX ]]; then
79
+ violations+=("$f [denied by opensource rules]")
80
+ fi
81
+ else
82
+ if [[ ! "$f" =~ $COMMERCIAL_ALLOW_REGEX ]]; then
83
+ violations+=("$f [not in commercial whitelist]")
84
+ fi
85
+ fi
86
+ done <<< "$FILES"
87
+
88
+ if [[ ${#critical[@]} -gt 0 ]]; then
89
+ echo "[CRITICAL] secret-like files in package — DO NOT PUBLISH:"
90
+ for f in "${critical[@]}"; do
91
+ echo " $f"
92
+ done
93
+ echo ""
94
+ fi
95
+
96
+ if [[ ${#violations[@]} -gt 0 ]]; then
97
+ echo "[VIOLATION] ${#violations[@]} file(s) violate $EDITION rules:"
98
+ for v in "${violations[@]}"; do
99
+ echo " $v"
100
+ done
101
+ echo ""
102
+ exit 1
103
+ fi
104
+
105
+ if [[ ${#critical[@]} -gt 0 ]]; then
106
+ exit 1
107
+ fi
108
+
109
+ count=$(echo "$FILES" | grep -c .)
110
+ echo "OK: package passes $EDITION audit ($count file(s))"