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
@@ -0,0 +1,415 @@
1
+ """
2
+ uninstaller.py - 项目安全卸载与清理
3
+ =====================================
4
+
5
+ 安全地卸载已安装的 GitHub 项目:
6
+ 1. 检测项目安装的文件和依赖
7
+ 2. 清理 virtualenv / Docker 容器 / 编译产物
8
+ 3. 从安装记录中移除
9
+ 4. 可选:保留配置文件(不删用户数据)
10
+
11
+ 安全优先:
12
+ - 不删除 home 目录之外的文件(除非用户确认)
13
+ - 不删除非 gitinstall 安装的目录
14
+ - 删除前显示完整清理计划
15
+
16
+ 零外部依赖,纯 Python 标准库。
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import json
22
+ import os
23
+ import shutil
24
+ import subprocess
25
+ from dataclasses import dataclass, field
26
+ from pathlib import Path
27
+ from typing import Optional
28
+
29
+
30
+ @dataclass
31
+ class CleanupItem:
32
+ """待清理项"""
33
+ path: str
34
+ item_type: str # directory, file, venv, docker, cache
35
+ size_bytes: int = 0
36
+ description: str = ""
37
+ safe: bool = True # 是否安全删除
38
+
39
+
40
+ @dataclass
41
+ class UninstallPlan:
42
+ """卸载计划"""
43
+ owner: str
44
+ repo: str
45
+ install_dir: str
46
+ items: list[CleanupItem] = field(default_factory=list)
47
+ total_size: int = 0
48
+ warnings: list[str] = field(default_factory=list)
49
+ error: str = ""
50
+
51
+ @property
52
+ def total_size_mb(self) -> float:
53
+ return self.total_size / (1024 * 1024)
54
+
55
+
56
+ # ── 安全路径白名单 ──
57
+ SAFE_BASES = [
58
+ Path.home(),
59
+ Path("/tmp"),
60
+ ]
61
+
62
+
63
+ def _is_safe_path(path: Path) -> bool:
64
+ """检查路径是否在安全范围内"""
65
+ resolved = path.resolve()
66
+ # 不能删除 home 目录本身
67
+ if resolved == Path.home():
68
+ return False
69
+ # 不能删除系统关键目录
70
+ danger_paths = ["/", "/usr", "/bin", "/etc", "/var", "/System", "/Library",
71
+ "/opt", "/Applications", "/sbin"]
72
+ if str(resolved) in danger_paths:
73
+ return False
74
+ # 必须在白名单目录下
75
+ return any(
76
+ str(resolved).startswith(str(base.resolve()))
77
+ for base in SAFE_BASES
78
+ )
79
+
80
+
81
+ def _dir_size(path: Path) -> int:
82
+ """计算目录大小"""
83
+ total = 0
84
+ try:
85
+ for entry in path.rglob("*"):
86
+ if entry.is_file():
87
+ try:
88
+ total += entry.stat().st_size
89
+ except OSError:
90
+ pass
91
+ except (PermissionError, OSError):
92
+ pass
93
+ return total
94
+
95
+
96
+ def _find_venvs(install_dir: Path) -> list[Path]:
97
+ """查找 virtualenv 目录"""
98
+ venvs = []
99
+ for name in ("venv", ".venv", "env", ".env", ".conda"):
100
+ venv_dir = install_dir / name
101
+ if venv_dir.is_dir() and (
102
+ (venv_dir / "bin" / "python").exists() or
103
+ (venv_dir / "bin" / "activate").exists() or
104
+ (venv_dir / "pyvenv.cfg").exists() or
105
+ (venv_dir / "conda-meta").is_dir()
106
+ ):
107
+ venvs.append(venv_dir)
108
+ return venvs
109
+
110
+
111
+ def _find_docker_artifacts(install_dir: Path) -> list[str]:
112
+ """查找 Docker 相关产物(容器/镜像名称)"""
113
+ artifacts = []
114
+ compose_files = ["docker-compose.yml", "docker-compose.yaml",
115
+ "compose.yml", "compose.yaml"]
116
+ for cf in compose_files:
117
+ if (install_dir / cf).exists():
118
+ # 使用目录名作为项目名
119
+ project_name = install_dir.name.lower().replace(" ", "")
120
+ artifacts.append(f"docker-compose:{project_name}")
121
+ return artifacts
122
+
123
+
124
+ def _find_cache(owner: str, repo: str) -> list[Path]:
125
+ """查找缓存文件"""
126
+ caches = []
127
+ cache_base = Path.home() / ".cache" / "gitinstall"
128
+ if cache_base.exists():
129
+ for p in cache_base.iterdir():
130
+ if repo.lower() in p.name.lower() or f"{owner}_{repo}" in p.name:
131
+ caches.append(p)
132
+ return caches
133
+
134
+
135
+ def _find_build_artifacts(install_dir: Path) -> list[Path]:
136
+ """查找编译产物"""
137
+ artifacts = []
138
+ build_dirs = [
139
+ "build", "dist", "__pycache__", ".eggs", "*.egg-info",
140
+ "node_modules", "target", ".next", ".nuxt",
141
+ ".gradle", ".mvn", "bin", "obj",
142
+ ]
143
+ for name in build_dirs:
144
+ if "*" in name:
145
+ for p in install_dir.glob(name):
146
+ if p.is_dir():
147
+ artifacts.append(p)
148
+ else:
149
+ d = install_dir / name
150
+ if d.is_dir():
151
+ artifacts.append(d)
152
+ return artifacts
153
+
154
+
155
+ # ─────────────────────────────────────────────
156
+ # 卸载计划生成
157
+ # ─────────────────────────────────────────────
158
+
159
+ def plan_uninstall(
160
+ owner: str,
161
+ repo: str,
162
+ install_dir: str,
163
+ keep_config: bool = False,
164
+ clean_only: bool = False,
165
+ ) -> UninstallPlan:
166
+ """
167
+ 生成卸载计划(不执行)。
168
+
169
+ Args:
170
+ owner: GitHub owner
171
+ repo: GitHub repo
172
+ install_dir: 安装目录
173
+ keep_config: 是否保留配置文件
174
+ clean_only: 仅清理缓存和编译产物(不删主目录)
175
+ """
176
+ plan = UninstallPlan(owner=owner, repo=repo, install_dir=install_dir)
177
+ install_path = Path(install_dir)
178
+
179
+ if not install_path.exists():
180
+ plan.error = f"目录不存在: {install_dir}"
181
+ return plan
182
+
183
+ if not _is_safe_path(install_path):
184
+ plan.error = f"安全检查失败:{install_dir} 不在安全范围内"
185
+ plan.warnings.append("仅允许删除 HOME 目录下的安装")
186
+ return plan
187
+
188
+ # 1. 查找 virtualenv
189
+ for venv in _find_venvs(install_path):
190
+ size = _dir_size(venv)
191
+ plan.items.append(CleanupItem(
192
+ path=str(venv),
193
+ item_type="venv",
194
+ size_bytes=size,
195
+ description=f"Python 虚拟环境: {venv.name}",
196
+ ))
197
+ plan.total_size += size
198
+
199
+ # 2. 查找 Docker 产物
200
+ for docker_ref in _find_docker_artifacts(install_path):
201
+ plan.items.append(CleanupItem(
202
+ path=docker_ref,
203
+ item_type="docker",
204
+ description=f"Docker 容器/网络: {docker_ref}",
205
+ ))
206
+
207
+ # 3. 查找编译产物
208
+ for build_dir in _find_build_artifacts(install_path):
209
+ size = _dir_size(build_dir)
210
+ plan.items.append(CleanupItem(
211
+ path=str(build_dir),
212
+ item_type="cache",
213
+ size_bytes=size,
214
+ description=f"编译产物: {build_dir.name}",
215
+ ))
216
+ plan.total_size += size
217
+
218
+ # 4. 查找缓存
219
+ for cache_path in _find_cache(owner, repo):
220
+ size = _dir_size(cache_path) if cache_path.is_dir() else cache_path.stat().st_size
221
+ plan.items.append(CleanupItem(
222
+ path=str(cache_path),
223
+ item_type="cache",
224
+ size_bytes=size,
225
+ description=f"缓存: {cache_path.name}",
226
+ ))
227
+ plan.total_size += size
228
+
229
+ # 5. 主目录(如果不是仅清理模式)
230
+ if not clean_only:
231
+ size = _dir_size(install_path)
232
+ # 减去已列出的子目录大小避免重复计算
233
+ sub_sizes = sum(item.size_bytes for item in plan.items
234
+ if str(item.path).startswith(str(install_path)))
235
+ main_size = max(0, size - sub_sizes)
236
+
237
+ plan.items.append(CleanupItem(
238
+ path=str(install_path),
239
+ item_type="directory",
240
+ size_bytes=main_size,
241
+ description=f"项目主目录: {install_path.name}",
242
+ ))
243
+ plan.total_size = size # 用目录总大小
244
+
245
+ # 6. 警告
246
+ if (install_path / ".git").exists():
247
+ plan.warnings.append("目录包含 .git 仓库,删除后无法恢复本地修改")
248
+
249
+ user_data_patterns = [".env", "config.local.*", "*.db", "*.sqlite", "data/"]
250
+ for pattern in user_data_patterns:
251
+ matches = list(install_path.glob(pattern))
252
+ if matches:
253
+ names = [m.name for m in matches[:3]]
254
+ plan.warnings.append(f"检测到可能的用户数据: {', '.join(names)}")
255
+ if keep_config:
256
+ plan.warnings.append("--keep-config 已启用,将保留这些文件")
257
+
258
+ return plan
259
+
260
+
261
+ def execute_uninstall(plan: UninstallPlan, keep_config: bool = False) -> dict:
262
+ """
263
+ 执行卸载计划。
264
+
265
+ Returns:
266
+ {"success": bool, "removed": [...], "errors": [...]}
267
+ """
268
+ removed = []
269
+ errors = []
270
+
271
+ if plan.error:
272
+ return {"success": False, "removed": [], "errors": [plan.error]}
273
+
274
+ install_path = Path(plan.install_dir)
275
+
276
+ # 配置文件模式
277
+ config_patterns = {".env", "config.local", ".gitinstall.json"}
278
+
279
+ for item in plan.items:
280
+ path = Path(item.path)
281
+
282
+ try:
283
+ if item.item_type == "docker":
284
+ # Docker 停止容器
285
+ project_name = item.path.split(":")[-1] if ":" in item.path else ""
286
+ if project_name:
287
+ try:
288
+ subprocess.run(
289
+ ["docker", "compose", "-p", project_name, "down", "--remove-orphans"],
290
+ capture_output=True, timeout=30,
291
+ cwd=str(install_path) if install_path.exists() else None,
292
+ )
293
+ removed.append(f"Docker: {project_name}")
294
+ except (subprocess.TimeoutExpired, FileNotFoundError):
295
+ errors.append(f"Docker 清理失败: {project_name}")
296
+ continue
297
+
298
+ if not path.exists():
299
+ continue
300
+
301
+ if not _is_safe_path(path):
302
+ errors.append(f"跳过不安全路径: {path}")
303
+ continue
304
+
305
+ # keep_config 模式:跳过配置文件
306
+ if keep_config and item.item_type == "directory":
307
+ # 保留配置文件,仅删除其他内容
308
+ _remove_dir_except_configs(path, config_patterns)
309
+ removed.append(f"清理(保留配置): {path}")
310
+ continue
311
+
312
+ if path.is_dir():
313
+ shutil.rmtree(path)
314
+ else:
315
+ path.unlink()
316
+ removed.append(str(path))
317
+
318
+ except (PermissionError, OSError) as e:
319
+ errors.append(f"{path}: {e}")
320
+
321
+ return {
322
+ "success": len(errors) == 0,
323
+ "removed": removed,
324
+ "errors": errors,
325
+ "freed_mb": round(plan.total_size_mb, 1),
326
+ }
327
+
328
+
329
+ def _remove_dir_except_configs(path: Path, config_patterns: set[str]):
330
+ """删除目录内容但保留配置文件"""
331
+ for child in path.iterdir():
332
+ if child.name in config_patterns:
333
+ continue
334
+ if child.name.startswith(".env"):
335
+ continue
336
+ try:
337
+ if child.is_dir():
338
+ shutil.rmtree(child)
339
+ else:
340
+ child.unlink()
341
+ except (PermissionError, OSError):
342
+ pass
343
+
344
+
345
+ # ─────────────────────────────────────────────
346
+ # 格式化输出
347
+ # ─────────────────────────────────────────────
348
+
349
+ def _size_str(size_bytes: int) -> str:
350
+ """格式化文件大小"""
351
+ if size_bytes < 1024:
352
+ return f"{size_bytes} B"
353
+ if size_bytes < 1024 * 1024:
354
+ return f"{size_bytes / 1024:.1f} KB"
355
+ if size_bytes < 1024 * 1024 * 1024:
356
+ return f"{size_bytes / (1024 * 1024):.1f} MB"
357
+ return f"{size_bytes / (1024 * 1024 * 1024):.2f} GB"
358
+
359
+
360
+ _TYPE_ICONS = {
361
+ "directory": "📂",
362
+ "venv": "🐍",
363
+ "docker": "🐳",
364
+ "cache": "🗂️ ",
365
+ "file": "📄",
366
+ }
367
+
368
+
369
+ def format_uninstall_plan(plan: UninstallPlan) -> str:
370
+ """格式化卸载计划"""
371
+ lines = ["", f"🗑️ 卸载计划: {plan.owner}/{plan.repo}", "=" * 50]
372
+
373
+ if plan.error:
374
+ lines.append(f" ❌ {plan.error}")
375
+ return "\n".join(lines)
376
+
377
+ if not plan.items:
378
+ lines.append(" (无需清理的内容)")
379
+ return "\n".join(lines)
380
+
381
+ for item in plan.items:
382
+ icon = _TYPE_ICONS.get(item.item_type, "📦")
383
+ size = f" ({_size_str(item.size_bytes)})" if item.size_bytes > 0 else ""
384
+ lines.append(f" {icon} {item.description}{size}")
385
+ lines.append(f" {item.path}")
386
+
387
+ lines.append(f"\n 📊 总计释放空间: {_size_str(plan.total_size)}")
388
+
389
+ if plan.warnings:
390
+ lines.append("\n ⚠️ 警告:")
391
+ for w in plan.warnings:
392
+ lines.append(f" {w}")
393
+
394
+ return "\n".join(lines)
395
+
396
+
397
+ def uninstall_to_dict(plan: UninstallPlan) -> dict:
398
+ """序列化卸载计划为 JSON"""
399
+ return {
400
+ "owner": plan.owner,
401
+ "repo": plan.repo,
402
+ "install_dir": plan.install_dir,
403
+ "items": [
404
+ {
405
+ "path": item.path,
406
+ "type": item.item_type,
407
+ "size_bytes": item.size_bytes,
408
+ "description": item.description,
409
+ }
410
+ for item in plan.items
411
+ ],
412
+ "total_size_mb": round(plan.total_size_mb, 1),
413
+ "warnings": plan.warnings,
414
+ "error": plan.error,
415
+ }