gitinstall 1.1.0__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 (59) hide show
  1. gitinstall/__init__.py +61 -0
  2. gitinstall/_sdk.py +541 -0
  3. gitinstall/academic.py +831 -0
  4. gitinstall/admin.html +327 -0
  5. gitinstall/auto_update.py +384 -0
  6. gitinstall/autopilot.py +349 -0
  7. gitinstall/badge.py +476 -0
  8. gitinstall/checkpoint.py +330 -0
  9. gitinstall/cicd.py +499 -0
  10. gitinstall/clawhub.html +718 -0
  11. gitinstall/config_schema.py +353 -0
  12. gitinstall/db.py +984 -0
  13. gitinstall/db_backend.py +445 -0
  14. gitinstall/dep_chain.py +337 -0
  15. gitinstall/dependency_audit.py +1153 -0
  16. gitinstall/detector.py +542 -0
  17. gitinstall/doctor.py +493 -0
  18. gitinstall/education.py +869 -0
  19. gitinstall/enterprise.py +802 -0
  20. gitinstall/error_fixer.py +953 -0
  21. gitinstall/event_bus.py +251 -0
  22. gitinstall/executor.py +577 -0
  23. gitinstall/feature_flags.py +138 -0
  24. gitinstall/fetcher.py +921 -0
  25. gitinstall/huggingface.py +922 -0
  26. gitinstall/hw_detect.py +988 -0
  27. gitinstall/i18n.py +664 -0
  28. gitinstall/installer_registry.py +362 -0
  29. gitinstall/knowledge_base.py +379 -0
  30. gitinstall/license_check.py +605 -0
  31. gitinstall/llm.py +569 -0
  32. gitinstall/log.py +236 -0
  33. gitinstall/main.py +1408 -0
  34. gitinstall/mcp_agent.py +841 -0
  35. gitinstall/mcp_server.py +386 -0
  36. gitinstall/monorepo.py +810 -0
  37. gitinstall/multi_source.py +425 -0
  38. gitinstall/onboard.py +276 -0
  39. gitinstall/planner.py +222 -0
  40. gitinstall/planner_helpers.py +323 -0
  41. gitinstall/planner_known_projects.py +1010 -0
  42. gitinstall/planner_templates.py +996 -0
  43. gitinstall/remote_gpu.py +633 -0
  44. gitinstall/resilience.py +608 -0
  45. gitinstall/run_tests.py +572 -0
  46. gitinstall/skills.py +476 -0
  47. gitinstall/tool_schemas.py +324 -0
  48. gitinstall/trending.py +279 -0
  49. gitinstall/uninstaller.py +415 -0
  50. gitinstall/validate_top100.py +607 -0
  51. gitinstall/watchdog.py +180 -0
  52. gitinstall/web.py +1277 -0
  53. gitinstall/web_ui.html +2277 -0
  54. gitinstall-1.1.0.dist-info/METADATA +275 -0
  55. gitinstall-1.1.0.dist-info/RECORD +59 -0
  56. gitinstall-1.1.0.dist-info/WHEEL +5 -0
  57. gitinstall-1.1.0.dist-info/entry_points.txt +3 -0
  58. gitinstall-1.1.0.dist-info/licenses/LICENSE +21 -0
  59. gitinstall-1.1.0.dist-info/top_level.txt +1 -0
gitinstall/main.py ADDED
@@ -0,0 +1,1408 @@
1
+ """
2
+ main.py - gitinstall 总入口
3
+ =====================================
4
+
5
+ 支持两种调用方式:
6
+
7
+ 1. OpenClaw Skill 工具调用:
8
+ python main.py detect
9
+ python main.py fetch comfyanonymous/ComfyUI
10
+ python main.py plan comfyanonymous/ComfyUI
11
+ python main.py install comfyanonymous/ComfyUI [--dir ~/AI] [--llm none]
12
+
13
+ 2. 独立 CLI(pip install gitinstall 后):
14
+ gitinstall comfyanonymous/ComfyUI
15
+ gitinstall --dry-run AUTOMATIC1111/stable-diffusion-webui
16
+ gitinstall --llm groq ollama/ollama
17
+
18
+ 所有输出为 JSON(方便 OpenClaw 解析)+ 人类可读的进度信息(stderr)。
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import json
24
+ import os
25
+ import sys
26
+ from pathlib import Path
27
+
28
+ # ── 将 tools 目录加入 Python 路径(兼容直接执行和 import)──
29
+ _THIS_DIR = Path(__file__).parent
30
+ if str(_THIS_DIR) not in sys.path:
31
+ sys.path.insert(0, str(_THIS_DIR))
32
+
33
+ from log import get_logger
34
+ from i18n import t
35
+ from detector import EnvironmentDetector, format_env_summary
36
+ from fetcher import fetch_project, fetch_project_local, fetch_project_from_path, is_local_path, format_project_summary
37
+ from llm import create_provider, INSTALL_SYSTEM_PROMPT, INSTALL_SYSTEM_PROMPT_SMALL, HeuristicProvider
38
+ from executor import InstallExecutor, check_command_safety
39
+ from planner import SmartPlanner
40
+ from hw_detect import get_gpu_info, check_pytorch_compatibility, get_full_ai_hardware_report
41
+ from planner_known_projects import check_hardware_compatibility
42
+
43
+ logger = get_logger(__name__)
44
+
45
+
46
+ # ─────────────────────────────────────────────
47
+ # 子命令:detect
48
+ # ─────────────────────────────────────────────
49
+
50
+ def cmd_detect() -> dict:
51
+ """检测当前系统环境,输出 JSON"""
52
+ logger.info(t("install.detecting_env"))
53
+ env = EnvironmentDetector().detect()
54
+ logger.info(format_env_summary(env))
55
+ return {"status": "ok", "env": env}
56
+
57
+
58
+ # ─────────────────────────────────────────────
59
+ # 子命令:fetch
60
+ # ─────────────────────────────────────────────
61
+
62
+ def cmd_fetch(identifier: str) -> dict:
63
+ """获取 GitHub 项目信息,输出 JSON"""
64
+ logger.info(t("install.fetching", project=identifier))
65
+ try:
66
+ info = fetch_project(identifier)
67
+ logger.info(format_project_summary(info))
68
+ return {
69
+ "status": "ok",
70
+ "project": {
71
+ "full_name": info.full_name,
72
+ "description": info.description,
73
+ "stars": info.stars,
74
+ "language": info.language,
75
+ "project_type": info.project_type,
76
+ "clone_url": info.clone_url,
77
+ "homepage": info.homepage,
78
+ "has_dockerfile": "docker" in info.project_type,
79
+ "dependency_files": list(info.dependency_files.keys()),
80
+ "readme_preview": info.readme[:500],
81
+ },
82
+ }
83
+ except FileNotFoundError as e:
84
+ return {"status": "error", "message": str(e)}
85
+ except Exception as e:
86
+ return {"status": "error", "message": f"获取失败:{e}"}
87
+
88
+
89
+ # ─────────────────────────────────────────────
90
+ # 子命令:plan
91
+ # ─────────────────────────────────────────────
92
+
93
+ def cmd_plan(identifier: str, llm_force: str = None, use_local: bool = False) -> dict:
94
+ """
95
+ 生成安装计划(不执行)。
96
+
97
+ 生成策略(按优先级):
98
+ 1. SmartPlanner 已知项目数据库 → 无需任何 AI,命中率最高
99
+ 2. SmartPlanner 类型模板 → 无需任何 AI,依赖文件驱动
100
+ 3. 真实 LLM(如有配置) → 分析 README 生成个性化步骤
101
+ LLM 仅作为增强,不是必需项
102
+
103
+ 数据获取模式:
104
+ use_local=True → git clone --depth 1 本地分析(无 API 限额)
105
+ use_local=False → GitHub REST API(默认,受 60 次/小时限制)
106
+ """
107
+ # 1. 环境检测
108
+ logger.info("🔍 检测环境...")
109
+ env = EnvironmentDetector().detect()
110
+
111
+ # 2. 项目信息(本地路径 vs 本地 clone vs API 模式)
112
+ source_path = ""
113
+ try:
114
+ if is_local_path(identifier):
115
+ logger.info(f"📂 本地路径模式:直接分析目录(无需网络)...")
116
+ info = fetch_project_from_path(identifier)
117
+ source_path = str(info.clone_url) # 实际是解析后的本地绝对路径
118
+ elif use_local:
119
+ logger.info("📥 本地模式:git clone + 本地文件分析(无 API 限额)...")
120
+ info = fetch_project_local(identifier)
121
+ else:
122
+ logger.info("📡 获取项目信息...")
123
+ info = fetch_project(identifier)
124
+ except Exception as e:
125
+ return {"status": "error", "message": str(e)}
126
+
127
+ # 3. 首先用 SmartPlanner(零 AI,零 API Key)
128
+ logger.info(t("install.analyzing"))
129
+ planner = SmartPlanner()
130
+ smart_plan = planner.generate_plan(
131
+ owner=info.owner,
132
+ repo=info.repo,
133
+ env=env,
134
+ project_types=info.project_type,
135
+ dependency_files=info.dependency_files,
136
+ readme=info.readme,
137
+ clone_url=info.clone_url if not source_path else "",
138
+ source_path=source_path,
139
+ )
140
+ confidence = smart_plan.get("confidence", "low")
141
+ strategy = smart_plan.get("strategy", "")
142
+
143
+ # 4. 判断是否需要 LLM
144
+ # - 已知项目(high):直接用 SmartPlanner 结果,最准确
145
+ # - 明确指定 --llm none:不用 LLM
146
+ # - 类型模板/提取(medium/low)且有可用 LLM:用 LLM 补充
147
+ llm = create_provider(force=llm_force)
148
+ use_llm = (
149
+ confidence != "high" # 非已知项目才需要 LLM
150
+ and llm_force != "none" # 未强制禁用 LLM
151
+ and not isinstance(llm, HeuristicProvider) # 有真正的 LLM 可用
152
+ )
153
+
154
+ plan = smart_plan # 默认用 SmartPlanner
155
+
156
+ if use_llm:
157
+ logger.info(f"🤖 SmartPlanner 置信度 {confidence},尝试 LLM 补充分析({llm.name})...")
158
+ try:
159
+ user_prompt = _build_plan_prompt(env, info)
160
+ # 小模型(名字含 1b/1.5b/1.7b/3b)用精简 prompt + 较小 max_tokens
161
+ model_name = llm.name.lower()
162
+ is_small_model = any(x in model_name for x in ["1b", "1.5b", "1.7b", "3b"])
163
+ sys_prompt = INSTALL_SYSTEM_PROMPT_SMALL if is_small_model else INSTALL_SYSTEM_PROMPT
164
+ max_tok = 800 if is_small_model else 3000
165
+ response = llm.complete(sys_prompt, user_prompt, max_tokens=max_tok)
166
+ llm_plan = _validate_plan_schema(_parse_plan_response(response))
167
+ # LLM 成功且步骤更丰富时采用 LLM 结果
168
+ if llm_plan.get("steps") and len(llm_plan["steps"]) >= len(smart_plan.get("steps", [])):
169
+ plan = llm_plan
170
+ plan["strategy"] = f"llm_enhanced({strategy})"
171
+ except Exception as ex:
172
+ logger.warning(t("llm.api_error") + f" ({ex})")
173
+ else:
174
+ source = "已知项目数据库" if confidence == "high" else "类型模板"
175
+ logger.info(f"✅ SmartPlanner {source} 命中(无需 LLM)")
176
+
177
+ # 5. 安全预检每个步骤
178
+ safe_steps = []
179
+ for step in plan.get("steps", []):
180
+ cmd = step.get("command", "")
181
+ is_safe, safety_warning = check_command_safety(cmd)
182
+ step["_safe"] = is_safe
183
+ if not step.get("_warning"):
184
+ step["_warning"] = safety_warning
185
+ if is_safe:
186
+ safe_steps.append(step)
187
+ else:
188
+ logger.info(f"🚫 已过滤危险命令:{cmd}")
189
+ plan["steps"] = safe_steps
190
+
191
+ llm_desc = llm.name if use_llm else "SmartPlanner(无需 AI Key)"
192
+
193
+ # 6. AI 硬件智能检测 + 兼容性检查
194
+ gpu_info = get_gpu_info()
195
+ hw_compat = check_hardware_compatibility(f"{info.owner}/{info.repo}", gpu_info, env)
196
+ if hw_compat.get("warnings"):
197
+ logger.info("\n⚡ 硬件兼容性提示:")
198
+ for w in hw_compat["warnings"]:
199
+ logger.warning(f" ⚠️ {w}")
200
+ for r in hw_compat.get("recommendations", []):
201
+ logger.info(f" 💡 {r}")
202
+
203
+ return {
204
+ "status": "ok",
205
+ "plan": plan,
206
+ "llm_used": llm_desc,
207
+ "confidence": confidence,
208
+ "project": info.full_name,
209
+ "hardware_check": hw_compat,
210
+ "gpu_info": {
211
+ "type": gpu_info.get("type"),
212
+ "name": gpu_info.get("name"),
213
+ "vram_gb": gpu_info.get("vram_gb"),
214
+ },
215
+ # 供韧性层(resilience)使用
216
+ "_owner": info.owner,
217
+ "_repo": info.repo,
218
+ "_project_types": info.project_type,
219
+ "_dependency_files": info.dependency_files,
220
+ "_env": env,
221
+ }
222
+
223
+
224
+ # ─────────────────────────────────────────────
225
+ # 子命令:install
226
+ # ─────────────────────────────────────────────
227
+
228
+ def cmd_install(
229
+ identifier: str,
230
+ install_dir: str = None,
231
+ llm_force: str = None,
232
+ dry_run: bool = False,
233
+ use_local: bool = False,
234
+ ) -> dict:
235
+ """
236
+ 端到端安装:环境检测 → 项目信息 → 生成计划 → 执行 → 错误修复
237
+ 新增:预检层 + 多策略回退 + 安全审计 + 许可证检查 + Skills 匹配
238
+ """
239
+ from resilience import preflight_check, generate_fallback_plans
240
+
241
+ # 1. 获取安装计划
242
+ plan_result = cmd_plan(identifier, llm_force=llm_force, use_local=use_local)
243
+ if plan_result["status"] != "ok":
244
+ return plan_result
245
+
246
+ plan = plan_result["plan"]
247
+ steps = plan.get("steps", [])
248
+ owner = plan_result.get("_owner", "")
249
+ repo = plan_result.get("_repo", "")
250
+ project_types = plan_result.get("_project_types", [])
251
+ dependency_files = plan_result.get("_dependency_files", {})
252
+ env = plan_result.get("_env", {})
253
+
254
+ # 1b. 安全审计:扫描依赖中的 CVE/恶意包/typosquatting
255
+ audit_warnings = []
256
+ if dependency_files:
257
+ try:
258
+ from dependency_audit import audit_project, RISK_CRITICAL, RISK_HIGH
259
+ audit_results = audit_project(dependency_files)
260
+ for ar in audit_results:
261
+ for vuln in ar.vulnerabilities:
262
+ if vuln.risk in (RISK_CRITICAL, RISK_HIGH):
263
+ audit_warnings.append(f" 🚨 [{vuln.risk.upper()}] {vuln.package}: {vuln.description}")
264
+ if audit_warnings:
265
+ logger.warning(f"\n⚠️ 依赖安全审计发现 {len(audit_warnings)} 个高危问题:")
266
+ for w in audit_warnings[:5]:
267
+ logger.warning(w)
268
+ if len(audit_warnings) > 5:
269
+ logger.warning(f" ... 还有 {len(audit_warnings) - 5} 个")
270
+ else:
271
+ logger.info("✅ 依赖安全审计通过")
272
+ except Exception:
273
+ pass # 审计失败不阻塞安装
274
+
275
+ # 1c. 许可证检查
276
+ license_risk = ""
277
+ if owner and repo:
278
+ try:
279
+ from license_check import fetch_license_from_github, analyze_license
280
+ spdx_id, license_text = fetch_license_from_github(owner, repo)
281
+ if spdx_id or license_text:
282
+ lic_result = analyze_license(spdx_id, license_text)
283
+ license_risk = lic_result.risk
284
+ if lic_result.issues:
285
+ logger.info(f"\n📜 许可证({spdx_id or '未知'}):")
286
+ for issue in lic_result.issues[:3]:
287
+ logger.info(f" {issue}")
288
+ else:
289
+ logger.info(f"📜 许可证:{spdx_id or '未知'} ✅")
290
+ except Exception:
291
+ pass # 许可证检查失败不阻塞安装
292
+
293
+ # 1d. Skills 匹配:查找社区安装策略
294
+ matched_skills = []
295
+ try:
296
+ from skills import SkillManager
297
+ sm = SkillManager()
298
+ matched_skills = sm.find_matching_skills(
299
+ owner=owner, repo=repo,
300
+ project_types=project_types,
301
+ file_list=list(dependency_files.keys()),
302
+ )
303
+ if matched_skills:
304
+ skill_names = [s.meta.name for s in matched_skills[:3]]
305
+ logger.info(f"\n🧩 匹配到 {len(matched_skills)} 个 Skills:{', '.join(skill_names)}")
306
+ except Exception:
307
+ pass # Skills 匹配失败不阻塞安装
308
+
309
+ if not steps:
310
+ return {"status": "error", "message": "未能生成有效的安装步骤,请手动查阅 README。"}
311
+
312
+ # 2. 预检层:检查计划中缺失的工具,提前安装
313
+ pf = preflight_check(steps)
314
+ if not pf.all_ready:
315
+ logger.info(f"\n🔧 预检发现 {len(pf.missing_tools)} 个缺失工具:{', '.join(pf.missing_tools)}")
316
+ # 把预检安装步骤插入到计划最前面
317
+ steps = pf.install_commands + steps
318
+ plan["steps"] = steps
319
+
320
+ # 3. 展示计划(dry-run 在此结束)
321
+ logger.info("\n" + "─" * 50)
322
+ logger.info(f"📋 安装计划:{plan_result['project']}")
323
+ logger.info(f" LLM:{plan_result['llm_used']}")
324
+ logger.info("─" * 50)
325
+ for i, step in enumerate(steps, 1):
326
+ warning = step.get("_warning", "")
327
+ warn_icon = "⚠️ " if warning else ""
328
+ logger.info(f" {i}. {warn_icon}{step.get('description', '')}:")
329
+ logger.info(f" $ {step.get('command', '')}")
330
+ if plan.get("launch_command"):
331
+ logger.info(f"\n ▶ 启动命令:{plan['launch_command']}")
332
+ logger.info("─" * 50)
333
+
334
+ if dry_run:
335
+ logger.info("\n(--dry-run 模式,不执行任何命令)")
336
+ return {"status": "ok", "dry_run": True, "plan": plan}
337
+
338
+ # 4. 执行主计划
339
+ llm = create_provider(force=llm_force)
340
+ executor = InstallExecutor(llm_provider=llm, verbose=True)
341
+
342
+ # 调整第一步的工作目录(如果指定了安装目录)
343
+ base_dir = install_dir
344
+ if base_dir:
345
+ base_dir = str(Path(base_dir).expanduser())
346
+ Path(base_dir).mkdir(parents=True, exist_ok=True)
347
+ executor.executor.work_dir = base_dir
348
+ executor.executor._current_dir = base_dir
349
+
350
+ result = executor.execute_plan(plan, project_name=plan_result["project"])
351
+
352
+ # 5. 主计划失败 → 自动触发多策略回退
353
+ if not result.success and owner and repo:
354
+ primary_strategy = plan.get("strategy", "")
355
+ logger.info(f"\n{'='*50}")
356
+ logger.info(f"⚡ 主计划失败(策略:{primary_strategy}),启动多策略回退...")
357
+
358
+ fallback_plans = generate_fallback_plans(owner, repo, project_types, env, dependency_files=dependency_files)
359
+ tried = {primary_strategy}
360
+
361
+ for fb in fallback_plans:
362
+ if fb.strategy in tried:
363
+ continue
364
+ tried.add(fb.strategy)
365
+
366
+ logger.info(f"\n🔄 尝试回退策略 Tier-{fb.tier}:{fb.strategy}(置信度:{fb.confidence})")
367
+
368
+ # 构建回退 plan dict
369
+ fb_plan = {
370
+ "steps": fb.steps,
371
+ "launch_command": plan.get("launch_command", ""),
372
+ "strategy": fb.strategy,
373
+ }
374
+
375
+ # 重置执行器状态
376
+ executor.executor.reset(base_dir)
377
+
378
+ fb_result = executor.execute_plan(fb_plan, project_name=plan_result["project"])
379
+ if fb_result.success:
380
+ logger.info(f"\n✅ 回退策略 {fb.strategy} 成功!")
381
+ result = fb_result
382
+ break
383
+ else:
384
+ logger.warning(f" ❌ 回退策略 {fb.strategy} 也失败")
385
+
386
+ # 6. 序列化输出 + 安装遥测
387
+ try:
388
+ from db import record_install_telemetry
389
+ record_install_telemetry(
390
+ project=plan_result["project"],
391
+ strategy=plan.get("strategy", ""),
392
+ gpu_info=plan_result.get("gpu_info"),
393
+ env=env,
394
+ success=result.success,
395
+ error_type="step_failed" if not result.success else None,
396
+ error_message=result.error_summary if not result.success else None,
397
+ duration_sec=None,
398
+ steps_total=len(result.steps),
399
+ steps_completed=sum(1 for s in result.steps if s.success),
400
+ )
401
+ except Exception:
402
+ pass # 遥测失败不影响安装流程
403
+
404
+ # 7. 安装成功 → 记录到 InstallTracker(供 updates/uninstall 使用)
405
+ if result.success and owner and repo:
406
+ try:
407
+ from auto_update import InstallTracker
408
+ tracker = InstallTracker()
409
+ tracker.record_install(
410
+ owner=owner,
411
+ repo=repo,
412
+ install_dir=result.install_dir or install_dir or "",
413
+ )
414
+ logger.info("📝 已记录安装信息(支持 updates/uninstall)")
415
+ except Exception:
416
+ pass # 记录失败不影响安装结果
417
+
418
+ return {
419
+ "status": "ok" if result.success else "error",
420
+ "project": result.project,
421
+ "success": result.success,
422
+ "plan_strategy": plan.get("strategy", ""),
423
+ "install_dir": result.install_dir,
424
+ "launch_command": result.launch_command,
425
+ "plan": plan,
426
+ "error_summary": result.error_summary,
427
+ "steps_completed": sum(1 for s in result.steps if s.success),
428
+ "steps_total": len(result.steps),
429
+ "audit_warnings": len(audit_warnings),
430
+ "license_risk": license_risk,
431
+ "matched_skills": [s.meta.name for s in matched_skills],
432
+ }
433
+
434
+
435
+ # ─────────────────────────────────────────────
436
+ # 辅助函数
437
+ # ─────────────────────────────────────────────
438
+
439
+ def _sanitize_for_prompt(text: str) -> str:
440
+ """
441
+ 清洗用户可控文本(README、依赖文件等),防止 prompt 注入。
442
+ 移除可能影响 LLM 指令理解的标记。
443
+ """
444
+ import re as _re
445
+ # 移除常见 prompt 注入标记
446
+ injection_patterns = [
447
+ r'(?i)ignore\s+(?:all\s+)?previous\s+instructions?',
448
+ r'(?i)forget\s+(?:all\s+)?(?:above|previous)',
449
+ r'(?i)you\s+are\s+now\s+(?:a|an)',
450
+ r'(?i)new\s+instructions?\s*:',
451
+ r'(?i)system\s*(?:prompt|message)\s*:',
452
+ r'(?i)assistant\s*(?:prompt|message)\s*:',
453
+ r'(?i)\[INST\]',
454
+ r'(?i)<\|(?:im_start|im_end|system|user|assistant)\|>',
455
+ ]
456
+ cleaned = text
457
+ for pat in injection_patterns:
458
+ cleaned = _re.sub(pat, '[FILTERED]', cleaned)
459
+ return cleaned
460
+
461
+
462
+ def _build_plan_prompt(env: dict, info) -> str:
463
+ """构建发给 LLM 的安装计划请求 prompt"""
464
+ os_info = env.get("os", {})
465
+ gpu_info = env.get("gpu", {})
466
+ pms = env.get("package_managers", {})
467
+ runtimes = env.get("runtimes", {})
468
+
469
+ available_pms = [k for k, v in pms.items() if v.get("available")]
470
+ python_ver = runtimes.get("python", {}).get("version", "unknown")
471
+ has_git = runtimes.get("git", {}).get("available", False)
472
+ has_docker = runtimes.get("docker", {}).get("available", False)
473
+
474
+ # 依赖文件摘要(限制长度防止注入)
475
+ dep_summary = ""
476
+ for fname, content in info.dependency_files.items():
477
+ safe_content = _sanitize_for_prompt(content[:500])
478
+ dep_summary += f"\n### {fname}\n```\n{safe_content}\n```\n"
479
+
480
+ # 清洗 README 内容,防止 prompt 注入
481
+ safe_readme = _sanitize_for_prompt(info.readme[:8000])
482
+
483
+ return f"""
484
+ ## 目标项目
485
+ 仓库:{info.full_name}
486
+ 描述:{info.description}
487
+ 主要语言:{info.language}
488
+ 项目类型:{', '.join(info.project_type)}
489
+ 克隆地址:{info.clone_url}
490
+
491
+ ## 当前用户系统环境
492
+ 操作系统:{os_info.get('type', 'unknown')} {os_info.get('version', '')} ({os_info.get('arch', '')})
493
+ {"芯片:" + os_info.get('chip', '') if os_info.get('chip') else ''}
494
+ {"WSL2:是" if os_info.get('is_wsl') else ''}
495
+ GPU:{gpu_info.get('name', '无')} - {gpu_info.get('type', 'cpu_only')}
496
+ {"CUDA:" + gpu_info.get('cuda_version', '') if gpu_info.get('cuda_version') else ''}
497
+ Python:{python_ver}
498
+ Git:{'已安装' if has_git else '未安装'}
499
+ Docker:{'已安装' if has_docker else '未安装'}
500
+ 可用包管理器:{', '.join(available_pms)}
501
+
502
+ ## 项目 README(节选)
503
+ {safe_readme}
504
+
505
+ ## 依赖文件
506
+ {dep_summary if dep_summary else "(无检测到的依赖文件)"}
507
+
508
+ ## 要求
509
+ 请根据以上环境,生成适配 {os_info.get('type', '')} {os_info.get('arch', '')} 的完整安装步骤。
510
+ 安装目标目录:~/(用户主目录)
511
+ """
512
+
513
+
514
+ def _parse_plan_response(response: str) -> dict:
515
+ """解析 LLM 返回的安装计划 JSON"""
516
+ # 尝试直接解析
517
+ try:
518
+ return json.loads(response)
519
+ except json.JSONDecodeError:
520
+ pass
521
+
522
+ # 提取代码块中的 JSON
523
+ import re
524
+ match = re.search(r'```(?:json)?\n(.*?)```', response, re.DOTALL)
525
+ if match:
526
+ try:
527
+ return json.loads(match.group(1))
528
+ except json.JSONDecodeError:
529
+ pass
530
+
531
+ # 尝试找到第一个 { 开始的 JSON
532
+ start = response.find("{")
533
+ end = response.rfind("}") + 1
534
+ if start >= 0 and end > start:
535
+ try:
536
+ return json.loads(response[start:end])
537
+ except json.JSONDecodeError:
538
+ pass
539
+
540
+ # 无法解析,返回空计划
541
+ return {"project_name": "", "steps": [], "launch_command": ""}
542
+
543
+
544
+ def _validate_plan_schema(plan: dict) -> dict:
545
+ """
546
+ 验证 LLM 返回的 plan 结构,防止 prompt 注入攻击。
547
+ 仅保留合法字段,过滤异常数据。
548
+ """
549
+ safe = {
550
+ "project_name": str(plan.get("project_name", ""))[:200],
551
+ "steps": [],
552
+ "launch_command": str(plan.get("launch_command", ""))[:500],
553
+ }
554
+ for step in plan.get("steps", []):
555
+ if not isinstance(step, dict):
556
+ continue
557
+ s = {
558
+ "description": str(step.get("description", ""))[:500],
559
+ "command": str(step.get("command", ""))[:2000],
560
+ }
561
+ # 只保留非空命令
562
+ if s["command"].strip():
563
+ safe["steps"].append(s)
564
+ return safe
565
+
566
+
567
+ # ─────────────────────────────────────────────
568
+ # 子命令:doctor(系统诊断)
569
+ # ─────────────────────────────────────────────
570
+
571
+ def cmd_doctor(json_output: bool = False) -> dict:
572
+ """运行系统诊断"""
573
+ from doctor import run_doctor, format_doctor_report, doctor_to_dict
574
+
575
+ logger.info("🩺 正在运行系统诊断...")
576
+ report = run_doctor()
577
+
578
+ if json_output:
579
+ return doctor_to_dict(report)
580
+
581
+ logger.info(format_doctor_report(report))
582
+ return doctor_to_dict(report)
583
+
584
+
585
+ # ─────────────────────────────────────────────
586
+ # 子命令:skills(插件管理)
587
+ # ─────────────────────────────────────────────
588
+
589
+ def cmd_skills(args) -> dict:
590
+ """Skills 插件管理"""
591
+ from skills import SkillManager, ensure_builtin_skills, format_skills_list
592
+
593
+ mgr = SkillManager()
594
+
595
+ if args.skills_action == "list":
596
+ skills = mgr.list_skills()
597
+ logger.info(f"\n🔧 已安装 Skills ({len(skills)} 个):")
598
+ logger.info(format_skills_list(skills))
599
+ return {
600
+ "status": "ok",
601
+ "count": len(skills),
602
+ "skills": [{"name": s.meta.name, "version": s.meta.version,
603
+ "description": s.meta.description, "enabled": s.enabled}
604
+ for s in skills],
605
+ }
606
+
607
+ elif args.skills_action == "init":
608
+ ensure_builtin_skills()
609
+ skills = mgr.list_skills()
610
+ logger.info(f"✅ 已初始化 {len(skills)} 个内建 Skills")
611
+ return {"status": "ok", "initialized": len(skills)}
612
+
613
+ elif args.skills_action == "create":
614
+ try:
615
+ path = mgr.create_skill(
616
+ name=args.name,
617
+ description=args.desc,
618
+ steps=[],
619
+ )
620
+ logger.info(f"✅ Skill '{args.name}' 已创建: {path}")
621
+ return {"status": "ok", "name": args.name, "path": str(path)}
622
+ except (ValueError, FileExistsError) as e:
623
+ return {"status": "error", "message": str(e)}
624
+
625
+ elif args.skills_action == "remove":
626
+ if mgr.remove_skill(args.name):
627
+ logger.info(f"✅ Skill '{args.name}' 已删除")
628
+ return {"status": "ok", "removed": args.name}
629
+ return {"status": "error", "message": f"Skill '{args.name}' 不存在"}
630
+
631
+ elif args.skills_action == "export":
632
+ data = mgr.export_skill(args.name)
633
+ if data:
634
+ return {"status": "ok", "skill_data": data}
635
+ return {"status": "error", "message": f"Skill '{args.name}' 不存在"}
636
+
637
+ return {"status": "error", "message": "未知 skills 子命令"}
638
+
639
+
640
+ # ─────────────────────────────────────────────
641
+ # 子命令:config(配置管理)
642
+ # ─────────────────────────────────────────────
643
+
644
+ def cmd_config(args) -> dict:
645
+ """配置文件管理"""
646
+ from config_schema import load_and_validate, format_validation_result
647
+ from onboard import CONFIG_FILE
648
+
649
+ if args.config_action == "show":
650
+ result = load_and_validate()
651
+ # 隐藏敏感信息
652
+ display_config = dict(result.config)
653
+ for key in ["github_token"]:
654
+ if display_config.get(key):
655
+ display_config[key] = display_config[key][:8] + "..."
656
+ if display_config.get("llm_key"):
657
+ display_config["llm_key"] = {k: v[:8] + "..." for k, v in display_config["llm_key"].items()}
658
+ logger.info(json.dumps(display_config, indent=2, ensure_ascii=False))
659
+ return {"status": "ok", "config": display_config}
660
+
661
+ elif args.config_action == "validate":
662
+ result = load_and_validate()
663
+ logger.info(format_validation_result(result))
664
+ return {
665
+ "status": "ok" if result.valid else "error",
666
+ "valid": result.valid,
667
+ "errors": [{"path": e.path, "message": e.message} for e in result.errors],
668
+ "warnings": result.warnings,
669
+ }
670
+
671
+ elif args.config_action == "path":
672
+ logger.info(str(CONFIG_FILE))
673
+ return {"status": "ok", "path": str(CONFIG_FILE)}
674
+
675
+ return {"status": "error", "message": "未知 config 子命令"}
676
+
677
+
678
+ # ─────────────────────────────────────────────
679
+ # 子命令:platforms(平台列表)
680
+ # ─────────────────────────────────────────────
681
+
682
+ def cmd_platforms() -> dict:
683
+ """列出支持的代码托管平台"""
684
+ from multi_source import get_supported_platforms
685
+
686
+ platforms = get_supported_platforms()
687
+ logger.info("\n📦 支持的代码托管平台:")
688
+ for p in platforms:
689
+ token_status = ""
690
+ if p["env_token"]:
691
+ import os
692
+ has_token = bool(os.getenv(p["env_token"], "").strip())
693
+ token_status = " ✅" if has_token else f" (设置 {p['env_token']} 提升配额)"
694
+ logger.info(f" • {p['name']:12s} {p['domain']}{token_status}")
695
+ logger.info(" 用法: gitinstall install <platform-url>/owner/repo")
696
+ logger.info(" 示例: gitinstall install gitlab.com/inkscape/inkscape")
697
+ return {"status": "ok", "platforms": platforms}
698
+
699
+
700
+ # ─────────────────────────────────────────────
701
+ # 子命令:audit(依赖安全审计)
702
+ # ─────────────────────────────────────────────
703
+
704
+ def cmd_audit(identifier: str, online: bool = False) -> dict:
705
+ """审计项目依赖的安全风险"""
706
+ from dependency_audit import audit_project, format_audit_results, audit_to_dict
707
+
708
+ logger.info(f"🔍 正在审计 {identifier} 的依赖安全...")
709
+ try:
710
+ info = fetch_project(identifier)
711
+ if not info.dependency_files:
712
+ logger.warning(" ⚠️ 未找到依赖文件")
713
+ return {"status": "ok", "message": "未找到依赖文件", "results": []}
714
+
715
+ results = audit_project(info.dependency_files, online=online)
716
+ logger.info(format_audit_results(results))
717
+ return {"status": "ok", **audit_to_dict(results)}
718
+ except Exception as e:
719
+ return {"status": "error", "message": f"审计失败:{e}"}
720
+
721
+
722
+ # ─────────────────────────────────────────────
723
+ # 子命令:license(许可证检查)
724
+ # ─────────────────────────────────────────────
725
+
726
+ def cmd_license(identifier: str) -> dict:
727
+ """检查项目许可证兼容性"""
728
+ from license_check import (
729
+ analyze_license, format_license_result, license_to_dict,
730
+ fetch_license_from_github,
731
+ )
732
+ from fetcher import parse_repo_identifier
733
+
734
+ logger.info(f"📜 正在检查 {identifier} 的许可证...")
735
+ try:
736
+ owner, repo = parse_repo_identifier(identifier)
737
+ spdx_id, license_text = fetch_license_from_github(owner, repo)
738
+
739
+ if not spdx_id and not license_text:
740
+ logger.warning(" ⚠️ 未找到许可证信息")
741
+ return {"status": "ok", "message": "项目未声明许可证", "risk": "warning"}
742
+
743
+ result = analyze_license(spdx_id, license_text)
744
+ logger.info(format_license_result(result))
745
+ return {"status": "ok", **license_to_dict(result)}
746
+ except Exception as e:
747
+ return {"status": "error", "message": f"许可证检查失败:{e}"}
748
+
749
+
750
+ # ─────────────────────────────────────────────
751
+ # 子命令:updates(更新检查)
752
+ # ─────────────────────────────────────────────
753
+
754
+ def cmd_updates(args) -> dict:
755
+ """管理已安装项目和更新检查"""
756
+ from auto_update import (
757
+ InstallTracker, check_all_updates,
758
+ format_installed_list, format_update_results, updates_to_dict,
759
+ )
760
+
761
+ tracker = InstallTracker()
762
+
763
+ if args.updates_action == "list":
764
+ projects = tracker.list_installed()
765
+ logger.info(format_installed_list(projects))
766
+ return {
767
+ "status": "ok",
768
+ "installed": [p.to_dict() for p in projects],
769
+ "total": len(projects),
770
+ }
771
+
772
+ elif args.updates_action == "check":
773
+ logger.info("🔄 正在检查所有项目的更新...")
774
+ results = check_all_updates(tracker)
775
+ logger.info(format_update_results(results))
776
+ return {"status": "ok", **updates_to_dict(results)}
777
+
778
+ elif args.updates_action == "remove":
779
+ name = args.name
780
+ parts = name.split("/")
781
+ if len(parts) != 2:
782
+ return {"status": "error", "message": f"格式错误:应为 owner/repo,收到 '{name}'"}
783
+ ok = tracker.remove_project(parts[0], parts[1])
784
+ if ok:
785
+ logger.info(f" ✅ 已移除 {name} 的记录")
786
+ else:
787
+ logger.warning(f" ❌ 未找到 {name}")
788
+ return {"status": "ok" if ok else "error", "removed": ok}
789
+
790
+ return {"status": "error", "message": "未知子命令"}
791
+
792
+
793
+ # ─────────────────────────────────────────────
794
+ # 子命令:resume(断点恢复)
795
+ # ─────────────────────────────────────────────
796
+
797
+ def cmd_resume(identifier: str = None, llm_force: str = None,
798
+ install_dir: str = None) -> dict:
799
+ """恢复中断的安装"""
800
+ from checkpoint import CheckpointManager, format_checkpoint_list, format_resume_plan
801
+
802
+ mgr = CheckpointManager()
803
+
804
+ if not identifier:
805
+ # 列出可恢复的安装
806
+ resumable = mgr.get_resumable()
807
+ if not resumable:
808
+ logger.info(" ✅ 没有中断的安装任务")
809
+ return {"status": "ok", "resumable": []}
810
+ logger.info(format_checkpoint_list(resumable))
811
+ return {
812
+ "status": "ok",
813
+ "resumable": [cp.project for cp in resumable],
814
+ }
815
+
816
+ # 找到指定项目的断点
817
+ from fetcher import parse_repo_identifier
818
+ try:
819
+ owner, repo = parse_repo_identifier(identifier)
820
+ except Exception as e:
821
+ return {"status": "error", "message": f"无法解析项目: {e}"}
822
+
823
+ cp = mgr.get_checkpoint(owner, repo)
824
+ if not cp:
825
+ return {"status": "error", "message": f"未找到 {owner}/{repo} 的断点记录"}
826
+
827
+ resume_idx = mgr.get_resume_step(owner, repo)
828
+ if resume_idx is None:
829
+ return {"status": "ok", "message": "该安装已完成,无需恢复"}
830
+
831
+ logger.info(format_resume_plan(cp, resume_idx))
832
+
833
+ # 从断点恢复执行
834
+ remaining_steps = [s for s in cp.steps[resume_idx:]
835
+ if s.status in ("pending", "failed")]
836
+ if not remaining_steps:
837
+ return {"status": "ok", "message": "所有步骤已完成"}
838
+
839
+ plan = {
840
+ "steps": [{"command": s.command, "description": s.description}
841
+ for s in remaining_steps],
842
+ "launch_command": cp.plan.get("launch_command", ""),
843
+ }
844
+
845
+ result = cmd_install(identifier, install_dir=install_dir,
846
+ llm_force=llm_force)
847
+ return result
848
+
849
+
850
+ # ─────────────────────────────────────────────
851
+ # 子命令:flags(功能开关)
852
+ # ─────────────────────────────────────────────
853
+
854
+ def cmd_flags(args) -> dict:
855
+ """查看/管理功能开关"""
856
+ from feature_flags import get_all_status, format_flags_table, list_flags, is_enabled
857
+
858
+ if args.flags_action == "list":
859
+ group = getattr(args, "group", None)
860
+ if group:
861
+ flags = list_flags(group)
862
+ logger.info(f"\n🚩 功能开关({group} 组):")
863
+ for f in flags:
864
+ icon = "✅" if is_enabled(f.name) else "❌"
865
+ logger.info(f" {icon} {f.name}: {f.description}")
866
+ return {"status": "ok", "flags": [{"name": f.name, "enabled": is_enabled(f.name), "description": f.description} for f in flags]}
867
+ status = get_all_status()
868
+ logger.info(format_flags_table())
869
+ return {"status": "ok", "flags": status}
870
+
871
+ elif args.flags_action == "show":
872
+ status = get_all_status()
873
+ logger.info(format_flags_table())
874
+ return {"status": "ok", "flags": status}
875
+
876
+ return {"status": "error", "message": "未知 flags 子命令"}
877
+
878
+
879
+ # ─────────────────────────────────────────────
880
+ # 子命令:registry(安装器注册表)
881
+ # ─────────────────────────────────────────────
882
+
883
+ def cmd_registry(args) -> dict:
884
+ """查看安装器注册表"""
885
+ from installer_registry import InstallerRegistry
886
+
887
+ registry = InstallerRegistry()
888
+
889
+ if args.registry_action == "list":
890
+ all_installers = registry.list_all()
891
+ available = registry.list_available()
892
+ logger.info(registry.format_registry())
893
+ return {
894
+ "status": "ok",
895
+ "total": len(all_installers),
896
+ "available": [i.info.name for i in available],
897
+ "installers": registry.to_dict(),
898
+ }
899
+
900
+ elif args.registry_action == "detect":
901
+ available = registry.list_available()
902
+ logger.info(f"\n🔧 检测到 {len(available)} 个可用安装器:")
903
+ for inst in available:
904
+ logger.info(f" ✅ {inst.info.name} v{inst.info.version or '?'}")
905
+ return {
906
+ "status": "ok",
907
+ "available": [{
908
+ "name": inst.info.name,
909
+ "version": inst.info.version or "",
910
+ "ecosystems": inst.info.ecosystems,
911
+ } for inst in available],
912
+ }
913
+
914
+ return {"status": "error", "message": "未知 registry 子命令"}
915
+
916
+
917
+ # ─────────────────────────────────────────────
918
+ # 子命令:events(事件历史)
919
+ # ─────────────────────────────────────────────
920
+
921
+ def cmd_events(args) -> dict:
922
+ """查看事件历史"""
923
+ from event_bus import get_event_bus
924
+
925
+ bus = get_event_bus()
926
+ event_type = getattr(args, "type", None)
927
+ limit = getattr(args, "limit", 50)
928
+ history = bus.get_history(event_type=event_type, limit=limit)
929
+
930
+ if not history:
931
+ logger.info(" 📭 暂无事件记录")
932
+ return {"status": "ok", "events": [], "total": 0}
933
+
934
+ logger.info(f"\n📡 事件历史(最近 {len(history)} 条):")
935
+ for evt in history:
936
+ logger.info(f" [{evt.timestamp}] {evt.event_type} - {evt.project}")
937
+ if evt.data:
938
+ for k, v in list(evt.data.items())[:3]:
939
+ logger.info(f" {k}: {v}")
940
+
941
+ return {
942
+ "status": "ok",
943
+ "events": [e.to_dict() for e in history],
944
+ "total": len(history),
945
+ }
946
+
947
+
948
+ # ─────────────────────────────────────────────
949
+ # 子命令:chain(依赖链)
950
+ # ─────────────────────────────────────────────
951
+
952
+ def cmd_chain(identifier: str, llm_force: str = None,
953
+ use_local: bool = False) -> dict:
954
+ """可视化项目安装依赖链"""
955
+ from dep_chain import build_chain_from_plan, format_dep_chain
956
+
957
+ plan_result = cmd_plan(identifier, llm_force=llm_force, use_local=use_local)
958
+ if plan_result["status"] != "ok":
959
+ return plan_result
960
+
961
+ plan = plan_result["plan"]
962
+ chain = build_chain_from_plan(plan)
963
+ logger.info(format_dep_chain(chain))
964
+
965
+ return {
966
+ "status": "ok",
967
+ "project": plan_result.get("project", identifier),
968
+ "chain": chain.to_dict(),
969
+ "has_cycle": chain.has_cycle(),
970
+ "node_count": len(chain.nodes),
971
+ }
972
+
973
+
974
+ # ─────────────────────────────────────────────
975
+ # 子命令:kb(安装知识库)
976
+ # ─────────────────────────────────────────────
977
+
978
+ def cmd_kb(args) -> dict:
979
+ """管理安装知识库"""
980
+ from knowledge_base import KnowledgeBase, format_kb_stats, format_search_results
981
+
982
+ kb = KnowledgeBase()
983
+
984
+ if args.kb_action == "stats":
985
+ stats = kb.get_stats()
986
+ logger.info(format_kb_stats(stats))
987
+ return {"status": "ok", **stats}
988
+
989
+ elif args.kb_action == "search":
990
+ query = getattr(args, "query", "")
991
+ if not query:
992
+ return {"status": "error", "message": "请指定搜索关键词"}
993
+ results = kb.search(project=query)
994
+ logger.info(format_search_results(results))
995
+ return {
996
+ "status": "ok",
997
+ "results": [{
998
+ "project": r.entry.project,
999
+ "score": r.score,
1000
+ "success": r.entry.success,
1001
+ "strategy": r.entry.strategy,
1002
+ "reasons": r.match_reasons,
1003
+ } for r in results],
1004
+ }
1005
+
1006
+ elif args.kb_action == "rate":
1007
+ project = getattr(args, "project", "")
1008
+ rate_info = kb.get_success_rate(project)
1009
+ rate_pct = f"{rate_info['rate']:.1%}"
1010
+ logger.info(f" 📊 成功率:{rate_pct}({rate_info['success']}/{rate_info['total']})")
1011
+ return {"status": "ok", **rate_info}
1012
+
1013
+ return {"status": "error", "message": "未知 kb 子命令"}
1014
+
1015
+
1016
+ # ─────────────────────────────────────────────
1017
+ # 子命令:autopilot(批量安装)
1018
+ # ─────────────────────────────────────────────
1019
+
1020
+ def cmd_autopilot(args) -> dict:
1021
+ """批量自动安装"""
1022
+ from autopilot import (
1023
+ parse_project_list, run_autopilot, resume_autopilot,
1024
+ format_batch_result,
1025
+ )
1026
+
1027
+ if getattr(args, "autopilot_action", "") == "resume":
1028
+ logger.info("🚗 恢复上次自动驾驶...")
1029
+ result = resume_autopilot(
1030
+ llm_force=getattr(args, "llm", None),
1031
+ install_dir=getattr(args, "dir", None),
1032
+ )
1033
+ if not result:
1034
+ return {"status": "error", "message": "没有可恢复的自动驾驶任务"}
1035
+ logger.info(format_batch_result(result))
1036
+ return {"status": "ok", **result.to_dict()}
1037
+
1038
+ # run
1039
+ source = getattr(args, "projects", "")
1040
+ if not source:
1041
+ return {"status": "error", "message": "请指定项目列表或文件"}
1042
+
1043
+ projects = parse_project_list(source)
1044
+ if not projects:
1045
+ return {"status": "error", "message": f"未能解析出有效项目:{source}"}
1046
+
1047
+ logger.info(f"🚗 自动驾驶模式:{len(projects)} 个项目")
1048
+ for i, p in enumerate(projects, 1):
1049
+ logger.info(f" {i}. {p}")
1050
+
1051
+ dry_run = getattr(args, "dry_run", False)
1052
+ if dry_run:
1053
+ return {
1054
+ "status": "ok",
1055
+ "dry_run": True,
1056
+ "projects": projects,
1057
+ "total": len(projects),
1058
+ }
1059
+
1060
+ result = run_autopilot(
1061
+ projects,
1062
+ install_dir=getattr(args, "dir", None),
1063
+ llm_force=getattr(args, "llm", None),
1064
+ )
1065
+ logger.info(format_batch_result(result))
1066
+ return {"status": "ok", **result.to_dict()}
1067
+
1068
+
1069
+ # ─────────────────────────────────────────────
1070
+ # 子命令:uninstall(安全卸载)
1071
+ # ─────────────────────────────────────────────
1072
+
1073
+ def cmd_uninstall(identifier: str, keep_config: bool = False,
1074
+ clean_only: bool = False, confirm: bool = False) -> dict:
1075
+ """安全卸载已安装的项目"""
1076
+ from auto_update import InstallTracker
1077
+ from uninstaller import (
1078
+ plan_uninstall, execute_uninstall,
1079
+ format_uninstall_plan, uninstall_to_dict,
1080
+ )
1081
+ from fetcher import parse_repo_identifier
1082
+
1083
+ try:
1084
+ owner, repo = parse_repo_identifier(identifier)
1085
+ except Exception as e:
1086
+ return {"status": "error", "message": f"无法解析项目: {e}"}
1087
+
1088
+ tracker = InstallTracker()
1089
+ project = tracker.get_project(owner, repo)
1090
+
1091
+ if not project:
1092
+ return {"status": "error", "message": f"未找到 {owner}/{repo} 的安装记录。使用 'gitinstall updates list' 查看已安装项目。"}
1093
+
1094
+ plan = plan_uninstall(
1095
+ owner, repo, project.install_dir,
1096
+ keep_config=keep_config, clean_only=clean_only,
1097
+ )
1098
+
1099
+ logger.info(format_uninstall_plan(plan))
1100
+
1101
+ if plan.error:
1102
+ return {"status": "error", **uninstall_to_dict(plan)}
1103
+
1104
+ if not confirm:
1105
+ logger.warning("\n ⚠️ 添加 --confirm 确认执行卸载")
1106
+ return {"status": "ok", "action": "dry_run", **uninstall_to_dict(plan)}
1107
+
1108
+ # 执行卸载
1109
+ result = execute_uninstall(plan, keep_config=keep_config)
1110
+ if result["success"]:
1111
+ tracker.remove_project(owner, repo)
1112
+ logger.info(f"\n ✅ 已卸载 {owner}/{repo},释放 {result['freed_mb']} MB")
1113
+ else:
1114
+ logger.warning(f"\n ⚠️ 部分清理失败: {result['errors']}")
1115
+
1116
+ return {"status": "ok" if result["success"] else "partial", **result}
1117
+
1118
+
1119
+ # ─────────────────────────────────────────────
1120
+ # CLI 入口
1121
+ # ─────────────────────────────────────────────
1122
+
1123
+ def main():
1124
+ import argparse
1125
+
1126
+ parser = argparse.ArgumentParser(
1127
+ prog="gitinstall",
1128
+ description="一句话安装任何开源项目(支持 GitHub/GitLab/Bitbucket/Gitee/Codeberg/本地路径)",
1129
+ formatter_class=argparse.RawDescriptionHelpFormatter,
1130
+ epilog="""
1131
+ 示例:
1132
+ python main.py web # 启动 Web 界面(推荐)
1133
+ python main.py detect
1134
+ python main.py plan comfyanonymous/ComfyUI
1135
+ python main.py install comfyanonymous/ComfyUI
1136
+ python main.py install gitlab.com/user/project # GitLab 项目
1137
+ python main.py install gitee.com/user/project # Gitee 国内项目
1138
+ python main.py install ./my-local-project # 本地目录(无需网络)
1139
+ python main.py install ~/projects/my-app # 本地绝对路径
1140
+ python main.py install comfyanonymous/ComfyUI --dir ~/AI --llm groq
1141
+ python main.py install comfyanonymous/ComfyUI --dry-run
1142
+ python main.py doctor # 系统诊断
1143
+ python main.py onboard # 交互式引导
1144
+ python main.py skills list # 查看已安装 Skills
1145
+ python main.py audit pytorch/pytorch # 依赖安全审计
1146
+ python main.py license torvalds/linux # 许可证检查
1147
+ python main.py updates check # 检查已安装项目更新
1148
+ python main.py uninstall owner/repo --confirm # 安全卸载项目
1149
+ python main.py validate # 验证 Top 100 兼容性
1150
+ python main.py validate --quick # 仅验证新入榜项目
1151
+ python main.py resume # 列出可恢复的安装
1152
+ python main.py flags list # 查看功能开关
1153
+ python main.py registry list # 查看安装器注册表
1154
+ python main.py events # 查看事件历史
1155
+ python main.py chain owner/repo # 可视化依赖链
1156
+ python main.py kb stats # 知识库统计
1157
+ python main.py autopilot run projects.txt # 批量自动安装
1158
+ """,
1159
+ )
1160
+
1161
+ subparsers = parser.add_subparsers(dest="command", required=True)
1162
+
1163
+ # detect
1164
+ subparsers.add_parser("detect", help="检测当前系统环境")
1165
+
1166
+ # fetch
1167
+ fetch_p = subparsers.add_parser("fetch", help="获取项目信息(支持多平台)")
1168
+ fetch_p.add_argument("project", help="URL / owner/repo(支持 GitHub/GitLab/Bitbucket/Gitee/Codeberg)")
1169
+
1170
+ # plan
1171
+ plan_p = subparsers.add_parser("plan", help="生成安装计划(不执行)")
1172
+ plan_p.add_argument("project", help="URL / owner/repo / 本地路径(支持多平台)")
1173
+ plan_p.add_argument("--llm", default=None, help="指定 LLM: anthropic/openai/groq/ollama/lmstudio/none")
1174
+ plan_p.add_argument("--local", action="store_true", help="本地模式:git clone 后本地分析(不消耗 API 配额)")
1175
+
1176
+ # install
1177
+ install_p = subparsers.add_parser("install", help="安装开源项目(支持多平台)")
1178
+ install_p.add_argument("project", help="URL / owner/repo / 本地路径(支持 GitHub/GitLab/Bitbucket/Gitee/Codeberg/本地目录)")
1179
+ install_p.add_argument("--dir", default=None, help="指定安装目录(默认 ~/项目名)")
1180
+ install_p.add_argument("--llm", default=None, help="指定 LLM: anthropic/openai/groq/ollama/lmstudio/none")
1181
+ install_p.add_argument("--local", action="store_true", help="本地模式:git clone 后本地分析(不消耗 API 配额)")
1182
+ install_p.add_argument("--dry-run", action="store_true", help="只生成计划,不执行")
1183
+
1184
+ # doctor
1185
+ doctor_p = subparsers.add_parser("doctor", help="🩺 系统诊断(检查环境、API、缓存、GPU)")
1186
+ doctor_p.add_argument("--json", action="store_true", dest="json_output", help="输出 JSON 格式")
1187
+
1188
+ # onboard
1189
+ subparsers.add_parser("onboard", help="🦀 交互式引导向导(首次使用推荐)")
1190
+
1191
+ # skills
1192
+ skills_p = subparsers.add_parser("skills", help="🔧 Skills 插件管理")
1193
+ skills_sub = skills_p.add_subparsers(dest="skills_action", required=True)
1194
+ skills_sub.add_parser("list", help="列出已安装的 Skills")
1195
+ skills_sub.add_parser("init", help="初始化内建 Skills")
1196
+ skills_create = skills_sub.add_parser("create", help="创建新 Skill")
1197
+ skills_create.add_argument("name", help="Skill 名称(小写字母+连字符)")
1198
+ skills_create.add_argument("--desc", required=True, help="Skill 描述")
1199
+ skills_remove = skills_sub.add_parser("remove", help="删除 Skill")
1200
+ skills_remove.add_argument("name", help="要删除的 Skill 名称")
1201
+ skills_export = skills_sub.add_parser("export", help="导出 Skill 为 JSON")
1202
+ skills_export.add_argument("name", help="要导出的 Skill 名称")
1203
+
1204
+ # config
1205
+ config_p = subparsers.add_parser("config", help="⚙️ 配置管理")
1206
+ config_sub = config_p.add_subparsers(dest="config_action", required=True)
1207
+ config_sub.add_parser("show", help="显示当前配置")
1208
+ config_sub.add_parser("validate", help="验证配置文件")
1209
+ config_sub.add_parser("path", help="显示配置文件路径")
1210
+
1211
+ # platforms
1212
+ subparsers.add_parser("platforms", help="📦 列出支持的代码托管平台")
1213
+
1214
+ # audit
1215
+ audit_p = subparsers.add_parser("audit", help="🔒 依赖安全审计(CVE/误植/废弃包)")
1216
+ audit_p.add_argument("project", help="URL / owner/repo")
1217
+ audit_p.add_argument("--online", action="store_true", help="查询在线漏洞数据库(更全面但较慢)")
1218
+
1219
+ # license
1220
+ license_p = subparsers.add_parser("license", help="📜 许可证兼容性检查")
1221
+ license_p.add_argument("project", help="URL / owner/repo")
1222
+
1223
+ # updates
1224
+ updates_p = subparsers.add_parser("updates", help="🔄 已安装项目更新管理")
1225
+ updates_sub = updates_p.add_subparsers(dest="updates_action", required=True)
1226
+ updates_sub.add_parser("list", help="列出已安装项目")
1227
+ updates_sub.add_parser("check", help="检查所有项目更新")
1228
+ updates_remove = updates_sub.add_parser("remove", help="移除安装记录")
1229
+ updates_remove.add_argument("name", help="owner/repo")
1230
+
1231
+ # uninstall
1232
+ uninstall_p = subparsers.add_parser("uninstall", help="🗑️ 安全卸载已安装项目")
1233
+ uninstall_p.add_argument("project", help="owner/repo")
1234
+ uninstall_p.add_argument("--keep-config", action="store_true", help="保留配置文件")
1235
+ uninstall_p.add_argument("--clean-only", action="store_true", help="仅清理缓存和编译产物")
1236
+ uninstall_p.add_argument("--confirm", action="store_true", help="确认执行卸载(否则仅预览)")
1237
+
1238
+ # mcp
1239
+ subparsers.add_parser("mcp", help="🤖 启动 MCP 服务器(供 Claude Desktop / Cursor 等 AI 工具调用)")
1240
+
1241
+ # schema
1242
+ schema_p = subparsers.add_parser("schema", help="📋 输出 AI 工具调用 Schema(OpenAI/Anthropic/Gemini/JSON)")
1243
+ schema_p.add_argument("--format", default="openai",
1244
+ choices=["openai", "anthropic", "gemini", "json_schema"],
1245
+ help="Schema 格式(默认: openai,兼容 Ollama/vLLM/LM Studio/任意 OpenAI 兼容 API)")
1246
+
1247
+ # web
1248
+ web_p = subparsers.add_parser("web", help="启动 Web 图形界面(推荐)")
1249
+ web_p.add_argument("--port", type=int, default=8080, help="端口号 (默认: 8080)")
1250
+ web_p.add_argument("--host", default="", help="绑定地址 (默认: 127.0.0.1,生产环境用 0.0.0.0)")
1251
+ web_p.add_argument("--no-open", action="store_true", help="不自动打开浏览器")
1252
+
1253
+ # validate
1254
+ val_p = subparsers.add_parser("validate", help="验证 Top 100 热门项目兼容性(内部 CI)")
1255
+ val_p.add_argument("--quick", action="store_true", help="仅验证新入榜项目")
1256
+ val_p.add_argument("--report", action="store_true", help="仅显示上次验证报告")
1257
+ val_p.add_argument("--category", default=None, help="按分类过滤: AI/Web/工具/IoT")
1258
+
1259
+ # resume
1260
+ resume_p = subparsers.add_parser("resume", help="🔄 恢复中断的安装(断点续装)")
1261
+ resume_p.add_argument("project", nargs="?", default=None, help="owner/repo(不指定则列出可恢复的)")
1262
+ resume_p.add_argument("--dir", default=None, help="安装目录")
1263
+ resume_p.add_argument("--llm", default=None, help="指定 LLM")
1264
+
1265
+ # flags
1266
+ flags_p = subparsers.add_parser("flags", help="🚩 功能开关管理")
1267
+ flags_sub = flags_p.add_subparsers(dest="flags_action", required=True)
1268
+ flags_list = flags_sub.add_parser("list", help="列出所有功能开关")
1269
+ flags_list.add_argument("--group", default=None, help="按组过滤: security/experimental/performance/general")
1270
+ flags_sub.add_parser("show", help="显示功能开关状态")
1271
+
1272
+ # registry
1273
+ registry_p = subparsers.add_parser("registry", help="🔧 安装器注册表")
1274
+ registry_sub = registry_p.add_subparsers(dest="registry_action", required=True)
1275
+ registry_sub.add_parser("list", help="列出所有安装器")
1276
+ registry_sub.add_parser("detect", help="检测可用安装器")
1277
+
1278
+ # events
1279
+ events_p = subparsers.add_parser("events", help="📡 安装事件历史")
1280
+ events_p.add_argument("--type", default=None, help="筛选事件类型")
1281
+ events_p.add_argument("--limit", type=int, default=50, help="最大条数")
1282
+
1283
+ # chain
1284
+ chain_p = subparsers.add_parser("chain", help="🔗 可视化安装依赖链")
1285
+ chain_p.add_argument("project", help="owner/repo")
1286
+ chain_p.add_argument("--llm", default=None, help="指定 LLM")
1287
+ chain_p.add_argument("--local", action="store_true", help="本地模式")
1288
+
1289
+ # kb
1290
+ kb_p = subparsers.add_parser("kb", help="📚 安装知识库")
1291
+ kb_sub = kb_p.add_subparsers(dest="kb_action", required=True)
1292
+ kb_sub.add_parser("stats", help="知识库统计")
1293
+ kb_search = kb_sub.add_parser("search", help="搜索相似安装案例")
1294
+ kb_search.add_argument("query", help="搜索关键词(project/type/language)")
1295
+ kb_rate = kb_sub.add_parser("rate", help="查看项目安装成功率")
1296
+ kb_rate.add_argument("project", nargs="?", default="", help="owner/repo(不指定则全局)")
1297
+
1298
+ # autopilot
1299
+ autopilot_p = subparsers.add_parser("autopilot", help="🚗 批量自动安装")
1300
+ autopilot_sub = autopilot_p.add_subparsers(dest="autopilot_action", required=True)
1301
+ ap_run = autopilot_sub.add_parser("run", help="执行批量安装")
1302
+ ap_run.add_argument("projects", help="项目列表(文件路径 / 空格分隔 owner/repo)")
1303
+ ap_run.add_argument("--dir", default=None, help="安装目录")
1304
+ ap_run.add_argument("--llm", default=None, help="指定 LLM")
1305
+ ap_run.add_argument("--dry-run", action="store_true", help="仅预览")
1306
+ autopilot_sub.add_parser("resume", help="恢复上次自动驾驶")
1307
+
1308
+ args = parser.parse_args()
1309
+
1310
+ # 路由到对应命令
1311
+ if args.command == "detect":
1312
+ result = cmd_detect()
1313
+ elif args.command == "fetch":
1314
+ result = cmd_fetch(args.project)
1315
+ elif args.command == "plan":
1316
+ result = cmd_plan(args.project, llm_force=args.llm, use_local=args.local)
1317
+ elif args.command == "install":
1318
+ result = cmd_install(
1319
+ args.project,
1320
+ install_dir=args.dir,
1321
+ llm_force=args.llm,
1322
+ dry_run=args.dry_run,
1323
+ use_local=args.local,
1324
+ )
1325
+ elif args.command == "doctor":
1326
+ result = cmd_doctor(json_output=args.json_output)
1327
+ elif args.command == "onboard":
1328
+ from onboard import run_onboard
1329
+ run_onboard()
1330
+ return
1331
+ elif args.command == "skills":
1332
+ result = cmd_skills(args)
1333
+ elif args.command == "config":
1334
+ result = cmd_config(args)
1335
+ elif args.command == "platforms":
1336
+ result = cmd_platforms()
1337
+ elif args.command == "audit":
1338
+ result = cmd_audit(args.project, online=args.online)
1339
+ elif args.command == "license":
1340
+ result = cmd_license(args.project)
1341
+ elif args.command == "updates":
1342
+ result = cmd_updates(args)
1343
+ elif args.command == "uninstall":
1344
+ result = cmd_uninstall(
1345
+ args.project,
1346
+ keep_config=args.keep_config,
1347
+ clean_only=args.clean_only,
1348
+ confirm=args.confirm,
1349
+ )
1350
+ elif args.command == "mcp":
1351
+ from mcp_server import serve
1352
+ serve()
1353
+ return
1354
+ elif args.command == "schema":
1355
+ from tool_schemas import to_json
1356
+ print(to_json(args.format))
1357
+ return
1358
+ elif args.command == "web":
1359
+ from web import start_server
1360
+ start_server(port=args.port, host=args.host, open_browser=not args.no_open)
1361
+ return
1362
+ elif args.command == "validate":
1363
+ from validate_top100 import cmd_validate
1364
+ result = cmd_validate(
1365
+ quick=args.quick,
1366
+ report_only=args.report,
1367
+ category=args.category,
1368
+ )
1369
+ elif args.command == "resume":
1370
+ result = cmd_resume(
1371
+ identifier=args.project,
1372
+ llm_force=args.llm,
1373
+ install_dir=args.dir,
1374
+ )
1375
+ elif args.command == "flags":
1376
+ result = cmd_flags(args)
1377
+ elif args.command == "registry":
1378
+ result = cmd_registry(args)
1379
+ elif args.command == "events":
1380
+ result = cmd_events(args)
1381
+ elif args.command == "chain":
1382
+ result = cmd_chain(args.project, llm_force=args.llm, use_local=args.local)
1383
+ elif args.command == "kb":
1384
+ result = cmd_kb(args)
1385
+ elif args.command == "autopilot":
1386
+ result = cmd_autopilot(args)
1387
+ else:
1388
+ parser.print_help()
1389
+ sys.exit(1)
1390
+
1391
+ # 输出 JSON(OpenClaw 读取 stdout)
1392
+ _output_and_exit(result)
1393
+
1394
+
1395
+ def _output_and_exit(result: dict):
1396
+ """CLI 专用:输出 JSON 并根据状态设置退出码。SDK 不调用此函数。"""
1397
+ import json as _json
1398
+ print(_json.dumps(result, ensure_ascii=False, indent=2))
1399
+ if result.get("status") == "error":
1400
+ sys.exit(1)
1401
+
1402
+
1403
+ # pyproject.toml 入口别名
1404
+ cli_main = main
1405
+
1406
+
1407
+ if __name__ == "__main__":
1408
+ main()