create-dwy 0.5.1 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/index.js +6 -15
- package/package.json +3 -3
- package/src/create.js +61 -37
- package/src/sync.js +206 -61
- package/templates/claude-global/CLAUDE.md +92 -0
- package/templates/claude-global/rules/dwy-android-core.md +610 -0
- package/templates/claude-global/rules/dwy-api-design.md +110 -0
- package/templates/claude-global/rules/dwy-db-migration.md +46 -0
- package/templates/claude-global/rules/dwy-flutter-core.md +564 -0
- package/templates/claude-global/rules/dwy-flutter-network.md +222 -0
- package/templates/claude-global/rules/dwy-flutter-router.md +97 -0
- package/templates/claude-global/rules/dwy-flutter-testing.md +94 -0
- package/templates/claude-global/rules/dwy-git-commit-convention.md +82 -0
- package/templates/claude-global/rules/dwy-git-security.md +53 -0
- package/templates/claude-global/rules/dwy-payload-limits.md +178 -0
- package/templates/claude-global/rules/{prefer-open-source.md → dwy-prefer-open-source.md} +4 -0
- package/templates/claude-global/rules/dwy-project-structure.md +181 -0
- package/templates/claude-global/rules/{python-code-style.md → dwy-python-core.md} +292 -358
- package/templates/claude-global/rules/dwy-python-fastapi.md +272 -0
- package/templates/claude-global/rules/dwy-python-orm.md +61 -0
- package/templates/claude-global/rules/{backend-security.md → dwy-python-security.md} +99 -72
- package/templates/claude-global/rules/dwy-python-testing.md +53 -0
- package/templates/claude-global/rules/{server-security.md → dwy-server-security.md} +1 -2
- package/templates/claude-global/rules/dwy-tdd-development.md +70 -0
- package/templates/claude-global/rules/dwy-tech-stack.md +60 -0
- package/templates/claude-global/rules/dwy-vue-api.md +146 -0
- package/templates/claude-global/rules/{vue-code-style.md → dwy-vue-core.md} +338 -152
- package/templates/claude-global/rules/dwy-vue-pinia.md +75 -0
- package/templates/claude-global/rules/dwy-vue-testing.md +102 -0
- package/templates/claude-global/skills/dwy-deploy-audit/SKILL.md +481 -0
- package/templates/claude-global/skills/dwy-deploy-audit/references/checks-data.md +117 -0
- package/templates/claude-global/skills/dwy-deploy-audit/references/checks-network.md +74 -0
- package/templates/claude-global/skills/dwy-deploy-audit/references/checks-runtime.md +184 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_base.sh +39 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_capacity.sh +278 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_db.sh +122 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_docker.sh +249 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_env.sh +64 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_https.sh +60 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_logs.sh +209 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_nginx.sh +92 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_resilience.sh +157 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_secrets.sh +327 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_services.sh +77 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_ssh.sh +56 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/run_all.sh +105 -0
- package/templates/claude-global/skills/dwy-docker/SKILL.md +187 -0
- package/templates/claude-global/skills/dwy-docker/references/ask-templates.md +142 -0
- package/templates/claude-global/skills/dwy-docker/references/templates.md +479 -0
- package/templates/claude-global/skills/dwy-docker/references/version-rules.md +76 -0
- package/templates/claude-global/skills/dwy-docker/scripts/query_dockerhub.py +204 -0
- package/templates/claude-global/skills/dwy-docker/scripts/scan_images.sh +185 -0
- package/templates/claude-global/skills/dwy-eapi/SKILL.md +433 -0
- package/templates/claude-global/skills/dwy-eapi/references/tasks-integration-guide.md +387 -0
- package/templates/claude-global/skills/dwy-ekit/SKILL.md +85 -0
- package/templates/claude-global/skills/dwy-ekit/references/data.md +135 -0
- package/templates/claude-global/skills/dwy-ekit/references/form.md +68 -0
- package/templates/claude-global/skills/dwy-ekit/references/request.md +183 -0
- package/templates/claude-global/skills/dwy-ekit/references/ui.md +109 -0
- package/templates/claude-global/skills/{dwy-frontend-eui → dwy-eui}/SKILL.md +129 -15
- package/templates/claude-global/skills/dwy-eui/references/eui-design-guide.md +257 -0
- package/templates/claude-global/skills/dwy-eui/references/eui-integration-guide.md +302 -0
- package/templates/claude-global/skills/dwy-eui/references/eui-landing-design-guide.md +437 -0
- package/templates/claude-global/skills/dwy-fullstack-scaffold/SKILL.md +214 -0
- package/templates/claude-global/skills/dwy-mirror-source/SKILL.md +275 -0
- package/templates/claude-global/skills/dwy-mirror-source/references/dockerfile-snippets.md +228 -0
- package/templates/claude-global/skills/dwy-mirror-source/references/mirror-providers.md +150 -0
- package/templates/claude-global/skills/dwy-mirror-source/scripts/__pycache__/check_mirrors.cpython-313.pyc +0 -0
- package/templates/claude-global/skills/dwy-mirror-source/scripts/apply_mirrors.py +666 -0
- package/templates/claude-global/skills/dwy-mirror-source/scripts/check_mirrors.py +868 -0
- package/templates/claude-global/skills/dwy-pentest/SKILL.md +344 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/__init__.py +1 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/__init__.cpython-311.pyc +0 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/config.cpython-311.pyc +0 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/executor.cpython-311.pyc +0 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/severity.cpython-311.pyc +0 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/validator.cpython-311.pyc +0 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/config.py +109 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/executor.py +157 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/severity.py +96 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/validator.py +113 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/pentest_auth.py +289 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/pentest_recon.py +217 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/pentest_report.py +343 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/pentest_scan.py +205 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/pentest_ssl.py +208 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/pentest_vuln.py +205 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/pentest_web.py +330 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/pyproject.toml +14 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/setup_tools.sh +97 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/uv.lock +415 -0
- package/templates/claude-global/skills/{dwy-release → dwy-publish}/SKILL.md +42 -18
- package/templates/claude-global/skills/dwy-vue-core/SKILL.md +890 -0
- package/templates/project/backend/.dwy.hbs +5 -0
- package/templates/project/backend/.python-version +1 -0
- package/templates/project/backend/CLAUDE.md.hbs +40 -0
- package/templates/project/{web/backend → backend}/alembic/script.py.mako +2 -0
- package/templates/project/{web/backend → backend}/alembic.ini.hbs +0 -1
- package/templates/project/backend/deploy.sh.hbs +18 -0
- package/templates/project/{web → backend}/docker-compose.prod.yml.hbs +1 -1
- package/templates/project/{web/backend → backend}/pyproject.toml.hbs +4 -5
- package/templates/project/{web/backend → backend}/src/app/config.py.hbs +1 -1
- package/templates/project/backend/src/app/database.py +10 -0
- package/templates/project/backend/src/app/dependencies.py +5 -0
- package/templates/project/{web/backend → backend}/src/app/main.py.hbs +3 -3
- package/templates/project/backend/src/app/models/base.py +5 -0
- package/templates/project/web/CLAUDE.md.hbs +29 -36
- package/templates/project/web/{frontend → apps/web}/index.html.hbs +1 -0
- package/templates/project/web/{frontend → apps/web}/package.json.hbs +4 -3
- package/templates/project/web/apps/web/src/App.vue +9 -0
- package/templates/project/web/apps/web/src/api/client.ts +5 -0
- package/templates/project/web/apps/web/src/composables/.gitkeep +0 -0
- package/templates/project/web/apps/web/src/layouts/DefaultLayout.vue +8 -0
- package/templates/project/web/{frontend → apps/web}/src/router/index.ts +1 -1
- package/templates/project/web/{frontend → apps/web}/src/stores/auth.ts +4 -2
- package/templates/project/web/apps/web/src/style.css +2 -0
- package/templates/project/web/apps/web/src/types/router.d.ts +8 -0
- package/templates/project/web/apps/web/src/views/HomeView.vue +8 -0
- package/templates/project/web/apps/web/tsconfig.app.json +18 -0
- package/templates/project/web/apps/web/tsconfig.json +6 -0
- package/templates/project/web/{frontend → apps/web}/vite.config.ts.hbs +7 -1
- package/templates/project/web/package.json.hbs +16 -0
- package/templates/project/web/packages/.gitkeep +0 -0
- package/templates/project/web/pnpm-workspace.yaml +3 -0
- package/templates/claude-global/hooks/post-sync-claude-global.sh +0 -72
- package/templates/claude-global/rules/agent-team-execution.md +0 -138
- package/templates/claude-global/rules/agent-team-patterns.md +0 -157
- package/templates/claude-global/rules/git-commit-convention.md +0 -106
- package/templates/claude-global/rules/git-security.md +0 -144
- package/templates/claude-global/skills/dwy-backend-base/SKILL.md +0 -279
- package/templates/claude-global/skills/dwy-frontend-core/SKILL.md +0 -181
- package/templates/project/web/.claude/commands/deploy.md +0 -4
- package/templates/project/web/backend/src/app/models/base.py +0 -5
- package/templates/project/web/deploy.sh.hbs +0 -113
- package/templates/project/web/frontend/src/App.vue +0 -3
- package/templates/project/web/frontend/src/api/client.ts +0 -13
- package/templates/project/web/frontend/src/style.css +0 -1
- package/templates/project/web/frontend/src/views/HomeView.vue +0 -5
- package/templates/project/web/frontend/tsconfig.app.json +0 -14
- package/templates/project/web/frontend/tsconfig.json +0 -4
- /package/templates/{project/web/backend → claude-global/skills/dwy-pentest/scripts}/.python-version +0 -0
- /package/templates/project/{web/backend → backend}/Dockerfile +0 -0
- /package/templates/project/{web/backend → backend}/alembic/env.py.hbs +0 -0
- /package/templates/{claude-global/skills → project/backend/alembic/versions}/.gitkeep +0 -0
- /package/templates/project/{web → backend}/docker-compose.dev.yml.hbs +0 -0
- /package/templates/project/{web/backend → backend}/src/app/__init__.py +0 -0
- /package/templates/project/{web/backend → backend}/src/app/models/__init__.py +0 -0
- /package/templates/project/{web/backend → backend}/src/app/routers/__init__.py +0 -0
- /package/templates/project/{web/backend → backend}/src/app/routers/health.py +0 -0
- /package/templates/project/{web/backend → backend}/src/app/schemas/__init__.py +0 -0
- /package/templates/project/{web/backend → backend}/src/app/services/__init__.py +0 -0
- /package/templates/project/{web/backend → backend}/tests/__init__.py +0 -0
- /package/templates/project/{web/backend → backend}/tests/conftest.py.hbs +0 -0
- /package/templates/project/{web/backend/alembic/versions → mobile}/.gitkeep +0 -0
- /package/templates/project/web/{frontend → apps/web}/src/components/.gitkeep +0 -0
- /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()
|