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,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
+ }