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,384 @@
|
|
|
1
|
+
"""
|
|
2
|
+
auto_update.py - 已安装项目自动更新追踪
|
|
3
|
+
==========================================
|
|
4
|
+
|
|
5
|
+
追踪已安装的 GitHub 项目,检测新版本:
|
|
6
|
+
1. 记录每个项目的安装版本(commit SHA / tag / release)
|
|
7
|
+
2. 检查 GitHub 是否有新 commit / release
|
|
8
|
+
3. 显示更新摘要(新增功能 / 修复 / breaking changes)
|
|
9
|
+
4. 一键更新已安装项目
|
|
10
|
+
|
|
11
|
+
数据存储:~/.gitinstall/installed.json
|
|
12
|
+
|
|
13
|
+
零外部依赖,纯 Python 标准库。
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import time
|
|
21
|
+
import urllib.error
|
|
22
|
+
import urllib.request
|
|
23
|
+
from dataclasses import dataclass, field
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Optional
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ── 数据路径 ──
|
|
29
|
+
DATA_DIR = Path.home() / ".gitinstall"
|
|
30
|
+
INSTALLED_FILE = DATA_DIR / "installed.json"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class InstalledProject:
|
|
35
|
+
"""已安装项目记录"""
|
|
36
|
+
owner: str
|
|
37
|
+
repo: str
|
|
38
|
+
install_dir: str
|
|
39
|
+
installed_at: str # ISO timestamp
|
|
40
|
+
installed_commit: str = "" # commit SHA
|
|
41
|
+
installed_tag: str = "" # tag / release version
|
|
42
|
+
installed_branch: str = "main"
|
|
43
|
+
last_check: str = "" # 最后检查时间
|
|
44
|
+
auto_update: bool = False # 是否自动更新
|
|
45
|
+
notes: str = ""
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def full_name(self) -> str:
|
|
49
|
+
return f"{self.owner}/{self.repo}"
|
|
50
|
+
|
|
51
|
+
def to_dict(self) -> dict:
|
|
52
|
+
return {
|
|
53
|
+
"owner": self.owner,
|
|
54
|
+
"repo": self.repo,
|
|
55
|
+
"install_dir": self.install_dir,
|
|
56
|
+
"installed_at": self.installed_at,
|
|
57
|
+
"installed_commit": self.installed_commit,
|
|
58
|
+
"installed_tag": self.installed_tag,
|
|
59
|
+
"installed_branch": self.installed_branch,
|
|
60
|
+
"last_check": self.last_check,
|
|
61
|
+
"auto_update": self.auto_update,
|
|
62
|
+
"notes": self.notes,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def from_dict(cls, d: dict) -> "InstalledProject":
|
|
67
|
+
return cls(
|
|
68
|
+
owner=str(d.get("owner", "")),
|
|
69
|
+
repo=str(d.get("repo", "")),
|
|
70
|
+
install_dir=str(d.get("install_dir", "")),
|
|
71
|
+
installed_at=str(d.get("installed_at", "")),
|
|
72
|
+
installed_commit=str(d.get("installed_commit", "")),
|
|
73
|
+
installed_tag=str(d.get("installed_tag", "")),
|
|
74
|
+
installed_branch=str(d.get("installed_branch", "main")),
|
|
75
|
+
last_check=str(d.get("last_check", "")),
|
|
76
|
+
auto_update=bool(d.get("auto_update", False)),
|
|
77
|
+
notes=str(d.get("notes", "")),
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass
|
|
82
|
+
class UpdateInfo:
|
|
83
|
+
"""更新信息"""
|
|
84
|
+
owner: str
|
|
85
|
+
repo: str
|
|
86
|
+
has_update: bool = False
|
|
87
|
+
current_commit: str = ""
|
|
88
|
+
latest_commit: str = ""
|
|
89
|
+
current_tag: str = ""
|
|
90
|
+
latest_tag: str = ""
|
|
91
|
+
commits_behind: int = 0
|
|
92
|
+
latest_release_name: str = ""
|
|
93
|
+
latest_release_body: str = ""
|
|
94
|
+
latest_release_date: str = ""
|
|
95
|
+
error: str = ""
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# ─────────────────────────────────────────────
|
|
99
|
+
# 安装记录管理
|
|
100
|
+
# ─────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
class InstallTracker:
|
|
103
|
+
"""管理已安装项目记录"""
|
|
104
|
+
|
|
105
|
+
def __init__(self, data_file: Path = None):
|
|
106
|
+
self.data_file = data_file or INSTALLED_FILE
|
|
107
|
+
|
|
108
|
+
def _ensure_dir(self):
|
|
109
|
+
self.data_file.parent.mkdir(parents=True, exist_ok=True)
|
|
110
|
+
|
|
111
|
+
def _load(self) -> list[dict]:
|
|
112
|
+
if not self.data_file.exists():
|
|
113
|
+
return []
|
|
114
|
+
try:
|
|
115
|
+
with open(self.data_file, encoding="utf-8") as f:
|
|
116
|
+
data = json.load(f)
|
|
117
|
+
return data if isinstance(data, list) else []
|
|
118
|
+
except (json.JSONDecodeError, OSError):
|
|
119
|
+
return []
|
|
120
|
+
|
|
121
|
+
def _save(self, records: list[dict]):
|
|
122
|
+
self._ensure_dir()
|
|
123
|
+
with open(self.data_file, "w", encoding="utf-8") as f:
|
|
124
|
+
json.dump(records, f, indent=2, ensure_ascii=False)
|
|
125
|
+
# 安全权限
|
|
126
|
+
try:
|
|
127
|
+
os.chmod(self.data_file, 0o600)
|
|
128
|
+
except OSError:
|
|
129
|
+
pass
|
|
130
|
+
|
|
131
|
+
def record_install(
|
|
132
|
+
self,
|
|
133
|
+
owner: str,
|
|
134
|
+
repo: str,
|
|
135
|
+
install_dir: str,
|
|
136
|
+
commit: str = "",
|
|
137
|
+
tag: str = "",
|
|
138
|
+
branch: str = "main",
|
|
139
|
+
) -> InstalledProject:
|
|
140
|
+
"""记录一次安装"""
|
|
141
|
+
records = self._load()
|
|
142
|
+
|
|
143
|
+
# 检查是否已存在
|
|
144
|
+
full_name = f"{owner}/{repo}".lower()
|
|
145
|
+
records = [r for r in records if f"{r.get('owner','')}/{r.get('repo','')}".lower() != full_name]
|
|
146
|
+
|
|
147
|
+
project = InstalledProject(
|
|
148
|
+
owner=owner,
|
|
149
|
+
repo=repo,
|
|
150
|
+
install_dir=install_dir,
|
|
151
|
+
installed_at=time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
|
152
|
+
installed_commit=commit,
|
|
153
|
+
installed_tag=tag,
|
|
154
|
+
installed_branch=branch,
|
|
155
|
+
)
|
|
156
|
+
records.append(project.to_dict())
|
|
157
|
+
self._save(records)
|
|
158
|
+
return project
|
|
159
|
+
|
|
160
|
+
def list_installed(self) -> list[InstalledProject]:
|
|
161
|
+
"""列出所有已安装项目"""
|
|
162
|
+
return [InstalledProject.from_dict(r) for r in self._load()]
|
|
163
|
+
|
|
164
|
+
def get_project(self, owner: str, repo: str) -> Optional[InstalledProject]:
|
|
165
|
+
"""获取特定项目的安装记录"""
|
|
166
|
+
full_name = f"{owner}/{repo}".lower()
|
|
167
|
+
for r in self._load():
|
|
168
|
+
if f"{r.get('owner','')}/{r.get('repo','')}".lower() == full_name:
|
|
169
|
+
return InstalledProject.from_dict(r)
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
def remove_project(self, owner: str, repo: str) -> bool:
|
|
173
|
+
"""删除安装记录"""
|
|
174
|
+
records = self._load()
|
|
175
|
+
full_name = f"{owner}/{repo}".lower()
|
|
176
|
+
new_records = [r for r in records if f"{r.get('owner','')}/{r.get('repo','')}".lower() != full_name]
|
|
177
|
+
if len(new_records) == len(records):
|
|
178
|
+
return False
|
|
179
|
+
self._save(new_records)
|
|
180
|
+
return True
|
|
181
|
+
|
|
182
|
+
def update_check_time(self, owner: str, repo: str):
|
|
183
|
+
"""更新最后检查时间"""
|
|
184
|
+
records = self._load()
|
|
185
|
+
full_name = f"{owner}/{repo}".lower()
|
|
186
|
+
for r in records:
|
|
187
|
+
if f"{r.get('owner','')}/{r.get('repo','')}".lower() == full_name:
|
|
188
|
+
r["last_check"] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
|
189
|
+
break
|
|
190
|
+
self._save(records)
|
|
191
|
+
|
|
192
|
+
def set_auto_update(self, owner: str, repo: str, enabled: bool) -> bool:
|
|
193
|
+
"""设置自动更新开关"""
|
|
194
|
+
records = self._load()
|
|
195
|
+
full_name = f"{owner}/{repo}".lower()
|
|
196
|
+
found = False
|
|
197
|
+
for r in records:
|
|
198
|
+
if f"{r.get('owner','')}/{r.get('repo','')}".lower() == full_name:
|
|
199
|
+
r["auto_update"] = enabled
|
|
200
|
+
found = True
|
|
201
|
+
break
|
|
202
|
+
if found:
|
|
203
|
+
self._save(records)
|
|
204
|
+
return found
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
# ─────────────────────────────────────────────
|
|
208
|
+
# GitHub API 更新检查
|
|
209
|
+
# ─────────────────────────────────────────────
|
|
210
|
+
|
|
211
|
+
def _github_headers() -> dict:
|
|
212
|
+
headers = {"User-Agent": "gitinstall/1.0", "Accept": "application/json"}
|
|
213
|
+
token = os.getenv("GITHUB_TOKEN", "")
|
|
214
|
+
if token:
|
|
215
|
+
headers["Authorization"] = f"token {token}"
|
|
216
|
+
return headers
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def check_for_update(project: InstalledProject) -> UpdateInfo:
|
|
220
|
+
"""
|
|
221
|
+
检查项目是否有新版本。
|
|
222
|
+
|
|
223
|
+
比较:
|
|
224
|
+
1. 最新 commit vs 安装时的 commit
|
|
225
|
+
2. 最新 release / tag vs 安装时的 tag
|
|
226
|
+
"""
|
|
227
|
+
info = UpdateInfo(
|
|
228
|
+
owner=project.owner,
|
|
229
|
+
repo=project.repo,
|
|
230
|
+
current_commit=project.installed_commit,
|
|
231
|
+
current_tag=project.installed_tag,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
headers = _github_headers()
|
|
235
|
+
|
|
236
|
+
# 检查最新 commit
|
|
237
|
+
try:
|
|
238
|
+
branch = project.installed_branch or "main"
|
|
239
|
+
url = f"https://api.github.com/repos/{project.owner}/{project.repo}/commits/{branch}"
|
|
240
|
+
req = urllib.request.Request(url, headers=headers)
|
|
241
|
+
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
242
|
+
data = json.loads(resp.read())
|
|
243
|
+
info.latest_commit = data.get("sha", "")[:12]
|
|
244
|
+
if info.current_commit and info.latest_commit:
|
|
245
|
+
if not info.latest_commit.startswith(info.current_commit[:7]):
|
|
246
|
+
info.has_update = True
|
|
247
|
+
except (urllib.error.URLError, OSError, json.JSONDecodeError):
|
|
248
|
+
info.error = "无法连接 GitHub API"
|
|
249
|
+
return info
|
|
250
|
+
|
|
251
|
+
# 检查最新 release
|
|
252
|
+
try:
|
|
253
|
+
url = f"https://api.github.com/repos/{project.owner}/{project.repo}/releases/latest"
|
|
254
|
+
req = urllib.request.Request(url, headers=headers)
|
|
255
|
+
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
256
|
+
data = json.loads(resp.read())
|
|
257
|
+
info.latest_tag = data.get("tag_name", "")
|
|
258
|
+
info.latest_release_name = data.get("name", "")
|
|
259
|
+
info.latest_release_body = data.get("body", "")[:500]
|
|
260
|
+
info.latest_release_date = data.get("published_at", "")
|
|
261
|
+
|
|
262
|
+
if info.current_tag and info.latest_tag:
|
|
263
|
+
if info.current_tag != info.latest_tag:
|
|
264
|
+
info.has_update = True
|
|
265
|
+
except urllib.error.HTTPError as e:
|
|
266
|
+
if e.code != 404: # 404 = 没有 release,正常
|
|
267
|
+
info.error = f"API 错误: {e.code}"
|
|
268
|
+
except (urllib.error.URLError, OSError, json.JSONDecodeError):
|
|
269
|
+
pass
|
|
270
|
+
|
|
271
|
+
# 对比 commits behind(如果有安装 commit)
|
|
272
|
+
if project.installed_commit and info.latest_commit:
|
|
273
|
+
try:
|
|
274
|
+
url = (
|
|
275
|
+
f"https://api.github.com/repos/{project.owner}/{project.repo}"
|
|
276
|
+
f"/compare/{project.installed_commit}...{info.latest_commit}"
|
|
277
|
+
)
|
|
278
|
+
req = urllib.request.Request(url, headers=headers)
|
|
279
|
+
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
280
|
+
data = json.loads(resp.read())
|
|
281
|
+
info.commits_behind = data.get("ahead_by", 0)
|
|
282
|
+
if info.commits_behind > 0:
|
|
283
|
+
info.has_update = True
|
|
284
|
+
except (urllib.error.URLError, OSError, json.JSONDecodeError):
|
|
285
|
+
pass
|
|
286
|
+
|
|
287
|
+
return info
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def check_all_updates(tracker: InstallTracker = None) -> list[UpdateInfo]:
|
|
291
|
+
"""检查所有已安装项目的更新"""
|
|
292
|
+
tracker = tracker or InstallTracker()
|
|
293
|
+
results = []
|
|
294
|
+
for project in tracker.list_installed():
|
|
295
|
+
info = check_for_update(project)
|
|
296
|
+
tracker.update_check_time(project.owner, project.repo)
|
|
297
|
+
results.append(info)
|
|
298
|
+
return results
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
# ─────────────────────────────────────────────
|
|
302
|
+
# 格式化输出
|
|
303
|
+
# ─────────────────────────────────────────────
|
|
304
|
+
|
|
305
|
+
def format_installed_list(projects: list[InstalledProject]) -> str:
|
|
306
|
+
"""格式化已安装项目列表"""
|
|
307
|
+
if not projects:
|
|
308
|
+
return " (未记录任何安装项目)\n 安装项目后会自动记录到此列表"
|
|
309
|
+
|
|
310
|
+
lines = ["", "📦 已安装项目", "=" * 50]
|
|
311
|
+
for p in projects:
|
|
312
|
+
auto = "🔄" if p.auto_update else " "
|
|
313
|
+
tag = f" @ {p.installed_tag}" if p.installed_tag else ""
|
|
314
|
+
commit = f" [{p.installed_commit[:7]}]" if p.installed_commit else ""
|
|
315
|
+
lines.append(f" {auto} {p.full_name}{tag}{commit}")
|
|
316
|
+
lines.append(f" 📂 {p.install_dir}")
|
|
317
|
+
lines.append(f" ⏰ 安装于 {p.installed_at[:10]}")
|
|
318
|
+
if p.last_check:
|
|
319
|
+
lines.append(f" 🔍 最后检查 {p.last_check[:10]}")
|
|
320
|
+
lines.append(f"\n 共 {len(projects)} 个项目")
|
|
321
|
+
return "\n".join(lines)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def format_update_results(updates: list[UpdateInfo]) -> str:
|
|
325
|
+
"""格式化更新检查结果"""
|
|
326
|
+
if not updates:
|
|
327
|
+
return " (没有可检查的项目)"
|
|
328
|
+
|
|
329
|
+
lines = ["", "🔄 更新检查结果", "=" * 50]
|
|
330
|
+
|
|
331
|
+
available = [u for u in updates if u.has_update]
|
|
332
|
+
up_to_date = [u for u in updates if not u.has_update and not u.error]
|
|
333
|
+
errors = [u for u in updates if u.error]
|
|
334
|
+
|
|
335
|
+
if available:
|
|
336
|
+
lines.append(f"\n📢 有 {len(available)} 个项目可更新:")
|
|
337
|
+
for u in available:
|
|
338
|
+
lines.append(f" 🆕 {u.owner}/{u.repo}")
|
|
339
|
+
if u.commits_behind:
|
|
340
|
+
lines.append(f" 落后 {u.commits_behind} 个 commit")
|
|
341
|
+
if u.latest_tag and u.current_tag:
|
|
342
|
+
lines.append(f" {u.current_tag} → {u.latest_tag}")
|
|
343
|
+
if u.latest_release_name:
|
|
344
|
+
lines.append(f" 📋 {u.latest_release_name}")
|
|
345
|
+
if u.latest_release_body:
|
|
346
|
+
# 只显示前 3 行
|
|
347
|
+
body_lines = u.latest_release_body.strip().splitlines()[:3]
|
|
348
|
+
for bl in body_lines:
|
|
349
|
+
lines.append(f" {bl.strip()}")
|
|
350
|
+
|
|
351
|
+
if up_to_date:
|
|
352
|
+
lines.append(f"\n✅ {len(up_to_date)} 个项目已是最新:")
|
|
353
|
+
for u in up_to_date:
|
|
354
|
+
lines.append(f" ✅ {u.owner}/{u.repo}")
|
|
355
|
+
|
|
356
|
+
if errors:
|
|
357
|
+
lines.append(f"\n⚠️ {len(errors)} 个项目检查失败:")
|
|
358
|
+
for u in errors:
|
|
359
|
+
lines.append(f" ❌ {u.owner}/{u.repo}: {u.error}")
|
|
360
|
+
|
|
361
|
+
return "\n".join(lines)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def updates_to_dict(updates: list[UpdateInfo]) -> dict:
|
|
365
|
+
"""序列化更新结果为 JSON"""
|
|
366
|
+
return {
|
|
367
|
+
"updates": [
|
|
368
|
+
{
|
|
369
|
+
"owner": u.owner,
|
|
370
|
+
"repo": u.repo,
|
|
371
|
+
"has_update": u.has_update,
|
|
372
|
+
"current_commit": u.current_commit,
|
|
373
|
+
"latest_commit": u.latest_commit,
|
|
374
|
+
"current_tag": u.current_tag,
|
|
375
|
+
"latest_tag": u.latest_tag,
|
|
376
|
+
"commits_behind": u.commits_behind,
|
|
377
|
+
"latest_release_name": u.latest_release_name,
|
|
378
|
+
"error": u.error,
|
|
379
|
+
}
|
|
380
|
+
for u in updates
|
|
381
|
+
],
|
|
382
|
+
"total": len(updates),
|
|
383
|
+
"available": sum(1 for u in updates if u.has_update),
|
|
384
|
+
}
|