feat(user): add SSH key shortcuts
This commit is contained in:
parent
71ca2bb683
commit
37e736c4d8
20
README.md
20
README.md
|
|
@ -551,6 +551,24 @@ gitlink-cli profile +activity
|
|||
gitlink-cli profile +contribution --user zhangsan --year 2025
|
||||
```
|
||||
|
||||
### User Account
|
||||
|
||||
```bash
|
||||
# Show current authenticated user
|
||||
gitlink-cli user +me
|
||||
|
||||
# List SSH public keys
|
||||
gitlink-cli user +keys
|
||||
|
||||
# Add an SSH public key from inline content or a file
|
||||
gitlink-cli user +add-key --title laptop --key "ssh-ed25519 AAAA..."
|
||||
gitlink-cli user +add-key --title laptop --from ~/.ssh/id_ed25519.pub
|
||||
gitlink-cli user +add-key --from ~/.ssh/id_rsa.pub
|
||||
|
||||
# Delete an SSH public key
|
||||
gitlink-cli user +delete-key --id 123
|
||||
```
|
||||
|
||||
### Workflow Agent Commands
|
||||
|
||||
`workflow` provides rule-based repository analysis for maintainers and AI Agents. It currently supports:
|
||||
|
|
@ -747,7 +765,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, etc.) |
|
||||
| `gitlink-user` | User management (profile info, SSH keys, 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) |
|
||||
|
|
|
|||
|
|
@ -550,6 +550,25 @@ gitlink-cli dataset +delete-attachment --owner me --repo proj --uuid <uuid> --ye
|
|||
```
|
||||
|
||||
> 注意:`dataset +list`(平台数据集查询)已在生产 gitlink.org.cn 验证可用。按仓库的 `+view`/`+create`/`+update` 遵循已发布的 OpenAPI 契约,但生产环境尚未部署(当前返回 404),待平台上线后即可生效。
|
||||
|
||||
### 用户账号
|
||||
|
||||
```bash
|
||||
# 查看当前登录用户
|
||||
gitlink-cli user +me
|
||||
|
||||
# 列出 SSH 公钥
|
||||
gitlink-cli user +keys
|
||||
|
||||
# 通过内容或文件添加 SSH 公钥
|
||||
gitlink-cli user +add-key --title laptop --key "ssh-ed25519 AAAA..."
|
||||
gitlink-cli user +add-key --title laptop --from ~/.ssh/id_ed25519.pub
|
||||
gitlink-cli user +add-key --from ~/.ssh/id_rsa.pub
|
||||
|
||||
# 删除 SSH 公钥
|
||||
gitlink-cli user +delete-key --id 123
|
||||
```
|
||||
|
||||
### Raw API
|
||||
|
||||
Shortcuts 未覆盖的接口可通过 Raw API 直接调用:
|
||||
|
|
@ -620,7 +639,7 @@ git push gitlink
|
|||
| `gitlink-ci` | CI/CD 操作(构建、日志等) |
|
||||
| `gitlink-pipeline` | 流水线工作流操作(运行、日志、启停、删除等) |
|
||||
| `gitlink-search` | 搜索功能(仓库、用户等) |
|
||||
| `gitlink-user` | 用户管理(个人信息等) |
|
||||
| `gitlink-user` | 用户管理(个人信息、SSH 公钥等) |
|
||||
| `gitlink-pm` | 项目管理(Sprint、看板、周报等) |
|
||||
| `gitlink-workflow` | AI 自动化工作流(Issue 分类、PR Review、Release Notes 等) |
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
# User SSH Key Shortcuts
|
||||
|
||||
## Summary
|
||||
|
||||
Adds account-level SSH public key management shortcuts under the existing `user` command group.
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
gitlink-cli user +keys
|
||||
gitlink-cli user +add-key --title laptop --key "ssh-ed25519 AAAA..."
|
||||
gitlink-cli user +add-key --title laptop --from ~/.ssh/id_ed25519.pub
|
||||
gitlink-cli user +add-key --from ~/.ssh/id_rsa.pub
|
||||
gitlink-cli user +delete-key --id 123
|
||||
```
|
||||
|
||||
## Behavior
|
||||
|
||||
- `user +keys` calls `GET /public_keys` with `--page` and `--limit`.
|
||||
- `user +add-key` calls `POST /public_keys` with `title` and `key`.
|
||||
- `user +delete-key` calls `DELETE /public_keys/{id}`.
|
||||
- `user +add-key` accepts either inline key content or a public key file path.
|
||||
- `user +add-key --from` can infer the default title from the public key filename.
|
||||
- Key content and key IDs are validated before a write/delete request is sent.
|
||||
|
||||
## Tests
|
||||
|
||||
- Unit tests cover list pagination, inline key creation, file-based key creation, invalid key sources, delete path construction, and invalid key IDs.
|
||||
|
|
@ -93,7 +93,10 @@
|
|||
"cmd.search.repos.short": "Search repositories",
|
||||
"cmd.search.short": "Search operations",
|
||||
"cmd.search.users.short": "Search users",
|
||||
"cmd.user.add_key.short": "Add an SSH public key to the current user",
|
||||
"cmd.user.delete_key.short": "Delete one of the current user's SSH public keys",
|
||||
"cmd.user.info.short": "Show user profile",
|
||||
"cmd.user.keys.short": "List current user's SSH public keys",
|
||||
"cmd.user.me.short": "Show current authenticated user",
|
||||
"cmd.user.short": "User operations",
|
||||
"cmd.version.short": "Print version information",
|
||||
|
|
@ -218,6 +221,10 @@
|
|||
"flag.sort_by": "Sort field",
|
||||
"flag.sort_direction": "Sort direction: asc, desc",
|
||||
"flag.user": "User login (default: current user)",
|
||||
"flag.user.key.content": "SSH public key content",
|
||||
"flag.user.key.from": "Read SSH public key content from a file",
|
||||
"flag.user.key.id": "SSH key ID",
|
||||
"flag.user.key.title": "SSH key title. Defaults to the filename when --from is used.",
|
||||
"flag.user.login": "User login name",
|
||||
"flag.webhook.active": "Whether the webhook is active: true or false",
|
||||
"flag.webhook.branch_filter": "Branch glob filter for push/create/delete events",
|
||||
|
|
|
|||
|
|
@ -93,7 +93,10 @@
|
|||
"cmd.search.repos.short": "搜索仓库",
|
||||
"cmd.search.short": "搜索操作",
|
||||
"cmd.search.users.short": "搜索用户",
|
||||
"cmd.user.add_key.short": "为当前用户添加 SSH 公钥",
|
||||
"cmd.user.delete_key.short": "删除当前用户的 SSH 公钥",
|
||||
"cmd.user.info.short": "显示用户资料",
|
||||
"cmd.user.keys.short": "列出当前用户的 SSH 公钥",
|
||||
"cmd.user.me.short": "显示当前认证用户",
|
||||
"cmd.user.short": "用户操作",
|
||||
"cmd.version.short": "打印版本信息",
|
||||
|
|
@ -218,6 +221,10 @@
|
|||
"flag.sort_by": "排序字段",
|
||||
"flag.sort_direction": "排序方向:asc、desc",
|
||||
"flag.user": "用户登录名(默认:当前用户)",
|
||||
"flag.user.key.content": "SSH 公钥内容",
|
||||
"flag.user.key.from": "从文件读取 SSH 公钥内容",
|
||||
"flag.user.key.id": "SSH 公钥 ID",
|
||||
"flag.user.key.title": "SSH 公钥标题。使用 --from 时默认取文件名。",
|
||||
"flag.user.login": "用户登录名",
|
||||
"flag.webhook.active": "Webhook 是否启用:true 或 false",
|
||||
"flag.webhook.branch_filter": "用于 push/create/delete 事件的分支 glob 筛选",
|
||||
|
|
|
|||
|
|
@ -2,6 +2,11 @@ package user
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gitlink-org/gitlink-cli/internal/i18n"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
|
||||
|
|
@ -39,6 +44,77 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
|
|||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "keys",
|
||||
Description: tr.T("cmd.user.keys.short"),
|
||||
Flags: []common.Flag{
|
||||
{Name: "page", Short: "p", Usage: tr.T("flag.page"), Default: "1"},
|
||||
{Name: "limit", Short: "l", Usage: tr.T("flag.limit"), Default: "20"},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
q := url.Values{}
|
||||
q.Set("page", ctx.Arg("page"))
|
||||
q.Set("limit", ctx.Arg("limit"))
|
||||
env, err := ctx.CallAPIWithQuery("GET", publicKeysPath(), q)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "add-key",
|
||||
Description: tr.T("cmd.user.add_key.short"),
|
||||
Flags: []common.Flag{
|
||||
{Name: "title", Short: "t", Usage: tr.T("flag.user.key.title")},
|
||||
{Name: "key", Short: "k", Usage: tr.T("flag.user.key.content")},
|
||||
{Name: "from", Short: "f", Usage: tr.T("flag.user.key.from")},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
key, err := publicKeyContent(ctx.Arg("key"), ctx.Arg("from"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
title, err := publicKeyTitle(ctx.Arg("title"), ctx.Arg("from"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
env, err := ctx.CallAPI("POST", publicKeysPath(), map[string]interface{}{
|
||||
"title": title,
|
||||
"key": key,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "delete-key",
|
||||
Description: tr.T("cmd.user.delete_key.short"),
|
||||
Flags: []common.Flag{
|
||||
{Name: "id", Short: "i", Usage: tr.T("flag.user.key.id"), Required: true},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
id, err := ctx.RequireArg("id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
id = strings.TrimSpace(id)
|
||||
if id == "" {
|
||||
return fmt.Errorf("required flag --id is missing")
|
||||
}
|
||||
keyID, err := parsePublicKeyID(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
env, err := ctx.CallAPI("DELETE", fmt.Sprintf("%s/%d", publicKeysPath(), keyID), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -48,3 +124,60 @@ func shortcutTranslator(translators ...*i18n.Translator) *i18n.Translator {
|
|||
}
|
||||
return i18n.Default()
|
||||
}
|
||||
|
||||
func parsePublicKeyID(raw string) (int, error) {
|
||||
id, err := strconv.Atoi(strings.TrimSpace(raw))
|
||||
if err != nil || id <= 0 {
|
||||
return 0, fmt.Errorf("SSH key ID must be a positive integer")
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func publicKeysPath() string {
|
||||
return "/public_keys"
|
||||
}
|
||||
|
||||
func publicKeyTitle(title, from string) (string, error) {
|
||||
title = strings.TrimSpace(title)
|
||||
if title != "" {
|
||||
return title, nil
|
||||
}
|
||||
from = strings.TrimSpace(from)
|
||||
if from != "" {
|
||||
if base := filepath.Base(from); base != "." && base != string(filepath.Separator) {
|
||||
return base, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("required SSH key title is missing; use --title or provide --from")
|
||||
}
|
||||
|
||||
func publicKeyContent(inline, from string) (string, error) {
|
||||
inline = strings.TrimSpace(inline)
|
||||
from = strings.TrimSpace(from)
|
||||
if inline != "" && from != "" {
|
||||
return "", fmt.Errorf("use either --key or --from, not both")
|
||||
}
|
||||
if from != "" {
|
||||
content, err := os.ReadFile(from)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("read SSH public key file: %w", err)
|
||||
}
|
||||
inline = strings.TrimSpace(string(content))
|
||||
}
|
||||
if inline == "" {
|
||||
return "", fmt.Errorf("required SSH public key content is missing; use --key or --from")
|
||||
}
|
||||
if !hasPublicKeyPrefix(inline) {
|
||||
return "", fmt.Errorf("SSH public key content should start with ssh-rsa, ssh-dss, ssh-ed25519, ecdsa-sha2-, or sk-")
|
||||
}
|
||||
return inline, nil
|
||||
}
|
||||
|
||||
func hasPublicKeyPrefix(key string) bool {
|
||||
for _, prefix := range []string{"ssh-rsa", "ssh-dss", "ssh-ed25519", "ecdsa-sha2-", "sk-"} {
|
||||
if strings.HasPrefix(key, prefix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import (
|
|||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/gitlink-org/gitlink-cli/internal/client"
|
||||
|
|
@ -34,9 +36,12 @@ func findShortcut(t *testing.T, name string) *common.Shortcut {
|
|||
return nil
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, v interface{}) {
|
||||
func writeJSON(t *testing.T, w http.ResponseWriter, v interface{}) {
|
||||
t.Helper()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(v)
|
||||
if err := json.NewEncoder(w).Encode(v); err != nil {
|
||||
t.Fatalf("write response: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// --- me ---
|
||||
|
|
@ -46,7 +51,7 @@ func TestUserMe(t *testing.T) {
|
|||
if r.URL.Path != "/users/me.json" {
|
||||
t.Fatalf("unexpected path: %s", r.URL.Path)
|
||||
}
|
||||
writeJSON(w, map[string]interface{}{
|
||||
writeJSON(t, w, map[string]interface{}{
|
||||
"login": "currentuser",
|
||||
"name": "Current User",
|
||||
"id": float64(1),
|
||||
|
|
@ -67,7 +72,7 @@ func TestUserInfo(t *testing.T) {
|
|||
if r.URL.Path != "/users/alice.json" {
|
||||
t.Fatalf("unexpected path: %s", r.URL.Path)
|
||||
}
|
||||
writeJSON(w, map[string]interface{}{
|
||||
writeJSON(t, w, map[string]interface{}{
|
||||
"login": "alice",
|
||||
"name": "Alice",
|
||||
})
|
||||
|
|
@ -92,6 +97,192 @@ func TestUserInfoMissingLogin(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// --- SSH public keys ---
|
||||
|
||||
func TestUserKeysUsesPublicKeysEndpoint(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "GET" || r.URL.Path != "/public_keys.json" {
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
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 != "50" {
|
||||
t.Fatalf("limit query = %q, want 50", got)
|
||||
}
|
||||
writeJSON(t, w, map[string]interface{}{
|
||||
"total_count": 1,
|
||||
"public_keys": []interface{}{
|
||||
map[string]interface{}{"id": 1, "name": "laptop"},
|
||||
},
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "keys", map[string]string{
|
||||
"page": "2",
|
||||
"limit": "50",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("keys shortcut failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserAddKeySendsTitleAndInlineKey(t *testing.T) {
|
||||
const key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDemoKey user@example.com"
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" || r.URL.Path != "/public_keys.json" {
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
var body map[string]string
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
t.Fatalf("decode request: %v", err)
|
||||
}
|
||||
if body["title"] != "laptop" {
|
||||
t.Fatalf("title = %q, want laptop", body["title"])
|
||||
}
|
||||
if body["key"] != key {
|
||||
t.Fatalf("key = %q, want %q", body["key"], key)
|
||||
}
|
||||
writeJSON(t, w, map[string]interface{}{
|
||||
"id": 2,
|
||||
"name": "laptop",
|
||||
"fingerprint": "SHA256:demo",
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "add-key", map[string]string{
|
||||
"title": "laptop",
|
||||
"key": key,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("add-key shortcut failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserAddKeyReadsKeyFromFileAndDefaultsTitle(t *testing.T) {
|
||||
const key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDemo user@example.com"
|
||||
path := filepath.Join(t.TempDir(), "id_rsa.pub")
|
||||
if err := os.WriteFile(path, []byte(" "+key+"\n"), 0o600); err != nil {
|
||||
t.Fatalf("write key file: %v", err)
|
||||
}
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" || r.URL.Path != "/public_keys.json" {
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
var body map[string]string
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
t.Fatalf("decode request: %v", err)
|
||||
}
|
||||
if body["title"] != "id_rsa.pub" {
|
||||
t.Fatalf("title = %q, want id_rsa.pub", body["title"])
|
||||
}
|
||||
if body["key"] != key {
|
||||
t.Fatalf("key = %q, want %q", body["key"], key)
|
||||
}
|
||||
writeJSON(t, w, map[string]interface{}{"id": 3})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "add-key", map[string]string{"from": path})
|
||||
if err != nil {
|
||||
t.Fatalf("add-key from file failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserAddKeyRejectsAmbiguousKeySourcesBeforeRequest(t *testing.T) {
|
||||
called := false
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
called = true
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "add-key", map[string]string{
|
||||
"title": "laptop",
|
||||
"key": "ssh-ed25519 AAAA",
|
||||
"from": "id_ed25519.pub",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatalf("expected ambiguous key source error")
|
||||
}
|
||||
if called {
|
||||
t.Fatalf("server was called for invalid key sources")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserAddKeyRejectsInlineKeyWithoutTitleBeforeRequest(t *testing.T) {
|
||||
called := false
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
called = true
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "add-key", map[string]string{"key": "ssh-rsa AAAA"})
|
||||
if err == nil {
|
||||
t.Fatalf("expected missing title error")
|
||||
}
|
||||
if called {
|
||||
t.Fatalf("server was called without title")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserDeleteKeyUsesPublicKeyID(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "DELETE" || r.URL.Path != "/public_keys/12.json" {
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
writeJSON(t, w, map[string]interface{}{
|
||||
"status": 0,
|
||||
"message": "success",
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
if err := runShortcut(t, server, "delete-key", map[string]string{"id": "12"}); err != nil {
|
||||
t.Fatalf("delete-key shortcut failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserDeleteKeyRejectsNonNumericIDBeforeRequest(t *testing.T) {
|
||||
called := false
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
called = true
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "delete-key", map[string]string{"id": "12/34"})
|
||||
if err == nil {
|
||||
t.Fatalf("expected invalid key id error")
|
||||
}
|
||||
if called {
|
||||
t.Fatalf("server was called for invalid key id")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublicKeyContentAcceptsCommonOpenSSHPrefixes(t *testing.T) {
|
||||
for _, key := range []string{
|
||||
"ssh-rsa AAAA",
|
||||
"ssh-dss AAAA",
|
||||
"ssh-ed25519 AAAA",
|
||||
"ecdsa-sha2-nistp256 AAAA",
|
||||
"sk-ssh-ed25519@openssh.com AAAA",
|
||||
} {
|
||||
t.Run(key, func(t *testing.T) {
|
||||
if _, err := publicKeyContent(key, ""); err != nil {
|
||||
t.Fatalf("publicKeyContent(%q) returned error: %v", key, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublicKeyContentRejectsNonPublicKeyPrefix(t *testing.T) {
|
||||
if _, err := publicKeyContent("not-a-key", ""); err == nil {
|
||||
t.Fatalf("expected invalid public key prefix error")
|
||||
}
|
||||
}
|
||||
|
||||
// --- HTTP error paths ---
|
||||
|
||||
func TestUserMeHTTPError(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -139,7 +139,7 @@ skills/
|
|||
| Skill | 说明 | 常用命令 |
|
||||
|-------|------|----------|
|
||||
| **gitlink-search** | 搜索功能 | `search +repos`, `search +users` |
|
||||
| **gitlink-user** | 用户管理 | `user +me`, `user +info` |
|
||||
| **gitlink-user** | 用户管理 | `user +me`, `user +info`, `user +keys`, `user +add-key`, `user +delete-key` |
|
||||
| **gitlink-org** | 组织管理 | `org +list`, `org +info`, `org +members` |
|
||||
| **gitlink-ci** | CI/CD | `ci +builds`, `ci +logs` |
|
||||
| **gitlink-pipeline** | 流水线工作流 | `pipeline +runs`, `pipeline +run`, `pipeline +logs` |
|
||||
|
|
|
|||
|
|
@ -23,6 +23,20 @@ metadata:
|
|||
| `user +me` | 当前登录用户 | 是 |
|
||||
| `user +info` | 查看用户详情 | 否 |
|
||||
|
||||
## SSH Public Keys
|
||||
|
||||
Use these shortcuts to manage SSH public keys for the current authenticated user:
|
||||
|
||||
```bash
|
||||
gitlink-cli user +keys
|
||||
gitlink-cli user +add-key --title laptop --from ~/.ssh/id_ed25519.pub
|
||||
gitlink-cli user +add-key --from ~/.ssh/id_rsa.pub
|
||||
gitlink-cli user +add-key --title laptop --key "ssh-ed25519 AAAA..."
|
||||
gitlink-cli user +delete-key --id 123
|
||||
```
|
||||
|
||||
`user +add-key` accepts either `--key` or `--from`, not both. When `--from` is used without `--title`, the filename is used as the default title. `user +delete-key` requires a positive integer key ID.
|
||||
|
||||
## 使用示例
|
||||
|
||||
```bash
|
||||
|
|
|
|||
Loading…
Reference in New Issue