create-dwy 0.5.0 → 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 +213 -53
  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} +314 -357
  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} +116 -79
  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} +361 -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/settings.json +20 -0
  31. package/templates/claude-global/skills/dwy-deploy-audit/SKILL.md +481 -0
  32. package/templates/claude-global/skills/dwy-deploy-audit/references/checks-data.md +117 -0
  33. package/templates/claude-global/skills/dwy-deploy-audit/references/checks-network.md +74 -0
  34. package/templates/claude-global/skills/dwy-deploy-audit/references/checks-runtime.md +184 -0
  35. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_base.sh +39 -0
  36. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_capacity.sh +278 -0
  37. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_db.sh +122 -0
  38. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_docker.sh +249 -0
  39. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_env.sh +64 -0
  40. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_https.sh +60 -0
  41. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_logs.sh +209 -0
  42. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_nginx.sh +92 -0
  43. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_resilience.sh +157 -0
  44. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_secrets.sh +327 -0
  45. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_services.sh +77 -0
  46. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_ssh.sh +56 -0
  47. package/templates/claude-global/skills/dwy-deploy-audit/scripts/run_all.sh +105 -0
  48. package/templates/claude-global/skills/dwy-docker/SKILL.md +187 -0
  49. package/templates/claude-global/skills/dwy-docker/references/ask-templates.md +142 -0
  50. package/templates/claude-global/skills/dwy-docker/references/templates.md +479 -0
  51. package/templates/claude-global/skills/dwy-docker/references/version-rules.md +76 -0
  52. package/templates/claude-global/skills/dwy-docker/scripts/query_dockerhub.py +204 -0
  53. package/templates/claude-global/skills/dwy-docker/scripts/scan_images.sh +185 -0
  54. package/templates/claude-global/skills/dwy-eapi/SKILL.md +433 -0
  55. package/templates/claude-global/skills/dwy-eapi/references/tasks-integration-guide.md +387 -0
  56. package/templates/claude-global/skills/dwy-ekit/SKILL.md +85 -0
  57. package/templates/claude-global/skills/dwy-ekit/references/data.md +135 -0
  58. package/templates/claude-global/skills/dwy-ekit/references/form.md +68 -0
  59. package/templates/claude-global/skills/dwy-ekit/references/request.md +183 -0
  60. package/templates/claude-global/skills/dwy-ekit/references/ui.md +109 -0
  61. package/templates/claude-global/skills/{dwy-frontend-eui → dwy-eui}/SKILL.md +129 -15
  62. package/templates/claude-global/skills/dwy-eui/references/eui-design-guide.md +257 -0
  63. package/templates/claude-global/skills/dwy-eui/references/eui-integration-guide.md +302 -0
  64. package/templates/claude-global/skills/dwy-eui/references/eui-landing-design-guide.md +437 -0
  65. package/templates/claude-global/skills/dwy-fullstack-scaffold/SKILL.md +214 -0
  66. package/templates/claude-global/skills/dwy-mirror-source/SKILL.md +275 -0
  67. package/templates/claude-global/skills/dwy-mirror-source/references/dockerfile-snippets.md +228 -0
  68. package/templates/claude-global/skills/dwy-mirror-source/references/mirror-providers.md +150 -0
  69. package/templates/claude-global/skills/dwy-mirror-source/scripts/__pycache__/check_mirrors.cpython-313.pyc +0 -0
  70. package/templates/claude-global/skills/dwy-mirror-source/scripts/apply_mirrors.py +666 -0
  71. package/templates/claude-global/skills/dwy-mirror-source/scripts/check_mirrors.py +868 -0
  72. package/templates/claude-global/skills/dwy-pentest/SKILL.md +344 -0
  73. package/templates/claude-global/skills/dwy-pentest/scripts/lib/__init__.py +1 -0
  74. package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/__init__.cpython-311.pyc +0 -0
  75. package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/config.cpython-311.pyc +0 -0
  76. package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/executor.cpython-311.pyc +0 -0
  77. package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/severity.cpython-311.pyc +0 -0
  78. package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/validator.cpython-311.pyc +0 -0
  79. package/templates/claude-global/skills/dwy-pentest/scripts/lib/config.py +109 -0
  80. package/templates/claude-global/skills/dwy-pentest/scripts/lib/executor.py +157 -0
  81. package/templates/claude-global/skills/dwy-pentest/scripts/lib/severity.py +96 -0
  82. package/templates/claude-global/skills/dwy-pentest/scripts/lib/validator.py +113 -0
  83. package/templates/claude-global/skills/dwy-pentest/scripts/pentest_auth.py +289 -0
  84. package/templates/claude-global/skills/dwy-pentest/scripts/pentest_recon.py +217 -0
  85. package/templates/claude-global/skills/dwy-pentest/scripts/pentest_report.py +343 -0
  86. package/templates/claude-global/skills/dwy-pentest/scripts/pentest_scan.py +205 -0
  87. package/templates/claude-global/skills/dwy-pentest/scripts/pentest_ssl.py +208 -0
  88. package/templates/claude-global/skills/dwy-pentest/scripts/pentest_vuln.py +205 -0
  89. package/templates/claude-global/skills/dwy-pentest/scripts/pentest_web.py +330 -0
  90. package/templates/claude-global/skills/dwy-pentest/scripts/pyproject.toml +14 -0
  91. package/templates/claude-global/skills/dwy-pentest/scripts/setup_tools.sh +97 -0
  92. package/templates/claude-global/skills/dwy-pentest/scripts/uv.lock +415 -0
  93. package/templates/claude-global/skills/dwy-publish/SKILL.md +135 -0
  94. package/templates/claude-global/skills/dwy-vue-core/SKILL.md +890 -0
  95. package/templates/project/backend/.dwy.hbs +5 -0
  96. package/templates/project/backend/.python-version +1 -0
  97. package/templates/project/backend/CLAUDE.md.hbs +40 -0
  98. package/templates/project/{web/backend → backend}/alembic/script.py.mako +2 -0
  99. package/templates/project/{web/backend → backend}/alembic.ini.hbs +0 -1
  100. package/templates/project/backend/deploy.sh.hbs +18 -0
  101. package/templates/project/{web → backend}/docker-compose.prod.yml.hbs +1 -1
  102. package/templates/project/{web/backend → backend}/pyproject.toml.hbs +4 -5
  103. package/templates/project/{web/backend → backend}/src/app/config.py.hbs +1 -1
  104. package/templates/project/backend/src/app/database.py +10 -0
  105. package/templates/project/backend/src/app/dependencies.py +5 -0
  106. package/templates/project/{web/backend → backend}/src/app/main.py.hbs +3 -3
  107. package/templates/project/backend/src/app/models/base.py +5 -0
  108. package/templates/project/web/CLAUDE.md.hbs +29 -36
  109. package/templates/project/web/{frontend → apps/web}/index.html.hbs +1 -0
  110. package/templates/project/web/{frontend → apps/web}/package.json.hbs +4 -3
  111. package/templates/project/web/apps/web/src/App.vue +9 -0
  112. package/templates/project/web/apps/web/src/api/client.ts +5 -0
  113. package/templates/project/web/apps/web/src/composables/.gitkeep +0 -0
  114. package/templates/project/web/apps/web/src/layouts/DefaultLayout.vue +8 -0
  115. package/templates/project/web/{frontend → apps/web}/src/router/index.ts +1 -1
  116. package/templates/project/web/{frontend → apps/web}/src/stores/auth.ts +4 -2
  117. package/templates/project/web/apps/web/src/style.css +2 -0
  118. package/templates/project/web/apps/web/src/types/router.d.ts +8 -0
  119. package/templates/project/web/apps/web/src/views/HomeView.vue +8 -0
  120. package/templates/project/web/apps/web/tsconfig.app.json +18 -0
  121. package/templates/project/web/apps/web/tsconfig.json +6 -0
  122. package/templates/project/web/{frontend → apps/web}/vite.config.ts.hbs +7 -1
  123. package/templates/project/web/package.json.hbs +16 -0
  124. package/templates/project/web/packages/.gitkeep +0 -0
  125. package/templates/project/web/pnpm-workspace.yaml +3 -0
  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-security.md +0 -144
  129. package/templates/claude-global/skills/dwy-auto-sync/SKILL.md +0 -52
  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,666 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ 应用镜像源加速到本地配置文件。
4
+
5
+ 强制原则:
6
+ 1. 默认 --dry-run,只输出 diff,必须显式去掉才会落盘
7
+ 2. 检测到企业私服时跳过,不覆盖
8
+ 3. 不重启 Docker daemon / 不 source rc,仅给提示
9
+
10
+ 用法:
11
+ python3 apply_mirrors.py [选项]
12
+
13
+ 选项:
14
+ --tools T1,T2,... 逗号分隔的工具列表(默认全部)
15
+ --scope {user,project,both} 默认 user(项目级请显式指定)
16
+ --provider {aliyun,tsinghua,ustc} 推荐源,默认读 preference.json
17
+ --project-path PATH 默认 cwd
18
+ --dry-run 只输出 diff,不写文件(默认开启,必须 --apply 才落盘)
19
+ --apply 显式确认落盘(与 --dry-run 互斥)
20
+ --backup 写文件前备份原文件为 .bak.<timestamp>
21
+ """
22
+
23
+ import argparse
24
+ import datetime
25
+ import difflib
26
+ import json
27
+ import os
28
+ import re
29
+ import shutil
30
+ import subprocess
31
+ import sys
32
+ from dataclasses import dataclass, field
33
+ from pathlib import Path
34
+
35
+ try:
36
+ import tomllib
37
+ except ImportError:
38
+ print("错误:需要 Python 3.11+", file=sys.stderr)
39
+ sys.exit(2)
40
+
41
+ # 复用 check 脚本里的常量
42
+ sys.path.insert(0, str(Path(__file__).parent))
43
+ from check_mirrors import ( # noqa: E402
44
+ MIRRORS, KNOWN_GOOD, PRIVATE_PATTERNS, PUBLIC_MIRROR_DOMAINS,
45
+ is_private_registry, classify_url, is_installed, home, is_macos,
46
+ read_text_safe, parse_npmrc, load_preference,
47
+ )
48
+
49
+
50
+ # ============================================================
51
+ # 数据结构
52
+ # ============================================================
53
+
54
+ @dataclass
55
+ class Change:
56
+ tool: str
57
+ scope: str # user / project
58
+ config_path: str
59
+ action: str # create / modify / skip
60
+ old_content: str = ""
61
+ new_content: str = ""
62
+ note: str = ""
63
+ is_json: bool = False # JSON 类配置文件需要保留缩进格式
64
+
65
+
66
+ # ============================================================
67
+ # 工具函数
68
+ # ============================================================
69
+
70
+ def make_diff(old: str, new: str, path: str) -> str:
71
+ if old == new:
72
+ return ""
73
+ diff = difflib.unified_diff(
74
+ old.splitlines(keepends=True),
75
+ new.splitlines(keepends=True),
76
+ fromfile=f"a/{path}",
77
+ tofile=f"b/{path}",
78
+ n=3,
79
+ )
80
+ return "".join(diff)
81
+
82
+
83
+ def write_with_backup(path: Path, content: str, backup: bool):
84
+ path.parent.mkdir(parents=True, exist_ok=True)
85
+ if backup and path.exists():
86
+ ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
87
+ backup_path = path.with_suffix(path.suffix + f".bak.{ts}")
88
+ shutil.copy2(path, backup_path)
89
+ path.write_text(content, encoding="utf-8")
90
+
91
+
92
+ def replace_or_add_ini_line(content: str, key: str, value: str, section: str | None = None) -> str:
93
+ """
94
+ 在 INI 风格内容中替换或添加 key=value。
95
+
96
+ section 为 None 时表示无 section(如 .npmrc)。
97
+ 存在则替换,不存在则在 section 末尾或文件末尾追加。
98
+ """
99
+ pattern = rf"^(\s*){re.escape(key)}\s*=.*$"
100
+ new_line = f"{key} = {value}"
101
+ if re.search(pattern, content, re.MULTILINE | re.IGNORECASE):
102
+ return re.sub(pattern, new_line, content, count=1, flags=re.MULTILINE | re.IGNORECASE)
103
+
104
+ if section:
105
+ section_pat = rf"^\[{re.escape(section)}\]\s*$"
106
+ if re.search(section_pat, content, re.MULTILINE | re.IGNORECASE):
107
+ # 在 section 下追加
108
+ lines = content.splitlines()
109
+ out = []
110
+ in_section = False
111
+ inserted = False
112
+ for line in lines:
113
+ out.append(line)
114
+ if not inserted:
115
+ if re.match(section_pat, line, re.IGNORECASE):
116
+ in_section = True
117
+ out.append(new_line)
118
+ inserted = True
119
+ return "\n".join(out) + ("\n" if not content.endswith("\n") else "")
120
+ # 没有 section,添加 section + line
121
+ prefix = "\n" if content and not content.endswith("\n") else ""
122
+ return content + f"{prefix}[{section}]\n{new_line}\n"
123
+
124
+ # 无 section
125
+ prefix = "\n" if content and not content.endswith("\n") else ""
126
+ return content + f"{prefix}{new_line}\n"
127
+
128
+
129
+ # ============================================================
130
+ # 各工具的 apply 函数(返回 Change 列表)
131
+ # ============================================================
132
+
133
+ def apply_pip(provider: str, _project: Path) -> list[Change]:
134
+ if not (is_installed("pip") or is_installed("pip3")):
135
+ return []
136
+
137
+ target = MIRRORS[provider]["pip"]
138
+ host = re.sub(r"^https?://", "", target).split("/")[0]
139
+
140
+ # 候选路径,优先选已存在的
141
+ candidates = []
142
+ if is_macos():
143
+ candidates.append(home() / "Library/Application Support/pip/pip.conf")
144
+ candidates.extend([
145
+ home() / ".config/pip/pip.conf",
146
+ home() / ".pip/pip.conf",
147
+ ])
148
+ path = next((p for p in candidates if p.exists()), candidates[0])
149
+ old = read_text_safe(path) if path.exists() else ""
150
+
151
+ m = re.search(r"^\s*index[-_]url\s*=\s*(.+)$", old, re.MULTILINE | re.IGNORECASE)
152
+ current = m.group(1).strip() if m else ""
153
+ if is_private_registry(current):
154
+ return [Change("pip", "user", str(path), "skip", note="检测到私服,跳过")]
155
+ if classify_url("pip", current) == "ok":
156
+ return []
157
+
158
+ new = replace_or_add_ini_line(old, "index-url", target, section="global")
159
+ new = replace_or_add_ini_line(new, "trusted-host", host, section="install")
160
+ if old == new:
161
+ return []
162
+ return [Change("pip", "user", str(path),
163
+ "create" if not old else "modify", old, new)]
164
+
165
+
166
+ def apply_uv(provider: str, project: Path) -> list[Change]:
167
+ if not is_installed("uv"):
168
+ return []
169
+
170
+ changes: list[Change] = []
171
+ target_index = MIRRORS[provider]["uv"]
172
+ target_python = MIRRORS[provider]["uv_python_install"]
173
+ user_uv = home() / ".config/uv/uv.toml"
174
+
175
+ old = read_text_safe(user_uv) if user_uv.exists() else ""
176
+
177
+ # 解析现有内容
178
+ existing_index = ""
179
+ existing_python_mirror = ""
180
+ if old:
181
+ try:
182
+ data = tomllib.loads(old)
183
+ indexes = data.get("index", []) or []
184
+ urls = [i.get("url", "") for i in indexes if isinstance(i, dict)]
185
+ existing_index = urls[0] if urls else ""
186
+ existing_python_mirror = data.get("python-install-mirror", "")
187
+ except tomllib.TOMLDecodeError:
188
+ return [Change("uv", "user", str(user_uv), "skip",
189
+ note="uv.toml 解析失败,请手动修复")]
190
+
191
+ # 私服跳过
192
+ if existing_index and is_private_registry(existing_index):
193
+ return [Change("uv", "user", str(user_uv), "skip", note="PyPI 索引是私服,跳过")]
194
+
195
+ # 计算新内容:python-install-mirror 总是放最前,再放 [[index]]
196
+ need_index = classify_url("pip", existing_index) != "ok"
197
+ need_python = "npmmirror.com" not in existing_python_mirror
198
+
199
+ if not need_index and not need_python:
200
+ return []
201
+
202
+ # 重新生成整个 uv.toml(用结构化方式更可靠)
203
+ new_lines = []
204
+ new_lines.append(f'python-install-mirror = "{target_python}"')
205
+ new_lines.append("")
206
+ new_lines.append("[[index]]")
207
+ new_lines.append(f'url = "{target_index}"')
208
+ new_lines.append("default = true")
209
+ new_content = "\n".join(new_lines) + "\n"
210
+
211
+ # 保留原 uv.toml 中其他字段(如 cache-dir、resolution 等)
212
+ if old:
213
+ try:
214
+ data = tomllib.loads(old)
215
+ preserved = {k: v for k, v in data.items()
216
+ if k not in ("python-install-mirror", "index")}
217
+ if preserved:
218
+ # 简单序列化保留字段(仅基础类型,复杂结构保持原样附加)
219
+ extra_lines = []
220
+ for k, v in preserved.items():
221
+ if isinstance(v, str):
222
+ extra_lines.append(f'{k} = "{v}"')
223
+ elif isinstance(v, bool):
224
+ extra_lines.append(f'{k} = {"true" if v else "false"}')
225
+ elif isinstance(v, (int, float)):
226
+ extra_lines.append(f"{k} = {v}")
227
+ # 其他复杂类型(dict/list)跳过,提示用户手动迁移
228
+ if extra_lines:
229
+ new_content = "\n".join(extra_lines) + "\n\n" + new_content
230
+ except tomllib.TOMLDecodeError:
231
+ pass
232
+
233
+ if old == new_content:
234
+ return []
235
+ note = "包含两套源:PyPI 包索引 + Python 解释器下载(python-install-mirror)"
236
+ changes.append(Change("uv", "user", str(user_uv),
237
+ "create" if not old else "modify", old, new_content, note=note))
238
+
239
+ # 项目级 pyproject.toml — 不自动改
240
+ pyproject = project / "pyproject.toml"
241
+ if pyproject.exists():
242
+ try:
243
+ with pyproject.open("rb") as f:
244
+ data = tomllib.load(f)
245
+ indexes = data.get("tool", {}).get("uv", {}).get("index", []) or []
246
+ urls = [i.get("url", "") for i in indexes if isinstance(i, dict)]
247
+ if not indexes:
248
+ changes.append(Change("uv", "project", str(pyproject), "skip",
249
+ note="pyproject.toml 由项目维护,不自动改。建议手动添加 [[tool.uv.index]]"))
250
+ elif urls and is_private_registry(urls[0]):
251
+ changes.append(Change("uv", "project", str(pyproject), "skip",
252
+ note="项目使用私服,跳过"))
253
+ except tomllib.TOMLDecodeError:
254
+ pass
255
+
256
+ return changes
257
+
258
+
259
+ def apply_npm(provider: str, project: Path, scope: str) -> list[Change]:
260
+ if not is_installed("npm"):
261
+ return []
262
+
263
+ target = MIRRORS[provider]["npm"]
264
+ changes: list[Change] = []
265
+
266
+ paths = []
267
+ if scope in ("user", "both"):
268
+ paths.append((home() / ".npmrc", "user"))
269
+ if scope in ("project", "both"):
270
+ proj = project / ".npmrc"
271
+ if proj.exists() or scope == "project":
272
+ paths.append((proj, "project"))
273
+
274
+ for path, scp in paths:
275
+ old = read_text_safe(path) if path.exists() else ""
276
+ cfg = parse_npmrc(old)
277
+ existing = cfg.get("registry", "")
278
+ if existing and is_private_registry(existing):
279
+ changes.append(Change("npm", scp, str(path), "skip", note="私服跳过"))
280
+ continue
281
+ if classify_url("npm", existing) == "ok":
282
+ continue
283
+ new = replace_or_add_ini_line(old, "registry", target)
284
+ if old == new:
285
+ continue
286
+ changes.append(Change("npm", scp, str(path),
287
+ "create" if not old else "modify", old, new))
288
+ return changes
289
+
290
+
291
+ def apply_pnpm(provider: str, project: Path, scope: str) -> list[Change]:
292
+ # pnpm 共用 .npmrc,不重复修改,但需告知
293
+ if is_installed("pnpm"):
294
+ return [Change("pnpm", "user", str(home() / ".npmrc"), "skip",
295
+ note="pnpm 与 npm 共用 .npmrc,已被 npm 修改覆盖")]
296
+ return []
297
+
298
+
299
+ def apply_yarn(provider: str, _project: Path, _scope: str) -> list[Change]:
300
+ if not is_installed("yarn"):
301
+ return []
302
+ target = MIRRORS[provider]["npm"]
303
+ yarnrc_v2 = home() / ".yarnrc.yml"
304
+ yarnrc_v1 = home() / ".yarnrc"
305
+
306
+ if yarnrc_v2.exists():
307
+ old = read_text_safe(yarnrc_v2)
308
+ if re.search(r"^\s*npmRegistryServer:", old, re.MULTILINE):
309
+ new = re.sub(r"^\s*npmRegistryServer:.*$",
310
+ f'npmRegistryServer: "{target}"',
311
+ old, count=1, flags=re.MULTILINE)
312
+ else:
313
+ new = old.rstrip() + f'\nnpmRegistryServer: "{target}"\n'
314
+ if old != new:
315
+ return [Change("yarn", "user", str(yarnrc_v2), "modify", old, new)]
316
+ elif yarnrc_v1.exists():
317
+ old = read_text_safe(yarnrc_v1)
318
+ if re.search(r"^\s*registry\s+", old, re.MULTILINE):
319
+ new = re.sub(r"^\s*registry\s+.*$",
320
+ f'registry "{target}"',
321
+ old, count=1, flags=re.MULTILINE)
322
+ else:
323
+ new = old.rstrip() + f'\nregistry "{target}"\n'
324
+ if old != new:
325
+ return [Change("yarn", "user", str(yarnrc_v1), "modify", old, new)]
326
+ else:
327
+ # 默认创建 v2 格式
328
+ new = f'npmRegistryServer: "{target}"\n'
329
+ return [Change("yarn", "user", str(yarnrc_v2), "create", "", new)]
330
+ return []
331
+
332
+
333
+ def apply_docker(provider: str, _project: Path) -> list[Change]:
334
+ if not is_installed("docker"):
335
+ return []
336
+
337
+ mirrors = MIRRORS[provider]["docker"]
338
+ if is_macos():
339
+ path = home() / ".docker/daemon.json"
340
+ else:
341
+ path = Path("/etc/docker/daemon.json")
342
+ if not path.exists() and not os.access(str(path.parent), os.W_OK):
343
+ # Linux 上无写权限退到用户级
344
+ path = home() / ".docker/daemon.json"
345
+
346
+ old = read_text_safe(path) if path.exists() else ""
347
+ try:
348
+ data = json.loads(old) if old else {}
349
+ except json.JSONDecodeError:
350
+ return [Change("docker", "user", str(path), "skip", note="daemon.json 解析失败,请手动修复")]
351
+
352
+ existing = data.get("registry-mirrors", []) or []
353
+ if any(is_private_registry(u) for u in existing):
354
+ return [Change("docker", "user", str(path), "skip", note="存在私服镜像,跳过")]
355
+
356
+ # 合并:去重,保留 existing 中 known good 的
357
+ merged = []
358
+ for u in mirrors + existing:
359
+ if u not in merged:
360
+ merged.append(u)
361
+
362
+ if data.get("registry-mirrors") == merged:
363
+ return []
364
+
365
+ data["registry-mirrors"] = merged
366
+ new = json.dumps(data, indent=2, ensure_ascii=False) + "\n"
367
+ note = "改完后请手动重启 Docker(macOS: 退出 Docker Desktop 重开;Linux: sudo systemctl restart docker)"
368
+ return [Change("docker", "user", str(path),
369
+ "create" if not old else "modify", old, new, note=note, is_json=True)]
370
+
371
+
372
+ def apply_go(provider: str, _project: Path) -> list[Change]:
373
+ if not is_installed("go"):
374
+ return []
375
+ target = MIRRORS[provider]["go"]
376
+ if is_macos():
377
+ env_file = home() / "Library/Application Support/go/env"
378
+ else:
379
+ env_file = home() / ".config/go/env"
380
+ old = read_text_safe(env_file) if env_file.exists() else ""
381
+
382
+ if re.search(r"^GOPROXY=.*", old, re.MULTILINE):
383
+ new = re.sub(r"^GOPROXY=.*", f"GOPROXY={target}", old, count=1, flags=re.MULTILINE)
384
+ else:
385
+ new = old.rstrip() + ("\n" if old else "") + f"GOPROXY={target}\n"
386
+
387
+ if old == new:
388
+ return []
389
+ note = "可改用 go env -w GOPROXY=... 命令同等效果"
390
+ return [Change("go", "user", str(env_file),
391
+ "create" if not old else "modify", old, new, note=note)]
392
+
393
+
394
+ def apply_cargo(provider: str, _project: Path) -> list[Change]:
395
+ if not is_installed("cargo"):
396
+ return []
397
+ name = MIRRORS[provider]["cargo_replace_with"]
398
+ url = MIRRORS[provider]["cargo_url"]
399
+ path = home() / ".cargo/config.toml"
400
+ old = read_text_safe(path) if path.exists() else ""
401
+
402
+ block = (
403
+ f'[source.crates-io]\n'
404
+ f'replace-with = "{name}"\n\n'
405
+ f'[source.{name}]\n'
406
+ f'registry = "{url}"\n'
407
+ )
408
+
409
+ if old:
410
+ # 已有 [source.crates-io] 段就替换 replace-with 行;否则追加
411
+ if re.search(r"\[source\.crates-io\]", old):
412
+ new = re.sub(r"replace-with\s*=\s*\".*?\"",
413
+ f'replace-with = "{name}"', old, count=1)
414
+ # 确保对应 source 块存在,如果没有就追加
415
+ if not re.search(rf"\[source\.{re.escape(name)}\]", new):
416
+ new = new.rstrip() + f'\n\n[source.{name}]\nregistry = "{url}"\n'
417
+ else:
418
+ new = old.rstrip() + "\n\n" + block
419
+ else:
420
+ new = block
421
+
422
+ if old == new:
423
+ return []
424
+ return [Change("cargo", "user", str(path),
425
+ "create" if not old else "modify", old, new)]
426
+
427
+
428
+ def apply_maven(provider: str, _project: Path) -> list[Change]:
429
+ if not (is_installed("mvn") or (home() / ".m2").exists()):
430
+ return []
431
+ target = MIRRORS[provider]["maven"]
432
+ path = home() / ".m2/settings.xml"
433
+ old = read_text_safe(path) if path.exists() else ""
434
+
435
+ mirror_block = f""" <mirror>
436
+ <id>aliyun</id>
437
+ <name>aliyun maven</name>
438
+ <mirrorOf>*,!internal</mirrorOf>
439
+ <url>{target}</url>
440
+ </mirror>"""
441
+
442
+ if not old.strip():
443
+ new = f"""<?xml version="1.0" encoding="UTF-8"?>
444
+ <settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
445
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
446
+ xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
447
+ <mirrors>
448
+ {mirror_block}
449
+ </mirrors>
450
+ </settings>
451
+ """
452
+ return [Change("maven", "user", str(path), "create", "", new)]
453
+
454
+ # 已有内容:检查是否已配
455
+ if re.search(r"<mirror>.*?<url>\s*" + re.escape(target),
456
+ old, re.DOTALL | re.IGNORECASE):
457
+ return []
458
+ if re.search(r"<mirrors>", old, re.IGNORECASE):
459
+ new = re.sub(r"(<mirrors>)", r"\1\n" + mirror_block, old, count=1, flags=re.IGNORECASE)
460
+ else:
461
+ # 在 </settings> 前插入
462
+ new = re.sub(r"(</settings>)",
463
+ f" <mirrors>\n{mirror_block}\n </mirrors>\n\\1",
464
+ old, count=1, flags=re.IGNORECASE)
465
+ return [Change("maven", "user", str(path), "modify", old, new)]
466
+
467
+
468
+ def apply_gradle(provider: str, _project: Path) -> list[Change]:
469
+ has_gradle = is_installed("gradle") or (home() / ".gradle").exists()
470
+ if not has_gradle:
471
+ return []
472
+ target_main = MIRRORS[provider]["maven"]
473
+ target_plugin = MIRRORS[provider]["gradle_plugin"]
474
+ path = home() / ".gradle/init.gradle.kts"
475
+ old = read_text_safe(path) if path.exists() else ""
476
+
477
+ new_content = f'''// Gradle 全局镜像源(由 dwy-mirror-source 生成)
478
+ allprojects {{
479
+ repositories {{
480
+ maven {{ url = uri("{target_main}") }}
481
+ mavenCentral()
482
+ }}
483
+ }}
484
+
485
+ settingsEvaluated {{
486
+ pluginManagement {{
487
+ repositories {{
488
+ maven {{ url = uri("{target_plugin}") }}
489
+ gradlePluginPortal()
490
+ }}
491
+ }}
492
+ }}
493
+ '''
494
+ if old == new_content:
495
+ return []
496
+ return [Change("gradle", "user", str(path),
497
+ "create" if not old else "modify", old, new_content)]
498
+
499
+
500
+ def apply_homebrew(provider: str, _project: Path) -> list[Change]:
501
+ if not is_installed("brew"):
502
+ return []
503
+
504
+ brew_url = MIRRORS[provider]["homebrew_brew"]
505
+ core_url = MIRRORS[provider]["homebrew_core"]
506
+ bottles_url = MIRRORS[provider]["homebrew_bottles"]
507
+
508
+ # Homebrew 不通过单一文件配置,而是通过 git remote + 环境变量。
509
+ # 这里输出操作指令而不是 diff,让用户手动执行(涉及多个仓库 git 操作)。
510
+ note = (
511
+ "Homebrew 镜像配置需多步手动执行:\n"
512
+ f" 1. brew_repo=$(brew --repo)\n"
513
+ f" 2. git -C \"$brew_repo\" remote set-url origin {brew_url}\n"
514
+ f" 3. core=\"$brew_repo/Library/Taps/homebrew/homebrew-core\"\n"
515
+ f" 4. [ -d \"$core\" ] && git -C \"$core\" remote set-url origin {core_url}\n"
516
+ f" 5. 在 ~/.zshrc 添加: export HOMEBREW_BOTTLE_DOMAIN={bottles_url}\n"
517
+ f" 6. brew update\n"
518
+ "本 skill 不直接 git remote set-url,避免覆盖你的 brew tap 配置。"
519
+ )
520
+ return [Change("homebrew", "user", "brew --repo + ~/.zshrc", "skip", note=note)]
521
+
522
+
523
+ def apply_flutter(provider: str, _project: Path) -> list[Change]:
524
+ if not is_installed("flutter"):
525
+ return []
526
+ pub_url = MIRRORS[provider]["flutter_pub"]
527
+ storage_url = MIRRORS[provider]["flutter_storage"]
528
+
529
+ # 选 zsh 优先,然后 bash
530
+ zshrc = home() / ".zshrc"
531
+ bashrc = home() / ".bashrc"
532
+ target_rc = zshrc if zshrc.exists() else bashrc
533
+ if not target_rc.exists():
534
+ target_rc = zshrc # 创建 .zshrc
535
+
536
+ old = read_text_safe(target_rc) if target_rc.exists() else ""
537
+
538
+ # 移除已有的 PUB_HOSTED_URL / FLUTTER_STORAGE_BASE_URL 行
539
+ new = re.sub(r"^export\s+PUB_HOSTED_URL=.*$", "", old, flags=re.MULTILINE)
540
+ new = re.sub(r"^export\s+FLUTTER_STORAGE_BASE_URL=.*$", "", new, flags=re.MULTILINE)
541
+ new = new.rstrip() + (
542
+ "\n\n# Flutter 国内镜像(由 dwy-mirror-source 添加)\n"
543
+ f"export PUB_HOSTED_URL={pub_url}\n"
544
+ f"export FLUTTER_STORAGE_BASE_URL={storage_url}\n"
545
+ )
546
+ if old == new:
547
+ return []
548
+ note = f"修改后需 source {target_rc} 或重开终端生效"
549
+ return [Change("flutter", "user", str(target_rc),
550
+ "create" if not old else "modify", old, new, note=note)]
551
+
552
+
553
+ # ============================================================
554
+ # 主流程
555
+ # ============================================================
556
+
557
+ ALL_APPLIES = [
558
+ ("pip", apply_pip),
559
+ ("uv", apply_uv),
560
+ ("npm", apply_npm),
561
+ ("pnpm", apply_pnpm),
562
+ ("yarn", apply_yarn),
563
+ ("docker", apply_docker),
564
+ ("go", apply_go),
565
+ ("cargo", apply_cargo),
566
+ ("maven", apply_maven),
567
+ ("gradle", apply_gradle),
568
+ ("homebrew", apply_homebrew),
569
+ ("flutter", apply_flutter),
570
+ ]
571
+
572
+
573
+ def main():
574
+ parser = argparse.ArgumentParser(description="应用国内镜像源加速")
575
+ parser.add_argument("--tools", default="",
576
+ help="逗号分隔的工具列表,默认全部")
577
+ parser.add_argument("--scope", choices=["user", "project", "both"], default="user")
578
+ parser.add_argument("--provider", choices=["aliyun", "tsinghua", "ustc"], default=None)
579
+ parser.add_argument("--project-path", type=Path, default=Path("."))
580
+ parser.add_argument("--apply", action="store_true",
581
+ help="实际写入(默认只 dry-run 输出 diff)")
582
+ parser.add_argument("--backup", action="store_true",
583
+ help="写文件前备份原文件")
584
+ parser.add_argument("--dry-run", action="store_true",
585
+ help="(默认行为)只输出 diff,不落盘")
586
+ args = parser.parse_args()
587
+
588
+ provider = args.provider or load_preference()
589
+ project = args.project_path.resolve()
590
+ only = [t.strip() for t in args.tools.split(",") if t.strip()] if args.tools else []
591
+
592
+ # 默认 dry-run,必须 --apply 才落盘
593
+ dry_run = not args.apply
594
+
595
+ all_changes: list[Change] = []
596
+ for name, fn in ALL_APPLIES:
597
+ if only and name not in only:
598
+ continue
599
+ try:
600
+ sig = fn.__code__.co_varnames[: fn.__code__.co_argcount]
601
+ if "scope" in sig:
602
+ changes = fn(provider, project, args.scope)
603
+ else:
604
+ changes = fn(provider, project)
605
+ except Exception as e:
606
+ changes = [Change(name, "user", "-", "skip", note=f"apply 异常: {e}")]
607
+ all_changes.extend(changes)
608
+
609
+ if not all_changes:
610
+ print("✅ 所有工具已加速或无需修改")
611
+ return
612
+
613
+ print("=" * 60)
614
+ print(f"修复计划(provider={provider}, dry_run={dry_run})")
615
+ print("=" * 60)
616
+ print()
617
+
618
+ notes_after = []
619
+ for c in all_changes:
620
+ prefix = {
621
+ "create": "📝 CREATE",
622
+ "modify": "✏️ MODIFY",
623
+ "skip": "⏭️ SKIP",
624
+ }.get(c.action, c.action)
625
+ print(f"--- {prefix} [{c.tool}/{c.scope}] {c.config_path}")
626
+ if c.note:
627
+ print(f" 备注: {c.note}")
628
+ if c.action != "skip" and c.old_content != c.new_content:
629
+ diff = make_diff(c.old_content, c.new_content, c.config_path)
630
+ if diff:
631
+ print(diff)
632
+ print()
633
+ if c.action != "skip" and c.note:
634
+ notes_after.append(f" • {c.tool}: {c.note}")
635
+
636
+ if dry_run:
637
+ print("=" * 60)
638
+ print("以上为 dry-run 输出。确认无误后加 --apply 真正落盘。")
639
+ print("=" * 60)
640
+ return
641
+
642
+ # 真正落盘
643
+ print("=" * 60)
644
+ print("开始写入...")
645
+ print("=" * 60)
646
+ for c in all_changes:
647
+ if c.action == "skip":
648
+ print(f" ⏭️ {c.tool}: {c.note}")
649
+ continue
650
+ if c.old_content == c.new_content:
651
+ continue
652
+ try:
653
+ write_with_backup(Path(c.config_path), c.new_content, args.backup)
654
+ print(f" ✅ {c.tool}: {c.config_path}")
655
+ except OSError as e:
656
+ print(f" 💥 {c.tool}: 写入失败 — {e}")
657
+
658
+ if notes_after:
659
+ print()
660
+ print("⚠️ 操作后续步骤:")
661
+ for n in notes_after:
662
+ print(n)
663
+
664
+
665
+ if __name__ == "__main__":
666
+ main()