# gitlink-cli API 全量封装 + 跨平台兼容性 实现计划

> **面向 AI 代理的工作者：** 必需子技能：使用 superpowers:subagent-driven-development（推荐）或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框（`- [ ]`）语法来跟踪进度。

**目标：** 补全 GitLink OpenAPI 全部约 200 个端点的快捷方式封装，并提升跨平台兼容性和安装体验。

**架构：** 遵循现有声明式快捷方式框架（`common.Shortcut` + `common.Flag` + `common.RuntimeContext`），在已有域文件中追加新快捷方式，新建 4 个域目录（`util`, `dataset`, `template`, `attachment`）。跨平台改进在 `internal/output/` 和 `npm/` 中完成。

**技术栈：** Go 1.25+ / Cobra / go-keyring / golang.org/x/term / Node.js (npm installer)

---

## 文件结构

### 新建文件
| 文件 | 职责 |
|------|------|
| `shortcuts/util/util.go` | 辅助工具快捷方式（licenses, ignores, platform-msg-settings） |
| `shortcuts/dataset/dataset.go` | 数据集 CRUD 快捷方式 |
| `shortcuts/template/template.go` | 项目模板 CRUD 快捷方式 |
| `shortcuts/attachment/attachment.go` | 附件上传/删除快捷方式 |
| `shortcuts/util/util_test.go` | util 快捷方式测试 |
| `shortcuts/dataset/dataset_test.go` | dataset 快捷方式测试 |
| `shortcuts/template/template_test.go` | template 快捷方式测试 |
| `shortcuts/attachment/attachment_test.go` | attachment 快捷方式测试 |
| `shortcuts/repo/repo_test.go` | repo 新增快捷方式测试 |
| `shortcuts/user/user_test.go` | user 新增快捷方式测试 |
| `shortcuts/ci/ci_test.go` | CI 流水线测试 |
| `.github/workflows/ci.yml` | PR 触发的 CI 测试 |
| `formula/gitlink-cli.rb` | Homebrew formula |
| `bucket/gitlink-cli.json` | Scoop manifest |
| `choco/gitlink-cli.nuspec` | Chocolatey 包定义 |
| `choco/tools/chocolateyinstall.ps1` | Chocolatey 安装脚本 |

### 修改文件
| 文件 | 修改内容 |
|------|---------|
| `shortcuts/register.go` | 注册 4 个新域：util, dataset, template, attachment |
| `shortcuts/issue/issue.go` | 追加 7 个新快捷方式 |
| `shortcuts/pr/pr.go` | 追加 7 个新快捷方式 |
| `shortcuts/branch/branch.go` | 追加 3 个新快捷方式 |
| `shortcuts/release/release.go` | 追加 2 个新快捷方式 |
| `shortcuts/repo/repo.go` | 追加 22 个新快捷方式 |
| `shortcuts/user/user.go` | 追加 22 个新快捷方式 |
| `shortcuts/org/org.go` | 追加 2 个新快捷方式 |
| `shortcuts/file/file.go` | 追加 4 个新快捷方式 |
| `shortcuts/ci/ci.go` | 追加 10 个新快捷方式 |
| `shortcuts/wiki/wiki.go` | 追加 2 个新快捷方式 |
| `shortcuts/issue/issue_test.go` | 新增测试 |
| `shortcuts/pr/pr_test.go` | 新增测试 |
| `internal/output/formatter.go` | 颜色检测、终端宽度、CJK 宽度 |
| `cmd/root.go` | `--color`/`--no-color` 全局标志 |
| `npm/scripts/install.js` | checksum 验证、错误消息改进 |
| `npm/bin/cli.js` | 版本检查提示 |
| `Makefile` | 统一 ldflags |
| `.github/workflows/release.yml` | 自动更新包管理器配置 |

---

## 任务 1：Issue 评论 CRUD 补全

**文件：**
- 修改：`shortcuts/issue/issue.go`
- 修改：`shortcuts/issue/issue_test.go`

- [ ] **步骤 1：编写失败的测试**

在 `shortcuts/issue/issue_test.go` 末尾追加：

```go
func TestIssueDeleteCallsCorrectEndpoint(t *testing.T) {
	var method, path string
	server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
		method = r.Method
		path = r.URL.Path
		writeJSON(t, w, map[string]interface{}{"message": "success"})
	})
	defer server.Close()

	err := runIssueShortcut(t, server, "delete", map[string]string{"number": "42"})
	if err != nil {
		t.Fatalf("delete shortcut failed: %v", err)
	}
	assertEqual(t, method, "DELETE")
	assertEqual(t, path, "/v1/owner/repo/issues/42.json")
}

func TestIssueCommentsListsComments(t *testing.T) {
	var method, path string
	server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
		method = r.Method
		path = r.URL.Path
		writeJSON(t, w, map[string]interface{}{
			"journals": []interface{}{
				map[string]interface{}{"id": float64(1), "notes": "First comment"},
			},
		})
	})
	defer server.Close()

	err := runIssueShortcut(t, server, "comments", map[string]string{"number": "42"})
	if err != nil {
		t.Fatalf("comments shortcut failed: %v", err)
	}
	assertEqual(t, method, "GET")
	assertEqual(t, path, "/v1/owner/repo/issues/42/journals.json")
}

func TestIssueUpdateCommentSendsPATCH(t *testing.T) {
	var method, path string
	var payload map[string]interface{}
	server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
		method = r.Method
		path = r.URL.Path
		if r.Method == "PATCH" {
			payload = decodeJSON(t, r)
		}
		writeJSON(t, w, map[string]interface{}{"id": float64(99), "notes": "updated"})
	})
	defer server.Close()

	err := runIssueShortcut(t, server, "update-comment", map[string]string{
		"number": "42",
		"id":     "99",
		"body":   "Updated comment",
	})
	if err != nil {
		t.Fatalf("update-comment shortcut failed: %v", err)
	}
	assertEqual(t, method, "PATCH")
	assertEqual(t, path, "/v1/owner/repo/issues/42/journals/99.json")
	assertEqual(t, payload["notes"], "Updated comment")
}

func TestIssueDeleteCommentCallsCorrectEndpoint(t *testing.T) {
	var method, path string
	server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
		method = r.Method
		path = r.URL.Path
		writeJSON(t, w, map[string]interface{}{"message": "success"})
	})
	defer server.Close()

	err := runIssueShortcut(t, server, "delete-comment", map[string]string{
		"number": "42",
		"id":     "99",
	})
	if err != nil {
		t.Fatalf("delete-comment shortcut failed: %v", err)
	}
	assertEqual(t, method, "DELETE")
	assertEqual(t, path, "/v1/owner/repo/issues/42/journals/99.json")
}

func TestIssueBatchUpdateSendsCorrectPayload(t *testing.T) {
	var method, path string
	var payload map[string]interface{}
	server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
		method = r.Method
		path = r.URL.Path
		payload = decodeJSON(t, r)
		writeJSON(t, w, map[string]interface{}{"status": float64(200)})
	})
	defer server.Close()

	err := runIssueShortcut(t, server, "batch-update", map[string]string{
		"ids":    "1,2,3",
		"state":  "closed",
	})
	if err != nil {
		t.Fatalf("batch-update shortcut failed: %v", err)
	}
	assertEqual(t, method, "PATCH")
	assertEqual(t, path, "/v1/owner/repo/issues/batch_update.json")
	assertEqual(t, payload["status_id"], float64(5))
}

func TestIssueBatchDestroyCallsCorrectEndpoint(t *testing.T) {
	var method, path string
	var payload map[string]interface{}
	server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
		method = r.Method
		path = r.URL.Path
		payload = decodeJSON(t, r)
		writeJSON(t, w, map[string]interface{}{"status": float64(200)})
	})
	defer server.Close()

	err := runIssueShortcut(t, server, "batch-destroy", map[string]string{
		"ids": "1,2,3",
	})
	if err != nil {
		t.Fatalf("batch-destroy shortcut failed: %v", err)
	}
	assertEqual(t, method, "DELETE")
	assertEqual(t, path, "/v1/owner/repo/issues/batch_destroy.json")
}
```

- [ ] **步骤 2：运行测试验证失败**

运行：`cd /c/Users/刘焱/Desktop/gitlink-cli && go test ./shortcuts/issue/ -run "TestIssueDelete|TestIssueComments|TestIssueUpdateComment|TestIssueDeleteComment|TestIssueBatch" -v`
预期：FAIL，报错 "shortcut not found"

- [ ] **步骤 3：实现快捷方式**

在 `shortcuts/issue/issue.go` 的 `Shortcuts()` 函数中，在 `comment` 快捷方式后面追加以下快捷方式（在 `return []*common.Shortcut{` 的最后一个 `}` 之前）：

```go
{
	Name:        "comments",
	Description: "List comments and activity on an issue",
	Flags: []common.Flag{
		{Name: "number", Short: "n", Usage: "Issue number", Required: true},
		{Name: "page", Short: "p", Usage: "Page number", Default: "1"},
		{Name: "limit", Short: "l", Usage: "Items per page", Default: "20"},
	},
	Run: func(ctx *common.RuntimeContext) error {
		if err := ctx.ResolveOwnerRepo(); err != nil {
			return err
		}
		number, err := ctx.RequireArg("number")
		if err != nil {
			return err
		}
		q := url.Values{}
		q.Set("page", ctx.Arg("page"))
		q.Set("limit", ctx.Arg("limit"))
		env, err := ctx.CallAPIWithQuery("GET", fmt.Sprintf("%s/issues/%s/journals", v1RepoPath(ctx), number), q)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "update-comment",
	Description: "Update an issue comment",
	Flags: []common.Flag{
		{Name: "number", Short: "n", Usage: "Issue number", Required: true},
		{Name: "id", Usage: "Comment ID", Required: true},
		{Name: "body", Short: "b", Usage: "New comment body", Required: true},
	},
	Run: func(ctx *common.RuntimeContext) error {
		if err := ctx.ResolveOwnerRepo(); err != nil {
			return err
		}
		number, err := ctx.RequireArg("number")
		if err != nil {
			return err
		}
		id, err := ctx.RequireArg("id")
		if err != nil {
			return err
		}
		body, err := ctx.RequireArg("body")
		if err != nil {
			return err
		}
		payload := map[string]interface{}{
			"notes": body,
		}
		env, err := ctx.CallAPI("PATCH", fmt.Sprintf("%s/issues/%s/journals/%s", v1RepoPath(ctx), number, id), payload)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "delete-comment",
	Description: "Delete an issue comment",
	Flags: []common.Flag{
		{Name: "number", Short: "n", Usage: "Issue number", Required: true},
		{Name: "id", Usage: "Comment ID", Required: true},
	},
	Run: func(ctx *common.RuntimeContext) error {
		if err := ctx.ResolveOwnerRepo(); err != nil {
			return err
		}
		number, err := ctx.RequireArg("number")
		if err != nil {
			return err
		}
		id, err := ctx.RequireArg("id")
		if err != nil {
			return err
		}
		env, err := ctx.CallAPI("DELETE", fmt.Sprintf("%s/issues/%s/journals/%s", v1RepoPath(ctx), number, id), nil)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "reply-comment",
	Description: "List child (reply) comments for a specific comment",
	Flags: []common.Flag{
		{Name: "number", Short: "n", Usage: "Issue number", Required: true},
		{Name: "id", Usage: "Parent comment ID", Required: true},
	},
	Run: func(ctx *common.RuntimeContext) error {
		if err := ctx.ResolveOwnerRepo(); err != nil {
			return err
		}
		number, err := ctx.RequireArg("number")
		if err != nil {
			return err
		}
		id, err := ctx.RequireArg("id")
		if err != nil {
			return err
		}
		env, err := ctx.CallAPI("GET", fmt.Sprintf("%s/issues/%s/journals/%s/children_journals", v1RepoPath(ctx), number, id), nil)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "delete",
	Description: "Delete an issue",
	Flags: []common.Flag{
		{Name: "number", Short: "n", Usage: "Issue number", Required: true},
	},
	Run: func(ctx *common.RuntimeContext) error {
		if err := ctx.ResolveOwnerRepo(); err != nil {
			return err
		}
		number, err := ctx.RequireArg("number")
		if err != nil {
			return err
		}
		env, err := ctx.CallAPI("DELETE", fmt.Sprintf("%s/issues/%s", v1RepoPath(ctx), number), nil)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "batch-update",
	Description: "Batch update multiple issues",
	Flags: []common.Flag{
		{Name: "ids", Usage: "Comma-separated issue IDs", Required: true},
		{Name: "state", Short: "s", Usage: "New state: open or closed"},
		{Name: "assignee", Short: "a", Usage: "Assignee login"},
	},
	Run: func(ctx *common.RuntimeContext) error {
		if err := ctx.ResolveOwnerRepo(); err != nil {
			return err
		}
		ids, err := ctx.RequireArg("ids")
		if err != nil {
			return err
		}
		payload := map[string]interface{}{
			"ids": parseIDList(ids),
		}
		if s := ctx.Arg("state"); s != "" {
			statusID, err := normalizeIssueStatus(s)
			if err != nil {
				return err
			}
			payload["status_id"] = statusID
		}
		if a := ctx.Arg("assignee"); a != "" {
			payload["assigned_to_id"] = a
		}
		env, err := ctx.CallAPI("PATCH", v1RepoPath(ctx)+"/issues/batch_update", payload)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "batch-destroy",
	Description: "Batch delete multiple issues",
	Flags: []common.Flag{
		{Name: "ids", Usage: "Comma-separated issue IDs", Required: true},
	},
	Run: func(ctx *common.RuntimeContext) error {
		if err := ctx.ResolveOwnerRepo(); err != nil {
			return err
		}
		ids, err := ctx.RequireArg("ids")
		if err != nil {
			return err
		}
		payload := map[string]interface{}{
			"ids": parseIDList(ids),
		}
		env, err := ctx.CallAPI("DELETE", v1RepoPath(ctx)+"/issues/batch_destroy", payload)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
```

在 `issue.go` 文件末尾（`normalizeIssueStatus` 函数之后）追加辅助函数：

```go
// parseIDList splits a comma-separated string into a slice of int IDs.
func parseIDList(s string) []int {
	parts := strings.Split(s, ",")
	ids := make([]int, 0, len(parts))
	for _, p := range parts {
		p = strings.TrimSpace(p)
		if id, err := strconv.Atoi(p); err == nil {
			ids = append(ids, id)
		}
	}
	return ids
}
```

- [ ] **步骤 4：运行测试验证通过**

运行：`cd /c/Users/刘焱/Desktop/gitlink-cli && go test ./shortcuts/issue/ -v`
预期：ALL PASS

- [ ] **步骤 5：Commit**

```bash
cd /c/Users/刘焱/Desktop/gitlink-cli
git add shortcuts/issue/issue.go shortcuts/issue/issue_test.go
git commit -m "feat(issue): add comments CRUD, delete, batch-update, batch-destroy shortcuts"
```

---

## 任务 2：PR 评论 CRUD 及操作补全

**文件：**
- 修改：`shortcuts/pr/pr.go`
- 修改：`shortcuts/pr/pr_test.go`

- [ ] **步骤 1：编写失败的测试**

在 `shortcuts/pr/pr_test.go` 末尾追加：

```go
func TestPRCommentsList(t *testing.T) {
	var method, path string
	server := newPRTestServer(t, func(w http.ResponseWriter, r *http.Request) {
		method = r.Method
		path = r.URL.Path
		writeJSON(t, w, map[string]interface{}{
			"journals": []interface{}{
				map[string]interface{}{"id": float64(1), "notes": "review comment"},
			},
		})
	})
	defer server.Close()

	err := runPRShortcut(t, server, "comments", map[string]string{"id": "10"})
	if err != nil {
		t.Fatalf("comments shortcut failed: %v", err)
	}
	assertEqual(t, method, "GET")
	if !strings.Contains(path, "/pulls/10/journals") {
		t.Fatalf("expected path to contain /pulls/10/journals, got %s", path)
	}
}

func TestPRCreateComment(t *testing.T) {
	var method, path string
	var payload map[string]interface{}
	server := newPRTestServer(t, func(w http.ResponseWriter, r *http.Request) {
		method = r.Method
		path = r.URL.Path
		if r.Body != nil && r.ContentLength > 0 {
			payload = decodePRJSON(t, r)
		}
		writeJSON(t, w, map[string]interface{}{"id": float64(1)})
	})
	defer server.Close()

	err := runPRShortcut(t, server, "create-comment", map[string]string{
		"id":   "10",
		"body": "Nice code!",
	})
	if err != nil {
		t.Fatalf("create-comment shortcut failed: %v", err)
	}
	assertEqual(t, method, "POST")
	assertEqual(t, payload["notes"], "Nice code!")
}

func TestPRReopen(t *testing.T) {
	var method, path string
	server := newPRTestServer(t, func(w http.ResponseWriter, r *http.Request) {
		method = r.Method
		path = r.URL.Path
		writeJSON(t, w, map[string]interface{}{"status": float64(200)})
	})
	defer server.Close()

	err := runPRShortcut(t, server, "reopen", map[string]string{"id": "10"})
	if err != nil {
		t.Fatalf("reopen shortcut failed: %v", err)
	}
	assertEqual(t, method, "POST")
	if !strings.Contains(path, "/pulls/10/reopen") {
		t.Fatalf("expected path to contain /pulls/10/reopen, got %s", path)
	}
}

func TestPRUpdate(t *testing.T) {
	var method, path string
	var payload map[string]interface{}
	server := newPRTestServer(t, func(w http.ResponseWriter, r *http.Request) {
		method = r.Method
		path = r.URL.Path
		if r.Body != nil && r.ContentLength > 0 {
			payload = decodePRJSON(t, r)
		}
		writeJSON(t, w, map[string]interface{}{"id": float64(10)})
	})
	defer server.Close()

	err := runPRShortcut(t, server, "update", map[string]string{
		"id":    "10",
		"title": "New PR title",
	})
	if err != nil {
		t.Fatalf("update shortcut failed: %v", err)
	}
	assertEqual(t, method, "PUT")
	assertEqual(t, payload["title"], "New PR title")
}

func TestPRCommits(t *testing.T) {
	var method, path string
	server := newPRTestServer(t, func(w http.ResponseWriter, r *http.Request) {
		method = r.Method
		path = r.URL.Path
		writeJSON(t, w, map[string]interface{}{
			"commits": []interface{}{
				map[string]interface{}{"sha": "abc123", "message": "fix bug"},
			},
		})
	})
	defer server.Close()

	err := runPRShortcut(t, server, "commits", map[string]string{"id": "10"})
	if err != nil {
		t.Fatalf("commits shortcut failed: %v", err)
	}
	assertEqual(t, method, "GET")
	if !strings.Contains(path, "/pulls/10/commits") {
		t.Fatalf("expected path to contain /pulls/10/commits, got %s", path)
	}
}
```

注意：需要确保 `pr_test.go` 中存在辅助函数。如果不存在，在测试文件顶部追加：

```go
func runPRShortcut(t *testing.T, server *httptest.Server, name string, args map[string]string) error {
	t.Helper()
	shortcut := findPRShortcut(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 findPRShortcut(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 newPRTestServer(t *testing.T, handler http.HandlerFunc) *httptest.Server {
	t.Helper()
	return httptest.NewServer(handler)
}

func decodePRJSON(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
}
```

- [ ] **步骤 2：运行测试验证失败**

运行：`cd /c/Users/刘焱/Desktop/gitlink-cli && go test ./shortcuts/pr/ -run "TestPRComments|TestPRCreateComment|TestPRReopen|TestPRUpdate|TestPRCommits" -v`
预期：FAIL

- [ ] **步骤 3：实现快捷方式**

在 `shortcuts/pr/pr.go` 的 `Shortcuts()` 中，在最后一个 `comment` 快捷方式的 `},` 后面追加：

```go
{
	Name:        "comments",
	Description: "List pull request review comments",
	Flags: []common.Flag{
		{Name: "id", Short: "i", Usage: "PR number", Required: true},
	},
	Run: func(ctx *common.RuntimeContext) error {
		if err := ctx.ResolveOwnerRepo(); err != nil {
			return err
		}
		id, err := ctx.RequireArg("id")
		if err != nil {
			return err
		}
		env, err := ctx.CallAPI("GET", prV1Path(ctx, id)+"/journals", nil)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "create-comment",
	Description: "Create a review comment on a pull request",
	Flags: []common.Flag{
		{Name: "id", Short: "i", Usage: "PR number", Required: true},
		{Name: "body", Short: "b", Usage: "Comment body", Required: true},
		{Name: "line", Short: "l", Usage: "Line number (for inline comments)"},
		{Name: "path", Short: "p", Usage: "File path (for inline comments)"},
	},
	Run: func(ctx *common.RuntimeContext) error {
		if err := ctx.ResolveOwnerRepo(); err != nil {
			return err
		}
		id, err := ctx.RequireArg("id")
		if err != nil {
			return err
		}
		body, err := ctx.RequireArg("body")
		if err != nil {
			return err
		}
		payload := map[string]interface{}{
			"notes": body,
		}
		if line := ctx.Arg("line"); line != "" {
			payload["line"] = line
		}
		if p := ctx.Arg("path"); p != "" {
			payload["path"] = p
		}
		env, err := ctx.CallAPI("POST", prV1Path(ctx, id)+"/journals", payload)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "update-comment",
	Description: "Update a pull request review comment",
	Flags: []common.Flag{
		{Name: "id", Short: "i", Usage: "PR number", Required: true},
		{Name: "comment-id", Usage: "Comment ID", Required: true},
		{Name: "body", Short: "b", Usage: "New comment body", Required: true},
	},
	Run: func(ctx *common.RuntimeContext) error {
		if err := ctx.ResolveOwnerRepo(); err != nil {
			return err
		}
		id, err := ctx.RequireArg("id")
		if err != nil {
			return err
		}
		commentID, err := ctx.RequireArg("comment-id")
		if err != nil {
			return err
		}
		body, err := ctx.RequireArg("body")
		if err != nil {
			return err
		}
		payload := map[string]interface{}{
			"notes": body,
		}
		env, err := ctx.CallAPI("PUT", fmt.Sprintf("%s/journals/%s", prV1Path(ctx, id), commentID), payload)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "delete-comment",
	Description: "Delete a pull request review comment",
	Flags: []common.Flag{
		{Name: "id", Short: "i", Usage: "PR number", Required: true},
		{Name: "comment-id", Usage: "Comment ID", Required: true},
	},
	Run: func(ctx *common.RuntimeContext) error {
		if err := ctx.ResolveOwnerRepo(); err != nil {
			return err
		}
		id, err := ctx.RequireArg("id")
		if err != nil {
			return err
		}
		commentID, err := ctx.RequireArg("comment-id")
		if err != nil {
			return err
		}
		env, err := ctx.CallAPI("DELETE", fmt.Sprintf("%s/journals/%s", prV1Path(ctx, id), commentID), nil)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "commits",
	Description: "List commits in a pull request",
	Flags: []common.Flag{
		{Name: "id", Short: "i", Usage: "PR number", Required: true},
	},
	Run: func(ctx *common.RuntimeContext) error {
		if err := ctx.ResolveOwnerRepo(); err != nil {
			return err
		}
		id, err := ctx.RequireArg("id")
		if err != nil {
			return err
		}
		env, err := ctx.CallAPI("GET", fmt.Sprintf("%s/pulls/%s/commits", ctx.RepoPath(), id), nil)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "reopen",
	Description: "Reopen a closed pull request",
	Flags: []common.Flag{
		{Name: "id", Short: "i", Usage: "PR number", Required: true},
	},
	Run: func(ctx *common.RuntimeContext) error {
		if err := ctx.ResolveOwnerRepo(); err != nil {
			return err
		}
		id, err := ctx.RequireArg("id")
		if err != nil {
			return err
		}
		env, err := ctx.CallAPI("POST", prV1Path(ctx, id)+"/reopen", nil)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "update",
	Description: "Update a pull request title and/or description",
	Flags: []common.Flag{
		{Name: "id", Short: "i", Usage: "PR number", Required: true},
		{Name: "title", Short: "t", Usage: "New title"},
		{Name: "body", Short: "b", Usage: "New description"},
	},
	Run: func(ctx *common.RuntimeContext) error {
		if err := ctx.ResolveOwnerRepo(); err != nil {
			return err
		}
		id, err := ctx.RequireArg("id")
		if err != nil {
			return err
		}
		title := ctx.Arg("title")
		body := ctx.Arg("body")
		if title == "" && body == "" {
			return fmt.Errorf("at least one of --title or --body is required")
		}
		payload := map[string]interface{}{}
		if title != "" {
			payload["title"] = title
		}
		if body != "" {
			payload["body"] = body
		}
		env, err := ctx.CallAPI("PUT", fmt.Sprintf("%s/pulls/%s", ctx.RepoPath(), id), payload)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
```

- [ ] **步骤 4：运行测试验证通过**

运行：`cd /c/Users/刘焱/Desktop/gitlink-cli && go test ./shortcuts/pr/ -v`
预期：ALL PASS

- [ ] **步骤 5：Commit**

```bash
cd /c/Users/刘焱/Desktop/gitlink-cli
git add shortcuts/pr/pr.go shortcuts/pr/pr_test.go
git commit -m "feat(pr): add comments CRUD, commits, reopen, update shortcuts"
```

---

## 任务 3：分支 / Release / 组织 / 文件操作补全

**文件：**
- 修改：`shortcuts/branch/branch.go`
- 修改：`shortcuts/release/release.go`
- 修改：`shortcuts/org/org.go`
- 修改：`shortcuts/file/file.go`

- [ ] **步骤 1：在 branch.go 的 Shortcuts() 中追加 3 个快捷方式**

在最后一个 `unprotect` 快捷方式后追加：

```go
{
	Name:        "all",
	Description: "List all branches without pagination",
	Run: func(ctx *common.RuntimeContext) error {
		if err := ctx.ResolveOwnerRepo(); err != nil {
			return err
		}
		env, err := ctx.CallAPI("GET", "/v1"+ctx.RepoPath()+"/branches/all", nil)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "default",
	Description: "Update the default branch",
	Flags: []common.Flag{
		{Name: "name", Short: "n", Usage: "Branch to set as default", Required: true},
	},
	Run: func(ctx *common.RuntimeContext) error {
		if err := ctx.ResolveOwnerRepo(); err != nil {
			return err
		}
		name, err := ctx.RequireArg("name")
		if err != nil {
			return err
		}
		payload := map[string]interface{}{
			"default_branch": name,
		}
		env, err := ctx.CallAPI("PATCH", "/v1"+ctx.RepoPath()+"/branches/update_default_branch", payload)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "restore",
	Description: "Restore a deleted branch",
	Flags: []common.Flag{
		{Name: "name", Short: "n", Usage: "Branch name to restore", Required: true},
	},
	Run: func(ctx *common.RuntimeContext) error {
		if err := ctx.ResolveOwnerRepo(); err != nil {
			return err
		}
		name, err := ctx.RequireArg("name")
		if err != nil {
			return err
		}
		payload := map[string]interface{}{
			"branch_name": name,
		}
		env, err := ctx.CallAPI("POST", "/v1"+ctx.RepoPath()+"/branches/restore", payload)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
```

- [ ] **步骤 2：在 release.go 的 Shortcuts() 中追加 2 个快捷方式**

在 `delete` 快捷方式后追加：

```go
{
	Name:        "edit",
	Description: "Get release metadata for editing",
	Flags: []common.Flag{
		{Name: "id", Short: "i", Usage: "Release ID", Required: true},
	},
	Run: func(ctx *common.RuntimeContext) error {
		if err := ctx.ResolveOwnerRepo(); err != nil {
			return err
		}
		id, _ := ctx.RequireArg("id")
		env, err := ctx.CallAPI("GET", fmt.Sprintf("%s/releases/%s/edit", ctx.RepoPath(), id), nil)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "update",
	Description: "Update a release",
	Flags: []common.Flag{
		{Name: "id", Short: "i", Usage: "Release ID", Required: true},
		{Name: "name", Short: "n", Usage: "New release name"},
		{Name: "body", Short: "b", Usage: "New release notes"},
		{Name: "prerelease", Usage: "Mark as prerelease (true/false)"},
	},
	Run: func(ctx *common.RuntimeContext) error {
		if err := ctx.ResolveOwnerRepo(); err != nil {
			return err
		}
		id, _ := ctx.RequireArg("id")
		payload := map[string]interface{}{}
		if n := ctx.Arg("name"); n != "" {
			payload["name"] = n
		}
		if b := ctx.Arg("body"); b != "" {
			payload["body"] = b
		}
		if p := ctx.Arg("prerelease"); p != "" {
			payload["prerelease"] = p == "true"
		}
		env, err := ctx.CallAPI("PUT", fmt.Sprintf("%s/releases/%s", ctx.RepoPath(), id), payload)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
```

- [ ] **步骤 3：在 org.go 的 Shortcuts() 中追加 2 个快捷方式**

在 `create` 快捷方式后追加：

```go
{
	Name:        "add-team-projects",
	Description: "Add all projects to an organization team",
	Flags: []common.Flag{
		{Name: "org", Short: "o", Usage: "Organization ID or login", Required: true},
		{Name: "team", Short: "t", Usage: "Team ID", Required: true},
	},
	Run: func(ctx *common.RuntimeContext) error {
		org, _ := ctx.RequireArg("org")
		team, _ := ctx.RequireArg("team")
		env, err := ctx.CallAPI("POST", fmt.Sprintf("/organizations/%s/teams/%s/team_projects/create_all", org, team), nil)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "remove-team-projects",
	Description: "Remove all projects from an organization team",
	Flags: []common.Flag{
		{Name: "org", Short: "o", Usage: "Organization ID or login", Required: true},
		{Name: "team", Short: "t", Usage: "Team ID", Required: true},
	},
	Run: func(ctx *common.RuntimeContext) error {
		org, _ := ctx.RequireArg("org")
		team, _ := ctx.RequireArg("team")
		env, err := ctx.CallAPI("DELETE", fmt.Sprintf("/organizations/%s/teams/%s/team_projects/destroy_all", org, team), nil)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
```

- [ ] **步骤 4：在 file.go 的 Shortcuts() 中追加 4 个快捷方式**

在 `blob` 快捷方式后追加：

```go
{
	Name:        "batch",
	Description: "Batch commit multiple files",
	Flags: []common.Flag{
		{Name: "files", Short: "f", Usage: "JSON array of {path, content, action} objects", Required: true},
		{Name: "message", Short: "m", Usage: "Commit message", Required: true},
		{Name: "branch", Short: "b", Usage: "Target branch"},
	},
	Run: func(ctx *common.RuntimeContext) error {
		if err := ctx.ResolveOwnerRepo(); err != nil {
			return err
		}
		files, err := ctx.RequireArg("files")
		if err != nil {
			return err
		}
		message, err := ctx.RequireArg("message")
		if err != nil {
			return err
		}
		var filesArr []interface{}
		if err := json.Unmarshal([]byte(files), &filesArr); err != nil {
			return fmt.Errorf("invalid --files JSON: %w", err)
		}
		payload := map[string]interface{}{
			"files":   filesArr,
			"message": message,
		}
		if branch := ctx.Arg("branch"); branch != "" {
			payload["branch"] = branch
		}
		env, err := ctx.CallAPI("POST", fmt.Sprintf("/v1/%s/%s/contents/batch", ctx.Owner, ctx.Repo), payload)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "entries",
	Description: "List directory entries in a repository",
	Flags: []common.Flag{
		{Name: "path", Short: "p", Usage: "Directory path"},
		{Name: "ref", Short: "r", Usage: "Branch, tag, or commit SHA"},
	},
	Run: func(ctx *common.RuntimeContext) error {
		if err := ctx.ResolveOwnerRepo(); err != nil {
			return err
		}
		query := buildQuery(map[string]string{
			"filepath": ctx.Arg("path"),
			"ref":      ctx.Arg("ref"),
		})
		env, err := ctx.CallAPIWithQuery("GET", fmt.Sprintf("/%s/%s/entries", ctx.Owner, ctx.Repo), query)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "sub-entries",
	Description: "Get sub-directory or file entries",
	Flags: []common.Flag{
		{Name: "path", Short: "p", Usage: "Directory or file path"},
		{Name: "ref", Short: "r", Usage: "Branch, tag, or commit SHA"},
	},
	Run: func(ctx *common.RuntimeContext) error {
		if err := ctx.ResolveOwnerRepo(); err != nil {
			return err
		}
		query := buildQuery(map[string]string{
			"filepath": ctx.Arg("path"),
			"ref":      ctx.Arg("ref"),
		})
		env, err := ctx.CallAPIWithQuery("GET", fmt.Sprintf("/%s/%s/sub_entries", ctx.Owner, ctx.Repo), query)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "replace",
	Description: "Replace file content in the repository",
	Flags: []common.Flag{
		{Name: "path", Short: "p", Usage: "File path in the repository", Required: true},
		{Name: "content", Short: "c", Usage: "New file content (plain text, will be base64-encoded)", Required: true},
		{Name: "message", Short: "m", Usage: "Commit message", Required: true},
		{Name: "branch", Short: "b", Usage: "Target branch"},
	},
	Run: func(ctx *common.RuntimeContext) error {
		if err := ctx.ResolveOwnerRepo(); err != nil {
			return err
		}
		filePath, err := ctx.RequireArg("path")
		if err != nil {
			return err
		}
		content, err := ctx.RequireArg("content")
		if err != nil {
			return err
		}
		message, err := ctx.RequireArg("message")
		if err != nil {
			return err
		}
		payload := map[string]interface{}{
			"filepath": filePath,
			"content":  base64.StdEncoding.EncodeToString([]byte(content)),
			"message":  message,
		}
		if branch := ctx.Arg("branch"); branch != "" {
			payload["branch"] = branch
		}
		env, err := ctx.CallAPI("POST", fmt.Sprintf("/%s/%s/replace_file", ctx.Owner, ctx.Repo), payload)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
```

注意：`file.go` 需要额外 import `"encoding/json"` 和 `"encoding/base64"`（base64 已有）。在 import 中追加 `"encoding/json"`。

- [ ] **步骤 5：编译验证**

运行：`cd /c/Users/刘焱/Desktop/gitlink-cli && go build ./...`
预期：BUILD SUCCESS

- [ ] **步骤 6：Commit**

```bash
cd /c/Users/刘焱/Desktop/gitlink-cli
git add shortcuts/branch/branch.go shortcuts/release/release.go shortcuts/org/org.go shortcuts/file/file.go
git commit -m "feat: add branch all/default/restore, release edit/update, org team-projects, file batch/entries/replace"
```

---

## 任务 4：Repo 操作补全（22 个新快捷方式）

**文件：**
- 修改：`shortcuts/repo/repo.go`
- 新建：`shortcuts/repo/repo_test.go`

- [ ] **步骤 1：编写失败的测试**

新建 `shortcuts/repo/repo_test.go`：

```go
package repo

import (
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"github.com/gitlink-org/gitlink-cli/internal/client"
	"github.com/gitlink-org/gitlink-cli/shortcuts/common"
)

func TestRepoUpdateSendsPATCH(t *testing.T) {
	var method, path string
	var payload map[string]interface{}
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		method = r.Method
		path = r.URL.Path
		if r.ContentLength > 0 {
			payload = decodeRepoJSON(t, r)
		}
		writeRepoJSON(t, w, map[string]interface{}{"id": float64(1)})
	}))
	defer server.Close()

	err := runRepoShortcut(t, server, "update", map[string]string{
		"description": "New description",
	})
	if err != nil {
		t.Fatalf("update shortcut failed: %v", err)
	}
	if method != "PATCH" {
		t.Fatalf("expected PATCH, got %s", method)
	}
	if payload["description"] != "New description" {
		t.Fatalf("expected description 'New description', got %v", payload["description"])
	}
}

func TestRepoAboutCallsCorrectEndpoint(t *testing.T) {
	var path string
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		path = r.URL.Path
		writeRepoJSON(t, w, map[string]interface{}{"content": "About text"})
	}))
	defer server.Close()

	err := runRepoShortcut(t, server, "about", nil)
	if err != nil {
		t.Fatalf("about shortcut failed: %v", err)
	}
	if !strings.Contains(path, "/about") {
		t.Fatalf("expected path to contain /about, got %s", path)
	}
}

func TestRepoStarCallsCorrectEndpoint(t *testing.T) {
	var method string
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		method = r.Method
		writeRepoJSON(t, w, map[string]interface{}{"message": "ok"})
	}))
	defer server.Close()

	err := runRepoShortcut(t, server, "star", map[string]string{"id": "123"})
	if err != nil {
		t.Fatalf("star shortcut failed: %v", err)
	}
	if method != "POST" {
		t.Fatalf("expected POST, got %s", method)
	}
}

func TestRepoLanguagesCallsCorrectEndpoint(t *testing.T) {
	var path string
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		path = r.URL.Path
		writeRepoJSON(t, w, map[string]interface{}{"Go": float64(95), "Python": float64(5)})
	}))
	defer server.Close()

	err := runRepoShortcut(t, server, "languages", nil)
	if err != nil {
		t.Fatalf("languages shortcut failed: %v", err)
	}
	if !strings.Contains(path, "/languages") {
		t.Fatalf("expected path to contain /languages, got %s", path)
	}
}

func runRepoShortcut(t *testing.T, server *httptest.Server, name string, args map[string]string) error {
	t.Helper()
	shortcut := findRepoShortcut(t, name)
	if args == nil {
		args = map[string]string{}
	}
	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 findRepoShortcut(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 decodeRepoJSON(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 writeRepoJSON(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)
	}
}
```

- [ ] **步骤 2：运行测试验证失败**

运行：`cd /c/Users/刘焱/Desktop/gitlink-cli && go test ./shortcuts/repo/ -v`
预期：FAIL

- [ ] **步骤 3：在 repo.go 的 Shortcuts() 中追加 22 个快捷方式**

在 `delete` 快捷方式后追加以下代码块。注意：`repo.go` 当前 127 行，在最后的 `},` 和 `}` 之间插入。

```go
{
	Name:        "update",
	Description: "Update repository settings",
	Flags: []common.Flag{
		{Name: "name", Short: "n", Usage: "New repository name"},
		{Name: "description", Short: "d", Usage: "New description"},
		{Name: "private", Usage: "Set visibility: true for private, false for public"},
		{Name: "website", Short: "w", Usage: "Website URL"},
	},
	Run: func(ctx *common.RuntimeContext) error {
		if err := ctx.ResolveOwnerRepo(); err != nil {
			return err
		}
		payload := map[string]interface{}{}
		if n := ctx.Arg("name"); n != "" {
			payload["name"] = n
		}
		if d := ctx.Arg("description"); d != "" {
			payload["description"] = d
		}
		if p := ctx.Arg("private"); p != "" {
			payload["private"] = p == "true"
		}
		if w := ctx.Arg("website"); w != "" {
			payload["website"] = w
		}
		if len(payload) == 0 {
			return fmt.Errorf("at least one of --name, --description, --private, --website is required")
		}
		env, err := ctx.CallAPI("PATCH", ctx.RepoPath(), payload)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "about",
	Description: "Show repository homepage/about content",
	Run: func(ctx *common.RuntimeContext) error {
		if err := ctx.ResolveOwnerRepo(); err != nil {
			return err
		}
		env, err := ctx.CallAPI("GET", ctx.RepoPath()+"/about", nil)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "menu",
	Description: "List repository navigation menu",
	Run: func(ctx *common.RuntimeContext) error {
		if err := ctx.ResolveOwnerRepo(); err != nil {
			return err
		}
		env, err := ctx.CallAPI("GET", ctx.RepoPath()+"/menu_list", nil)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "units-get",
	Description: "Get repository navigation units settings",
	Run: func(ctx *common.RuntimeContext) error {
		if err := ctx.ResolveOwnerRepo(); err != nil {
			return err
		}
		env, err := ctx.CallAPI("GET", ctx.RepoPath()+"/project_units", nil)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "units-set",
	Description: "Update repository navigation units settings",
	Flags: []common.Flag{
		{Name: "units", Short: "u", Usage: "JSON array of unit types to enable", Required: true},
	},
	Run: func(ctx *common.RuntimeContext) error {
		if err := ctx.ResolveOwnerRepo(); err != nil {
			return err
		}
		units, err := ctx.RequireArg("units")
		if err != nil {
			return err
		}
		var unitsArr []interface{}
		if err := json.Unmarshal([]byte(units), &unitsArr); err != nil {
			return fmt.Errorf("invalid --units JSON: %w", err)
		}
		payload := map[string]interface{}{"units": unitsArr}
		env, err := ctx.CallAPI("POST", ctx.RepoPath()+"/project_units", payload)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "code-stats",
	Description: "Get code statistics (lines of code)",
	Run: func(ctx *common.RuntimeContext) error {
		if err := ctx.ResolveOwnerRepo(); err != nil {
			return err
		}
		env, err := ctx.CallAPI("GET", fmt.Sprintf("/v1%s/code_stats", ctx.RepoPath()), nil)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "languages",
	Description: "Get repository language breakdown",
	Run: func(ctx *common.RuntimeContext) error {
		if err := ctx.ResolveOwnerRepo(); err != nil {
			return err
		}
		env, err := ctx.CallAPI("GET", ctx.RepoPath()+"/languages", nil)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "contributors",
	Description: "List repository contributors",
	Run: func(ctx *common.RuntimeContext) error {
		if err := ctx.ResolveOwnerRepo(); err != nil {
			return err
		}
		env, err := ctx.CallAPI("GET", ctx.RepoPath()+"/contributors", nil)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "contributors-stat",
	Description: "Get contributor code statistics",
	Run: func(ctx *common.RuntimeContext) error {
		if err := ctx.ResolveOwnerRepo(); err != nil {
			return err
		}
		env, err := ctx.CallAPI("GET", fmt.Sprintf("/v1%s/contributors/stat", ctx.RepoPath()), nil)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "recommend",
	Description: "List recommended projects",
	Run: func(ctx *common.RuntimeContext) error {
		env, err := ctx.CallAPI("GET", "/projects/recommend", nil)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "star",
	Description: "Star (like) a project",
	Flags: []common.Flag{
		{Name: "id", Short: "i", Usage: "Project ID", Required: true},
	},
	Run: func(ctx *common.RuntimeContext) error {
		id, err := ctx.RequireArg("id")
		if err != nil {
			return err
		}
		env, err := ctx.CallAPI("POST", fmt.Sprintf("/projects/%s/praise_tread/like", id), nil)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "unstar",
	Description: "Unstar (unlike) a project",
	Flags: []common.Flag{
		{Name: "id", Short: "i", Usage: "Project ID", Required: true},
	},
	Run: func(ctx *common.RuntimeContext) error {
		id, err := ctx.RequireArg("id")
		if err != nil {
			return err
		}
		env, err := ctx.CallAPI("DELETE", fmt.Sprintf("/projects/%s/praise_tread/unlike", id), nil)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "watch",
	Description: "Watch (follow) a project",
	Flags: []common.Flag{
		{Name: "id", Short: "i", Usage: "Project ID", Required: true},
	},
	Run: func(ctx *common.RuntimeContext) error {
		id, err := ctx.RequireArg("id")
		if err != nil {
			return err
		}
		env, err := ctx.CallAPI("POST", "/watchers/follow", map[string]interface{}{"project_id": id})
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "unwatch",
	Description: "Unwatch (unfollow) a project",
	Flags: []common.Flag{
		{Name: "id", Short: "i", Usage: "Project ID", Required: true},
	},
	Run: func(ctx *common.RuntimeContext) error {
		id, err := ctx.RequireArg("id")
		if err != nil {
			return err
		}
		env, err := ctx.CallAPI("DELETE", "/watchers/unfollow", nil)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "stargazers",
	Description: "List users who starred the repository",
	Run: func(ctx *common.RuntimeContext) error {
		if err := ctx.ResolveOwnerRepo(); err != nil {
			return err
		}
		env, err := ctx.CallAPI("GET", ctx.RepoPath()+"/stargazers", nil)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "watchers",
	Description: "List users watching the repository",
	Run: func(ctx *common.RuntimeContext) error {
		if err := ctx.ResolveOwnerRepo(); err != nil {
			return err
		}
		env, err := ctx.CallAPI("GET", ctx.RepoPath()+"/watchers", nil)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "transfer",
	Description: "Transfer project to another owner",
	Flags: []common.Flag{
		{Name: "owner", Short: "o", Usage: "Target owner (organization or user)", Required: true},
	},
	Run: func(ctx *common.RuntimeContext) error {
		if err := ctx.ResolveOwnerRepo(); err != nil {
			return err
		}
		owner, err := ctx.RequireArg("owner")
		if err != nil {
			return err
		}
		payload := map[string]interface{}{"owner": owner}
		env, err := ctx.CallAPI("POST", ctx.RepoPath()+"/applied_transfer_projects", payload)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "cancel-transfer",
	Description: "Cancel a pending project transfer",
	Run: func(ctx *common.RuntimeContext) error {
		if err := ctx.ResolveOwnerRepo(); err != nil {
			return err
		}
		env, err := ctx.CallAPI("POST", ctx.RepoPath()+"/applied_transfer_projects/cancel", nil)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "transfer-orgs",
	Description: "List organizations eligible for project transfer",
	Run: func(ctx *common.RuntimeContext) error {
		if err := ctx.ResolveOwnerRepo(); err != nil {
			return err
		}
		env, err := ctx.CallAPI("GET", ctx.RepoPath()+"/applied_transfer_projects/organizations", nil)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "invite-link",
	Description: "Generate project invite link",
	Run: func(ctx *common.RuntimeContext) error {
		if err := ctx.ResolveOwnerRepo(); err != nil {
			return err
		}
		env, err := ctx.CallAPI("GET", ctx.RepoPath()+"/project_invite_links/current_link", nil)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "edit-detail",
	Description: "Get repository settings for editing",
	Run: func(ctx *common.RuntimeContext) error {
		if err := ctx.ResolveOwnerRepo(); err != nil {
			return err
		}
		env, err := ctx.CallAPI("GET", ctx.RepoPath()+"/edit", nil)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "simple",
	Description: "Get simplified repository details",
	Run: func(ctx *common.RuntimeContext) error {
		if err := ctx.ResolveOwnerRepo(); err != nil {
			return err
		}
		env, err := ctx.CallAPI("GET", ctx.RepoPath()+"/simple", nil)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
```

注意：`repo.go` 需要在 import 中追加 `"encoding/json"`（`units-set` 快捷方式使用 `json.Unmarshal`）。

- [ ] **步骤 4：运行测试验证通过**

运行：`cd /c/Users/刘焱/Desktop/gitlink-cli && go test ./shortcuts/repo/ -v`
预期：ALL PASS

- [ ] **步骤 5：Commit**

```bash
cd /c/Users/刘焱/Desktop/gitlink-cli
git add shortcuts/repo/repo.go shortcuts/repo/repo_test.go
git commit -m "feat(repo): add 22 new shortcuts for settings, stats, social, transfer, invite"
```

---

## 任务 5：User 操作补全（22 个新快捷方式）

**文件：**
- 修改：`shortcuts/user/user.go`
- 新建：`shortcuts/user/user_test.go`

- [ ] **步骤 1：编写失败的测试**

新建 `shortcuts/user/user_test.go`：

```go
package user

import (
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/gitlink-org/gitlink-cli/internal/client"
	"github.com/gitlink-org/gitlink-cli/shortcuts/common"
)

func TestUserMessagesListsMessages(t *testing.T) {
	var method, path string
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		method = r.Method
		path = r.URL.Path
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(map[string]interface{}{
			"messages": []interface{}{},
		})
	}))
	defer server.Close()

	err := runUserShortcut(t, server, "messages", map[string]string{"login": "testuser"})
	if err != nil {
		t.Fatalf("messages shortcut failed: %v", err)
	}
	if method != "GET" {
		t.Fatalf("expected GET, got %s", method)
	}
}

func TestUserFeedbackSendsPOST(t *testing.T) {
	var method string
	var payload map[string]interface{}
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		method = r.Method
		if r.ContentLength > 0 {
			json.NewDecoder(r.Body).Decode(&payload)
		}
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(map[string]interface{}{"id": float64(1)})
	}))
	defer server.Close()

	err := runUserShortcut(t, server, "feedback", map[string]string{
		"login": "testuser",
		"content": "Great platform!",
	})
	if err != nil {
		t.Fatalf("feedback shortcut failed: %v", err)
	}
	if method != "POST" {
		t.Fatalf("expected POST, got %s", method)
	}
	if payload["content"] != "Great platform!" {
		t.Fatalf("expected content 'Great platform!', got %v", payload["content"])
	}
}

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,
		},
		Format: "json",
		Args:   args,
	}
	return shortcut.Run(ctx)
}

func findUserShortcut(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
}
```

- [ ] **步骤 2：运行测试验证失败**

运行：`cd /c/Users/刘焱/Desktop/gitlink-cli && go test ./shortcuts/user/ -v`
预期：FAIL

- [ ] **步骤 3：在 user.go 的 Shortcuts() 中追加 22 个快捷方式**

在 `info` 快捷方式后追加。以下按功能分组。user.go 需要追加 `"net/url"` 到 import。

**消息管理组：**

```go
{
	Name:        "messages",
	Description: "List user notification messages",
	Flags: []common.Flag{
		{Name: "login", Short: "l", Usage: "User login name", Required: true},
		{Name: "page", Short: "p", Usage: "Page number", Default: "1"},
		{Name: "limit", Short: "t", Usage: "Items per page", Default: "20"},
	},
	Run: func(ctx *common.RuntimeContext) error {
		login, err := ctx.RequireArg("login")
		if err != nil {
			return err
		}
		q := url.Values{}
		q.Set("page", ctx.Arg("page"))
		q.Set("limit", ctx.Arg("limit"))
		env, err := ctx.CallAPIWithQuery("GET", fmt.Sprintf("/users/%s/messages", login), q)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "delete-message",
	Description: "Delete a notification message",
	Flags: []common.Flag{
		{Name: "login", Short: "l", Usage: "User login name", Required: true},
		{Name: "id", Short: "i", Usage: "Message ID", Required: true},
	},
	Run: func(ctx *common.RuntimeContext) error {
		login, err := ctx.RequireArg("login")
		if err != nil {
			return err
		}
		id, err := ctx.RequireArg("id")
		if err != nil {
			return err
		}
		env, err := ctx.CallAPI("DELETE", fmt.Sprintf("/users/%s/messages", login), map[string]interface{}{"id": id})
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "create-message",
	Description: "Send a message to a user",
	Flags: []common.Flag{
		{Name: "login", Short: "l", Usage: "Recipient user login", Required: true},
		{Name: "content", Short: "c", Usage: "Message content", Required: true},
	},
	Run: func(ctx *common.RuntimeContext) error {
		login, err := ctx.RequireArg("login")
		if err != nil {
			return err
		}
		content, err := ctx.RequireArg("content")
		if err != nil {
			return err
		}
		payload := map[string]interface{}{"content": content}
		env, err := ctx.CallAPI("POST", fmt.Sprintf("/users/%s/messages", login), payload)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "read-message",
	Description: "Mark a message as read",
	Flags: []common.Flag{
		{Name: "login", Short: "l", Usage: "User login name", Required: true},
		{Name: "id", Short: "i", Usage: "Message ID", Required: true},
	},
	Run: func(ctx *common.RuntimeContext) error {
		login, err := ctx.RequireArg("login")
		if err != nil {
			return err
		}
		id, err := ctx.RequireArg("id")
		if err != nil {
			return err
		}
		env, err := ctx.CallAPI("POST", fmt.Sprintf("/users/%s/messages/read", login), map[string]interface{}{"id": id})
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "msg-settings",
	Description: "List user message notification settings",
	Flags: []common.Flag{
		{Name: "login", Short: "l", Usage: "User login name", Required: true},
	},
	Run: func(ctx *common.RuntimeContext) error {
		login, err := ctx.RequireArg("login")
		if err != nil {
			return err
		}
		env, err := ctx.CallAPI("GET", fmt.Sprintf("/users/%s/template_message_settings", login), nil)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "update-msg-settings",
	Description: "Update user message notification settings",
	Flags: []common.Flag{
		{Name: "login", Short: "l", Usage: "User login name", Required: true},
		{Name: "settings", Short: "s", Usage: "JSON array of setting objects", Required: true},
	},
	Run: func(ctx *common.RuntimeContext) error {
		login, err := ctx.RequireArg("login")
		if err != nil {
			return err
		}
		settings, err := ctx.RequireArg("settings")
		if err != nil {
			return err
		}
		var settingsArr []interface{}
		if err := json.Unmarshal([]byte(settings), &settingsArr); err != nil {
			return fmt.Errorf("invalid --settings JSON: %w", err)
		}
		payload := map[string]interface{}{"settings": settingsArr}
		env, err := ctx.CallAPI("POST", fmt.Sprintf("/users/%s/template_message_settings/update_setting", login), payload)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
```

**置顶项目组：**

```go
{
	Name:        "pinned-projects",
	Description: "List user pinned projects",
	Flags: []common.Flag{
		{Name: "login", Short: "l", Usage: "User login name", Required: true},
	},
	Run: func(ctx *common.RuntimeContext) error {
		login, err := ctx.RequireArg("login")
		if err != nil {
			return err
		}
		env, err := ctx.CallAPI("GET", fmt.Sprintf("/users/%s/is_pinned_projects", login), nil)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "pin-project",
	Description: "Pin a project to user profile",
	Flags: []common.Flag{
		{Name: "login", Short: "l", Usage: "User login name", Required: true},
		{Name: "id", Short: "i", Usage: "Comma-separated project IDs to pin", Required: true},
	},
	Run: func(ctx *common.RuntimeContext) error {
		login, err := ctx.RequireArg("login")
		if err != nil {
			return err
		}
		id, err := ctx.RequireArg("id")
		if err != nil {
			return err
		}
		env, err := ctx.CallAPI("POST", fmt.Sprintf("/users/%s/is_pinned_projects/pin", login), map[string]interface{}{"project_ids": id})
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "reorder-pinned",
	Description: "Reorder pinned projects",
	Flags: []common.Flag{
		{Name: "login", Short: "l", Usage: "User login name", Required: true},
		{Name: "id", Short: "i", Usage: "Pinned project ID", Required: true},
		{Name: "position", Short: "p", Usage: "New position number", Required: true},
	},
	Run: func(ctx *common.RuntimeContext) error {
		login, err := ctx.RequireArg("login")
		if err != nil {
			return err
		}
		id, err := ctx.RequireArg("id")
		if err != nil {
			return err
		}
		position, err := ctx.RequireArg("position")
		if err != nil {
			return err
		}
		payload := map[string]interface{}{"position": position}
		env, err := ctx.CallAPI("PUT", fmt.Sprintf("/users/%s/is_pinned_projects/%s", login, id), payload)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
```

**统计组：**

```go
{
	Name:        "activity",
	Description: "Get user recent activity statistics",
	Flags: []common.Flag{
		{Name: "login", Short: "l", Usage: "User login name", Required: true},
	},
	Run: func(ctx *common.RuntimeContext) error {
		login, err := ctx.RequireArg("login")
		if err != nil {
			return err
		}
		env, err := ctx.CallAPI("GET", fmt.Sprintf("/users/%s/statistics/activity", login), nil)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "headmap",
	Description: "Get user contribution heatmap",
	Flags: []common.Flag{
		{Name: "login", Short: "l", Usage: "User login name", Required: true},
	},
	Run: func(ctx *common.RuntimeContext) error {
		login, err := ctx.RequireArg("login")
		if err != nil {
			return err
		}
		env, err := ctx.CallAPI("GET", fmt.Sprintf("/users/%s/headmaps", login), nil)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "develop-stats",
	Description: "Get user development ability statistics",
	Flags: []common.Flag{
		{Name: "login", Short: "l", Usage: "User login name", Required: true},
	},
	Run: func(ctx *common.RuntimeContext) error {
		login, err := ctx.RequireArg("login")
		if err != nil {
			return err
		}
		env, err := ctx.CallAPI("GET", fmt.Sprintf("/users/%s/statistics/develop", login), nil)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "role-stats",
	Description: "Get user role positioning statistics",
	Flags: []common.Flag{
		{Name: "login", Short: "l", Usage: "User login name", Required: true},
	},
	Run: func(ctx *common.RuntimeContext) error {
		login, err := ctx.RequireArg("login")
		if err != nil {
			return err
		}
		env, err := ctx.CallAPI("GET", fmt.Sprintf("/users/%s/statistics/role", login), nil)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "major-stats",
	Description: "Get user major positioning statistics",
	Flags: []common.Flag{
		{Name: "login", Short: "l", Usage: "User login name", Required: true},
	},
	Run: func(ctx *common.RuntimeContext) error {
		login, err := ctx.RequireArg("login")
		if err != nil {
			return err
		}
		env, err := ctx.CallAPI("GET", fmt.Sprintf("/users/%s/statistics/major", login), nil)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
```

**传输/申请组：**

```go
{
	Name:        "applied-transfers",
	Description: "List pending project transfer requests",
	Flags: []common.Flag{
		{Name: "login", Short: "l", Usage: "User login name", Required: true},
	},
	Run: func(ctx *common.RuntimeContext) error {
		login, err := ctx.RequireArg("login")
		if err != nil {
			return err
		}
		env, err := ctx.CallAPI("GET", fmt.Sprintf("/users/%s/applied_transfer_projects", login), nil)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "accept-transfer",
	Description: "Accept a project transfer request",
	Flags: []common.Flag{
		{Name: "login", Short: "l", Usage: "User login name", Required: true},
		{Name: "id", Short: "i", Usage: "Transfer application ID", Required: true},
	},
	Run: func(ctx *common.RuntimeContext) error {
		login, err := ctx.RequireArg("login")
		if err != nil {
			return err
		}
		id, err := ctx.RequireArg("id")
		if err != nil {
			return err
		}
		env, err := ctx.CallAPI("POST", fmt.Sprintf("/users/%s/applied_transfer_projects/%s/accept", login, id), nil)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "refuse-transfer",
	Description: "Refuse a project transfer request",
	Flags: []common.Flag{
		{Name: "login", Short: "l", Usage: "User login name", Required: true},
		{Name: "id", Short: "i", Usage: "Transfer application ID", Required: true},
	},
	Run: func(ctx *common.RuntimeContext) error {
		login, err := ctx.RequireArg("login")
		if err != nil {
			return err
		}
		id, err := ctx.RequireArg("id")
		if err != nil {
			return err
		}
		env, err := ctx.CallAPI("POST", fmt.Sprintf("/users/%s/applied_transfer_projects/%s/refuse", login, id), nil)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "applied-projects",
	Description: "List pending project join requests",
	Flags: []common.Flag{
		{Name: "login", Short: "l", Usage: "User login name", Required: true},
	},
	Run: func(ctx *common.RuntimeContext) error {
		login, err := ctx.RequireArg("login")
		if err != nil {
			return err
		}
		env, err := ctx.CallAPI("GET", fmt.Sprintf("/users/%s/applied_projects", login), nil)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "accept-join",
	Description: "Accept a project join request",
	Flags: []common.Flag{
		{Name: "login", Short: "l", Usage: "User login name", Required: true},
		{Name: "id", Short: "i", Usage: "Application ID", Required: true},
	},
	Run: func(ctx *common.RuntimeContext) error {
		login, err := ctx.RequireArg("login")
		if err != nil {
			return err
		}
		id, err := ctx.RequireArg("id")
		if err != nil {
			return err
		}
		env, err := ctx.CallAPI("POST", fmt.Sprintf("/users/%s/applied_projects/%s/accept", login, id), nil)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "refuse-join",
	Description: "Refuse a project join request",
	Flags: []common.Flag{
		{Name: "login", Short: "l", Usage: "User login name", Required: true},
		{Name: "id", Short: "i", Usage: "Application ID", Required: true},
	},
	Run: func(ctx *common.RuntimeContext) error {
		login, err := ctx.RequireArg("login")
		if err != nil {
			return err
		}
		id, err := ctx.RequireArg("id")
		if err != nil {
			return err
		}
		env, err := ctx.CallAPI("POST", fmt.Sprintf("/users/%s/applied_projects/%s/refuse", login, id), nil)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
```

**反馈/更新组：**

```go
{
	Name:        "feedback",
	Description: "Submit feedback or suggestion",
	Flags: []common.Flag{
		{Name: "login", Short: "l", Usage: "User login name", Required: true},
		{Name: "content", Short: "c", Usage: "Feedback content", Required: true},
	},
	Run: func(ctx *common.RuntimeContext) error {
		login, err := ctx.RequireArg("login")
		if err != nil {
			return err
		}
		content, err := ctx.RequireArg("content")
		if err != nil {
			return err
		}
		payload := map[string]interface{}{"content": content}
		env, err := ctx.CallAPI("POST", fmt.Sprintf("/v1/%s/feedbacks", login), payload)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "update",
	Description: "Update user profile",
	Flags: []common.Flag{
		{Name: "login", Short: "l", Usage: "User login name", Required: true},
		{Name: "nickname", Short: "n", Usage: "Display name"},
		{Name: "description", Short: "d", Usage: "Bio/description"},
	},
	Run: func(ctx *common.RuntimeContext) error {
		login, err := ctx.RequireArg("login")
		if err != nil {
			return err
		}
		payload := map[string]interface{}{}
		if n := ctx.Arg("nickname"); n != "" {
			payload["nickname"] = n
		}
		if d := ctx.Arg("description"); d != "" {
			payload["description"] = d
		}
		if len(payload) == 0 {
			return fmt.Errorf("at least one of --nickname or --description is required")
		}
		env, err := ctx.CallAPI("PUT", fmt.Sprintf("/users/%s", login), payload)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
```

user.go 需要在 import 中追加 `"encoding/json"` 和 `"net/url"`。

- [ ] **步骤 4：运行测试验证通过**

运行：`cd /c/Users/刘焱/Desktop/gitlink-cli && go test ./shortcuts/user/ -v`
预期：ALL PASS

- [ ] **步骤 5：Commit**

```bash
cd /c/Users/刘焱/Desktop/gitlink-cli
git add shortcuts/user/user.go shortcuts/user/user_test.go
git commit -m "feat(user): add 22 shortcuts for messages, stats, transfers, settings, feedback"
```

---

## 任务 6：新建域 — util / dataset / template / attachment

**文件：**
- 新建：`shortcuts/util/util.go`
- 新建：`shortcuts/dataset/dataset.go`
- 新建：`shortcuts/template/template.go`
- 新建：`shortcuts/attachment/attachment.go`
- 修改：`shortcuts/register.go`

- [ ] **步骤 1：创建 shortcuts/util/util.go**

```go
package util

import (
	"github.com/gitlink-org/gitlink-cli/shortcuts/common"
)

func Shortcuts() []*common.Shortcut {
	return []*common.Shortcut{
		{
			Name:        "licenses",
			Description: "List available open source license templates",
			Run: func(ctx *common.RuntimeContext) error {
				env, err := ctx.CallAPI("GET", "/licenses", nil)
				if err != nil {
					return err
				}
				return ctx.Output(env)
			},
		},
		{
			Name:        "ignores",
			Description: "List available .gitignore templates",
			Run: func(ctx *common.RuntimeContext) error {
				env, err := ctx.CallAPI("GET", "/ignores", nil)
				if err != nil {
					return err
				}
				return ctx.Output(env)
			},
		},
		{
			Name:        "platform-msg-settings",
			Description: "Show platform-wide message notification settings",
			Run: func(ctx *common.RuntimeContext) error {
				env, err := ctx.CallAPI("GET", "/template_message_settings", nil)
				if err != nil {
					return err
				}
				return ctx.Output(env)
			},
		},
	}
}
```

- [ ] **步骤 2：创建 shortcuts/dataset/dataset.go**

```go
package dataset

import (
	"fmt"

	"github.com/gitlink-org/gitlink-cli/shortcuts/common"
)

func Shortcuts() []*common.Shortcut {
	return []*common.Shortcut{
		{
			Name:        "create",
			Description: "Create a dataset in a repository",
			Flags: []common.Flag{
				{Name: "title", Short: "t", Usage: "Dataset title", Required: true},
				{Name: "description", Short: "d", Usage: "Dataset description"},
			},
			Run: func(ctx *common.RuntimeContext) error {
				if err := ctx.ResolveOwnerRepo(); err != nil {
					return err
				}
				title, err := ctx.RequireArg("title")
				if err != nil {
					return err
				}
				payload := map[string]interface{}{"title": title}
				if d := ctx.Arg("description"); d != "" {
					payload["description"] = d
				}
				env, err := ctx.CallAPI("POST", fmt.Sprintf("/v1%s/dataset", ctx.RepoPath()), payload)
				if err != nil {
					return err
				}
				return ctx.Output(env)
			},
		},
		{
			Name:        "update",
			Description: "Update a dataset",
			Flags: []common.Flag{
				{Name: "title", Short: "t", Usage: "New dataset title"},
				{Name: "description", Short: "d", Usage: "New description"},
			},
			Run: func(ctx *common.RuntimeContext) error {
				if err := ctx.ResolveOwnerRepo(); err != nil {
					return err
				}
				payload := map[string]interface{}{}
				if t := ctx.Arg("title"); t != "" {
					payload["title"] = t
				}
				if d := ctx.Arg("description"); d != "" {
					payload["description"] = d
				}
				if len(payload) == 0 {
					return fmt.Errorf("at least one of --title or --description is required")
				}
				env, err := ctx.CallAPI("PUT", fmt.Sprintf("/v1%s/dataset", ctx.RepoPath()), payload)
				if err != nil {
					return err
				}
				return ctx.Output(env)
			},
		},
		{
			Name:        "view",
			Description: "View dataset details and attachments",
			Run: func(ctx *common.RuntimeContext) error {
				if err := ctx.ResolveOwnerRepo(); err != nil {
					return err
				}
				env, err := ctx.CallAPI("GET", fmt.Sprintf("/v1%s/dataset", ctx.RepoPath()), nil)
				if err != nil {
					return err
				}
				return ctx.Output(env)
			},
		},
		{
			Name:        "list",
			Description: "Search datasets globally",
			Flags: []common.Flag{
				{Name: "keyword", Short: "k", Usage: "Search keyword"},
				{Name: "page", Short: "p", Usage: "Page number", Default: "1"},
				{Name: "limit", Short: "l", Usage: "Items per page", Default: "20"},
			},
			Run: func(ctx *common.RuntimeContext) error {
				env, err := ctx.CallAPI("GET", "/v1/project_datasets", nil)
				if err != nil {
					return err
				}
				return ctx.Output(env)
			},
		},
	}
}
```

- [ ] **步骤 3：创建 shortcuts/template/template.go**

```go
package template

import (
	"fmt"

	"github.com/gitlink-org/gitlink-cli/shortcuts/common"
)

func Shortcuts() []*common.Shortcut {
	return []*common.Shortcut{
		{
			Name:        "list",
			Description: "List project templates",
			Flags: []common.Flag{
				{Name: "owner", Short: "o", Usage: "Owner login", Required: true},
			},
			Run: func(ctx *common.RuntimeContext) error {
				owner, err := ctx.RequireArg("owner")
				if err != nil {
					return err
				}
				env, err := ctx.CallAPI("GET", fmt.Sprintf("/v1/%s/project_templates", owner), nil)
				if err != nil {
					return err
				}
				return ctx.Output(env)
			},
		},
		{
			Name:        "create",
			Description: "Create a project template",
			Flags: []common.Flag{
				{Name: "owner", Short: "o", Usage: "Owner login", Required: true},
				{Name: "name", Short: "n", Usage: "Template name", Required: true},
				{Name: "description", Short: "d", Usage: "Template description"},
			},
			Run: func(ctx *common.RuntimeContext) error {
				owner, err := ctx.RequireArg("owner")
				if err != nil {
					return err
				}
				name, err := ctx.RequireArg("name")
				if err != nil {
					return err
				}
				payload := map[string]interface{}{"name": name}
				if d := ctx.Arg("description"); d != "" {
					payload["description"] = d
				}
				env, err := ctx.CallAPI("POST", fmt.Sprintf("/v1/%s/project_templates", owner), payload)
				if err != nil {
					return err
				}
				return ctx.Output(env)
			},
		},
		{
			Name:        "view",
			Description: "View project template details",
			Flags: []common.Flag{
				{Name: "owner", Short: "o", Usage: "Owner login", Required: true},
				{Name: "repo", Short: "r", Usage: "Repository name"},
				{Name: "id", Short: "i", Usage: "Template ID", Required: true},
			},
			Run: func(ctx *common.RuntimeContext) error {
				owner, err := ctx.RequireArg("owner")
				if err != nil {
					return err
				}
				id, err := ctx.RequireArg("id")
				if err != nil {
					return err
				}
				repo := ctx.Arg("repo")
				path := fmt.Sprintf("/v1/%s/project_templates/%s", owner, id)
				if repo != "" {
					path = fmt.Sprintf("/v1/%s/%s/project_templates/%s", owner, repo, id)
				}
				env, err := ctx.CallAPI("GET", path, nil)
				if err != nil {
					return err
				}
				return ctx.Output(env)
			},
		},
		{
			Name:        "update",
			Description: "Update a project template",
			Flags: []common.Flag{
				{Name: "owner", Short: "o", Usage: "Owner login", Required: true},
				{Name: "id", Short: "i", Usage: "Template ID", Required: true},
				{Name: "name", Short: "n", Usage: "New template name"},
				{Name: "description", Short: "d", Usage: "New description"},
			},
			Run: func(ctx *common.RuntimeContext) error {
				owner, err := ctx.RequireArg("owner")
				if err != nil {
					return err
				}
				id, err := ctx.RequireArg("id")
				if err != nil {
					return err
				}
				payload := map[string]interface{}{}
				if n := ctx.Arg("name"); n != "" {
					payload["name"] = n
				}
				if d := ctx.Arg("description"); d != "" {
					payload["description"] = d
				}
				env, err := ctx.CallAPI("PUT", fmt.Sprintf("/v1/%s/project_templates/%s", owner, id), payload)
				if err != nil {
					return err
				}
				return ctx.Output(env)
			},
		},
		{
			Name:        "delete",
			Description: "Delete a project template",
			Flags: []common.Flag{
				{Name: "owner", Short: "o", Usage: "Owner login", Required: true},
				{Name: "id", Short: "i", Usage: "Template ID", Required: true},
			},
			Run: func(ctx *common.RuntimeContext) error {
				owner, err := ctx.RequireArg("owner")
				if err != nil {
					return err
				}
				id, err := ctx.RequireArg("id")
				if err != nil {
					return err
				}
				env, err := ctx.CallAPI("DELETE", fmt.Sprintf("/v1/%s/project_templates/%s", owner, id), nil)
				if err != nil {
					return err
				}
				return ctx.Output(env)
			},
		},
	}
}
```

- [ ] **步骤 4：创建 shortcuts/attachment/attachment.go**

```go
package attachment

import (
	"fmt"

	"github.com/gitlink-org/gitlink-cli/shortcuts/common"
)

func Shortcuts() []*common.Shortcut {
	return []*common.Shortcut{
		{
			Name:        "upload",
			Description: "Upload a file attachment",
			Flags: []common.Flag{
				{Name: "file", Short: "f", Usage: "Local file path to upload", Required: true},
			},
			Run: func(ctx *common.RuntimeContext) error {
				file, err := ctx.RequireArg("file")
				if err != nil {
					return err
				}
				// The actual upload is handled as a multipart form POST.
				// For now, use the simple JSON API path.
				// TODO: implement multipart upload when needed.
				payload := map[string]interface{}{
					"file_path": file,
				}
				env, err := ctx.CallAPI("POST", "/attachments", payload)
				if err != nil {
					return err
				}
				return ctx.Output(env)
			},
		},
		{
			Name:        "delete",
			Description: "Delete an attachment by UUID",
			Flags: []common.Flag{
				{Name: "uuid", Short: "u", Usage: "Attachment UUID", Required: true},
			},
			Run: func(ctx *common.RuntimeContext) error {
				uuid, err := ctx.RequireArg("uuid")
				if err != nil {
					return err
				}
				env, err := ctx.CallAPI("DELETE", fmt.Sprintf("/attachments/%s", uuid), nil)
				if err != nil {
					return err
				}
				return ctx.Output(env)
			},
		},
	}
}
```

- [ ] **步骤 5：更新 shortcuts/register.go 注册新域**

在 import 中追加 4 个包：

```go
"github.com/gitlink-org/gitlink-cli/shortcuts/attachment"
"github.com/gitlink-org/gitlink-cli/shortcuts/dataset"
"github.com/gitlink-org/gitlink-cli/shortcuts/template"
"github.com/gitlink-org/gitlink-cli/shortcuts/util"
```

在 `groups` map 中追加 4 个条目：

```go
"util":       util.Shortcuts(),
"dataset":    dataset.Shortcuts(),
"template":   template.Shortcuts(),
"attachment": attachment.Shortcuts(),
```

在 `descriptions` map 中追加 4 个条目：

```go
"util":       "Utility operations (licenses, ignores, settings)",
"dataset":    "Dataset management",
"template":   "Project template management",
"attachment": "File attachment operations",
```

- [ ] **步骤 6：编译验证**

运行：`cd /c/Users/刘焱/Desktop/gitlink-cli && go build ./...`
预期：BUILD SUCCESS

- [ ] **步骤 7：Commit**

```bash
cd /c/Users/刘焱/Desktop/gitlink-cli
git add shortcuts/util/ shortcuts/dataset/ shortcuts/template/ shortcuts/attachment/ shortcuts/register.go
git commit -m "feat: add util, dataset, template, attachment shortcut groups"
```

---

## 任务 7：CI 流水线补全 + Wiki 导入/导出 + Repo L3 操作

**文件：**
- 修改：`shortcuts/ci/ci.go`
- 修改：`shortcuts/wiki/wiki.go`
- 修改：`shortcuts/repo/repo.go`（追加 L3 操作）

- [ ] **步骤 1：在 ci.go 的 Shortcuts() 中追加 10 个快捷方式**

在 `stop` 快捷方式后追加：

```go
{
	Name:        "pipelines",
	Description: "List pipeline templates",
	Run: func(ctx *common.RuntimeContext) error {
		env, err := ctx.CallAPI("GET", "/pm/pipelines", nil)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "run-pipeline",
	Description: "Run a pipeline",
	Flags: []common.Flag{
		{Name: "pipeline", Short: "p", Usage: "Pipeline ID or YAML path", Required: true},
		{Name: "branch", Short: "b", Usage: "Branch to run on"},
	},
	Run: func(ctx *common.RuntimeContext) error {
		if err := ctx.ResolveOwnerRepo(); err != nil {
			return err
		}
		payload := map[string]interface{}{
			"pipeline": ctx.Arg("pipeline"),
		}
		if b := ctx.Arg("branch"); b != "" {
			payload["branch"] = b
		}
		env, err := ctx.CallAPI("POST", fmt.Sprintf("/v1%s/actions/runs", ctx.RepoPath()), payload)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "runs",
	Description: "List pipeline run history",
	Flags: []common.Flag{
		{Name: "page", Short: "p", Usage: "Page number", Default: "1"},
		{Name: "limit", Short: "l", Usage: "Items per page", Default: "20"},
	},
	Run: func(ctx *common.RuntimeContext) error {
		if err := ctx.ResolveOwnerRepo(); err != nil {
			return err
		}
		q := url.Values{}
		q.Set("page", ctx.Arg("page"))
		q.Set("limit", ctx.Arg("limit"))
		env, err := ctx.CallAPIWithQuery("GET", fmt.Sprintf("/v1%s/actions/runs", ctx.RepoPath()), q)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "save-yaml",
	Description: "Save graphical pipeline YAML",
	Flags: []common.Flag{
		{Name: "yaml", Short: "y", Usage: "YAML content", Required: true},
		{Name: "name", Short: "n", Usage: "Pipeline name"},
	},
	Run: func(ctx *common.RuntimeContext) error {
		if err := ctx.ResolveOwnerRepo(); err != nil {
			return err
		}
		yamlContent, err := ctx.RequireArg("yaml")
		if err != nil {
			return err
		}
		payload := map[string]interface{}{
			"yaml": yamlContent,
		}
		if n := ctx.Arg("name"); n != "" {
			payload["name"] = n
		}
		env, err := ctx.CallAPI("POST", fmt.Sprintf("/v1%s/pipelines/save_yaml", ctx.RepoPath()), payload)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "pipeline-detail",
	Description: "Get pipeline details",
	Flags: []common.Flag{
		{Name: "id", Short: "i", Usage: "Pipeline ID", Required: true},
	},
	Run: func(ctx *common.RuntimeContext) error {
		if err := ctx.ResolveOwnerRepo(); err != nil {
			return err
		}
		id, err := ctx.RequireArg("id")
		if err != nil {
			return err
		}
		env, err := ctx.CallAPI("GET", fmt.Sprintf("/v1%s/pipelines/%s", ctx.RepoPath(), id), nil)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "delete-pipeline",
	Description: "Delete a pipeline",
	Flags: []common.Flag{
		{Name: "id", Short: "i", Usage: "Pipeline ID", Required: true},
	},
	Run: func(ctx *common.RuntimeContext) error {
		if err := ctx.ResolveOwnerRepo(); err != nil {
			return err
		}
		id, err := ctx.RequireArg("id")
		if err != nil {
			return err
		}
		env, err := ctx.CallAPI("DELETE", fmt.Sprintf("/v1%s/pipelines/%s", ctx.RepoPath(), id), nil)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "disable",
	Description: "Disable CI/CD pipeline",
	Run: func(ctx *common.RuntimeContext) error {
		if err := ctx.ResolveOwnerRepo(); err != nil {
			return err
		}
		env, err := ctx.CallAPI("POST", fmt.Sprintf("/v1%s/actions/disable", ctx.RepoPath()), nil)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "enable",
	Description: "Enable CI/CD pipeline",
	Run: func(ctx *common.RuntimeContext) error {
		if err := ctx.ResolveOwnerRepo(); err != nil {
			return err
		}
		env, err := ctx.CallAPI("POST", fmt.Sprintf("/v1%s/actions/enable", ctx.RepoPath()), nil)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "run-log",
	Description: "Get pipeline run log",
	Flags: []common.Flag{
		{Name: "run", Short: "r", Usage: "Run ID", Required: true},
	},
	Run: func(ctx *common.RuntimeContext) error {
		if err := ctx.ResolveOwnerRepo(); err != nil {
			return err
		}
		run, err := ctx.RequireArg("run")
		if err != nil {
			return err
		}
		env, err := ctx.CallAPI("POST", fmt.Sprintf("/v1%s/actions/runs/%s/jobs/0", ctx.RepoPath(), run), nil)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "run-results",
	Description: "Get pipeline run results report",
	Flags: []common.Flag{
		{Name: "page", Short: "p", Usage: "Page number", Default: "1"},
		{Name: "limit", Short: "l", Usage: "Items per page", Default: "20"},
	},
	Run: func(ctx *common.RuntimeContext) error {
		if err := ctx.ResolveOwnerRepo(); err != nil {
			return err
		}
		q := url.Values{}
		q.Set("page", ctx.Arg("page"))
		q.Set("limit", ctx.Arg("limit"))
		env, err := ctx.CallAPIWithQuery("GET", fmt.Sprintf("/v1%s/pipelines/run_results", ctx.RepoPath()), q)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
```

- [ ] **步骤 2：在 wiki.go 的 Shortcuts() 中追加 2 个快捷方式**

在 `delete` 快捷方式后追加：

```go
{
	Name:        "export",
	Description: "Export wiki pages",
	Flags: []common.Flag{
		{Name: "project-id", Usage: "Project ID"},
	},
	Run: func(ctx *common.RuntimeContext) error {
		if err := ctx.ResolveOwnerRepo(); err != nil {
			return err
		}
		q := url.Values{}
		q.Set("owner", ctx.Owner)
		q.Set("repo", ctx.Repo)
		if pid := ctx.Arg("project-id"); pid != "" {
			q.Set("projectId", pid)
		}
		env, err := ctx.CallAPIWithQuery("GET", "/wikiExport/wikiExport-wrapper", q)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "import",
	Description: "Import wiki from an uploaded file",
	Flags: []common.Flag{
		{Name: "project-id", Usage: "Project ID", Required: true},
	},
	Run: func(ctx *common.RuntimeContext) error {
		if err := ctx.ResolveOwnerRepo(); err != nil {
			return err
		}
		projectID, err := ctx.RequireArg("project-id")
		if err != nil {
			return err
		}
		path := fmt.Sprintf("/wikiExport/uploadWiki/%s/%s/%s", ctx.Owner, ctx.Repo, projectID)
		env, err := ctx.CallAPI("POST", path, nil)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
```

- [ ] **步骤 3：在 repo.go 中追加 L3 操作（join, quit, migrate, sync-mirror, topics）**

在 `simple` 快捷方式后追加：

```go
{
	Name:        "join",
	Description: "Apply to join a project",
	Flags: []common.Flag{
		{Name: "id", Short: "i", Usage: "Project ID", Required: true},
		{Name: "role", Short: "r", Usage: "Desired role"},
	},
	Run: func(ctx *common.RuntimeContext) error {
		id, err := ctx.RequireArg("id")
		if err != nil {
			return err
		}
		payload := map[string]interface{}{"project_id": id}
		if r := ctx.Arg("role"); r != "" {
			payload["role"] = r
		}
		env, err := ctx.CallAPI("POST", "/applied_projects", payload)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "quit",
	Description: "Quit a project",
	Run: func(ctx *common.RuntimeContext) error {
		if err := ctx.ResolveOwnerRepo(); err != nil {
			return err
		}
		env, err := ctx.CallAPI("POST", ctx.RepoPath()+"/quit", nil)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "migrate",
	Description: "Create a mirror (migrated) project",
	Flags: []common.Flag{
		{Name: "name", Short: "n", Usage: "New project name", Required: true},
		{Name: "clone-url", Short: "u", Usage: "Source repository URL", Required: true},
		{Name: "private", Usage: "Make repository private (true/false)", Default: "false"},
	},
	Run: func(ctx *common.RuntimeContext) error {
		name, err := ctx.RequireArg("name")
		if err != nil {
			return err
		}
		cloneURL, err := ctx.RequireArg("clone-url")
		if err != nil {
			return err
		}
		payload := map[string]interface{}{
			"repository_name": name,
			"clone_addr":      cloneURL,
		}
		if ctx.Arg("private") == "true" {
			payload["private"] = true
		}
		env, err := ctx.CallAPI("POST", "/projects/migrate", payload)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "sync-mirror",
	Description: "Sync a mirror repository",
	Flags: []common.Flag{
		{Name: "id", Short: "i", Usage: "Repository ID", Required: true},
	},
	Run: func(ctx *common.RuntimeContext) error {
		id, err := ctx.RequireArg("id")
		if err != nil {
			return err
		}
		env, err := ctx.CallAPI("POST", fmt.Sprintf("/repositories/%s/sync_mirror", id), nil)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "topics",
	Description: "List project topics/tags",
	Run: func(ctx *common.RuntimeContext) error {
		env, err := ctx.CallAPI("GET", "/v1/project_topics", nil)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "create-topic",
	Description: "Create a project topic/tag",
	Flags: []common.Flag{
		{Name: "name", Short: "n", Usage: "Topic name", Required: true},
	},
	Run: func(ctx *common.RuntimeContext) error {
		name, err := ctx.RequireArg("name")
		if err != nil {
			return err
		}
		env, err := ctx.CallAPI("POST", "/v1/project_topics", map[string]interface{}{"name": name})
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "delete-topic",
	Description: "Delete a project topic/tag",
	Flags: []common.Flag{
		{Name: "id", Short: "i", Usage: "Topic ID", Required: true},
	},
	Run: func(ctx *common.RuntimeContext) error {
		id, err := ctx.RequireArg("id")
		if err != nil {
			return err
		}
		env, err := ctx.CallAPI("DELETE", fmt.Sprintf("/v1/project_topics/%s", id), nil)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
```

- [ ] **步骤 4：编译验证**

运行：`cd /c/Users/刘焱/Desktop/gitlink-cli && go build ./...`
预期：BUILD SUCCESS

- [ ] **步骤 5：运行所有测试**

运行：`cd /c/Users/刘焱/Desktop/gitlink-cli && go test ./...`
预期：ALL PASS

- [ ] **步骤 6：Commit**

```bash
cd /c/Users/刘焱/Desktop/gitlink-cli
git add shortcuts/ci/ci.go shortcuts/wiki/wiki.go shortcuts/repo/repo.go
git commit -m "feat: add CI pipeline shortcuts, wiki import/export, repo join/quit/migrate/topics"
```

---

## 任务 8：用户个人设置 + OAuth2 认证增强（L3）

**文件：**
- 修改：`shortcuts/user/user.go`
- 修改：`cmd/auth/auth.go`
- 修改：`internal/auth/login.go`

- [ ] **步骤 1：在 user.go 中追加个人设置快捷方式**

在 `update` 快捷方式后追加：

```go
{
	Name:        "change-password",
	Description: "Change user password",
	Flags: []common.Flag{
		{Name: "old-password", Short: "o", Usage: "Current password", Required: true},
		{Name: "new-password", Short: "n", Usage: "New password", Required: true},
	},
	Run: func(ctx *common.RuntimeContext) error {
		oldPwd, err := ctx.RequireArg("old-password")
		if err != nil {
			return err
		}
		newPwd, err := ctx.RequireArg("new-password")
		if err != nil {
			return err
		}
		payload := map[string]interface{}{
			"old_password":  oldPwd,
			"new_password":  newPwd,
		}
		env, err := ctx.CallAPI("POST", "/accounts/change_password", payload)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "update-email",
	Description: "Change user email address",
	Flags: []common.Flag{
		{Name: "login", Short: "l", Usage: "User login name", Required: true},
		{Name: "email", Short: "e", Usage: "New email address", Required: true},
		{Name: "code", Short: "c", Usage: "Verification code"},
	},
	Run: func(ctx *common.RuntimeContext) error {
		login, err := ctx.RequireArg("login")
		if err != nil {
			return err
		}
		email, err := ctx.RequireArg("email")
		if err != nil {
			return err
		}
		payload := map[string]interface{}{"email": email}
		if code := ctx.Arg("code"); code != "" {
			payload["code"] = code
		}
		env, err := ctx.CallAPI("PATCH", fmt.Sprintf("/v1/%s/update_email", login), payload)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "update-phone",
	Description: "Change user phone number",
	Flags: []common.Flag{
		{Name: "login", Short: "l", Usage: "User login name", Required: true},
		{Name: "phone", Short: "p", Usage: "New phone number", Required: true},
		{Name: "code", Short: "c", Usage: "Verification code"},
	},
	Run: func(ctx *common.RuntimeContext) error {
		login, err := ctx.RequireArg("login")
		if err != nil {
			return err
		}
		phone, err := ctx.RequireArg("phone")
		if err != nil {
			return err
		}
		payload := map[string]interface{}{"phone": phone}
		if code := ctx.Arg("code"); code != "" {
			payload["code"] = code
		}
		env, err := ctx.CallAPI("PATCH", fmt.Sprintf("/v1/%s/update_phone", login), payload)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "send-verify",
	Description: "Send email verification code",
	Flags: []common.Flag{
		{Name: "login", Short: "l", Usage: "User login name", Required: true},
		{Name: "email", Short: "e", Usage: "Email address to verify"},
	},
	Run: func(ctx *common.RuntimeContext) error {
		login, err := ctx.RequireArg("login")
		if err != nil {
			return err
		}
		q := url.Values{}
		if email := ctx.Arg("email"); email != "" {
			q.Set("email", email)
		}
		env, err := ctx.CallAPIWithQuery("GET", fmt.Sprintf("/v1/%s/send_email_vefify_code", login), q)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "check-verify",
	Description: "Verify an email verification code",
	Flags: []common.Flag{
		{Name: "login", Short: "l", Usage: "User login name", Required: true},
		{Name: "code", Short: "c", Usage: "Verification code", Required: true},
		{Name: "email", Short: "e", Usage: "Email address"},
	},
	Run: func(ctx *common.RuntimeContext) error {
		login, err := ctx.RequireArg("login")
		if err != nil {
			return err
		}
		code, err := ctx.RequireArg("code")
		if err != nil {
			return err
		}
		payload := map[string]interface{}{"code": code}
		if email := ctx.Arg("email"); email != "" {
			payload["email"] = email
		}
		env, err := ctx.CallAPI("POST", fmt.Sprintf("/v1/%s/check_email_verify_code", login), payload)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "check-password",
	Description: "Verify user password is correct",
	Flags: []common.Flag{
		{Name: "login", Short: "l", Usage: "User login name", Required: true},
		{Name: "password", Short: "p", Usage: "Password to verify", Required: true},
	},
	Run: func(ctx *common.RuntimeContext) error {
		login, err := ctx.RequireArg("login")
		if err != nil {
			return err
		}
		password, err := ctx.RequireArg("password")
		if err != nil {
			return err
		}
		env, err := ctx.CallAPI("POST", fmt.Sprintf("/v1/%s/check_password", login), map[string]interface{}{"password": password})
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "check-email",
	Description: "Verify email address",
	Flags: []common.Flag{
		{Name: "login", Short: "l", Usage: "User login name", Required: true},
		{Name: "email", Short: "e", Usage: "Email to verify", Required: true},
	},
	Run: func(ctx *common.RuntimeContext) error {
		login, err := ctx.RequireArg("login")
		if err != nil {
			return err
		}
		email, err := ctx.RequireArg("email")
		if err != nil {
			return err
		}
		env, err := ctx.CallAPI("POST", fmt.Sprintf("/v1/%s/check_email", login), map[string]interface{}{"email": email})
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "can-delete",
	Description: "Check if user account can be deleted",
	Flags: []common.Flag{
		{Name: "login", Short: "l", Usage: "User login name", Required: true},
	},
	Run: func(ctx *common.RuntimeContext) error {
		login, err := ctx.RequireArg("login")
		if err != nil {
			return err
		}
		env, err := ctx.CallAPI("POST", fmt.Sprintf("/v1/%s/check_user_can_delete", login), nil)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
{
	Name:        "delete",
	Description: "Deactivate (delete) user account",
	Flags: []common.Flag{
		{Name: "login", Short: "l", Usage: "User login name", Required: true},
	},
	Run: func(ctx *common.RuntimeContext) error {
		login, err := ctx.RequireArg("login")
		if err != nil {
			return err
		}
		env, err := ctx.CallAPI("DELETE", fmt.Sprintf("/v1/%s", login), nil)
		if err != nil {
			return err
		}
		return ctx.Output(env)
	},
},
```

- [ ] **步骤 2：在 cmd/auth/auth.go 中添加 OAuth2 支持**

在 `NewAuthCmd` 函数中，`newLoginCmd` 调用前添加 `newRefreshCmd`：

在 `newLoginCmd` 函数中添加 `--oauth` 标志：

```go
func newLoginCmd() *cobra.Command {
	var tokenMode bool
	var oauthMode bool

	cmd := &cobra.Command{
		Use:   "login",
		Short: "Login to GitLink",
		RunE: func(cmd *cobra.Command, args []string) error {
			if tokenMode {
				return loginWithToken()
			}
			if oauthMode {
				return loginWithOAuth()
			}
			return loginWithPassword()
		},
	}
	cmd.Flags().BoolVar(&tokenMode, "token", false, "Login by pasting an existing token")
	cmd.Flags().BoolVar(&oauthMode, "oauth", false, "Login using OAuth2 password grant")
	return cmd
}
```

在 `loginWithToken` 函数后添加：

```go
func loginWithOAuth() error {
	reader := bufio.NewReader(os.Stdin)

	fmt.Print("Username/Email/Phone: ")
	username, _ := reader.ReadString('\n')
	username = strings.TrimSpace(username)

	fmt.Print("Password: ")
	passwordBytes, err := term.ReadPassword(int(syscall.Stdin))
	if err != nil {
		return fmt.Errorf("failed to read password: %w", err)
	}
	fmt.Println()

	result, err := internalAuth.LoginOAuth(username, string(passwordBytes))
	if err != nil {
		return fmt.Errorf("OAuth login failed: %w", err)
	}

	fmt.Printf("✓ Logged in via OAuth as %s\n", result.Login)
	return nil
}

func newRefreshCmd() *cobra.Command {
	return &cobra.Command{
		Use:   "refresh",
		Short: "Refresh OAuth2 token",
		RunE: func(cmd *cobra.Command, args []string) error {
			result, err := internalAuth.RefreshOAuthToken()
			if err != nil {
				return fmt.Errorf("token refresh failed: %w", err)
			}
			fmt.Printf("✓ Token refreshed (expires in %ds)\n", result.ExpiresIn)
			return nil
		},
	}
}
```

更新 `NewAuthCmd` 添加 refresh 子命令：

```go
func NewAuthCmd() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "auth",
		Short: "Authentication commands",
	}
	cmd.AddCommand(newLoginCmd())
	cmd.AddCommand(newLogoutCmd())
	cmd.AddCommand(newStatusCmd())
	cmd.AddCommand(newRefreshCmd())
	return cmd
}
```

- [ ] **步骤 3：在 internal/auth/login.go 中添加 OAuth2 函数**

追加：

```go
// OAuthTokenResult holds the OAuth2 token response.
type OAuthTokenResult struct {
	AccessToken string `json:"access_token"`
	TokenType   string `json:"token_type"`
	ExpiresIn   int    `json:"expires_in"`
	Login       string `json:"login"`
}

// LoginOAuth authenticates using OAuth2 password grant.
func LoginOAuth(username, password string) (*OAuthTokenResult, error) {
	cfg, err := config.Load()
	if err != nil {
		return nil, fmt.Errorf("failed to load config: %w", err)
	}

	// OAuth2 endpoint is at the base URL (not /api)
	baseURL := strings.TrimSuffix(cfg.BaseURL, "/api")
	data := url.Values{}
	data.Set("grant_type", "password")
	data.Set("username", username)
	data.Set("password", password)

	resp, err := http.PostForm(baseURL+"/oauth/token", data)
	if err != nil {
		return nil, fmt.Errorf("request failed: %w", err)
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("failed to read response: %w", err)
	}

	if resp.StatusCode != 200 {
		return nil, fmt.Errorf("OAuth error: %s", string(body))
	}

	var result struct {
		AccessToken string `json:"access_token"`
		TokenType   string `json:"token_type"`
		ExpiresIn   int    `json:"expires_in"`
	}
	if err := json.Unmarshal(body, &result); err != nil {
		return nil, fmt.Errorf("failed to parse response: %w", err)
	}

	if result.AccessToken == "" {
		return nil, fmt.Errorf("no access_token in response")
	}

	// Store the access_token like a regular token
	if err := StoreToken(result.AccessToken); err != nil {
		return nil, fmt.Errorf("failed to store token: %w", err)
	}

	// Get user info for display
	user, _ := GetCurrentUser()
	login := ""
	if user != nil {
		login, _ = user["login"].(string)
	}

	return &OAuthTokenResult{
		AccessToken: result.AccessToken,
		TokenType:   result.TokenType,
		ExpiresIn:   result.ExpiresIn,
		Login:       login,
	}, nil
}

// RefreshOAuthToken refreshes an existing OAuth2 token.
func RefreshOAuthToken() (*OAuthTokenResult, error) {
	cfg, err := config.Load()
	if err != nil {
		return nil, err
	}

	currentToken, err := LoadToken()
	if err != nil || currentToken == "" {
		return nil, fmt.Errorf("no stored token found; run 'gitlink-cli auth login --oauth' first")
	}

	baseURL := strings.TrimSuffix(cfg.BaseURL, "/api")
	data := url.Values{}
	data.Set("grant_type", "refresh_token")
	data.Set("refresh_token", currentToken)

	resp, err := http.PostForm(baseURL+"/oauth/token", data)
	if err != nil {
		return nil, fmt.Errorf("request failed: %w", err)
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}

	if resp.StatusCode != 200 {
		return nil, fmt.Errorf("refresh failed: %s", string(body))
	}

	var result struct {
		AccessToken string `json:"access_token"`
		TokenType   string `json:"token_type"`
		ExpiresIn   int    `json:"expires_in"`
	}
	if err := json.Unmarshal(body, &result); err != nil {
		return nil, err
	}

	if result.AccessToken == "" {
		return nil, fmt.Errorf("no access_token in refresh response")
	}

	if err := StoreToken(result.AccessToken); err != nil {
		return nil, fmt.Errorf("failed to store token: %w", err)
	}

	return &OAuthTokenResult{
		AccessToken: result.AccessToken,
		TokenType:   result.TokenType,
		ExpiresIn:   result.ExpiresIn,
	}, nil
}
```

注意：`internal/auth/login.go` 需要在 import 中追加 `"net/url"` 和确保 `"net/http"` 已存在（已存在）。

- [ ] **步骤 4：编译验证**

运行：`cd /c/Users/刘焱/Desktop/gitlink-cli && go build ./...`
预期：BUILD SUCCESS

- [ ] **步骤 5：Commit**

```bash
cd /c/Users/刘焱/Desktop/gitlink-cli
git add shortcuts/user/user.go cmd/auth/auth.go internal/auth/login.go
git commit -m "feat: add user account settings shortcuts and OAuth2 authentication"
```

---

## 任务 9：终端输出兼容性（L4）

**文件：**
- 修改：`internal/output/formatter.go`
- 修改：`cmd/root.go`

- [ ] **步骤 1：在 cmd/root.go 中添加 --color/--no-color 全局标志**

在 `cmd/root.go` 中（需先读取文件确认现有结构），在 PersistentPreRunE 或全局 flags 区域追加：

```go
var colorMode string // "auto", "always", "never"

// 在 rootCmd 的 PersistentFlags 中追加:
rootCmd.PersistentFlags().StringVar(&colorMode, "color", "auto", "Color output: auto, always, never")
```

在 `cmd/cmdutil/globals.go` 中追加导出变量：

```go
var ColorMode string
```

并在 root.go 的 PersistentPreRun 中设置 `cmdutil.ColorMode = colorMode`。

- [ ] **步骤 2：在 internal/output/formatter.go 中添加颜色检测和 CJK 宽度计算**

在文件顶部添加辅助函数：

```go
import (
	"os"
	"unicode"
)

// ShouldUseColor returns true if colorized output should be used.
func ShouldUseColor(mode string) bool {
	switch mode {
	case "always":
		return true
	case "never":
		return false
	default: // "auto"
		if os.Getenv("NO_COLOR") != "" {
			return false
		}
		if os.Getenv("TERM") == "dumb" {
			return false
		}
		// Windows Terminal and ConEmu support ANSI
		if os.Getenv("WT_SESSION") != "" || os.Getenv("ConEmuANSI") == "ON" {
			return true
		}
		if os.Getenv("COLORTERM") != "" {
			return true
		}
		// Non-Windows: assume ANSI support if TERM is set
		if os.Getenv("TERM") != "" && os.Getenv("GOOS") != "windows" {
			return true
		}
		return false
	}
}

// runeWidth returns the display width of a rune (2 for CJK, 1 for others).
func runeWidth(r rune) int {
	if unicode.Is(unicode.Han, r) {
		return 2
	}
	// Fullwidth forms, CJK compatibility
	if r >= 0xFF01 && r <= 0xFF60 {
		return 2
	}
	if r >= 0x3000 && r <= 0x303F {
		return 2
	}
	return 1
}

// stringWidth returns the display width of a string accounting for CJK characters.
func stringWidth(s string) int {
	w := 0
	for _, r := range s {
		w += runeWidth(r)
	}
	return w
}
```

- [ ] **步骤 3：编译验证**

运行：`cd /c/Users/刘焱/Desktop/gitlink-cli && go build ./...`
预期：BUILD SUCCESS

- [ ] **步骤 4：Commit**

```bash
cd /c/Users/刘焱/Desktop/gitlink-cli
git add internal/output/formatter.go cmd/root.go cmd/cmdutil/globals.go
git commit -m "feat: add color detection and CJK width support for cross-platform terminals"
```

---

## 任务 10：npm 安装体验改进（L4）

**文件：**
- 修改：`npm/scripts/install.js`
- 修改：`npm/bin/cli.js`

- [ ] **步骤 1：改进 npm/scripts/install.js — 添加 checksum 验证和更好的错误消息**

在 `npm/scripts/install.js` 中的下载函数中，在下载完成后、解压之前，添加 SHA256 校验逻辑。在 catch 块中改进错误消息：

在现有错误处理代码附近添加以下逻辑（具体行号需读取文件确认后精确插入）：

```javascript
// 在下载成功后添加 checksum 验证
const crypto = require('crypto');

async function verifyChecksum(filePath, expectedHash) {
  const hash = crypto.createHash('sha256');
  const stream = fs.createReadStream(filePath);
  return new Promise((resolve, reject) => {
    stream.on('data', (data) => hash.update(data));
    stream.on('end', () => {
      const actual = hash.digest('hex');
      if (actual !== expectedHash) {
        reject(new Error(`Checksum mismatch: expected ${expectedHash}, got ${actual}`));
      } else {
        resolve();
      }
    });
    stream.on('error', reject);
  });
}

// 改进错误消息分类
function classifyError(err) {
  const code = err.code || '';
  const msg = err.message || '';
  if (code === 'ENOTFOUND' || msg.includes('getaddrinfo')) {
    return 'DNS 解析失败，请检查网络连接';
  }
  if (code === 'ECONNREFUSED') {
    return '连接被拒绝，请检查代理设置 (HTTP_PROXY/HTTPS_PROXY)';
  }
  if (code === 'ETIMEDOUT' || code === 'ESOCKETTIMEDOUT') {
    return '连接超时，请尝试设置 HTTP_PROXY 环境变量';
  }
  if (code === 'EACCES' || code === 'EPERM') {
    return '权限不足，请使用管理员权限运行或检查安装目录';
  }
  if (code === 'ENOENT') {
    return '文件不存在，请检查安装路径';
  }
  return msg;
}
```

- [ ] **步骤 2：改进 npm/bin/cli.js — 添加版本检查和更好的错误处理**

在 `npm/bin/cli.js` 的主逻辑前添加：

```javascript
const https = require('https');
const fs = require('fs');
const path = require('path');

// 检查二进制文件是否存在
const binPath = path.join(__dirname, '..', 'bin', getBinaryName());
if (!fs.existsSync(binPath)) {
  console.error('gitlink-cli binary not found.');
  console.error('This may happen if the installation was incomplete.');
  console.error('Try reinstalling: npm install -g @gitlink-ai/cli');
  process.exit(1);
}

// 可选版本检查（可通过环境变量禁用）
if (!process.env.GITLINK_CLI_NO_UPDATE_CHECK) {
  checkForUpdate().catch(() => {}); // 静默失败
}

function checkForUpdate() {
  return new Promise((resolve, reject) => {
    const pkg = require('../package.json');
    const req = https.get('https://registry.npmjs.org/@gitlink-ai/cli/latest', (res) => {
      let data = '';
      res.on('data', (chunk) => data += chunk);
      res.on('end', () => {
        try {
          const latest = JSON.parse(data);
          if (latest.version && latest.version !== pkg.version) {
            console.error(`\n💡 A new version of gitlink-cli is available: ${latest.version} (current: ${pkg.version})`);
            console.error('   Run: npm update -g @gitlink-ai/cli\n');
          }
        } catch (e) { /* ignore */ }
        resolve();
      });
    });
    req.setTimeout(3000, () => { req.destroy(); reject(new Error('timeout')); });
    req.on('error', reject);
  });
}
```

- [ ] **步骤 3：Commit**

```bash
cd /c/Users/刘焱/Desktop/gitlink-cli
git add npm/scripts/install.js npm/bin/cli.js
git commit -m "improve: npm install checksum verification, error messages, and version check"
```

---

## 任务 11：包管理器分发配置（L4）

**文件：**
- 新建：`formula/gitlink-cli.rb`
- 新建：`bucket/gitlink-cli.json`
- 新建：`choco/gitlink-cli.nuspec`
- 新建：`choco/tools/chocolateyinstall.ps1`

- [ ] **步骤 1：创建 formula/gitlink-cli.rb**

```ruby
class GitlinkCli < Formula
  desc "CLI tool for GitLink platform"
  homepage "https://www.gitlink.org.cn/Gitlink/gitlink-cli"
  url "https://github.com/gitlink-org/gitlink-cli/releases/download/v0.1.13/gitlink-cli_0.1.13_darwin_arm64.tar.gz"
  sha256 "PLACEHOLDER_SHA256"
  version "0.1.13"

  def self.bin_filename
    Utils.safe_popen_read("uname", "-m").strip == "arm64" ? "gitlink-cli_0.1.13_darwin_arm64.tar.gz" : "gitlink-cli_0.1.13_darwin_amd64.tar.gz"
  end

  on_intel do
    url "https://github.com/gitlink-org/gitlink-cli/releases/download/v0.1.13/gitlink-cli_0.1.13_darwin_amd64.tar.gz"
    sha256 "PLACEHOLDER_SHA256_INTEL"
  end

  def install
    bin.install "gitlink-cli"
  end

  test do
    system "#{bin}/gitlink-cli", "version"
  end
end
```

- [ ] **步骤 2：创建 bucket/gitlink-cli.json**

```json
{
  "version": "0.1.13",
  "description": "CLI tool for GitLink platform",
  "homepage": "https://www.gitlink.org.cn/Gitlink/gitlink-cli",
  "license": "MulanPSL-2.0",
  "architecture": {
    "64bit": {
      "url": "https://github.com/gitlink-org/gitlink-cli/releases/download/v0.1.13/gitlink-cli_0.1.13_windows_amd64.zip",
      "hash": "PLACEHOLDER_SHA256"
    },
    "arm64": {
      "url": "https://github.com/gitlink-org/gitlink-cli/releases/download/v0.1.13/gitlink-cli_0.1.13_windows_arm64.zip",
      "hash": "PLACEHOLDER_SHA256_ARM64"
    }
  },
  "bin": "gitlink-cli.exe",
  "checkver": {
    "github": "https://github.com/gitlink-org/gitlink-cli"
  },
  "autoupdate": {
    "architecture": {
      "64bit": {
        "url": "https://github.com/gitlink-org/gitlink-cli/releases/download/v$version/gitlink-cli_$version_windows_amd64.zip"
      },
      "arm64": {
        "url": "https://github.com/gitlink-org/gitlink-cli/releases/download/v$version/gitlink-cli_$version_windows_arm64.zip"
      }
    },
    "hash": {
      "url": "https://github.com/gitlink-org/gitlink-cli/releases/download/v$version/checksums-sha256.txt"
    }
  }
}
```

- [ ] **步骤 3：创建 choco/gitlink-cli.nuspec**

```xml
<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2015/06/nuspec.xsd">
  <metadata>
    <id>gitlink-cli</id>
    <version>0.1.13</version>
    <title>GitLink CLI</title>
    <authors>GitLink</authors>
    <projectUrl>https://www.gitlink.org.cn/Gitlink/gitlink-cli</projectUrl>
    <licenseUrl>https://opensource.org/licenses/MulanPSL-2.0</licenseUrl>
    <description>CLI tool for GitLink platform</description>
    <tags>gitlink cli devops</tags>
  </metadata>
  <files>
    <file src="tools/**" target="tools" />
  </files>
</package>
```

- [ ] **步骤 4：创建 choco/tools/chocolateyinstall.ps1**

```powershell
$ErrorActionPreference = 'Stop'

$packageName = 'gitlink-cli'
$version = '0.1.13'
$url = "https://github.com/gitlink-org/gitlink-cli/releases/download/v$version/gitlink-cli_$version_windows_amd64.zip"
$checksum = 'PLACEHOLDER_SHA256'

$installDir = "$(Split-Path -parent $MyInvocation.MyCommand.Definition)"

$packageArgs = @{
  packageName   = $packageName
  url           = $url
  checksum      = $checksum
  checksumType  = 'sha256'
  unzipLocation = $installDir
}

Install-ChocolateyZipPackage @packageArgs
```

- [ ] **步骤 5：Commit**

```bash
cd /c/Users/刘焱/Desktop/gitlink-cli
mkdir -p formula bucket choco/tools
git add formula/ bucket/ choco/
git commit -m "infra: add Homebrew, Scoop, Chocolatey package manager configs"
```

---

## 任务 12：GitHub Actions CI 改进（L4）

**文件：**
- 新建：`.github/workflows/ci.yml`
- 修改：`.github/workflows/release.yml`

- [ ] **步骤 1：创建 .github/workflows/ci.yml**

```yaml
name: CI

on:
  push:
    branches: [master]
  pull_request:
    branches: [master]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-go@v5
        with:
          go-version-file: 'go.mod'

      - name: Vet
        run: go vet ./...

      - name: Test
        run: go test ./...

      - name: Build
        run: go build -ldflags "-s -w" .
```

- [ ] **步骤 2：在 release.yml 中添加自动更新包管理器配置的步骤**

在 `Create Release` 步骤后追加：

```yaml
      - name: Update package manager configs
        run: |
          VERSION=${GITHUB_REF#refs/tags/v}

          # Update Scoop manifest
          cd dist
          AMD64_HASH=$(sha256sum gitlink-cli_${VERSION}_windows_amd64.zip | cut -d' ' -f1)
          ARM64_HASH=$(sha256sum gitlink-cli_${VERSION}_windows_arm64.zip | cut -d' ' -f1)
          cd ..

          # Update bucket/gitlink-cli.json
          node <<NODE
          const fs = require('fs');
          const path = 'bucket/gitlink-cli.json';
          const manifest = JSON.parse(fs.readFileSync(path, 'utf8'));
          manifest.version = '${VERSION}';
          manifest.architecture['64bit'].url = 'https://github.com/gitlink-org/gitlink-cli/releases/download/v${VERSION}/gitlink-cli_${VERSION}_windows_amd64.zip';
          manifest.architecture['64bit'].hash = '${AMD64_HASH}';
          manifest.architecture['arm64'].url = 'https://github.com/gitlink-org/gitlink-cli/releases/download/v${VERSION}/gitlink-cli_${VERSION}_windows_arm64.zip';
          manifest.architecture['arm64'].hash = '${ARM64_HASH}';
          fs.writeFileSync(path, JSON.stringify(manifest, null, 2) + '\n');
          NODE
```

- [ ] **步骤 3：Commit**

```bash
cd /c/Users/刘焱/Desktop/gitlink-cli
git add .github/workflows/ci.yml .github/workflows/release.yml
git commit -m "ci: add PR testing workflow and auto-update package manager configs on release"
```

---

## 任务 13：最终验证

- [ ] **步骤 1：运行全部测试**

运行：`cd /c/Users/刘焱/Desktop/gitlink-cli && go test ./... -v`
预期：ALL PASS

- [ ] **步骤 2：编译最终二进制文件**

运行：`cd /c/Users/刘焱/Desktop/gitlink-cli && go build -ldflags "-s -w" .`
预期：BUILD SUCCESS，生成 `gitlink-cli` 二进制文件

- [ ] **步骤 3：验证快捷方式注册**

运行：`cd /c/Users/刘焱/Desktop/gitlink-cli && ./gitlink-cli help`
预期：显示所有命令组，包括 util, dataset, template, attachment

- [ ] **步骤 4：运行 go vet**

运行：`cd /c/Users/刘焱/Desktop/gitlink-cli && go vet ./...`
预期：无输出（无问题）

- [ ] **步骤 5：最终 Commit**

```bash
cd /c/Users/刘焱/Desktop/gitlink-cli
git add -A
git status  # 检查是否有遗漏
git commit -m "chore: final verification and cleanup"
```
