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.
- gitinstall/__init__.py +61 -0
- gitinstall/_sdk.py +541 -0
- gitinstall/academic.py +831 -0
- gitinstall/admin.html +327 -0
- gitinstall/auto_update.py +384 -0
- gitinstall/autopilot.py +349 -0
- gitinstall/badge.py +476 -0
- gitinstall/checkpoint.py +330 -0
- gitinstall/cicd.py +499 -0
- gitinstall/clawhub.html +718 -0
- gitinstall/config_schema.py +353 -0
- gitinstall/db.py +984 -0
- gitinstall/db_backend.py +445 -0
- gitinstall/dep_chain.py +337 -0
- gitinstall/dependency_audit.py +1153 -0
- gitinstall/detector.py +542 -0
- gitinstall/doctor.py +493 -0
- gitinstall/education.py +869 -0
- gitinstall/enterprise.py +802 -0
- gitinstall/error_fixer.py +953 -0
- gitinstall/event_bus.py +251 -0
- gitinstall/executor.py +577 -0
- gitinstall/feature_flags.py +138 -0
- gitinstall/fetcher.py +921 -0
- gitinstall/huggingface.py +922 -0
- gitinstall/hw_detect.py +988 -0
- gitinstall/i18n.py +664 -0
- gitinstall/installer_registry.py +362 -0
- gitinstall/knowledge_base.py +379 -0
- gitinstall/license_check.py +605 -0
- gitinstall/llm.py +569 -0
- gitinstall/log.py +236 -0
- gitinstall/main.py +1408 -0
- gitinstall/mcp_agent.py +841 -0
- gitinstall/mcp_server.py +386 -0
- gitinstall/monorepo.py +810 -0
- gitinstall/multi_source.py +425 -0
- gitinstall/onboard.py +276 -0
- gitinstall/planner.py +222 -0
- gitinstall/planner_helpers.py +323 -0
- gitinstall/planner_known_projects.py +1010 -0
- gitinstall/planner_templates.py +996 -0
- gitinstall/remote_gpu.py +633 -0
- gitinstall/resilience.py +608 -0
- gitinstall/run_tests.py +572 -0
- gitinstall/skills.py +476 -0
- gitinstall/tool_schemas.py +324 -0
- gitinstall/trending.py +279 -0
- gitinstall/uninstaller.py +415 -0
- gitinstall/validate_top100.py +607 -0
- gitinstall/watchdog.py +180 -0
- gitinstall/web.py +1277 -0
- gitinstall/web_ui.html +2277 -0
- gitinstall-1.1.0.dist-info/METADATA +275 -0
- gitinstall-1.1.0.dist-info/RECORD +59 -0
- gitinstall-1.1.0.dist-info/WHEEL +5 -0
- gitinstall-1.1.0.dist-info/entry_points.txt +3 -0
- gitinstall-1.1.0.dist-info/licenses/LICENSE +21 -0
- 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
|
+
}
|