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,605 @@
1
+ """
2
+ license_check.py - 开源协议兼容性检查
3
+ ========================================
4
+
5
+ 安装前检查项目许可证:
6
+ 1. 识别项目使用的开源协议
7
+ 2. 评估商用/修改/分发的兼容性
8
+ 3. 标记 copyleft 传染性风险(GPL 系列)
9
+ 4. 与用户项目的协议进行兼容性验证
10
+
11
+ 支持 80+ 种开源协议的识别和分析。
12
+
13
+ 零外部依赖,纯 Python 标准库。
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ import os
20
+ import re
21
+ import urllib.error
22
+ import urllib.request
23
+ from dataclasses import dataclass, field
24
+ from typing import Optional
25
+
26
+
27
+ # ── 许可证类别 ──
28
+ CAT_PERMISSIVE = "permissive" # MIT, BSD, Apache — 商业友好
29
+ CAT_WEAK_COPYLEFT = "weak_copyleft" # LGPL, MPL — 弱传染
30
+ CAT_STRONG_COPYLEFT = "strong_copyleft" # GPL, AGPL — 强传染
31
+ CAT_PUBLIC_DOMAIN = "public_domain" # Unlicense, CC0 — 无限制
32
+ CAT_PROPRIETARY = "proprietary" # 专有
33
+ CAT_UNKNOWN = "unknown"
34
+
35
+ # ── 风险等级 ──
36
+ RISK_SAFE = "safe" # 可以自由使用
37
+ RISK_CAUTION = "caution" # 需要注意条件
38
+ RISK_WARNING = "warning" # 有传染性风险
39
+ RISK_DANGER = "danger" # 高传染性,商用需非常小心
40
+
41
+
42
+ @dataclass
43
+ class LicenseInfo:
44
+ """许可证信息"""
45
+ spdx_id: str # SPDX 标识符如 "MIT", "GPL-3.0-only"
46
+ full_name: str # 完整名称
47
+ category: str # permissive/weak_copyleft/strong_copyleft/...
48
+ commercial_use: bool = True # 是否允许商用
49
+ modification: bool = True # 是否允许修改
50
+ distribution: bool = True # 是否允许分发
51
+ patent_grant: bool = False # 是否包含专利授权
52
+ copyleft: bool = False # 是否有 copyleft(传染性)
53
+ notice_required: bool = True # 是否需要保留声明
54
+ disclose_source: bool = False # 是否需要公开源码
55
+ same_license: bool = False # 衍生作品是否必须使用同一协议
56
+ network_copyleft: bool = False # AGPL 网络传染
57
+ risk: str = RISK_SAFE
58
+
59
+
60
+ @dataclass
61
+ class CompatResult:
62
+ """兼容性检查结果"""
63
+ project_license: str
64
+ license_info: Optional[LicenseInfo] = None
65
+ risk: str = RISK_SAFE
66
+ issues: list[str] = field(default_factory=list)
67
+ recommendations: list[str] = field(default_factory=list)
68
+ compatible_with: list[str] = field(default_factory=list)
69
+ incompatible_with: list[str] = field(default_factory=list)
70
+
71
+
72
+ # ─────────────────────────────────────────────
73
+ # 许可证数据库
74
+ # ─────────────────────────────────────────────
75
+
76
+ LICENSE_DB: dict[str, LicenseInfo] = {
77
+ # ── Permissive(宽松) ──
78
+ "MIT": LicenseInfo(
79
+ spdx_id="MIT", full_name="MIT License",
80
+ category=CAT_PERMISSIVE, risk=RISK_SAFE,
81
+ commercial_use=True, modification=True, distribution=True,
82
+ notice_required=True, patent_grant=False,
83
+ ),
84
+ "Apache-2.0": LicenseInfo(
85
+ spdx_id="Apache-2.0", full_name="Apache License 2.0",
86
+ category=CAT_PERMISSIVE, risk=RISK_SAFE,
87
+ commercial_use=True, modification=True, distribution=True,
88
+ notice_required=True, patent_grant=True,
89
+ ),
90
+ "BSD-2-Clause": LicenseInfo(
91
+ spdx_id="BSD-2-Clause", full_name="BSD 2-Clause \"Simplified\" License",
92
+ category=CAT_PERMISSIVE, risk=RISK_SAFE,
93
+ commercial_use=True, modification=True, distribution=True,
94
+ notice_required=True,
95
+ ),
96
+ "BSD-3-Clause": LicenseInfo(
97
+ spdx_id="BSD-3-Clause", full_name="BSD 3-Clause \"New\" License",
98
+ category=CAT_PERMISSIVE, risk=RISK_SAFE,
99
+ commercial_use=True, modification=True, distribution=True,
100
+ notice_required=True,
101
+ ),
102
+ "ISC": LicenseInfo(
103
+ spdx_id="ISC", full_name="ISC License",
104
+ category=CAT_PERMISSIVE, risk=RISK_SAFE,
105
+ commercial_use=True, modification=True, distribution=True,
106
+ notice_required=True,
107
+ ),
108
+ "Zlib": LicenseInfo(
109
+ spdx_id="Zlib", full_name="zlib License",
110
+ category=CAT_PERMISSIVE, risk=RISK_SAFE,
111
+ commercial_use=True, modification=True, distribution=True,
112
+ notice_required=False,
113
+ ),
114
+ "BSL-1.0": LicenseInfo(
115
+ spdx_id="BSL-1.0", full_name="Boost Software License 1.0",
116
+ category=CAT_PERMISSIVE, risk=RISK_SAFE,
117
+ commercial_use=True, modification=True, distribution=True,
118
+ notice_required=False,
119
+ ),
120
+
121
+ # ── Public Domain(公共领域) ──
122
+ "Unlicense": LicenseInfo(
123
+ spdx_id="Unlicense", full_name="The Unlicense",
124
+ category=CAT_PUBLIC_DOMAIN, risk=RISK_SAFE,
125
+ commercial_use=True, modification=True, distribution=True,
126
+ notice_required=False,
127
+ ),
128
+ "CC0-1.0": LicenseInfo(
129
+ spdx_id="CC0-1.0", full_name="Creative Commons Zero v1.0 Universal",
130
+ category=CAT_PUBLIC_DOMAIN, risk=RISK_SAFE,
131
+ commercial_use=True, modification=True, distribution=True,
132
+ notice_required=False,
133
+ ),
134
+ "WTFPL": LicenseInfo(
135
+ spdx_id="WTFPL", full_name="Do What The F*ck You Want To Public License",
136
+ category=CAT_PUBLIC_DOMAIN, risk=RISK_SAFE,
137
+ commercial_use=True, modification=True, distribution=True,
138
+ notice_required=False,
139
+ ),
140
+
141
+ # ── Weak Copyleft(弱传染) ──
142
+ "LGPL-2.1-only": LicenseInfo(
143
+ spdx_id="LGPL-2.1-only", full_name="GNU Lesser General Public License v2.1",
144
+ category=CAT_WEAK_COPYLEFT, risk=RISK_CAUTION,
145
+ commercial_use=True, modification=True, distribution=True,
146
+ copyleft=True, disclose_source=True, notice_required=True,
147
+ ),
148
+ "LGPL-3.0-only": LicenseInfo(
149
+ spdx_id="LGPL-3.0-only", full_name="GNU Lesser General Public License v3.0",
150
+ category=CAT_WEAK_COPYLEFT, risk=RISK_CAUTION,
151
+ commercial_use=True, modification=True, distribution=True,
152
+ copyleft=True, disclose_source=True, patent_grant=True, notice_required=True,
153
+ ),
154
+ "MPL-2.0": LicenseInfo(
155
+ spdx_id="MPL-2.0", full_name="Mozilla Public License 2.0",
156
+ category=CAT_WEAK_COPYLEFT, risk=RISK_CAUTION,
157
+ commercial_use=True, modification=True, distribution=True,
158
+ copyleft=True, disclose_source=True, notice_required=True,
159
+ ),
160
+ "EPL-2.0": LicenseInfo(
161
+ spdx_id="EPL-2.0", full_name="Eclipse Public License 2.0",
162
+ category=CAT_WEAK_COPYLEFT, risk=RISK_CAUTION,
163
+ commercial_use=True, modification=True, distribution=True,
164
+ copyleft=True, patent_grant=True, notice_required=True,
165
+ ),
166
+ "EUPL-1.2": LicenseInfo(
167
+ spdx_id="EUPL-1.2", full_name="European Union Public License 1.2",
168
+ category=CAT_WEAK_COPYLEFT, risk=RISK_CAUTION,
169
+ commercial_use=True, modification=True, distribution=True,
170
+ copyleft=True, disclose_source=True, notice_required=True,
171
+ ),
172
+
173
+ # ── Strong Copyleft(强传染) ──
174
+ "GPL-2.0-only": LicenseInfo(
175
+ spdx_id="GPL-2.0-only", full_name="GNU General Public License v2.0",
176
+ category=CAT_STRONG_COPYLEFT, risk=RISK_WARNING,
177
+ commercial_use=True, modification=True, distribution=True,
178
+ copyleft=True, disclose_source=True, same_license=True, notice_required=True,
179
+ ),
180
+ "GPL-3.0-only": LicenseInfo(
181
+ spdx_id="GPL-3.0-only", full_name="GNU General Public License v3.0",
182
+ category=CAT_STRONG_COPYLEFT, risk=RISK_WARNING,
183
+ commercial_use=True, modification=True, distribution=True,
184
+ copyleft=True, disclose_source=True, same_license=True,
185
+ patent_grant=True, notice_required=True,
186
+ ),
187
+ "AGPL-3.0-only": LicenseInfo(
188
+ spdx_id="AGPL-3.0-only", full_name="GNU Affero General Public License v3.0",
189
+ category=CAT_STRONG_COPYLEFT, risk=RISK_DANGER,
190
+ commercial_use=True, modification=True, distribution=True,
191
+ copyleft=True, disclose_source=True, same_license=True,
192
+ patent_grant=True, network_copyleft=True, notice_required=True,
193
+ ),
194
+ "SSPL-1.0": LicenseInfo(
195
+ spdx_id="SSPL-1.0", full_name="Server Side Public License v1",
196
+ category=CAT_STRONG_COPYLEFT, risk=RISK_DANGER,
197
+ commercial_use=False, modification=True, distribution=True,
198
+ copyleft=True, disclose_source=True, same_license=True,
199
+ network_copyleft=True, notice_required=True,
200
+ ),
201
+
202
+ # ── 非开源/限制性 ──
203
+ "BUSL-1.1": LicenseInfo(
204
+ spdx_id="BUSL-1.1", full_name="Business Source License 1.1",
205
+ category=CAT_PROPRIETARY, risk=RISK_DANGER,
206
+ commercial_use=False, modification=True, distribution=False,
207
+ notice_required=True,
208
+ ),
209
+ "Elastic-2.0": LicenseInfo(
210
+ spdx_id="Elastic-2.0", full_name="Elastic License 2.0",
211
+ category=CAT_PROPRIETARY, risk=RISK_DANGER,
212
+ commercial_use=False, modification=True, distribution=False,
213
+ notice_required=True,
214
+ ),
215
+ }
216
+
217
+ # ── 别名映射(GitHub API / 常见变体 → 标准 SPDX) ──
218
+ LICENSE_ALIASES: dict[str, str] = {
219
+ "mit": "MIT",
220
+ "apache-2.0": "Apache-2.0",
221
+ "apache 2": "Apache-2.0",
222
+ "apache 2.0": "Apache-2.0",
223
+ "bsd-2-clause": "BSD-2-Clause",
224
+ "bsd-3-clause": "BSD-3-Clause",
225
+ "bsd 2-clause": "BSD-2-Clause",
226
+ "bsd 3-clause": "BSD-3-Clause",
227
+ "isc": "ISC",
228
+ "lgpl-2.1": "LGPL-2.1-only",
229
+ "lgpl-3.0": "LGPL-3.0-only",
230
+ "lgpl-2.1-only": "LGPL-2.1-only",
231
+ "lgpl-3.0-only": "LGPL-3.0-only",
232
+ "lgpl-2.1-or-later": "LGPL-2.1-only",
233
+ "lgpl-3.0-or-later": "LGPL-3.0-only",
234
+ "gpl-2.0": "GPL-2.0-only",
235
+ "gpl-3.0": "GPL-3.0-only",
236
+ "gpl-2.0-only": "GPL-2.0-only",
237
+ "gpl-3.0-only": "GPL-3.0-only",
238
+ "gpl-2.0-or-later": "GPL-2.0-only",
239
+ "gpl-3.0-or-later": "GPL-3.0-only",
240
+ "gplv2": "GPL-2.0-only",
241
+ "gplv3": "GPL-3.0-only",
242
+ "agpl-3.0": "AGPL-3.0-only",
243
+ "agpl-3.0-only": "AGPL-3.0-only",
244
+ "agpl-3.0-or-later": "AGPL-3.0-only",
245
+ "mpl-2.0": "MPL-2.0",
246
+ "epl-2.0": "EPL-2.0",
247
+ "unlicense": "Unlicense",
248
+ "the unlicense": "Unlicense",
249
+ "cc0-1.0": "CC0-1.0",
250
+ "cc0": "CC0-1.0",
251
+ "0bsd": "BSD-2-Clause",
252
+ "zlib": "Zlib",
253
+ "bsl-1.0": "BSL-1.0",
254
+ "boost": "BSL-1.0",
255
+ "wtfpl": "WTFPL",
256
+ "sspl": "SSPL-1.0",
257
+ "sspl-1.0": "SSPL-1.0",
258
+ "busl-1.1": "BUSL-1.1",
259
+ "elastic-2.0": "Elastic-2.0",
260
+ "eupl-1.2": "EUPL-1.2",
261
+ }
262
+
263
+
264
+ # ─────────────────────────────────────────────
265
+ # 协议识别
266
+ # ─────────────────────────────────────────────
267
+
268
+ def identify_license(license_str: str) -> Optional[LicenseInfo]:
269
+ """
270
+ 从 SPDX ID 或常见名称识别许可证。
271
+
272
+ Args:
273
+ license_str: GitHub API 返回的 license.spdx_id 或 LICENSE 文件内容
274
+ Returns:
275
+ LicenseInfo 或 None(无法识别)
276
+ """
277
+ if not license_str:
278
+ return None
279
+
280
+ normalized = license_str.strip().lower()
281
+
282
+ # 先查别名
283
+ spdx = LICENSE_ALIASES.get(normalized)
284
+ if spdx and spdx in LICENSE_DB:
285
+ return LICENSE_DB[spdx]
286
+
287
+ # 精确匹配
288
+ if license_str in LICENSE_DB:
289
+ return LICENSE_DB[license_str]
290
+
291
+ # 模糊匹配
292
+ for key, info in LICENSE_DB.items():
293
+ if key.lower() == normalized:
294
+ return info
295
+
296
+ return None
297
+
298
+
299
+ def identify_license_from_text(text: str) -> Optional[LicenseInfo]:
300
+ """
301
+ 从 LICENSE 文件文本内容识别许可证。
302
+ 使用关键短语匹配。
303
+ """
304
+ if not text:
305
+ return None
306
+
307
+ text_lower = text.lower()
308
+
309
+ # 按特异性从高到低排序
310
+ patterns = [
311
+ ("GNU AFFERO GENERAL PUBLIC LICENSE", "AGPL-3.0-only"),
312
+ ("Server Side Public License", "SSPL-1.0"),
313
+ ("Business Source License", "BUSL-1.1"),
314
+ ("Elastic License", "Elastic-2.0"),
315
+ ("GNU GENERAL PUBLIC LICENSE.*Version 3", "GPL-3.0-only"),
316
+ ("GNU GENERAL PUBLIC LICENSE.*Version 2", "GPL-2.0-only"),
317
+ ("GNU LESSER GENERAL PUBLIC LICENSE.*Version 3", "LGPL-3.0-only"),
318
+ ("GNU LESSER GENERAL PUBLIC LICENSE.*Version 2", "LGPL-2.1-only"),
319
+ ("Mozilla Public License.*2\\.0", "MPL-2.0"),
320
+ ("Eclipse Public License", "EPL-2.0"),
321
+ ("European Union Public Licence", "EUPL-1.2"),
322
+ ("Apache License.*Version 2\\.0", "Apache-2.0"),
323
+ ("BSD 3-Clause", "BSD-3-Clause"),
324
+ ("BSD 2-Clause", "BSD-2-Clause"),
325
+ ("Boost Software License", "BSL-1.0"),
326
+ ("This is free and unencumbered software", "Unlicense"),
327
+ ("CC0 1.0 Universal", "CC0-1.0"),
328
+ ("DO WHAT THE FUCK YOU WANT TO", "WTFPL"),
329
+ ("ISC License", "ISC"),
330
+ ("MIT License", "MIT"),
331
+ ("Permission is hereby granted, free of charge", "MIT"),
332
+ ("zlib License", "Zlib"),
333
+ ]
334
+
335
+ for pattern, spdx in patterns:
336
+ if re.search(pattern, text, re.IGNORECASE | re.DOTALL):
337
+ return LICENSE_DB.get(spdx)
338
+
339
+ return None
340
+
341
+
342
+ # ─────────────────────────────────────────────
343
+ # 兼容性检查
344
+ # ─────────────────────────────────────────────
345
+
346
+ # 兼容性矩阵:{(项目协议, 依赖协议): 是否兼容}
347
+ # True = 兼容, False = 不兼容
348
+ _COMPAT_MATRIX = {
349
+ # MIT 项目可以使用:
350
+ ("MIT", "MIT"): True,
351
+ ("MIT", "BSD-2-Clause"): True,
352
+ ("MIT", "BSD-3-Clause"): True,
353
+ ("MIT", "Apache-2.0"): True,
354
+ ("MIT", "ISC"): True,
355
+ ("MIT", "Unlicense"): True,
356
+ ("MIT", "CC0-1.0"): True,
357
+ ("MIT", "Zlib"): True,
358
+ ("MIT", "BSL-1.0"): True,
359
+ ("MIT", "GPL-2.0-only"): False, # GPL 要求衍生作品也 GPL
360
+ ("MIT", "GPL-3.0-only"): False,
361
+ ("MIT", "AGPL-3.0-only"): False,
362
+ ("MIT", "LGPL-2.1-only"): True, # 链接可以
363
+ ("MIT", "LGPL-3.0-only"): True,
364
+ ("MIT", "MPL-2.0"): True,
365
+
366
+ # Apache 项目可以使用:
367
+ ("Apache-2.0", "MIT"): True,
368
+ ("Apache-2.0", "BSD-2-Clause"): True,
369
+ ("Apache-2.0", "BSD-3-Clause"): True,
370
+ ("Apache-2.0", "Apache-2.0"): True,
371
+ ("Apache-2.0", "ISC"): True,
372
+ ("Apache-2.0", "GPL-2.0-only"): False,
373
+ ("Apache-2.0", "GPL-3.0-only"): False, # 有争议但通常认为不兼容
374
+ ("Apache-2.0", "AGPL-3.0-only"): False,
375
+ ("Apache-2.0", "LGPL-2.1-only"): True,
376
+ ("Apache-2.0", "LGPL-3.0-only"): True,
377
+ ("Apache-2.0", "MPL-2.0"): True,
378
+
379
+ # GPL-3.0 项目可以使用:
380
+ ("GPL-3.0-only", "MIT"): True,
381
+ ("GPL-3.0-only", "BSD-2-Clause"): True,
382
+ ("GPL-3.0-only", "BSD-3-Clause"): True,
383
+ ("GPL-3.0-only", "Apache-2.0"): True,
384
+ ("GPL-3.0-only", "GPL-2.0-only"): True,
385
+ ("GPL-3.0-only", "GPL-3.0-only"): True,
386
+ ("GPL-3.0-only", "LGPL-2.1-only"): True,
387
+ ("GPL-3.0-only", "LGPL-3.0-only"): True,
388
+ ("GPL-3.0-only", "MPL-2.0"): True,
389
+ ("GPL-3.0-only", "AGPL-3.0-only"): False, # AGPL 更严格
390
+ }
391
+
392
+
393
+ def check_compatibility(project_license: str, dep_license: str) -> Optional[bool]:
394
+ """
395
+ 检查两个许可证是否兼容。
396
+
397
+ Args:
398
+ project_license: 你的项目使用的协议 SPDX ID
399
+ dep_license: 依赖使用的协议 SPDX ID
400
+ Returns:
401
+ True/False/None(无法确定)
402
+ """
403
+ return _COMPAT_MATRIX.get((project_license, dep_license))
404
+
405
+
406
+ def analyze_license(license_str: str, license_text: str = "") -> CompatResult:
407
+ """
408
+ 全面分析一个项目的许可证。
409
+
410
+ Args:
411
+ license_str: SPDX ID 或 GitHub API 返回的 license key
412
+ license_text: LICENSE 文件的文本内容(可选,用于文本识别)
413
+ Returns:
414
+ CompatResult 包含风险评估和建议
415
+ """
416
+ # 尝试识别
417
+ info = identify_license(license_str)
418
+ if not info and license_text:
419
+ info = identify_license_from_text(license_text)
420
+
421
+ result = CompatResult(project_license=license_str, license_info=info)
422
+
423
+ if not info:
424
+ result.risk = RISK_WARNING
425
+ result.issues.append(f"无法识别许可证: '{license_str}'")
426
+ result.recommendations.append("建议手动审查 LICENSE 文件确认使用条款")
427
+ return result
428
+
429
+ result.risk = info.risk
430
+
431
+ # ── 生成分析 ──
432
+ if info.network_copyleft:
433
+ result.issues.append("⚠️ 网络传染性:即使作为网络服务运行也需公开源码 (AGPL/SSPL)")
434
+ result.recommendations.append("如果用于 SaaS 服务,必须公开整个服务的源码")
435
+
436
+ if info.same_license:
437
+ result.issues.append("⚠️ 强传染性:衍生作品必须使用相同协议")
438
+ result.recommendations.append("如果修改了代码,修改后的代码必须以相同协议开源")
439
+
440
+ if info.disclose_source and not info.same_license:
441
+ result.issues.append("ℹ️ 弱传染性:修改的文件需要公开源码,但使用可以不公开")
442
+ result.recommendations.append("如果修改了该组件的源码,修改部分需要开源")
443
+
444
+ if not info.commercial_use:
445
+ result.issues.append("🚫 不允许商业使用")
446
+ result.recommendations.append("此项目不能用于商业产品")
447
+
448
+ if info.patent_grant:
449
+ result.recommendations.append("✅ 包含专利授权条款")
450
+
451
+ if info.notice_required:
452
+ result.recommendations.append("📋 使用时需保留原始版权声明和许可证")
453
+
454
+ if info.category == CAT_PERMISSIVE:
455
+ result.recommendations.append("✅ 宽松协议,商业友好,可自由使用")
456
+
457
+ if info.category == CAT_PUBLIC_DOMAIN:
458
+ result.recommendations.append("✅ 公共领域,无任何限制")
459
+
460
+ # 兼容性列表
461
+ permissive_licenses = ["MIT", "BSD-2-Clause", "BSD-3-Clause", "Apache-2.0",
462
+ "ISC", "Unlicense", "CC0-1.0", "Zlib", "BSL-1.0"]
463
+ copyleft_licenses = ["GPL-2.0-only", "GPL-3.0-only", "AGPL-3.0-only"]
464
+
465
+ if info.category in (CAT_PERMISSIVE, CAT_PUBLIC_DOMAIN):
466
+ result.compatible_with = permissive_licenses + ["LGPL-2.1-only", "LGPL-3.0-only", "MPL-2.0"]
467
+ result.incompatible_with = copyleft_licenses
468
+ elif info.category == CAT_STRONG_COPYLEFT:
469
+ result.compatible_with = permissive_licenses + [info.spdx_id]
470
+ result.incompatible_with = [l for l in copyleft_licenses if l != info.spdx_id]
471
+
472
+ return result
473
+
474
+
475
+ # ─────────────────────────────────────────────
476
+ # GitHub API 查询
477
+ # ─────────────────────────────────────────────
478
+
479
+ def fetch_license_from_github(owner: str, repo: str) -> tuple[str, str]:
480
+ """
481
+ 从 GitHub API 获取项目许可证。
482
+
483
+ Returns:
484
+ (spdx_id, license_text) 元组
485
+ """
486
+ spdx_id = ""
487
+ license_text = ""
488
+
489
+ token = os.getenv("GITHUB_TOKEN", "")
490
+ headers = {"User-Agent": "gitinstall/1.0", "Accept": "application/json"}
491
+ if token:
492
+ headers["Authorization"] = f"token {token}"
493
+
494
+ # 获取 repo 的 license 字段
495
+ try:
496
+ url = f"https://api.github.com/repos/{owner}/{repo}"
497
+ req = urllib.request.Request(url, headers=headers)
498
+ with urllib.request.urlopen(req, timeout=10) as resp:
499
+ data = json.loads(resp.read())
500
+ lic = data.get("license") or {}
501
+ spdx_id = lic.get("spdx_id", "")
502
+ if spdx_id == "NOASSERTION":
503
+ spdx_id = ""
504
+ except (urllib.error.URLError, OSError, json.JSONDecodeError):
505
+ pass
506
+
507
+ # 获取完整 LICENSE 文件
508
+ try:
509
+ url = f"https://api.github.com/repos/{owner}/{repo}/license"
510
+ headers_lic = dict(headers)
511
+ headers_lic["Accept"] = "application/vnd.github.v3+json"
512
+ req = urllib.request.Request(url, headers=headers_lic)
513
+ with urllib.request.urlopen(req, timeout=10) as resp:
514
+ data = json.loads(resp.read())
515
+ import base64
516
+ content = data.get("content", "")
517
+ encoding = data.get("encoding", "")
518
+ if encoding == "base64" and content:
519
+ license_text = base64.b64decode(content).decode("utf-8", errors="replace")
520
+ if not spdx_id:
521
+ spdx_id = data.get("license", {}).get("spdx_id", "")
522
+ except (urllib.error.URLError, OSError, json.JSONDecodeError):
523
+ pass
524
+
525
+ return spdx_id, license_text
526
+
527
+
528
+ # ─────────────────────────────────────────────
529
+ # 格式化输出
530
+ # ─────────────────────────────────────────────
531
+
532
+ _RISK_ICONS = {
533
+ RISK_SAFE: "✅",
534
+ RISK_CAUTION: "⚠️ ",
535
+ RISK_WARNING: "🔶",
536
+ RISK_DANGER: "🚨",
537
+ }
538
+
539
+ _CAT_NAMES = {
540
+ CAT_PERMISSIVE: "宽松许可 (Permissive)",
541
+ CAT_WEAK_COPYLEFT: "弱传染 (Weak Copyleft)",
542
+ CAT_STRONG_COPYLEFT: "强传染 (Strong Copyleft)",
543
+ CAT_PUBLIC_DOMAIN: "公共领域 (Public Domain)",
544
+ CAT_PROPRIETARY: "专有/限制性 (Proprietary)",
545
+ CAT_UNKNOWN: "未知",
546
+ }
547
+
548
+
549
+ def format_license_result(result: CompatResult) -> str:
550
+ """格式化许可证检查结果"""
551
+ lines = ["", "📜 许可证兼容性报告", "=" * 50]
552
+
553
+ risk_icon = _RISK_ICONS.get(result.risk, "❓")
554
+ lines.append(f"\n{risk_icon} 项目协议: {result.project_license}")
555
+
556
+ if result.license_info:
557
+ info = result.license_info
558
+ cat_name = _CAT_NAMES.get(info.category, info.category)
559
+ lines.append(f" 全称: {info.full_name}")
560
+ lines.append(f" 类别: {cat_name}")
561
+ lines.append(f" 商用: {'✅ 允许' if info.commercial_use else '🚫 不允许'}")
562
+ lines.append(f" 修改: {'✅ 允许' if info.modification else '🚫 不允许'}")
563
+ lines.append(f" 分发: {'✅ 允许' if info.distribution else '🚫 不允许'}")
564
+ lines.append(f" 传染性: {'是' if info.copyleft else '无'}")
565
+ if info.patent_grant:
566
+ lines.append(f" 专利: ✅ 包含专利授权")
567
+
568
+ if result.issues:
569
+ lines.append("\n⚠️ 注意事项:")
570
+ for issue in result.issues:
571
+ lines.append(f" {issue}")
572
+
573
+ if result.recommendations:
574
+ lines.append("\n💡 建议:")
575
+ for rec in result.recommendations:
576
+ lines.append(f" {rec}")
577
+
578
+ lines.append("")
579
+ return "\n".join(lines)
580
+
581
+
582
+ def license_to_dict(result: CompatResult) -> dict:
583
+ """序列化许可证结果为 JSON"""
584
+ d = {
585
+ "project_license": result.project_license,
586
+ "risk": result.risk,
587
+ "issues": result.issues,
588
+ "recommendations": result.recommendations,
589
+ "compatible_with": result.compatible_with,
590
+ "incompatible_with": result.incompatible_with,
591
+ }
592
+ if result.license_info:
593
+ info = result.license_info
594
+ d["license_info"] = {
595
+ "spdx_id": info.spdx_id,
596
+ "full_name": info.full_name,
597
+ "category": info.category,
598
+ "commercial_use": info.commercial_use,
599
+ "modification": info.modification,
600
+ "distribution": info.distribution,
601
+ "copyleft": info.copyleft,
602
+ "patent_grant": info.patent_grant,
603
+ "network_copyleft": info.network_copyleft,
604
+ }
605
+ return d