diff --git a/README.md b/README.md index 71837ae..c9ea82c 100644 --- a/README.md +++ b/README.md @@ -463,6 +463,49 @@ gitlink-cli ci +log --owner Gitlink --repo forgeplus -i gitlink-cli ci +restart --owner Gitlink --repo forgeplus -i ``` +### 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 @@ -727,7 +770,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 diff --git a/README.zh-CN.md b/README.zh-CN.md index 1661a1e..906646e 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -441,6 +441,49 @@ gitlink-cli release +update --owner Gitlink --repo forgeplus -i -b gitlink-cli release +delete --owner Gitlink --repo forgeplus -i --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 @@ -600,6 +643,7 @@ git push gitlink | `gitlink-user` | 用户管理(个人信息等) | | `gitlink-pm` | 项目管理(Sprint、看板、周报等) | | `gitlink-workflow` | AI 自动化工作流(Issue 分类、PR Review、Release Notes 等) | +| `gitlink-health` | 项目健康度诊断(5 维度评分:文档、许可证、社区、成熟度、CI/CD) | ## 项目结构 diff --git a/doc/health-check-design.md b/doc/health-check-design.md new file mode 100644 index 0000000..77b5690 --- /dev/null +++ b/doc/health-check-design.md @@ -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 的整合分析能力 +- 可扩展性好,未来可添加更多诊断维度 diff --git a/internal/i18n/locales/en-US.json b/internal/i18n/locales/en-US.json index 0739395..b643a84 100644 --- a/internal/i18n/locales/en-US.json +++ b/internal/i18n/locales/en-US.json @@ -267,5 +267,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" } diff --git a/internal/i18n/locales/zh-CN.json b/internal/i18n/locales/zh-CN.json index 2e6fc4d..5a4549a 100644 --- a/internal/i18n/locales/zh-CN.json +++ b/internal/i18n/locales/zh-CN.json @@ -267,5 +267,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": "修复测试失败的问题" } diff --git a/shortcuts/health/diagnose.go b/shortcuts/health/diagnose.go new file mode 100644 index 0000000..9a4c07c --- /dev/null +++ b/shortcuts/health/diagnose.go @@ -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 "ℹ️" + } +} diff --git a/shortcuts/health/health.go b/shortcuts/health/health.go index 04f26d1..887f0bd 100644 --- a/shortcuts/health/health.go +++ b/shortcuts/health/health.go @@ -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), } } diff --git a/shortcuts/health/health_test.go b/shortcuts/health/health_test.go new file mode 100644 index 0000000..9798bd3 --- /dev/null +++ b/shortcuts/health/health_test.go @@ -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 +} diff --git a/shortcuts/health/scoring.go b/shortcuts/health/scoring.go new file mode 100644 index 0000000..3de859e --- /dev/null +++ b/shortcuts/health/scoring.go @@ -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 +} diff --git a/shortcuts/health/suggestions.go b/shortcuts/health/suggestions.go new file mode 100644 index 0000000..e460150 --- /dev/null +++ b/shortcuts/health/suggestions.go @@ -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 +} diff --git a/skills/README.md b/skills/README.md index d4df553..c3df84d 100644 --- a/skills/README.md +++ b/skills/README.md @@ -152,7 +152,7 @@ skills/ | **gitlink-pipeline** | 流水线工作流 | `pipeline +runs`, `pipeline +run`, `pipeline +logs` | | **gitlink-pm** | 项目管理 | 通过 Raw API 访问 | | **gitlink-workflow** | AI 工作流 | Issue 分类、PR Review、Release Notes | -| **gitlink-health** | 开源项目健康度 | 详情见SKILL.md | +| **gitlink-health** | 项目健康度诊断 | `health diagnose` (5 维度评分:文档、许可证、社区、成熟度、CI/CD) | --- diff --git a/skills/gitlink-health/SKILL.md b/skills/gitlink-health/SKILL.md index 18e0abc..d6d9d66 100644 --- a/skills/gitlink-health/SKILL.md +++ b/skills/gitlink-health/SKILL.md @@ -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 --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(深度分析) | 参数 | 说明 | 默认值 | |------|------|--------|