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.
Files changed (34) hide show
  1. ClaudeSettings/DOT.claudeignore +7 -0
  2. ClaudeSettings/README.md +100 -0
  3. ClaudeSettings/commands/generate_changelog.sh +49 -0
  4. ClaudeSettings/commands/show_env +92 -0
  5. ClaudeSettings/commands/zco-clean +164 -0
  6. ClaudeSettings/commands/zco-git-summary +15 -0
  7. ClaudeSettings/commands/zco-git-tag +42 -0
  8. ClaudeSettings/hooks/CHANGELOG.md +157 -0
  9. ClaudeSettings/hooks/README.md +254 -0
  10. ClaudeSettings/hooks/save_chat_plain.py +148 -0
  11. ClaudeSettings/hooks/save_chat_spec.py +398 -0
  12. ClaudeSettings/rules/README.md +270 -0
  13. ClaudeSettings/rules/go/.golangci.yml.template +170 -0
  14. ClaudeSettings/rules/go/GoBuildAutoVersion.v250425.md +95 -0
  15. ClaudeSettings/rules/go/check-standards.sh +128 -0
  16. ClaudeSettings/rules/go/coding-standards.md +973 -0
  17. ClaudeSettings/rules/go/example.go +207 -0
  18. ClaudeSettings/rules/go/go-testing.md +691 -0
  19. ClaudeSettings/rules/go/list-comments.sh +85 -0
  20. ClaudeSettings/settings.sample.json +71 -0
  21. ClaudeSettings/skills/README.md +225 -0
  22. ClaudeSettings/skills/zco-docs-update/SKILL.md +381 -0
  23. ClaudeSettings/skills/zco-help/SKILL.md +601 -0
  24. ClaudeSettings/skills/zco-plan/SKILL.md +661 -0
  25. ClaudeSettings/skills/zco-plan-new/SKILL.md +585 -0
  26. ClaudeSettings/zco-scripts/co-docs-update.sh +150 -0
  27. ClaudeSettings/zco-scripts/test_update_plan_metadata.py +328 -0
  28. ClaudeSettings/zco-scripts/update-plan-metadata.py +324 -0
  29. zco_claude-0.0.8.dist-info/METADATA +190 -0
  30. zco_claude-0.0.8.dist-info/RECORD +34 -0
  31. zco_claude-0.0.8.dist-info/WHEEL +5 -0
  32. zco_claude-0.0.8.dist-info/entry_points.txt +3 -0
  33. zco_claude-0.0.8.dist-info/top_level.txt +1 -0
  34. 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()