@hunyed15/codecgc 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/.claude/hooks/route-edit.ps1 +86 -0
  2. package/INSTALLATION.md +550 -0
  3. package/LICENSE +21 -0
  4. package/README.md +171 -0
  5. package/bin/cgc-build.js +4 -0
  6. package/bin/cgc-doctor.js +4 -0
  7. package/bin/cgc-entry.js +4 -0
  8. package/bin/cgc-external-audit.js +4 -0
  9. package/bin/cgc-fix.js +4 -0
  10. package/bin/cgc-history.js +4 -0
  11. package/bin/cgc-install.js +4 -0
  12. package/bin/cgc-lifecycle.js +4 -0
  13. package/bin/cgc-package-audit.js +4 -0
  14. package/bin/cgc-plan.js +4 -0
  15. package/bin/cgc-release-readiness.js +4 -0
  16. package/bin/cgc-review.js +4 -0
  17. package/bin/cgc-route.js +4 -0
  18. package/bin/cgc-status.js +4 -0
  19. package/bin/cgc-test.js +4 -0
  20. package/bin/cgc.js +4 -0
  21. package/bin/codecgc.js +1284 -0
  22. package/codecgc/cgc/SKILL.md +46 -0
  23. package/codecgc/cgc-arch/SKILL.md +61 -0
  24. package/codecgc/cgc-build/SKILL.md +53 -0
  25. package/codecgc/cgc-decide/SKILL.md +55 -0
  26. package/codecgc/cgc-fix/SKILL.md +47 -0
  27. package/codecgc/cgc-learn/SKILL.md +46 -0
  28. package/codecgc/cgc-onboard/SKILL.md +52 -0
  29. package/codecgc/cgc-plan/SKILL.md +48 -0
  30. package/codecgc/cgc-refactor/SKILL.md +46 -0
  31. package/codecgc/cgc-req/SKILL.md +61 -0
  32. package/codecgc/cgc-review/SKILL.md +57 -0
  33. package/codecgc/cgc-roadmap/SKILL.md +55 -0
  34. package/codecgc/cgc-test/SKILL.md +21 -0
  35. package/codecgc/reference/api-cgc-review-libdoc.md +13 -0
  36. package/codecgc/reference/artifact-class-policy.md +81 -0
  37. package/codecgc/reference/build-flow.md +95 -0
  38. package/codecgc/reference/checklist-contract.md +103 -0
  39. package/codecgc/reference/execution-audit.md +121 -0
  40. package/codecgc/reference/execution-model.md +118 -0
  41. package/codecgc/reference/execution-routing.md +130 -0
  42. package/codecgc/reference/executor-contract.md +87 -0
  43. package/codecgc/reference/external-capability-registry.json +104 -0
  44. package/codecgc/reference/fix-flow.md +94 -0
  45. package/codecgc/reference/fixture-governance.md +60 -0
  46. package/codecgc/reference/flow-execution.md +65 -0
  47. package/codecgc/reference/lifecycle-map.md +172 -0
  48. package/codecgc/reference/lifecycle-playbook.md +104 -0
  49. package/codecgc/reference/long-lived-artifacts.md +98 -0
  50. package/codecgc/reference/operation-guide.md +242 -0
  51. package/codecgc/reference/release-maintenance-playbook.md +150 -0
  52. package/codecgc/reference/review-writeback.md +141 -0
  53. package/codecgc/reference/role-model.md +128 -0
  54. package/codecgc/reference/runtime-boundary.md +72 -0
  55. package/codecgc/reference/shared-conventions.md +93 -0
  56. package/codecgc/reference/workflow-scaffold.md +57 -0
  57. package/codexmcp/LICENSE +21 -0
  58. package/codexmcp/README.md +294 -0
  59. package/codexmcp/pyproject.toml +37 -0
  60. package/codexmcp/src/codexmcp/__init__.py +4 -0
  61. package/codexmcp/src/codexmcp/cli.py +12 -0
  62. package/codexmcp/src/codexmcp/server.py +529 -0
  63. package/geminimcp/README.md +258 -0
  64. package/geminimcp/pyproject.toml +15 -0
  65. package/geminimcp/src/geminimcp/__init__.py +4 -0
  66. package/geminimcp/src/geminimcp/cli.py +12 -0
  67. package/geminimcp/src/geminimcp/server.py +465 -0
  68. package/model-routing.yaml +30 -0
  69. package/package.json +90 -0
  70. package/requirements.txt +1 -0
  71. package/scripts/README-codecgc-cli.md +89 -0
  72. package/scripts/audit_codecgc_external_capabilities.py +276 -0
  73. package/scripts/audit_codecgc_historical_audits.py +242 -0
  74. package/scripts/audit_codecgc_lifecycle.py +241 -0
  75. package/scripts/audit_codecgc_package_runtime.py +445 -0
  76. package/scripts/audit_codecgc_release_readiness.py +202 -0
  77. package/scripts/audit_codecgc_review_policy.py +82 -0
  78. package/scripts/audit_codecgc_workflow_history.py +317 -0
  79. package/scripts/build_codecgc_task.py +487 -0
  80. package/scripts/codecgc_artifact_roots.py +40 -0
  81. package/scripts/codecgc_cli.py +843 -0
  82. package/scripts/codecgc_command_surface.py +28 -0
  83. package/scripts/codecgc_console_io.py +45 -0
  84. package/scripts/codecgc_executor_registry.py +54 -0
  85. package/scripts/codecgc_file_evidence.py +349 -0
  86. package/scripts/codecgc_flow_control.py +233 -0
  87. package/scripts/codecgc_governance_dedupe.py +161 -0
  88. package/scripts/codecgc_plan_decision.py +103 -0
  89. package/scripts/codecgc_review_control.py +588 -0
  90. package/scripts/codecgc_roadmap_templates.py +149 -0
  91. package/scripts/codecgc_routing_paths.py +16 -0
  92. package/scripts/codecgc_routing_template.py +135 -0
  93. package/scripts/codecgc_runtime_paths.py +22 -0
  94. package/scripts/codecgc_session_recovery.py +44 -0
  95. package/scripts/codecgc_step_control.py +154 -0
  96. package/scripts/codecgc_workflow_runtime.py +63 -0
  97. package/scripts/codecgc_workflow_templates.py +437 -0
  98. package/scripts/entry_codecgc_workflow.py +3419 -0
  99. package/scripts/exercise_mcp_tools.py +109 -0
  100. package/scripts/expand_codecgc_roadmap.py +664 -0
  101. package/scripts/init_codecgc_roadmap.py +134 -0
  102. package/scripts/init_codecgc_workflow.py +207 -0
  103. package/scripts/install_codecgc.py +938 -0
  104. package/scripts/migrate_demo_workflows_to_fixtures.py +128 -0
  105. package/scripts/normalize_codecgc_audits.py +114 -0
  106. package/scripts/normalize_codecgc_governance_docs.py +79 -0
  107. package/scripts/normalize_codecgc_workflow_docs.py +269 -0
  108. package/scripts/plan_codecgc_workflow.py +970 -0
  109. package/scripts/refresh_codecgc_review_policy.py +223 -0
  110. package/scripts/review_codecgc_workflow.py +88 -0
  111. package/scripts/route_codecgc_workflow.py +671 -0
  112. package/scripts/run_codecgc_build.py +104 -0
  113. package/scripts/run_codecgc_fix.py +104 -0
  114. package/scripts/run_codecgc_flow_step.py +165 -0
  115. package/scripts/run_codecgc_task.py +410 -0
  116. package/scripts/run_codecgc_test.py +105 -0
  117. package/scripts/sync_codecgc_mcp_config.py +41 -0
  118. package/scripts/write_codecgc_architecture.py +78 -0
  119. package/scripts/write_codecgc_decision.py +83 -0
  120. package/scripts/write_codecgc_explore.py +118 -0
  121. package/scripts/write_codecgc_guide.py +141 -0
  122. package/scripts/write_codecgc_learning.py +87 -0
  123. package/scripts/write_codecgc_libdoc.py +140 -0
  124. package/scripts/write_codecgc_refactor.py +78 -0
  125. package/scripts/write_codecgc_requirement.py +78 -0
  126. package/scripts/write_codecgc_review.py +291 -0
  127. package/scripts/write_codecgc_roadmap.py +122 -0
  128. package/scripts/write_codecgc_trick.py +123 -0
@@ -0,0 +1,938 @@
1
+ import argparse
2
+ import json
3
+ import os
4
+ import shutil
5
+ import subprocess
6
+ import sys
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from codecgc_console_io import render_summary_block
11
+ from codecgc_executor_registry import build_executor_registry
12
+ from codecgc_executor_registry import resolve_python_command
13
+ from codecgc_routing_template import sync_workspace_routing_file
14
+ from codecgc_runtime_paths import PACKAGE_ROOT
15
+ from codecgc_runtime_paths import resolve_workspace_root
16
+ from sync_codecgc_mcp_config import build_mcp_config
17
+ from sync_codecgc_mcp_config import write_mcp_config
18
+
19
+
20
+ WORKSPACE = PACKAGE_ROOT
21
+ CLAUDE_DIR = WORKSPACE / ".claude"
22
+ HOOKS_DIR = CLAUDE_DIR / "hooks"
23
+ SETTINGS_PATH = CLAUDE_DIR / "settings.json"
24
+ MCP_CONFIG_PATH = WORKSPACE / ".mcp.json"
25
+ PROJECT_HOOK_PATH = HOOKS_DIR / "route-edit.ps1"
26
+ PROJECT_ROUTING_PATH = WORKSPACE / "model-routing.yaml"
27
+
28
+
29
+ DEFAULT_HOOKS = {
30
+ "PreToolUse": [
31
+ {
32
+ "matcher": "Edit|Write",
33
+ "hooks": [
34
+ {
35
+ "type": "command",
36
+ "command": "powershell -ExecutionPolicy Bypass -File .claude/hooks/route-edit.ps1",
37
+ }
38
+ ],
39
+ }
40
+ ]
41
+ }
42
+
43
+ SENSITIVE_KEYWORDS = ("token", "secret", "key", "password", "auth")
44
+ MCP_RUNTIME_REQUIREMENT = 'mcp[cli]>=1.21.2'
45
+
46
+
47
+ def get_user_claude_root(override_root: str = "") -> Path:
48
+ override = override_root.strip() or os.environ.get("CODECGC_USER_CLAUDE_DIR", "").strip()
49
+ if override:
50
+ return Path(override).expanduser().resolve()
51
+
52
+ if os.name == "nt":
53
+ base = Path(os.environ.get("USERPROFILE", str(Path.home())))
54
+ else:
55
+ base = Path.home()
56
+ return (base / ".claude").resolve()
57
+
58
+
59
+ def get_user_claude_paths(override_root: str = "") -> dict[str, Path]:
60
+ root = get_user_claude_root(override_root)
61
+ return {
62
+ "root": root,
63
+ "settings": root / "settings.json",
64
+ "mcp": root / "mcp.json",
65
+ "hooks_dir": root / "hooks",
66
+ "hook_script": root / "hooks" / "route-edit.ps1",
67
+ }
68
+
69
+
70
+ def get_workspace_paths(override_workspace: str = "") -> dict[str, Path]:
71
+ root = resolve_workspace_root(override_workspace)
72
+ claude_dir = root / ".claude"
73
+ hooks_dir = claude_dir / "hooks"
74
+ return {
75
+ "root": root,
76
+ "claude_dir": claude_dir,
77
+ "hooks_dir": hooks_dir,
78
+ "settings": claude_dir / "settings.json",
79
+ "mcp": root / ".mcp.json",
80
+ "hook_script": hooks_dir / "route-edit.ps1",
81
+ "routing_file": root / "model-routing.yaml",
82
+ }
83
+
84
+
85
+ def load_json_file(path: Path) -> dict[str, Any]:
86
+ if not path.exists():
87
+ return {}
88
+ return json.loads(path.read_text(encoding="utf-8"))
89
+
90
+
91
+ def load_text_file(path: Path) -> str:
92
+ if not path.exists():
93
+ return ""
94
+ return path.read_text(encoding="utf-8")
95
+
96
+
97
+ def build_hook_payload(command_text: str) -> dict[str, Any]:
98
+ return {
99
+ "PreToolUse": [
100
+ {
101
+ "matcher": "Edit|Write",
102
+ "hooks": [
103
+ {
104
+ "type": "command",
105
+ "command": command_text,
106
+ }
107
+ ],
108
+ }
109
+ ]
110
+ }
111
+
112
+
113
+ def merge_hook_settings(current: dict[str, Any], command_text: str) -> tuple[dict[str, Any], bool]:
114
+ hooks = current.get("hooks")
115
+ expected_hooks = build_hook_payload(command_text)
116
+ if not isinstance(hooks, dict):
117
+ current["hooks"] = expected_hooks
118
+ return current, True
119
+
120
+ pre_tool_use = hooks.get("PreToolUse")
121
+ if not isinstance(pre_tool_use, list):
122
+ hooks["PreToolUse"] = expected_hooks["PreToolUse"]
123
+ return current, True
124
+
125
+ expected = expected_hooks["PreToolUse"][0]
126
+ for item in pre_tool_use:
127
+ if not isinstance(item, dict):
128
+ continue
129
+ if item.get("matcher") != expected["matcher"]:
130
+ continue
131
+ hook_list = item.get("hooks")
132
+ if not isinstance(hook_list, list):
133
+ item["hooks"] = expected["hooks"]
134
+ return current, True
135
+ for hook in hook_list:
136
+ if not isinstance(hook, dict):
137
+ continue
138
+ if hook.get("type") == "command" and hook.get("command") == expected["hooks"][0]["command"]:
139
+ return current, False
140
+ hook_list.append(expected["hooks"][0])
141
+ return current, True
142
+
143
+ pre_tool_use.append(expected)
144
+ return current, True
145
+
146
+
147
+ def write_json_file(path: Path, payload: dict[str, Any]) -> Path:
148
+ path.parent.mkdir(parents=True, exist_ok=True)
149
+ path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
150
+ return path
151
+
152
+
153
+ def shell_quote(value: str) -> str:
154
+ text = str(value)
155
+ if not text:
156
+ return '""'
157
+ if any(char.isspace() for char in text) or any(char in text for char in '"&()[]{}^=;!+,`~'):
158
+ escaped = text.replace('"', '\\"')
159
+ return f'"{escaped}"'
160
+ return text
161
+
162
+
163
+ def is_sensitive_key(key: str) -> bool:
164
+ lowered = key.lower()
165
+ return any(keyword in lowered for keyword in SENSITIVE_KEYWORDS)
166
+
167
+
168
+ def sanitize_for_preview(value: Any, key_hint: str = "") -> Any:
169
+ if isinstance(value, dict):
170
+ return {
171
+ str(key): ("***REDACTED***" if is_sensitive_key(str(key)) else sanitize_for_preview(item, str(key)))
172
+ for key, item in value.items()
173
+ }
174
+ if isinstance(value, list):
175
+ return [sanitize_for_preview(item, key_hint) for item in value]
176
+ if is_sensitive_key(key_hint):
177
+ return "***REDACTED***"
178
+ return value
179
+
180
+
181
+ def settings_have_hook_command(settings: dict[str, Any], command_text: str) -> bool:
182
+ hooks = settings.get("hooks")
183
+ if not isinstance(hooks, dict):
184
+ return False
185
+ pre_tool_use = hooks.get("PreToolUse")
186
+ if not isinstance(pre_tool_use, list):
187
+ return False
188
+ for item in pre_tool_use:
189
+ if not isinstance(item, dict):
190
+ continue
191
+ if item.get("matcher") != "Edit|Write":
192
+ continue
193
+ hook_list = item.get("hooks")
194
+ if not isinstance(hook_list, list):
195
+ continue
196
+ for hook in hook_list:
197
+ if not isinstance(hook, dict):
198
+ continue
199
+ if hook.get("type") == "command" and hook.get("command") == command_text:
200
+ return True
201
+ return False
202
+
203
+
204
+ def build_workspace_hook_command(workspace_paths: dict[str, Path]) -> str:
205
+ return "powershell -ExecutionPolicy Bypass -File .claude/hooks/route-edit.ps1"
206
+
207
+
208
+ def build_mode_summary_payload(
209
+ *,
210
+ scope: str,
211
+ human_summary: str,
212
+ recommended_next_action: str = "",
213
+ extra: dict[str, Any] | None = None,
214
+ ) -> dict[str, Any]:
215
+ summary = {
216
+ "scope": scope,
217
+ "human_summary": human_summary,
218
+ "recommended_next_action": recommended_next_action,
219
+ }
220
+ if extra:
221
+ summary.update(extra)
222
+ return summary
223
+
224
+
225
+ def install_local_runtime(override_workspace: str = "") -> dict[str, Any]:
226
+ workspace_paths = get_workspace_paths(override_workspace)
227
+ mcp_path = write_mcp_config(workspace_paths["mcp"])
228
+ routing_path = sync_workspace_routing_file(workspace_paths["routing_file"])
229
+
230
+ workspace_paths["claude_dir"].mkdir(parents=True, exist_ok=True)
231
+ workspace_paths["hooks_dir"].mkdir(parents=True, exist_ok=True)
232
+
233
+ settings = load_json_file(workspace_paths["settings"])
234
+ merged_settings, settings_changed = merge_hook_settings(
235
+ settings,
236
+ build_workspace_hook_command(workspace_paths),
237
+ )
238
+ if settings_changed or not workspace_paths["settings"].exists():
239
+ write_json_file(workspace_paths["settings"], merged_settings)
240
+
241
+ if PROJECT_HOOK_PATH.resolve() != workspace_paths["hook_script"].resolve():
242
+ shutil.copyfile(PROJECT_HOOK_PATH, workspace_paths["hook_script"])
243
+
244
+ summary = build_mode_summary_payload(
245
+ scope="项目级 Claude 与 MCP 集成面",
246
+ human_summary="项目级 CodeCGC 集成文件已同步。",
247
+ recommended_next_action="cgc-status",
248
+ )
249
+
250
+ return {
251
+ "success": True,
252
+ "mode": "local",
253
+ "workspace": str(workspace_paths["root"]),
254
+ "mcp_config": str(mcp_path),
255
+ "routing_file": str(routing_path),
256
+ "claude_settings": str(workspace_paths["settings"]),
257
+ "hook_script": str(workspace_paths["hook_script"]),
258
+ "notes": [
259
+ "Repository-local MCP config was synced from the executor registry.",
260
+ "Project-local model-routing.yaml was synchronized and preserves custom path blocks.",
261
+ "Claude pre-edit guardrail hook was synchronized into the target workspace.",
262
+ "This mode prepares project-level integration surfaces for the selected workspace.",
263
+ ],
264
+ "summary": summary,
265
+ }
266
+
267
+
268
+ def build_user_hook_command(user_paths: dict[str, Path]) -> str:
269
+ return f"powershell -ExecutionPolicy Bypass -File {user_paths['hook_script']}"
270
+
271
+
272
+ def preview_user_install(override_root: str = "") -> dict[str, Any]:
273
+ user_paths = get_user_claude_paths(override_root)
274
+ user_settings = load_json_file(user_paths["settings"])
275
+ merged_settings, settings_changed = merge_hook_settings(user_settings, build_user_hook_command(user_paths))
276
+ mcp_config = build_mcp_config()
277
+ recommended_next_action = f"cgc-install --mode user --user-root {shell_quote(str(user_paths['root']))}"
278
+ summary = build_mode_summary_payload(
279
+ scope="用户级 Claude 集成预演",
280
+ human_summary="已完成用户级 Claude 集成预演,未写入任何文件。",
281
+ recommended_next_action=recommended_next_action,
282
+ )
283
+
284
+ return {
285
+ "success": True,
286
+ "mode": "user-dry-run",
287
+ "workspace": str(WORKSPACE),
288
+ "user_claude_root": str(user_paths["root"]),
289
+ "planned_files": {
290
+ "settings_json": str(user_paths["settings"]),
291
+ "mcp_json": str(user_paths["mcp"]),
292
+ "hook_script": str(user_paths["hook_script"]),
293
+ },
294
+ "would_write": {
295
+ "settings_changed": settings_changed or not user_paths["settings"].exists(),
296
+ "mcp_changed": True,
297
+ "hook_changed": True,
298
+ },
299
+ "preview": {
300
+ "settings": sanitize_for_preview(merged_settings),
301
+ "mcp": mcp_config,
302
+ },
303
+ "notes": [
304
+ "This mode does not modify user-level Claude files.",
305
+ "Use this preview to inspect the future user-level integration surface.",
306
+ "Current CodeCGC product policy still defaults to project-local installation.",
307
+ ],
308
+ "summary": summary,
309
+ }
310
+
311
+
312
+ def install_user_runtime(override_root: str = "") -> dict[str, Any]:
313
+ user_paths = get_user_claude_paths(override_root)
314
+ user_paths["root"].mkdir(parents=True, exist_ok=True)
315
+ user_paths["hooks_dir"].mkdir(parents=True, exist_ok=True)
316
+
317
+ settings = load_json_file(user_paths["settings"])
318
+ merged_settings, settings_changed = merge_hook_settings(settings, build_user_hook_command(user_paths))
319
+ write_json_file(user_paths["settings"], merged_settings)
320
+ write_json_file(user_paths["mcp"], build_mcp_config())
321
+ shutil.copyfile(PROJECT_HOOK_PATH, user_paths["hook_script"])
322
+ summary = build_mode_summary_payload(
323
+ scope="用户级 Claude 集成面",
324
+ human_summary="用户级 Claude 集成文件已写入。",
325
+ recommended_next_action="cgc-install --mode status",
326
+ )
327
+
328
+ return {
329
+ "success": True,
330
+ "mode": "user",
331
+ "workspace": str(WORKSPACE),
332
+ "user_claude_root": str(user_paths["root"]),
333
+ "written_files": {
334
+ "settings_json": str(user_paths["settings"]),
335
+ "mcp_json": str(user_paths["mcp"]),
336
+ "hook_script": str(user_paths["hook_script"]),
337
+ },
338
+ "changes": {
339
+ "settings_changed": settings_changed or not user_paths["settings"].exists(),
340
+ "mcp_changed": True,
341
+ "hook_changed": True,
342
+ },
343
+ "notes": [
344
+ "User-level Claude integration files were written to the selected root.",
345
+ "The user-level hook script was copied from the project hook source.",
346
+ "This mode is explicit and should be used only when a broader Claude integration surface is intended.",
347
+ ],
348
+ "summary": summary,
349
+ }
350
+
351
+
352
+ def build_workspace_install_command(workspace_root: Path) -> str:
353
+ return f"cgc-install --mode local --workspace {shell_quote(str(workspace_root))}"
354
+
355
+
356
+ def build_user_preview_command(user_root: Path) -> str:
357
+ return f"cgc-install --mode user-dry-run --user-root {shell_quote(str(user_root))}"
358
+
359
+
360
+ def build_doctor_fix_command(workspace_root: Path) -> str:
361
+ return f"cgc-install --workspace {shell_quote(str(workspace_root))}"
362
+
363
+
364
+ def build_install_mode_summary(result: dict[str, Any]) -> str:
365
+ mode = str(result.get("mode", "")).strip()
366
+
367
+ if mode == "local":
368
+ lines = [
369
+ f"- 工作区: {result.get('workspace', '')}",
370
+ "- 范围: 项目级 Claude 与 MCP 集成面",
371
+ "- 摘要: 项目级 CodeCGC 集成文件已同步。",
372
+ f"- MCP 配置: {result.get('mcp_config', '')}",
373
+ f"- Routing 文件: {result.get('routing_file', '')}",
374
+ f"- Claude 设置: {result.get('claude_settings', '')}",
375
+ f"- Hook 脚本: {result.get('hook_script', '')}",
376
+ "- 说明: 可选外部能力如 MemOS 不由 cgc-install 自动写入;如需启用,请在 Claude 中单独配置官方 MCP。",
377
+ ]
378
+ next_actions = [
379
+ "cgc-status",
380
+ "cgc-doctor",
381
+ ]
382
+ return render_summary_block("CodeCGC 安装", lines, next_actions)
383
+
384
+ if mode == "user-dry-run":
385
+ planned = result.get("planned_files", {}) if isinstance(result.get("planned_files"), dict) else {}
386
+ lines = [
387
+ f"- 工作区: {result.get('workspace', '')}",
388
+ f"- 用户 Claude 根目录: {result.get('user_claude_root', '')}",
389
+ "- 范围: 用户级 Claude 集成预演",
390
+ "- 摘要: 已完成用户级 Claude 集成预演,未写入任何文件。",
391
+ f"- 预演 Settings: {planned.get('settings_json', '')}",
392
+ f"- 预演 MCP: {planned.get('mcp_json', '')}",
393
+ f"- 预演 Hook: {planned.get('hook_script', '')}",
394
+ "- 说明: 该预演只覆盖 CodeCGC 必需执行器;MemOS 等可选外部能力仍建议在 Claude 中独立配置。",
395
+ ]
396
+ next_actions = []
397
+ user_root = str(result.get("user_claude_root", "")).strip()
398
+ if user_root:
399
+ next_actions.append(f"cgc-install --mode user --user-root {shell_quote(user_root)}")
400
+ next_actions.append("cgc-install --mode status")
401
+ return render_summary_block("CodeCGC 用户级预演", lines, next_actions)
402
+
403
+ if mode == "user":
404
+ written = result.get("written_files", {}) if isinstance(result.get("written_files"), dict) else {}
405
+ lines = [
406
+ f"- 工作区: {result.get('workspace', '')}",
407
+ f"- 用户 Claude 根目录: {result.get('user_claude_root', '')}",
408
+ "- 范围: 用户级 Claude 集成面",
409
+ "- 摘要: 用户级 Claude 集成文件已写入。",
410
+ f"- Settings: {written.get('settings_json', '')}",
411
+ f"- MCP: {written.get('mcp_json', '')}",
412
+ f"- Hook 脚本: {written.get('hook_script', '')}",
413
+ "- 说明: 该安装只写入 CodeCGC 必需执行器;MemOS 等可选外部能力仍需在 Claude 中单独配置。",
414
+ ]
415
+ next_actions = [
416
+ "cgc-install --mode status",
417
+ "cgc-doctor",
418
+ ]
419
+ return render_summary_block("CodeCGC 用户级安装", lines, next_actions)
420
+
421
+ return ""
422
+
423
+
424
+ def collect_project_status(workspace_paths: dict[str, Path]) -> dict[str, Any]:
425
+ expected_mcp = build_mcp_config()
426
+ expected_hook_command = build_workspace_hook_command(workspace_paths)
427
+ expected_hook_text = load_text_file(PROJECT_HOOK_PATH)
428
+ current_settings = load_json_file(workspace_paths["settings"])
429
+ current_mcp = load_json_file(workspace_paths["mcp"])
430
+ current_hook_text = load_text_file(workspace_paths["hook_script"])
431
+ routing_exists = workspace_paths["routing_file"].exists()
432
+
433
+ hook_registered = settings_have_hook_command(current_settings, expected_hook_command)
434
+ mcp_matches = current_mcp == expected_mcp if workspace_paths["mcp"].exists() else False
435
+ hook_file_matches = current_hook_text == expected_hook_text if workspace_paths["hook_script"].exists() else False
436
+
437
+ missing = []
438
+ if not routing_exists:
439
+ missing.append("routing_file")
440
+ if not mcp_matches:
441
+ missing.append("mcp_json")
442
+ if not hook_registered:
443
+ missing.append("claude_settings_hook")
444
+ if not hook_file_matches:
445
+ missing.append("hook_script")
446
+
447
+ ready = not missing
448
+ return {
449
+ "mcp_json_path": str(workspace_paths["mcp"]),
450
+ "routing_file_path": str(workspace_paths["routing_file"]),
451
+ "claude_settings_path": str(workspace_paths["settings"]),
452
+ "hook_script_path": str(workspace_paths["hook_script"]),
453
+ "mcp_json_exists": workspace_paths["mcp"].exists(),
454
+ "routing_file_exists": routing_exists,
455
+ "claude_settings_exists": workspace_paths["settings"].exists(),
456
+ "hook_exists": workspace_paths["hook_script"].exists(),
457
+ "mcp_matches_expected": mcp_matches,
458
+ "hook_registered": hook_registered,
459
+ "hook_file_matches_expected": hook_file_matches,
460
+ "ready": ready,
461
+ "missing_or_outdated": missing,
462
+ "recommended_command": "" if ready else build_workspace_install_command(workspace_paths["root"]),
463
+ "hook_expected": {"hooks": build_hook_payload(expected_hook_command)},
464
+ }
465
+
466
+
467
+ def collect_user_status(user_paths: dict[str, Path]) -> dict[str, Any]:
468
+ expected_mcp = build_mcp_config()
469
+ expected_hook_command = build_user_hook_command(user_paths)
470
+ expected_hook_text = load_text_file(PROJECT_HOOK_PATH)
471
+ current_settings = load_json_file(user_paths["settings"])
472
+ current_mcp = load_json_file(user_paths["mcp"])
473
+ current_hook_text = load_text_file(user_paths["hook_script"])
474
+
475
+ hook_registered = settings_have_hook_command(current_settings, expected_hook_command)
476
+ mcp_matches = current_mcp == expected_mcp if user_paths["mcp"].exists() else False
477
+ hook_file_matches = current_hook_text == expected_hook_text if user_paths["hook_script"].exists() else False
478
+
479
+ missing = []
480
+ if not mcp_matches:
481
+ missing.append("mcp_json")
482
+ if not hook_registered:
483
+ missing.append("claude_settings_hook")
484
+ if not hook_file_matches:
485
+ missing.append("hook_script")
486
+
487
+ ready = not missing
488
+ return {
489
+ "root": str(user_paths["root"]),
490
+ "settings_json": str(user_paths["settings"]),
491
+ "mcp_json": str(user_paths["mcp"]),
492
+ "hook_script": str(user_paths["hook_script"]),
493
+ "settings_exists": user_paths["settings"].exists(),
494
+ "mcp_exists": user_paths["mcp"].exists(),
495
+ "hook_exists": user_paths["hook_script"].exists(),
496
+ "mcp_matches_expected": mcp_matches,
497
+ "hook_registered": hook_registered,
498
+ "hook_file_matches_expected": hook_file_matches,
499
+ "ready": ready,
500
+ "missing_or_outdated": missing,
501
+ "recommended_command": "" if ready else build_user_preview_command(user_paths["root"]),
502
+ }
503
+
504
+
505
+ def collect_install_status(override_workspace: str = "") -> dict[str, Any]:
506
+ workspace_paths = get_workspace_paths(override_workspace)
507
+ user_paths = get_user_claude_paths()
508
+ project_status = collect_project_status(workspace_paths)
509
+ user_status = collect_user_status(user_paths)
510
+ recommended_next_command = project_status["recommended_command"] or user_status["recommended_command"]
511
+ human_summary = "项目级 CodeCGC 集成已就绪。"
512
+ if not project_status["ready"]:
513
+ human_summary = "项目级 CodeCGC 集成尚未就绪。"
514
+ elif not user_status["ready"]:
515
+ human_summary = "项目级集成已就绪;用户级 Claude 集成仍是可选项,当前尚未就绪。"
516
+ status_summary = {
517
+ "project_ready": project_status["ready"],
518
+ "user_ready": user_status["ready"],
519
+ "default_policy": "project-local-first",
520
+ "recommended_next_command": recommended_next_command,
521
+ "recommended_project_command": project_status["recommended_command"],
522
+ "recommended_user_command": user_status["recommended_command"],
523
+ "human_summary": human_summary,
524
+ "scope": "项目级集成就绪状态,以及用户级 Claude 集成预演状态",
525
+ }
526
+
527
+ return {
528
+ "success": True,
529
+ "mode": "status",
530
+ "workspace": str(workspace_paths["root"]),
531
+ "summary": status_summary,
532
+ "status_summary": status_summary,
533
+ "project": project_status,
534
+ "user_preview_targets": user_status,
535
+ }
536
+
537
+
538
+ def find_python_command() -> str:
539
+ candidates = ["python", "py"] if os.name == "nt" else ["python3", "python"]
540
+ for candidate in candidates:
541
+ if shutil.which(candidate):
542
+ return candidate
543
+ return ""
544
+
545
+
546
+ def probe_python_import(runtime_command: str, module_name: str, runtime_env: dict[str, str]) -> dict[str, Any]:
547
+ try:
548
+ result = subprocess.run(
549
+ [runtime_command, "-c", f"import {module_name}; print('ok')"],
550
+ cwd=WORKSPACE,
551
+ capture_output=True,
552
+ text=True,
553
+ encoding="utf-8",
554
+ errors="replace",
555
+ env=runtime_env,
556
+ )
557
+ except Exception as error:
558
+ return {
559
+ "ok": False,
560
+ "detail": str(error),
561
+ "error_type": type(error).__name__,
562
+ "stdout": "",
563
+ "stderr": "",
564
+ "returncode": None,
565
+ }
566
+
567
+ stdout_text = (result.stdout or "").strip()
568
+ stderr_text = (result.stderr or "").strip()
569
+ return {
570
+ "ok": result.returncode == 0 and stdout_text == "ok",
571
+ "detail": stdout_text if result.returncode == 0 and stdout_text == "ok" else stderr_text or stdout_text or f"returncode={result.returncode}",
572
+ "error_type": "",
573
+ "stdout": stdout_text,
574
+ "stderr": stderr_text,
575
+ "returncode": result.returncode,
576
+ }
577
+
578
+
579
+ def build_pip_install_command(python_command: str, requirement: str) -> str:
580
+ runtime_command = python_command.strip() or sys.executable
581
+ return f"{shell_quote(runtime_command)} -m pip install {shell_quote(requirement)}"
582
+
583
+
584
+ def build_local_editable_install_command(python_command: str) -> str:
585
+ runtime_command = python_command.strip() or sys.executable
586
+ codexmcp_path = WORKSPACE / "codexmcp"
587
+ geminimcp_path = WORKSPACE / "geminimcp"
588
+ if not (codexmcp_path / "pyproject.toml").exists():
589
+ return ""
590
+ if not (geminimcp_path / "pyproject.toml").exists():
591
+ return ""
592
+ return (
593
+ f"{shell_quote(runtime_command)} -m pip install -e {shell_quote(str(codexmcp_path))} "
594
+ f"-e {shell_quote(str(geminimcp_path))}"
595
+ )
596
+
597
+
598
+ def classify_doctor_failures(
599
+ checks: list[dict[str, Any]],
600
+ configured_python_command: str,
601
+ workspace_root: Path,
602
+ ) -> tuple[list[dict[str, str]], list[str]]:
603
+ failure_categories: list[dict[str, str]] = []
604
+ recommended_commands: list[str] = []
605
+ seen_codes: set[str] = set()
606
+ seen_commands: set[str] = set()
607
+
608
+ def add_failure(code: str, summary: str, suggestion: str, command: str = "") -> None:
609
+ if code in seen_codes:
610
+ return
611
+ seen_codes.add(code)
612
+ failure_categories.append(
613
+ {
614
+ "code": code,
615
+ "summary": summary,
616
+ "suggestion": suggestion,
617
+ }
618
+ )
619
+ if command and command not in seen_commands:
620
+ seen_commands.add(command)
621
+ recommended_commands.append(command)
622
+
623
+ runtime_command = configured_python_command.strip() or sys.executable
624
+ install_command = build_doctor_fix_command(workspace_root)
625
+ editable_install_command = build_local_editable_install_command(runtime_command)
626
+ failed_names = {
627
+ str(item.get("name", "")).strip()
628
+ for item in checks
629
+ if isinstance(item, dict) and not item.get("ok")
630
+ }
631
+ configured_python_missing = "configured_python_command_exists" in failed_names
632
+
633
+ for item in checks:
634
+ if not isinstance(item, dict) or item.get("ok"):
635
+ continue
636
+ name = str(item.get("name", "")).strip()
637
+ detail = str(item.get("detail", "")).strip()
638
+
639
+ if name == "python_available":
640
+ add_failure(
641
+ "python-unavailable",
642
+ "系统 PATH 中没有找到可用 Python 命令。",
643
+ "先安装 Python 3.12+,并确保 `python` 或 `py` 可在命令行直接调用。",
644
+ )
645
+ continue
646
+
647
+ if name == "configured_python_command_exists":
648
+ add_failure(
649
+ "configured-python-missing",
650
+ "配置的 Python 解释器不存在或不可执行。",
651
+ "检查 `CODECGC_PYTHON_COMMAND` 是否指向正确解释器,或者移除该变量后重试。",
652
+ )
653
+ continue
654
+
655
+ if name == "python_runtime_import_probe_mcp":
656
+ if configured_python_missing:
657
+ continue
658
+ add_failure(
659
+ "mcp-runtime-missing",
660
+ "当前 Python 环境缺少 `mcp` 运行时依赖。",
661
+ "在当前解释器下安装 MCP CLI 依赖后重新执行 doctor。",
662
+ build_pip_install_command(runtime_command, MCP_RUNTIME_REQUIREMENT),
663
+ )
664
+ continue
665
+
666
+ if name == "python_runtime_import_probe_codexmcp":
667
+ if configured_python_missing:
668
+ continue
669
+ if "No module named 'codexmcp'" in detail or 'No module named "codexmcp"' in detail:
670
+ add_failure(
671
+ "codexmcp-package-missing",
672
+ "当前解释器无法导入本地 `codexmcp` 包。",
673
+ "确认当前安装包已包含 `codexmcp/src`;仓库开发环境可执行本地 editable install,安装产物则应重新安装 CodeCGC 包。",
674
+ editable_install_command,
675
+ )
676
+ else:
677
+ add_failure(
678
+ "codexmcp-runtime-broken",
679
+ "`codexmcp` 启动入口存在,但当前运行时仍无法导入。",
680
+ "仓库开发环境可先重装本地执行器包;若你使用的是已安装产物,则优先重新安装 CodeCGC,再检查执行器源码是否缺失或损坏。",
681
+ editable_install_command,
682
+ )
683
+ continue
684
+
685
+ if name == "python_runtime_import_probe_geminimcp":
686
+ if configured_python_missing:
687
+ continue
688
+ if "No module named 'geminimcp'" in detail or 'No module named "geminimcp"' in detail:
689
+ add_failure(
690
+ "geminimcp-package-missing",
691
+ "当前解释器无法导入本地 `geminimcp` 包。",
692
+ "确认当前安装包已包含 `geminimcp/src`;仓库开发环境可执行本地 editable install,安装产物则应重新安装 CodeCGC 包。",
693
+ editable_install_command,
694
+ )
695
+ else:
696
+ add_failure(
697
+ "geminimcp-runtime-broken",
698
+ "`geminimcp` 启动入口存在,但当前运行时仍无法导入。",
699
+ "仓库开发环境可先重装本地执行器包;若你使用的是已安装产物,则优先重新安装 CodeCGC,再检查执行器源码是否缺失或损坏。",
700
+ editable_install_command,
701
+ )
702
+ continue
703
+
704
+ if name == "project_integration_ready":
705
+ add_failure(
706
+ "project-integration-missing",
707
+ "项目级 Claude 集成面未就绪。",
708
+ "重新执行项目级安装以同步 `.mcp.json`、hook 与 Claude settings。",
709
+ install_command,
710
+ )
711
+ continue
712
+
713
+ if name in {"routing_file_exists", "project_hook_source_exists"}:
714
+ add_failure(
715
+ "packaged-runtime-missing-files",
716
+ "运行时所需的路由文件或 hook 源文件缺失。",
717
+ "检查当前安装包是否完整,必要时重新安装或重新打包后再试。",
718
+ )
719
+ continue
720
+
721
+ return failure_categories, recommended_commands
722
+
723
+
724
+ def collect_doctor_status(override_workspace: str = "") -> dict[str, Any]:
725
+ workspace_paths = get_workspace_paths(override_workspace)
726
+ project_status = collect_project_status(workspace_paths)
727
+ registry = build_executor_registry()
728
+ python_command = find_python_command()
729
+ configured_python_command = resolve_python_command()
730
+
731
+ checks: list[dict[str, Any]] = [
732
+ {
733
+ "name": "workspace_root_exists",
734
+ "ok": workspace_paths["root"].exists(),
735
+ "detail": str(workspace_paths["root"]),
736
+ },
737
+ {
738
+ "name": "python_available",
739
+ "ok": bool(python_command),
740
+ "detail": python_command or "python-not-found",
741
+ },
742
+ {
743
+ "name": "configured_python_command_exists",
744
+ "ok": Path(configured_python_command).exists() if Path(configured_python_command).is_absolute() else bool(shutil.which(configured_python_command)),
745
+ "detail": configured_python_command,
746
+ },
747
+ {
748
+ "name": "routing_file_exists",
749
+ "ok": workspace_paths["routing_file"].exists(),
750
+ "detail": str(workspace_paths["routing_file"]),
751
+ },
752
+ {
753
+ "name": "project_hook_source_exists",
754
+ "ok": PROJECT_HOOK_PATH.exists(),
755
+ "detail": str(PROJECT_HOOK_PATH),
756
+ },
757
+ ]
758
+
759
+ for target, config in registry.items():
760
+ pythonpath = Path(str(config["pythonpath"]))
761
+ module_path = pythonpath / Path(str(config["python_module"]).replace(".", "/")).with_suffix(".py")
762
+ checks.append(
763
+ {
764
+ "name": f"{target}_pythonpath_exists",
765
+ "ok": pythonpath.exists(),
766
+ "detail": str(pythonpath),
767
+ }
768
+ )
769
+ checks.append(
770
+ {
771
+ "name": f"{target}_entry_module_exists",
772
+ "ok": module_path.exists(),
773
+ "detail": str(module_path),
774
+ }
775
+ )
776
+
777
+ runtime_probe_command = configured_python_command if configured_python_command else (python_command or sys.executable)
778
+ runtime_env = dict(os.environ)
779
+ combined_pythonpath = os.pathsep.join([str(WORKSPACE / "codexmcp" / "src"), str(WORKSPACE / "geminimcp" / "src")])
780
+ existing_pythonpath = runtime_env.get("PYTHONPATH", "").strip()
781
+ runtime_env["PYTHONPATH"] = f"{combined_pythonpath}{os.pathsep}{existing_pythonpath}" if existing_pythonpath else combined_pythonpath
782
+
783
+ for module_name in ("mcp", "codexmcp.cli", "geminimcp.cli"):
784
+ probe = probe_python_import(runtime_probe_command, module_name, runtime_env)
785
+ checks.append(
786
+ {
787
+ "name": f"python_runtime_import_probe_{module_name.split('.')[0]}",
788
+ "ok": probe["ok"],
789
+ "detail": probe["detail"],
790
+ "module": module_name,
791
+ "returncode": probe["returncode"],
792
+ "stderr": probe["stderr"],
793
+ "stdout": probe["stdout"],
794
+ "error_type": probe["error_type"],
795
+ }
796
+ )
797
+
798
+ checks.append(
799
+ {
800
+ "name": "project_integration_ready",
801
+ "ok": bool(project_status["ready"]),
802
+ "detail": ", ".join(project_status["missing_or_outdated"]) or "ready",
803
+ }
804
+ )
805
+
806
+ failed = [item["name"] for item in checks if not item["ok"]]
807
+ ready = not failed
808
+ failure_categories, recommended_runtime_fix_commands = classify_doctor_failures(
809
+ checks,
810
+ configured_python_command,
811
+ workspace_paths["root"],
812
+ )
813
+ human_summary = "CodeCGC 自检通过。"
814
+ if not ready:
815
+ human_summary = "CodeCGC 自检发现运行前置或集成面存在缺失。"
816
+ doctor_summary = {
817
+ "ready": ready,
818
+ "failed_checks": failed,
819
+ "human_summary": human_summary,
820
+ "scope": "运行前置、执行器可导入性,以及项目级集成就绪状态",
821
+ "recommended_fix_command": "" if ready else build_doctor_fix_command(workspace_paths["root"]),
822
+ "recommended_runtime_fix_command": "" if not recommended_runtime_fix_commands else recommended_runtime_fix_commands[0],
823
+ "recommended_runtime_fix_commands": recommended_runtime_fix_commands,
824
+ "failure_categories": failure_categories,
825
+ }
826
+
827
+ return {
828
+ "success": True,
829
+ "mode": "doctor",
830
+ "workspace": str(workspace_paths["root"]),
831
+ "summary": doctor_summary,
832
+ "doctor_summary": doctor_summary,
833
+ "checks": checks,
834
+ "project": project_status,
835
+ "python": {
836
+ "current_executable": sys.executable,
837
+ "discovered_command": python_command,
838
+ "configured_command": configured_python_command,
839
+ },
840
+ }
841
+
842
+
843
+ def build_parser() -> argparse.ArgumentParser:
844
+ parser = argparse.ArgumentParser(description="Install or inspect CodeCGC integration surfaces.")
845
+ parser.add_argument("--mode", choices=["local", "user-dry-run", "user", "status", "doctor"], default="local")
846
+ parser.add_argument(
847
+ "--format",
848
+ choices=["json", "summary"],
849
+ default="summary",
850
+ help="Output format. Summary is the default product-facing mode; use json for debugging or automation.",
851
+ )
852
+ parser.add_argument(
853
+ "--workspace",
854
+ default="",
855
+ help="Optional target workspace root for local/status modes. Defaults to the current CodeCGC repository root.",
856
+ )
857
+ parser.add_argument("--user-root", default="", help="Optional explicit Claude user root for user/user-dry-run modes.")
858
+ return parser
859
+
860
+
861
+ def main() -> int:
862
+ parser = build_parser()
863
+ args = parser.parse_args()
864
+
865
+ if args.mode == "local":
866
+ result = install_local_runtime(args.workspace)
867
+ elif args.mode == "user-dry-run":
868
+ result = preview_user_install(args.user_root)
869
+ elif args.mode == "user":
870
+ result = install_user_runtime(args.user_root)
871
+ elif args.mode == "status":
872
+ result = collect_install_status(args.workspace)
873
+ elif args.mode == "doctor":
874
+ result = collect_doctor_status(args.workspace)
875
+ else:
876
+ raise ValueError(f"Unsupported install mode: {args.mode}")
877
+
878
+ if args.mode == "status" and args.format == "summary":
879
+ summary = result.get("summary", {}) if isinstance(result.get("summary"), dict) else {}
880
+ project = result.get("project", {}) if isinstance(result.get("project"), dict) else {}
881
+ user = result.get("user_preview_targets", {}) if isinstance(result.get("user_preview_targets"), dict) else {}
882
+ lines = [
883
+ f"- 工作区: {result.get('workspace', '')}",
884
+ f"- 范围: {summary.get('scope', '')}",
885
+ f"- 项目级就绪: {'是' if summary.get('project_ready') else '否'}",
886
+ f"- 用户级就绪: {'是' if summary.get('user_ready') else '否'}",
887
+ f"- 策略: {summary.get('default_policy', '')}",
888
+ f"- 摘要: {summary.get('human_summary', '')}",
889
+ f"- 项目缺失项: {', '.join(project.get('missing_or_outdated', [])) or '无'}",
890
+ f"- 用户缺失项: {', '.join(user.get('missing_or_outdated', [])) or '无'}",
891
+ ]
892
+ recommended_project = str(summary.get("recommended_project_command", "")).strip()
893
+ recommended_user = str(summary.get("recommended_user_command", "")).strip()
894
+ recommended_next = str(summary.get("recommended_next_command", "")).strip()
895
+ next_actions = [item for item in [recommended_next, recommended_project, recommended_user] if item]
896
+ print(render_summary_block("CodeCGC 安装状态", lines, next_actions))
897
+ return 0 if result.get("success") else 1
898
+
899
+ if args.mode == "doctor" and args.format == "summary":
900
+ summary = result.get("summary", {}) if isinstance(result.get("summary"), dict) else {}
901
+ checks = result.get("checks", []) if isinstance(result.get("checks"), list) else []
902
+ lines = [
903
+ f"- 工作区: {result.get('workspace', '')}",
904
+ f"- 范围: {summary.get('scope', '')}",
905
+ f"- 就绪: {'是' if summary.get('ready') else '否'}",
906
+ f"- 摘要: {summary.get('human_summary', '')}",
907
+ ]
908
+ failed_checks = summary.get("failed_checks", [])
909
+ lines.append(f"- 失败检查项: {', '.join(str(item) for item in failed_checks) or '无'}")
910
+ failure_categories = summary.get("failure_categories", [])
911
+ for item in failure_categories:
912
+ if not isinstance(item, dict):
913
+ continue
914
+ lines.append(
915
+ f"- 失败分类 {item.get('code', '')}: {item.get('summary', '')} | {item.get('suggestion', '')}"
916
+ )
917
+ for item in checks:
918
+ if not isinstance(item, dict):
919
+ continue
920
+ lines.append(
921
+ f"- 检查 {item.get('name', '')}: {'通过' if item.get('ok') else '失败'} ({item.get('detail', '')})"
922
+ )
923
+ fix_command = str(summary.get("recommended_fix_command", "")).strip()
924
+ runtime_fix_command = str(summary.get("recommended_runtime_fix_command", "")).strip()
925
+ next_actions = [item for item in [runtime_fix_command, fix_command] if item]
926
+ print(render_summary_block("CodeCGC Doctor", lines, next_actions))
927
+ return 0 if result.get("success") else 1
928
+
929
+ if args.format == "summary" and args.mode in {"local", "user-dry-run", "user"}:
930
+ print(build_install_mode_summary(result))
931
+ return 0 if result.get("success") else 1
932
+
933
+ print(json.dumps(result, ensure_ascii=False, indent=2))
934
+ return 0 if result.get("success") else 1
935
+
936
+
937
+ if __name__ == "__main__":
938
+ raise SystemExit(main())