Merge PR #309: 新增 health diagnose 命令,实现项目健康度综合诊断功能

This commit is contained in:
wbtiger 2026-07-14 22:50:04 +08:00
commit 1b2e44653f
12 changed files with 2184 additions and 8 deletions

View File

@ -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

View File

@ -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 |
## 项目结构

486
doc/health-check-design.md Normal file
View File

@ -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 的整合分析能力
- 可扩展性好,未来可添加更多诊断维度

View File

@ -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"
}

View File

@ -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": "修复测试失败的问题"
}

View File

@ -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 ""
}
}

View File

@ -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),
}
}

View File

@ -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
}

406
shortcuts/health/scoring.go Normal file
View File

@ -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
}

View File

@ -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
}

View File

@ -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) |
---

View File

@ -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/CD10/20 [critical]
💡 改进建议:
1. 增加核心贡献者数量,提升社区活跃度
2. 激活 CI/CD 服务,提升代码质量保障
3. 增加项目星标和 Fork 数量
```
---
### health +fetch深度分析
| 参数 | 说明 | 默认值 |
|------|------|--------|