zco-claude 0.0.8__py3-none-any.whl
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.
- ClaudeSettings/DOT.claudeignore +7 -0
- ClaudeSettings/README.md +100 -0
- ClaudeSettings/commands/generate_changelog.sh +49 -0
- ClaudeSettings/commands/show_env +92 -0
- ClaudeSettings/commands/zco-clean +164 -0
- ClaudeSettings/commands/zco-git-summary +15 -0
- ClaudeSettings/commands/zco-git-tag +42 -0
- ClaudeSettings/hooks/CHANGELOG.md +157 -0
- ClaudeSettings/hooks/README.md +254 -0
- ClaudeSettings/hooks/save_chat_plain.py +148 -0
- ClaudeSettings/hooks/save_chat_spec.py +398 -0
- ClaudeSettings/rules/README.md +270 -0
- ClaudeSettings/rules/go/.golangci.yml.template +170 -0
- ClaudeSettings/rules/go/GoBuildAutoVersion.v250425.md +95 -0
- ClaudeSettings/rules/go/check-standards.sh +128 -0
- ClaudeSettings/rules/go/coding-standards.md +973 -0
- ClaudeSettings/rules/go/example.go +207 -0
- ClaudeSettings/rules/go/go-testing.md +691 -0
- ClaudeSettings/rules/go/list-comments.sh +85 -0
- ClaudeSettings/settings.sample.json +71 -0
- ClaudeSettings/skills/README.md +225 -0
- ClaudeSettings/skills/zco-docs-update/SKILL.md +381 -0
- ClaudeSettings/skills/zco-help/SKILL.md +601 -0
- ClaudeSettings/skills/zco-plan/SKILL.md +661 -0
- ClaudeSettings/skills/zco-plan-new/SKILL.md +585 -0
- ClaudeSettings/zco-scripts/co-docs-update.sh +150 -0
- ClaudeSettings/zco-scripts/test_update_plan_metadata.py +328 -0
- ClaudeSettings/zco-scripts/update-plan-metadata.py +324 -0
- zco_claude-0.0.8.dist-info/METADATA +190 -0
- zco_claude-0.0.8.dist-info/RECORD +34 -0
- zco_claude-0.0.8.dist-info/WHEEL +5 -0
- zco_claude-0.0.8.dist-info/entry_points.txt +3 -0
- zco_claude-0.0.8.dist-info/top_level.txt +1 -0
- zco_claude_init.py +1732 -0
zco_claude_init.py
ADDED
|
@@ -0,0 +1,1732 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
zco_claude_init.py
|
|
4
|
+
作用:
|
|
5
|
+
基于 ClaudeSettings 扩展项目的 .claude 配置目录, 快速初始化项目
|
|
6
|
+
|
|
7
|
+
步骤:
|
|
8
|
+
0. 为目标项目创建 .claudeignore 文件
|
|
9
|
+
1. 新建一个 $HOME/.claude/settings.json 配置, 有备份
|
|
10
|
+
2. 软链接 .claude/rules/* 目录到目标项目
|
|
11
|
+
3. 软链接 .claude/hooks/* 目录到目标项目
|
|
12
|
+
4. 软链接 .claude/command/* 到目标项目
|
|
13
|
+
5. 如果目标目录已存在, 则提示是否覆盖
|
|
14
|
+
6. 记录已链接的项目到 _.linked-projects.json
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
./zco_claude_init.py <target_project_path>
|
|
18
|
+
|
|
19
|
+
Example:
|
|
20
|
+
./zco_claude_init.py /path/to/another/project
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import os
|
|
24
|
+
import sys
|
|
25
|
+
import argparse
|
|
26
|
+
import json
|
|
27
|
+
import shutil
|
|
28
|
+
import difflib
|
|
29
|
+
from datetime import datetime
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
|
|
32
|
+
VERSION = "v0.0.6.260205"
|
|
33
|
+
ZCO_CLAUDE_ROOT = os.path.dirname(os.path.realpath(__file__))
|
|
34
|
+
#ZCO_CLAUDE_TPL_DIR = os.path.join(ZCO_CLAUDE_ROOT, "ClaudeSettings")
|
|
35
|
+
ZCO_CLAUDE_TPL_DIR = Path(ZCO_CLAUDE_ROOT) / "ClaudeSettings"
|
|
36
|
+
ZCO_CLAUDE_RECORD_FILE = Path.home() / ".claude" / "zco-linked-projects.json"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class M_Color:
|
|
40
|
+
"""
|
|
41
|
+
颜色打印类, 前景颜色, foreground color
|
|
42
|
+
"""
|
|
43
|
+
GREEN = "\033[92m"
|
|
44
|
+
BLUE = "\033[94m"
|
|
45
|
+
RED = "\033[91m"
|
|
46
|
+
YELLOW = "\033[93m"
|
|
47
|
+
MAGENTA = "\033[95m"
|
|
48
|
+
CYAN = "\033[96m"
|
|
49
|
+
RESET = "\033[0m"
|
|
50
|
+
|
|
51
|
+
class M_ColorBg:
|
|
52
|
+
"""
|
|
53
|
+
颜色打印类, 背景颜色, background color
|
|
54
|
+
"""
|
|
55
|
+
GREEN = "\033[42m"
|
|
56
|
+
BLUE = "\033[44m"
|
|
57
|
+
RED = "\033[41m"
|
|
58
|
+
YELLOW = "\033[43m"
|
|
59
|
+
MAGENTA = "\033[45m"
|
|
60
|
+
CYAN = "\033[46m"
|
|
61
|
+
RESET = "\033[0m"
|
|
62
|
+
|
|
63
|
+
def pf_color(msg: str, color_code:str=M_Color.GREEN):
|
|
64
|
+
## 先判断当前是否是在终端环境
|
|
65
|
+
if not sys.stdout.isatty():
|
|
66
|
+
print(msg)
|
|
67
|
+
else:
|
|
68
|
+
print(f"{color_code}{msg}{M_Color.RESET}")
|
|
69
|
+
|
|
70
|
+
def debug(*args):
|
|
71
|
+
"""
|
|
72
|
+
调试打印函数
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
*args: 要打印的内容
|
|
76
|
+
"""
|
|
77
|
+
if os.environ.get("DEBUG"):
|
|
78
|
+
print("DEBUG:", *args)
|
|
79
|
+
|
|
80
|
+
def make_default_config():
|
|
81
|
+
##; 读取示例配置
|
|
82
|
+
source_dir = os.path.abspath(ZCO_CLAUDE_TPL_DIR)
|
|
83
|
+
default_settings = {
|
|
84
|
+
"env": {
|
|
85
|
+
"ZCO_TPL_VERSION": "v2",
|
|
86
|
+
"YJ_CLAUDE_CHAT_SAVE_SPEC": "0",
|
|
87
|
+
"YJ_CLAUDE_CHAT_SAVE_PLAIN": "0",
|
|
88
|
+
"CLAUDE_CODE_MAX_OUTPUT_TOKENS": "3000"
|
|
89
|
+
},
|
|
90
|
+
"alwaysThinkingEnabled": True,
|
|
91
|
+
"permissions": {
|
|
92
|
+
"deny": [
|
|
93
|
+
"Read(~/.ssh/**)", ##; 防止 AI 尝试读取你的私钥
|
|
94
|
+
"Read(~/.aws/**)", ##; 云服务凭证
|
|
95
|
+
"Read(**/Library/Application Support/Google/Chrome/**)",
|
|
96
|
+
"Read(./.DS_Store)", ##;
|
|
97
|
+
"Read(**/.DS_Store)",
|
|
98
|
+
"Read(**/__pycache__)",
|
|
99
|
+
"Read(**/__pycache__/**)",
|
|
100
|
+
"Read(*._.*)",
|
|
101
|
+
"Read(*.bak.*)",
|
|
102
|
+
"Read(*.tmp.*)",
|
|
103
|
+
"Read(_.*/**)",
|
|
104
|
+
"Read(*._/**)",
|
|
105
|
+
],
|
|
106
|
+
"ask": [
|
|
107
|
+
# 需求:读取这些配置文件前必须先询问
|
|
108
|
+
"Read(**/.git/**)",
|
|
109
|
+
"Read(**/app.local.conf)",
|
|
110
|
+
"Read(**/*.local.conf)",
|
|
111
|
+
"Read(**/config.local.yaml)",
|
|
112
|
+
"Read(**/.env*)", # 捕获 .env, .env.local 等
|
|
113
|
+
"Write(**/*.conf)", # 写入任何配置文件也要询问
|
|
114
|
+
"Write(**/*.yaml)",
|
|
115
|
+
"Read(**/.zshrc)",
|
|
116
|
+
"Read(**/.bashrc)",
|
|
117
|
+
"Read(**/.bash_profile)",
|
|
118
|
+
"Read(**/*.secret.*)",
|
|
119
|
+
"Write(**/docs/manual/**)"
|
|
120
|
+
],
|
|
121
|
+
"allow": [
|
|
122
|
+
# "Bash(echo:*)",
|
|
123
|
+
# "Bash(cat:*)",
|
|
124
|
+
# ... 你之前的 allow 配置
|
|
125
|
+
"Read(docs/plans/*)",
|
|
126
|
+
"Write(docs/plans/*)",
|
|
127
|
+
"Read(docs/*)",
|
|
128
|
+
"Read(readme.md)",
|
|
129
|
+
"Write(CLAUDE.md)",
|
|
130
|
+
"Write(_.claude_hist/*)",
|
|
131
|
+
"Write(/tmp/*)",
|
|
132
|
+
# 注意:不要把上面已经在 ask 里的文件又放进 allow,否则可能直接通过
|
|
133
|
+
"Bash(tree -L 2 -d:*)",
|
|
134
|
+
"Bash(tree:*)",
|
|
135
|
+
"Bash(head:*)",
|
|
136
|
+
"Bash(grep:*)",
|
|
137
|
+
"Bash(xargs cat:*)",
|
|
138
|
+
"Bash(xargs ls:*)",
|
|
139
|
+
"Bash(find:*)",
|
|
140
|
+
"Bash(wc:*)",
|
|
141
|
+
"Read(docs/*)",
|
|
142
|
+
"Bash(ls:*)",
|
|
143
|
+
"Bash(git submodule status:*)",
|
|
144
|
+
"Bash(git status:*)",
|
|
145
|
+
# 允许执行本项目下的自定义命令
|
|
146
|
+
"Bash(./.claude/commands/*)",
|
|
147
|
+
"Bash(./.claude/zco-scripts/*)",
|
|
148
|
+
f"Bash({source_dir}/commands/*)",
|
|
149
|
+
f"Bash({source_dir}/zco-scripts/*)"
|
|
150
|
+
]
|
|
151
|
+
},
|
|
152
|
+
"hooks": {
|
|
153
|
+
"Stop": [
|
|
154
|
+
{
|
|
155
|
+
"hooks": [
|
|
156
|
+
{
|
|
157
|
+
"type": "command",
|
|
158
|
+
"command": f"python3 {source_dir}/hooks/save_chat_plain.py"
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
"type": "command",
|
|
162
|
+
"command": f"python3 {source_dir}/hooks/save_chat_spec.py"
|
|
163
|
+
}
|
|
164
|
+
]
|
|
165
|
+
}
|
|
166
|
+
]
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return default_settings
|
|
170
|
+
|
|
171
|
+
def validate_paths(target_path, source_dir):
|
|
172
|
+
"""
|
|
173
|
+
验证目标路径和源路径
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
target_path: 目标项目路径
|
|
177
|
+
source_dir: 源项目目录(ClaudeSettings 目录)
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
tuple: (target_abs_path, source_abs_path) 绝对路径
|
|
181
|
+
|
|
182
|
+
Raises:
|
|
183
|
+
SystemExit: 如果路径无效
|
|
184
|
+
"""
|
|
185
|
+
##; 转换为绝对路径
|
|
186
|
+
target_abs = Path(target_path).resolve()
|
|
187
|
+
source_abs = Path(source_dir).resolve()
|
|
188
|
+
|
|
189
|
+
##; 检查目标路径是否存在
|
|
190
|
+
if not target_abs.exists():
|
|
191
|
+
print(f"错误:目标路径不存在: {target_abs}")
|
|
192
|
+
sys.exit(1)
|
|
193
|
+
|
|
194
|
+
##; 检查目标路径是否为目录
|
|
195
|
+
if not target_abs.is_dir():
|
|
196
|
+
print(f"错误:目标路径不是目录: {target_abs}")
|
|
197
|
+
sys.exit(1)
|
|
198
|
+
|
|
199
|
+
##; 检查源文件/目录是否存在
|
|
200
|
+
rules_dir = source_abs / "rules"
|
|
201
|
+
hooks_dir = source_abs / "hooks"
|
|
202
|
+
|
|
203
|
+
missing = []
|
|
204
|
+
if not rules_dir.exists():
|
|
205
|
+
missing.append(str(rules_dir))
|
|
206
|
+
if not hooks_dir.exists():
|
|
207
|
+
missing.append(str(hooks_dir))
|
|
208
|
+
|
|
209
|
+
if missing:
|
|
210
|
+
pf_color(f"警告:以下源文件/目录不存在,将跳过:", M_Color.YELLOW)
|
|
211
|
+
for m in missing:
|
|
212
|
+
pf_color(f" - {m}", M_Color.YELLOW)
|
|
213
|
+
|
|
214
|
+
return target_abs, source_abs
|
|
215
|
+
|
|
216
|
+
def make_symlink(source:Path, target:Path, description: str):
|
|
217
|
+
"""
|
|
218
|
+
创建软链接
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
source: 源文件/目录的绝对路径
|
|
222
|
+
target: 目标链接的绝对路径
|
|
223
|
+
description: 链接描述(用于日志)
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
bool: 是否成功创建链接
|
|
227
|
+
"""
|
|
228
|
+
##; 检查源是否存在
|
|
229
|
+
print("")
|
|
230
|
+
if not source.exists():
|
|
231
|
+
pf_color(f" 跳过 {description}:源不存在", M_Color.RED)
|
|
232
|
+
return False
|
|
233
|
+
|
|
234
|
+
##; 检查目标是否已存在
|
|
235
|
+
if target.exists() or target.is_symlink():
|
|
236
|
+
##; 如果已经是正确的软链接,跳过
|
|
237
|
+
if target.is_symlink() and target.resolve() == source.resolve():
|
|
238
|
+
pf_color(f" ✓ {description}:已存在正确的软链接", M_Color.GREEN)
|
|
239
|
+
return True
|
|
240
|
+
|
|
241
|
+
print(f" ! {description}:目标已存在: {target}")
|
|
242
|
+
response = input(" 是否删除并重新创建?(y/N): ")
|
|
243
|
+
if response.lower() != 'y':
|
|
244
|
+
pf_color(f" 跳过 {description}:用户取消", M_Color.YELLOW)
|
|
245
|
+
return False
|
|
246
|
+
|
|
247
|
+
##; 删除现有文件/链接
|
|
248
|
+
if target.is_symlink():
|
|
249
|
+
target.unlink()
|
|
250
|
+
elif target.is_dir():
|
|
251
|
+
import shutil
|
|
252
|
+
shutil.rmtree(target)
|
|
253
|
+
else:
|
|
254
|
+
target.unlink()
|
|
255
|
+
|
|
256
|
+
##; 确保目标目录存在
|
|
257
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
258
|
+
|
|
259
|
+
##; 创建软链接
|
|
260
|
+
try:
|
|
261
|
+
target.symlink_to(source)
|
|
262
|
+
pf_color(f" ✓ {description}:已创建软链接")
|
|
263
|
+
# print(f" {target} -> {source}")
|
|
264
|
+
return True
|
|
265
|
+
except Exception as e:
|
|
266
|
+
pf_color(f" ✗ {description}:创建失败 - {e}", M_Color.RED)
|
|
267
|
+
return False
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def make_links_for_subs(source_pdir, target_pdir, description, flag_file=False, flag_dir=True):
|
|
272
|
+
"""
|
|
273
|
+
创建软链接到子目录
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
source: 源目录的绝对路径
|
|
277
|
+
target: 目标目录的绝对路径
|
|
278
|
+
description: 链接描述(用于日志)
|
|
279
|
+
flag_file: 筛选允许创建文件软链接
|
|
280
|
+
flag_dir: 筛选允许创建目录软链接
|
|
281
|
+
"""
|
|
282
|
+
###; 先判断目标目录是否存在
|
|
283
|
+
abs_target = target_pdir.resolve()
|
|
284
|
+
abs_source = source_pdir.resolve()
|
|
285
|
+
n_cnt = 0
|
|
286
|
+
if not target_pdir.exists():
|
|
287
|
+
pf_color(f" 新建 {description}:{abs_target}, 即将对源子目录进行软链接", M_Color.CYAN)
|
|
288
|
+
target_pdir.mkdir(parents=True, exist_ok=True)
|
|
289
|
+
elif not target_pdir.is_dir():
|
|
290
|
+
# print(f" 跳过 {description}:目标不是目录: {target_pdir}")
|
|
291
|
+
pf_color(f" 跳过 {description}:目标不是目录: {target_pdir}", M_Color.RED)
|
|
292
|
+
return False
|
|
293
|
+
elif target_pdir.is_symlink() and abs_target == abs_source:
|
|
294
|
+
# print(f" 跳过 {description}:已经全局软连接")
|
|
295
|
+
pf_color(f" 跳过 {description}:已经全局软连接", M_Color.YELLOW)
|
|
296
|
+
return False
|
|
297
|
+
elif abs_target == abs_source:
|
|
298
|
+
# pf_color(f" 跳过 {description}:目标目录与源目录相同", M_Color.YELLOW)
|
|
299
|
+
return False
|
|
300
|
+
for item in source_pdir.iterdir():
|
|
301
|
+
if item.name.startswith("_.") or item.name.startswith(".") or item.name.startswith("__"):
|
|
302
|
+
pass
|
|
303
|
+
elif item.is_dir() and flag_dir:
|
|
304
|
+
src_path = item.resolve()
|
|
305
|
+
dst_path = abs_target / item.name
|
|
306
|
+
make_symlink(src_path, dst_path, f"{description} - {item.name}")
|
|
307
|
+
n_cnt += 1
|
|
308
|
+
elif item.is_file() and flag_file:
|
|
309
|
+
src_path = item.resolve()
|
|
310
|
+
dst_path = abs_target / item.name
|
|
311
|
+
make_symlink(src_path, dst_path, f"{description} - {item.name}")
|
|
312
|
+
n_cnt += 1
|
|
313
|
+
return n_cnt
|
|
314
|
+
|
|
315
|
+
def show_diff_side_by_side(old_content: str, new_content: str, width: int = 80):
|
|
316
|
+
"""
|
|
317
|
+
显示左右对比的彩色 DIFF
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
old_content: 旧配置内容
|
|
321
|
+
new_content: 新配置内容
|
|
322
|
+
width: 每列的宽度
|
|
323
|
+
"""
|
|
324
|
+
##; 分割为行
|
|
325
|
+
old_lines = old_content.splitlines()
|
|
326
|
+
new_lines = new_content.splitlines()
|
|
327
|
+
|
|
328
|
+
##; 使用 difflib 生成差异
|
|
329
|
+
diff = difflib.unified_diff(
|
|
330
|
+
old_lines,
|
|
331
|
+
new_lines,
|
|
332
|
+
lineterm='',
|
|
333
|
+
fromfile='Current Config',
|
|
334
|
+
tofile='New Config'
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
##; 颜色定义
|
|
338
|
+
ADDED = M_Color.GREEN
|
|
339
|
+
REMOVED = M_Color.RED
|
|
340
|
+
CHANGED = M_Color.YELLOW
|
|
341
|
+
RESET = M_Color.RESET
|
|
342
|
+
BLUE = M_Color.BLUE
|
|
343
|
+
|
|
344
|
+
print("\n" + "=" * (width * 2 + 5))
|
|
345
|
+
print(f"{BLUE}{'Current Config'.center(width)} | {'New Config'.center(width)}{RESET}")
|
|
346
|
+
print("=" * (width * 2 + 5))
|
|
347
|
+
|
|
348
|
+
##; 简单的并排显示
|
|
349
|
+
max_lines = max(len(old_lines), len(new_lines))
|
|
350
|
+
|
|
351
|
+
for i in range(max_lines):
|
|
352
|
+
old_line = old_lines[i] if i < len(old_lines) else ""
|
|
353
|
+
new_line = new_lines[i] if i < len(new_lines) else ""
|
|
354
|
+
|
|
355
|
+
##; 确定颜色
|
|
356
|
+
if old_line != new_line:
|
|
357
|
+
if old_line and not new_line:
|
|
358
|
+
##; 删除的行
|
|
359
|
+
left_color = REMOVED
|
|
360
|
+
right_color = RESET
|
|
361
|
+
elif not old_line and new_line:
|
|
362
|
+
##; 新增的行
|
|
363
|
+
left_color = RESET
|
|
364
|
+
right_color = ADDED
|
|
365
|
+
else:
|
|
366
|
+
##; 修改的行
|
|
367
|
+
left_color = CHANGED
|
|
368
|
+
right_color = CHANGED
|
|
369
|
+
else:
|
|
370
|
+
##; 相同的行
|
|
371
|
+
left_color = RESET
|
|
372
|
+
right_color = RESET
|
|
373
|
+
|
|
374
|
+
##; 截断或填充到指定宽度
|
|
375
|
+
old_display = (old_line[:width-3] + '...') if len(old_line) > width else old_line.ljust(width)
|
|
376
|
+
new_display = (new_line[:width-3] + '...') if len(new_line) > width else new_line.ljust(width)
|
|
377
|
+
|
|
378
|
+
print(f"{left_color}{old_display}{RESET} | {right_color}{new_display}{RESET}")
|
|
379
|
+
|
|
380
|
+
print("=" * (width * 2 + 5))
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def show_json_diff(old_json_str: str, new_json_str: str):
|
|
384
|
+
"""
|
|
385
|
+
显示 JSON 配置的差异(更智能的格式)
|
|
386
|
+
|
|
387
|
+
Args:
|
|
388
|
+
old_json_str: 旧 JSON 字符串
|
|
389
|
+
new_json_str: 新 JSON 字符串
|
|
390
|
+
"""
|
|
391
|
+
try:
|
|
392
|
+
old_obj = json.loads(old_json_str)
|
|
393
|
+
new_obj = json.loads(new_json_str)
|
|
394
|
+
|
|
395
|
+
##; 格式化输出
|
|
396
|
+
old_formatted = json.dumps(old_obj, ensure_ascii=False, indent=2)
|
|
397
|
+
new_formatted = json.dumps(new_obj, ensure_ascii=False, indent=2)
|
|
398
|
+
|
|
399
|
+
show_diff_side_by_side(old_formatted, new_formatted, width=70)
|
|
400
|
+
|
|
401
|
+
except json.JSONDecodeError as e:
|
|
402
|
+
pf_color(f" ⚠️ JSON 解析失败: {e}", M_Color.RED)
|
|
403
|
+
pf_color(" 将显示文本差异...", M_Color.YELLOW)
|
|
404
|
+
show_diff_side_by_side(old_json_str, new_json_str, width=70)
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
class M_ResUpdate:
|
|
408
|
+
YES = "y"
|
|
409
|
+
NO = "n"
|
|
410
|
+
MERGE = "m"
|
|
411
|
+
BLEND = "b"
|
|
412
|
+
MERGE_OLD = "f"
|
|
413
|
+
EXIT = "e"
|
|
414
|
+
|
|
415
|
+
def confirm_update() -> bool:
|
|
416
|
+
"""
|
|
417
|
+
让用户确认是否执行更新
|
|
418
|
+
|
|
419
|
+
Returns:
|
|
420
|
+
bool: True 表示确认更新,False 表示取消
|
|
421
|
+
"""
|
|
422
|
+
print("\n" + "=" * 80)
|
|
423
|
+
pf_color("是否要用新配置覆盖现有配置?", M_Color.YELLOW)
|
|
424
|
+
NOW_TAG = datetime.now().strftime("%y%m%d_%H%M")
|
|
425
|
+
print(" [y] 是,更新配置, 原配置文件将备份为 settings.local.json.{NOW_TAG}")
|
|
426
|
+
print(" [n] 否,保留现有配置 (默认)")
|
|
427
|
+
print(" [m] 合并配置, 但优先使用模板配置, 原配置文件将备份为 settings.local.json")
|
|
428
|
+
print(" [b] 合并配置, 但优先使用原有配置, 原配置文件将备份为 settings.local.json")
|
|
429
|
+
print(" [e] 取消操作, 退出当前进程")
|
|
430
|
+
print("=" * 80)
|
|
431
|
+
|
|
432
|
+
while True:
|
|
433
|
+
response = input("\n请选择 (y/n/m/b/e): ").lower().strip()
|
|
434
|
+
if response == '' or response == 'n':
|
|
435
|
+
pf_color(" 已取消更新,保留现有配置", M_Color.CYAN)
|
|
436
|
+
return M_ResUpdate.NO
|
|
437
|
+
elif response == 'y':
|
|
438
|
+
pf_color(" 确认更新配置, 原配置文件将备份为 settings.local.{NOW_TAG}.json", M_Color.GREEN)
|
|
439
|
+
return M_ResUpdate.YES
|
|
440
|
+
elif response == 'm':
|
|
441
|
+
pf_color(f" 合并两者(Merge),新生成合并后的配置, 原配置文件将备份为 settings.local.{NOW_TAG}.json", M_Color.CYAN)
|
|
442
|
+
return M_ResUpdate.MERGE
|
|
443
|
+
elif response == 'b':
|
|
444
|
+
pf_color(f" 合并两者(Blend),新生成合并后的配置, 原配置文件将备份为 settings.local.{NOW_TAG}.json", M_Color.CYAN)
|
|
445
|
+
return M_ResUpdate.BLEND
|
|
446
|
+
elif response == 'e':
|
|
447
|
+
pf_color(" 准备取消操作, 退出当前进程", M_Color.RED)
|
|
448
|
+
exit(0)
|
|
449
|
+
else:
|
|
450
|
+
pf_color(f" 无效的选项: {response},请输入 y/n/m/e", M_Color.RED)
|
|
451
|
+
|
|
452
|
+
def merge_json(low_obj: dict, high_obj: dict) -> dict:
|
|
453
|
+
"""
|
|
454
|
+
合并两个 JSON 对象,保留新对象中的所有字段
|
|
455
|
+
|
|
456
|
+
Args:
|
|
457
|
+
low_obj: 低优先级, 一般为旧JSON 对象
|
|
458
|
+
high_obj: 新优先级, 一般为新JSON 对象
|
|
459
|
+
|
|
460
|
+
Returns:
|
|
461
|
+
dict: 合并后的 JSON 对象
|
|
462
|
+
"""
|
|
463
|
+
merged_obj = low_obj.copy()
|
|
464
|
+
for key, value in high_obj.items():
|
|
465
|
+
if key in merged_obj:
|
|
466
|
+
if isinstance(value, dict) and isinstance(merged_obj[key], dict):
|
|
467
|
+
##; 递归合并嵌套字典
|
|
468
|
+
merged_obj[key] = merge_json(merged_obj[key], value)
|
|
469
|
+
elif isinstance(value, list) and isinstance(merged_obj[key], list):
|
|
470
|
+
##; 合并列表,保留新列表中的所有元素
|
|
471
|
+
merged_obj[key].extend(value)
|
|
472
|
+
else:
|
|
473
|
+
##; 直接覆盖值
|
|
474
|
+
merged_obj[key] = value
|
|
475
|
+
else:
|
|
476
|
+
##; 添加新字段
|
|
477
|
+
merged_obj[key] = value
|
|
478
|
+
return merged_obj
|
|
479
|
+
|
|
480
|
+
def is_json_content_equal(content1: str, content2: str) -> bool:
|
|
481
|
+
"""
|
|
482
|
+
比较两个 JSON 内容是否相同(忽略格式差异)
|
|
483
|
+
|
|
484
|
+
Args:
|
|
485
|
+
content1: 第一个 JSON 字符串
|
|
486
|
+
content2: 第二个 JSON 字符串
|
|
487
|
+
|
|
488
|
+
Returns:
|
|
489
|
+
bool: True 表示内容相同,False 表示不同
|
|
490
|
+
"""
|
|
491
|
+
try:
|
|
492
|
+
##; 解析为 Python 对象
|
|
493
|
+
obj1 = json.loads(content1)
|
|
494
|
+
obj2 = json.loads(content2)
|
|
495
|
+
|
|
496
|
+
##; 比较对象是否相等
|
|
497
|
+
return obj1 == obj2
|
|
498
|
+
except json.JSONDecodeError:
|
|
499
|
+
##; JSON 解析失败,降级为字符串比较
|
|
500
|
+
return content1.strip() == content2.strip()
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def upsert_template_settings(fp_dst_config: Path):
|
|
504
|
+
"""
|
|
505
|
+
生成配置文件,如果已存在则先显示 DIFF 并让用户确认, 如果修改则必须备份原配置文件
|
|
506
|
+
|
|
507
|
+
Args:
|
|
508
|
+
fp_dst_config: 目标配置文件路径
|
|
509
|
+
|
|
510
|
+
Returns:
|
|
511
|
+
bool: 是否成功生成配置
|
|
512
|
+
"""
|
|
513
|
+
##; 生成新配置内容
|
|
514
|
+
default_settings = make_default_config()
|
|
515
|
+
new_content = json.dumps(default_settings, ensure_ascii=False, indent=2)
|
|
516
|
+
|
|
517
|
+
##; 检查现有配置并显示 DIFF
|
|
518
|
+
if fp_dst_config.exists():
|
|
519
|
+
try:
|
|
520
|
+
##; 读取现有配置
|
|
521
|
+
with open(fp_dst_config, 'r', encoding='utf-8') as f:
|
|
522
|
+
old_content = f.read()
|
|
523
|
+
|
|
524
|
+
##; 检查内容是否相同
|
|
525
|
+
if is_json_content_equal(old_content, new_content):
|
|
526
|
+
pf_color(f"\n✓ 配置内容一致,无需更新: {fp_dst_config}", M_Color.GREEN)
|
|
527
|
+
return True
|
|
528
|
+
|
|
529
|
+
##; 内容不同,显示 DIFF
|
|
530
|
+
pf_color(f"\n⚠️ 检测到现有配置: {fp_dst_config}", M_Color.YELLOW)
|
|
531
|
+
pf_color("\n📊 配置差异对比:", M_Color.CYAN)
|
|
532
|
+
show_json_diff(old_content, new_content)
|
|
533
|
+
|
|
534
|
+
##; 让用户确认是否更新
|
|
535
|
+
x_ans = confirm_update()
|
|
536
|
+
if x_ans == M_ResUpdate.NO:
|
|
537
|
+
pf_color(f" ℹ️ 已保留现有配置,未做任何更改", M_Color.CYAN)
|
|
538
|
+
return False
|
|
539
|
+
elif x_ans == M_ResUpdate.MERGE:
|
|
540
|
+
##; 用户确认后,合并配置
|
|
541
|
+
old_obj = json.loads(old_content)
|
|
542
|
+
new_obj = json.loads(new_content)
|
|
543
|
+
merged_obj = merge_json(old_obj, new_obj)
|
|
544
|
+
new_content = json.dumps(merged_obj, ensure_ascii=False, indent=2)
|
|
545
|
+
elif x_ans == M_ResUpdate.BLEND:
|
|
546
|
+
##; 用户确认后,合并配置
|
|
547
|
+
old_obj = json.loads(old_content)
|
|
548
|
+
new_obj = json.loads(new_content)
|
|
549
|
+
merged_obj = merge_json(new_obj, old_obj)
|
|
550
|
+
new_content = json.dumps(merged_obj, ensure_ascii=False, indent=2)
|
|
551
|
+
|
|
552
|
+
##; 用户确认后,备份现有配置
|
|
553
|
+
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
|
554
|
+
backup_file = fp_dst_config.parent / f"settings.json.bak.{timestamp}"
|
|
555
|
+
shutil.copy2(fp_dst_config, backup_file)
|
|
556
|
+
os.chmod(backup_file, 0o444)
|
|
557
|
+
pf_color(f"\n 📦 已备份现有配置到: {backup_file}", M_Color.YELLOW)
|
|
558
|
+
|
|
559
|
+
except Exception as e:
|
|
560
|
+
pf_color(f" ⚠️ 读取现有配置失败: {e}", M_Color.RED)
|
|
561
|
+
pf_color(f" 将直接覆盖...", M_Color.YELLOW)
|
|
562
|
+
|
|
563
|
+
##; 确保目标目录存在
|
|
564
|
+
fp_dst_config.parent.mkdir(parents=True, exist_ok=True)
|
|
565
|
+
|
|
566
|
+
##; 写入配置
|
|
567
|
+
try:
|
|
568
|
+
with open(fp_dst_config, 'w', encoding='utf-8') as f:
|
|
569
|
+
f.write(new_content)
|
|
570
|
+
|
|
571
|
+
pf_color(f"\n ✅ 已生成配置: {fp_dst_config}", M_Color.GREEN)
|
|
572
|
+
return True
|
|
573
|
+
except Exception as e:
|
|
574
|
+
pf_color(f"\n ✗ 写入配置失败: {e}", M_Color.RED)
|
|
575
|
+
return False
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def generate_global_settings(source_dir: Path):
|
|
579
|
+
"""
|
|
580
|
+
生成配置文件,如果已存在则先显示 DIFF 并让用户确认
|
|
581
|
+
|
|
582
|
+
Args:
|
|
583
|
+
source_dir: 源项目目录(包含 hooks/ 目录)
|
|
584
|
+
|
|
585
|
+
Returns:
|
|
586
|
+
bool: 是否成功生成配置
|
|
587
|
+
"""
|
|
588
|
+
|
|
589
|
+
home_dir = Path.home()
|
|
590
|
+
global_settings = home_dir / ".claude" / "settings.json"
|
|
591
|
+
upsert_template_settings(global_settings)
|
|
592
|
+
pf_color(f"\n Tips: HOME/.claude/settings.json 优先级较低, 会被项目本地配置覆盖", M_Color.CYAN)
|
|
593
|
+
pf_color(
|
|
594
|
+
f"""\n
|
|
595
|
+
HOME/.claude/settings.json (低) >
|
|
596
|
+
PROJECT/.claude/settings.json (中) >
|
|
597
|
+
PROJECT/.claude/settings.local.json (高)
|
|
598
|
+
""", M_Color.CYAN)
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
def generate_project_settings(target_path: Path):
|
|
602
|
+
"""
|
|
603
|
+
为指定项目生成本地配置文件 .claude/settings.local.json
|
|
604
|
+
|
|
605
|
+
Args:
|
|
606
|
+
target_path: 目标项目路径
|
|
607
|
+
source_dir: 源模板配置目录(ClaudeSettings 目录)
|
|
608
|
+
|
|
609
|
+
Returns:
|
|
610
|
+
bool: 是否成功生成配置
|
|
611
|
+
"""
|
|
612
|
+
##; 确保目标路径存在
|
|
613
|
+
if not target_path.exists() or not target_path.is_dir():
|
|
614
|
+
pf_color(f" ✗ 目标路径不存在或不是目录: {target_path}", M_Color.RED)
|
|
615
|
+
return False
|
|
616
|
+
|
|
617
|
+
##; 本地配置文件路径
|
|
618
|
+
local_settings = target_path / ".claude" / "settings.local.json"
|
|
619
|
+
upsert_template_settings(local_settings)
|
|
620
|
+
pf_color(f"\n Tips: PROJECT/.claude/settings.local.json 优先级最高, 不会影响其他项目配置", M_Color.CYAN)
|
|
621
|
+
|
|
622
|
+
class RecordItem:
|
|
623
|
+
"""
|
|
624
|
+
记录项目链接信息的数据类
|
|
625
|
+
|
|
626
|
+
Attributes:
|
|
627
|
+
tpl_src_dir: 模板源目录
|
|
628
|
+
target_path: 目标项目路径
|
|
629
|
+
linked_time: 链接时间
|
|
630
|
+
check_time: 最新检查时间
|
|
631
|
+
check_status: 检查状态 (exist/not-found)
|
|
632
|
+
IsGitRepo: 是否为Git仓库
|
|
633
|
+
"""
|
|
634
|
+
|
|
635
|
+
def __init__(self, tpl_src_dir, target_path, linked_time,
|
|
636
|
+
check_time=None, check_status=None, IsGitRepo=None):
|
|
637
|
+
self.tpl_src_dir = tpl_src_dir
|
|
638
|
+
self.target_path = target_path
|
|
639
|
+
self.linked_time = linked_time
|
|
640
|
+
self.check_time = check_time
|
|
641
|
+
self.check_status = check_status
|
|
642
|
+
self.IsGitRepo = IsGitRepo
|
|
643
|
+
|
|
644
|
+
def to_dict(self):
|
|
645
|
+
"""转换为字典格式,只包含非 None 的字段"""
|
|
646
|
+
result = dict(
|
|
647
|
+
tpl_src_dir=self.tpl_src_dir,
|
|
648
|
+
target_path=self.target_path,
|
|
649
|
+
linked_time=self.linked_time,
|
|
650
|
+
)
|
|
651
|
+
if self.check_time is not None:
|
|
652
|
+
result["check_time"] = self.check_time
|
|
653
|
+
if self.check_status is not None:
|
|
654
|
+
result["check_status"] = self.check_status
|
|
655
|
+
if self.IsGitRepo is not None:
|
|
656
|
+
result["IsGitRepo"] = self.IsGitRepo
|
|
657
|
+
return result
|
|
658
|
+
|
|
659
|
+
@classmethod
|
|
660
|
+
def from_dict(cls, data: dict):
|
|
661
|
+
"""从字典创建 RecordItem"""
|
|
662
|
+
return cls(
|
|
663
|
+
tpl_src_dir=data.get("tpl_src_dir", ""),
|
|
664
|
+
target_path=data.get("target_path", ""),
|
|
665
|
+
linked_time=data.get("linked_time", ""),
|
|
666
|
+
check_time=data.get("check_time"),
|
|
667
|
+
check_status=data.get("check_status"),
|
|
668
|
+
IsGitRepo=data.get("IsGitRepo"),
|
|
669
|
+
)
|
|
670
|
+
|
|
671
|
+
@classmethod
|
|
672
|
+
def from_tuple(cls, target_path, linked_time, *args):
|
|
673
|
+
"""从元组创建 RecordItem(兼容旧格式)"""
|
|
674
|
+
return cls(
|
|
675
|
+
tpl_src_dir="",
|
|
676
|
+
target_path=target_path,
|
|
677
|
+
linked_time=linked_time,
|
|
678
|
+
)
|
|
679
|
+
|
|
680
|
+
@classmethod
|
|
681
|
+
def from_any(cls, data):
|
|
682
|
+
"""从任意格式创建 RecordItem"""
|
|
683
|
+
if isinstance(data, dict):
|
|
684
|
+
return cls.from_dict(data)
|
|
685
|
+
elif isinstance(data, (list, tuple)):
|
|
686
|
+
return cls.from_tuple(*data)
|
|
687
|
+
else:
|
|
688
|
+
raise ValueError(f"Unknown data type: {type(data)}")
|
|
689
|
+
|
|
690
|
+
def is_git_repo(path: Path) -> bool:
|
|
691
|
+
"""
|
|
692
|
+
检查指定路径是否为 Git 仓库
|
|
693
|
+
|
|
694
|
+
Args:
|
|
695
|
+
path: 要检查的路径
|
|
696
|
+
|
|
697
|
+
Returns:
|
|
698
|
+
bool: True 如果是 Git 仓库
|
|
699
|
+
"""
|
|
700
|
+
git_dir = path / ".git"
|
|
701
|
+
return git_dir.exists() and git_dir.is_dir()
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
def record_linked_project(source_dir, target_path, record_file=ZCO_CLAUDE_RECORD_FILE,
|
|
705
|
+
record_key="linked-projects", check_time=None, check_status=None):
|
|
706
|
+
"""
|
|
707
|
+
记录已链接的项目
|
|
708
|
+
|
|
709
|
+
Args:
|
|
710
|
+
source_dir: 源项目目录
|
|
711
|
+
target_path: 目标项目路径
|
|
712
|
+
record_file: 记录文件路径
|
|
713
|
+
record_key: 记录键名
|
|
714
|
+
check_time: 检查时间(可选)
|
|
715
|
+
check_status: 检查状态(可选)
|
|
716
|
+
"""
|
|
717
|
+
##; 读取现有记录
|
|
718
|
+
if record_file.exists():
|
|
719
|
+
try:
|
|
720
|
+
with open(record_file, 'r', encoding='utf-8') as f:
|
|
721
|
+
data = json.load(f)
|
|
722
|
+
except json.JSONDecodeError:
|
|
723
|
+
##; 文件损坏,重新创建
|
|
724
|
+
data = dict(
|
|
725
|
+
VERSION=VERSION,
|
|
726
|
+
ZCO_CLAUDE_ROOT=str(ZCO_CLAUDE_ROOT),
|
|
727
|
+
ZCO_CLAUDE_TPL_DIR=str(ZCO_CLAUDE_TPL_DIR),
|
|
728
|
+
)
|
|
729
|
+
data[record_key] = []
|
|
730
|
+
else:
|
|
731
|
+
data = dict(
|
|
732
|
+
VERSION=VERSION,
|
|
733
|
+
ZCO_CLAUDE_ROOT=str(ZCO_CLAUDE_ROOT),
|
|
734
|
+
ZCO_CLAUDE_TPL_DIR=str(ZCO_CLAUDE_TPL_DIR),
|
|
735
|
+
)
|
|
736
|
+
data[record_key] = []
|
|
737
|
+
|
|
738
|
+
##; 获取目标路径的绝对路径字符串
|
|
739
|
+
target_str = str(Path(target_path).resolve())
|
|
740
|
+
target_path_obj = Path(target_path)
|
|
741
|
+
|
|
742
|
+
##; 检查是否为 Git 仓库
|
|
743
|
+
is_git = is_git_repo(target_path_obj) if target_path_obj.exists() else None
|
|
744
|
+
|
|
745
|
+
##; 添加或更新记录
|
|
746
|
+
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
747
|
+
record_items = data.get(record_key, [])
|
|
748
|
+
|
|
749
|
+
found = False
|
|
750
|
+
for i, item in enumerate(record_items):
|
|
751
|
+
if isinstance(item, dict) and item.get("target_path") == target_str:
|
|
752
|
+
##; 更新现有记录
|
|
753
|
+
record_items[i] = {
|
|
754
|
+
"tpl_src_dir": str(source_dir),
|
|
755
|
+
"target_path": target_str,
|
|
756
|
+
"linked_time": item.get("linked_time", timestamp),
|
|
757
|
+
"check_time": check_time if check_time else timestamp,
|
|
758
|
+
"check_status": check_status if check_status else ("exist" if target_path_obj.exists() else "not-found"),
|
|
759
|
+
"IsGitRepo": is_git
|
|
760
|
+
}
|
|
761
|
+
found = True
|
|
762
|
+
break
|
|
763
|
+
elif isinstance(item, (list, tuple)) and len(item) >= 1 and item[0] == target_str:
|
|
764
|
+
##; 兼容旧格式,转换为新格式
|
|
765
|
+
record_items[i] = {
|
|
766
|
+
"tpl_src_dir": str(source_dir),
|
|
767
|
+
"target_path": target_str,
|
|
768
|
+
"linked_time": timestamp,
|
|
769
|
+
"check_time": check_time if check_time else timestamp,
|
|
770
|
+
"check_status": check_status if check_status else ("exist" if target_path_obj.exists() else "not-found"),
|
|
771
|
+
"IsGitRepo": is_git
|
|
772
|
+
}
|
|
773
|
+
found = True
|
|
774
|
+
break
|
|
775
|
+
|
|
776
|
+
if not found:
|
|
777
|
+
##; 添加新记录
|
|
778
|
+
record_items.append({
|
|
779
|
+
"tpl_src_dir": str(source_dir),
|
|
780
|
+
"target_path": target_str,
|
|
781
|
+
"linked_time": timestamp,
|
|
782
|
+
"check_time": check_time if check_time else timestamp,
|
|
783
|
+
"check_status": check_status if check_status else ("exist" if target_path_obj.exists() else "not-found"),
|
|
784
|
+
"IsGitRepo": is_git
|
|
785
|
+
})
|
|
786
|
+
|
|
787
|
+
##; 更新数据
|
|
788
|
+
data[record_key] = record_items
|
|
789
|
+
|
|
790
|
+
##; 确保目录存在
|
|
791
|
+
record_file.parent.mkdir(parents=True, exist_ok=True)
|
|
792
|
+
|
|
793
|
+
##; 写入文件
|
|
794
|
+
with open(record_file, 'w', encoding='utf-8') as f:
|
|
795
|
+
json.dump(data, f, ensure_ascii=False, indent=2)
|
|
796
|
+
|
|
797
|
+
print(f"\n已记录到:{record_file}")
|
|
798
|
+
|
|
799
|
+
|
|
800
|
+
def read_ignore_file(file_path):
|
|
801
|
+
"""
|
|
802
|
+
读取 ignore 文件并返回有效规则列表(忽略空行和注释)
|
|
803
|
+
|
|
804
|
+
Args:
|
|
805
|
+
file_path: ignore 文件路径(Path 对象)
|
|
806
|
+
|
|
807
|
+
Returns:
|
|
808
|
+
list: 有效的 ignore 规则列表
|
|
809
|
+
"""
|
|
810
|
+
if not file_path.exists():
|
|
811
|
+
return []
|
|
812
|
+
|
|
813
|
+
valid_lines = []
|
|
814
|
+
try:
|
|
815
|
+
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
|
|
816
|
+
for line in f:
|
|
817
|
+
line = line.rstrip()
|
|
818
|
+
##; 跳过空行和注释行
|
|
819
|
+
if line and not line.startswith('#'):
|
|
820
|
+
valid_lines.append(line)
|
|
821
|
+
except Exception as e:
|
|
822
|
+
print(f" ! 读取文件失败 {file_path}: {e}")
|
|
823
|
+
return []
|
|
824
|
+
|
|
825
|
+
return valid_lines
|
|
826
|
+
|
|
827
|
+
|
|
828
|
+
def merge_unique(ary1, ary2, ary3):
|
|
829
|
+
"""
|
|
830
|
+
合并三个数组并去重,保持首次出现的顺序
|
|
831
|
+
|
|
832
|
+
Args:
|
|
833
|
+
ary1, ary2, ary3: 要合并的列表
|
|
834
|
+
|
|
835
|
+
Returns:
|
|
836
|
+
tuple: (merged_list, stats_dict) 合并后的列表和统计信息
|
|
837
|
+
"""
|
|
838
|
+
seen = set()
|
|
839
|
+
merged = []
|
|
840
|
+
|
|
841
|
+
stats = {
|
|
842
|
+
'ary1_contributed': 0,
|
|
843
|
+
'ary2_contributed': 0,
|
|
844
|
+
'ary3_contributed': 0,
|
|
845
|
+
'total_unique': 0
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
##; 合并 ary1
|
|
849
|
+
for line in ary1:
|
|
850
|
+
if line not in seen:
|
|
851
|
+
seen.add(line)
|
|
852
|
+
merged.append(line)
|
|
853
|
+
stats['ary1_contributed'] += 1
|
|
854
|
+
|
|
855
|
+
##; 合并 ary2
|
|
856
|
+
for line in ary2:
|
|
857
|
+
if line not in seen:
|
|
858
|
+
seen.add(line)
|
|
859
|
+
merged.append(line)
|
|
860
|
+
stats['ary2_contributed'] += 1
|
|
861
|
+
|
|
862
|
+
##; 合并 ary3
|
|
863
|
+
for line in ary3:
|
|
864
|
+
if line not in seen:
|
|
865
|
+
seen.add(line)
|
|
866
|
+
merged.append(line)
|
|
867
|
+
stats['ary3_contributed'] += 1
|
|
868
|
+
|
|
869
|
+
stats['total_unique'] = len(merged)
|
|
870
|
+
|
|
871
|
+
return merged, stats
|
|
872
|
+
|
|
873
|
+
|
|
874
|
+
def init_claudeignore(target_path):
|
|
875
|
+
"""
|
|
876
|
+
为目标项目创建 .claudeignore 文件
|
|
877
|
+
|
|
878
|
+
合并以下文件的内容(去重,保持顺序,忽略空行和注释):
|
|
879
|
+
1. 目标项目现有的 .claudeignore
|
|
880
|
+
2. $HOME/.gitignore_global
|
|
881
|
+
3. 目标项目的 .gitignore
|
|
882
|
+
|
|
883
|
+
Args:
|
|
884
|
+
target_path: 目标项目路径(Path 对象)
|
|
885
|
+
|
|
886
|
+
Returns:
|
|
887
|
+
bool: 是否成功创建/更新文件
|
|
888
|
+
"""
|
|
889
|
+
target_abs = Path(target_path).resolve()
|
|
890
|
+
|
|
891
|
+
print("\n生成 .claudeignore...")
|
|
892
|
+
|
|
893
|
+
##; 1. 读取三个来源
|
|
894
|
+
claudeignore_orig = target_abs / ".claudeignore"
|
|
895
|
+
gitignore_global = Path.home() / ".gitignore_global"
|
|
896
|
+
gitignore_local = target_abs / ".gitignore"
|
|
897
|
+
m_ignore = ZCO_CLAUDE_TPL_DIR / "DOT.claudeignore"
|
|
898
|
+
|
|
899
|
+
ary1 = read_ignore_file(claudeignore_orig)
|
|
900
|
+
ary2 = read_ignore_file(gitignore_global)
|
|
901
|
+
ary3 = read_ignore_file(gitignore_local)
|
|
902
|
+
ary4 = read_ignore_file(m_ignore)
|
|
903
|
+
|
|
904
|
+
print(f" 读取源文件:")
|
|
905
|
+
print(f" - .claudeignore: {len(ary1)} 条规则")
|
|
906
|
+
print(f" - $HOME/.gitignore_global: {len(ary2)} 条规则")
|
|
907
|
+
print(f" - .gitignore: {len(ary3)} 条规则")
|
|
908
|
+
if len(ary2) == 0:
|
|
909
|
+
ary2 = ary4
|
|
910
|
+
|
|
911
|
+
##; 2. 合并去重
|
|
912
|
+
merged, stats = merge_unique(ary1, ary2, ary3)
|
|
913
|
+
|
|
914
|
+
if not merged:
|
|
915
|
+
print(" ! 没有找到任何 ignore 规则,跳过生成")
|
|
916
|
+
return False
|
|
917
|
+
|
|
918
|
+
##; 3. 生成新内容
|
|
919
|
+
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
920
|
+
|
|
921
|
+
content_lines = []
|
|
922
|
+
content_lines.append(f"###; update@{timestamp}")
|
|
923
|
+
content_lines.append("")
|
|
924
|
+
|
|
925
|
+
if stats['ary1_contributed'] > 0:
|
|
926
|
+
content_lines.append("#######; merged from origin .claudeignore")
|
|
927
|
+
##; 只输出来自 ary1 的规则
|
|
928
|
+
for line in merged[:stats['ary1_contributed']]:
|
|
929
|
+
content_lines.append(line)
|
|
930
|
+
content_lines.append("")
|
|
931
|
+
|
|
932
|
+
ary2_start = stats['ary1_contributed']
|
|
933
|
+
ary2_end = ary2_start + stats['ary2_contributed']
|
|
934
|
+
if stats['ary2_contributed'] > 0:
|
|
935
|
+
content_lines.append("#######; merged from $HOME/.gitignore_global")
|
|
936
|
+
for line in merged[ary2_start:ary2_end]:
|
|
937
|
+
content_lines.append(line)
|
|
938
|
+
content_lines.append("")
|
|
939
|
+
|
|
940
|
+
ary3_start = ary2_end
|
|
941
|
+
if stats['ary3_contributed'] > 0:
|
|
942
|
+
content_lines.append("#######; merged from .gitignore")
|
|
943
|
+
for line in merged[ary3_start:]:
|
|
944
|
+
content_lines.append(line)
|
|
945
|
+
content_lines.append("")
|
|
946
|
+
|
|
947
|
+
##; 4. 写入文件
|
|
948
|
+
output_file = target_abs / ".claudeignore"
|
|
949
|
+
|
|
950
|
+
##; 如果文件存在,备份
|
|
951
|
+
if output_file.exists():
|
|
952
|
+
backup_name = f".claudeignore.bak.{datetime.now().strftime('%Y%m%d_%H%M%S')}"
|
|
953
|
+
backup = target_abs / backup_name
|
|
954
|
+
shutil.copy2(output_file, backup)
|
|
955
|
+
print(f" ✓ 已备份原文件: {backup_name}")
|
|
956
|
+
|
|
957
|
+
try:
|
|
958
|
+
with open(output_file, 'w', encoding='utf-8') as f:
|
|
959
|
+
f.write('\n'.join(content_lines))
|
|
960
|
+
|
|
961
|
+
print(f" ✓ 已生成 .claudeignore:")
|
|
962
|
+
print(f" - 总规则数: {stats['total_unique']} 条(已去重)")
|
|
963
|
+
print(f" - 来自 .claudeignore: {stats['ary1_contributed']} 条")
|
|
964
|
+
print(f" - 来自 .gitignore_global: {stats['ary2_contributed']} 条")
|
|
965
|
+
print(f" - 来自 .gitignore: {stats['ary3_contributed']} 条")
|
|
966
|
+
print(f" - 文件位置: {output_file}")
|
|
967
|
+
|
|
968
|
+
return True
|
|
969
|
+
except Exception as e:
|
|
970
|
+
print(f" ✗ 写入文件失败: {e}")
|
|
971
|
+
return False
|
|
972
|
+
|
|
973
|
+
|
|
974
|
+
def is_valid_symlink(link_path: Path, expected_source: Path) -> bool:
|
|
975
|
+
"""
|
|
976
|
+
检查软链接是否有效
|
|
977
|
+
|
|
978
|
+
Args:
|
|
979
|
+
link_path: 软链接路径
|
|
980
|
+
expected_source: 期望的源路径
|
|
981
|
+
|
|
982
|
+
Returns:
|
|
983
|
+
bool: True 表示有效,False 表示无效
|
|
984
|
+
"""
|
|
985
|
+
if not link_path.exists():
|
|
986
|
+
return False
|
|
987
|
+
|
|
988
|
+
if not link_path.is_symlink():
|
|
989
|
+
return False
|
|
990
|
+
|
|
991
|
+
##; 检查软链接是否指向正确的源
|
|
992
|
+
actual_source = link_path.resolve()
|
|
993
|
+
return actual_source == expected_source.resolve()
|
|
994
|
+
|
|
995
|
+
def cmd_init_global(tpl_dir=None):
|
|
996
|
+
"""
|
|
997
|
+
子命令: init-global - 初始化全局 .claudeignore 文件
|
|
998
|
+
|
|
999
|
+
Args:
|
|
1000
|
+
tpl_dir: 模板目录路径,默认为 ZCO_CLAUDE_TPL_DIR
|
|
1001
|
+
"""
|
|
1002
|
+
##; 确定模板目录
|
|
1003
|
+
if tpl_dir is None:
|
|
1004
|
+
source_abs = ZCO_CLAUDE_TPL_DIR.resolve()
|
|
1005
|
+
else:
|
|
1006
|
+
source_abs = Path(tpl_dir).resolve()
|
|
1007
|
+
if not source_abs.exists():
|
|
1008
|
+
pf_color(f"错误:模板目录不存在: {source_abs}", M_Color.RED)
|
|
1009
|
+
sys.exit(1)
|
|
1010
|
+
##; 没有子命令: 仅生成全局配置
|
|
1011
|
+
pf_color("\n📋 模式: 生成默认的全局配置", M_Color.CYAN)
|
|
1012
|
+
pf_color(f"配置路径: $HOME/.claude/settings.json\n", M_Color.CYAN)
|
|
1013
|
+
success = generate_global_settings(ZCO_CLAUDE_TPL_DIR)
|
|
1014
|
+
|
|
1015
|
+
if success:
|
|
1016
|
+
pf_color("\n✅ 完成!配置已生成或更新。", M_Color.GREEN)
|
|
1017
|
+
else:
|
|
1018
|
+
pf_color("\n⚠️ 配置生成失败或被取消。", M_Color.YELLOW)
|
|
1019
|
+
|
|
1020
|
+
|
|
1021
|
+
|
|
1022
|
+
def cmd_init_project(target_path=None, tpl_dir=None):
|
|
1023
|
+
"""
|
|
1024
|
+
子命令: init - 初始化项目的 .claude/ 配置
|
|
1025
|
+
|
|
1026
|
+
Args:
|
|
1027
|
+
target_path: 目标项目路径,默认为当前目录
|
|
1028
|
+
tpl_dir: 模板目录路径,默认为 ZCO_CLAUDE_TPL_DIR
|
|
1029
|
+
"""
|
|
1030
|
+
##; 确定目标路径
|
|
1031
|
+
if target_path is None:
|
|
1032
|
+
target_path = Path(os.getcwd())
|
|
1033
|
+
else:
|
|
1034
|
+
target_path = Path(target_path)
|
|
1035
|
+
|
|
1036
|
+
##; 确定模板目录
|
|
1037
|
+
if tpl_dir is None:
|
|
1038
|
+
source_abs = ZCO_CLAUDE_TPL_DIR.resolve()
|
|
1039
|
+
else:
|
|
1040
|
+
source_abs = Path(tpl_dir).resolve()
|
|
1041
|
+
if not source_abs.exists():
|
|
1042
|
+
pf_color(f"错误:模板目录不存在: {source_abs}", M_Color.RED)
|
|
1043
|
+
sys.exit(1)
|
|
1044
|
+
|
|
1045
|
+
pf_color("\n📋 模式: 初始化项目", M_Color.CYAN)
|
|
1046
|
+
print(f"目标项目:{target_path}")
|
|
1047
|
+
print(f"模板目录:{source_abs}")
|
|
1048
|
+
print(f"项目配置:{target_path}/.claude/settings.local.json\n")
|
|
1049
|
+
|
|
1050
|
+
##; 验证目标目录
|
|
1051
|
+
if not target_path.exists() or not target_path.is_dir():
|
|
1052
|
+
pf_color(f"错误:目标目录无效: {target_path}", M_Color.RED)
|
|
1053
|
+
sys.exit(1)
|
|
1054
|
+
|
|
1055
|
+
##; 生成项目本地配置
|
|
1056
|
+
print("生成项目本地配置...\n")
|
|
1057
|
+
generate_project_settings(target_path)
|
|
1058
|
+
|
|
1059
|
+
##; 创建目标 .claude 目录
|
|
1060
|
+
target_claude_dir = target_path / ".claude"
|
|
1061
|
+
target_claude_dir.mkdir(exist_ok=True)
|
|
1062
|
+
|
|
1063
|
+
##; 创建软链接
|
|
1064
|
+
print("\n开始链接配置到目标项目...\n")
|
|
1065
|
+
|
|
1066
|
+
results = []
|
|
1067
|
+
|
|
1068
|
+
##; rules 目录
|
|
1069
|
+
source_rules = ZCO_CLAUDE_TPL_DIR / "rules"
|
|
1070
|
+
target_rules = target_claude_dir / "rules"
|
|
1071
|
+
results.append(make_links_for_subs(source_rules, target_rules, "rules 目录"))
|
|
1072
|
+
|
|
1073
|
+
##; hooks 目录
|
|
1074
|
+
source_hooks = ZCO_CLAUDE_TPL_DIR / "hooks"
|
|
1075
|
+
target_hooks = target_claude_dir / "hooks"
|
|
1076
|
+
results.append(make_links_for_subs(source_hooks, target_hooks, "hooks 目录"))
|
|
1077
|
+
|
|
1078
|
+
##; skills 目录
|
|
1079
|
+
source_skills = ZCO_CLAUDE_TPL_DIR / "skills"
|
|
1080
|
+
target_skills = target_claude_dir / "skills"
|
|
1081
|
+
results.append(make_links_for_subs(source_skills, target_skills, "skills 目录"))
|
|
1082
|
+
|
|
1083
|
+
##; commands 目录
|
|
1084
|
+
source_commands = ZCO_CLAUDE_TPL_DIR / "commands"
|
|
1085
|
+
target_commands = target_claude_dir / "commands"
|
|
1086
|
+
n_cnt = make_links_for_subs(source_commands, target_commands, "commands 目录", flag_dir=True, flag_file=True)
|
|
1087
|
+
|
|
1088
|
+
##; zco-scripts 目录
|
|
1089
|
+
source_scripts = ZCO_CLAUDE_TPL_DIR / "zco-scripts"
|
|
1090
|
+
target_scripts = target_claude_dir / "zco-scripts"
|
|
1091
|
+
make_symlink(source_scripts, target_scripts, "zco-scripts 目录")
|
|
1092
|
+
|
|
1093
|
+
results.append(n_cnt)
|
|
1094
|
+
|
|
1095
|
+
pf_color(f"\n✅ 完成!", M_Color.GREEN)
|
|
1096
|
+
pf_color(f" - 已生成项目本地配置")
|
|
1097
|
+
pf_color(f" - 已生成项目本地配置 .claude/settings.local.json ")
|
|
1098
|
+
pf_color(f" - 成功完成对项目的 Claude 配置扩展")
|
|
1099
|
+
pf_color(f" 配置扩展源: {target_path}")
|
|
1100
|
+
|
|
1101
|
+
##; 生成 .claudeignore
|
|
1102
|
+
try:
|
|
1103
|
+
init_claudeignore(target_path)
|
|
1104
|
+
except Exception as e:
|
|
1105
|
+
print(f"\n✗ 生成 .claudeignore 失败: {e}")
|
|
1106
|
+
else:
|
|
1107
|
+
pf_color(f" - 已生成项目本地配置 .claude/.claudeignore ")
|
|
1108
|
+
|
|
1109
|
+
pf_color(
|
|
1110
|
+
f"""\n建议:
|
|
1111
|
+
[1] 执行 echo \"**/*.local.*\" >> .gitignore 来忽略本地配置文件
|
|
1112
|
+
[1] 请根据实际情况修改 .claude/settings.local.json 中的配置
|
|
1113
|
+
|
|
1114
|
+
欢迎一起构建和维护健康绿色的 ClaudeSettings 模板库!
|
|
1115
|
+
""", M_Color.CYAN)
|
|
1116
|
+
|
|
1117
|
+
##; 记录链接的项目
|
|
1118
|
+
if any(results):
|
|
1119
|
+
record_linked_project(source_abs, target_path)
|
|
1120
|
+
|
|
1121
|
+
|
|
1122
|
+
def cmd_list_linked_repos(record_file=None):
|
|
1123
|
+
"""
|
|
1124
|
+
子命令: list-linked-repos - 列出所有已链接的项目
|
|
1125
|
+
|
|
1126
|
+
Args:
|
|
1127
|
+
record_file: 记录文件路径,默认为 ZCO_CLAUDE_RECORD_FILE
|
|
1128
|
+
"""
|
|
1129
|
+
##; 确定记录文件路径
|
|
1130
|
+
if record_file is None:
|
|
1131
|
+
record_file = ZCO_CLAUDE_RECORD_FILE
|
|
1132
|
+
else:
|
|
1133
|
+
record_file = Path(record_file)
|
|
1134
|
+
|
|
1135
|
+
pf_color("\n📋 已链接项目列表\n", M_Color.CYAN)
|
|
1136
|
+
pf_color(f"记录文件: {record_file}\n", M_Color.GREEN)
|
|
1137
|
+
|
|
1138
|
+
##; 读取记录文件
|
|
1139
|
+
if not record_file.exists():
|
|
1140
|
+
print("无已链接项目")
|
|
1141
|
+
return
|
|
1142
|
+
|
|
1143
|
+
try:
|
|
1144
|
+
with open(record_file, 'r', encoding='utf-8') as f:
|
|
1145
|
+
data = json.load(f)
|
|
1146
|
+
except json.JSONDecodeError as e:
|
|
1147
|
+
pf_color(f"错误:无法解析记录文件 - {e}", M_Color.RED)
|
|
1148
|
+
return
|
|
1149
|
+
except Exception as e:
|
|
1150
|
+
pf_color(f"错误:读取记录文件失败 - {e}", M_Color.RED)
|
|
1151
|
+
return
|
|
1152
|
+
|
|
1153
|
+
record_key = "linked-projects"
|
|
1154
|
+
record_items = data.get(record_key, [])
|
|
1155
|
+
|
|
1156
|
+
if not record_items:
|
|
1157
|
+
print("无已链接项目")
|
|
1158
|
+
return
|
|
1159
|
+
|
|
1160
|
+
##; 格式化输出
|
|
1161
|
+
pf_color(f"{'链接时间':<22} {'项目路径'}", M_Color.CYAN)
|
|
1162
|
+
pf_color("-" * 80, M_Color.CYAN)
|
|
1163
|
+
|
|
1164
|
+
for i, item in enumerate(record_items):
|
|
1165
|
+
if isinstance(item, dict):
|
|
1166
|
+
linked_time = item.get("linked_time", "未知")
|
|
1167
|
+
target_path = item.get("target_path", "未知")
|
|
1168
|
+
elif isinstance(item, (list, tuple)) and len(item) >= 2:
|
|
1169
|
+
##; 兼容旧格式 (target_path, linked_time, ...)
|
|
1170
|
+
target_path = item[0]
|
|
1171
|
+
linked_time = item[1]
|
|
1172
|
+
else:
|
|
1173
|
+
continue
|
|
1174
|
+
|
|
1175
|
+
pf_color(f"[{i:03d}] [{linked_time}] {target_path}", M_Color.CYAN)
|
|
1176
|
+
|
|
1177
|
+
pf_color(f"\n总计: {len(record_items)} 个项目")
|
|
1178
|
+
|
|
1179
|
+
|
|
1180
|
+
def cmd_fix_linked_repos(record_file=None, remove_not_found=False):
|
|
1181
|
+
"""
|
|
1182
|
+
子命令: fix-linked-repos - 修复已链接项目的软链接
|
|
1183
|
+
|
|
1184
|
+
Args:
|
|
1185
|
+
record_file: 记录文件路径,默认为 ZCO_CLAUDE_RECORD_FILE
|
|
1186
|
+
remove_not_found: 是否删除不存在的项目记录
|
|
1187
|
+
"""
|
|
1188
|
+
##; 确定记录文件路径
|
|
1189
|
+
if record_file is None:
|
|
1190
|
+
record_file = ZCO_CLAUDE_RECORD_FILE
|
|
1191
|
+
else:
|
|
1192
|
+
record_file = Path(record_file)
|
|
1193
|
+
|
|
1194
|
+
pf_color("\n🔧 修复已链接项目的软链接\n", M_Color.CYAN)
|
|
1195
|
+
print(f"记录文件:{record_file}\n")
|
|
1196
|
+
|
|
1197
|
+
##; 读取记录文件
|
|
1198
|
+
if not record_file.exists():
|
|
1199
|
+
print("无已链接项目")
|
|
1200
|
+
return
|
|
1201
|
+
|
|
1202
|
+
try:
|
|
1203
|
+
with open(record_file, 'r', encoding='utf-8') as f:
|
|
1204
|
+
data = json.load(f)
|
|
1205
|
+
except json.JSONDecodeError as e:
|
|
1206
|
+
pf_color(f"错误:无法解析记录文件 - {e}", M_Color.RED)
|
|
1207
|
+
return
|
|
1208
|
+
except Exception as e:
|
|
1209
|
+
pf_color(f"错误:读取记录文件失败 - {e}", M_Color.RED)
|
|
1210
|
+
return
|
|
1211
|
+
|
|
1212
|
+
record_key = "linked-projects"
|
|
1213
|
+
record_items = data.get(record_key, [])
|
|
1214
|
+
|
|
1215
|
+
if not record_items:
|
|
1216
|
+
print("无已链接项目")
|
|
1217
|
+
return
|
|
1218
|
+
|
|
1219
|
+
source_abs = ZCO_CLAUDE_TPL_DIR.resolve()
|
|
1220
|
+
total_checked = 0
|
|
1221
|
+
total_fixed = 0
|
|
1222
|
+
total_valid = 0
|
|
1223
|
+
total_projects = 0
|
|
1224
|
+
removed_count = 0
|
|
1225
|
+
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
1226
|
+
|
|
1227
|
+
##; 需要检查的子目录
|
|
1228
|
+
subdirs = ['rules', 'hooks', 'skills', 'commands']
|
|
1229
|
+
|
|
1230
|
+
##; 创建新的记录列表(用于过滤已删除的项目)
|
|
1231
|
+
new_record_items = []
|
|
1232
|
+
|
|
1233
|
+
for item in record_items:
|
|
1234
|
+
##; 解析记录项
|
|
1235
|
+
record_item = RecordItem.from_any(item)
|
|
1236
|
+
target_path = Path(record_item.target_path)
|
|
1237
|
+
|
|
1238
|
+
##; 检查项目是否存在
|
|
1239
|
+
if not target_path.exists():
|
|
1240
|
+
check_status = "not-found"
|
|
1241
|
+
is_git = None
|
|
1242
|
+
|
|
1243
|
+
if remove_not_found:
|
|
1244
|
+
pf_color(f"⚠️ 项目不存在,已从记录中移除: {target_path}", M_Color.YELLOW)
|
|
1245
|
+
removed_count += 1
|
|
1246
|
+
continue ##; 跳过添加到新列表
|
|
1247
|
+
else:
|
|
1248
|
+
pf_color(f"⚠️ 项目不存在: {target_path}", M_Color.YELLOW)
|
|
1249
|
+
##; 更新记录字段
|
|
1250
|
+
record_item.check_time = timestamp
|
|
1251
|
+
record_item.check_status = check_status
|
|
1252
|
+
record_item.IsGitRepo = is_git
|
|
1253
|
+
new_record_items.append(record_item.to_dict())
|
|
1254
|
+
continue
|
|
1255
|
+
|
|
1256
|
+
##; 项目存在,进行修复检查
|
|
1257
|
+
total_projects += 1
|
|
1258
|
+
check_status = "exist"
|
|
1259
|
+
is_git = is_git_repo(target_path)
|
|
1260
|
+
print(f"\n检查项目: {target_path} (Git: {is_git})")
|
|
1261
|
+
|
|
1262
|
+
target_claude_dir = target_path / ".claude"
|
|
1263
|
+
if not target_claude_dir.exists():
|
|
1264
|
+
pf_color(f" 跳过: .claude 目录不存在", M_Color.YELLOW)
|
|
1265
|
+
##; 仍然更新记录字段
|
|
1266
|
+
record_item.check_time = timestamp
|
|
1267
|
+
record_item.check_status = check_status
|
|
1268
|
+
record_item.IsGitRepo = is_git
|
|
1269
|
+
new_record_items.append(record_item.to_dict())
|
|
1270
|
+
continue
|
|
1271
|
+
|
|
1272
|
+
project_checked = 0
|
|
1273
|
+
project_fixed = 0
|
|
1274
|
+
project_valid = 0
|
|
1275
|
+
|
|
1276
|
+
##; 检查每个子目录的软链接
|
|
1277
|
+
for subdir in subdirs:
|
|
1278
|
+
source_subdir = source_abs / subdir
|
|
1279
|
+
target_subdir = target_claude_dir / subdir
|
|
1280
|
+
|
|
1281
|
+
if not target_subdir.exists():
|
|
1282
|
+
continue
|
|
1283
|
+
|
|
1284
|
+
if not source_subdir.exists():
|
|
1285
|
+
pf_color(f" 跳过 {subdir}: 源目录不存在", M_Color.YELLOW)
|
|
1286
|
+
continue
|
|
1287
|
+
|
|
1288
|
+
for item_path in target_subdir.iterdir():
|
|
1289
|
+
project_checked += 1
|
|
1290
|
+
total_checked += 1
|
|
1291
|
+
|
|
1292
|
+
##; 确定期望的源路径
|
|
1293
|
+
source_item = source_subdir / item_path.name
|
|
1294
|
+
if not item_path.is_symlink():
|
|
1295
|
+
if item_path.exists():
|
|
1296
|
+
pf_color(f" ¶ {subdir}/{item_path.name} → 不是软链接,且存在, 自行跳过", M_Color.GREEN)
|
|
1297
|
+
continue
|
|
1298
|
+
elif not source_item.exists():
|
|
1299
|
+
pf_color(f" x {subdir}/{item_path.name} → 不是软链接,且不存在同名的配置模板", M_Color.RED)
|
|
1300
|
+
continue
|
|
1301
|
+
elif source_item.exists():
|
|
1302
|
+
pf_color(f" ∆ {subdir}/{item_path.name} → 不是软链接,且存在同名的配置模板, 可能存在自定义配置, 请自行检查", M_Color.CYAN)
|
|
1303
|
+
continue
|
|
1304
|
+
elif is_valid_symlink(item_path, source_item):
|
|
1305
|
+
project_valid += 1
|
|
1306
|
+
total_valid += 1
|
|
1307
|
+
print(f" ✓ {subdir}/{item_path.name} → 模板链接有效")
|
|
1308
|
+
else:
|
|
1309
|
+
##; 删除失效链接
|
|
1310
|
+
try:
|
|
1311
|
+
if item_path.is_symlink() or item_path.exists():
|
|
1312
|
+
item_path.unlink()
|
|
1313
|
+
|
|
1314
|
+
##; 重新创建
|
|
1315
|
+
if source_item.exists():
|
|
1316
|
+
item_path.symlink_to(source_item)
|
|
1317
|
+
project_fixed += 1
|
|
1318
|
+
total_fixed += 1
|
|
1319
|
+
pf_color(f" † {subdir}/{item_path.name} → 失效,已修复", M_Color.YELLOW)
|
|
1320
|
+
else:
|
|
1321
|
+
pf_color(f" ✗ {subdir}/{item_path.name} → 失效,源不存在", M_Color.RED)
|
|
1322
|
+
except Exception as e:
|
|
1323
|
+
pf_color(f" ✗ {subdir}/{item_path.name} → 修复失败: {e}", M_Color.RED)
|
|
1324
|
+
|
|
1325
|
+
##; 显示项目修复摘要
|
|
1326
|
+
if project_checked > 0:
|
|
1327
|
+
if project_fixed == 0:
|
|
1328
|
+
print(f" ✓ 所有软链接有效 ({project_valid}/{project_checked})")
|
|
1329
|
+
else:
|
|
1330
|
+
print(f" 修复: {project_fixed}, 有效: {project_valid}, 总计: {project_checked}")
|
|
1331
|
+
|
|
1332
|
+
##; 更新记录字段
|
|
1333
|
+
record_item.check_time = timestamp
|
|
1334
|
+
record_item.check_status = check_status
|
|
1335
|
+
record_item.IsGitRepo = is_git
|
|
1336
|
+
new_record_items.append(record_item.to_dict())
|
|
1337
|
+
|
|
1338
|
+
##; 更新记录文件
|
|
1339
|
+
data[record_key] = new_record_items
|
|
1340
|
+
try:
|
|
1341
|
+
with open(record_file, 'w', encoding='utf-8') as f:
|
|
1342
|
+
json.dump(data, f, ensure_ascii=False, indent=2)
|
|
1343
|
+
print(f"\n{M_Color.GREEN}✓ 记录文件已更新{M_Color.RESET}")
|
|
1344
|
+
except Exception as e:
|
|
1345
|
+
pf_color(f"\n⚠️ 更新记录文件失败: {e}", M_Color.YELLOW)
|
|
1346
|
+
|
|
1347
|
+
##; 显示总体摘要
|
|
1348
|
+
print(f"\n{'='*60}")
|
|
1349
|
+
pf_color("修复完成:", M_Color.GREEN)
|
|
1350
|
+
print(f" - 检查项目数: {total_projects}")
|
|
1351
|
+
print(f" - 检查软链接数: {total_checked}")
|
|
1352
|
+
print(f" - 有效软链接: {total_valid}")
|
|
1353
|
+
print(f" - 修复软链接: {total_fixed}")
|
|
1354
|
+
if remove_not_found:
|
|
1355
|
+
print(f" - 移除不存在项目: {removed_count}")
|
|
1356
|
+
print(f" - 记录项目数: {len(new_record_items)}")
|
|
1357
|
+
pf_color("修复完成:", M_Color.GREEN)
|
|
1358
|
+
print(f" - 检查项目数: {total_projects}")
|
|
1359
|
+
print(f" - 检查软链接数: {total_checked}")
|
|
1360
|
+
print(f" - 有效软链接: {total_valid}")
|
|
1361
|
+
print(f" - 修复软链接: {total_fixed}")
|
|
1362
|
+
|
|
1363
|
+
|
|
1364
|
+
def run_init_legacy(target_path):
|
|
1365
|
+
"""
|
|
1366
|
+
兼容旧版:初始化指定项目
|
|
1367
|
+
"""
|
|
1368
|
+
pf_color("\n📋 模式: 配置指定项目", M_Color.CYAN)
|
|
1369
|
+
|
|
1370
|
+
##; 验证路径
|
|
1371
|
+
target_abs, source_abs = validate_paths(target_path, ZCO_CLAUDE_TPL_DIR)
|
|
1372
|
+
|
|
1373
|
+
print(f"\n源项目:{source_abs}")
|
|
1374
|
+
print(f"目标项目:{target_abs}")
|
|
1375
|
+
print(f"项目配置:{target_abs}/.claude/settings.local.json\n")
|
|
1376
|
+
|
|
1377
|
+
##; 生成项目本地配置
|
|
1378
|
+
print("生成项目本地配置...\n")
|
|
1379
|
+
generate_project_settings(target_abs)
|
|
1380
|
+
|
|
1381
|
+
##; 创建目标 .claude 目录
|
|
1382
|
+
target_claude_dir = target_abs / ".claude"
|
|
1383
|
+
target_claude_dir.mkdir(exist_ok=True)
|
|
1384
|
+
|
|
1385
|
+
##; 创建软链接
|
|
1386
|
+
print("\n开始链接配置到目标项目...\n")
|
|
1387
|
+
|
|
1388
|
+
results = []
|
|
1389
|
+
|
|
1390
|
+
##; rules 目录
|
|
1391
|
+
source_rules = ZCO_CLAUDE_TPL_DIR / "rules"
|
|
1392
|
+
target_rules = target_claude_dir / "rules"
|
|
1393
|
+
results.append(make_links_for_subs(source_rules, target_rules, "rules 目录"))
|
|
1394
|
+
|
|
1395
|
+
##; hooks 目录
|
|
1396
|
+
source_hooks = ZCO_CLAUDE_TPL_DIR / "hooks"
|
|
1397
|
+
target_hooks = target_claude_dir / "hooks"
|
|
1398
|
+
results.append(make_links_for_subs(source_hooks, target_hooks, "hooks 目录"))
|
|
1399
|
+
|
|
1400
|
+
##; skills 目录
|
|
1401
|
+
source_skills = ZCO_CLAUDE_TPL_DIR / "skills"
|
|
1402
|
+
target_skills = target_claude_dir / "skills"
|
|
1403
|
+
results.append(make_links_for_subs(source_skills, target_skills, "skills 目录"))
|
|
1404
|
+
|
|
1405
|
+
##; commands 目录
|
|
1406
|
+
source_commands = ZCO_CLAUDE_TPL_DIR / "commands"
|
|
1407
|
+
target_commands = target_claude_dir / "commands"
|
|
1408
|
+
n_cnt = make_links_for_subs(source_commands, target_commands, "commands 目录", flag_dir=True, flag_file=True)
|
|
1409
|
+
|
|
1410
|
+
##; zco-scripts 目录
|
|
1411
|
+
source_scripts = ZCO_CLAUDE_TPL_DIR / "zco-scripts"
|
|
1412
|
+
target_scripts = target_claude_dir / "zco-scripts"
|
|
1413
|
+
make_symlink(source_scripts, target_scripts, "zco-scripts 目录")
|
|
1414
|
+
|
|
1415
|
+
results.append(n_cnt)
|
|
1416
|
+
|
|
1417
|
+
pf_color(f"\n✅ 完成!", M_Color.GREEN)
|
|
1418
|
+
pf_color(f" - 已生成项目本地配置")
|
|
1419
|
+
pf_color(f" - 已生成项目本地配置 .claude/settings.local.json ")
|
|
1420
|
+
pf_color(f" - 成功完成对项目的 Claude 配置扩展")
|
|
1421
|
+
pf_color(f" 配置扩展源: {target_abs}")
|
|
1422
|
+
|
|
1423
|
+
##; 生成 .claudeignore
|
|
1424
|
+
try:
|
|
1425
|
+
init_claudeignore(target_abs)
|
|
1426
|
+
except Exception as e:
|
|
1427
|
+
print(f"\n✗ 生成 .claudeignore 失败: {e}")
|
|
1428
|
+
else:
|
|
1429
|
+
pf_color(f" - 已生成项目本地配置 .claude/.claudeignore ")
|
|
1430
|
+
|
|
1431
|
+
pf_color(
|
|
1432
|
+
f"""\n建议:
|
|
1433
|
+
[1] 执行 echo \"**/*.local.*\" >> .gitignore 来忽略本地配置文件
|
|
1434
|
+
[1] 请根据实际情况修改 .claude/settings.local.json 中的配置
|
|
1435
|
+
|
|
1436
|
+
欢迎一起构建和维护健康绿色的 ClaudeSettings 模板库!
|
|
1437
|
+
""", M_Color.CYAN)
|
|
1438
|
+
|
|
1439
|
+
##; 记录链接的项目
|
|
1440
|
+
if any(results):
|
|
1441
|
+
record_linked_project(source_abs, target_abs)
|
|
1442
|
+
|
|
1443
|
+
|
|
1444
|
+
def cmd_fix(project_path=None, tpl_dir=None, record_file=None):
|
|
1445
|
+
"""
|
|
1446
|
+
子命令: fix - 修复指定项目的软链接并更新记录
|
|
1447
|
+
|
|
1448
|
+
Args:
|
|
1449
|
+
project_path: 目标项目路径,默认为当前目录
|
|
1450
|
+
tpl_dir: 模板目录路径,默认为 ZCO_CLAUDE_TPL_DIR
|
|
1451
|
+
record_file: 记录文件路径,默认为 ZCO_CLAUDE_RECORD_FILE
|
|
1452
|
+
"""
|
|
1453
|
+
##; 确定目标路径
|
|
1454
|
+
if project_path is None:
|
|
1455
|
+
target_path = Path(os.getcwd())
|
|
1456
|
+
else:
|
|
1457
|
+
target_path = Path(project_path)
|
|
1458
|
+
|
|
1459
|
+
##; 确定模板目录
|
|
1460
|
+
if tpl_dir is None:
|
|
1461
|
+
source_abs = ZCO_CLAUDE_TPL_DIR.resolve()
|
|
1462
|
+
else:
|
|
1463
|
+
source_abs = Path(tpl_dir).resolve()
|
|
1464
|
+
if not source_abs.exists():
|
|
1465
|
+
pf_color(f"错误:模板目录不存在: {source_abs}", M_Color.RED)
|
|
1466
|
+
sys.exit(1)
|
|
1467
|
+
|
|
1468
|
+
##; 确定记录文件
|
|
1469
|
+
if record_file is None:
|
|
1470
|
+
record_file = ZCO_CLAUDE_RECORD_FILE
|
|
1471
|
+
else:
|
|
1472
|
+
record_file = Path(record_file)
|
|
1473
|
+
|
|
1474
|
+
pf_color("\n🔧 修复项目软链接\n", M_Color.CYAN)
|
|
1475
|
+
print(f"目标项目:{target_path}")
|
|
1476
|
+
print(f"模板目录:{source_abs}\n")
|
|
1477
|
+
|
|
1478
|
+
##; 检查项目是否存在
|
|
1479
|
+
if not target_path.exists():
|
|
1480
|
+
pf_color(f"错误:项目不存在: {target_path}", M_Color.RED)
|
|
1481
|
+
##; 仍然更新记录
|
|
1482
|
+
record_linked_project(source_abs, target_path, record_file=record_file,
|
|
1483
|
+
check_status="not-found")
|
|
1484
|
+
return
|
|
1485
|
+
|
|
1486
|
+
##; 检查是否为 Git 仓库
|
|
1487
|
+
is_git = is_git_repo(target_path)
|
|
1488
|
+
|
|
1489
|
+
target_claude_dir = target_path / ".claude"
|
|
1490
|
+
if not target_claude_dir.exists():
|
|
1491
|
+
pf_color(f"警告:.claude 目录不存在,创建中...", M_Color.YELLOW)
|
|
1492
|
+
target_claude_dir.mkdir(parents=True, exist_ok=True)
|
|
1493
|
+
|
|
1494
|
+
##; 需要检查的子目录
|
|
1495
|
+
subdirs = ['rules', 'hooks', 'skills', 'commands']
|
|
1496
|
+
total_checked = 0
|
|
1497
|
+
total_fixed = 0
|
|
1498
|
+
total_valid = 0
|
|
1499
|
+
|
|
1500
|
+
print("开始检查和修复软链接...\n")
|
|
1501
|
+
|
|
1502
|
+
for subdir in subdirs:
|
|
1503
|
+
source_subdir = source_abs / subdir
|
|
1504
|
+
target_subdir = target_claude_dir / subdir
|
|
1505
|
+
|
|
1506
|
+
if not source_subdir.exists():
|
|
1507
|
+
pf_color(f" 跳过 {subdir}: 源目录不存在", M_Color.YELLOW)
|
|
1508
|
+
continue
|
|
1509
|
+
|
|
1510
|
+
##; 确保目标子目录存在
|
|
1511
|
+
if not target_subdir.exists():
|
|
1512
|
+
target_subdir.mkdir(parents=True, exist_ok=True)
|
|
1513
|
+
|
|
1514
|
+
for item in source_subdir.iterdir():
|
|
1515
|
+
if item.name.startswith("_."):
|
|
1516
|
+
continue
|
|
1517
|
+
|
|
1518
|
+
target_item = target_subdir / item.name
|
|
1519
|
+
total_checked += 1
|
|
1520
|
+
|
|
1521
|
+
if is_valid_symlink(target_item, item):
|
|
1522
|
+
total_valid += 1
|
|
1523
|
+
print(f" ✓ {subdir}/{item.name} → 有效")
|
|
1524
|
+
else:
|
|
1525
|
+
##; 删除失效链接或文件
|
|
1526
|
+
try:
|
|
1527
|
+
if target_item.exists() or target_item.is_symlink():
|
|
1528
|
+
target_item.unlink()
|
|
1529
|
+
##; 重新创建
|
|
1530
|
+
target_item.symlink_to(item)
|
|
1531
|
+
total_fixed += 1
|
|
1532
|
+
pf_color(f" † {subdir}/{item.name} → 已修复", M_Color.YELLOW)
|
|
1533
|
+
except Exception as e:
|
|
1534
|
+
pf_color(f" ✗ {subdir}/{item.name} → 修复失败: {e}", M_Color.RED)
|
|
1535
|
+
|
|
1536
|
+
##; 处理 zco-scripts 目录
|
|
1537
|
+
source_scripts = source_abs / "zco-scripts"
|
|
1538
|
+
target_scripts = target_claude_dir / "zco-scripts"
|
|
1539
|
+
if source_scripts.exists():
|
|
1540
|
+
if is_valid_symlink(target_scripts, source_scripts):
|
|
1541
|
+
print(f" ✓ zco-scripts → 有效")
|
|
1542
|
+
else:
|
|
1543
|
+
try:
|
|
1544
|
+
if target_scripts.exists() or target_scripts.is_symlink():
|
|
1545
|
+
target_scripts.unlink()
|
|
1546
|
+
target_scripts.symlink_to(source_scripts)
|
|
1547
|
+
pf_color(f" † zco-scripts → 已修复", M_Color.YELLOW)
|
|
1548
|
+
except Exception as e:
|
|
1549
|
+
pf_color(f" ✗ zco-scripts → 修复失败: {e}", M_Color.RED)
|
|
1550
|
+
|
|
1551
|
+
##; 更新记录
|
|
1552
|
+
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
1553
|
+
record_linked_project(source_abs, target_path, record_file=record_file,
|
|
1554
|
+
check_time=timestamp, check_status="exist")
|
|
1555
|
+
|
|
1556
|
+
##; 显示摘要
|
|
1557
|
+
print(f"\n{'='*60}")
|
|
1558
|
+
pf_color("修复完成:", M_Color.GREEN)
|
|
1559
|
+
print(f" - 检查软链接数: {total_checked}")
|
|
1560
|
+
print(f" - 有效软链接: {total_valid}")
|
|
1561
|
+
print(f" - 修复软链接: {total_fixed}")
|
|
1562
|
+
print(f" - Git 仓库: {is_git}")
|
|
1563
|
+
print(f" - 记录已更新")
|
|
1564
|
+
|
|
1565
|
+
|
|
1566
|
+
def main():
|
|
1567
|
+
"""主函数"""
|
|
1568
|
+
##; 向后兼容:检查第一个参数是否是子命令或路径
|
|
1569
|
+
import sys
|
|
1570
|
+
argv = sys.argv[1:]
|
|
1571
|
+
|
|
1572
|
+
##; 定义有效的子命令
|
|
1573
|
+
valid_commands = {'init', 'list-linked-repos', 'fix-linked-repos', 'fix'}
|
|
1574
|
+
|
|
1575
|
+
##; 检查是否是旧版用法(第一个参数是路径而不是子命令)
|
|
1576
|
+
is_legacy = False
|
|
1577
|
+
if argv and argv[0] not in valid_commands and not argv[0].startswith('-'):
|
|
1578
|
+
##; 第一个参数既不是子命令也不是选项,可能是路径
|
|
1579
|
+
##; 但需要排除 help 和 version
|
|
1580
|
+
if argv[0] not in ('-h', '--help', '--version'):
|
|
1581
|
+
##; 检查是否是有效的路径
|
|
1582
|
+
potential_path = Path(argv[0])
|
|
1583
|
+
if potential_path.exists() and potential_path.is_dir():
|
|
1584
|
+
is_legacy = True
|
|
1585
|
+
elif '/' in argv[0] or argv[0].startswith('.'):
|
|
1586
|
+
##; 包含路径分隔符或以 . 开头,可能是路径
|
|
1587
|
+
is_legacy = True
|
|
1588
|
+
|
|
1589
|
+
if is_legacy:
|
|
1590
|
+
##; 旧版用法:第一个参数是目标路径
|
|
1591
|
+
target_path = argv[0]
|
|
1592
|
+
run_init_legacy(target_path)
|
|
1593
|
+
return
|
|
1594
|
+
|
|
1595
|
+
##; 创建主解析器
|
|
1596
|
+
parser = argparse.ArgumentParser(
|
|
1597
|
+
description="Claude Code 配置管理工具",
|
|
1598
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
1599
|
+
epilog="""
|
|
1600
|
+
常用使用示例:
|
|
1601
|
+
1. 初始化全局配置:
|
|
1602
|
+
%(prog)s init
|
|
1603
|
+
|
|
1604
|
+
2. 初始化当前项目:
|
|
1605
|
+
%(prog)s init .
|
|
1606
|
+
|
|
1607
|
+
3. 列出已链接项目:
|
|
1608
|
+
%(prog)s list-linked-repos
|
|
1609
|
+
|
|
1610
|
+
4. 修复已链接项目的软链接:
|
|
1611
|
+
%(prog)s fix-linked-repos
|
|
1612
|
+
|
|
1613
|
+
5. 修复项目配置:
|
|
1614
|
+
%(prog)s fix /path/to/target/project
|
|
1615
|
+
|
|
1616
|
+
说明:
|
|
1617
|
+
- init . : 在当前目录初始化 .claude/ 配置
|
|
1618
|
+
- list-linked-repos: 显示所有已初始化的项目列表
|
|
1619
|
+
- fix-linked-repos: 检查并修复所有软链接
|
|
1620
|
+
- 更多帮助请参考: %(prog)s <command> --help
|
|
1621
|
+
eg: %(prog)s init --help
|
|
1622
|
+
"""
|
|
1623
|
+
)
|
|
1624
|
+
parser.add_argument(
|
|
1625
|
+
"--version",
|
|
1626
|
+
action="version",
|
|
1627
|
+
version=f"%(prog)s {VERSION}"
|
|
1628
|
+
)
|
|
1629
|
+
|
|
1630
|
+
##; 创建子命令解析器
|
|
1631
|
+
subparsers = parser.add_subparsers(dest='command', help='可用命令')
|
|
1632
|
+
|
|
1633
|
+
##; 子命令: init
|
|
1634
|
+
parser_init = subparsers.add_parser(
|
|
1635
|
+
'init',
|
|
1636
|
+
help='初始化项目的 .claude/ 配置',
|
|
1637
|
+
description='创建 .claude/ 目录和软链接'
|
|
1638
|
+
)
|
|
1639
|
+
parser_init.add_argument(
|
|
1640
|
+
'project_path',
|
|
1641
|
+
nargs='?',
|
|
1642
|
+
default=None,
|
|
1643
|
+
help='目标项目路径(可选), 如果为空则初始化全局的 $HOME/.claude/settings.json, 支持相对路径'
|
|
1644
|
+
)
|
|
1645
|
+
parser_init.add_argument(
|
|
1646
|
+
'--tpl',
|
|
1647
|
+
default=None,
|
|
1648
|
+
help=f"模板目录路径(可选,默认为 ${ZCO_CLAUDE_TPL_DIR})"
|
|
1649
|
+
)
|
|
1650
|
+
|
|
1651
|
+
##; 子命令: list-linked-repos
|
|
1652
|
+
parser_list = subparsers.add_parser(
|
|
1653
|
+
'list-linked-repos',
|
|
1654
|
+
help='列出所有已链接的项目',
|
|
1655
|
+
description='读取记录文件并显示所有已初始化项目'
|
|
1656
|
+
)
|
|
1657
|
+
parser_list.add_argument(
|
|
1658
|
+
'--record-file',
|
|
1659
|
+
default=None,
|
|
1660
|
+
help='记录文件路径(可选,默认为 ~/.claude/zco-linked-projects.json)'
|
|
1661
|
+
)
|
|
1662
|
+
|
|
1663
|
+
##; 子命令: fix-linked-repos
|
|
1664
|
+
parser_fix_repos = subparsers.add_parser(
|
|
1665
|
+
'fix-linked-repos',
|
|
1666
|
+
help='修复已链接项目的软链接',
|
|
1667
|
+
description='检查所有已链接项目的软链接,删除失效链接并重新创建'
|
|
1668
|
+
)
|
|
1669
|
+
parser_fix_repos.add_argument(
|
|
1670
|
+
'--record-file',
|
|
1671
|
+
default=None,
|
|
1672
|
+
help='记录文件路径(可选,默认为 ~/.claude/zco-linked-projects.json)'
|
|
1673
|
+
)
|
|
1674
|
+
parser_fix_repos.add_argument(
|
|
1675
|
+
'--remove-not-found',
|
|
1676
|
+
action='store_true',
|
|
1677
|
+
default=False,
|
|
1678
|
+
help='删除不存在的项目记录'
|
|
1679
|
+
)
|
|
1680
|
+
|
|
1681
|
+
##; 子命令: fix - 修复单个项目的软链接
|
|
1682
|
+
parser_fix = subparsers.add_parser(
|
|
1683
|
+
'fix',
|
|
1684
|
+
help='修复指定项目的软链接',
|
|
1685
|
+
description='修复指定项目的软链接并更新记录'
|
|
1686
|
+
)
|
|
1687
|
+
parser_fix.add_argument(
|
|
1688
|
+
'project_path',
|
|
1689
|
+
nargs='?',
|
|
1690
|
+
default=None,
|
|
1691
|
+
help='目标项目路径(可选,默认为当前目录)'
|
|
1692
|
+
)
|
|
1693
|
+
parser_fix.add_argument(
|
|
1694
|
+
'--tpl',
|
|
1695
|
+
default=None,
|
|
1696
|
+
help='模板目录路径(可选,默认为 ClaudeSettings)'
|
|
1697
|
+
)
|
|
1698
|
+
parser_fix.add_argument(
|
|
1699
|
+
'--record-file',
|
|
1700
|
+
default=None,
|
|
1701
|
+
help='记录文件路径(可选,默认为 ~/.claude/zco-linked-projects.json)'
|
|
1702
|
+
)
|
|
1703
|
+
|
|
1704
|
+
##; 解析参数
|
|
1705
|
+
args = parser.parse_args()
|
|
1706
|
+
|
|
1707
|
+
##; 处理子命令
|
|
1708
|
+
if args.command == 'init':
|
|
1709
|
+
if args.project_path is None:
|
|
1710
|
+
cmd_init_global(tpl_dir=args.tpl)
|
|
1711
|
+
else:
|
|
1712
|
+
cmd_init_project(target_path=args.project_path, tpl_dir=args.tpl)
|
|
1713
|
+
return
|
|
1714
|
+
|
|
1715
|
+
elif args.command == 'list-linked-repos':
|
|
1716
|
+
cmd_list_linked_repos(record_file=args.record_file)
|
|
1717
|
+
return
|
|
1718
|
+
|
|
1719
|
+
elif args.command == 'fix-linked-repos':
|
|
1720
|
+
cmd_fix_linked_repos(record_file=args.record_file, remove_not_found=args.remove_not_found)
|
|
1721
|
+
return
|
|
1722
|
+
|
|
1723
|
+
elif args.command == 'fix':
|
|
1724
|
+
cmd_fix(project_path=args.project_path, tpl_dir=args.tpl, record_file=args.record_file)
|
|
1725
|
+
return
|
|
1726
|
+
else:
|
|
1727
|
+
## print help
|
|
1728
|
+
parser.print_help()
|
|
1729
|
+
|
|
1730
|
+
|
|
1731
|
+
if __name__ == "__main__":
|
|
1732
|
+
main()
|