create-dwy 0.5.1 → 0.6.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.
Files changed (156) hide show
  1. package/bin/index.js +1 -1
  2. package/package.json +2 -2
  3. package/src/create.js +61 -37
  4. package/src/sync.js +212 -61
  5. package/templates/claude-global/CLAUDE.md +92 -0
  6. package/templates/claude-global/rules/dwy-android-core.md +610 -0
  7. package/templates/claude-global/rules/dwy-api-design.md +110 -0
  8. package/templates/claude-global/rules/dwy-db-migration.md +46 -0
  9. package/templates/claude-global/rules/dwy-flutter-core.md +564 -0
  10. package/templates/claude-global/rules/dwy-flutter-network.md +222 -0
  11. package/templates/claude-global/rules/dwy-flutter-router.md +97 -0
  12. package/templates/claude-global/rules/dwy-flutter-testing.md +94 -0
  13. package/templates/claude-global/rules/dwy-git-commit-convention.md +82 -0
  14. package/templates/claude-global/rules/dwy-git-security.md +53 -0
  15. package/templates/claude-global/rules/dwy-payload-limits.md +178 -0
  16. package/templates/claude-global/rules/{prefer-open-source.md → dwy-prefer-open-source.md} +4 -0
  17. package/templates/claude-global/rules/dwy-project-structure.md +181 -0
  18. package/templates/claude-global/rules/{python-code-style.md → dwy-python-core.md} +292 -358
  19. package/templates/claude-global/rules/dwy-python-fastapi.md +272 -0
  20. package/templates/claude-global/rules/dwy-python-orm.md +61 -0
  21. package/templates/claude-global/rules/{backend-security.md → dwy-python-security.md} +99 -72
  22. package/templates/claude-global/rules/dwy-python-testing.md +53 -0
  23. package/templates/claude-global/rules/{server-security.md → dwy-server-security.md} +1 -2
  24. package/templates/claude-global/rules/dwy-tdd-development.md +70 -0
  25. package/templates/claude-global/rules/dwy-tech-stack.md +60 -0
  26. package/templates/claude-global/rules/dwy-vue-api.md +146 -0
  27. package/templates/claude-global/rules/{vue-code-style.md → dwy-vue-core.md} +338 -152
  28. package/templates/claude-global/rules/dwy-vue-pinia.md +75 -0
  29. package/templates/claude-global/rules/dwy-vue-testing.md +102 -0
  30. package/templates/claude-global/skills/dwy-deploy-audit/SKILL.md +481 -0
  31. package/templates/claude-global/skills/dwy-deploy-audit/references/checks-data.md +117 -0
  32. package/templates/claude-global/skills/dwy-deploy-audit/references/checks-network.md +74 -0
  33. package/templates/claude-global/skills/dwy-deploy-audit/references/checks-runtime.md +184 -0
  34. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_base.sh +39 -0
  35. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_capacity.sh +278 -0
  36. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_db.sh +122 -0
  37. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_docker.sh +249 -0
  38. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_env.sh +64 -0
  39. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_https.sh +60 -0
  40. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_logs.sh +209 -0
  41. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_nginx.sh +92 -0
  42. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_resilience.sh +157 -0
  43. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_secrets.sh +327 -0
  44. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_services.sh +77 -0
  45. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_ssh.sh +56 -0
  46. package/templates/claude-global/skills/dwy-deploy-audit/scripts/run_all.sh +105 -0
  47. package/templates/claude-global/skills/dwy-docker/SKILL.md +187 -0
  48. package/templates/claude-global/skills/dwy-docker/references/ask-templates.md +142 -0
  49. package/templates/claude-global/skills/dwy-docker/references/templates.md +479 -0
  50. package/templates/claude-global/skills/dwy-docker/references/version-rules.md +76 -0
  51. package/templates/claude-global/skills/dwy-docker/scripts/query_dockerhub.py +204 -0
  52. package/templates/claude-global/skills/dwy-docker/scripts/scan_images.sh +185 -0
  53. package/templates/claude-global/skills/dwy-eapi/SKILL.md +433 -0
  54. package/templates/claude-global/skills/dwy-eapi/references/tasks-integration-guide.md +387 -0
  55. package/templates/claude-global/skills/dwy-ekit/SKILL.md +85 -0
  56. package/templates/claude-global/skills/dwy-ekit/references/data.md +135 -0
  57. package/templates/claude-global/skills/dwy-ekit/references/form.md +68 -0
  58. package/templates/claude-global/skills/dwy-ekit/references/request.md +183 -0
  59. package/templates/claude-global/skills/dwy-ekit/references/ui.md +109 -0
  60. package/templates/claude-global/skills/{dwy-frontend-eui → dwy-eui}/SKILL.md +129 -15
  61. package/templates/claude-global/skills/dwy-eui/references/eui-design-guide.md +257 -0
  62. package/templates/claude-global/skills/dwy-eui/references/eui-integration-guide.md +302 -0
  63. package/templates/claude-global/skills/dwy-eui/references/eui-landing-design-guide.md +437 -0
  64. package/templates/claude-global/skills/dwy-fullstack-scaffold/SKILL.md +214 -0
  65. package/templates/claude-global/skills/dwy-mirror-source/SKILL.md +275 -0
  66. package/templates/claude-global/skills/dwy-mirror-source/references/dockerfile-snippets.md +228 -0
  67. package/templates/claude-global/skills/dwy-mirror-source/references/mirror-providers.md +150 -0
  68. package/templates/claude-global/skills/dwy-mirror-source/scripts/__pycache__/check_mirrors.cpython-313.pyc +0 -0
  69. package/templates/claude-global/skills/dwy-mirror-source/scripts/apply_mirrors.py +666 -0
  70. package/templates/claude-global/skills/dwy-mirror-source/scripts/check_mirrors.py +868 -0
  71. package/templates/claude-global/skills/dwy-pentest/SKILL.md +344 -0
  72. package/templates/claude-global/skills/dwy-pentest/scripts/lib/__init__.py +1 -0
  73. package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/__init__.cpython-311.pyc +0 -0
  74. package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/config.cpython-311.pyc +0 -0
  75. package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/executor.cpython-311.pyc +0 -0
  76. package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/severity.cpython-311.pyc +0 -0
  77. package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/validator.cpython-311.pyc +0 -0
  78. package/templates/claude-global/skills/dwy-pentest/scripts/lib/config.py +109 -0
  79. package/templates/claude-global/skills/dwy-pentest/scripts/lib/executor.py +157 -0
  80. package/templates/claude-global/skills/dwy-pentest/scripts/lib/severity.py +96 -0
  81. package/templates/claude-global/skills/dwy-pentest/scripts/lib/validator.py +113 -0
  82. package/templates/claude-global/skills/dwy-pentest/scripts/pentest_auth.py +289 -0
  83. package/templates/claude-global/skills/dwy-pentest/scripts/pentest_recon.py +217 -0
  84. package/templates/claude-global/skills/dwy-pentest/scripts/pentest_report.py +343 -0
  85. package/templates/claude-global/skills/dwy-pentest/scripts/pentest_scan.py +205 -0
  86. package/templates/claude-global/skills/dwy-pentest/scripts/pentest_ssl.py +208 -0
  87. package/templates/claude-global/skills/dwy-pentest/scripts/pentest_vuln.py +205 -0
  88. package/templates/claude-global/skills/dwy-pentest/scripts/pentest_web.py +330 -0
  89. package/templates/claude-global/skills/dwy-pentest/scripts/pyproject.toml +14 -0
  90. package/templates/claude-global/skills/dwy-pentest/scripts/setup_tools.sh +97 -0
  91. package/templates/claude-global/skills/dwy-pentest/scripts/uv.lock +415 -0
  92. package/templates/claude-global/skills/{dwy-release → dwy-publish}/SKILL.md +42 -18
  93. package/templates/claude-global/skills/dwy-vue-core/SKILL.md +890 -0
  94. package/templates/project/backend/.dwy.hbs +5 -0
  95. package/templates/project/backend/.python-version +1 -0
  96. package/templates/project/backend/CLAUDE.md.hbs +40 -0
  97. package/templates/project/{web/backend → backend}/alembic/script.py.mako +2 -0
  98. package/templates/project/{web/backend → backend}/alembic.ini.hbs +0 -1
  99. package/templates/project/backend/deploy.sh.hbs +18 -0
  100. package/templates/project/{web → backend}/docker-compose.prod.yml.hbs +1 -1
  101. package/templates/project/{web/backend → backend}/pyproject.toml.hbs +4 -5
  102. package/templates/project/{web/backend → backend}/src/app/config.py.hbs +1 -1
  103. package/templates/project/backend/src/app/database.py +10 -0
  104. package/templates/project/backend/src/app/dependencies.py +5 -0
  105. package/templates/project/{web/backend → backend}/src/app/main.py.hbs +3 -3
  106. package/templates/project/backend/src/app/models/base.py +5 -0
  107. package/templates/project/web/CLAUDE.md.hbs +29 -36
  108. package/templates/project/web/{frontend → apps/web}/index.html.hbs +1 -0
  109. package/templates/project/web/{frontend → apps/web}/package.json.hbs +4 -3
  110. package/templates/project/web/apps/web/src/App.vue +9 -0
  111. package/templates/project/web/apps/web/src/api/client.ts +5 -0
  112. package/templates/project/web/apps/web/src/composables/.gitkeep +0 -0
  113. package/templates/project/web/apps/web/src/layouts/DefaultLayout.vue +8 -0
  114. package/templates/project/web/{frontend → apps/web}/src/router/index.ts +1 -1
  115. package/templates/project/web/{frontend → apps/web}/src/stores/auth.ts +4 -2
  116. package/templates/project/web/apps/web/src/style.css +2 -0
  117. package/templates/project/web/apps/web/src/types/router.d.ts +8 -0
  118. package/templates/project/web/apps/web/src/views/HomeView.vue +8 -0
  119. package/templates/project/web/apps/web/tsconfig.app.json +18 -0
  120. package/templates/project/web/apps/web/tsconfig.json +6 -0
  121. package/templates/project/web/{frontend → apps/web}/vite.config.ts.hbs +7 -1
  122. package/templates/project/web/package.json.hbs +16 -0
  123. package/templates/project/web/packages/.gitkeep +0 -0
  124. package/templates/project/web/pnpm-workspace.yaml +3 -0
  125. package/templates/claude-global/hooks/post-sync-claude-global.sh +0 -72
  126. package/templates/claude-global/rules/agent-team-execution.md +0 -138
  127. package/templates/claude-global/rules/agent-team-patterns.md +0 -157
  128. package/templates/claude-global/rules/git-commit-convention.md +0 -106
  129. package/templates/claude-global/rules/git-security.md +0 -144
  130. package/templates/claude-global/skills/dwy-backend-base/SKILL.md +0 -279
  131. package/templates/claude-global/skills/dwy-frontend-core/SKILL.md +0 -181
  132. package/templates/project/web/.claude/commands/deploy.md +0 -4
  133. package/templates/project/web/backend/src/app/models/base.py +0 -5
  134. package/templates/project/web/deploy.sh.hbs +0 -113
  135. package/templates/project/web/frontend/src/App.vue +0 -3
  136. package/templates/project/web/frontend/src/api/client.ts +0 -13
  137. package/templates/project/web/frontend/src/style.css +0 -1
  138. package/templates/project/web/frontend/src/views/HomeView.vue +0 -5
  139. package/templates/project/web/frontend/tsconfig.app.json +0 -14
  140. package/templates/project/web/frontend/tsconfig.json +0 -4
  141. /package/templates/{project/web/backend → claude-global/skills/dwy-pentest/scripts}/.python-version +0 -0
  142. /package/templates/project/{web/backend → backend}/Dockerfile +0 -0
  143. /package/templates/project/{web/backend → backend}/alembic/env.py.hbs +0 -0
  144. /package/templates/{claude-global/skills → project/backend/alembic/versions}/.gitkeep +0 -0
  145. /package/templates/project/{web → backend}/docker-compose.dev.yml.hbs +0 -0
  146. /package/templates/project/{web/backend → backend}/src/app/__init__.py +0 -0
  147. /package/templates/project/{web/backend → backend}/src/app/models/__init__.py +0 -0
  148. /package/templates/project/{web/backend → backend}/src/app/routers/__init__.py +0 -0
  149. /package/templates/project/{web/backend → backend}/src/app/routers/health.py +0 -0
  150. /package/templates/project/{web/backend → backend}/src/app/schemas/__init__.py +0 -0
  151. /package/templates/project/{web/backend → backend}/src/app/services/__init__.py +0 -0
  152. /package/templates/project/{web/backend → backend}/tests/__init__.py +0 -0
  153. /package/templates/project/{web/backend → backend}/tests/conftest.py.hbs +0 -0
  154. /package/templates/project/{web/backend/alembic/versions → mobile}/.gitkeep +0 -0
  155. /package/templates/project/web/{frontend → apps/web}/src/components/.gitkeep +0 -0
  156. /package/templates/project/web/{frontend → apps/web}/src/main.ts +0 -0
@@ -0,0 +1,868 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ 检查 13 类工具的镜像源配置:pip / uv / poetry / npm / pnpm / yarn /
4
+ docker / go / cargo / maven / gradle / homebrew / flutter
5
+
6
+ 用法:
7
+ python3 check_mirrors.py [选项]
8
+
9
+ 选项:
10
+ --scope {user,project,both} 扫描范围,默认 both
11
+ --project-path PATH 项目根目录,默认 cwd
12
+ --provider {aliyun,tsinghua,ustc} 推荐源,默认读 preference.json,再回退 aliyun
13
+ --only TOOL 只检查指定工具(可多次或用逗号)
14
+ --verbose 输出更多调试信息
15
+ --json 输出 JSON 格式(脚本间集成用)
16
+
17
+ 退出码:
18
+ 0 全部已加速或仅 not_installed
19
+ 1 存在 warn / missing / deprecated / private(人工干预)
20
+ 2 脚本本身出错
21
+ """
22
+
23
+ import argparse
24
+ import json
25
+ import os
26
+ import re
27
+ import shutil
28
+ import sys
29
+ from dataclasses import asdict, dataclass, field
30
+ from pathlib import Path
31
+
32
+ try:
33
+ import tomllib
34
+ except ImportError:
35
+ print("错误:本脚本需要 Python 3.11+(标准库 tomllib)", file=sys.stderr)
36
+ sys.exit(2)
37
+
38
+
39
+ # ============================================================
40
+ # 镜像源 URL 表(与 references/mirror-providers.md 对应)
41
+ # ============================================================
42
+
43
+ MIRRORS = {
44
+ "aliyun": {
45
+ "pip": "https://mirrors.aliyun.com/pypi/simple/",
46
+ "uv": "https://mirrors.aliyun.com/pypi/simple/",
47
+ "uv_python_install": "https://registry.npmmirror.com/-/binary/python-build-standalone",
48
+ "poetry": "https://mirrors.aliyun.com/pypi/simple/",
49
+ "npm": "https://registry.npmmirror.com",
50
+ "docker": ["https://docker.m.daocloud.io", "https://docker.mirrors.ustc.edu.cn"],
51
+ "go": "https://goproxy.cn,direct",
52
+ "cargo_url": "sparse+https://rsproxy.cn/index/",
53
+ "cargo_replace_with": "rsproxy-sparse",
54
+ "maven": "https://maven.aliyun.com/repository/public",
55
+ "gradle_plugin": "https://maven.aliyun.com/repository/gradle-plugin",
56
+ "homebrew_brew": "https://mirrors.aliyun.com/homebrew/brew.git",
57
+ "homebrew_core": "https://mirrors.aliyun.com/homebrew/homebrew-core.git",
58
+ "homebrew_bottles": "https://mirrors.aliyun.com/homebrew/homebrew-bottles",
59
+ "flutter_pub": "https://pub.flutter-io.cn",
60
+ "flutter_storage": "https://storage.flutter-io.cn",
61
+ },
62
+ "tsinghua": {
63
+ "pip": "https://pypi.tuna.tsinghua.edu.cn/simple",
64
+ "uv": "https://pypi.tuna.tsinghua.edu.cn/simple",
65
+ "uv_python_install": "https://registry.npmmirror.com/-/binary/python-build-standalone",
66
+ "poetry": "https://pypi.tuna.tsinghua.edu.cn/simple",
67
+ "npm": "https://registry.npmmirror.com",
68
+ "docker": ["https://docker.m.daocloud.io"],
69
+ "go": "https://goproxy.cn,direct",
70
+ "cargo_url": "sparse+https://mirrors.tuna.tsinghua.edu.cn/crates.io-index/",
71
+ "cargo_replace_with": "tuna-sparse",
72
+ "maven": "https://maven.aliyun.com/repository/public",
73
+ "gradle_plugin": "https://maven.aliyun.com/repository/gradle-plugin",
74
+ "homebrew_brew": "https://mirrors.tuna.tsinghua.edu.cn/git/homebrew/brew.git",
75
+ "homebrew_core": "https://mirrors.tuna.tsinghua.edu.cn/git/homebrew/homebrew-core.git",
76
+ "homebrew_bottles": "https://mirrors.tuna.tsinghua.edu.cn/homebrew-bottles",
77
+ "flutter_pub": "https://pub.flutter-io.cn",
78
+ "flutter_storage": "https://storage.flutter-io.cn",
79
+ },
80
+ "ustc": {
81
+ "pip": "https://mirrors.ustc.edu.cn/pypi/simple/",
82
+ "uv": "https://mirrors.ustc.edu.cn/pypi/simple/",
83
+ "uv_python_install": "https://registry.npmmirror.com/-/binary/python-build-standalone",
84
+ "poetry": "https://mirrors.ustc.edu.cn/pypi/simple/",
85
+ "npm": "https://registry.npmmirror.com",
86
+ "docker": ["https://docker.m.daocloud.io"],
87
+ "go": "https://goproxy.cn,direct",
88
+ "cargo_url": "sparse+https://mirrors.ustc.edu.cn/crates.io-index/",
89
+ "cargo_replace_with": "ustc-sparse",
90
+ "maven": "https://maven.aliyun.com/repository/public",
91
+ "gradle_plugin": "https://maven.aliyun.com/repository/gradle-plugin",
92
+ "homebrew_brew": "https://mirrors.ustc.edu.cn/brew.git",
93
+ "homebrew_core": "https://mirrors.ustc.edu.cn/homebrew-core.git",
94
+ "homebrew_bottles": "https://mirrors.ustc.edu.cn/homebrew-bottles",
95
+ "flutter_pub": "https://pub.flutter-io.cn",
96
+ "flutter_storage": "https://storage.flutter-io.cn",
97
+ },
98
+ }
99
+
100
+ # 已知国内加速源(任一匹配即视为 ok)
101
+ KNOWN_GOOD = {
102
+ "pip": [
103
+ r"mirrors\.aliyun\.com/pypi",
104
+ r"pypi\.tuna\.tsinghua\.edu\.cn",
105
+ r"mirrors\.ustc\.edu\.cn/pypi",
106
+ r"pypi\.douban\.com",
107
+ r"mirrors\.cloud\.tencent\.com/pypi",
108
+ r"mirrors\.huaweicloud\.com/repository/pypi",
109
+ r"mirrors\.bfsu\.edu\.cn/pypi",
110
+ ],
111
+ "npm": [
112
+ r"registry\.npmmirror\.com",
113
+ r"mirrors\.cloud\.tencent\.com/npm",
114
+ r"mirrors\.huaweicloud\.com/repository/npm",
115
+ ],
116
+ "docker": [
117
+ r"daocloud\.io",
118
+ r"docker\.mirrors\.ustc\.edu\.cn",
119
+ r"hub-mirror\.c\.163\.com",
120
+ r"mirror\.aliyuncs\.com",
121
+ r"mirror\.ccs\.tencentyun\.com",
122
+ r"mirror\.baidubce\.com",
123
+ ],
124
+ "go": [
125
+ r"goproxy\.cn",
126
+ r"goproxy\.io",
127
+ r"mirrors\.aliyun\.com/goproxy",
128
+ r"mirrors\.tencent\.com/go",
129
+ ],
130
+ "cargo": [
131
+ r"rsproxy\.cn",
132
+ r"mirrors\.tuna\.tsinghua\.edu\.cn/crates",
133
+ r"mirrors\.ustc\.edu\.cn/crates",
134
+ ],
135
+ "maven": [
136
+ r"maven\.aliyun\.com",
137
+ r"mirrors\.huaweicloud\.com/repository/maven",
138
+ r"mirrors\.cloud\.tencent\.com/nexus/repository/maven",
139
+ ],
140
+ "homebrew": [
141
+ r"mirrors\.aliyun\.com/homebrew",
142
+ r"mirrors\.tuna\.tsinghua\.edu\.cn/git/homebrew",
143
+ r"mirrors\.tuna\.tsinghua\.edu\.cn/homebrew",
144
+ r"mirrors\.ustc\.edu\.cn/(?:brew|homebrew)",
145
+ ],
146
+ "flutter": [
147
+ r"flutter-io\.cn",
148
+ r"mirrors\.cloud\.tencent\.com/flutter",
149
+ ],
150
+ }
151
+
152
+ # 默认(境外)源 / 视为 warn
153
+ DEFAULT_REMOTE = {
154
+ "pip": [r"pypi\.org/simple", r"files\.pythonhosted\.org"],
155
+ "npm": [r"registry\.npmjs\.org"],
156
+ "docker": [r"registry\.docker\.io", r"registry-1\.docker\.io"],
157
+ "go": [r"proxy\.golang\.org", r"^direct$", r"^off$"],
158
+ "cargo": [r"crates\.io"],
159
+ "maven": [r"repo\.maven\.apache\.org", r"repo1\.maven\.org", r"repo\.maven\.org"],
160
+ "homebrew": [r"github\.com/Homebrew", r"homebrew\.bintray\.com", r"ghcr\.io/homebrew"],
161
+ "flutter": [r"pub\.dev", r"storage\.googleapis\.com/flutter_infra"],
162
+ }
163
+
164
+ # 已弃用源(必须替换)
165
+ DEPRECATED = {
166
+ "npm": [r"registry\.npm\.taobao\.org", r"registry\.cnpmjs\.org", r"npm\.taobao\.org"],
167
+ "pip": [r"^http://"], # 任何 http pip 源都不安全
168
+ }
169
+
170
+ # 私服识别正则(不修改)
171
+ PRIVATE_PATTERNS = [
172
+ r"nexus",
173
+ r"artifactory",
174
+ r"harbor",
175
+ r"\.internal\b",
176
+ r"\.corp\b",
177
+ r"\.local\b",
178
+ r"\bcompany\b",
179
+ r":8081\b",
180
+ r":8082\b",
181
+ r":5000\b",
182
+ r"^https?://\d+\.\d+\.\d+\.\d+",
183
+ ]
184
+ # 公共加速器白名单(含 /repository/、/nexus 等路径但不是私服)
185
+ PUBLIC_MIRROR_DOMAINS = [
186
+ r"\.aliyun\.com",
187
+ r"\.tsinghua\.edu\.cn",
188
+ r"\.ustc\.edu\.cn",
189
+ r"\.bfsu\.edu\.cn",
190
+ r"\.huaweicloud\.com",
191
+ r"\.tencent(?:yun)?\.com",
192
+ r"\.tencentyun\.com",
193
+ r"\.npmmirror\.com",
194
+ r"\.flutter-io\.cn",
195
+ r"goproxy\.cn",
196
+ r"goproxy\.io",
197
+ r"rsproxy\.cn",
198
+ r"daocloud\.io",
199
+ ]
200
+
201
+
202
+ # ============================================================
203
+ # 数据类
204
+ # ============================================================
205
+
206
+ @dataclass
207
+ class CheckResult:
208
+ tool: str
209
+ scope: str # "user" / "project"
210
+ config_path: str
211
+ status: str # ok / warn / private / missing / no_config / not_installed / deprecated / error
212
+ current_value: str = ""
213
+ recommended: str = ""
214
+ note: str = ""
215
+
216
+
217
+ # ============================================================
218
+ # 通用工具函数
219
+ # ============================================================
220
+
221
+ def is_private_registry(url: str) -> bool:
222
+ if not url:
223
+ return False
224
+ # 先排除公共加速器(即使路径里有 /repository/、/nexus 等关键字也不算私服)
225
+ if any(re.search(p, url, re.IGNORECASE) for p in PUBLIC_MIRROR_DOMAINS):
226
+ return False
227
+ return any(re.search(p, url, re.IGNORECASE) for p in PRIVATE_PATTERNS)
228
+
229
+
230
+ def classify_url(tool: str, url: str) -> str:
231
+ url = (url or "").strip()
232
+ if not url:
233
+ return "no_config"
234
+ if is_private_registry(url):
235
+ return "private"
236
+ for pat in DEPRECATED.get(tool, []):
237
+ if re.search(pat, url, re.IGNORECASE):
238
+ return "deprecated"
239
+ for pat in KNOWN_GOOD.get(tool, []):
240
+ if re.search(pat, url, re.IGNORECASE):
241
+ return "ok"
242
+ for pat in DEFAULT_REMOTE.get(tool, []):
243
+ if re.search(pat, url, re.IGNORECASE):
244
+ return "warn"
245
+ # 未识别 - 可能是新公共源或未知私服,保守标记 warn
246
+ return "warn"
247
+
248
+
249
+ def is_installed(cmd: str) -> bool:
250
+ return shutil.which(cmd) is not None
251
+
252
+
253
+ def home() -> Path:
254
+ return Path.home()
255
+
256
+
257
+ def is_macos() -> bool:
258
+ return sys.platform == "darwin"
259
+
260
+
261
+ def is_linux() -> bool:
262
+ return sys.platform.startswith("linux")
263
+
264
+
265
+ def read_text_safe(p: Path) -> str:
266
+ try:
267
+ return p.read_text(encoding="utf-8", errors="ignore")
268
+ except OSError:
269
+ return ""
270
+
271
+
272
+ def parse_npmrc(content: str) -> dict[str, str]:
273
+ """.npmrc 是 key=value 格式,不带 section header。"""
274
+ out = {}
275
+ for line in content.splitlines():
276
+ line = line.strip()
277
+ if not line or line.startswith("#") or line.startswith(";"):
278
+ continue
279
+ if "=" in line:
280
+ k, _, v = line.partition("=")
281
+ out[k.strip().lower()] = v.strip().strip('"').strip("'")
282
+ return out
283
+
284
+
285
+ # ============================================================
286
+ # 各工具检查函数
287
+ # ============================================================
288
+
289
+ def check_pip(provider: str, _project: Path) -> list[CheckResult]:
290
+ if not (is_installed("pip") or is_installed("pip3")):
291
+ return [CheckResult("pip", "user", "-", "not_installed")]
292
+
293
+ candidates = []
294
+ if is_macos():
295
+ candidates.append(home() / "Library/Application Support/pip/pip.conf")
296
+ candidates.extend([
297
+ home() / ".config/pip/pip.conf",
298
+ home() / ".pip/pip.conf",
299
+ ])
300
+
301
+ recommended = MIRRORS[provider]["pip"]
302
+ config_path = next((p for p in candidates if p.exists()), None)
303
+ if not config_path:
304
+ return [CheckResult("pip", "user", str(candidates[0]), "missing",
305
+ "", recommended, note="pip.conf 不存在,pip 默认走 pypi.org")]
306
+
307
+ content = read_text_safe(config_path)
308
+ m = re.search(r"^\s*index[-_]url\s*=\s*(.+)$", content, re.MULTILINE | re.IGNORECASE)
309
+ current = m.group(1).strip() if m else ""
310
+ status = classify_url("pip", current)
311
+ return [CheckResult("pip", "user", str(config_path), status, current, recommended)]
312
+
313
+
314
+ def check_uv(provider: str, project: Path) -> list[CheckResult]:
315
+ if not is_installed("uv"):
316
+ return [CheckResult("uv", "user", "-", "not_installed")]
317
+
318
+ results: list[CheckResult] = []
319
+ recommended_index = MIRRORS[provider]["uv"]
320
+ recommended_python = MIRRORS[provider]["uv_python_install"]
321
+
322
+ user_uv = home() / ".config/uv/uv.toml"
323
+
324
+ # 用户级 uv.toml — 包含 [[index]] 和 python-install-mirror 两件事
325
+ user_data = {}
326
+ if user_uv.exists():
327
+ try:
328
+ with user_uv.open("rb") as f:
329
+ user_data = tomllib.load(f)
330
+ except (tomllib.TOMLDecodeError, OSError) as e:
331
+ results.append(CheckResult("uv", "user", str(user_uv), "error", "", recommended_index,
332
+ note=f"解析失败: {e}"))
333
+ return results
334
+
335
+ # uv index(PyPI 包索引)
336
+ indexes = user_data.get("index", []) or []
337
+ urls = [i.get("url", "") for i in indexes if isinstance(i, dict)]
338
+ current_index = urls[0] if urls else ""
339
+ if not user_uv.exists():
340
+ results.append(CheckResult("uv", "user", str(user_uv), "missing",
341
+ "", recommended_index,
342
+ note="uv.toml 不存在(PyPI 包索引)"))
343
+ else:
344
+ status = classify_url("pip", current_index) if current_index else "no_config"
345
+ note = "" if current_index else "uv.toml 未声明 [[index]]"
346
+ results.append(CheckResult("uv-index", "user", str(user_uv), status,
347
+ current_index, recommended_index, note=note))
348
+
349
+ # uv python-install-mirror(Python 解释器下载源)
350
+ # 优先级:uv.toml 字段 > UV_PYTHON_INSTALL_MIRROR 环境变量 > 默认 GitHub
351
+ cfg_python_mirror = user_data.get("python-install-mirror", "")
352
+ env_python_mirror = os.environ.get("UV_PYTHON_INSTALL_MIRROR", "")
353
+ current_python = cfg_python_mirror or env_python_mirror
354
+ source_note = ""
355
+ if cfg_python_mirror:
356
+ source_note = "来自 uv.toml"
357
+ elif env_python_mirror:
358
+ source_note = "来自 UV_PYTHON_INSTALL_MIRROR 环境变量"
359
+
360
+ if current_python:
361
+ # 国内可用源:npmmirror.com/-/binary
362
+ if "npmmirror.com" in current_python or "registry.npmmirror.com" in current_python:
363
+ status = "ok"
364
+ elif is_private_registry(current_python):
365
+ status = "private"
366
+ else:
367
+ status = "warn"
368
+ results.append(CheckResult("uv-python", "user", str(user_uv), status,
369
+ current_python, recommended_python, note=source_note))
370
+ else:
371
+ results.append(CheckResult("uv-python", "user", str(user_uv), "missing",
372
+ "", recommended_python,
373
+ note="未配置 python-install-mirror,uv 装 Python 解释器走 GitHub releases,国内极慢"))
374
+
375
+ # 项目级 pyproject.toml 中的 [[tool.uv.index]]
376
+ pyproject = project / "pyproject.toml"
377
+ if pyproject.exists():
378
+ try:
379
+ with pyproject.open("rb") as f:
380
+ data = tomllib.load(f)
381
+ indexes = data.get("tool", {}).get("uv", {}).get("index", []) or []
382
+ if indexes:
383
+ urls = [i.get("url", "") for i in indexes if isinstance(i, dict)]
384
+ current = urls[0] if urls else ""
385
+ status = classify_url("pip", current)
386
+ results.append(CheckResult("uv-index", "project", str(pyproject), status,
387
+ current, recommended_index))
388
+ else:
389
+ results.append(CheckResult("uv-index", "project", str(pyproject), "no_config",
390
+ "", recommended_index,
391
+ note="pyproject.toml 未声明 [[tool.uv.index]]"))
392
+ except tomllib.TOMLDecodeError as e:
393
+ results.append(CheckResult("uv-index", "project", str(pyproject), "error",
394
+ "", recommended_index, note=f"解析失败: {e}"))
395
+
396
+ return results
397
+
398
+
399
+ def check_poetry(provider: str, project: Path) -> list[CheckResult]:
400
+ if not is_installed("poetry"):
401
+ return [CheckResult("poetry", "user", "-", "not_installed")]
402
+
403
+ results: list[CheckResult] = []
404
+ recommended = MIRRORS[provider]["poetry"]
405
+
406
+ # 项目级 pyproject.toml 中的 [[tool.poetry.source]]
407
+ pyproject = project / "pyproject.toml"
408
+ if pyproject.exists():
409
+ try:
410
+ with pyproject.open("rb") as f:
411
+ data = tomllib.load(f)
412
+ sources = data.get("tool", {}).get("poetry", {}).get("source", []) or []
413
+ if sources:
414
+ urls = [s.get("url", "") for s in sources if isinstance(s, dict)]
415
+ current = urls[0] if urls else ""
416
+ status = classify_url("pip", current)
417
+ results.append(CheckResult("poetry", "project", str(pyproject), status, current, recommended))
418
+ else:
419
+ results.append(CheckResult("poetry", "project", str(pyproject), "no_config",
420
+ "", recommended,
421
+ note="未声明 [[tool.poetry.source]]"))
422
+ except tomllib.TOMLDecodeError as e:
423
+ results.append(CheckResult("poetry", "project", str(pyproject), "error",
424
+ "", recommended, note=f"解析失败: {e}"))
425
+
426
+ return results
427
+
428
+
429
+ def _check_npmrc(tool: str, provider: str, project: Path) -> list[CheckResult]:
430
+ results: list[CheckResult] = []
431
+ recommended = MIRRORS[provider]["npm"]
432
+
433
+ user_npmrc = home() / ".npmrc"
434
+ if user_npmrc.exists():
435
+ cfg = parse_npmrc(read_text_safe(user_npmrc))
436
+ current = cfg.get("registry", "")
437
+ status = classify_url("npm", current)
438
+ results.append(CheckResult(tool, "user", str(user_npmrc), status, current, recommended))
439
+ else:
440
+ results.append(CheckResult(tool, "user", str(user_npmrc), "missing",
441
+ "", recommended,
442
+ note=f"~/.npmrc 不存在,{tool} 默认走 npmjs.org"))
443
+
444
+ proj_npmrc = project / ".npmrc"
445
+ if proj_npmrc.exists():
446
+ cfg = parse_npmrc(read_text_safe(proj_npmrc))
447
+ current = cfg.get("registry", "")
448
+ status = classify_url("npm", current) if current else "no_config"
449
+ note = "项目级 .npmrc 未设置 registry" if not current else ""
450
+ results.append(CheckResult(tool, "project", str(proj_npmrc), status, current, recommended, note=note))
451
+
452
+ return results
453
+
454
+
455
+ def check_npm(provider: str, project: Path) -> list[CheckResult]:
456
+ if not is_installed("npm"):
457
+ return [CheckResult("npm", "user", "-", "not_installed")]
458
+ return _check_npmrc("npm", provider, project)
459
+
460
+
461
+ def check_pnpm(provider: str, project: Path) -> list[CheckResult]:
462
+ if not is_installed("pnpm"):
463
+ return [CheckResult("pnpm", "user", "-", "not_installed")]
464
+ # pnpm 共用 .npmrc,只有 user 级与 npm 重复,避免重复输出
465
+ # 但项目级 .npmrc 是同一个文件,pnpm 也读它 - 不用再加一次
466
+ return [CheckResult("pnpm", "user", str(home() / ".npmrc"),
467
+ "ok" if (home() / ".npmrc").exists() else "missing",
468
+ "", MIRRORS[provider]["npm"],
469
+ note="pnpm 与 npm 共用 ~/.npmrc,详见上方 npm 检查结果")]
470
+
471
+
472
+ def check_yarn(provider: str, project: Path) -> list[CheckResult]:
473
+ if not is_installed("yarn"):
474
+ return [CheckResult("yarn", "user", "-", "not_installed")]
475
+
476
+ results: list[CheckResult] = []
477
+ recommended = MIRRORS[provider]["npm"]
478
+
479
+ # yarn 1: ~/.yarnrc 用 registry "..." 格式
480
+ yarnrc_v1 = home() / ".yarnrc"
481
+ yarnrc_v2 = home() / ".yarnrc.yml"
482
+
483
+ if yarnrc_v2.exists():
484
+ content = read_text_safe(yarnrc_v2)
485
+ m = re.search(r"^\s*npmRegistryServer:\s*[\"']?([^\"'\s]+)", content, re.MULTILINE)
486
+ current = m.group(1) if m else ""
487
+ status = classify_url("npm", current)
488
+ results.append(CheckResult("yarn", "user", str(yarnrc_v2), status, current, recommended))
489
+ elif yarnrc_v1.exists():
490
+ content = read_text_safe(yarnrc_v1)
491
+ m = re.search(r"^\s*registry\s+[\"']?([^\"'\s]+)", content, re.MULTILINE)
492
+ current = m.group(1) if m else ""
493
+ status = classify_url("npm", current)
494
+ results.append(CheckResult("yarn", "user", str(yarnrc_v1), status, current, recommended))
495
+ else:
496
+ # 走 .npmrc
497
+ npmrc = home() / ".npmrc"
498
+ if npmrc.exists():
499
+ cfg = parse_npmrc(read_text_safe(npmrc))
500
+ current = cfg.get("registry", "")
501
+ status = classify_url("npm", current)
502
+ results.append(CheckResult("yarn", "user", str(npmrc), status, current, recommended,
503
+ note="yarn 回退到 ~/.npmrc"))
504
+ else:
505
+ results.append(CheckResult("yarn", "user", str(yarnrc_v1), "missing",
506
+ "", recommended, note="yarn 无任何配置"))
507
+
508
+ return results
509
+
510
+
511
+ def check_docker(provider: str, _project: Path) -> list[CheckResult]:
512
+ if not is_installed("docker"):
513
+ return [CheckResult("docker", "user", "-", "not_installed")]
514
+
515
+ recommended = ", ".join(MIRRORS[provider]["docker"])
516
+
517
+ candidates = []
518
+ if is_macos():
519
+ candidates.append(home() / ".docker/daemon.json")
520
+ elif is_linux():
521
+ candidates.extend([
522
+ Path("/etc/docker/daemon.json"),
523
+ home() / ".docker/daemon.json",
524
+ ])
525
+ else:
526
+ candidates.append(home() / ".docker/daemon.json")
527
+
528
+ config_path = next((p for p in candidates if p.exists()), None)
529
+ if not config_path:
530
+ return [CheckResult("docker", "user", str(candidates[0]), "missing",
531
+ "", recommended,
532
+ note="daemon.json 不存在,需创建或在 Docker Desktop GUI 设置 registry-mirrors")]
533
+
534
+ try:
535
+ data = json.loads(read_text_safe(config_path) or "{}")
536
+ except json.JSONDecodeError as e:
537
+ return [CheckResult("docker", "user", str(config_path), "error", "", recommended,
538
+ note=f"daemon.json 解析失败: {e}")]
539
+
540
+ mirrors = data.get("registry-mirrors", []) or []
541
+ if not mirrors:
542
+ return [CheckResult("docker", "user", str(config_path), "no_config", "", recommended,
543
+ note="daemon.json 缺少 registry-mirrors 字段")]
544
+
545
+ # 任一镜像匹配国内加速器即 ok
546
+ statuses = [classify_url("docker", url) for url in mirrors]
547
+ if any(s == "ok" for s in statuses):
548
+ status = "ok"
549
+ elif "private" in statuses:
550
+ status = "private"
551
+ elif "warn" in statuses or "deprecated" in statuses:
552
+ status = "warn"
553
+ else:
554
+ status = "warn"
555
+ return [CheckResult("docker", "user", str(config_path), status,
556
+ ", ".join(mirrors), recommended,
557
+ note="此项仅加速 docker.io。gcr.io/ghcr.io/quay.io/registry.k8s.io 等需改 image 引用前缀,详见 SKILL.md")]
558
+
559
+
560
+ def check_go(provider: str, _project: Path) -> list[CheckResult]:
561
+ if not is_installed("go"):
562
+ return [CheckResult("go", "user", "-", "not_installed")]
563
+
564
+ recommended = MIRRORS[provider]["go"]
565
+
566
+ # 通过 go env 查实际生效的 GOPROXY
567
+ import subprocess
568
+ try:
569
+ result = subprocess.run(
570
+ ["go", "env", "GOPROXY"],
571
+ capture_output=True, text=True, timeout=10, check=True,
572
+ )
573
+ current = result.stdout.strip()
574
+ except (subprocess.SubprocessError, FileNotFoundError) as e:
575
+ return [CheckResult("go", "user", "go env", "error", "", recommended,
576
+ note=f"go env 调用失败: {e}")]
577
+
578
+ # go env 文件路径
579
+ if is_macos():
580
+ env_file = home() / "Library/Application Support/go/env"
581
+ else:
582
+ env_file = home() / ".config/go/env"
583
+
584
+ status = classify_url("go", current)
585
+ return [CheckResult("go", "user", str(env_file), status, current, recommended)]
586
+
587
+
588
+ def check_cargo(provider: str, project: Path) -> list[CheckResult]:
589
+ if not is_installed("cargo"):
590
+ return [CheckResult("cargo", "user", "-", "not_installed")]
591
+
592
+ results: list[CheckResult] = []
593
+ recommended = MIRRORS[provider]["cargo_url"]
594
+
595
+ # 用户级
596
+ user_cfg = home() / ".cargo/config.toml"
597
+ user_cfg_old = home() / ".cargo/config" # 旧名
598
+ actual = user_cfg if user_cfg.exists() else user_cfg_old if user_cfg_old.exists() else None
599
+
600
+ if actual:
601
+ try:
602
+ with actual.open("rb") as f:
603
+ data = tomllib.load(f)
604
+ crates_io = data.get("source", {}).get("crates-io", {})
605
+ replace_with = crates_io.get("replace-with", "")
606
+ url = ""
607
+ if replace_with:
608
+ url = data.get("source", {}).get(replace_with, {}).get("registry", "")
609
+ current = url or ("default" if not replace_with else f"replace-with={replace_with} 但未定义")
610
+ status = classify_url("cargo", url) if url else "no_config"
611
+ results.append(CheckResult("cargo", "user", str(actual), status, current, recommended))
612
+ except tomllib.TOMLDecodeError as e:
613
+ results.append(CheckResult("cargo", "user", str(actual), "error", "", recommended,
614
+ note=f"解析失败: {e}"))
615
+ else:
616
+ results.append(CheckResult("cargo", "user", str(user_cfg), "missing",
617
+ "", recommended, note="~/.cargo/config.toml 不存在"))
618
+
619
+ # 项目级
620
+ proj_cfg = project / ".cargo/config.toml"
621
+ if proj_cfg.exists():
622
+ try:
623
+ with proj_cfg.open("rb") as f:
624
+ data = tomllib.load(f)
625
+ crates_io = data.get("source", {}).get("crates-io", {})
626
+ replace_with = crates_io.get("replace-with", "")
627
+ url = ""
628
+ if replace_with:
629
+ url = data.get("source", {}).get(replace_with, {}).get("registry", "")
630
+ current = url or "default"
631
+ status = classify_url("cargo", url) if url else "no_config"
632
+ results.append(CheckResult("cargo", "project", str(proj_cfg), status, current, recommended))
633
+ except tomllib.TOMLDecodeError as e:
634
+ results.append(CheckResult("cargo", "project", str(proj_cfg), "error", "", recommended,
635
+ note=f"解析失败: {e}"))
636
+
637
+ return results
638
+
639
+
640
+ def check_maven(provider: str, _project: Path) -> list[CheckResult]:
641
+ if not (is_installed("mvn") or (home() / ".m2").exists()):
642
+ return [CheckResult("maven", "user", "-", "not_installed")]
643
+
644
+ recommended = MIRRORS[provider]["maven"]
645
+ settings = home() / ".m2/settings.xml"
646
+
647
+ if not settings.exists():
648
+ return [CheckResult("maven", "user", str(settings), "missing",
649
+ "", recommended, note="~/.m2/settings.xml 不存在")]
650
+
651
+ content = read_text_safe(settings)
652
+ # 简单正则提取 <mirror><url>...</url>
653
+ urls = re.findall(r"<mirror>.*?<url>\s*([^<\s]+)\s*</url>", content, re.DOTALL | re.IGNORECASE)
654
+ if not urls:
655
+ return [CheckResult("maven", "user", str(settings), "no_config", "", recommended,
656
+ note="settings.xml 未配置 <mirror>")]
657
+
658
+ current = urls[0]
659
+ status = classify_url("maven", current)
660
+ return [CheckResult("maven", "user", str(settings), status, current, recommended)]
661
+
662
+
663
+ def check_gradle(provider: str, _project: Path) -> list[CheckResult]:
664
+ has_gradle = is_installed("gradle") or (home() / ".gradle").exists()
665
+ if not has_gradle:
666
+ return [CheckResult("gradle", "user", "-", "not_installed")]
667
+
668
+ recommended = MIRRORS[provider]["maven"]
669
+
670
+ candidates = [
671
+ home() / ".gradle/init.gradle.kts",
672
+ home() / ".gradle/init.gradle",
673
+ home() / ".gradle/init.d/init.gradle.kts",
674
+ home() / ".gradle/init.d/init.gradle",
675
+ ]
676
+ init_file = next((p for p in candidates if p.exists()), None)
677
+ if not init_file:
678
+ return [CheckResult("gradle", "user", str(candidates[0]), "missing",
679
+ "", recommended, note="未找到 init.gradle(.kts)")]
680
+
681
+ content = read_text_safe(init_file)
682
+ # 找 maven { url ... } 中第一个 url
683
+ m = re.search(r"maven\s*\{\s*url\s*[=]?\s*[\"'(]?([^\"')\s]+)", content)
684
+ current = m.group(1) if m else ""
685
+ status = classify_url("maven", current) if current else "no_config"
686
+ return [CheckResult("gradle", "user", str(init_file), status, current, recommended)]
687
+
688
+
689
+ def check_homebrew(provider: str, _project: Path) -> list[CheckResult]:
690
+ if not is_installed("brew"):
691
+ return [CheckResult("homebrew", "user", "-", "not_installed")]
692
+
693
+ import subprocess
694
+
695
+ recommended = MIRRORS[provider]["homebrew_brew"]
696
+
697
+ # 查 brew.git 远程
698
+ try:
699
+ repo = subprocess.run(["brew", "--repo"], capture_output=True, text=True,
700
+ timeout=10, check=True).stdout.strip()
701
+ remote = subprocess.run(["git", "-C", repo, "remote", "get-url", "origin"],
702
+ capture_output=True, text=True, timeout=10, check=True).stdout.strip()
703
+ except (subprocess.SubprocessError, FileNotFoundError) as e:
704
+ return [CheckResult("homebrew", "user", "brew --repo", "error", "", recommended,
705
+ note=f"读取 brew git remote 失败: {e}")]
706
+
707
+ status = classify_url("homebrew", remote)
708
+ return [CheckResult("homebrew", "user", repo, status, remote, recommended,
709
+ note="还需检查 HOMEBREW_BOTTLE_DOMAIN 环境变量")]
710
+
711
+
712
+ def check_flutter(provider: str, _project: Path) -> list[CheckResult]:
713
+ if not is_installed("flutter"):
714
+ return [CheckResult("flutter", "user", "-", "not_installed")]
715
+
716
+ recommended = MIRRORS[provider]["flutter_pub"]
717
+ pub_url = os.environ.get("PUB_HOSTED_URL", "")
718
+ storage_url = os.environ.get("FLUTTER_STORAGE_BASE_URL", "")
719
+
720
+ # 同时检查 shell rc
721
+ shell_rcs = [home() / ".zshrc", home() / ".bashrc", home() / ".profile"]
722
+ rc_with_config = None
723
+ for rc in shell_rcs:
724
+ if rc.exists() and re.search(r"PUB_HOSTED_URL", read_text_safe(rc)):
725
+ rc_with_config = rc
726
+ break
727
+
728
+ if pub_url:
729
+ status = classify_url("flutter", pub_url)
730
+ return [CheckResult("flutter", "user", str(rc_with_config or "env"), status,
731
+ pub_url, recommended,
732
+ note=f"FLUTTER_STORAGE_BASE_URL={storage_url or '未设置'}")]
733
+
734
+ if rc_with_config:
735
+ return [CheckResult("flutter", "user", str(rc_with_config), "warn",
736
+ "未在当前 shell 生效", recommended,
737
+ note="rc 文件里有配置但当前 shell 未 source")]
738
+
739
+ return [CheckResult("flutter", "user", str(home() / ".zshrc"), "missing",
740
+ "", recommended, note="未配置 PUB_HOSTED_URL")]
741
+
742
+
743
+ # ============================================================
744
+ # 主流程
745
+ # ============================================================
746
+
747
+ ALL_CHECKS = [
748
+ ("pip", check_pip),
749
+ ("uv", check_uv),
750
+ ("poetry", check_poetry),
751
+ ("npm", check_npm),
752
+ ("pnpm", check_pnpm),
753
+ ("yarn", check_yarn),
754
+ ("docker", check_docker),
755
+ ("go", check_go),
756
+ ("cargo", check_cargo),
757
+ ("maven", check_maven),
758
+ ("gradle", check_gradle),
759
+ ("homebrew", check_homebrew),
760
+ ("flutter", check_flutter),
761
+ ]
762
+
763
+
764
+ STATUS_ICON = {
765
+ "ok": "✅ ok",
766
+ "warn": "⚠️ warn",
767
+ "private": "🏢 private",
768
+ "missing": "❌ missing",
769
+ "no_config": "➖ no_config",
770
+ "not_installed": "— not_installed",
771
+ "deprecated": "🛑 deprecated",
772
+ "error": "💥 error",
773
+ }
774
+
775
+
776
+ def load_preference() -> str:
777
+ p = Path.home() / ".claude/skills/dwy-mirror-source/preference.json"
778
+ if p.exists():
779
+ try:
780
+ return json.loads(p.read_text()).get("preferred_provider", "aliyun")
781
+ except (json.JSONDecodeError, OSError):
782
+ pass
783
+ return "aliyun"
784
+
785
+
786
+ def render_text(results: list[CheckResult]) -> tuple[str, dict]:
787
+ by_status = {"ok": 0, "warn": 0, "private": 0, "missing": 0,
788
+ "no_config": 0, "not_installed": 0, "deprecated": 0, "error": 0}
789
+ lines = ["=" * 60, "镜像源检查报告", "=" * 60, ""]
790
+ for r in results:
791
+ by_status[r.status] = by_status.get(r.status, 0) + 1
792
+ scope_label = f"({r.scope})"
793
+ line = f" {r.tool:10s}{scope_label:11s}{STATUS_ICON.get(r.status, r.status):16s}"
794
+ if r.status == "not_installed":
795
+ lines.append(line + " 跳过")
796
+ continue
797
+ if r.current_value:
798
+ line += f"当前: {r.current_value[:50]}"
799
+ elif r.note:
800
+ line += r.note[:60]
801
+ lines.append(line)
802
+ if r.note and r.current_value and r.status not in ("ok", "not_installed"):
803
+ lines.append(f" 备注: {r.note}")
804
+ lines.append("")
805
+ lines.append("-" * 60)
806
+ need_fix = by_status["warn"] + by_status["missing"] + by_status["deprecated"] + by_status["no_config"]
807
+ skipped = by_status["private"]
808
+ lines.append(f"汇总: 共 {len(results)} 项 / 需修复 {need_fix} 项 / 私服跳过 {skipped} 项 / 未安装 {by_status['not_installed']} 项")
809
+ if need_fix:
810
+ lines.append("")
811
+ lines.append("下一步:让 Claude 帮你修复,或运行")
812
+ lines.append(" python3 apply_mirrors.py --dry-run")
813
+ return "\n".join(lines), by_status
814
+
815
+
816
+ def main():
817
+ parser = argparse.ArgumentParser(description="检查镜像源配置")
818
+ parser.add_argument("--scope", choices=["user", "project", "both"], default="both")
819
+ parser.add_argument("--project-path", default=".", type=Path)
820
+ parser.add_argument("--provider", choices=["aliyun", "tsinghua", "ustc"], default=None)
821
+ parser.add_argument("--only", action="append", default=[],
822
+ help="只检查指定工具,可多次或逗号分隔")
823
+ parser.add_argument("--verbose", action="store_true")
824
+ parser.add_argument("--json", action="store_true", dest="as_json")
825
+ args = parser.parse_args()
826
+
827
+ provider = args.provider or load_preference()
828
+ project_path = args.project_path.resolve()
829
+
830
+ # 解析 --only
831
+ only: list[str] = []
832
+ for item in args.only:
833
+ only.extend([s.strip() for s in item.split(",") if s.strip()])
834
+
835
+ results: list[CheckResult] = []
836
+ for tool_name, fn in ALL_CHECKS:
837
+ if only and tool_name not in only:
838
+ continue
839
+ try:
840
+ tool_results = fn(provider, project_path)
841
+ except Exception as e:
842
+ tool_results = [CheckResult(tool_name, "user", "-", "error",
843
+ "", "", note=f"check 异常: {e}")]
844
+ # 按 scope 过滤
845
+ if args.scope != "both":
846
+ tool_results = [r for r in tool_results if r.scope == args.scope or r.status == "not_installed"]
847
+ results.extend(tool_results)
848
+
849
+ if args.as_json:
850
+ print(json.dumps({"provider": provider,
851
+ "results": [asdict(r) for r in results]},
852
+ ensure_ascii=False, indent=2))
853
+ else:
854
+ text, by_status = render_text(results)
855
+ print(text)
856
+
857
+ # 退出码:有 warn / missing / deprecated → 1;error → 2;否则 0
858
+ has_error = any(r.status == "error" for r in results)
859
+ has_fix = any(r.status in ("warn", "missing", "deprecated", "no_config") for r in results)
860
+ if has_error:
861
+ sys.exit(2)
862
+ if has_fix:
863
+ sys.exit(1)
864
+ sys.exit(0)
865
+
866
+
867
+ if __name__ == "__main__":
868
+ main()