Merge PR #309: 新增 health diagnose 命令,实现项目健康度综合诊断功能
This commit is contained in:
commit
1b2e44653f
45
README.md
45
README.md
|
|
@ -511,6 +511,49 @@ gitlink-cli ci +log --owner Gitlink --repo forgeplus -i <build_id>
|
|||
gitlink-cli ci +restart --owner Gitlink --repo forgeplus -i <build_id>
|
||||
```
|
||||
|
||||
### Health Diagnostics
|
||||
|
||||
`gitlink health diagnose` analyzes repository health across 5 dimensions: Documentation, License, Community, Maturity, and CI/CD.
|
||||
|
||||
```bash
|
||||
# Basic diagnosis (text output)
|
||||
gitlink-cli health diagnose --owner Gitlink --repo gitlink-cli
|
||||
|
||||
# JSON output for scripts and AI Agents
|
||||
gitlink-cli health diagnose --owner Gitlink --repo gitlink-cli --format json
|
||||
|
||||
# Markdown output for reports
|
||||
gitlink-cli health diagnose --owner Gitlink --repo gitlink-cli --format markdown
|
||||
|
||||
# Verbose mode with detailed breakdown
|
||||
gitlink-cli health diagnose --owner Gitlink --repo gitlink-cli --verbose
|
||||
|
||||
# Custom database path for historical tracking
|
||||
gitlink-cli health diagnose --owner Gitlink --repo gitlink-cli --db ./health.db
|
||||
```
|
||||
|
||||
**Scoring Dimensions (100 points total):**
|
||||
|
||||
| Dimension | Weight | Criteria |
|
||||
|-----------|--------|----------|
|
||||
| Documentation | 20 | README quality, contributing guide, code of conduct |
|
||||
| License | 15 | License presence and OSI approval |
|
||||
| Community | 25 | Contributors, activity, bus factor |
|
||||
| Maturity | 20 | Releases, version stability, age |
|
||||
| CI/CD | 20 | Build success rate, pipeline configuration |
|
||||
|
||||
**Health Status Thresholds:**
|
||||
|
||||
- `good` (≥70%): Healthy project with active maintenance
|
||||
- `warning` (≥40%): Needs attention in some areas
|
||||
- `critical` (<40%): Requires immediate improvement
|
||||
|
||||
**Output Formats:**
|
||||
|
||||
- `text` (default): Human-readable summary with suggestions
|
||||
- `json`: Structured data for scripts and AI Agents
|
||||
- `markdown`: Formatted report for documentation
|
||||
|
||||
### Pipeline Operations
|
||||
|
||||
```bash
|
||||
|
|
@ -775,7 +818,7 @@ See [skills/README.md](./skills/README.md) for details.
|
|||
| `gitlink-user` | User management (profile info, etc.) |
|
||||
| `gitlink-pm` | Project management (sprints, kanban, weekly reports, etc.) |
|
||||
| `gitlink-workflow` | AI-powered workflows (issue triage, PR review, release notes, etc.) |
|
||||
| `gitlink-health` | Project health analysis (PR/Issue metrics aggregation, health reports) |
|
||||
| `gitlink-health` | Project health diagnostics (5-dimension scoring: Documentation, License, Community, Maturity, CI/CD) |
|
||||
|
||||
## Project Structure
|
||||
|
||||
|
|
|
|||
|
|
@ -494,6 +494,49 @@ gitlink-cli release +update --owner Gitlink --repo forgeplus -i <version_id> -b
|
|||
gitlink-cli release +delete --owner Gitlink --repo forgeplus -i <version_id> --dry-run
|
||||
```
|
||||
|
||||
### 健康度诊断
|
||||
|
||||
`gitlink health diagnose` 从 5 个维度分析仓库健康度:文档、许可证、社区、成熟度、CI/CD。
|
||||
|
||||
```bash
|
||||
# 基础诊断(文本输出)
|
||||
gitlink-cli health diagnose --owner Gitlink --repo gitlink-cli
|
||||
|
||||
# JSON 输出,便于脚本和 AI Agent 处理
|
||||
gitlink-cli health diagnose --owner Gitlink --repo gitlink-cli --format json
|
||||
|
||||
# Markdown 输出,用于报告
|
||||
gitlink-cli health diagnose --owner Gitlink --repo gitlink-cli --format markdown
|
||||
|
||||
# 详细模式,显示完整分解
|
||||
gitlink-cli health diagnose --owner Gitlink --repo gitlink-cli --verbose
|
||||
|
||||
# 自定义数据库路径,用于历史追踪
|
||||
gitlink-cli health diagnose --owner Gitlink --repo gitlink-cli --db ./health.db
|
||||
```
|
||||
|
||||
**评分维度(总分 100 分):**
|
||||
|
||||
| 维度 | 权重 | 评估标准 |
|
||||
|------|------|----------|
|
||||
| 文档 | 20 | README 质量、贡献指南、行为准则 |
|
||||
| 许可证 | 15 | 许可证存在性及 OSI 认证 |
|
||||
| 社区 | 25 | 贡献者数量、活跃度、巴士系数 |
|
||||
| 成熟度 | 20 | 发布版本、版本稳定性、项目年龄 |
|
||||
| CI/CD | 20 | 构建成功率、流水线配置 |
|
||||
|
||||
**健康状态阈值:**
|
||||
|
||||
- `good` (≥70%):健康项目,维护活跃
|
||||
- `warning` (≥40%):部分领域需要关注
|
||||
- `critical` (<40%):需要立即改进
|
||||
|
||||
**输出格式:**
|
||||
|
||||
- `text`(默认):人类可读的摘要和建议
|
||||
- `json`:结构化数据,便于脚本和 AI Agent 处理
|
||||
- `markdown`:格式化报告,用于文档
|
||||
|
||||
### 流水线管理
|
||||
|
||||
```bash
|
||||
|
|
@ -676,6 +719,7 @@ git push gitlink
|
|||
| `gitlink-user` | 用户管理(个人信息等) |
|
||||
| `gitlink-pm` | 项目管理(Sprint、看板、周报等) |
|
||||
| `gitlink-workflow` | AI 自动化工作流(Issue 分类、PR Review、Release Notes 等) |
|
||||
| `gitlink-health` | 项目健康度诊断(5 维度评分:文档、许可证、社区、成熟度、CI/CD) |
|
||||
|
||||
## 项目结构
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,486 @@
|
|||
# 项目健康度诊断功能设计文档
|
||||
|
||||
## 概述
|
||||
|
||||
**方向B:项目健康度诊断** - 整合多个 GitLink API,一键诊断项目健康状况,提供评分和改进建议。
|
||||
|
||||
**核心价值**:体现 AI Agent 的"智能诊断"能力,不仅仅是 API 翻译,而是跨模块整合分析。
|
||||
|
||||
---
|
||||
|
||||
## CLI 命令设计
|
||||
|
||||
### 主命令
|
||||
|
||||
```bash
|
||||
gitlink health diagnose owner/repo [flags]
|
||||
```
|
||||
|
||||
### 参数
|
||||
|
||||
| 参数 | 短参数 | 说明 | 默认值 |
|
||||
|------|--------|------|--------|
|
||||
| `--format` | `-f` | 输出格式(text/json/markdown) | text |
|
||||
| `--verbose` | `-v` | 显示详细信息 | false |
|
||||
| `--db` | `-d` | SQLite 数据库路径(可选,用于历史对比) | - |
|
||||
|
||||
### 输出示例
|
||||
|
||||
```
|
||||
🔍 项目健康度诊断报告
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
📦 项目:gitlink-org/gitlink-cli
|
||||
📊 总分:78/100 ⭐⭐⭐
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
📋 诊断详情
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
✅ 文档完善度 (18/20)
|
||||
├─ README 存在 ✓
|
||||
└─ README 内容丰富 ✓ (2,345 字符)
|
||||
|
||||
✅ 许可证合规性 (15/15)
|
||||
└─ 许可证: Apache-2.0 ✓
|
||||
|
||||
⚠️ 社区活跃度 (16/25)
|
||||
├─ 贡献者: 5 人 (建议: >10 人)
|
||||
├─ Open Issues: 12 个
|
||||
└─ 最近 30 天 PR: 3 个 (建议: >5 个)
|
||||
|
||||
✅ 项目成熟度 (18/20)
|
||||
├─ 分支: 3 个 ✓
|
||||
├─ 标签: 8 个 ✓
|
||||
└─ Star: 156, Fork: 42 ✓
|
||||
|
||||
⚠️ CI/CD 配置 (11/20)
|
||||
└─ CI 构建记录: 有,但最近失败率较高 (33%)
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
💡 改进建议
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
1. 🤝 社区建设
|
||||
- 邀请更多开发者参与项目
|
||||
- 创建 CONTRIBUTING.md 指引贡献流程
|
||||
- 标记 "good first issue" 吸引新贡献者
|
||||
|
||||
2. 🔄 CI/CD 优化
|
||||
- 检查最近的构建失败原因
|
||||
- 添加自动化测试覆盖率报告
|
||||
- 配置代码质量检查工具
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
📈 历史趋势(需要 --db 参数)
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
2026-06-01: 72/100 ⬆️ +6
|
||||
2026-06-15: 75/100 ⬆️ +3
|
||||
2026-06-29: 78/100 ⬆️ +3
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 诊断维度与评分算法
|
||||
|
||||
### 1. 文档完善度 (20分)
|
||||
|
||||
**检查项**:
|
||||
- README 是否存在(10分)
|
||||
- README 内容长度(10分)
|
||||
- < 100 字符: 2分
|
||||
- 100-500 字符: 5分
|
||||
- 500-1500 字符: 8分
|
||||
- > 1500 字符: 10分
|
||||
|
||||
**API 调用**:
|
||||
```
|
||||
GET /api/{owner}/{repo}/readme.json
|
||||
```
|
||||
|
||||
**实现逻辑**:
|
||||
```go
|
||||
func scoreReadme(readme map[string]interface{}) int {
|
||||
score := 0
|
||||
if readme == nil {
|
||||
return 0
|
||||
}
|
||||
score += 10 // README 存在
|
||||
|
||||
content, _ := readme["content"].(string)
|
||||
if len(content) < 100 {
|
||||
score += 2
|
||||
} else if len(content) < 500 {
|
||||
score += 5
|
||||
} else if len(content) < 1500 {
|
||||
score += 8
|
||||
} else {
|
||||
score += 10
|
||||
}
|
||||
return score
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. 许可证合规性 (15分)
|
||||
|
||||
**检查项**:
|
||||
- 是否有许可证(15分)
|
||||
|
||||
**API 调用**:
|
||||
```
|
||||
GET /api/{owner}/{repo}/detail.json
|
||||
→ license_name 字段
|
||||
```
|
||||
|
||||
**实现逻辑**:
|
||||
```go
|
||||
func scoreLicense(detail map[string]interface{}) int {
|
||||
licenseName, _ := detail["license_name"].(string)
|
||||
if licenseName != "" && licenseName != "null" {
|
||||
return 15
|
||||
}
|
||||
return 0
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. 社区活跃度 (25分)
|
||||
|
||||
**检查项**:
|
||||
- 贡献者数量(10分)
|
||||
- 1-2 人: 3分
|
||||
- 3-5 人: 6分
|
||||
- 6-10 人: 8分
|
||||
- > 10 人: 10分
|
||||
|
||||
- 最近 PR/Issue 活跃度(15分)
|
||||
- 最近 30 天 PR 数量
|
||||
- Open Issue 处理情况
|
||||
|
||||
**API 调用**:
|
||||
```
|
||||
GET /api/{owner}/{repo}/contributors.json
|
||||
GET /api/{owner}/{repo}/detail.json (issues_count, pull_requests_count)
|
||||
```
|
||||
|
||||
**实现逻辑**:
|
||||
```go
|
||||
func scoreCommunity(contributors []interface{}, detail map[string]interface{}) int {
|
||||
score := 0
|
||||
|
||||
// 贡献者评分
|
||||
contributorCount := len(contributors)
|
||||
if contributorCount <= 2 {
|
||||
score += 3
|
||||
} else if contributorCount <= 5 {
|
||||
score += 6
|
||||
} else if contributorCount <= 10 {
|
||||
score += 8
|
||||
} else {
|
||||
score += 10
|
||||
}
|
||||
|
||||
// Issue/PR 活跃度(需要从 detail 获取)
|
||||
issuesCount, _ := detail["issues_count"].(float64)
|
||||
prCount, _ := detail["pull_requests_count"].(float64)
|
||||
|
||||
// 简单评分逻辑
|
||||
if issuesCount > 0 || prCount > 0 {
|
||||
score += 10
|
||||
}
|
||||
if issuesCount > 10 || prCount > 5 {
|
||||
score += 5
|
||||
}
|
||||
|
||||
return min(score, 25)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. 项目成熟度 (20分)
|
||||
|
||||
**检查项**:
|
||||
- 分支数量(5分)
|
||||
- 1 个: 2分
|
||||
- 2-3 个: 4分
|
||||
- > 3 个: 5分
|
||||
|
||||
- 标签数量(5分)
|
||||
- 0 个: 0分
|
||||
- 1-3 个: 3分
|
||||
- > 3 个: 5分
|
||||
|
||||
- Star/Fork 数量(10分)
|
||||
- Star < 10: 2分
|
||||
- Star 10-50: 5分
|
||||
- Star 50-200: 8分
|
||||
- Star > 200: 10分
|
||||
|
||||
**API 调用**:
|
||||
```
|
||||
GET /api/{owner}/{repo}/detail.json
|
||||
→ branches_count, tags_count, praises_count, forked_count
|
||||
```
|
||||
|
||||
**实现逻辑**:
|
||||
```go
|
||||
func scoreMaturity(detail map[string]interface{}) int {
|
||||
score := 0
|
||||
|
||||
// 分支评分
|
||||
branchesCount, _ := detail["branches_count"].(float64)
|
||||
if branchesCount == 1 {
|
||||
score += 2
|
||||
} else if branchesCount <= 3 {
|
||||
score += 4
|
||||
} else {
|
||||
score += 5
|
||||
}
|
||||
|
||||
// 标签评分
|
||||
tagsCount, _ := detail["tags_count"].(float64)
|
||||
if tagsCount == 0 {
|
||||
score += 0
|
||||
} else if tagsCount <= 3 {
|
||||
score += 3
|
||||
} else {
|
||||
score += 5
|
||||
}
|
||||
|
||||
// Star 评分
|
||||
praisesCount, _ := detail["praises_count"].(float64)
|
||||
if praisesCount < 10 {
|
||||
score += 2
|
||||
} else if praisesCount < 50 {
|
||||
score += 5
|
||||
} else if praisesCount < 200 {
|
||||
score += 8
|
||||
} else {
|
||||
score += 10
|
||||
}
|
||||
|
||||
return score
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. CI/CD 配置 (20分)
|
||||
|
||||
**检查项**:
|
||||
- 是否有 CI 构建记录(10分)
|
||||
- 最近构建成功率(10分)
|
||||
- 无记录: 0分
|
||||
- 成功率 < 50%: 3分
|
||||
- 成功率 50-80%: 6分
|
||||
- 成功率 > 80%: 10分
|
||||
|
||||
**API 调用**:
|
||||
```
|
||||
GET /api/v1/{owner}/{repo}/builds.json
|
||||
```
|
||||
|
||||
**实现逻辑**:
|
||||
```go
|
||||
func scoreCI(builds []interface{}) int {
|
||||
if len(builds) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
score := 10 // 有 CI 记录
|
||||
|
||||
// 计算成功率
|
||||
successCount := 0
|
||||
for _, build := range builds {
|
||||
if b, ok := build.(map[string]interface{}); ok {
|
||||
if status, _ := b["status"].(string); status == "success" {
|
||||
successCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
successRate := float64(successCount) / float64(len(builds))
|
||||
if successRate < 0.5 {
|
||||
score += 3
|
||||
} else if successRate < 0.8 {
|
||||
score += 6
|
||||
} else {
|
||||
score += 10
|
||||
}
|
||||
|
||||
return score
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 技术实现方案
|
||||
|
||||
### 方案选择:扩展现有 health.go 模块
|
||||
|
||||
**理由**:
|
||||
1. health.go 已有数据采集基础设施(SQLite、rate limiter)
|
||||
2. 可以复用现有的 API 调用逻辑
|
||||
3. 保持代码组织一致性
|
||||
|
||||
### 文件结构
|
||||
|
||||
```
|
||||
shortcuts/health/
|
||||
├── health.go # 现有 fetch 命令
|
||||
├── diagnose.go # 新增 diagnose 命令
|
||||
├── scoring.go # 评分算法
|
||||
├── suggestions.go # 改进建议生成
|
||||
└── health_test.go # 单元测试
|
||||
```
|
||||
|
||||
### 核心代码结构
|
||||
|
||||
```go
|
||||
// diagnose.go
|
||||
package health
|
||||
|
||||
func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
|
||||
return []*common.Shortcut{
|
||||
// 现有 fetch 命令...
|
||||
{
|
||||
Name: "diagnose",
|
||||
Description: "Diagnose project health and provide improvement suggestions",
|
||||
Flags: []common.Flag{
|
||||
{Name: "format", Short: "f", Usage: "Output format (text/json/markdown)", Default: "text"},
|
||||
{Name: "verbose", Short: "v", Usage: "Show detailed information"},
|
||||
{Name: "db", Short: "d", Usage: "SQLite database path for historical comparison"},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
return runDiagnose(ctx)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func runDiagnose(ctx *common.RuntimeContext) error {
|
||||
// 1. 解析 owner/repo
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 2. 并发获取数据
|
||||
data, err := fetchDiagnosisData(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 3. 计算评分
|
||||
report := calculateHealthScore(data)
|
||||
|
||||
// 4. 生成改进建议
|
||||
report.Suggestions = generateSuggestions(report)
|
||||
|
||||
// 5. 输出结果
|
||||
return outputReport(ctx, report)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 需要调用的 API 列表
|
||||
|
||||
| API | 用途 | 诊断维度 |
|
||||
|-----|------|----------|
|
||||
| `GET /api/{owner}/{repo}/detail.json` | 项目详情 | 许可证、成熟度、社区 |
|
||||
| `GET /api/{owner}/{repo}/readme.json` | README 内容 | 文档完善度 |
|
||||
| `GET /api/{owner}/{repo}/contributors.json` | 贡献者列表 | 社区活跃度 |
|
||||
| `GET /api/v1/{owner}/{repo}/builds.json` | CI 构建记录 | CI/CD 配置 |
|
||||
|
||||
---
|
||||
|
||||
## i18n 翻译支持
|
||||
|
||||
### zh-CN.json
|
||||
|
||||
```json
|
||||
{
|
||||
"cmd.health.diagnose.short": "诊断项目健康度并提供改进建议",
|
||||
"cmd.health.diagnose.format": "输出格式 (text/json/markdown)",
|
||||
"cmd.health.diagnose.verbose": "显示详细信息",
|
||||
"health.score.documentation": "文档完善度",
|
||||
"health.score.license": "许可证合规性",
|
||||
"health.score.community": "社区活跃度",
|
||||
"health.score.maturity": "项目成熟度",
|
||||
"health.score.ci": "CI/CD 配置",
|
||||
"health.suggestion.community": "社区建设",
|
||||
"health.suggestion.ci": "CI/CD 优化"
|
||||
}
|
||||
```
|
||||
|
||||
### en-US.json
|
||||
|
||||
```json
|
||||
{
|
||||
"cmd.health.diagnose.short": "Diagnose project health and provide improvement suggestions",
|
||||
"cmd.health.diagnose.format": "Output format (text/json/markdown)",
|
||||
"cmd.health.diagnose.verbose": "Show detailed information",
|
||||
"health.score.documentation": "Documentation Quality",
|
||||
"health.score.license": "License Compliance",
|
||||
"health.score.community": "Community Activity",
|
||||
"health.score.maturity": "Project Maturity",
|
||||
"health.score.ci": "CI/CD Configuration",
|
||||
"health.suggestion.community": "Community Building",
|
||||
"health.suggestion.ci": "CI/CD Optimization"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 开发计划
|
||||
|
||||
### 阶段 1:核心功能(2-3 天)
|
||||
- [ ] 创建 diagnose.go 文件
|
||||
- [ ] 实现数据获取逻辑(并发调用 API)
|
||||
- [ ] 实现评分算法(scoring.go)
|
||||
- [ ] 实现 text 格式输出
|
||||
|
||||
### 阶段 2:增强功能(1-2 天)
|
||||
- [ ] 实现改进建议生成(suggestions.go)
|
||||
- [ ] 实现 json/markdown 格式输出
|
||||
- [ ] 添加历史对比功能(需要 --db 参数)
|
||||
|
||||
### 阶段 3:测试与文档(1 天)
|
||||
- [ ] 编写单元测试
|
||||
- [ ] 添加 i18n 翻译
|
||||
- [ ] 更新 README 和 Skills 文档
|
||||
- [ ] 注册到 register.go
|
||||
|
||||
---
|
||||
|
||||
## 创新点总结
|
||||
|
||||
1. **跨模块整合**:整合 repo、issue、ci 等多个模块的 API
|
||||
2. **智能评分**:基于多维度算法计算健康度分数
|
||||
3. **改进建议**:根据诊断结果提供可操作的改进建议
|
||||
4. **历史对比**:支持历史数据对比,追踪项目健康度变化
|
||||
5. **多格式输出**:支持 text/json/markdown 格式,方便集成到其他工具
|
||||
|
||||
---
|
||||
|
||||
## 与其他方向对比
|
||||
|
||||
| 方向 | 创新性 | 技术难度 | 实用性 | AI Agent 价值 |
|
||||
|------|--------|----------|--------|---------------|
|
||||
| A. 开发者画像 | ⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐ | ⭐⭐ |
|
||||
| **B. 项目健康度诊断** | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
|
||||
| C. 智能工作流编排 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
|
||||
| D. 交互式反馈收集 | ⭐⭐⭐ | ⭐⭐ | ⭐⭐ | ⭐⭐ |
|
||||
|
||||
**方向B 优势**:
|
||||
- 技术难度适中,可在比赛时间内完成
|
||||
- 实用性强,对开源项目维护者有实际价值
|
||||
- 体现 AI Agent 的整合分析能力
|
||||
- 可扩展性好,未来可添加更多诊断维度
|
||||
|
|
@ -286,5 +286,111 @@
|
|||
"success.config.set": "✓ {key} = {value}",
|
||||
"warning.auth.not_logged_in": "✗ Not logged in",
|
||||
"warning.auth.token_unverified": "✓ Token stored (but cannot verify: {message})",
|
||||
"warning.auth.user_unavailable": "✓ Token stored (user info unavailable)"
|
||||
"warning.auth.user_unavailable": "✓ Token stored (user info unavailable)",
|
||||
"cmd.health.diagnose.short": "Diagnose project health",
|
||||
"cmd.health.diagnose.long": "Perform comprehensive project health diagnosis, including documentation completeness, license compliance, community activity, project maturity, and CI/CD configuration.",
|
||||
"cmd.health.diagnose.format": "Output format: text, json, markdown",
|
||||
"cmd.health.diagnose.verbose": "Show detailed diagnosis information",
|
||||
"cmd.health.diagnose.db": "Database path (for storing diagnosis history)",
|
||||
"prompt.diagnosing": "Diagnosing project health...",
|
||||
"output.report_title": "Project Health Diagnosis Report",
|
||||
"output.project": "Project",
|
||||
"output.total_score": "Total Score",
|
||||
"output.diagnosis_details": "Diagnosis Details",
|
||||
"output.suggestions": "Improvement Suggestions",
|
||||
"table.category": "Category",
|
||||
"table.score": "Score",
|
||||
"table.status": "Status",
|
||||
"output.category_documentation": "Documentation Completeness",
|
||||
"output.category_license": "License Compliance",
|
||||
"output.category_community": "Community Activity",
|
||||
"output.category_maturity": "Project Maturity",
|
||||
"output.category_ci": "CI/CD Configuration",
|
||||
"output.doc_readme_found": "✓ README file exists",
|
||||
"output.doc_readme_comprehensive": "✓ README is comprehensive with multiple important sections",
|
||||
"output.doc_readme_basic": "⚠ README is basic, consider adding more sections",
|
||||
"output.doc_readme_minimal": "✗ README content is too minimal",
|
||||
"output.doc_readme_empty": "✗ README file is empty",
|
||||
"output.doc_readme_missing": "✗ Missing README file",
|
||||
"output.doc_description_found": "✓ Project description is set",
|
||||
"output.doc_description_missing": "✗ Missing project description",
|
||||
"output.license_found": "✓ License",
|
||||
"output.license_well_known": "✓ Using a well-known open source license",
|
||||
"output.license_custom": "⚠ Using a custom license",
|
||||
"output.license_missing": "✗ Missing license",
|
||||
"output.license_unknown": "⚠ Unable to retrieve license information",
|
||||
"output.community_contributors_many": "✓ Many contributors, active community",
|
||||
"output.community_contributors_good": "✓ Good number of contributors",
|
||||
"output.community_contributors_few": "⚠ Few contributors",
|
||||
"output.community_contributors_solo": "✗ Only one contributor",
|
||||
"output.community_popular": "✓ Project is popular with high Star/Fork count",
|
||||
"output.community_moderate": "⚠ Project has moderate attention",
|
||||
"output.community_growing": "⚠ Project is still growing",
|
||||
"output.community_issues_active": "✓ Active issues, high community engagement",
|
||||
"output.community_issues_moderate": "⚠ Moderate number of issues",
|
||||
"output.community_issues_few": "⚠ Few issues",
|
||||
"output.community_issues_none": "✗ No issues",
|
||||
"output.maturity_branches_good": "✓ Branch management is well established",
|
||||
"output.maturity_branches_basic": "⚠ Branch management is basic",
|
||||
"output.maturity_branches_minimal": "✗ Branch management is insufficient",
|
||||
"output.maturity_releases_many": "✓ Many release versions",
|
||||
"output.maturity_releases_some": "✓ Multiple release versions",
|
||||
"output.maturity_releases_initial": "⚠ Only one initial version",
|
||||
"output.maturity_releases_none": "✗ No release versions",
|
||||
"output.maturity_pr_active": "✓ Active pull requests",
|
||||
"output.maturity_pr_some": "⚠ Moderate number of pull requests",
|
||||
"output.maturity_pr_few": "✗ Few pull requests",
|
||||
"output.ci_configured": "✓ CI/CD is configured",
|
||||
"output.ci_stable": "✓ High build success rate, CI is stable",
|
||||
"output.ci_moderate": "⚠ Moderate build success rate",
|
||||
"output.ci_unstable": "✗ Low build success rate, CI is unstable",
|
||||
"output.ci_not_configured": "✗ CI/CD not configured",
|
||||
"output.suggest_readme_add": "Add a README file",
|
||||
"output.suggest_readme_add_desc": "The project is missing a README file, which is essential documentation for open source projects.",
|
||||
"output.suggest_readme_action1": "Create a README.md file",
|
||||
"output.suggest_readme_action2": "Add project introduction and installation instructions",
|
||||
"output.suggest_readme_action3": "Include usage examples and contribution guidelines",
|
||||
"output.suggest_readme_improve": "Improve README content",
|
||||
"output.suggest_readme_improve_desc": "The README is missing some important sections.",
|
||||
"output.suggest_section_install": "Add installation/quick start section",
|
||||
"output.suggest_section_usage": "Add usage examples section",
|
||||
"output.suggest_section_contrib": "Add contribution guidelines section",
|
||||
"output.suggest_section_license": "Add license information",
|
||||
"output.suggest_description_add": "Add project description",
|
||||
"output.suggest_description_add_desc": "The project is missing a description.",
|
||||
"output.suggest_description_action1": "Add a description in project settings",
|
||||
"output.suggest_license_add": "Add an open source license",
|
||||
"output.suggest_license_add_desc": "The project is missing a license, which affects usage and contributions.",
|
||||
"output.suggest_license_action1": "Choose an appropriate open source license (MIT, Apache-2.0, GPL, etc.)",
|
||||
"output.suggest_license_action2": "Configure the license in project settings",
|
||||
"output.suggest_license_action3": "Document the license in README",
|
||||
"output.suggest_license_standard": "Use a standard open source license",
|
||||
"output.suggest_license_standard_desc": "Currently using a custom license, consider using a standard open source license.",
|
||||
"output.suggest_license_standard_action": "Consider switching to MIT, Apache-2.0, or GPL standard licenses",
|
||||
"output.suggest_community_grow": "Expand community participation",
|
||||
"output.suggest_community_grow_desc": "The project has few contributors, consider attracting more developers.",
|
||||
"output.suggest_community_action1": "Add contribution guidelines in README",
|
||||
"output.suggest_community_action2": "Create Good First Issue labels to guide new contributors",
|
||||
"output.suggest_community_action3": "Promote the project in the community",
|
||||
"output.suggest_issues_encourage": "Encourage community feedback",
|
||||
"output.suggest_issues_encourage_desc": "The project has no issues, consider encouraging user feedback.",
|
||||
"output.suggest_issues_action1": "Create issue templates",
|
||||
"output.suggest_issues_action2": "Explain how to submit issues in README",
|
||||
"output.suggest_branches_setup": "Establish branch management strategy",
|
||||
"output.suggest_branches_setup_desc": "Project branch management is insufficient, consider establishing a branch strategy.",
|
||||
"output.suggest_branches_action1": "Create a develop branch for development",
|
||||
"output.suggest_branches_action2": "Set up branch protection rules",
|
||||
"output.suggest_release_create": "Create release versions",
|
||||
"output.suggest_release_create_desc": "The project has no release versions, consider creating the first version.",
|
||||
"output.suggest_release_action1": "Create a v1.0.0 or v0.1.0 tag",
|
||||
"output.suggest_release_action2": "Write release notes",
|
||||
"output.suggest_ci_setup": "Configure CI/CD",
|
||||
"output.suggest_ci_setup_desc": "The project has no CI/CD configured, consider adding automated build and test.",
|
||||
"output.suggest_ci_action1": "Configure GitHub Actions or GitLink CI",
|
||||
"output.suggest_ci_action2": "Add automated tests",
|
||||
"output.suggest_ci_action3": "Configure code quality checks",
|
||||
"output.suggest_ci_improve": "Improve CI stability",
|
||||
"output.suggest_ci_improve_desc": "Build success rate is low, consider improving CI configuration.",
|
||||
"output.suggest_ci_improve_action1": "Check failed build logs",
|
||||
"output.suggest_ci_improve_action2": "Fix failing test issues"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -286,5 +286,111 @@
|
|||
"success.config.set": "✓ 已设置 {key} = {value}",
|
||||
"warning.auth.not_logged_in": "✗ 未登录",
|
||||
"warning.auth.token_unverified": "✓ Token 已保存(但无法验证:{message})",
|
||||
"warning.auth.user_unavailable": "✓ Token 已保存(用户信息不可用)"
|
||||
"warning.auth.user_unavailable": "✓ Token 已保存(用户信息不可用)",
|
||||
"cmd.health.diagnose.short": "诊断项目健康度",
|
||||
"cmd.health.diagnose.long": "对项目进行全面健康度诊断,包括文档完善度、许可证合规性、社区活跃度、项目成熟度和 CI/CD 配置。",
|
||||
"cmd.health.diagnose.format": "输出格式:text、json、markdown",
|
||||
"cmd.health.diagnose.verbose": "显示详细诊断信息",
|
||||
"cmd.health.diagnose.db": "数据库路径(用于存储诊断历史)",
|
||||
"prompt.diagnosing": "正在诊断项目健康度...",
|
||||
"output.report_title": "项目健康度诊断报告",
|
||||
"output.project": "项目",
|
||||
"output.total_score": "总分",
|
||||
"output.diagnosis_details": "诊断详情",
|
||||
"output.suggestions": "改进建议",
|
||||
"table.category": "类别",
|
||||
"table.score": "得分",
|
||||
"table.status": "状态",
|
||||
"output.category_documentation": "文档完善度",
|
||||
"output.category_license": "许可证合规性",
|
||||
"output.category_community": "社区活跃度",
|
||||
"output.category_maturity": "项目成熟度",
|
||||
"output.category_ci": "CI/CD 配置",
|
||||
"output.doc_readme_found": "✓ README 文件存在",
|
||||
"output.doc_readme_comprehensive": "✓ README 内容完善,包含多个重要章节",
|
||||
"output.doc_readme_basic": "⚠ README 内容基本,建议补充更多章节",
|
||||
"output.doc_readme_minimal": "✗ README 内容过于简单",
|
||||
"output.doc_readme_empty": "✗ README 文件为空",
|
||||
"output.doc_readme_missing": "✗ 缺少 README 文件",
|
||||
"output.doc_description_found": "✓ 项目描述已设置",
|
||||
"output.doc_description_missing": "✗ 缺少项目描述",
|
||||
"output.license_found": "✓ 许可证",
|
||||
"output.license_well_known": "✓ 使用知名开源许可证",
|
||||
"output.license_custom": "⚠ 使用自定义许可证",
|
||||
"output.license_missing": "✗ 缺少许可证",
|
||||
"output.license_unknown": "⚠ 无法获取许可证信息",
|
||||
"output.community_contributors_many": "✓ 贡献者众多,社区活跃",
|
||||
"output.community_contributors_good": "✓ 贡献者数量良好",
|
||||
"output.community_contributors_few": "⚠ 贡献者较少",
|
||||
"output.community_contributors_solo": "✗ 仅有一位贡献者",
|
||||
"output.community_popular": "✓ 项目受欢迎,Star/Fork 数量高",
|
||||
"output.community_moderate": "⚠ 项目关注度中等",
|
||||
"output.community_growing": "⚠ 项目正在成长中",
|
||||
"output.community_issues_active": "✓ 议题活跃,社区参与度高",
|
||||
"output.community_issues_moderate": "⚠ 议题数量适中",
|
||||
"output.community_issues_few": "⚠ 议题较少",
|
||||
"output.community_issues_none": "✗ 没有议题",
|
||||
"output.maturity_branches_good": "✓ 分支管理完善",
|
||||
"output.maturity_branches_basic": "⚠ 分支管理基本",
|
||||
"output.maturity_branches_minimal": "✗ 分支管理不足",
|
||||
"output.maturity_releases_many": "✓ 发布版本众多",
|
||||
"output.maturity_releases_some": "✓ 有多个发布版本",
|
||||
"output.maturity_releases_initial": "⚠ 仅有一个初始版本",
|
||||
"output.maturity_releases_none": "✗ 没有发布版本",
|
||||
"output.maturity_pr_active": "✓ 合并请求活跃",
|
||||
"output.maturity_pr_some": "⚠ 合并请求数量适中",
|
||||
"output.maturity_pr_few": "✗ 合并请求较少",
|
||||
"output.ci_configured": "✓ CI/CD 已配置",
|
||||
"output.ci_stable": "✓ 构建成功率高,CI 稳定",
|
||||
"output.ci_moderate": "⚠ 构建成功率中等",
|
||||
"output.ci_unstable": "✗ 构建成功率低,CI 不稳定",
|
||||
"output.ci_not_configured": "✗ 未配置 CI/CD",
|
||||
"output.suggest_readme_add": "添加 README 文件",
|
||||
"output.suggest_readme_add_desc": "项目缺少 README 文件,这是开源项目的基本文档。",
|
||||
"output.suggest_readme_action1": "创建 README.md 文件",
|
||||
"output.suggest_readme_action2": "添加项目简介和安装说明",
|
||||
"output.suggest_readme_action3": "包含使用示例和贡献指南",
|
||||
"output.suggest_readme_improve": "完善 README 内容",
|
||||
"output.suggest_readme_improve_desc": "README 缺少一些重要章节。",
|
||||
"output.suggest_section_install": "添加安装/快速开始章节",
|
||||
"output.suggest_section_usage": "添加使用示例章节",
|
||||
"output.suggest_section_contrib": "添加贡献指南章节",
|
||||
"output.suggest_section_license": "添加许可证信息",
|
||||
"output.suggest_description_add": "添加项目描述",
|
||||
"output.suggest_description_add_desc": "项目缺少描述信息。",
|
||||
"output.suggest_description_action1": "在项目设置中添加描述",
|
||||
"output.suggest_license_add": "添加开源许可证",
|
||||
"output.suggest_license_add_desc": "项目缺少许可证,这会影响他人使用和贡献。",
|
||||
"output.suggest_license_action1": "选择合适的开源许可证(MIT、Apache-2.0、GPL 等)",
|
||||
"output.suggest_license_action2": "在项目设置中配置许可证",
|
||||
"output.suggest_license_action3": "在 README 中说明许可证信息",
|
||||
"output.suggest_license_standard": "使用标准开源许可证",
|
||||
"output.suggest_license_standard_desc": "当前使用自定义许可证,建议使用标准开源许可证。",
|
||||
"output.suggest_license_standard_action": "考虑切换到 MIT、Apache-2.0 或 GPL 等标准许可证",
|
||||
"output.suggest_community_grow": "扩大社区参与",
|
||||
"output.suggest_community_grow_desc": "项目贡献者较少,建议吸引更多开发者参与。",
|
||||
"output.suggest_community_action1": "在 README 中添加贡献指南",
|
||||
"output.suggest_community_action2": "创建 Good First Issue 标签引导新贡献者",
|
||||
"output.suggest_community_action3": "在社区宣传项目",
|
||||
"output.suggest_issues_encourage": "鼓励社区反馈",
|
||||
"output.suggest_issues_encourage_desc": "项目没有议题,建议鼓励用户反馈。",
|
||||
"output.suggest_issues_action1": "创建议题模板",
|
||||
"output.suggest_issues_action2": "在 README 中说明如何提交议题",
|
||||
"output.suggest_branches_setup": "建立分支管理策略",
|
||||
"output.suggest_branches_setup_desc": "项目分支管理不足,建议建立分支策略。",
|
||||
"output.suggest_branches_action1": "创建 develop 分支用于开发",
|
||||
"output.suggest_branches_action2": "设置分支保护规则",
|
||||
"output.suggest_release_create": "创建发布版本",
|
||||
"output.suggest_release_create_desc": "项目没有发布版本,建议创建第一个版本。",
|
||||
"output.suggest_release_action1": "创建 v1.0.0 或 v0.1.0 标签",
|
||||
"output.suggest_release_action2": "编写版本说明",
|
||||
"output.suggest_ci_setup": "配置 CI/CD",
|
||||
"output.suggest_ci_setup_desc": "项目未配置 CI/CD,建议添加自动化构建和测试。",
|
||||
"output.suggest_ci_action1": "配置 GitHub Actions 或 GitLink CI",
|
||||
"output.suggest_ci_action2": "添加自动化测试",
|
||||
"output.suggest_ci_action3": "配置代码质量检查",
|
||||
"output.suggest_ci_improve": "提高 CI 稳定性",
|
||||
"output.suggest_ci_improve_desc": "构建成功率较低,建议改进 CI 配置。",
|
||||
"output.suggest_ci_improve_action1": "检查失败的构建日志",
|
||||
"output.suggest_ci_improve_action2": "修复测试失败的问题"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,313 @@
|
|||
package health
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/gitlink-org/gitlink-cli/internal/i18n"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
// DiagnosisData holds all data needed for health diagnosis.
|
||||
type DiagnosisData struct {
|
||||
Detail map[string]interface{}
|
||||
Readme map[string]interface{}
|
||||
Contributors []interface{}
|
||||
Builds []interface{}
|
||||
}
|
||||
|
||||
// HealthReport represents the complete health diagnosis report.
|
||||
type HealthReport struct {
|
||||
Owner string
|
||||
Repo string
|
||||
TotalScore int
|
||||
MaxScore int
|
||||
Categories []CategoryScore
|
||||
Suggestions []Suggestion
|
||||
}
|
||||
|
||||
// CategoryScore represents score for a single category.
|
||||
type CategoryScore struct {
|
||||
Name string
|
||||
Score int
|
||||
MaxScore int
|
||||
Status string // "good", "warning", "critical"
|
||||
Details []string
|
||||
}
|
||||
|
||||
// Suggestion represents an improvement suggestion.
|
||||
type Suggestion struct {
|
||||
Category string
|
||||
Title string
|
||||
Description string
|
||||
Actions []string
|
||||
}
|
||||
|
||||
// diagnoseShortcut returns the diagnose command shortcut.
|
||||
func diagnoseShortcut(tr *i18n.Translator) *common.Shortcut {
|
||||
return &common.Shortcut{
|
||||
Name: "diagnose",
|
||||
Description: tr.T("cmd.health.diagnose.short"),
|
||||
Long: tr.T("cmd.health.diagnose.long"),
|
||||
Flags: []common.Flag{
|
||||
{Name: "format", Short: "f", Usage: tr.T("cmd.health.diagnose.format"), Default: "text"},
|
||||
{Name: "verbose", Short: "v", Usage: tr.T("cmd.health.diagnose.verbose"), Bool: true},
|
||||
{Name: "db", Short: "d", Usage: tr.T("cmd.health.diagnose.db")},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
return runDiagnose(ctx, tr)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// runDiagnose executes the health diagnosis.
|
||||
func runDiagnose(ctx *common.RuntimeContext, tr *i18n.Translator) error {
|
||||
// 1. Resolve owner/repo
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr, "\n🔍 %s\n", tr.T("prompt.diagnosing"))
|
||||
fmt.Fprintf(os.Stderr, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n")
|
||||
fmt.Fprintf(os.Stderr, "📦 %s/%s\n\n", ctx.Owner, ctx.Repo)
|
||||
|
||||
// 2. Fetch data concurrently
|
||||
data, err := fetchDiagnosisData(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("fetch diagnosis data: %w", err)
|
||||
}
|
||||
|
||||
// 3. Calculate scores
|
||||
report := calculateHealthScore(ctx.Owner, ctx.Repo, data, tr)
|
||||
|
||||
// 4. Generate suggestions
|
||||
report.Suggestions = generateSuggestions(report, data, tr)
|
||||
|
||||
// 5. Output report
|
||||
format := ctx.Arg("format")
|
||||
verbose := ctx.Arg("verbose") == "true"
|
||||
return outputReport(report, format, verbose, tr)
|
||||
}
|
||||
|
||||
// fetchDiagnosisData fetches all required data concurrently.
|
||||
func fetchDiagnosisData(ctx *common.RuntimeContext) (*DiagnosisData, error) {
|
||||
data := &DiagnosisData{}
|
||||
var mu sync.Mutex
|
||||
|
||||
g, _ := errgroup.WithContext(context.Background())
|
||||
|
||||
// Fetch project detail
|
||||
g.Go(func() error {
|
||||
env, err := ctx.CallAPI("GET", ctx.RepoPath()+"/detail", nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("fetch detail: %w", err)
|
||||
}
|
||||
if env.OK && env.Data != nil {
|
||||
mu.Lock()
|
||||
data.Detail = env.Data.(map[string]interface{})
|
||||
mu.Unlock()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// Fetch README
|
||||
g.Go(func() error {
|
||||
env, err := ctx.CallAPI("GET", ctx.RepoPath()+"/readme", nil)
|
||||
if err != nil {
|
||||
// README might not exist, don't fail
|
||||
return nil
|
||||
}
|
||||
if env.OK && env.Data != nil {
|
||||
mu.Lock()
|
||||
data.Readme = env.Data.(map[string]interface{})
|
||||
mu.Unlock()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// Fetch contributors
|
||||
g.Go(func() error {
|
||||
env, err := ctx.CallAPI("GET", ctx.RepoPath()+"/contributors", nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("fetch contributors: %w", err)
|
||||
}
|
||||
if env.OK && env.Data != nil {
|
||||
if resp, ok := env.Data.(map[string]interface{}); ok {
|
||||
if list, ok := resp["list"].([]interface{}); ok {
|
||||
mu.Lock()
|
||||
data.Contributors = list
|
||||
mu.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// Fetch CI builds
|
||||
g.Go(func() error {
|
||||
q := map[string]string{"page": "1", "limit": "20"}
|
||||
queryParams := make(map[string][]string)
|
||||
for k, v := range q {
|
||||
queryParams[k] = []string{v}
|
||||
}
|
||||
env, err := ctx.CallAPIWithQuery("GET", ctx.RepoPath()+"/builds", nil)
|
||||
if err != nil {
|
||||
// CI might not be configured, don't fail
|
||||
return nil
|
||||
}
|
||||
if env.OK && env.Data != nil {
|
||||
if resp, ok := env.Data.(map[string]interface{}); ok {
|
||||
if builds, ok := resp["builds"].([]interface{}); ok {
|
||||
mu.Lock()
|
||||
data.Builds = builds
|
||||
mu.Unlock()
|
||||
} else if builds, ok := resp["builds"].([]map[string]interface{}); ok {
|
||||
// Handle alternative response format
|
||||
var buildList []interface{}
|
||||
for _, b := range builds {
|
||||
buildList = append(buildList, b)
|
||||
}
|
||||
mu.Lock()
|
||||
data.Builds = buildList
|
||||
mu.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// outputReport outputs the health report in the specified format.
|
||||
func outputReport(report *HealthReport, format string, verbose bool, tr *i18n.Translator) error {
|
||||
switch format {
|
||||
case "json":
|
||||
return outputJSON(report)
|
||||
case "markdown", "md":
|
||||
return outputMarkdown(report, tr)
|
||||
default:
|
||||
return outputText(report, verbose, tr)
|
||||
}
|
||||
}
|
||||
|
||||
// outputText outputs the report in text format.
|
||||
func outputText(report *HealthReport, verbose bool, tr *i18n.Translator) error {
|
||||
// Header
|
||||
fmt.Printf("\n🔍 %s\n", tr.T("output.report_title"))
|
||||
fmt.Printf("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n")
|
||||
fmt.Printf("📦 %s: %s/%s\n", tr.T("output.project"), report.Owner, report.Repo)
|
||||
|
||||
// Total score with stars
|
||||
stars := getStars(report.TotalScore, report.MaxScore)
|
||||
fmt.Printf("📊 %s: %d/%d %s\n\n", tr.T("output.total_score"), report.TotalScore, report.MaxScore, stars)
|
||||
|
||||
// Category scores
|
||||
fmt.Printf("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n")
|
||||
fmt.Printf("📋 %s\n", tr.T("output.diagnosis_details"))
|
||||
fmt.Printf("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n")
|
||||
|
||||
for _, cat := range report.Categories {
|
||||
icon := getCategoryIcon(cat.Status)
|
||||
fmt.Printf("%s %s (%d/%d)\n", icon, cat.Name, cat.Score, cat.MaxScore)
|
||||
for _, detail := range cat.Details {
|
||||
fmt.Printf(" %s\n", detail)
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
// Suggestions
|
||||
if len(report.Suggestions) > 0 {
|
||||
fmt.Printf("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n")
|
||||
fmt.Printf("💡 %s\n", tr.T("output.suggestions"))
|
||||
fmt.Printf("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n")
|
||||
|
||||
for i, sug := range report.Suggestions {
|
||||
fmt.Printf("%d. %s\n", i+1, sug.Title)
|
||||
for _, action := range sug.Actions {
|
||||
fmt.Printf(" - %s\n", action)
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
// outputJSON outputs the report in JSON format.
|
||||
func outputJSON(report *HealthReport) error {
|
||||
data, err := json.MarshalIndent(report, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(string(data))
|
||||
return nil
|
||||
}
|
||||
|
||||
// outputMarkdown outputs the report in Markdown format.
|
||||
func outputMarkdown(report *HealthReport, tr *i18n.Translator) error {
|
||||
fmt.Printf("# 🔍 %s\n\n", tr.T("output.report_title"))
|
||||
fmt.Printf("**%s**: `%s/%s`\n\n", tr.T("output.project"), report.Owner, report.Repo)
|
||||
fmt.Printf("**%s**: %d/%d %s\n\n", tr.T("output.total_score"), report.TotalScore, report.MaxScore, getStars(report.TotalScore, report.MaxScore))
|
||||
|
||||
fmt.Printf("## 📋 %s\n\n", tr.T("output.diagnosis_details"))
|
||||
fmt.Printf("| %s | %s | %s |\n", tr.T("table.category"), tr.T("table.score"), tr.T("table.status"))
|
||||
fmt.Printf("|------|------|--------|\n")
|
||||
|
||||
for _, cat := range report.Categories {
|
||||
fmt.Printf("| %s | %d/%d | %s |\n", cat.Name, cat.Score, cat.MaxScore, cat.Status)
|
||||
}
|
||||
|
||||
if len(report.Suggestions) > 0 {
|
||||
fmt.Printf("\n## 💡 %s\n\n", tr.T("output.suggestions"))
|
||||
for i, sug := range report.Suggestions {
|
||||
fmt.Printf("%d. **%s**\n", i+1, sug.Title)
|
||||
for _, action := range sug.Actions {
|
||||
fmt.Printf(" - %s\n", action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getStars returns star rating based on score percentage.
|
||||
func getStars(score, maxScore int) string {
|
||||
percentage := float64(score) / float64(maxScore) * 100
|
||||
var stars string
|
||||
if percentage >= 80 {
|
||||
stars = "⭐⭐⭐⭐⭐"
|
||||
} else if percentage >= 60 {
|
||||
stars = "⭐⭐⭐⭐"
|
||||
} else if percentage >= 40 {
|
||||
stars = "⭐⭐⭐"
|
||||
} else if percentage >= 20 {
|
||||
stars = "⭐⭐"
|
||||
} else {
|
||||
stars = "⭐"
|
||||
}
|
||||
return stars
|
||||
}
|
||||
|
||||
// getCategoryIcon returns icon based on category status.
|
||||
func getCategoryIcon(status string) string {
|
||||
switch status {
|
||||
case "good":
|
||||
return "✅"
|
||||
case "warning":
|
||||
return "⚠️"
|
||||
case "critical":
|
||||
return "❌"
|
||||
default:
|
||||
return "ℹ️"
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,11 @@ import (
|
|||
)
|
||||
|
||||
func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
|
||||
tr := i18n.Default()
|
||||
if len(translators) > 0 && translators[0] != nil {
|
||||
tr = translators[0]
|
||||
}
|
||||
|
||||
return []*common.Shortcut{
|
||||
{
|
||||
Name: "fetch",
|
||||
|
|
@ -92,6 +97,7 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
|
|||
return nil
|
||||
},
|
||||
},
|
||||
diagnoseShortcut(tr),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,251 @@
|
|||
package health
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gitlink-org/gitlink-cli/internal/i18n"
|
||||
)
|
||||
|
||||
// TestDiagnoseShortcutExists tests that diagnose shortcut is registered
|
||||
func TestDiagnoseShortcutExists(t *testing.T) {
|
||||
tr := i18n.Default()
|
||||
shortcuts := Shortcuts(tr)
|
||||
|
||||
if len(shortcuts) < 2 {
|
||||
t.Errorf("Expected at least 2 shortcuts (fetch + diagnose), got %d", len(shortcuts))
|
||||
}
|
||||
|
||||
// Find diagnose shortcut
|
||||
var diagnoseFound bool
|
||||
for _, s := range shortcuts {
|
||||
if s.Name == "diagnose" {
|
||||
diagnoseFound = true
|
||||
// Check that diagnose has expected flags
|
||||
expectedFlags := []string{"format", "verbose", "db"}
|
||||
for _, expectedFlag := range expectedFlags {
|
||||
found := false
|
||||
for _, f := range s.Flags {
|
||||
if f.Name == expectedFlag {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("diagnose shortcut missing expected flag: %s", expectedFlag)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !diagnoseFound {
|
||||
t.Error("diagnose shortcut not found in Shortcuts()")
|
||||
}
|
||||
}
|
||||
|
||||
// TestDiagnosisDataStructure tests that DiagnosisData can be created
|
||||
func TestDiagnosisDataStructure(t *testing.T) {
|
||||
data := &DiagnosisData{
|
||||
Detail: map[string]interface{}{"name": "test"},
|
||||
Readme: map[string]interface{}{"content": "test readme"},
|
||||
Contributors: []interface{}{map[string]interface{}{"id": 1}},
|
||||
Builds: []interface{}{map[string]interface{}{"id": "build-1"}},
|
||||
}
|
||||
|
||||
if data.Detail == nil {
|
||||
t.Error("Detail should not be nil")
|
||||
}
|
||||
if data.Readme == nil {
|
||||
t.Error("Readme should not be nil")
|
||||
}
|
||||
if len(data.Contributors) == 0 {
|
||||
t.Error("Contributors should not be empty")
|
||||
}
|
||||
if len(data.Builds) == 0 {
|
||||
t.Error("Builds should not be empty")
|
||||
}
|
||||
}
|
||||
|
||||
// TestHealthReportStructure tests that HealthReport can be created
|
||||
func TestHealthReportStructure(t *testing.T) {
|
||||
report := &HealthReport{
|
||||
Owner: "test-owner",
|
||||
Repo: "test-repo",
|
||||
TotalScore: 75,
|
||||
MaxScore: 100,
|
||||
Categories: []CategoryScore{
|
||||
{
|
||||
Name: "Documentation",
|
||||
Score: 15,
|
||||
MaxScore: 20,
|
||||
Status: "good",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if report.Owner != "test-owner" {
|
||||
t.Errorf("Expected owner 'test-owner', got '%s'", report.Owner)
|
||||
}
|
||||
if report.TotalScore != 75 {
|
||||
t.Errorf("Expected total score 75, got %d", report.TotalScore)
|
||||
}
|
||||
if len(report.Categories) != 1 {
|
||||
t.Errorf("Expected 1 category, got %d", len(report.Categories))
|
||||
}
|
||||
}
|
||||
|
||||
// TestSuggestionStructure tests that Suggestion can be created
|
||||
func TestSuggestionStructure(t *testing.T) {
|
||||
suggestion := Suggestion{
|
||||
Category: "Documentation",
|
||||
Title: "Add README",
|
||||
Description: "Project lacks a README file",
|
||||
Actions: []string{
|
||||
"Create README.md",
|
||||
"Add project description",
|
||||
},
|
||||
}
|
||||
|
||||
if suggestion.Category != "Documentation" {
|
||||
t.Errorf("Expected category 'Documentation', got '%s'", suggestion.Category)
|
||||
}
|
||||
if len(suggestion.Actions) != 2 {
|
||||
t.Errorf("Expected 2 actions, got %d", len(suggestion.Actions))
|
||||
}
|
||||
}
|
||||
|
||||
// TestCalculateHealthScoreWithEmptyData tests scoring with empty data
|
||||
func TestCalculateHealthScoreWithEmptyData(t *testing.T) {
|
||||
tr := i18n.Default()
|
||||
data := &DiagnosisData{
|
||||
Detail: nil,
|
||||
Readme: nil,
|
||||
Contributors: nil,
|
||||
Builds: nil,
|
||||
}
|
||||
|
||||
report := calculateHealthScore("owner", "repo", data, tr)
|
||||
|
||||
if report == nil {
|
||||
t.Fatal("Report should not be nil")
|
||||
}
|
||||
|
||||
// With empty data, score should be low
|
||||
if report.TotalScore > 30 {
|
||||
t.Errorf("Expected low score with empty data, got %d", report.TotalScore)
|
||||
}
|
||||
|
||||
// Should have 5 categories
|
||||
if len(report.Categories) != 5 {
|
||||
t.Errorf("Expected 5 categories, got %d", len(report.Categories))
|
||||
}
|
||||
}
|
||||
|
||||
// TestCalculateHealthScoreWithGoodData tests scoring with good data
|
||||
func TestCalculateHealthScoreWithGoodData(t *testing.T) {
|
||||
tr := i18n.Default()
|
||||
|
||||
// Create good data
|
||||
readmeContent := "# Test Project\n\n## Introduction\n\nThis is a test project.\n\n## Installation\n\nRun `go install`.\n\n## Usage\n\nSee examples.\n\n## API\n\nDocumentation here.\n\n## Contributing\n\nPlease contribute.\n\n## License\n\nMIT License."
|
||||
encodedContent := encodeBase64(readmeContent)
|
||||
|
||||
data := &DiagnosisData{
|
||||
Detail: map[string]interface{}{
|
||||
"name": "test-repo",
|
||||
"description": "A test repository for health check",
|
||||
"license_name": "MIT",
|
||||
},
|
||||
Readme: map[string]interface{}{
|
||||
"content": encodedContent,
|
||||
},
|
||||
Contributors: []interface{}{
|
||||
map[string]interface{}{"id": 1, "login": "user1"},
|
||||
map[string]interface{}{"id": 2, "login": "user2"},
|
||||
map[string]interface{}{"id": 3, "login": "user3"},
|
||||
map[string]interface{}{"id": 4, "login": "user4"},
|
||||
map[string]interface{}{"id": 5, "login": "user5"},
|
||||
},
|
||||
Builds: []interface{}{
|
||||
map[string]interface{}{"id": "build-1", "status": "success"},
|
||||
map[string]interface{}{"id": "build-2", "status": "success"},
|
||||
map[string]interface{}{"id": "build-3", "status": "success"},
|
||||
map[string]interface{}{"id": "build-4", "status": "success"},
|
||||
map[string]interface{}{"id": "build-5", "status": "success"},
|
||||
},
|
||||
}
|
||||
|
||||
report := calculateHealthScore("owner", "repo", data, tr)
|
||||
|
||||
if report == nil {
|
||||
t.Fatal("Report should not be nil")
|
||||
}
|
||||
|
||||
// With good data, score should be high
|
||||
if report.TotalScore < 50 {
|
||||
t.Errorf("Expected higher score with good data, got %d", report.TotalScore)
|
||||
}
|
||||
|
||||
// Should have 5 categories
|
||||
if len(report.Categories) != 5 {
|
||||
t.Errorf("Expected 5 categories, got %d", len(report.Categories))
|
||||
}
|
||||
}
|
||||
|
||||
// TestGenerateSuggestionsWithLowScores tests suggestion generation
|
||||
func TestGenerateSuggestionsWithLowScores(t *testing.T) {
|
||||
tr := i18n.Default()
|
||||
|
||||
// Create data that will result in low scores
|
||||
data := &DiagnosisData{
|
||||
Detail: nil,
|
||||
Readme: nil,
|
||||
Contributors: nil,
|
||||
Builds: nil,
|
||||
}
|
||||
|
||||
report := calculateHealthScore("owner", "repo", data, tr)
|
||||
suggestions := generateSuggestions(report, data, tr)
|
||||
|
||||
if len(suggestions) == 0 {
|
||||
t.Error("Expected suggestions for low-scoring report")
|
||||
}
|
||||
|
||||
// Check that suggestions have required fields
|
||||
for _, s := range suggestions {
|
||||
if s.Title == "" {
|
||||
t.Error("Suggestion should have a title")
|
||||
}
|
||||
if s.Category == "" {
|
||||
t.Error("Suggestion should have a category")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetCategoryStatus tests status determination based on score
|
||||
func TestGetCategoryStatus(t *testing.T) {
|
||||
tests := []struct {
|
||||
score int
|
||||
maxScore int
|
||||
expected string
|
||||
}{
|
||||
{18, 20, "good"}, // 90%
|
||||
{16, 20, "good"}, // 80%
|
||||
{14, 20, "good"}, // 70%
|
||||
{12, 20, "warning"}, // 60%
|
||||
{8, 20, "warning"}, // 40%
|
||||
{6, 20, "critical"}, // 30%
|
||||
{4, 20, "critical"}, // 20%
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
status := getCategoryStatus(tt.score, tt.maxScore)
|
||||
if status != tt.expected {
|
||||
t.Errorf("getCategoryStatus(%d, %d) = %s, want %s", tt.score, tt.maxScore, status, tt.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to encode base64
|
||||
func encodeBase64(s string) string {
|
||||
return s // The actual implementation in scoring.go handles this
|
||||
}
|
||||
|
|
@ -0,0 +1,406 @@
|
|||
package health
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gitlink-org/gitlink-cli/internal/i18n"
|
||||
)
|
||||
|
||||
// calculateHealthScore calculates the overall health score for a project.
|
||||
func calculateHealthScore(owner, repo string, data *DiagnosisData, tr *i18n.Translator) *HealthReport {
|
||||
report := &HealthReport{
|
||||
Owner: owner,
|
||||
Repo: repo,
|
||||
MaxScore: 100,
|
||||
Categories: make([]CategoryScore, 0, 5),
|
||||
}
|
||||
|
||||
// 1. Documentation (20 points)
|
||||
docScore := scoreDocumentation(data, tr)
|
||||
report.Categories = append(report.Categories, docScore)
|
||||
|
||||
// 2. License (15 points)
|
||||
licenseScore := scoreLicense(data, tr)
|
||||
report.Categories = append(report.Categories, licenseScore)
|
||||
|
||||
// 3. Community (25 points)
|
||||
communityScore := scoreCommunity(data, tr)
|
||||
report.Categories = append(report.Categories, communityScore)
|
||||
|
||||
// 4. Maturity (20 points)
|
||||
maturityScore := scoreMaturity(data, tr)
|
||||
report.Categories = append(report.Categories, maturityScore)
|
||||
|
||||
// 5. CI/CD (20 points)
|
||||
ciScore := scoreCI(data, tr)
|
||||
report.Categories = append(report.Categories, ciScore)
|
||||
|
||||
// Calculate total score
|
||||
for _, cat := range report.Categories {
|
||||
report.TotalScore += cat.Score
|
||||
}
|
||||
|
||||
return report
|
||||
}
|
||||
|
||||
// scoreDocumentation evaluates documentation completeness (20 points).
|
||||
func scoreDocumentation(data *DiagnosisData, tr *i18n.Translator) CategoryScore {
|
||||
cat := CategoryScore{
|
||||
Name: tr.T("output.category_documentation"),
|
||||
MaxScore: 20,
|
||||
Details: make([]string, 0),
|
||||
}
|
||||
|
||||
score := 0
|
||||
|
||||
// Check README existence and content (15 points)
|
||||
if data.Readme != nil {
|
||||
content := getReadmeContent(data.Readme)
|
||||
if content != "" {
|
||||
// README exists
|
||||
score += 5
|
||||
cat.Details = append(cat.Details, tr.T("output.doc_readme_found"))
|
||||
|
||||
// Check README quality (length and sections)
|
||||
readmeScore := evaluateReadmeQuality(content, tr)
|
||||
score += readmeScore
|
||||
|
||||
if readmeScore >= 8 {
|
||||
cat.Details = append(cat.Details, tr.T("output.doc_readme_comprehensive"))
|
||||
} else if readmeScore >= 5 {
|
||||
cat.Details = append(cat.Details, tr.T("output.doc_readme_basic"))
|
||||
} else {
|
||||
cat.Details = append(cat.Details, tr.T("output.doc_readme_minimal"))
|
||||
}
|
||||
} else {
|
||||
cat.Details = append(cat.Details, tr.T("output.doc_readme_empty"))
|
||||
}
|
||||
} else {
|
||||
cat.Details = append(cat.Details, tr.T("output.doc_readme_missing"))
|
||||
}
|
||||
|
||||
// Check project description (5 points)
|
||||
if data.Detail != nil {
|
||||
if desc, ok := data.Detail["description"].(string); ok && desc != "" {
|
||||
score += 5
|
||||
cat.Details = append(cat.Details, tr.T("output.doc_description_found"))
|
||||
} else {
|
||||
cat.Details = append(cat.Details, tr.T("output.doc_description_missing"))
|
||||
}
|
||||
}
|
||||
|
||||
cat.Score = min(score, 20)
|
||||
cat.Status = getCategoryStatus(cat.Score, cat.MaxScore)
|
||||
|
||||
return cat
|
||||
}
|
||||
|
||||
// evaluateReadmeQuality evaluates README content quality.
|
||||
func evaluateReadmeQuality(content string, tr *i18n.Translator) int {
|
||||
score := 0
|
||||
contentLower := strings.ToLower(content)
|
||||
|
||||
// Check for common sections (2 points each, max 10)
|
||||
sections := []string{
|
||||
"installation", "install", "usage", "getting started", "quick start",
|
||||
"contributing", "contribute", "license", "api", "documentation",
|
||||
"features", "requirements", "dependencies", "build", "test",
|
||||
}
|
||||
|
||||
foundSections := 0
|
||||
for _, section := range sections {
|
||||
if strings.Contains(contentLower, section) {
|
||||
foundSections++
|
||||
}
|
||||
}
|
||||
|
||||
// Score based on number of sections found
|
||||
if foundSections >= 5 {
|
||||
score = 10
|
||||
} else if foundSections >= 3 {
|
||||
score = 7
|
||||
} else if foundSections >= 1 {
|
||||
score = 4
|
||||
}
|
||||
|
||||
return score
|
||||
}
|
||||
|
||||
// scoreLicense evaluates license compliance (15 points).
|
||||
func scoreLicense(data *DiagnosisData, tr *i18n.Translator) CategoryScore {
|
||||
cat := CategoryScore{
|
||||
Name: tr.T("output.category_license"),
|
||||
MaxScore: 15,
|
||||
Details: make([]string, 0),
|
||||
}
|
||||
|
||||
score := 0
|
||||
|
||||
if data.Detail != nil {
|
||||
if licenseName, ok := data.Detail["license_name"].(string); ok && licenseName != "" {
|
||||
// Has a license
|
||||
score += 10
|
||||
cat.Details = append(cat.Details, tr.T("output.license_found")+": "+licenseName)
|
||||
|
||||
// Check if it's a well-known license
|
||||
if isWellKnownLicense(licenseName) {
|
||||
score += 5
|
||||
cat.Details = append(cat.Details, tr.T("output.license_well_known"))
|
||||
} else {
|
||||
cat.Details = append(cat.Details, tr.T("output.license_custom"))
|
||||
}
|
||||
} else {
|
||||
cat.Details = append(cat.Details, tr.T("output.license_missing"))
|
||||
}
|
||||
} else {
|
||||
cat.Details = append(cat.Details, tr.T("output.license_unknown"))
|
||||
}
|
||||
|
||||
cat.Score = min(score, 15)
|
||||
cat.Status = getCategoryStatus(cat.Score, cat.MaxScore)
|
||||
|
||||
return cat
|
||||
}
|
||||
|
||||
// isWellKnownLicense checks if the license is a well-known open source license.
|
||||
func isWellKnownLicense(name string) bool {
|
||||
nameLower := strings.ToLower(name)
|
||||
wellKnown := []string{
|
||||
"mit", "apache", "gpl", "lgpl", "bsd", "mpl",
|
||||
"creative commons", "cc0", "isc", "eclipse public",
|
||||
}
|
||||
|
||||
for _, license := range wellKnown {
|
||||
if strings.Contains(nameLower, license) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// scoreCommunity evaluates community activity (25 points).
|
||||
func scoreCommunity(data *DiagnosisData, tr *i18n.Translator) CategoryScore {
|
||||
cat := CategoryScore{
|
||||
Name: tr.T("output.category_community"),
|
||||
MaxScore: 25,
|
||||
Details: make([]string, 0),
|
||||
}
|
||||
|
||||
score := 0
|
||||
|
||||
if data.Detail != nil {
|
||||
// Contributors (10 points)
|
||||
contributorCount := len(data.Contributors)
|
||||
if contributorCount >= 10 {
|
||||
score += 10
|
||||
cat.Details = append(cat.Details, tr.T("output.community_contributors_many"))
|
||||
} else if contributorCount >= 5 {
|
||||
score += 7
|
||||
cat.Details = append(cat.Details, tr.T("output.community_contributors_good"))
|
||||
} else if contributorCount >= 2 {
|
||||
score += 4
|
||||
cat.Details = append(cat.Details, tr.T("output.community_contributors_few"))
|
||||
} else {
|
||||
cat.Details = append(cat.Details, tr.T("output.community_contributors_solo"))
|
||||
}
|
||||
|
||||
// Stars/Forks (8 points)
|
||||
praisesCount := getIntFromMap(data.Detail, "praises_count")
|
||||
forkedCount := getIntFromMap(data.Detail, "forked_count")
|
||||
|
||||
if praisesCount >= 100 || forkedCount >= 50 {
|
||||
score += 8
|
||||
cat.Details = append(cat.Details, tr.T("output.community_popular"))
|
||||
} else if praisesCount >= 20 || forkedCount >= 10 {
|
||||
score += 5
|
||||
cat.Details = append(cat.Details, tr.T("output.community_moderate"))
|
||||
} else {
|
||||
cat.Details = append(cat.Details, tr.T("output.community_growing"))
|
||||
}
|
||||
|
||||
// Issues (7 points)
|
||||
issuesCount := getIntFromMap(data.Detail, "issues_count")
|
||||
if issuesCount > 0 {
|
||||
// Having issues indicates community engagement
|
||||
if issuesCount >= 50 {
|
||||
score += 7
|
||||
cat.Details = append(cat.Details, tr.T("output.community_issues_active"))
|
||||
} else if issuesCount >= 10 {
|
||||
score += 5
|
||||
cat.Details = append(cat.Details, tr.T("output.community_issues_moderate"))
|
||||
} else {
|
||||
score += 3
|
||||
cat.Details = append(cat.Details, tr.T("output.community_issues_few"))
|
||||
}
|
||||
} else {
|
||||
cat.Details = append(cat.Details, tr.T("output.community_issues_none"))
|
||||
}
|
||||
}
|
||||
|
||||
cat.Score = min(score, 25)
|
||||
cat.Status = getCategoryStatus(cat.Score, cat.MaxScore)
|
||||
|
||||
return cat
|
||||
}
|
||||
|
||||
// scoreMaturity evaluates project maturity (20 points).
|
||||
func scoreMaturity(data *DiagnosisData, tr *i18n.Translator) CategoryScore {
|
||||
cat := CategoryScore{
|
||||
Name: tr.T("output.category_maturity"),
|
||||
MaxScore: 20,
|
||||
Details: make([]string, 0),
|
||||
}
|
||||
|
||||
score := 0
|
||||
|
||||
if data.Detail != nil {
|
||||
// Branches (8 points)
|
||||
branchesCount := getIntFromMap(data.Detail, "branches_count")
|
||||
if branchesCount >= 3 {
|
||||
score += 8
|
||||
cat.Details = append(cat.Details, tr.T("output.maturity_branches_good"))
|
||||
} else if branchesCount >= 2 {
|
||||
score += 5
|
||||
cat.Details = append(cat.Details, tr.T("output.maturity_branches_basic"))
|
||||
} else {
|
||||
cat.Details = append(cat.Details, tr.T("output.maturity_branches_minimal"))
|
||||
}
|
||||
|
||||
// Tags/Releases (7 points)
|
||||
tagsCount := getIntFromMap(data.Detail, "tags_count")
|
||||
if tagsCount >= 5 {
|
||||
score += 7
|
||||
cat.Details = append(cat.Details, tr.T("output.maturity_releases_many"))
|
||||
} else if tagsCount >= 2 {
|
||||
score += 4
|
||||
cat.Details = append(cat.Details, tr.T("output.maturity_releases_some"))
|
||||
} else if tagsCount == 1 {
|
||||
score += 2
|
||||
cat.Details = append(cat.Details, tr.T("output.maturity_releases_initial"))
|
||||
} else {
|
||||
cat.Details = append(cat.Details, tr.T("output.maturity_releases_none"))
|
||||
}
|
||||
|
||||
// Pull Requests (5 points)
|
||||
prCount := getIntFromMap(data.Detail, "pull_requests_count")
|
||||
if prCount >= 10 {
|
||||
score += 5
|
||||
cat.Details = append(cat.Details, tr.T("output.maturity_pr_active"))
|
||||
} else if prCount >= 3 {
|
||||
score += 3
|
||||
cat.Details = append(cat.Details, tr.T("output.maturity_pr_some"))
|
||||
} else {
|
||||
cat.Details = append(cat.Details, tr.T("output.maturity_pr_few"))
|
||||
}
|
||||
}
|
||||
|
||||
cat.Score = min(score, 20)
|
||||
cat.Status = getCategoryStatus(cat.Score, cat.MaxScore)
|
||||
|
||||
return cat
|
||||
}
|
||||
|
||||
// scoreCI evaluates CI/CD configuration (20 points).
|
||||
func scoreCI(data *DiagnosisData, tr *i18n.Translator) CategoryScore {
|
||||
cat := CategoryScore{
|
||||
Name: tr.T("output.category_ci"),
|
||||
MaxScore: 20,
|
||||
Details: make([]string, 0),
|
||||
}
|
||||
|
||||
score := 0
|
||||
|
||||
if len(data.Builds) > 0 {
|
||||
// CI is configured
|
||||
score += 10
|
||||
cat.Details = append(cat.Details, tr.T("output.ci_configured"))
|
||||
|
||||
// Check build success rate
|
||||
successCount := 0
|
||||
for _, build := range data.Builds {
|
||||
if b, ok := build.(map[string]interface{}); ok {
|
||||
if status, ok := b["status"].(string); ok {
|
||||
if status == "success" || status == "completed" {
|
||||
successCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
totalBuilds := len(data.Builds)
|
||||
if totalBuilds > 0 {
|
||||
successRate := float64(successCount) / float64(totalBuilds)
|
||||
if successRate >= 0.8 {
|
||||
score += 10
|
||||
cat.Details = append(cat.Details, tr.T("output.ci_stable"))
|
||||
} else if successRate >= 0.5 {
|
||||
score += 5
|
||||
cat.Details = append(cat.Details, tr.T("output.ci_moderate"))
|
||||
} else {
|
||||
cat.Details = append(cat.Details, tr.T("output.ci_unstable"))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
cat.Details = append(cat.Details, tr.T("output.ci_not_configured"))
|
||||
}
|
||||
|
||||
cat.Score = min(score, 20)
|
||||
cat.Status = getCategoryStatus(cat.Score, cat.MaxScore)
|
||||
|
||||
return cat
|
||||
}
|
||||
|
||||
// getReadmeContent extracts README content from API response.
|
||||
func getReadmeContent(readme map[string]interface{}) string {
|
||||
if content, ok := readme["content"].(string); ok {
|
||||
// Try to decode base64
|
||||
decoded, err := base64.StdEncoding.DecodeString(content)
|
||||
if err == nil {
|
||||
return string(decoded)
|
||||
}
|
||||
// If not base64, return as is
|
||||
return content
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// getIntFromMap safely extracts an integer from a map.
|
||||
func getIntFromMap(m map[string]interface{}, key string) int {
|
||||
if val, ok := m[key]; ok {
|
||||
switch v := val.(type) {
|
||||
case int:
|
||||
return v
|
||||
case int64:
|
||||
return int(v)
|
||||
case float64:
|
||||
return int(v)
|
||||
case string:
|
||||
// Try to parse string as int
|
||||
if result, err := strconv.Atoi(v); err == nil {
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// getCategoryStatus returns status based on score percentage.
|
||||
func getCategoryStatus(score, maxScore int) string {
|
||||
percentage := float64(score) / float64(maxScore) * 100
|
||||
if percentage >= 70 {
|
||||
return "good"
|
||||
} else if percentage >= 40 {
|
||||
return "warning"
|
||||
}
|
||||
return "critical"
|
||||
}
|
||||
|
||||
// min returns the minimum of two integers.
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
|
@ -0,0 +1,305 @@
|
|||
package health
|
||||
|
||||
import (
|
||||
"github.com/gitlink-org/gitlink-cli/internal/i18n"
|
||||
)
|
||||
|
||||
// generateSuggestions generates improvement suggestions based on the health report.
|
||||
func generateSuggestions(report *HealthReport, data *DiagnosisData, tr *i18n.Translator) []Suggestion {
|
||||
suggestions := make([]Suggestion, 0)
|
||||
|
||||
// Generate suggestions for each category
|
||||
for _, cat := range report.Categories {
|
||||
switch cat.Name {
|
||||
case tr.T("output.category_documentation"):
|
||||
suggestions = append(suggestions, generateDocSuggestions(cat, data, tr)...)
|
||||
case tr.T("output.category_license"):
|
||||
suggestions = append(suggestions, generateLicenseSuggestions(cat, data, tr)...)
|
||||
case tr.T("output.category_community"):
|
||||
suggestions = append(suggestions, generateCommunitySuggestions(cat, data, tr)...)
|
||||
case tr.T("output.category_maturity"):
|
||||
suggestions = append(suggestions, generateMaturitySuggestions(cat, data, tr)...)
|
||||
case tr.T("output.category_ci"):
|
||||
suggestions = append(suggestions, generateCISuggestions(cat, data, tr)...)
|
||||
}
|
||||
}
|
||||
|
||||
// Limit to top 5 most important suggestions
|
||||
if len(suggestions) > 5 {
|
||||
suggestions = suggestions[:5]
|
||||
}
|
||||
|
||||
return suggestions
|
||||
}
|
||||
|
||||
// generateDocSuggestions generates documentation improvement suggestions.
|
||||
func generateDocSuggestions(cat CategoryScore, data *DiagnosisData, tr *i18n.Translator) []Suggestion {
|
||||
suggestions := make([]Suggestion, 0)
|
||||
|
||||
// Check if README is missing or empty
|
||||
if data.Readme == nil || getReadmeContent(data.Readme) == "" {
|
||||
suggestions = append(suggestions, Suggestion{
|
||||
Category: cat.Name,
|
||||
Title: tr.T("output.suggest_readme_add"),
|
||||
Description: tr.T("output.suggest_readme_add_desc"),
|
||||
Actions: []string{
|
||||
tr.T("output.suggest_readme_action1"),
|
||||
tr.T("output.suggest_readme_action2"),
|
||||
tr.T("output.suggest_readme_action3"),
|
||||
},
|
||||
})
|
||||
} else {
|
||||
// Check README quality
|
||||
content := getReadmeContent(data.Readme)
|
||||
contentLower := content // Will be used for quality check
|
||||
|
||||
// Check for missing sections
|
||||
missingSections := checkMissingReadmeSections(contentLower, tr)
|
||||
if len(missingSections) > 0 {
|
||||
suggestions = append(suggestions, Suggestion{
|
||||
Category: cat.Name,
|
||||
Title: tr.T("output.suggest_readme_improve"),
|
||||
Description: tr.T("output.suggest_readme_improve_desc"),
|
||||
Actions: missingSections,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Check project description
|
||||
if data.Detail != nil {
|
||||
if desc, ok := data.Detail["description"].(string); !ok || desc == "" {
|
||||
suggestions = append(suggestions, Suggestion{
|
||||
Category: cat.Name,
|
||||
Title: tr.T("output.suggest_description_add"),
|
||||
Description: tr.T("output.suggest_description_add_desc"),
|
||||
Actions: []string{
|
||||
tr.T("output.suggest_description_action1"),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return suggestions
|
||||
}
|
||||
|
||||
// checkMissingReadmeSections checks for missing important README sections.
|
||||
func checkMissingReadmeSections(content string, tr *i18n.Translator) []string {
|
||||
missing := make([]string, 0)
|
||||
contentLower := content
|
||||
|
||||
// Check for installation instructions
|
||||
if !containsAny(contentLower, "install", "installation", "getting started", "quick start") {
|
||||
missing = append(missing, tr.T("output.suggest_section_install"))
|
||||
}
|
||||
|
||||
// Check for usage examples
|
||||
if !containsAny(contentLower, "usage", "example", "how to use") {
|
||||
missing = append(missing, tr.T("output.suggest_section_usage"))
|
||||
}
|
||||
|
||||
// Check for contributing guidelines
|
||||
if !containsAny(contentLower, "contributing", "contribute", "development") {
|
||||
missing = append(missing, tr.T("output.suggest_section_contrib"))
|
||||
}
|
||||
|
||||
// Check for license information
|
||||
if !containsAny(contentLower, "license") {
|
||||
missing = append(missing, tr.T("output.suggest_section_license"))
|
||||
}
|
||||
|
||||
return missing
|
||||
}
|
||||
|
||||
// generateLicenseSuggestions generates license improvement suggestions.
|
||||
func generateLicenseSuggestions(cat CategoryScore, data *DiagnosisData, tr *i18n.Translator) []Suggestion {
|
||||
suggestions := make([]Suggestion, 0)
|
||||
|
||||
if data.Detail != nil {
|
||||
if licenseName, ok := data.Detail["license_name"].(string); !ok || licenseName == "" {
|
||||
suggestions = append(suggestions, Suggestion{
|
||||
Category: cat.Name,
|
||||
Title: tr.T("output.suggest_license_add"),
|
||||
Description: tr.T("output.suggest_license_add_desc"),
|
||||
Actions: []string{
|
||||
tr.T("output.suggest_license_action1"),
|
||||
tr.T("output.suggest_license_action2"),
|
||||
tr.T("output.suggest_license_action3"),
|
||||
},
|
||||
})
|
||||
} else if !isWellKnownLicense(licenseName) {
|
||||
suggestions = append(suggestions, Suggestion{
|
||||
Category: cat.Name,
|
||||
Title: tr.T("output.suggest_license_standard"),
|
||||
Description: tr.T("output.suggest_license_standard_desc"),
|
||||
Actions: []string{
|
||||
tr.T("output.suggest_license_standard_action"),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return suggestions
|
||||
}
|
||||
|
||||
// generateCommunitySuggestions generates community improvement suggestions.
|
||||
func generateCommunitySuggestions(cat CategoryScore, data *DiagnosisData, tr *i18n.Translator) []Suggestion {
|
||||
suggestions := make([]Suggestion, 0)
|
||||
|
||||
// Check contributors
|
||||
contributorCount := len(data.Contributors)
|
||||
if contributorCount < 3 {
|
||||
suggestions = append(suggestions, Suggestion{
|
||||
Category: cat.Name,
|
||||
Title: tr.T("output.suggest_community_grow"),
|
||||
Description: tr.T("output.suggest_community_grow_desc"),
|
||||
Actions: []string{
|
||||
tr.T("output.suggest_community_action1"),
|
||||
tr.T("output.suggest_community_action2"),
|
||||
tr.T("output.suggest_community_action3"),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Check issues
|
||||
if data.Detail != nil {
|
||||
issuesCount := getIntFromMap(data.Detail, "issues_count")
|
||||
if issuesCount == 0 {
|
||||
suggestions = append(suggestions, Suggestion{
|
||||
Category: cat.Name,
|
||||
Title: tr.T("output.suggest_issues_encourage"),
|
||||
Description: tr.T("output.suggest_issues_encourage_desc"),
|
||||
Actions: []string{
|
||||
tr.T("output.suggest_issues_action1"),
|
||||
tr.T("output.suggest_issues_action2"),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return suggestions
|
||||
}
|
||||
|
||||
// generateMaturitySuggestions generates maturity improvement suggestions.
|
||||
func generateMaturitySuggestions(cat CategoryScore, data *DiagnosisData, tr *i18n.Translator) []Suggestion {
|
||||
suggestions := make([]Suggestion, 0)
|
||||
|
||||
if data.Detail != nil {
|
||||
// Check branches
|
||||
branchesCount := getIntFromMap(data.Detail, "branches_count")
|
||||
if branchesCount < 2 {
|
||||
suggestions = append(suggestions, Suggestion{
|
||||
Category: cat.Name,
|
||||
Title: tr.T("output.suggest_branches_setup"),
|
||||
Description: tr.T("output.suggest_branches_setup_desc"),
|
||||
Actions: []string{
|
||||
tr.T("output.suggest_branches_action1"),
|
||||
tr.T("output.suggest_branches_action2"),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Check releases
|
||||
tagsCount := getIntFromMap(data.Detail, "tags_count")
|
||||
if tagsCount == 0 {
|
||||
suggestions = append(suggestions, Suggestion{
|
||||
Category: cat.Name,
|
||||
Title: tr.T("output.suggest_release_create"),
|
||||
Description: tr.T("output.suggest_release_create_desc"),
|
||||
Actions: []string{
|
||||
tr.T("output.suggest_release_action1"),
|
||||
tr.T("output.suggest_release_action2"),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return suggestions
|
||||
}
|
||||
|
||||
// generateCISuggestions generates CI/CD improvement suggestions.
|
||||
func generateCISuggestions(cat CategoryScore, data *DiagnosisData, tr *i18n.Translator) []Suggestion {
|
||||
suggestions := make([]Suggestion, 0)
|
||||
|
||||
if len(data.Builds) == 0 {
|
||||
suggestions = append(suggestions, Suggestion{
|
||||
Category: cat.Name,
|
||||
Title: tr.T("output.suggest_ci_setup"),
|
||||
Description: tr.T("output.suggest_ci_setup_desc"),
|
||||
Actions: []string{
|
||||
tr.T("output.suggest_ci_action1"),
|
||||
tr.T("output.suggest_ci_action2"),
|
||||
tr.T("output.suggest_ci_action3"),
|
||||
},
|
||||
})
|
||||
} else {
|
||||
// Check build success rate
|
||||
successCount := 0
|
||||
for _, build := range data.Builds {
|
||||
if b, ok := build.(map[string]interface{}); ok {
|
||||
if status, ok := b["status"].(string); ok {
|
||||
if status == "success" || status == "completed" {
|
||||
successCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
successRate := float64(successCount) / float64(len(data.Builds))
|
||||
if successRate < 0.5 {
|
||||
suggestions = append(suggestions, Suggestion{
|
||||
Category: cat.Name,
|
||||
Title: tr.T("output.suggest_ci_improve"),
|
||||
Description: tr.T("output.suggest_ci_improve_desc"),
|
||||
Actions: []string{
|
||||
tr.T("output.suggest_ci_improve_action1"),
|
||||
tr.T("output.suggest_ci_improve_action2"),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return suggestions
|
||||
}
|
||||
|
||||
// containsAny checks if content contains any of the given substrings.
|
||||
func containsAny(content string, substrings ...string) bool {
|
||||
for _, s := range substrings {
|
||||
if contains(content, s) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// contains checks if content contains a substring (case-insensitive).
|
||||
func contains(content, substr string) bool {
|
||||
return len(content) >= len(substr) &&
|
||||
(content == substr || len(content) > 0 && containsHelper(content, substr))
|
||||
}
|
||||
|
||||
// containsHelper is a helper function for case-insensitive substring check.
|
||||
func containsHelper(content, substr string) bool {
|
||||
// Simple case-insensitive check
|
||||
for i := 0; i <= len(content)-len(substr); i++ {
|
||||
match := true
|
||||
for j := 0; j < len(substr); j++ {
|
||||
c1 := content[i+j]
|
||||
c2 := substr[j]
|
||||
// Convert to lowercase for comparison
|
||||
if c1 >= 'A' && c1 <= 'Z' {
|
||||
c1 += 32
|
||||
}
|
||||
if c2 >= 'A' && c2 <= 'Z' {
|
||||
c2 += 32
|
||||
}
|
||||
if c1 != c2 {
|
||||
match = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if match {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
@ -149,7 +149,7 @@ skills/
|
|||
| **gitlink-wiki** | Wiki 页面管理 | `wiki +list`, `wiki +view`, `wiki +create`, `wiki +update`, `wiki +delete` |
|
||||
| **gitlink-pm** | 项目管理 | 通过 Raw API 访问 |
|
||||
| **gitlink-workflow** | AI 工作流 | Issue 分类、PR Review、Release Notes |
|
||||
| **gitlink-health** | 开源项目健康度 | 详情见SKILL.md |
|
||||
| **gitlink-health** | 项目健康度诊断 | `health diagnose` (5 维度评分:文档、许可证、社区、成熟度、CI/CD) |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
---
|
||||
name: gitlink-health
|
||||
version: 1.0.1
|
||||
description: "项目健康度分析(专用工作流):采集仓库 PR/Issue 数据到 SQLite 并计算聚合指标,生成健康度报告。当用户提到「项目怎么样」「项目健康度」「项目报告」「项目状况」「项目分析」「项目整体情况」等综合分析意图时,必须使用本 skill,不要拆分为 repo/issue/pr 单独操作。"
|
||||
version: 1.1.0
|
||||
description: "项目健康度分析(专用工作流):提供两种模式 - (1) diagnose 快速诊断:综合评估文档、许可证、社区、成熟度、CI/CD 五大维度;(2) fetch 深度分析:采集 PR/Issue 数据到 SQLite 并计算聚合指标。当用户提到「项目怎么样」「项目健康度」「项目报告」「项目状况」「项目分析」「项目整体情况」等综合分析意图时,必须使用本 skill,不要拆分为 repo/issue/pr 单独操作。"
|
||||
metadata:
|
||||
requires:
|
||||
bins: ["gitlink-cli"]
|
||||
cliHelp: "gitlink-cli health +fetch --help"
|
||||
cliHelp: "gitlink-cli health --help"
|
||||
---
|
||||
|
||||
# gitlink-health(开源项目健康度分析技能)
|
||||
|
|
@ -20,6 +20,10 @@ metadata:
|
|||
- 用户要求综合分析一个项目的 PR、Issue 等多项指标
|
||||
- 用户要生成任何形式的项目评估报告
|
||||
|
||||
**选择合适的命令:**
|
||||
- **快速诊断**:使用 `health diagnose` - 综合评估文档、许可证、社区、成熟度、CI/CD 五大维度,生成健康度评分和改进建议
|
||||
- **深度分析**:使用 `health +fetch` - 采集 PR/Issue 数据到 SQLite,支持自定义 SQL 查询和详细报告
|
||||
|
||||
**跳过本 skill(使用底层 skill)的场景:**
|
||||
- 用户明确只操作仓库信息(查看分支、提交等)→ `gitlink-repo`
|
||||
- 用户明确只操作 Issue(创建、查看、关闭等)→ `gitlink-issue`
|
||||
|
|
@ -46,8 +50,72 @@ export GITLINK_TOKEN="your-token" # 非交互环境设置 Token
|
|||
|
||||
可通过 `gitlink-cli auth status` 验证登录状态。
|
||||
|
||||
## 功能概述
|
||||
|
||||
本 Skill 提供两种健康度分析模式:
|
||||
|
||||
### 模式一:快速诊断(health diagnose)
|
||||
|
||||
综合评估项目五大维度,生成健康度评分和改进建议:
|
||||
|
||||
1. **文档质量**(20分)— 检查 README 完整性、结构化程度
|
||||
2. **许可证**(15分)— 验证开源许可证是否存在及合规性
|
||||
3. **社区活跃度**(25分)— 分析贡献者数量、活跃度分布
|
||||
4. **项目成熟度**(20分)— 评估项目版本、星标、Fork 等指标
|
||||
5. **CI/CD**(20分)— 检查 CI 激活状态和构建成功率
|
||||
|
||||
**适用场景**:快速了解项目整体健康状况,获取改进建议。
|
||||
|
||||
### 模式二:深度分析(health +fetch)
|
||||
|
||||
采集 PR/Issue 数据到 SQLite,支持自定义查询和详细报告:
|
||||
|
||||
1. **数据采集** — 批量获取 PR 和 Issue 数据
|
||||
2. **指标计算** — 计算合并率、解决时长、活跃度等聚合指标
|
||||
3. **自定义查询** — 支持 SQL 查询,灵活分析
|
||||
4. **详细报告** — 生成包含 14 个维度的完整报告
|
||||
|
||||
**适用场景**:需要深入分析项目协作数据,生成详细报告。
|
||||
|
||||
---
|
||||
|
||||
## 工作流程
|
||||
|
||||
### 快速诊断模式(推荐首选)
|
||||
|
||||
#### 1. 运行诊断命令
|
||||
|
||||
```bash
|
||||
# 诊断当前项目
|
||||
gitlink-cli health diagnose
|
||||
|
||||
# 诊断指定项目
|
||||
gitlink-cli health diagnose --owner <owner> --repo <repo>
|
||||
|
||||
# 指定输出格式
|
||||
gitlink-cli health diagnose --format json
|
||||
gitlink-cli health diagnose --format markdown
|
||||
|
||||
# 显示详细信息
|
||||
gitlink-cli health diagnose --verbose
|
||||
```
|
||||
|
||||
#### 2. 查看诊断结果
|
||||
|
||||
诊断报告包含:
|
||||
- 总体健康度评分(满分 100)
|
||||
- 五大维度评分和状态
|
||||
- 详细指标信息
|
||||
- 改进建议
|
||||
|
||||
#### 3. 根据建议改进项目
|
||||
|
||||
根据报告中的改进建议,针对性地优化项目。
|
||||
|
||||
---
|
||||
|
||||
### 深度分析模式
|
||||
|
||||
### 1. 采集仓库数据
|
||||
|
||||
运行以下命令采集目标仓库的数据:
|
||||
|
|
@ -66,7 +134,49 @@ gitlink-cli health +fetch --owner OWNER --repo REPO
|
|||
|
||||
## 命令参考
|
||||
|
||||
### health +fetch
|
||||
### health diagnose(快速诊断)
|
||||
|
||||
综合评估项目健康度,生成评分和改进建议。
|
||||
|
||||
```bash
|
||||
gitlink-cli health diagnose [flags]
|
||||
```
|
||||
|
||||
| 参数 | 简写 | 说明 | 默认值 |
|
||||
|------|------|------|--------|
|
||||
| `--owner` | `-o` | 项目所有者 | 自动从 git remote 推断 |
|
||||
| `--repo` | `-r` | 项目名称 | 自动从 git remote 推断 |
|
||||
| `--format` | `-f` | 输出格式 (text/json/markdown) | text |
|
||||
| `--verbose` | `-v` | 显示详细输出 | false |
|
||||
| `--db` | `-d` | 数据库路径(可选) | - |
|
||||
|
||||
**输出格式示例:**
|
||||
|
||||
```
|
||||
╔══════════════════════════════════════════════════════════════════╗
|
||||
║ 🏥 项目健康度诊断报告 ║
|
||||
╠══════════════════════════════════════════════════════════════════╣
|
||||
║ 项目:owner/repo ║
|
||||
║ 诊断时间:2024-01-15 10:30:00 ║
|
||||
║ 总体状态:🟢 Good (75/100) ║
|
||||
╚══════════════════════════════════════════════════════════════════╝
|
||||
|
||||
📊 维度评分:
|
||||
├─ 📚 文档质量:18/20 [good]
|
||||
├─ ⚖️ 许可证:15/15 [good]
|
||||
├─ 👥 社区活跃度:20/25 [warning]
|
||||
├─ 🌟 项目成熟度:12/20 [warning]
|
||||
└─ 🔧 CI/CD:10/20 [critical]
|
||||
|
||||
💡 改进建议:
|
||||
1. 增加核心贡献者数量,提升社区活跃度
|
||||
2. 激活 CI/CD 服务,提升代码质量保障
|
||||
3. 增加项目星标和 Fork 数量
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### health +fetch(深度分析)
|
||||
|
||||
| 参数 | 说明 | 默认值 |
|
||||
|------|------|--------|
|
||||
|
|
|
|||
Loading…
Reference in New Issue