Merge PR #198: feat(message): 增加消息中心快捷命令
# Conflicts: # README.md # shortcuts/register.go
This commit is contained in:
commit
9b31ce8aac
153
README.md
153
README.md
|
|
@ -5,7 +5,7 @@
|
|||
[](https://golang.org)
|
||||
[](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
|
|||
<a href="https://www.gitlink.org.cn/jiangtx" title="jiangtx"><img src="https://www.gitlink.org.cn/system/lets/letter_avatars/2/J/67_157_94/120.png" width="40" height="40" alt="jiangtx" style="border-radius: 50%;"></a>
|
||||
<br><sub><a href="https://www.gitlink.org.cn/jiangtx">jiangtx</a></sub>
|
||||
</div>
|
||||
<div align="center">
|
||||
<a href="https://www.gitlink.org.cn/luwanzhou" title="luwanzhou"><img src="https://www.gitlink.org.cn/system/lets/letter_avatars/2/L/165_135_246/120.png" width="40" height="40" alt="luwanzhou" style="border-radius: 50%;"></a>
|
||||
<br><sub><a href="https://www.gitlink.org.cn/luwanzhou">luwanzhou</a></sub>
|
||||
</div>
|
||||
<div align="center">
|
||||
<a href="https://www.gitlink.org.cn/whale_hihihi" title="whale_hihihi"><img src="https://www.gitlink.org.cn/images/avatars/User/137722?t=1778575729" width="40" height="40" alt="whale_hihihi" style="border-radius: 50%;"></a>
|
||||
<br><sub><a href="https://www.gitlink.org.cn/whale_hihihi">whale_hihihi</a></sub>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
## 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/reopen/label/assign/comment/export/import, 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 |
|
||||
|
|
@ -115,12 +107,12 @@ The official [GitLink](https://www.gitlink.org.cn) CLI tool — built for humans
|
|||
| ⚙️ Pipeline | Run, inspect, enable, disable, delete pipeline workflows and logs |
|
||||
| 🔔 Webhook | Manage repo webhooks and test deliveries |
|
||||
| 🔍 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 |
|
||||
| 📋 PM | Sprint management, kanban boards, weekly reports |
|
||||
| 🤖 Workflow | AI-powered issue triage, PR review, release notes |
|
||||
|
||||
The `message` shortcut group adds inbox automation for listing messages, checking unread counters, marking messages as read, and deleting older notifications.
|
||||
|
||||
## Installation & Quick Start
|
||||
|
||||
### Requirements
|
||||
|
|
@ -249,30 +241,11 @@ 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
|
||||
|
||||
# List and update repository navigation units
|
||||
gitlink-cli repo +units --owner Gitlink --repo forgeplus
|
||||
gitlink-cli repo +set-units --owner Gitlink --repo forgeplus --units code,issues,pulls,wiki
|
||||
|
||||
# Create a repository
|
||||
gitlink-cli repo +create -n my-project -d "Project description"
|
||||
|
||||
# Fork a repository
|
||||
gitlink-cli repo +fork --owner Gitlink --repo forgeplus
|
||||
|
||||
# List organizations that can receive a repository transfer
|
||||
gitlink-cli repo +transfer-orgs --owner Gitlink --repo forgeplus
|
||||
|
||||
# Preview a repository transfer without changing data
|
||||
gitlink-cli repo +transfer --owner Gitlink --repo forgeplus --target-owner my-org --dry-run
|
||||
|
||||
# Confirm and send a repository transfer request
|
||||
gitlink-cli repo +transfer --owner Gitlink --repo forgeplus --target-owner my-org --yes
|
||||
|
||||
# Preview canceling a pending repository transfer
|
||||
gitlink-cli repo +transfer-cancel --owner Gitlink --repo forgeplus --dry-run
|
||||
|
||||
# Confirm canceling a pending repository transfer
|
||||
gitlink-cli repo +transfer-cancel --owner Gitlink --repo forgeplus --yes
|
||||
```
|
||||
|
||||
### Webhook Management
|
||||
|
|
@ -314,17 +287,20 @@ gitlink-cli member +role --owner Gitlink --repo forgeplus --user-id 101 --role D
|
|||
gitlink-cli member +invite-link --owner Gitlink --repo forgeplus --role developer --apply true
|
||||
```
|
||||
|
||||
### Attachment Operations
|
||||
### Message Center Management
|
||||
|
||||
```bash
|
||||
# Upload a local file
|
||||
gitlink-cli attachment +upload -f ./build.log -d "CI build log"
|
||||
# List the latest notifications with unread filtering
|
||||
gitlink-cli message +list --login Mengz --type notification --status unread --limit 10
|
||||
|
||||
# Upload a file and attach container metadata
|
||||
gitlink-cli attachment +upload -f ./release-notes.md --container-id 42 --container-type VersionRelease
|
||||
# Show unread counters for notifications and @mentions
|
||||
gitlink-cli message +stats --login Mengz
|
||||
|
||||
# Delete an uploaded attachment
|
||||
gitlink-cli attachment +delete -i 791eccbf-2e35-4301-ad95-8c937a117f40
|
||||
# Preview marking all notifications as read
|
||||
gitlink-cli message +read --login Mengz --type notification --all --dry-run
|
||||
|
||||
# Delete selected @mention messages
|
||||
gitlink-cli message +delete --login Mengz --type atme --ids 101,102
|
||||
```
|
||||
|
||||
### Issue Management
|
||||
|
|
@ -354,38 +330,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
|
||||
|
||||
# Batch reopen closed issues by issue numbers
|
||||
gitlink-cli issue +batch-reopen --owner Gitlink --repo forgeplus --numbers 123,124 --dry-run
|
||||
gitlink-cli issue +batch-reopen --owner Gitlink --repo forgeplus --from issues.csv
|
||||
|
||||
# Batch add/remove labels by API issue IDs (requires numeric tag IDs)
|
||||
gitlink-cli issue +batch-label --owner Gitlink --repo forgeplus --ids 101,102 --add 1,2 --dry-run
|
||||
gitlink-cli issue +batch-label --owner Gitlink --repo forgeplus --ids 101,102 --remove 3
|
||||
|
||||
# Batch assign/unassign users by API issue IDs (requires numeric user IDs)
|
||||
gitlink-cli issue +batch-assign --owner Gitlink --repo forgeplus --ids 101,102 --add 5,6 --dry-run
|
||||
gitlink-cli issue +batch-assign --owner Gitlink --repo forgeplus --ids 101,102 --remove 7
|
||||
|
||||
# Batch add comments to multiple issues by issue numbers
|
||||
gitlink-cli issue +batch-comment --owner Gitlink --repo forgeplus --numbers 123,124 --message "Batch update notice" --dry-run
|
||||
gitlink-cli issue +batch-comment --owner Gitlink --repo forgeplus --from issues.csv --message "Processed"
|
||||
|
||||
# Export issues to CSV or JSON with filters
|
||||
gitlink-cli issue +batch-export --owner Gitlink --repo forgeplus --state open --format csv --output issues.csv
|
||||
gitlink-cli issue +batch-export --owner Gitlink --repo forgeplus --state closed --keyword bug --format json --output closed_issues.json
|
||||
|
||||
# Import issues from a CSV file
|
||||
gitlink-cli issue +batch-import --owner Gitlink --repo forgeplus --from new_issues.csv --dry-run
|
||||
gitlink-cli issue +batch-import --owner Gitlink --repo forgeplus --from new_issues.csv
|
||||
|
||||
# Add a comment
|
||||
gitlink-cli issue +comment --owner Gitlink --repo forgeplus -i 123 -b "Fixed"
|
||||
|
||||
|
|
@ -541,16 +485,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
|
||||
|
|
@ -561,29 +495,6 @@ 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.
|
||||
|
||||
```bash
|
||||
# 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
|
||||
```
|
||||
|
||||
### Workflow Agent Commands
|
||||
|
||||
`workflow` provides rule-based repository analysis for maintainers and AI Agents. It currently supports:
|
||||
|
|
@ -683,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 <uuid> --dry-run
|
||||
gitlink-cli dataset +delete-attachment --owner me --repo proj --uuid <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:
|
||||
|
|
@ -765,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) |
|
||||
|
|
@ -835,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
|
||||
|
|
@ -900,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
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
# Message Center Shortcuts
|
||||
|
||||
## Summary
|
||||
|
||||
This change adds a new `message` shortcut group for personal inbox management in GitLink CLI.
|
||||
It covers message listing, unread counters, batch mark-as-read, and batch delete workflows without forcing users to drop down to raw API calls.
|
||||
|
||||
## Included Commands
|
||||
|
||||
- `message +list` filters inbox items by message type, read status, page, and limit.
|
||||
- `message +stats` returns unread counters for notifications and `@me` messages.
|
||||
- `message +read` marks selected message IDs, or all unread messages of a given type, as read.
|
||||
- `message +delete` deletes selected message IDs, or all unread messages of a given type.
|
||||
|
||||
## Usability Details
|
||||
|
||||
- `--login` defaults to the authenticated user when omitted.
|
||||
- `--dry-run` is supported for write operations so users can inspect destructive requests first.
|
||||
- `content_text` is added to list output to expose HTML-free plain text that is easier to grep, diff, and script.
|
||||
|
||||
## Validation
|
||||
|
||||
- Added shortcut tests for list, stats, read, delete, ID parsing, and content normalization.
|
||||
- Verified registration by wiring the `message` group into the global shortcut registry.
|
||||
- Updated `README.md` with feature coverage and usage examples.
|
||||
|
|
@ -0,0 +1,418 @@
|
|||
package message
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
|
||||
)
|
||||
|
||||
const (
|
||||
messageTypeNotification = "notification"
|
||||
messageTypeAtme = "atme"
|
||||
messageTypeAll = "all"
|
||||
|
||||
messageStatusAll = "all"
|
||||
messageStatusUnread = "unread"
|
||||
messageStatusRead = "read"
|
||||
)
|
||||
|
||||
var htmlTagPattern = regexp.MustCompile(`<[^>]+>`)
|
||||
|
||||
type currentUserResponse struct {
|
||||
Login string `json:"login"`
|
||||
}
|
||||
|
||||
type listResponse struct {
|
||||
TotalCount int `json:"total_count"`
|
||||
Type string `json:"type"`
|
||||
UnreadNotification int `json:"unread_notification"`
|
||||
UnreadAtme int `json:"unread_atme"`
|
||||
Messages []messageRow `json:"messages"`
|
||||
}
|
||||
|
||||
type messageRow struct {
|
||||
ID int64 `json:"id"`
|
||||
Status int `json:"status"`
|
||||
Content string `json:"content"`
|
||||
NotificationURL string `json:"notification_url"`
|
||||
Source string `json:"source"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
TimeAgo string `json:"time_ago"`
|
||||
Type string `json:"type"`
|
||||
Sender map[string]interface{} `json:"sender"`
|
||||
}
|
||||
|
||||
type messageOutput struct {
|
||||
ID int64 `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Status int `json:"status"`
|
||||
Read bool `json:"read"`
|
||||
Source string `json:"source,omitempty"`
|
||||
Content string `json:"content"`
|
||||
ContentText string `json:"content_text"`
|
||||
NotificationURL string `json:"notification_url,omitempty"`
|
||||
CreatedAt string `json:"created_at,omitempty"`
|
||||
TimeAgo string `json:"time_ago,omitempty"`
|
||||
Sender map[string]interface{} `json:"sender,omitempty"`
|
||||
}
|
||||
|
||||
// Shortcuts returns message management shortcuts.
|
||||
func Shortcuts() []*common.Shortcut {
|
||||
return []*common.Shortcut{
|
||||
{
|
||||
Name: "list",
|
||||
Description: "List user messages with filters and plain-text content",
|
||||
Flags: []common.Flag{
|
||||
{Name: "login", Short: "l", Usage: "Target user login (defaults to current authenticated user)"},
|
||||
{Name: "type", Short: "t", Usage: "Message type: notification, atme, or all", Default: messageTypeAll},
|
||||
{Name: "status", Usage: "Message status: unread, read, or all", Default: messageStatusAll},
|
||||
{Name: "page", Usage: "Page number", Default: "1"},
|
||||
{Name: "limit", Usage: "Items per page", Default: "20"},
|
||||
},
|
||||
Run: runList,
|
||||
},
|
||||
{
|
||||
Name: "stats",
|
||||
Description: "Show unread message counters for a user",
|
||||
Flags: []common.Flag{
|
||||
{Name: "login", Short: "l", Usage: "Target user login (defaults to current authenticated user)"},
|
||||
{Name: "type", Short: "t", Usage: "Message type: notification, atme, or all", Default: messageTypeAll},
|
||||
},
|
||||
Run: runStats,
|
||||
},
|
||||
{
|
||||
Name: "read",
|
||||
Description: "Mark selected messages as read",
|
||||
Flags: []common.Flag{
|
||||
{Name: "login", Short: "l", Usage: "Target user login (defaults to current authenticated user)"},
|
||||
{Name: "type", Short: "t", Usage: "Message type: notification or atme", Required: true},
|
||||
{Name: "ids", Usage: "Comma-separated message IDs"},
|
||||
{Name: "all", Usage: "Mark all unread messages of the selected type as read", Bool: true, Default: "false"},
|
||||
{Name: "dry-run", Usage: "Preview the request without sending it", Bool: true, Default: "false"},
|
||||
},
|
||||
Run: runRead,
|
||||
},
|
||||
{
|
||||
Name: "delete",
|
||||
Description: "Delete selected messages",
|
||||
Flags: []common.Flag{
|
||||
{Name: "login", Short: "l", Usage: "Target user login (defaults to current authenticated user)"},
|
||||
{Name: "type", Short: "t", Usage: "Message type: notification or atme", Required: true},
|
||||
{Name: "ids", Usage: "Comma-separated message IDs"},
|
||||
{Name: "all", Usage: "Delete all unread messages of the selected type", Bool: true, Default: "false"},
|
||||
{Name: "dry-run", Usage: "Preview the request without sending it", Bool: true, Default: "false"},
|
||||
},
|
||||
Run: runDelete,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func runList(ctx *common.RuntimeContext) error {
|
||||
login, err := resolveTargetLogin(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
query, messageType, status, page, limit, err := buildListQuery(ctx.Arg("type"), ctx.Arg("status"), ctx.Arg("page"), ctx.Arg("limit"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
response, err := fetchMessages(ctx, login, query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return ctx.OutputData(map[string]interface{}{
|
||||
"action": "list_messages",
|
||||
"login": login,
|
||||
"type": messageType,
|
||||
"status": status,
|
||||
"page": page,
|
||||
"limit": limit,
|
||||
"total_count": response.TotalCount,
|
||||
"unread_notification": response.UnreadNotification,
|
||||
"unread_atme": response.UnreadAtme,
|
||||
"messages": normalizeMessages(response.Messages),
|
||||
})
|
||||
}
|
||||
|
||||
func runStats(ctx *common.RuntimeContext) error {
|
||||
login, err := resolveTargetLogin(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
messageType, err := parseListType(ctx.Arg("type"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
query := url.Values{}
|
||||
query.Set("page", "1")
|
||||
query.Set("limit", "1")
|
||||
if messageType != messageTypeAll {
|
||||
query.Set("type", messageType)
|
||||
}
|
||||
|
||||
response, err := fetchMessages(ctx, login, query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return ctx.OutputData(map[string]interface{}{
|
||||
"action": "message_stats",
|
||||
"login": login,
|
||||
"type": messageType,
|
||||
"total_count": response.TotalCount,
|
||||
"unread_notification": response.UnreadNotification,
|
||||
"unread_atme": response.UnreadAtme,
|
||||
"unread_total": response.UnreadNotification + response.UnreadAtme,
|
||||
})
|
||||
}
|
||||
|
||||
func runRead(ctx *common.RuntimeContext) error {
|
||||
return mutateMessages(ctx, "read_messages", httpMutation{
|
||||
Method: "POST",
|
||||
PathSuffix: "/read",
|
||||
})
|
||||
}
|
||||
|
||||
func runDelete(ctx *common.RuntimeContext) error {
|
||||
return mutateMessages(ctx, "delete_messages", httpMutation{
|
||||
Method: "DELETE",
|
||||
PathSuffix: "",
|
||||
})
|
||||
}
|
||||
|
||||
type httpMutation struct {
|
||||
Method string
|
||||
PathSuffix string
|
||||
}
|
||||
|
||||
func mutateMessages(ctx *common.RuntimeContext, action string, mutation httpMutation) error {
|
||||
login, err := resolveTargetLogin(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
messageType, err := parseMutationType(ctx.Arg("type"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ids, mode, err := parseMutationIDs(ctx.Arg("ids"), parseBoolArg(ctx.Arg("all")))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result := map[string]interface{}{
|
||||
"action": action,
|
||||
"login": login,
|
||||
"type": messageType,
|
||||
"mode": mode,
|
||||
"ids": ids,
|
||||
"dry_run": parseBoolArg(ctx.Arg("dry-run")),
|
||||
"item_count": len(ids),
|
||||
}
|
||||
if mode == "all" {
|
||||
result["item_count"] = "all"
|
||||
}
|
||||
|
||||
if parseBoolArg(ctx.Arg("dry-run")) {
|
||||
return ctx.OutputData(result)
|
||||
}
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"type": messageType,
|
||||
"ids": ids,
|
||||
}
|
||||
env, err := ctx.CallAPI(mutation.Method, fmt.Sprintf("/api/users/%s/messages%s", login, mutation.PathSuffix), payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
result["updated"] = env.Data
|
||||
return ctx.OutputData(result)
|
||||
}
|
||||
|
||||
func resolveTargetLogin(ctx *common.RuntimeContext) (string, error) {
|
||||
if login := strings.TrimSpace(ctx.Arg("login")); login != "" {
|
||||
return login, nil
|
||||
}
|
||||
|
||||
env, err := ctx.CallAPI("GET", "/users/me", nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("fetch current user: %w", err)
|
||||
}
|
||||
var current currentUserResponse
|
||||
if err := decodeEnvelopeData(env.Data, ¤t); err != nil {
|
||||
return "", fmt.Errorf("parse current user: %w", err)
|
||||
}
|
||||
if strings.TrimSpace(current.Login) == "" {
|
||||
return "", fmt.Errorf("current user response did not include a login")
|
||||
}
|
||||
return current.Login, nil
|
||||
}
|
||||
|
||||
func buildListQuery(typeValue, statusValue, pageValue, limitValue string) (url.Values, string, string, int, int, error) {
|
||||
messageType, err := parseListType(typeValue)
|
||||
if err != nil {
|
||||
return nil, "", "", 0, 0, err
|
||||
}
|
||||
status, statusCode, err := parseListStatus(statusValue)
|
||||
if err != nil {
|
||||
return nil, "", "", 0, 0, err
|
||||
}
|
||||
page, err := parsePositiveInt(pageValue, "page")
|
||||
if err != nil {
|
||||
return nil, "", "", 0, 0, err
|
||||
}
|
||||
limit, err := parsePositiveInt(limitValue, "limit")
|
||||
if err != nil {
|
||||
return nil, "", "", 0, 0, err
|
||||
}
|
||||
|
||||
query := url.Values{}
|
||||
query.Set("page", strconv.Itoa(page))
|
||||
query.Set("limit", strconv.Itoa(limit))
|
||||
if messageType != messageTypeAll {
|
||||
query.Set("type", messageType)
|
||||
}
|
||||
if statusCode != 0 {
|
||||
query.Set("status", strconv.Itoa(statusCode))
|
||||
}
|
||||
return query, messageType, status, page, limit, nil
|
||||
}
|
||||
|
||||
func fetchMessages(ctx *common.RuntimeContext, login string, query url.Values) (*listResponse, error) {
|
||||
env, err := ctx.CallAPIWithQuery("GET", fmt.Sprintf("/api/users/%s/messages", login), query)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fetch messages: %w", err)
|
||||
}
|
||||
var response listResponse
|
||||
if err := decodeEnvelopeData(env.Data, &response); err != nil {
|
||||
return nil, fmt.Errorf("parse message list: %w", err)
|
||||
}
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
func normalizeMessages(rows []messageRow) []messageOutput {
|
||||
items := make([]messageOutput, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
items = append(items, messageOutput{
|
||||
ID: row.ID,
|
||||
Type: row.Type,
|
||||
Status: row.Status,
|
||||
Read: row.Status == 2,
|
||||
Source: row.Source,
|
||||
Content: row.Content,
|
||||
ContentText: normalizeMessageText(row.Content),
|
||||
NotificationURL: row.NotificationURL,
|
||||
CreatedAt: row.CreatedAt,
|
||||
TimeAgo: row.TimeAgo,
|
||||
Sender: row.Sender,
|
||||
})
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func normalizeMessageText(value string) string {
|
||||
value = htmlTagPattern.ReplaceAllString(value, " ")
|
||||
value = html.UnescapeString(value)
|
||||
return strings.Join(strings.Fields(value), " ")
|
||||
}
|
||||
|
||||
func parseListType(value string) (string, error) {
|
||||
value = strings.ToLower(strings.TrimSpace(value))
|
||||
if value == "" {
|
||||
value = messageTypeAll
|
||||
}
|
||||
switch value {
|
||||
case messageTypeAll, messageTypeNotification, messageTypeAtme:
|
||||
return value, nil
|
||||
default:
|
||||
return "", fmt.Errorf("invalid --type value %q", value)
|
||||
}
|
||||
}
|
||||
|
||||
func parseMutationType(value string) (string, error) {
|
||||
value = strings.ToLower(strings.TrimSpace(value))
|
||||
switch value {
|
||||
case messageTypeNotification, messageTypeAtme:
|
||||
return value, nil
|
||||
default:
|
||||
return "", fmt.Errorf("invalid --type value %q", value)
|
||||
}
|
||||
}
|
||||
|
||||
func parseListStatus(value string) (string, int, error) {
|
||||
value = strings.ToLower(strings.TrimSpace(value))
|
||||
if value == "" {
|
||||
value = messageStatusAll
|
||||
}
|
||||
switch value {
|
||||
case messageStatusAll:
|
||||
return value, 0, nil
|
||||
case messageStatusUnread:
|
||||
return value, 1, nil
|
||||
case messageStatusRead:
|
||||
return value, 2, nil
|
||||
default:
|
||||
return "", 0, fmt.Errorf("invalid --status value %q", value)
|
||||
}
|
||||
}
|
||||
|
||||
func parsePositiveInt(value, name string) (int, error) {
|
||||
parsed, err := strconv.Atoi(strings.TrimSpace(value))
|
||||
if err != nil || parsed <= 0 {
|
||||
return 0, fmt.Errorf("invalid --%s value %q", name, value)
|
||||
}
|
||||
return parsed, nil
|
||||
}
|
||||
|
||||
func parseMutationIDs(value string, all bool) ([]int64, string, error) {
|
||||
if all {
|
||||
if strings.TrimSpace(value) != "" {
|
||||
return nil, "", fmt.Errorf("--ids cannot be used together with --all")
|
||||
}
|
||||
return []int64{-1}, "all", nil
|
||||
}
|
||||
|
||||
parts := strings.Split(strings.TrimSpace(value), ",")
|
||||
ids := make([]int64, 0, len(parts))
|
||||
seen := map[int64]bool{}
|
||||
for _, part := range parts {
|
||||
token := strings.TrimSpace(part)
|
||||
if token == "" {
|
||||
continue
|
||||
}
|
||||
id, err := strconv.ParseInt(token, 10, 64)
|
||||
if err != nil || id <= 0 {
|
||||
return nil, "", fmt.Errorf("invalid --ids value %q", token)
|
||||
}
|
||||
if seen[id] {
|
||||
continue
|
||||
}
|
||||
seen[id] = true
|
||||
ids = append(ids, id)
|
||||
}
|
||||
if len(ids) == 0 {
|
||||
return nil, "", fmt.Errorf("one of --ids or --all is required")
|
||||
}
|
||||
sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
|
||||
return ids, "selected", nil
|
||||
}
|
||||
|
||||
func parseBoolArg(value string) bool {
|
||||
return strings.EqualFold(strings.TrimSpace(value), "true")
|
||||
}
|
||||
|
||||
func decodeEnvelopeData(data interface{}, target interface{}) error {
|
||||
raw, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(raw, target)
|
||||
}
|
||||
|
|
@ -0,0 +1,291 @@
|
|||
package message
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/gitlink-org/gitlink-cli/internal/client"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
|
||||
)
|
||||
|
||||
func runMessageShortcut(t *testing.T, server *httptest.Server, name string, args map[string]string) error {
|
||||
t.Helper()
|
||||
shortcut := findMessageShortcut(t, name)
|
||||
ctx := &common.RuntimeContext{
|
||||
Client: &client.Client{HTTP: server.Client(), BaseURL: server.URL},
|
||||
Format: "json",
|
||||
Args: args,
|
||||
}
|
||||
if ctx.Args == nil {
|
||||
ctx.Args = map[string]string{}
|
||||
}
|
||||
return shortcut.Run(ctx)
|
||||
}
|
||||
|
||||
func findMessageShortcut(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 writeMessageJSON(t *testing.T, w http.ResponseWriter, value interface{}) {
|
||||
t.Helper()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(value); err != nil {
|
||||
t.Fatalf("encode json: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func decodeMessageJSON(t *testing.T, r *http.Request) map[string]interface{} {
|
||||
t.Helper()
|
||||
defer r.Body.Close()
|
||||
var value map[string]interface{}
|
||||
if err := json.NewDecoder(r.Body).Decode(&value); err != nil {
|
||||
t.Fatalf("decode request body: %v", err)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func TestMessageListUsesCurrentUser(t *testing.T) {
|
||||
var sawList bool
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == http.MethodGet && r.URL.Path == "/users/me.json":
|
||||
writeMessageJSON(t, w, map[string]interface{}{"login": "alice"})
|
||||
case r.Method == http.MethodGet && r.URL.Path == "/api/users/alice/messages.json":
|
||||
sawList = true
|
||||
if got := r.URL.Query().Get("type"); got != messageTypeNotification {
|
||||
t.Fatalf("type query = %q, want %q", got, messageTypeNotification)
|
||||
}
|
||||
if got := r.URL.Query().Get("status"); got != "1" {
|
||||
t.Fatalf("status query = %q, want 1", got)
|
||||
}
|
||||
if got := r.URL.Query().Get("page"); got != "2" {
|
||||
t.Fatalf("page query = %q, want 2", got)
|
||||
}
|
||||
if got := r.URL.Query().Get("limit"); got != "5" {
|
||||
t.Fatalf("limit query = %q, want 5", got)
|
||||
}
|
||||
writeMessageJSON(t, w, messageListFixture())
|
||||
default:
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.String())
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runMessageShortcut(t, server, "list", map[string]string{
|
||||
"type": messageTypeNotification,
|
||||
"status": messageStatusUnread,
|
||||
"page": "2",
|
||||
"limit": "5",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("list failed: %v", err)
|
||||
}
|
||||
if !sawList {
|
||||
t.Fatal("expected message list request to be sent")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageStatsUsesSelectedLogin(t *testing.T) {
|
||||
var sawStats bool
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet || r.URL.Path != "/api/users/alice/messages.json" {
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.String())
|
||||
}
|
||||
sawStats = true
|
||||
if got := r.URL.Query().Get("page"); got != "1" {
|
||||
t.Fatalf("page query = %q, want 1", got)
|
||||
}
|
||||
if got := r.URL.Query().Get("limit"); got != "1" {
|
||||
t.Fatalf("limit query = %q, want 1", got)
|
||||
}
|
||||
if got := r.URL.Query().Get("type"); got != messageTypeAtme {
|
||||
t.Fatalf("type query = %q, want %q", got, messageTypeAtme)
|
||||
}
|
||||
writeMessageJSON(t, w, messageListFixture())
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runMessageShortcut(t, server, "stats", map[string]string{
|
||||
"login": "alice",
|
||||
"type": messageTypeAtme,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("stats failed: %v", err)
|
||||
}
|
||||
if !sawStats {
|
||||
t.Fatal("expected message stats request to be sent")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageReadDryRunDoesNotWrite(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatalf("dry-run should not write, got %s %s", r.Method, r.URL.String())
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runMessageShortcut(t, server, "read", map[string]string{
|
||||
"login": "alice",
|
||||
"type": messageTypeNotification,
|
||||
"ids": "101,202",
|
||||
"dry-run": "true",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("read dry-run failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageReadMarksAllUnread(t *testing.T) {
|
||||
var payload map[string]interface{}
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost || r.URL.Path != "/api/users/alice/messages/read.json" {
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.String())
|
||||
}
|
||||
payload = decodeMessageJSON(t, r)
|
||||
writeMessageJSON(t, w, map[string]interface{}{"status": 0, "message": "updated"})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runMessageShortcut(t, server, "read", map[string]string{
|
||||
"login": "alice",
|
||||
"type": messageTypeNotification,
|
||||
"all": "true",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("read failed: %v", err)
|
||||
}
|
||||
|
||||
if payload["type"] != messageTypeNotification {
|
||||
t.Fatalf("type = %#v, want %q", payload["type"], messageTypeNotification)
|
||||
}
|
||||
ids := payload["ids"].([]interface{})
|
||||
if len(ids) != 1 || ids[0].(float64) != -1 {
|
||||
t.Fatalf("ids = %#v, want [-1]", ids)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageDeletePostsSelectedIDs(t *testing.T) {
|
||||
var payload map[string]interface{}
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodDelete || r.URL.Path != "/api/users/alice/messages.json" {
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.String())
|
||||
}
|
||||
payload = decodeMessageJSON(t, r)
|
||||
writeMessageJSON(t, w, map[string]interface{}{"status": 0, "message": "deleted"})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runMessageShortcut(t, server, "delete", map[string]string{
|
||||
"login": "alice",
|
||||
"type": messageTypeAtme,
|
||||
"ids": "202,101,202",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("delete failed: %v", err)
|
||||
}
|
||||
|
||||
if payload["type"] != messageTypeAtme {
|
||||
t.Fatalf("type = %#v, want %q", payload["type"], messageTypeAtme)
|
||||
}
|
||||
ids := payload["ids"].([]interface{})
|
||||
if len(ids) != 2 || ids[0].(float64) != 101 || ids[1].(float64) != 202 {
|
||||
t.Fatalf("ids = %#v, want [101 202]", ids)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseMutationIDsRejectsInvalidInput(t *testing.T) {
|
||||
if _, _, err := parseMutationIDs("", false); err == nil {
|
||||
t.Fatal("expected error when neither --ids nor --all is provided")
|
||||
}
|
||||
if _, _, err := parseMutationIDs("1,abc", false); err == nil {
|
||||
t.Fatal("expected error for invalid message id")
|
||||
}
|
||||
if _, _, err := parseMutationIDs("1", true); err == nil {
|
||||
t.Fatal("expected error when --ids and --all are used together")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeMessageText(t *testing.T) {
|
||||
got := normalizeMessageText("someone <b>@you</b> & <i>team</i>")
|
||||
want := "someone @you & team"
|
||||
if got != want {
|
||||
t.Fatalf("normalizeMessageText() = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeMessagesIncludesPlainTextContent(t *testing.T) {
|
||||
got := normalizeMessages([]messageRow{
|
||||
{
|
||||
ID: 101,
|
||||
Type: messageTypeNotification,
|
||||
Status: 1,
|
||||
Content: "merged <b>successfully</b>",
|
||||
NotificationURL: "https://example.com/pulls/1",
|
||||
Source: "PullRequestMerged",
|
||||
CreatedAt: "2026-06-10 12:00:00",
|
||||
TimeAgo: "1 hour ago",
|
||||
Sender: map[string]interface{}{
|
||||
"login": "alice",
|
||||
},
|
||||
},
|
||||
})
|
||||
want := []messageOutput{
|
||||
{
|
||||
ID: 101,
|
||||
Type: messageTypeNotification,
|
||||
Status: 1,
|
||||
Read: false,
|
||||
Source: "PullRequestMerged",
|
||||
Content: "merged <b>successfully</b>",
|
||||
ContentText: "merged successfully",
|
||||
NotificationURL: "https://example.com/pulls/1",
|
||||
CreatedAt: "2026-06-10 12:00:00",
|
||||
TimeAgo: "1 hour ago",
|
||||
Sender: map[string]interface{}{
|
||||
"login": "alice",
|
||||
},
|
||||
},
|
||||
}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("normalizeMessages() = %#v, want %#v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func messageListFixture() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"total_count": 2,
|
||||
"type": messageTypeNotification,
|
||||
"unread_notification": 1,
|
||||
"unread_atme": 3,
|
||||
"messages": []map[string]interface{}{
|
||||
{
|
||||
"id": 101,
|
||||
"status": 1,
|
||||
"content": "your pull request was <b>merged</b>",
|
||||
"notification_url": "https://example.com/pulls/1",
|
||||
"source": "PullRequestMerged",
|
||||
"created_at": "2026-06-10 12:00:00",
|
||||
"time_ago": "1 hour ago",
|
||||
"type": messageTypeNotification,
|
||||
},
|
||||
{
|
||||
"id": 202,
|
||||
"status": 2,
|
||||
"content": "someone <b>@you</b>",
|
||||
"type": messageTypeAtme,
|
||||
"sender": map[string]interface{}{
|
||||
"login": "bob",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,6 @@ import (
|
|||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/gitlink-org/gitlink-cli/internal/i18n"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/attachment"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/branch"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/ci"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
|
||||
|
|
@ -14,6 +13,7 @@ import (
|
|||
"github.com/gitlink-org/gitlink-cli/shortcuts/label"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/license"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/member"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/message"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/milestone"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/org"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/pipeline"
|
||||
|
|
@ -33,47 +33,47 @@ func RegisterAll(root *cobra.Command, translators ...*i18n.Translator) {
|
|||
tr = translators[0]
|
||||
}
|
||||
groups := map[string][]*common.Shortcut{
|
||||
"attachment": attachment.Shortcuts(),
|
||||
"repo": repo.Shortcuts(tr),
|
||||
"issue": issue.Shortcuts(tr),
|
||||
"label": label.Shortcuts(),
|
||||
"license": license.Shortcuts(),
|
||||
"member": member.Shortcuts(),
|
||||
"milestone": milestone.Shortcuts(),
|
||||
"pipeline": pipeline.Shortcuts(),
|
||||
"pr": pr.Shortcuts(tr),
|
||||
"release": release.Shortcuts(tr),
|
||||
"branch": branch.Shortcuts(tr),
|
||||
"org": org.Shortcuts(tr),
|
||||
"user": user.Shortcuts(tr),
|
||||
"search": search.Shortcuts(tr),
|
||||
"ci": ci.Shortcuts(tr),
|
||||
"compare": compare.Shortcuts(),
|
||||
"webhook": webhook.Shortcuts(tr),
|
||||
"health": health.Shortcuts(tr),
|
||||
"workflow": workflow.Shortcuts(),
|
||||
"repo": repo.Shortcuts(tr),
|
||||
"issue": issue.Shortcuts(tr),
|
||||
"label": label.Shortcuts(),
|
||||
"license": license.Shortcuts(),
|
||||
"member": member.Shortcuts(),
|
||||
"message": message.Shortcuts(),
|
||||
"milestone": milestone.Shortcuts(),
|
||||
"pipeline": pipeline.Shortcuts(),
|
||||
"pr": pr.Shortcuts(tr),
|
||||
"release": release.Shortcuts(tr),
|
||||
"branch": branch.Shortcuts(tr),
|
||||
"org": org.Shortcuts(tr),
|
||||
"user": user.Shortcuts(tr),
|
||||
"search": search.Shortcuts(tr),
|
||||
"ci": ci.Shortcuts(tr),
|
||||
"compare": compare.Shortcuts(),
|
||||
"webhook": webhook.Shortcuts(tr),
|
||||
"health": health.Shortcuts(tr),
|
||||
"workflow": workflow.Shortcuts(),
|
||||
}
|
||||
|
||||
descriptions := map[string]string{
|
||||
"attachment": "Attachment operations",
|
||||
"repo": tr.T("cmd.repo.short"),
|
||||
"issue": tr.T("cmd.issue.short"),
|
||||
"label": "Issue label operations",
|
||||
"license": "License operations",
|
||||
"member": "Repository member operations",
|
||||
"milestone": "Milestone operations",
|
||||
"pipeline": "Pipeline operations",
|
||||
"pr": tr.T("cmd.pr.short"),
|
||||
"release": tr.T("cmd.release.short"),
|
||||
"branch": tr.T("cmd.branch.short"),
|
||||
"org": tr.T("cmd.org.short"),
|
||||
"user": tr.T("cmd.user.short"),
|
||||
"search": tr.T("cmd.search.short"),
|
||||
"ci": tr.T("cmd.ci.short"),
|
||||
"compare": "Compare branches, tags, or commits",
|
||||
"webhook": tr.T("cmd.webhook.short"),
|
||||
"health": "Project health data collection",
|
||||
"workflow": "AI agent workflow analysis",
|
||||
"repo": tr.T("cmd.repo.short"),
|
||||
"issue": tr.T("cmd.issue.short"),
|
||||
"label": "Issue label operations",
|
||||
"license": "License operations",
|
||||
"member": "Repository member operations",
|
||||
"message": "Message center operations",
|
||||
"milestone": "Milestone operations",
|
||||
"pipeline": "Pipeline operations",
|
||||
"pr": tr.T("cmd.pr.short"),
|
||||
"release": tr.T("cmd.release.short"),
|
||||
"branch": tr.T("cmd.branch.short"),
|
||||
"org": tr.T("cmd.org.short"),
|
||||
"user": tr.T("cmd.user.short"),
|
||||
"search": tr.T("cmd.search.short"),
|
||||
"ci": tr.T("cmd.ci.short"),
|
||||
"compare": "Compare branches, tags, or commits",
|
||||
"webhook": tr.T("cmd.webhook.short"),
|
||||
"health": "Project health data collection",
|
||||
"workflow": "AI agent workflow analysis",
|
||||
}
|
||||
|
||||
for name, shortcuts := range groups {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ func TestRegisterAll(t *testing.T) {
|
|||
"attachment", "repo", "issue", "label", "license", "pr", "release", "branch",
|
||||
"org", "user", "search", "ci", "workflow",
|
||||
"compare", "member", "milestone", "pipeline", "webhook",
|
||||
"health",
|
||||
"health", "message",
|
||||
}
|
||||
|
||||
groupSet := map[string]bool{}
|
||||
|
|
|
|||
Loading…
Reference in New Issue