diff --git a/README.md b/README.md index eeaa346..6feb19e 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![Go Version](https://img.shields.io/badge/Go-1.26%2B-blue.svg)](https://golang.org) [![npm version](https://img.shields.io/npm/v/@gitlink-ai/cli.svg)](https://www.npmjs.com/package/@gitlink-ai/cli) -The official [GitLink](https://www.gitlink.org.cn) CLI tool — built for humans and AI Agents. Supports **macOS, Linux, and Windows**. Covers repository management, issue tracking, pull requests, webhooks, member collaboration, CI/CD, and AI-powered workflows, with 40+ commands and AI Agent [Skills](./skills/README.md). +The official [GitLink](https://www.gitlink.org.cn) CLI tool — built for humans and AI Agents. Supports **macOS, Linux, and Windows**. Covers repository management, issue tracking, pull requests, webhooks, member collaboration, CI/CD, and AI-powered workflows, with 40+ commands and AI Agent [Skills](./skills/). **[中文文档](./README.zh-CN.md)** @@ -78,19 +78,11 @@ The official [GitLink](https://www.gitlink.org.cn) CLI tool — built for humans jiangtx
jiangtx -
- luwanzhou -
luwanzhou -
-
- whale_hihihi -
whale_hihihi -
## Why gitlink-cli? -- **Agent-Native Design** — Structured [Skills](./skills/README.md) out of the box, compatible with Claude Code, OpenClaw, and other AI platforms — Agents can operate GitLink with zero extra setup +- **Agent-Native Design** — Structured [Skills](./skills/) out of the box, compatible with Claude Code, OpenClaw, and other AI platforms — Agents can operate GitLink with zero extra setup - **Wide Coverage** — Repository, Issue, PR, Webhook, Member, Branch, Release, CI, Pipeline, Org, Search, and User workflows are covered by high-level commands - **AI-Friendly & Optimized** — Every command is tested with real Agents, featuring concise parameters, smart defaults, and structured output - **Cross-Platform** — Runs on macOS, Linux, and Windows (x64/arm64), install via `npm install -g @gitlink-ai/cli` in one command, binary auto-downloaded @@ -104,7 +96,7 @@ The official [GitLink](https://www.gitlink.org.cn) CLI tool — built for humans | Category | Capabilities | |----------|-------------| | 📦 Repo | List, create, fork, delete repositories, view repo info, insights, and interactions | -| 🐛 Issue | Create, update, close, batch close/update/delete, comment on issues | +| 🐛 Issue | Create, update, close, batch close, comment on issues | | 🔖 Label | Create, list, update, delete issue labels | | 🔀 PR | Create, merge, review pull requests, view changed files | | 👥 Member | List, add, remove repository members, change roles, create and accept invite links | @@ -114,13 +106,9 @@ The official [GitLink](https://www.gitlink.org.cn) CLI tool — built for humans | 🔧 CI | View builds, logs, CI/CD operations | | ⚙️ Pipeline | Run, inspect, enable, disable, delete pipeline workflows and logs | | 🔔 Webhook | Manage repo webhooks and test deliveries | -| 📖 Wiki | List, view, create, update, and delete wiki pages | | 🔍 Search | Search repositories, users | -| 📊 Dataset | Query research datasets by project | -| 👤 User | View user profiles and info | -| 📊 Profile | User ability, role, major, activity, and contribution statistics | +| 👤 User | View profiles, manage public keys, inspect user statistics | | 📋 PM | Sprint management, kanban boards, weekly reports | -| 📝 Template | Manage project templates for issues and pull requests | | 🤖 Workflow | AI-powered issue triage, PR review, release notes | ## Installation & Quick Start @@ -228,17 +216,6 @@ gitlink-cli repo +readme --owner Gitlink --repo forgeplus --ref master gitlink-cli repo +tree --owner Gitlink --repo forgeplus --ref master gitlink-cli repo +tree --owner Gitlink --repo forgeplus --path src --ref main -# Search repository files and inspect commit history -gitlink-cli repo +files --owner Gitlink --repo forgeplus --search README --ref master -gitlink-cli repo +commits --owner Gitlink --repo forgeplus --ref master --limit 20 -gitlink-cli repo +commit-files --owner Gitlink --repo forgeplus --sha -gitlink-cli repo +commit-diff --owner Gitlink --repo forgeplus --sha - -# Inspect repository tags -gitlink-cli repo +tags --owner Gitlink --repo forgeplus --name v1 --only-name true -gitlink-cli repo +tag --owner Gitlink --repo forgeplus --name v1.0.0 -gitlink-cli repo +delete-tag --owner Gitlink --repo forgeplus --name v1.0.0 --dry-run - # Show language breakdown gitlink-cli repo +languages --owner Gitlink --repo forgeplus @@ -262,16 +239,6 @@ gitlink-cli repo +unfollow --owner Gitlink --repo forgeplus --project-id 123 gitlink-cli repo +like --owner Gitlink --repo forgeplus gitlink-cli repo +unlike --owner Gitlink --repo forgeplus --project-id 123 -# Preview and apply a multi-file commit -gitlink-cli repo +batch-commit --owner me --repo proj \ - --branch master --message "docs: update guide" \ - --files 'update:README.md:# Updated;create:docs/demo.md:# Demo' \ - --dry-run -gitlink-cli repo +batch-commit --owner me --repo proj \ - --branch master --message "docs: update guide" \ - --files 'update:README.md:# Updated;delete:old.md' \ - --yes - # Create a repository gitlink-cli repo +create -n my-project -d "Project description" @@ -296,28 +263,6 @@ gitlink-cli webhook +test --owner Gitlink --repo forgeplus --id 68 gitlink-cli webhook +tasks --owner Gitlink --repo forgeplus --id 68 ``` -### Wiki Management - -```bash -# List wiki pages (table of contents) -gitlink-cli wiki +list --owner Gitlink --repo forgeplus --project-id 12345 - -# View a wiki page by page name -gitlink-cli wiki +view --owner Gitlink --repo forgeplus --project-id 12345 -n home - -# Create a wiki page -gitlink-cli wiki +create --owner Gitlink --repo forgeplus --project-id 12345 \ - -n getting-started -t "Getting Started" -c "# Getting Started Guide" - -# Update a wiki page title and/or content -gitlink-cli wiki +update --owner Gitlink --repo forgeplus --project-id 12345 -n home -t "New Title" -gitlink-cli wiki +update --owner Gitlink --repo forgeplus --project-id 12345 -n home -c "# Updated content" -gitlink-cli wiki +update --owner Gitlink --repo forgeplus --project-id 12345 -n home -t "New Title" -c "New content" - -# Delete a wiki page -gitlink-cli wiki +delete --owner Gitlink --repo forgeplus --project-id 12345 -n old-page -``` - ### Member Management ```bash @@ -367,14 +312,6 @@ gitlink-cli issue +batch-close --owner Gitlink --repo forgeplus --numbers 123,12 # Batch close issues from a CSV file gitlink-cli issue +batch-close --owner Gitlink --repo forgeplus --from issues.csv -# Preview batch metadata update by API issue IDs -# Note: --ids uses API issue IDs, not web URL issue numbers. -gitlink-cli issue +batch-update --owner Gitlink --repo forgeplus --ids 101,102 --status-id 3 --priority-id 2 --dry-run - -# Destructive batch delete requires both dry-run first and --yes for real execution -gitlink-cli issue +batch-delete --owner Gitlink --repo forgeplus --ids 101,102 --dry-run -gitlink-cli issue +batch-delete --owner Gitlink --repo forgeplus --ids 101,102 --yes - # Add a comment gitlink-cli issue +comment --owner Gitlink --repo forgeplus -i 123 -b "Fixed" @@ -418,30 +355,6 @@ gitlink-cli label +update --owner Gitlink --repo forgeplus -i 42 -c "#00FF00" gitlink-cli label +delete --owner Gitlink --repo forgeplus -i 42 ``` -### Template Management - -```bash -# List project templates -gitlink-cli template +list --owner Gitlink --repo forgeplus - -# Get template details -gitlink-cli template +get --owner Gitlink --repo forgeplus -i 1 - -# Create an issue template -gitlink-cli template +create --owner Gitlink --repo forgeplus \ - -t "ProjectTemplates::Issue" -n "Bug Report" -c "## Description\n..." - -# Create a pull request template -gitlink-cli template +create --owner Gitlink --repo forgeplus \ - -t "ProjectTemplates::PullRequest" -n "Feature PR" -c "## Summary\n..." - -# Update a template -gitlink-cli template +update --owner Gitlink --repo forgeplus -i 1 -n "Updated Name" - -# Delete a template -gitlink-cli template +delete --owner Gitlink --repo forgeplus -i 1 -``` - ### Pull Requests ```bash @@ -466,9 +379,6 @@ gitlink-cli pr +reopen --owner Gitlink --repo forgeplus -i 42 # View changed files gitlink-cli pr +files --owner Gitlink --repo forgeplus -i 42 -# View commits included in a PR -gitlink-cli pr +commits --owner Gitlink --repo forgeplus -i 42 - # List PR patchset versions gitlink-cli pr +versions --owner Gitlink --repo forgeplus -i 42 @@ -557,16 +467,6 @@ gitlink-cli pipeline +disable --owner Gitlink --repo forgeplus --id 7 --workflow gitlink-cli pipeline +delete --owner Gitlink --repo forgeplus --id 7 --dry-run ``` -### Ignore File Templates - -```bash -# List all available .gitignore templates -gitlink-cli ignore +list - -# Filter templates by name -gitlink-cli ignore +list --name Go -``` - ### Search ```bash @@ -577,39 +477,22 @@ gitlink-cli search +repos -k "machine learning" gitlink-cli search +users -k "zhangsan" ``` -### User Profile - -`profile` surfaces GitLink's native user statistics (ability, role, major, activity, -contribution). When `--user` is omitted it defaults to the authenticated user. -`user` also provides contributor-oriented shortcuts for heatmaps, aggregate -statistics, and project trends. +### User Operations ```bash -# Current authenticated user +# Current authenticated user and detailed profile gitlink-cli user +me +gitlink-cli user +current --format json -# User contribution heatmap and aggregate statistics -gitlink-cli user +heatmap --user zhangsan --year 2026 -gitlink-cli user +statistics --user zhangsan --start-time 1704067200 --end-time 1735689600 +# Public key management +gitlink-cli user +keys --page 1 --limit 20 +gitlink-cli user +key-create --title "work laptop" --key-file ~/.ssh/id_rsa.pub --dry-run +gitlink-cli user +key-delete --id 7 --dry-run -# Project trend data (short alias: user +trends) -gitlink-cli user +project-trends --user zhangsan - -# Development ability scores + language breakdown -gitlink-cli profile +ability --user zhangsan - -# Role positioning / major (discipline) categories -gitlink-cli profile +role --user zhangsan -gitlink-cli profile +major --user zhangsan - -# Ability within a time window (Unix timestamps) -gitlink-cli profile +ability --user zhangsan --start-time 1704067200 --end-time 1735689600 - -# Recent activity (issues / PRs / commits per day) for the current user -gitlink-cli profile +activity - -# Contribution heatmap for a given year -gitlink-cli profile +contribution --user zhangsan --year 2025 +# User statistics +gitlink-cli user +activity --login zhangsan +gitlink-cli user +headmap --login zhangsan --year 2026 +gitlink-cli user +develop --login zhangsan --start-time 1704067200 --end-time 1735689599 ``` ### Workflow Agent Commands @@ -711,32 +594,6 @@ Safety: - `workflow +pr-summary` does not comment, approve, reject, or merge pull requests. - `workflow +repo-report` aggregates health, issue triage, and PR review summary signals without remote writes. -### Dataset - -`dataset` manages and queries GitLink research datasets (title, description, -paper content, license, owning project). - -```bash -# List datasets for one or more projects (by numeric project ID) -gitlink-cli dataset +list --ids 5988 - -# View a repository's dataset and attachments -gitlink-cli dataset +view --owner Gitlink --repo forgeplus - -# Create / update a repository's dataset (preview first with --dry-run) -gitlink-cli dataset +create --owner me --repo proj -t "My dataset" -d "..." --license-id 359 --dry-run -gitlink-cli dataset +update --owner me --repo proj -t "My dataset" -d "updated" - -# Delete a dataset attachment (destructive: preview, then confirm with --yes) -gitlink-cli dataset +delete-attachment --owner me --repo proj --uuid --dry-run -gitlink-cli dataset +delete-attachment --owner me --repo proj --uuid --yes -``` - -> Note: `dataset +list` (platform dataset query) is verified on production -> gitlink.org.cn. The per-repo `+view`/`+create`/`+update` routes follow the -> published OpenAPI contract but are not yet deployed on production (they return -> 404 there); they will work once the platform enables them. - ### Raw API For endpoints not covered by shortcuts, use the Raw API directly: @@ -793,13 +650,13 @@ git push gitlink The `skills/` directory contains Agent Skill files for AI-automated GitLink operations. -See [skills/README.md](./skills/README.md) for details. +See [skills/README.md](skills/README.md) for details. | Skill | Description | |-------|-------------| | `gitlink-shared` | Authentication, global parameters, safety rules, API notes | | `gitlink-repo` | Repository operations (create, view, delete, fork, insights, etc.) | -| `gitlink-issue` | Issue operations (create, update, close, batch update/delete, comment, etc.) | +| `gitlink-issue` | Issue operations (create, update, close, comment, etc.) | | `gitlink-pr` | Pull request operations (create, merge, review, etc.) | | `gitlink-member` | Repository member and invite link management | | `gitlink-branch` | Branch management (create, delete, list, protect, unprotect) | @@ -808,7 +665,7 @@ See [skills/README.md](./skills/README.md) for details. | `gitlink-pipeline` | Pipeline workflow operations (runs, logs, enable, disable, delete, etc.) | | `gitlink-search` | Search (repositories, users, etc.) | | `gitlink-org` | Organization management (members, teams, etc.) | -| `gitlink-user` | User management (profile info, heatmaps, statistics, project trends, etc.) | +| `gitlink-user` | User management (profiles, public keys, statistics, 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) | @@ -863,7 +720,7 @@ gitlink-cli/ ## Documentation -- [Skills Guide](./skills/README.md) — AI Agent Skills detailed documentation +- [Skills Guide](skills/README.md) — AI Agent Skills detailed documentation - [Design Document](doc/design.md) — Architecture design and development plan ## FAQ @@ -928,7 +785,7 @@ gitlink-cli uses Windows Credential Manager for secure token storage. If Credent ### Q: Where can I find the full API reference? -See [skills/gitlink-shared/references/api-reference.md](./skills/gitlink-shared/references/api-reference.md). +See [skills/gitlink-shared/REFERENCE.md](skills/gitlink-shared/REFERENCE.md). ## License diff --git a/README.zh-CN.md b/README.zh-CN.md index ad9a845..a9d221a 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -5,7 +5,7 @@ [![Go Version](https://img.shields.io/badge/Go-1.26%2B-blue.svg)](https://golang.org) [![npm version](https://img.shields.io/npm/v/@gitlink-ai/cli.svg)](https://www.npmjs.com/package/@gitlink-ai/cli) -[GitLink(确实开源)](https://www.gitlink.org.cn) 官方 CLI 工具 — 为人类和 AI Agent 双重设计。支持 **macOS、Linux、Windows**,覆盖仓库管理、Issue 追踪、Pull Request、Webhook、成员协作、CI/CD 和 AI 自动化工作流,包含 40+ 命令和 AI Agent [Skills](./skills/README.md)。 +[GitLink(确实开源)](https://www.gitlink.org.cn) 官方 CLI 工具 — 为人类和 AI Agent 双重设计。支持 **macOS、Linux、Windows**,覆盖仓库管理、Issue 追踪、Pull Request、Webhook、成员协作、CI/CD 和 AI 自动化工作流,包含 40+ 命令和 AI Agent [Skills](./skills/)。 **[English](./README.md)** @@ -78,19 +78,11 @@ jiangtx
jiangtx -
- luwanzhou -
luwanzhou -
-
- whale_hihihi -
whale_hihihi -
## 为什么选择 gitlink-cli? -- **Agent-Native 设计** — 开箱即用结构化 [Skills](./skills/README.md),兼容 Claude Code — Agent 零配置即可操作 GitLink +- **Agent-Native 设计** — 开箱即用结构化 [Skills](./skills/),兼容 Claude Code — Agent 零配置即可操作 GitLink - **广泛覆盖** — 仓库、Issue、PR、Webhook、成员、分支、Release、CI、Pipeline、组织、搜索、用户等常用工作流均提供高层命令 - **AI 友好 & 优化** — 每条命令都经过真实 Agent 测试,简洁参数、智能默认值、结构化输出 - **跨平台** — macOS、Linux、Windows (x64/arm64) 全支持,`npm` 一条命令安装 @@ -104,7 +96,7 @@ | 分类 | 能力 | |------|------| | 📦 仓库 | 列出、创建、Fork、删除仓库,查看仓库信息、洞察数据和互动状态 | -| 🐛 Issue | 创建、更新、关闭、批量关闭/更新/删除、评论 Issue | +| 🐛 Issue | 创建、更新、关闭、批量关闭、评论 Issue | | 🔖 标签 | 创建、列出、更新、删除 Issue 标签 | | 🔀 PR | 创建、合并、Review Pull Request,查看变更文件 | | 👥 成员 | 列出、添加、移除仓库成员,调整角色,生成和接受邀请链接 | @@ -113,13 +105,9 @@ | 🏢 组织 | 管理组织、成员、团队 | | 🔧 CI | 查看构建、日志、CI/CD 操作 | | ⚙️ Pipeline | 运行、查看、启停、删除流水线工作流并查询日志 | -| 📖 Wiki | 列出、查看、创建、更新、删除 Wiki 页面 | | 🔍 搜索 | 搜索仓库、用户 | -| 📊 数据集 | 按项目查询科研数据集 | -| 👤 用户 | 查看用户资料和信息 | -| 📊 画像 | 用户开发能力、角色定位、专业定位、近期活动、贡献热力图统计 | +| 👤 用户 | 查看资料、管理 Public Keys、查询用户统计 | | 📋 项目管理 | Sprint 管理、看板、周报 | -| 📝 模板 | 管理 Issue 和 Pull Request 项目模板 | | 🤖 工作流 | AI 驱动的 Issue 分类、PR Review、Release Notes | ## 安装与快速上手 @@ -239,17 +227,6 @@ gitlink-cli repo +readme --owner Gitlink --repo forgeplus --ref master gitlink-cli repo +tree --owner Gitlink --repo forgeplus --ref master gitlink-cli repo +tree --owner Gitlink --repo forgeplus --path src --ref main -# 搜索仓库文件并查看提交历史 -gitlink-cli repo +files --owner Gitlink --repo forgeplus --search README --ref master -gitlink-cli repo +commits --owner Gitlink --repo forgeplus --ref master --limit 20 -gitlink-cli repo +commit-files --owner Gitlink --repo forgeplus --sha -gitlink-cli repo +commit-diff --owner Gitlink --repo forgeplus --sha - -# 查看仓库标签 -gitlink-cli repo +tags --owner Gitlink --repo forgeplus --name v1 --only-name true -gitlink-cli repo +tag --owner Gitlink --repo forgeplus --name v1.0.0 -gitlink-cli repo +delete-tag --owner Gitlink --repo forgeplus --name v1.0.0 --dry-run - # 查看语言占比 gitlink-cli repo +languages --owner Gitlink --repo forgeplus @@ -273,16 +250,6 @@ gitlink-cli repo +unfollow --owner Gitlink --repo forgeplus --project-id 123 gitlink-cli repo +like --owner Gitlink --repo forgeplus gitlink-cli repo +unlike --owner Gitlink --repo forgeplus --project-id 123 -# 预览并执行多文件提交 -gitlink-cli repo +batch-commit --owner me --repo proj \ - --branch master --message "docs: update guide" \ - --files 'update:README.md:# Updated;create:docs/demo.md:# Demo' \ - --dry-run -gitlink-cli repo +batch-commit --owner me --repo proj \ - --branch master --message "docs: update guide" \ - --files 'update:README.md:# Updated;delete:old.md' \ - --yes - # 创建仓库 gitlink-cli repo +create -n my-project -d "项目描述" @@ -307,28 +274,6 @@ gitlink-cli webhook +test --owner Gitlink --repo forgeplus --id 68 gitlink-cli webhook +tasks --owner Gitlink --repo forgeplus --id 68 ``` -### Wiki 管理 - -```bash -# 列出 Wiki 页面(目录结构) -gitlink-cli wiki +list --owner Gitlink --repo forgeplus --project-id 12345 - -# 查看 Wiki 页面 -gitlink-cli wiki +view --owner Gitlink --repo forgeplus --project-id 12345 -n home - -# 创建 Wiki 页面 -gitlink-cli wiki +create --owner Gitlink --repo forgeplus --project-id 12345 \ - -n getting-started -t "快速开始" -c "# 快速开始指南" - -# 更新 Wiki 页面标题和/或内容 -gitlink-cli wiki +update --owner Gitlink --repo forgeplus --project-id 12345 -n home -t "新标题" -gitlink-cli wiki +update --owner Gitlink --repo forgeplus --project-id 12345 -n home -c "# 更新后的内容" -gitlink-cli wiki +update --owner Gitlink --repo forgeplus --project-id 12345 -n home -t "新标题" -c "新内容" - -# 删除 Wiki 页面 -gitlink-cli wiki +delete --owner Gitlink --repo forgeplus --project-id 12345 -n old-page -``` - ### 成员管理 ```bash @@ -378,14 +323,6 @@ gitlink-cli issue +batch-close --owner Gitlink --repo forgeplus --numbers 123,12 # 从 CSV 文件批量关闭 Issue gitlink-cli issue +batch-close --owner Gitlink --repo forgeplus --from issues.csv -# 按 API issue id 预览批量更新元数据 -# 注意:--ids 是 API issue id,不是网页 URL 中的 Issue 编号。 -gitlink-cli issue +batch-update --owner Gitlink --repo forgeplus --ids 101,102 --status-id 3 --priority-id 2 --dry-run - -# 危险批量删除必须先 dry-run,真实执行还要显式 --yes -gitlink-cli issue +batch-delete --owner Gitlink --repo forgeplus --ids 101,102 --dry-run -gitlink-cli issue +batch-delete --owner Gitlink --repo forgeplus --ids 101,102 --yes - # 添加评论 gitlink-cli issue +comment --owner Gitlink --repo forgeplus -i 123 -b "已修复" @@ -428,30 +365,6 @@ gitlink-cli label +update --owner Gitlink --repo forgeplus -i 42 -c "#00FF00" gitlink-cli label +delete --owner Gitlink --repo forgeplus -i 42 ``` -### 模板管理 - -```bash -# 列出项目模板 -gitlink-cli template +list --owner Gitlink --repo forgeplus - -# 获取模板详情 -gitlink-cli template +get --owner Gitlink --repo forgeplus -i 1 - -# 创建 Issue 模板 -gitlink-cli template +create --owner Gitlink --repo forgeplus \ - -t "ProjectTemplates::Issue" -n "Bug 报告" -c "## 描述\n..." - -# 创建 Pull Request 模板 -gitlink-cli template +create --owner Gitlink --repo forgeplus \ - -t "ProjectTemplates::PullRequest" -n "功能 PR" -c "## 概述\n..." - -# 更新模板 -gitlink-cli template +update --owner Gitlink --repo forgeplus -i 1 -n "新名称" - -# 删除模板 -gitlink-cli template +delete --owner Gitlink --repo forgeplus -i 1 -``` - ### Pull Request ```bash @@ -476,9 +389,6 @@ gitlink-cli pr +reopen --owner Gitlink --repo forgeplus -i 42 # 查看 PR 变更文件 gitlink-cli pr +files --owner Gitlink --repo forgeplus -i 42 -# 查看 PR 包含的提交 -gitlink-cli pr +commits --owner Gitlink --repo forgeplus -i 42 - # 查看 PR patchset/version 列表 gitlink-cli pr +versions --owner Gitlink --repo forgeplus -i 42 @@ -535,16 +445,6 @@ gitlink-cli pipeline +disable --owner Gitlink --repo forgeplus --id 7 --workflow gitlink-cli pipeline +delete --owner Gitlink --repo forgeplus --id 7 --dry-run ``` -### 忽略文件模板 - -```bash -# 列出所有可用的 .gitignore 模板 -gitlink-cli ignore +list - -# 按名称筛选模板 -gitlink-cli ignore +list --name Go -``` - ### 搜索 ```bash @@ -555,61 +455,24 @@ gitlink-cli search +repos -k "machine learning" gitlink-cli search +users -k "zhangsan" ``` -### 用户画像 - -`profile` 暴露 GitLink 原生的用户画像统计(开发能力、角色定位、专业定位、近期活动、贡献热力图)。 -省略 `--user` 时默认使用当前认证用户。 -`user` 同时提供面向贡献者分析的热力图、聚合统计和项目趋势快捷入口。 +### 用户操作 ```bash -# 当前认证用户 +# 当前登录用户和详细资料 gitlink-cli user +me +gitlink-cli user +current --format json -# 用户贡献热力图和聚合统计 -gitlink-cli user +heatmap --user zhangsan --year 2026 -gitlink-cli user +statistics --user zhangsan --start-time 1704067200 --end-time 1735689600 +# Public Key 管理 +gitlink-cli user +keys --page 1 --limit 20 +gitlink-cli user +key-create --title "work laptop" --key-file ~/.ssh/id_rsa.pub --dry-run +gitlink-cli user +key-delete --id 7 --dry-run -# 项目趋势数据(短别名:user +trends) -gitlink-cli user +project-trends --user zhangsan - -# 开发能力评分 + 语言分布 -gitlink-cli profile +ability --user zhangsan - -# 角色定位 / 专业(学科)定位 -gitlink-cli profile +role --user zhangsan -gitlink-cli profile +major --user zhangsan - -# 指定时间范围的开发能力(Unix 时间戳) -gitlink-cli profile +ability --user zhangsan --start-time 1704067200 --end-time 1735689600 - -# 当前用户的近期活动(每日 疑修 / 合并请求 / 提交) -gitlink-cli profile +activity - -# 指定年份的贡献热力图 -gitlink-cli profile +contribution --user zhangsan --year 2025 +# 用户统计 +gitlink-cli user +activity --login zhangsan +gitlink-cli user +headmap --login zhangsan --year 2026 +gitlink-cli user +develop --login zhangsan --start-time 1704067200 --end-time 1735689599 ``` -### 数据集 - -`dataset` 管理并查询 GitLink 科研数据集(标题、描述、论文内容、许可证、所属项目)。 - -```bash -# 按数字项目 ID 列出一个或多个项目的数据集 -gitlink-cli dataset +list --ids 5988 - -# 查看仓库的数据集及其附件 -gitlink-cli dataset +view --owner Gitlink --repo forgeplus - -# 创建 / 更新仓库数据集(先用 --dry-run 预览) -gitlink-cli dataset +create --owner me --repo proj -t "我的数据集" -d "..." --license-id 359 --dry-run -gitlink-cli dataset +update --owner me --repo proj -t "我的数据集" -d "更新" - -# 删除数据集附件(破坏性:先预览,再用 --yes 确认) -gitlink-cli dataset +delete-attachment --owner me --repo proj --uuid --dry-run -gitlink-cli dataset +delete-attachment --owner me --repo proj --uuid --yes -``` - -> 注意:`dataset +list`(平台数据集查询)已在生产 gitlink.org.cn 验证可用。按仓库的 `+view`/`+create`/`+update` 遵循已发布的 OpenAPI 契约,但生产环境尚未部署(当前返回 404),待平台上线后即可生效。 ### Raw API Shortcuts 未覆盖的接口可通过 Raw API 直接调用: @@ -666,13 +529,13 @@ git push gitlink `skills/` 目录包含 Claude Code Agent Skill 文件,支持 AI 自动化操作 GitLink 平台。 -详见 [skills/README.md](./skills/README.md) +详见 [skills/README.md](skills/README.md) | Skill | 说明 | |-------|------| | `gitlink-shared` | 认证、全局参数、安全规则、API 注意事项 | | `gitlink-repo` | 仓库操作(创建、查看、删除、Fork、洞察数据等) | -| `gitlink-issue` | Issue 操作(创建、更新、关闭、批量更新/删除、评论等) | +| `gitlink-issue` | Issue 操作(创建、更新、关闭、评论等) | | `gitlink-pr` | Pull Request 操作(创建、合并、Review 等) | | `gitlink-member` | 仓库成员与邀请链接管理 | | `gitlink-release` | 发布管理(创建、编辑、更新、查看、删除等) | @@ -680,7 +543,7 @@ git push gitlink | `gitlink-ci` | CI/CD 操作(构建、日志等) | | `gitlink-pipeline` | 流水线工作流操作(运行、日志、启停、删除等) | | `gitlink-search` | 搜索功能(仓库、用户等) | -| `gitlink-user` | 用户管理(个人信息、热力图、统计、项目趋势等) | +| `gitlink-user` | 用户管理(资料、Public Keys、统计等) | | `gitlink-pm` | 项目管理(Sprint、看板、周报等) | | `gitlink-workflow` | AI 自动化工作流(Issue 分类、PR Review、Release Notes 等) | @@ -734,7 +597,7 @@ gitlink-cli/ ## 文档 -- [Skills 使用指南](./skills/README.md) — AI Agent Skills 详细说明 +- [Skills 使用指南](skills/README.md) — AI Agent Skills 详细说明 - [设计文档](doc/design.md) — 架构设计和开发计划 ## 常见问题 @@ -797,7 +660,7 @@ gitlink-cli 使用 Windows Credential Manager 安全存储 Token。如果 Creden ### Q: 如何查看完整的 API 参考? -查看 [skills/gitlink-shared/references/api-reference.md](./skills/gitlink-shared/references/api-reference.md) +查看 [skills/gitlink-shared/REFERENCE.md](skills/gitlink-shared/REFERENCE.md) ## 许可证 diff --git a/doc/changes/user-account-stats-shortcuts.md b/doc/changes/user-account-stats-shortcuts.md new file mode 100644 index 0000000..8bec3e7 --- /dev/null +++ b/doc/changes/user-account-stats-shortcuts.md @@ -0,0 +1,36 @@ +# User Account And Stats Shortcuts + +Submitter: Wang Yue + +This change expands the `user` shortcut group with account metadata, Public Key management, and user statistics OpenAPI coverage. + +## Commands + +- `user +current` +- `user +keys` +- `user +key-create` +- `user +key-delete` +- `user +activity` +- `user +headmap` +- `user +develop` +- `user +role` +- `user +major` + +## API Mapping + +| Shortcut | Method | API path | +|----------|--------|----------| +| `user +current` | GET | `/api/users/get_user_info.json` | +| `user +keys` | GET | `/api/public_keys.json` | +| `user +key-create` | POST | `/api/public_keys.json` | +| `user +key-delete` | DELETE | `/api/public_keys/{id}.json` | +| `user +activity` | GET | `/api/users/{owner}/statistics/activity.json` | +| `user +headmap` | GET | `/api/users/{owner}/headmaps.json` | +| `user +develop` | GET | `/api/users/{owner}/statistics/develop.json` | +| `user +role` | GET | `/api/users/{owner}/statistics/role.json` | +| `user +major` | GET | `/api/users/{owner}/statistics/major.json` | + +## Verification + +- Unit tests cover public key list/create/delete requests, dry-run behavior, current profile lookup, user statistics endpoints, query parameters, and argument validation. +- `user +key-create` supports either inline `--key` content or `--key-file`. diff --git a/shortcuts/user/user.go b/shortcuts/user/user.go index 4a2ba22..3b1eaf2 100644 --- a/shortcuts/user/user.go +++ b/shortcuts/user/user.go @@ -3,22 +3,15 @@ package user import ( "fmt" "net/url" + "os" + "strings" "github.com/gitlink-org/gitlink-cli/internal/i18n" - "github.com/gitlink-org/gitlink-cli/internal/output" "github.com/gitlink-org/gitlink-cli/shortcuts/common" ) func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut { tr := shortcutTranslator(translators...) - userFlag := common.Flag{Name: "user", Short: "u", Usage: tr.T("flag.user")} - yearFlag := common.Flag{Name: "year", Usage: tr.T("flag.user.year")} - timeFlags := []common.Flag{ - {Name: "start-time", Usage: tr.T("flag.user.start_time")}, - {Name: "end-time", Usage: tr.T("flag.user.end_time")}, - } - windowFlags := append([]common.Flag{userFlag}, timeFlags...) - return []*common.Shortcut{ { Name: "me", @@ -31,6 +24,17 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut { return ctx.Output(env) }, }, + { + Name: "current", + Description: "Show current user profile details", + Run: func(ctx *common.RuntimeContext) error { + env, err := ctx.CallAPI("GET", "/users/get_user_info", nil) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, { Name: "info", Description: tr.T("cmd.user.info.short"), @@ -50,19 +54,17 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut { }, }, { - Name: "heatmap", - Description: tr.T("cmd.user.heatmap.short"), - Flags: []common.Flag{userFlag, yearFlag}, + Name: "keys", + Description: "List current user public keys", + Flags: []common.Flag{ + {Name: "page", Short: "p", Usage: "Page number", Default: "1"}, + {Name: "limit", Usage: "Items per page", Default: "20"}, + }, Run: func(ctx *common.RuntimeContext) error { - user, err := resolveUser(ctx) - if err != nil { - return err - } q := url.Values{} - if v := ctx.Arg("year"); v != "" { - q.Set("year", v) - } - env, err := ctx.CallAPIWithQuery("GET", fmt.Sprintf("/users/%s/headmaps", user), q) + setQueryIfPresent(q, ctx, "page", "page") + setQueryIfPresent(q, ctx, "limit", "limit") + env, err := ctx.CallAPIWithQuery("GET", "/public_keys", q) if err != nil { return err } @@ -70,82 +72,142 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut { }, }, { - Name: "statistics", - Description: tr.T("cmd.user.statistics.short"), - Flags: windowFlags, + Name: "key-create", + Description: "Create a public key", + Flags: []common.Flag{ + {Name: "title", Short: "t", Usage: "Public key title", Required: true}, + {Name: "key", Short: "k", Usage: "Public key content"}, + {Name: "key-file", Usage: "Path to a public key file"}, + {Name: "dry-run", Usage: "Preview the create request without adding a key", Bool: true, Default: "false"}, + }, Run: func(ctx *common.RuntimeContext) error { - return runWindowedUserGet(ctx, "/users/%s/statistics") + title, err := ctx.RequireArg("title") + if err != nil { + return err + } + key, err := publicKeyFromArgs(ctx) + if err != nil { + return err + } + payload := map[string]interface{}{ + "title": title, + "key": key, + } + path := "/public_keys" + if ctx.Arg("dry-run") == "true" { + return ctx.OutputData(map[string]interface{}{ + "dry_run": true, + "action": "create_public_key", + "method": "POST", + "path": path, + "payload": payload, + }) + } + env, err := ctx.CallAPI("POST", path, payload) + if err != nil { + return err + } + return ctx.Output(env) }, }, { - Name: "stats", - Description: tr.T("cmd.user.stats.short"), - Flags: windowFlags, + Name: "key-delete", + Description: "Delete a public key", + Flags: []common.Flag{ + {Name: "id", Short: "i", Usage: "Public key ID", Required: true}, + {Name: "dry-run", Usage: "Preview the delete request without removing a key", Bool: true, Default: "false"}, + }, Run: func(ctx *common.RuntimeContext) error { - return runWindowedUserGet(ctx, "/users/%s/statistics") + id, err := ctx.RequireArg("id") + if err != nil { + return err + } + path := fmt.Sprintf("/public_keys/%s", url.PathEscape(id)) + if ctx.Arg("dry-run") == "true" { + return ctx.OutputData(map[string]interface{}{ + "dry_run": true, + "action": "delete_public_key", + "method": "DELETE", + "path": path, + }) + } + env, err := ctx.CallAPI("DELETE", path, nil) + if err != nil { + return err + } + return ctx.Output(env) }, }, - { - Name: "project-trends", - Description: tr.T("cmd.user.project_trends.short"), - Flags: windowFlags, - Run: func(ctx *common.RuntimeContext) error { - return runWindowedUserGet(ctx, "/users/%s/project_trends") - }, - }, - { - Name: "trends", - Description: tr.T("cmd.user.trends.short"), - Flags: windowFlags, - Run: func(ctx *common.RuntimeContext) error { - return runWindowedUserGet(ctx, "/users/%s/project_trends") - }, + userStatsShortcut("activity", "Show user recent activity statistics", "statistics/activity", false, false), + userStatsShortcut("headmap", "Show user contribution heatmap", "headmaps", true, false), + userStatsShortcut("develop", "Show user development ability statistics", "statistics/develop", false, true), + userStatsShortcut("role", "Show user role statistics", "statistics/role", false, true), + userStatsShortcut("major", "Show user major statistics", "statistics/major", false, true), + } +} + +func userStatsShortcut(name, description, endpoint string, withYear, withTimeRange bool) *common.Shortcut { + flags := []common.Flag{ + {Name: "login", Short: "l", Usage: "User login name", Required: true}, + } + if withYear { + flags = append(flags, common.Flag{Name: "year", Usage: "Contribution year"}) + } + if withTimeRange { + flags = append(flags, + common.Flag{Name: "start-time", Usage: "Start timestamp"}, + common.Flag{Name: "end-time", Usage: "End timestamp"}, + ) + } + return &common.Shortcut{ + Name: name, + Description: description, + Flags: flags, + Run: func(ctx *common.RuntimeContext) error { + login, err := ctx.RequireArg("login") + if err != nil { + return err + } + q := url.Values{} + setQueryIfPresent(q, ctx, "year", "year") + setQueryIfPresent(q, ctx, "start-time", "start_time") + setQueryIfPresent(q, ctx, "end-time", "end_time") + path := fmt.Sprintf("/users/%s/%s", url.PathEscape(login), endpoint) + env, err := ctx.CallAPIWithQuery("GET", path, q) + if err != nil { + return err + } + return ctx.Output(env) }, } } -func runWindowedUserGet(ctx *common.RuntimeContext, pathFormat string) error { - user, err := resolveUser(ctx) - if err != nil { - return err +func publicKeyFromArgs(ctx *common.RuntimeContext) (string, error) { + key := strings.TrimSpace(ctx.Arg("key")) + keyFile := strings.TrimSpace(ctx.Arg("key-file")) + if key != "" && keyFile != "" { + return "", fmt.Errorf("use only one of --key or --key-file") } - q := url.Values{} - if v := ctx.Arg("start-time"); v != "" { - q.Set("start_time", v) + if key == "" && keyFile == "" { + return "", fmt.Errorf("required flag --key or --key-file is missing") } - if v := ctx.Arg("end-time"); v != "" { - q.Set("end_time", v) + if keyFile != "" { + data, err := os.ReadFile(keyFile) + if err != nil { + return "", fmt.Errorf("read --key-file: %w", err) + } + key = strings.TrimSpace(string(data)) } - env, err := ctx.CallAPIWithQuery("GET", fmt.Sprintf(pathFormat, user), q) - if err != nil { - return err + if key == "" { + return "", fmt.Errorf("public key content is empty") } - return ctx.Output(env) + return key, nil } -func resolveUser(ctx *common.RuntimeContext) (string, error) { - if v := ctx.Arg("user"); v != "" { - return v, nil +func setQueryIfPresent(q url.Values, ctx *common.RuntimeContext, flagName, queryName string) { + if value := ctx.Arg(flagName); value != "" { + q.Set(queryName, value) } - env, err := ctx.CallAPI("GET", "/users/me", nil) - if err != nil { - return "", err - } - if login := extractLogin(env); login != "" { - return login, nil - } - return "", fmt.Errorf("%s", ctx.Tr.T("error.user.required")) -} - -func extractLogin(env *output.Envelope) string { - data, ok := env.Data.(map[string]interface{}) - if !ok { - return "" - } - if v, ok := data["login"].(string); ok { - return v - } - return "" } func shortcutTranslator(translators ...*i18n.Translator) *i18n.Translator { diff --git a/shortcuts/user/user_test.go b/shortcuts/user/user_test.go index 51d931a..927d870 100644 --- a/shortcuts/user/user_test.go +++ b/shortcuts/user/user_test.go @@ -2,276 +2,328 @@ package user import ( "encoding/json" + "fmt" "net/http" "net/http/httptest" + "reflect" "testing" "github.com/gitlink-org/gitlink-cli/internal/client" "github.com/gitlink-org/gitlink-cli/shortcuts/common" ) -func runShortcut(t *testing.T, server *httptest.Server, name string, args map[string]string) error { - t.Helper() - shortcut := findShortcut(t, name) - ctx := &common.RuntimeContext{ - Client: &client.Client{HTTP: server.Client(), BaseURL: server.URL}, - Owner: "owner", - Repo: "repo", - Format: "json", - Args: args, - } - return shortcut.Run(ctx) -} - -func findShortcut(t *testing.T, name string) *common.Shortcut { - t.Helper() - for _, s := range Shortcuts() { - if s.Name == name { - return s - } - } - t.Fatalf("shortcut %q not found", name) - return nil -} - -func writeJSON(w http.ResponseWriter, v interface{}) { - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(v) -} - -// --- me --- - func TestUserMe(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/users/me.json" { - t.Fatalf("unexpected path: %s", r.URL.Path) - } - writeJSON(w, map[string]interface{}{ + server := newUserTestServer(t, func(w http.ResponseWriter, r *http.Request) { + assertUserRequest(t, r, "GET", "/users/me.json") + writeUserJSON(t, w, map[string]interface{}{ "login": "currentuser", "name": "Current User", "id": float64(1), }) - })) + }) defer server.Close() - err := runShortcut(t, server, "me", nil) - if err != nil { - t.Fatalf("me failed: %v", err) + if err := runUserShortcut(t, server, "me", nil); err != nil { + t.Fatalf("me shortcut failed: %v", err) } } -// --- info --- +func TestUserCurrent(t *testing.T) { + server := newUserTestServer(t, func(w http.ResponseWriter, r *http.Request) { + assertUserRequest(t, r, "GET", "/users/get_user_info.json") + writeUserJSON(t, w, map[string]interface{}{"login": "alice"}) + }) + defer server.Close() + + if err := runUserShortcut(t, server, "current", nil); err != nil { + t.Fatalf("current shortcut failed: %v", err) + } +} func TestUserInfo(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/users/alice.json" { - t.Fatalf("unexpected path: %s", r.URL.Path) - } - writeJSON(w, map[string]interface{}{ + server := newUserTestServer(t, func(w http.ResponseWriter, r *http.Request) { + assertUserRequest(t, r, "GET", "/users/alice.json") + writeUserJSON(t, w, map[string]interface{}{ "login": "alice", "name": "Alice", }) - })) + }) defer server.Close() - err := runShortcut(t, server, "info", map[string]string{"login": "alice"}) + if err := runUserShortcut(t, server, "info", map[string]string{"login": "alice"}); err != nil { + t.Fatalf("info shortcut failed: %v", err) + } +} + +func TestUserKeys(t *testing.T) { + server := newUserTestServer(t, func(w http.ResponseWriter, r *http.Request) { + assertUserRequest(t, r, "GET", "/public_keys.json") + assertUserQuery(t, r, "page", "2") + assertUserQuery(t, r, "limit", "50") + writeUserJSON(t, w, map[string]interface{}{"total_count": 0, "public_keys": []interface{}{}}) + }) + defer server.Close() + + err := runUserShortcut(t, server, "keys", map[string]string{ + "page": "2", + "limit": "50", + }) if err != nil { - t.Fatalf("info failed: %v", err) + t.Fatalf("keys shortcut failed: %v", err) + } +} + +func TestUserKeyCreatePayload(t *testing.T) { + var payload map[string]interface{} + server := newUserTestServer(t, func(w http.ResponseWriter, r *http.Request) { + assertUserRequest(t, r, "POST", "/public_keys.json") + payload = decodeUserJSON(t, r) + writeUserJSON(t, w, map[string]interface{}{"id": 1}) + }) + defer server.Close() + + err := runUserShortcut(t, server, "key-create", map[string]string{ + "title": "work laptop", + "key": "ssh-rsa AAAA test@example.com", + }) + if err != nil { + t.Fatalf("key-create shortcut failed: %v", err) + } + + assertUserEqual(t, payload["title"], "work laptop") + assertUserEqual(t, payload["key"], "ssh-rsa AAAA test@example.com") +} + +func TestUserKeyDelete(t *testing.T) { + server := newUserTestServer(t, func(w http.ResponseWriter, r *http.Request) { + assertUserRequest(t, r, "DELETE", "/public_keys/7.json") + writeUserJSON(t, w, map[string]interface{}{"status": 0, "message": "success"}) + }) + defer server.Close() + + if err := runUserShortcut(t, server, "key-delete", map[string]string{"id": "7"}); err != nil { + t.Fatalf("key-delete shortcut failed: %v", err) + } +} + +func TestUserKeyDryRunDoesNotCallAPI(t *testing.T) { + dryRunCases := []struct { + name string + args map[string]string + }{ + {name: "key-create", args: map[string]string{"title": "work laptop", "key": "ssh-rsa AAAA", "dry-run": "true"}}, + {name: "key-delete", args: map[string]string{"id": "7", "dry-run": "true"}}, + } + + for _, tc := range dryRunCases { + t.Run(tc.name, func(t *testing.T) { + server := newUserTestServer(t, func(w http.ResponseWriter, r *http.Request) { + t.Fatalf("dry-run should not call API, got %s %s", r.Method, r.URL.Path) + }) + defer server.Close() + + if err := runUserShortcut(t, server, tc.name, tc.args); err != nil { + t.Fatalf("%s dry-run failed: %v", tc.name, err) + } + }) + } +} + +func TestUserStatsShortcuts(t *testing.T) { + tests := []struct { + name string + args map[string]string + path string + queryKey string + queryValue string + queryKey2 string + queryValue2 string + }{ + {name: "activity", args: map[string]string{"login": "alice"}, path: "/users/alice/statistics/activity.json"}, + {name: "headmap", args: map[string]string{"login": "alice", "year": "2026"}, path: "/users/alice/headmaps.json", queryKey: "year", queryValue: "2026"}, + {name: "develop", args: map[string]string{"login": "alice", "start-time": "100", "end-time": "200"}, path: "/users/alice/statistics/develop.json", queryKey: "start_time", queryValue: "100", queryKey2: "end_time", queryValue2: "200"}, + {name: "role", args: map[string]string{"login": "alice", "start-time": "100", "end-time": "200"}, path: "/users/alice/statistics/role.json", queryKey: "start_time", queryValue: "100", queryKey2: "end_time", queryValue2: "200"}, + {name: "major", args: map[string]string{"login": "alice", "start-time": "100", "end-time": "200"}, path: "/users/alice/statistics/major.json", queryKey: "start_time", queryValue: "100", queryKey2: "end_time", queryValue2: "200"}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + server := newUserTestServer(t, func(w http.ResponseWriter, r *http.Request) { + assertUserRequest(t, r, "GET", tc.path) + if tc.queryKey != "" { + assertUserQuery(t, r, tc.queryKey, tc.queryValue) + } + if tc.queryKey2 != "" { + assertUserQuery(t, r, tc.queryKey2, tc.queryValue2) + } + writeUserJSON(t, w, map[string]interface{}{"status": 0}) + }) + defer server.Close() + + if err := runUserShortcut(t, server, tc.name, tc.args); err != nil { + t.Fatalf("%s shortcut failed: %v", tc.name, err) + } + }) } } func TestUserInfoMissingLogin(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - t.Fatal("no API call expected") - })) + server := newUserTestServer(t, func(w http.ResponseWriter, r *http.Request) { + t.Fatal("missing login should not call API") + }) defer server.Close() - err := runShortcut(t, server, "info", map[string]string{}) + err := runUserShortcut(t, server, "info", map[string]string{}) if err == nil { t.Fatal("expected error for missing login") } } -// --- heatmap --- - -func TestUserHeatmapExplicitUserWithYear(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/users/alice/headmaps.json" { - t.Fatalf("unexpected path: %s", r.URL.Path) - } - if got := r.URL.Query().Get("year"); got != "2026" { - t.Fatalf("year = %q, want 2026", got) - } - writeJSON(w, map[string]interface{}{ - "total_contributions": float64(12), - }) - })) +func TestUserKeyCreateRejectsMissingKey(t *testing.T) { + server := newUserTestServer(t, func(w http.ResponseWriter, r *http.Request) { + t.Fatalf("missing key should not call API, got %s %s", r.Method, r.URL.Path) + }) defer server.Close() - err := runShortcut(t, server, "heatmap", map[string]string{"user": "alice", "year": "2026"}) - if err != nil { - t.Fatalf("heatmap failed: %v", err) - } -} - -func TestUserHeatmapDefaultsToCurrentUser(t *testing.T) { - var calls []string - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - calls = append(calls, r.URL.Path) - switch r.URL.Path { - case "/users/me.json": - writeJSON(w, map[string]interface{}{"login": "currentuser"}) - case "/users/currentuser/headmaps.json": - writeJSON(w, map[string]interface{}{"headmaps": []interface{}{}}) - default: - t.Fatalf("unexpected path: %s", r.URL.Path) - } - })) - defer server.Close() - - err := runShortcut(t, server, "heatmap", map[string]string{}) - if err != nil { - t.Fatalf("heatmap failed: %v", err) - } - if len(calls) != 2 { - t.Fatalf("calls = %v, want 2 calls", calls) - } -} - -// --- statistics --- - -func TestUserStatisticsWithTimeWindow(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/users/alice/statistics.json" { - t.Fatalf("unexpected path: %s", r.URL.Path) - } - if got := r.URL.Query().Get("start_time"); got != "100" { - t.Fatalf("start_time = %q, want 100", got) - } - if got := r.URL.Query().Get("end_time"); got != "200" { - t.Fatalf("end_time = %q, want 200", got) - } - writeJSON(w, map[string]interface{}{ - "issues_count": float64(3), - }) - })) - defer server.Close() - - args := map[string]string{"user": "alice", "start-time": "100", "end-time": "200"} - err := runShortcut(t, server, "statistics", args) - if err != nil { - t.Fatalf("statistics failed: %v", err) - } -} - -func TestUserStatsAlias(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/users/alice/statistics.json" { - t.Fatalf("unexpected path: %s", r.URL.Path) - } - writeJSON(w, map[string]interface{}{}) - })) - defer server.Close() - - err := runShortcut(t, server, "stats", map[string]string{"user": "alice"}) - if err != nil { - t.Fatalf("stats alias failed: %v", err) - } -} - -// --- project trends --- - -func TestUserProjectTrendsWithTimeWindow(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/users/alice/project_trends.json" { - t.Fatalf("unexpected path: %s", r.URL.Path) - } - if got := r.URL.Query().Get("start_time"); got != "100" { - t.Fatalf("start_time = %q, want 100", got) - } - if got := r.URL.Query().Get("end_time"); got != "200" { - t.Fatalf("end_time = %q, want 200", got) - } - writeJSON(w, map[string]interface{}{ - "trends": []interface{}{}, - }) - })) - defer server.Close() - - args := map[string]string{"user": "alice", "start-time": "100", "end-time": "200"} - err := runShortcut(t, server, "project-trends", args) - if err != nil { - t.Fatalf("project-trends failed: %v", err) - } -} - -func TestUserTrendsAlias(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/users/alice/project_trends.json" { - t.Fatalf("unexpected path: %s", r.URL.Path) - } - writeJSON(w, map[string]interface{}{}) - })) - defer server.Close() - - err := runShortcut(t, server, "trends", map[string]string{"user": "alice"}) - if err != nil { - t.Fatalf("trends alias failed: %v", err) - } -} - -func TestUserDefaultUserMissingLogin(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/users/me.json" { - t.Fatalf("unexpected path: %s", r.URL.Path) - } - writeJSON(w, map[string]interface{}{"name": "no login"}) - })) - defer server.Close() - - err := runShortcut(t, server, "statistics", map[string]string{}) + err := runUserShortcut(t, server, "key-create", map[string]string{"title": "work laptop"}) if err == nil { - t.Fatal("expected error when /users/me has no login") + t.Fatal("expected missing key to return an error") } } -// --- HTTP error paths --- - func TestUserMeHTTPError(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + server := newUserTestServer(t, func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte("server error")) - })) + if _, err := w.Write([]byte("server error")); err != nil { + t.Fatalf("write response: %v", err) + } + }) defer server.Close() - err := runShortcut(t, server, "me", nil) - if err == nil { - t.Fatal("expected error for HTTP 500") - } -} - -func TestUserHeatmapHTTPError(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte("server error")) - })) - defer server.Close() - - err := runShortcut(t, server, "heatmap", map[string]string{"user": "alice"}) + err := runUserShortcut(t, server, "me", nil) if err == nil { t.Fatal("expected error for HTTP 500") } } func TestUserInfoHTTPError(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + server := newUserTestServer(t, func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte("server error")) - })) + if _, err := w.Write([]byte("server error")); err != nil { + t.Fatalf("write response: %v", err) + } + }) defer server.Close() - err := runShortcut(t, server, "info", map[string]string{"login": "alice"}) + err := runUserShortcut(t, server, "info", map[string]string{"login": "alice"}) if err == nil { t.Fatal("expected error for HTTP 500") } } + +func TestUserShortcutNames(t *testing.T) { + got := map[string]bool{} + for _, shortcut := range Shortcuts() { + got[shortcut.Name] = true + } + want := []string{"me", "current", "info", "keys", "key-create", "key-delete", "activity", "headmap", "develop", "role", "major"} + for _, name := range want { + if !got[name] { + t.Fatalf("missing shortcut %q in %v", name, got) + } + } + if len(got) != len(want) { + t.Fatalf("shortcut count = %d, want %d: %v", len(got), len(want), got) + } +} + +func runUserShortcut(t *testing.T, server *httptest.Server, name string, args map[string]string) error { + t.Helper() + shortcut := findUserShortcut(t, name) + ctx := &common.RuntimeContext{ + Client: &client.Client{ + HTTP: server.Client(), + BaseURL: server.URL, + }, + Owner: "owner", + Repo: "repo", + Format: "json", + Args: args, + } + if ctx.Args == nil { + ctx.Args = map[string]string{} + } + return shortcut.Run(ctx) +} + +func findUserShortcut(t *testing.T, name string) *common.Shortcut { + t.Helper() + for _, shortcut := range Shortcuts() { + if shortcut.Name == name { + return shortcut + } + } + t.Fatalf("shortcut %q not found", name) + return nil +} + +func newUserTestServer(t *testing.T, handler http.HandlerFunc) *httptest.Server { + t.Helper() + return httptest.NewServer(handler) +} + +func assertUserRequest(t *testing.T, r *http.Request, method, path string) { + t.Helper() + if r.Method != method || r.URL.Path != path { + t.Fatalf("got request %s %s, want %s %s", r.Method, r.URL.Path, method, path) + } +} + +func assertUserQuery(t *testing.T, r *http.Request, key, want string) { + t.Helper() + if got := r.URL.Query().Get(key); got != want { + t.Fatalf("query %s = %q, want %q", key, got, want) + } +} + +func decodeUserJSON(t *testing.T, r *http.Request) map[string]interface{} { + t.Helper() + var payload map[string]interface{} + if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { + t.Fatalf("failed to decode request body: %v", err) + } + return payload +} + +func writeUserJSON(t *testing.T, w http.ResponseWriter, payload interface{}) { + t.Helper() + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(payload); err != nil { + t.Fatalf("failed to write response: %v", err) + } +} + +func assertUserEqual(t *testing.T, got interface{}, want interface{}) { + t.Helper() + if !reflect.DeepEqual(got, want) { + t.Fatalf("got %v (%T), want %v (%T)", got, got, want, want) + } +} + +func ExampleShortcuts() { + for _, shortcut := range Shortcuts() { + fmt.Println(shortcut.Name) + } + // Output: + // me + // current + // info + // keys + // key-create + // key-delete + // activity + // headmap + // develop + // role + // major +} diff --git a/skills/README.md b/skills/README.md index 6735c07..487239c 100644 --- a/skills/README.md +++ b/skills/README.md @@ -32,7 +32,7 @@ gitlink-cli auth status gitlink-cli user +me ``` -详见: [gitlink-shared/SKILL.md](gitlink-shared/SKILL.md) +详见: [gitlink-shared/examples/auth-workflow.md](gitlink-shared/examples/auth-workflow.md) ### 2. 查看可用命令 @@ -64,19 +64,25 @@ skills/ ├── README.md # 本文件 ├── gitlink-shared/ # 共享基础规则 │ ├── SKILL.md # 认证、全局参数、安全规则、分支约定 -│ └── references/ -│ ├── api-reference.md # API 详细参考、错误处理 -│ ├── raw-api-batch.md # 批量 Raw API 调用参考 -│ └── troubleshooting.md # 常见问题排查 +│ ├── REFERENCE.md # API 详细参考、错误处理 +│ ├── TROUBLESHOOTING.md # 常见问题排查 +│ └── examples/ +│ └── auth-workflow.md # 认证工作流示例 ├── gitlink-repo/ # 仓库管理 │ ├── SKILL.md # 仓库操作指南 -│ └── references/ # 仓库命令参考文档 +│ ├── REFERENCE.md # 仓库 API 参考 +│ └── examples/ +│ └── repo-workflow.md # 仓库管理工作流 ├── gitlink-issue/ # Issue 管理 │ ├── SKILL.md # Issue 操作指南 -│ └── references/ # Issue 命令参考文档 +│ ├── REFERENCE.md # Issue API 参考 +│ └── examples/ +│ └── issue-workflow.md # Issue 全流程工作流 ├── gitlink-pr/ # Pull Request │ ├── SKILL.md # PR 操作指南 -│ └── references/ # PR 命令参考文档 +│ ├── REFERENCE.md # PR API 参考 +│ └── examples/ +│ └── pr-workflow.md # PR 工作流 ├── gitlink-member/ # 仓库成员管理 │ └── SKILL.md # 成员与邀请链接操作指南 ├── gitlink-branch/ # 分支管理 @@ -85,24 +91,25 @@ skills/ │ └── branch-workflow.md # 分支工作流 ├── gitlink-release/ # 版本发布 │ ├── SKILL.md # Release 操作指南 -│ └── references/ # Release 命令参考文档 -├── gitlink-release-auto/ # 自动化 Release 管理 -│ └── SKILL.md # 自动发版、版本号推荐、Release Notes 生成 +│ ├── REFERENCE.md # Release API 参考 +│ └── examples/ +│ └── release-workflow.md # Release 工作流 ├── gitlink-search/ # 搜索功能 │ ├── SKILL.md # 搜索操作指南 -│ └── references/ # 搜索命令参考文档 +│ └── examples/ +│ └── search-workflow.md # 搜索工作流 ├── gitlink-user/ # 用户管理 -│ ├── SKILL.md # 用户操作指南 -│ └── references/ # 用户命令参考文档 +│ └── SKILL.md # 用户操作指南 ├── gitlink-org/ # 组织管理 │ ├── SKILL.md # 组织操作指南 -│ └── references/ # 组织命令参考文档 +│ └── examples/ +│ └── org-workflow.md # 组织工作流 ├── gitlink-ci/ # CI/CD -│ └── SKILL.md # CI 操作指南 +│ ├── SKILL.md # CI 操作指南 +│ └── examples/ +│ └── ci-workflow.md # CI 工作流 ├── gitlink-pipeline/ # 流水线工作流 │ └── SKILL.md # Pipeline 操作指南 -├── gitlink-wiki/ # Wiki 页面管理 -│ └── SKILL.md # Wiki 操作指南 ├── gitlink-pm/ # 项目管理 │ └── SKILL.md # PM 操作指南 ├── gitlink-health/ # 项目健康度分析 @@ -128,7 +135,7 @@ skills/ |-------|------|----------| | **gitlink-shared** | 认证、全局参数、API 参考、安全规则、分支约定 | `auth login`, `auth status` | | **gitlink-repo** | 仓库管理与洞察 | `repo +list`, `repo +info`, `repo +languages`, `repo +contributors`, `repo +code-stats`, `repo +follow`, `repo +like` | -| **gitlink-issue** | Issue 管理 | `issue +create`, `issue +list`, `issue +view`, `issue +close`, `issue +batch-close`, `issue +batch-update`, `issue +batch-delete` | +| **gitlink-issue** | Issue 管理 | `issue +create`, `issue +list`, `issue +view`, `issue +close`, `issue +batch-close` | | **gitlink-pr** | Pull Request | `pr +list`, `pr +create`, `pr +view`, `pr +merge`, `pr +versions`, `pr +version-diff`, `pr +reviews`, `pr +review` | | **gitlink-member** | 仓库成员管理 | `member +list`, `member +add`, `member +batch-add`, `member +role`, `member +invite-link` | | **gitlink-branch** | 分支管理 | `branch +list`, `branch +create`, `branch +delete`, `branch +protect` | @@ -139,11 +146,10 @@ skills/ | Skill | 说明 | 常用命令 | |-------|------|----------| | **gitlink-search** | 搜索功能 | `search +repos`, `search +users` | -| **gitlink-user** | 用户管理 | `user +me`, `user +info`, `user +heatmap`, `user +statistics`, `user +project-trends` | +| **gitlink-user** | 用户管理 | `user +me`, `user +current`, `user +keys`, `user +activity` | | **gitlink-org** | 组织管理 | `org +list`, `org +info`, `org +members` | | **gitlink-ci** | CI/CD | `ci +builds`, `ci +logs` | | **gitlink-pipeline** | 流水线工作流 | `pipeline +runs`, `pipeline +run`, `pipeline +logs` | -| **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 | @@ -163,7 +169,7 @@ gitlink-cli repo +info gitlink-cli repo +info --owner wbtiger --repo gitlink-cli ``` -详见: [gitlink-repo/SKILL.md](gitlink-repo/SKILL.md) +详见: [gitlink-repo/examples/repo-workflow.md](gitlink-repo/examples/repo-workflow.md) ### 场景 2:创建和管理 Issue @@ -184,7 +190,7 @@ gitlink-cli issue +close -i 123 gitlink-cli issue +batch-close --numbers 123,124 --dry-run ``` -详见: [gitlink-issue/SKILL.md](gitlink-issue/SKILL.md) +详见: [gitlink-issue/examples/issue-workflow.md](gitlink-issue/examples/issue-workflow.md) ### 场景 3:管理分支和发布 @@ -202,7 +208,7 @@ gitlink-cli release +create -t v1.0.0 -n "v1.0.0 正式版" -b "更新内容..." gitlink-cli release +view -i ``` -详见: [gitlink-release/SKILL.md](gitlink-release/SKILL.md) +详见: [gitlink-release/examples/release-workflow.md](gitlink-release/examples/release-workflow.md) ### 场景 4:搜索和发现 @@ -218,7 +224,7 @@ gitlink-cli org +list gitlink-cli org +info -i Gitlink ``` -详见: [gitlink-search/SKILL.md](gitlink-search/SKILL.md) +详见: [gitlink-search/examples/search-workflow.md](gitlink-search/examples/search-workflow.md) --- @@ -227,8 +233,8 @@ gitlink-cli org +info -i Gitlink ### 快速查找 - **我想了解认证**: [gitlink-shared/SKILL.md](gitlink-shared/SKILL.md) -- **我想查看 API 细节**: [gitlink-shared/references/api-reference.md](gitlink-shared/references/api-reference.md) -- **我遇到了错误**: [gitlink-shared/references/troubleshooting.md](gitlink-shared/references/troubleshooting.md) +- **我想查看 API 细节**: [gitlink-shared/REFERENCE.md](gitlink-shared/REFERENCE.md) +- **我遇到了错误**: [gitlink-shared/TROUBLESHOOTING.md](gitlink-shared/TROUBLESHOOTING.md) - **我想看工作流示例**: 查看各 Skill 下的 `examples/` 目录 ### 按功能分类 @@ -236,12 +242,12 @@ gitlink-cli org +info -i Gitlink **仓库操作**: - [gitlink-repo/SKILL.md](gitlink-repo/SKILL.md) - 仓库命令 - [gitlink-branch/SKILL.md](gitlink-branch/SKILL.md) - 分支命令 -- [gitlink-repo/SKILL.md](gitlink-repo/SKILL.md) - 完整工作流 +- [gitlink-repo/examples/repo-workflow.md](gitlink-repo/examples/repo-workflow.md) - 完整工作流 **Issue 和 PR**: - [gitlink-issue/SKILL.md](gitlink-issue/SKILL.md) - Issue 命令 - [gitlink-pr/SKILL.md](gitlink-pr/SKILL.md) - PR 命令 -- [gitlink-issue/SKILL.md](gitlink-issue/SKILL.md) - Issue 工作流 +- [gitlink-issue/examples/issue-workflow.md](gitlink-issue/examples/issue-workflow.md) - Issue 工作流 **发布和搜索**: - [gitlink-release/SKILL.md](gitlink-release/SKILL.md) - Release 命令 @@ -283,11 +289,11 @@ gitlink-cli auth login ### Q: 如何查看完整的 API 参考? -A: 查看 [gitlink-shared/references/api-reference.md](gitlink-shared/references/api-reference.md) +A: 查看 [gitlink-shared/REFERENCE.md](gitlink-shared/REFERENCE.md) ### Q: 遇到错误怎么办? -A: 查看 [gitlink-shared/references/troubleshooting.md](gitlink-shared/references/troubleshooting.md) +A: 查看 [gitlink-shared/TROUBLESHOOTING.md](gitlink-shared/TROUBLESHOOTING.md) --- @@ -323,7 +329,7 @@ AI 代理可以: - 所有边界情况处理正确 - 完整的文档和示例 -详见: [../doc/design.md](../doc/design.md) +详见: [../doc/SKILLS_TEST_REPORT_2026-04-02.md](../doc/SKILLS_TEST_REPORT_2026-04-02.md) --- @@ -331,7 +337,8 @@ AI 代理可以: - [主项目 README](../README.md) - gitlink-cli 项目说明 - [设计文档](../doc/design.md) - 架构设计和开发计划 -- [API 参考文档](../doc/gitlink_api_reference.md) - GitLink API 参考文档 +- [测试报告](../doc/SKILLS_TEST_REPORT_2026-04-02.md) - 功能测试报告 +- [代码同步方案](../doc/CODE_SYNC_STRATEGY_FINAL.md) - GitHub ↔ GitLink 同步设计 - [gitlink-bisync](https://www.gitlink.org.cn/wbtiger/gitlink-bisync) - 代码双向同步系统 --- @@ -339,8 +346,8 @@ AI 代理可以: ## 📞 获取帮助 - **命令帮助**: `gitlink-cli --help` -- **故障排查**: [gitlink-shared/references/troubleshooting.md](gitlink-shared/references/troubleshooting.md) -- **API 参考**: [gitlink-shared/references/api-reference.md](gitlink-shared/references/api-reference.md) +- **故障排查**: [gitlink-shared/TROUBLESHOOTING.md](gitlink-shared/TROUBLESHOOTING.md) +- **API 参考**: [gitlink-shared/REFERENCE.md](gitlink-shared/REFERENCE.md) - **工作流示例**: 查看各 Skill 下的 `examples/` 目录 --- @@ -348,7 +355,7 @@ AI 代理可以: ## 🎓 下一步 1. 阅读 [gitlink-shared/SKILL.md](gitlink-shared/SKILL.md) 了解基础 -2. 查看 [gitlink-shared/SKILL.md](gitlink-shared/SKILL.md) 完成认证 +2. 查看 [gitlink-shared/examples/auth-workflow.md](gitlink-shared/examples/auth-workflow.md) 完成认证 3. 根据需求选择相应的 Skill 文档 4. 参考 `examples/` 目录中的工作流示例 5. 使用 AI 代理自动化你的工作流 diff --git a/skills/gitlink-user/SKILL.md b/skills/gitlink-user/SKILL.md index 8ab9c71..b405aa2 100644 --- a/skills/gitlink-user/SKILL.md +++ b/skills/gitlink-user/SKILL.md @@ -1,7 +1,7 @@ --- name: gitlink-user version: 1.0.0 -description: "用户操作:查看当前用户、用户详情、贡献热力图、统计和项目趋势。当用户需要查看 GitLink 用户信息时触发。" +description: "用户操作:查看当前用户、用户详情、Public Keys 和用户统计。当用户需要查看或管理 GitLink 用户信息时触发。" metadata: requires: bins: ["gitlink-cli"] @@ -21,34 +21,42 @@ metadata: | Shortcut | 说明 | 需要认证 | |----------|------|----------| | `user +me` | 当前登录用户 | 是 | +| `user +current` | 当前用户详细资料 | 是 | | `user +info` | 查看用户详情 | 否 | -| `user +heatmap` | 用户贡献热力图 | 省略 `--user` 时需要 | -| `user +statistics` | 用户聚合统计 | 省略 `--user` 时需要 | -| `user +stats` | `user +statistics` 的短别名 | 省略 `--user` 时需要 | -| `user +project-trends` | 用户项目趋势 | 省略 `--user` 时需要 | -| `user +trends` | `user +project-trends` 的短别名 | 省略 `--user` 时需要 | +| `user +keys` | 当前用户 Public Keys 列表 | 是 | +| `user +key-create` | 创建 Public Key | 是 | +| `user +key-delete` | 删除 Public Key | 是 | +| `user +activity` | 用户近期活动统计 | 否 | +| `user +headmap` | 用户贡献热力图 | 否 | +| `user +develop` | 用户开发能力统计 | 否 | +| `user +role` | 用户角色统计 | 否 | +| `user +major` | 用户专业定位统计 | 否 | ## 使用示例 ```bash # 查看当前用户 gitlink-cli user +me +gitlink-cli user +current --format json # 查看其他用户 gitlink-cli user +info --login zhangsan -# 用户贡献热力图 -gitlink-cli user +heatmap --user zhangsan --year 2026 +# Public Key 管理,写入/删除前先 dry-run +gitlink-cli user +keys --page 1 --limit 20 +gitlink-cli user +key-create --title "work laptop" --key-file ~/.ssh/id_rsa.pub --dry-run +gitlink-cli user +key-delete --id 7 --dry-run # 用户统计 -gitlink-cli user +statistics --user zhangsan --start-time 1704067200 --end-time 1735689600 - -# 用户项目动态 -gitlink-cli user +project-trends --user zhangsan +gitlink-cli user +activity --login zhangsan +gitlink-cli user +headmap --login zhangsan --year 2026 +gitlink-cli user +develop --login zhangsan --start-time 1704067200 --end-time 1735689599 +gitlink-cli user +role --login zhangsan --start-time 1704067200 --end-time 1735689599 +gitlink-cli user +major --login zhangsan --start-time 1704067200 --end-time 1735689599 ``` -## 注意事项 +## API 注意事项 -- `user +heatmap`、`user +statistics`、`user +project-trends` 都是只读命令。 -- 省略 `--user` 时会先调用 `user +me` 等价的 `/users/me` 解析当前登录用户,因此需要已登录。 -- `user +stats` 和 `user +trends` 是为贡献者分析工作流保留的短别名。 +- `user +key-create` 支持 `--key` 直接传入公钥内容,也支持 `--key-file` 从本地公钥文件读取。 +- `user +key-create` 和 `user +key-delete` 支持 `--dry-run`,写入/删除前建议先预览请求。 +- 统计命令使用用户 `login`,不是数字用户 ID。