feat(wiki): add OpenAPI shortcuts

This commit is contained in:
wangyue789 2026-05-27 18:39:00 +08:00
parent 52b7093846
commit 053875b071
8 changed files with 580 additions and 1 deletions

View File

@ -96,6 +96,7 @@ The official [GitLink](https://www.gitlink.org.cn) CLI tool — built for humans
| Category | Capabilities |
|----------|-------------|
| 📦 Repo | List, create, fork, delete repositories, view repo info, insights, and interactions |
| Wiki | Create, update, view, list, and delete wiki pages |
| 🐛 Issue | Create, update, close, batch close, comment on issues |
| 🔖 Label | Create, list, update, delete issue labels |
| 🔀 PR | Create, merge, review pull requests, view changed files |
@ -246,6 +247,23 @@ gitlink-cli repo +create -n my-project -d "Project description"
gitlink-cli repo +fork --owner Gitlink --repo forgeplus
```
### Wiki Management
```bash
# List and view wiki pages
gitlink-cli wiki +pages --owner Gitlink --repo forgeplus --project-id 123
gitlink-cli wiki +view --owner Gitlink --repo forgeplus --project-id 123 --page Home
# Create or update wiki pages
gitlink-cli wiki +create --owner Gitlink --repo forgeplus \
--project-id 123 --page Home --title Home --content "Welcome to Wiki"
gitlink-cli wiki +update --owner Gitlink --repo forgeplus \
--project-id 123 --page Home --title Home --content "Updated content"
# Preview wiki page deletion
gitlink-cli wiki +delete --owner Gitlink --repo forgeplus --project-id 123 --page Home --dry-run
```
### Webhook Management
```bash
@ -638,6 +656,7 @@ See [skills/README.md](skills/README.md) for details.
|-------|-------------|
| `gitlink-shared` | Authentication, global parameters, safety rules, API notes |
| `gitlink-repo` | Repository operations (create, view, delete, fork, insights, etc.) |
| `gitlink-wiki` | Wiki operations (create, update, view, list, delete) |
| `gitlink-issue` | Issue operations (create, update, close, comment, etc.) |
| `gitlink-pr` | Pull request operations (create, merge, review, etc.) |
| `gitlink-member` | Repository member and invite link management |

View File

@ -96,6 +96,7 @@
| 分类 | 能力 |
|------|------|
| 📦 仓库 | 列出、创建、Fork、删除仓库查看仓库信息、洞察数据和互动状态 |
| Wiki | 创建、更新、查看、列出和删除 Wiki 页面 |
| 🐛 Issue | 创建、更新、关闭、批量关闭、评论 Issue |
| 🔖 标签 | 创建、列出、更新、删除 Issue 标签 |
| 🔀 PR | 创建、合并、Review Pull Request查看变更文件 |
@ -257,6 +258,23 @@ gitlink-cli repo +create -n my-project -d "项目描述"
gitlink-cli repo +fork --owner Gitlink --repo forgeplus
```
### Wiki 管理
```bash
# 列出和查看 Wiki 页面
gitlink-cli wiki +pages --owner Gitlink --repo forgeplus --project-id 123
gitlink-cli wiki +view --owner Gitlink --repo forgeplus --project-id 123 --page Home
# 创建或更新 Wiki 页面
gitlink-cli wiki +create --owner Gitlink --repo forgeplus \
--project-id 123 --page Home --title Home --content "Welcome to Wiki"
gitlink-cli wiki +update --owner Gitlink --repo forgeplus \
--project-id 123 --page Home --title Home --content "Updated content"
# 预览删除 Wiki 页面
gitlink-cli wiki +delete --owner Gitlink --repo forgeplus --project-id 123 --page Home --dry-run
```
### Webhook 管理
```bash
@ -517,6 +535,7 @@ git push gitlink
|-------|------|
| `gitlink-shared` | 认证、全局参数、安全规则、API 注意事项 |
| `gitlink-repo` | 仓库操作创建、查看、删除、Fork、洞察数据等 |
| `gitlink-wiki` | Wiki 操作(创建、更新、查看、列表、删除) |
| `gitlink-issue` | Issue 操作(创建、更新、关闭、评论等) |
| `gitlink-pr` | Pull Request 操作创建、合并、Review 等) |
| `gitlink-member` | 仓库成员与邀请链接管理 |

View File

@ -0,0 +1,24 @@
# Wiki OpenAPI Shortcuts
## Summary
This change adds a dedicated `wiki` shortcut group for GitLink Wiki OpenAPI
coverage. It includes read, create, update, and delete workflows.
## Added shortcuts
| Shortcut | API |
|----------|-----|
| `wiki +pages` | `GET /api/wiki/wikiPages` |
| `wiki +view` | `GET /api/wiki/getWiki` |
| `wiki +create` | `POST /api/wiki/createWiki` |
| `wiki +update` | `PUT /api/wiki/updateWiki` |
| `wiki +delete` | `DELETE /api/wiki/deleteWiki` |
`wiki +create` and `wiki +update` accept either raw `--content`, which the CLI
base64-encodes, or pre-encoded `--content-base64`. `wiki +delete` supports
`--dry-run` for safe previews.
## Submitter
Wang Yue

View File

@ -22,6 +22,7 @@ import (
"github.com/gitlink-org/gitlink-cli/shortcuts/search"
"github.com/gitlink-org/gitlink-cli/shortcuts/user"
"github.com/gitlink-org/gitlink-cli/shortcuts/webhook"
"github.com/gitlink-org/gitlink-cli/shortcuts/wiki"
"github.com/gitlink-org/gitlink-cli/shortcuts/workflow"
)
@ -49,6 +50,7 @@ func RegisterAll(root *cobra.Command, translators ...*i18n.Translator) {
"compare": compare.Shortcuts(),
"webhook": webhook.Shortcuts(tr),
"health": health.Shortcuts(tr),
"wiki": wiki.Shortcuts(),
"workflow": workflow.Shortcuts(),
}
@ -70,6 +72,7 @@ func RegisterAll(root *cobra.Command, translators ...*i18n.Translator) {
"compare": "Compare branches, tags, or commits",
"webhook": tr.T("cmd.webhook.short"),
"health": "Project health data collection",
"wiki": "Wiki operations",
"workflow": "AI agent workflow analysis",
}

View File

@ -14,7 +14,7 @@ func TestRegisterAll(t *testing.T) {
"repo", "issue", "label", "license", "pr", "release", "branch",
"org", "user", "search", "ci", "workflow",
"compare", "member", "milestone", "pipeline", "webhook",
"health",
"health", "wiki",
}
groupSet := map[string]bool{}

232
shortcuts/wiki/wiki.go Normal file
View File

@ -0,0 +1,232 @@
package wiki
import (
"encoding/base64"
"fmt"
"net/url"
"strconv"
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
)
func Shortcuts() []*common.Shortcut {
return []*common.Shortcut{
{
Name: "pages",
Description: "List repository wiki pages",
Flags: wikiProjectFlags(),
Run: func(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
q, err := wikiProjectQuery(ctx)
if err != nil {
return err
}
env, err := ctx.CallAPIWithQuery("GET", "/wiki/wikiPages", q)
if err != nil {
return err
}
return ctx.Output(env)
},
},
{
Name: "view",
Description: "Show a wiki page",
Flags: append(wikiProjectFlags(),
common.Flag{Name: "page", Short: "p", Usage: "Wiki page name", Required: true},
),
Run: func(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
q, err := wikiProjectQuery(ctx)
if err != nil {
return err
}
page, err := ctx.RequireArg("page")
if err != nil {
return err
}
q.Set("pageName", page)
env, err := ctx.CallAPIWithQuery("GET", "/wiki/getWiki", q)
if err != nil {
return err
}
return ctx.Output(env)
},
},
{
Name: "create",
Description: "Create a wiki page",
Flags: wikiWriteFlags(true),
Run: func(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
payload, err := wikiWritePayload(ctx, true)
if err != nil {
return err
}
env, err := ctx.CallAPI("POST", "/wiki/createWiki", payload)
if err != nil {
return err
}
return ctx.Output(env)
},
},
{
Name: "update",
Description: "Update a wiki page",
Flags: wikiWriteFlags(false),
Run: func(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
payload, err := wikiWritePayload(ctx, false)
if err != nil {
return err
}
env, err := ctx.CallAPI("PUT", "/wiki/updateWiki", payload)
if err != nil {
return err
}
return ctx.Output(env)
},
},
{
Name: "delete",
Description: "Delete a wiki page",
Flags: append(wikiProjectFlags(),
common.Flag{Name: "page", Short: "p", Usage: "Wiki page name", Required: true},
common.Flag{Name: "dry-run", Usage: "Preview the delete request without changing wiki state", Bool: true, Default: "false"},
),
Run: func(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
payload, err := wikiBasePayload(ctx)
if err != nil {
return err
}
page, err := ctx.RequireArg("page")
if err != nil {
return err
}
payload["pageName"] = page
if ctx.Arg("dry-run") == "true" {
return ctx.OutputData(map[string]interface{}{
"dry_run": true,
"action": "delete_wiki_page",
"method": "DELETE",
"path": "/wiki/deleteWiki",
"payload": payload,
})
}
env, err := ctx.CallAPI("DELETE", "/wiki/deleteWiki", payload)
if err != nil {
return err
}
return ctx.Output(env)
},
},
}
}
func wikiProjectFlags() []common.Flag {
return []common.Flag{
{Name: "project-id", Usage: "GitLink project ID", Required: true},
}
}
func wikiWriteFlags(contentRequired bool) []common.Flag {
return append(wikiProjectFlags(),
common.Flag{Name: "page", Short: "p", Usage: "Wiki page name", Required: true},
common.Flag{Name: "title", Short: "t", Usage: "Wiki page title", Required: true},
common.Flag{Name: "message", Short: "m", Usage: "Commit message"},
common.Flag{Name: "content", Usage: "Wiki content; encoded to base64 before sending"},
common.Flag{Name: "content-base64", Usage: "Pre-encoded wiki content"},
)
}
func wikiProjectQuery(ctx *common.RuntimeContext) (url.Values, error) {
projectID, err := wikiProjectID(ctx)
if err != nil {
return nil, err
}
q := url.Values{}
q.Set("owner", ctx.Owner)
q.Set("repo", ctx.Repo)
q.Set("projectId", strconv.Itoa(projectID))
return q, nil
}
func wikiWritePayload(ctx *common.RuntimeContext, contentRequired bool) (map[string]interface{}, error) {
payload, err := wikiBasePayload(ctx)
if err != nil {
return nil, err
}
page, err := ctx.RequireArg("page")
if err != nil {
return nil, err
}
title, err := ctx.RequireArg("title")
if err != nil {
return nil, err
}
content, ok, err := wikiContent(ctx, contentRequired)
if err != nil {
return nil, err
}
payload["pageName"] = page
payload["title"] = title
if message := ctx.Arg("message"); message != "" {
payload["message"] = message
}
if ok {
payload["content_base64"] = content
}
return payload, nil
}
func wikiBasePayload(ctx *common.RuntimeContext) (map[string]interface{}, error) {
projectID, err := wikiProjectID(ctx)
if err != nil {
return nil, err
}
return map[string]interface{}{
"owner": ctx.Owner,
"repo": ctx.Repo,
"projectId": projectID,
}, nil
}
func wikiProjectID(ctx *common.RuntimeContext) (int, error) {
value, err := ctx.RequireArg("project-id")
if err != nil {
return 0, err
}
id, err := strconv.Atoi(value)
if err != nil || id <= 0 {
return 0, fmt.Errorf("--project-id must be a positive integer")
}
return id, nil
}
func wikiContent(ctx *common.RuntimeContext, required bool) (string, bool, error) {
content := ctx.Arg("content")
encoded := ctx.Arg("content-base64")
if content != "" && encoded != "" {
return "", false, fmt.Errorf("--content cannot be used with --content-base64")
}
if content != "" {
return base64.StdEncoding.EncodeToString([]byte(content)), true, nil
}
if encoded != "" {
return encoded, true, nil
}
if required {
return "", false, fmt.Errorf("required flag --content is missing (or use --content-base64)")
}
return "", false, nil
}

218
shortcuts/wiki/wiki_test.go Normal file
View File

@ -0,0 +1,218 @@
package wiki
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/gitlink-org/gitlink-cli/internal/client"
"github.com/gitlink-org/gitlink-cli/internal/i18n"
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
)
func TestWikiShortcutsRouteToOpenAPIEndpoints(t *testing.T) {
cases := []struct {
name string
args map[string]string
method string
path string
query map[string]string
payload map[string]interface{}
}{
{
name: "pages",
args: map[string]string{"project-id": "123"},
method: "GET",
path: "/wiki/wikiPages.json",
query: map[string]string{"owner": "owner", "repo": "repo", "projectId": "123"},
},
{
name: "view",
args: map[string]string{"project-id": "123", "page": "Home"},
method: "GET",
path: "/wiki/getWiki.json",
query: map[string]string{"owner": "owner", "repo": "repo", "projectId": "123", "pageName": "Home"},
},
{
name: "create",
args: map[string]string{"project-id": "123", "page": "Home", "title": "Home", "message": "Add Home", "content": "hello"},
method: "POST",
path: "/wiki/createWiki.json",
payload: map[string]interface{}{
"owner": "owner",
"repo": "repo",
"projectId": float64(123),
"pageName": "Home",
"title": "Home",
"message": "Add Home",
"content_base64": "aGVsbG8=",
},
},
{
name: "update",
args: map[string]string{"project-id": "123", "page": "Home", "title": "Home", "content-base64": "dXBkYXRlZA=="},
method: "PUT",
path: "/wiki/updateWiki.json",
payload: map[string]interface{}{
"owner": "owner",
"repo": "repo",
"projectId": float64(123),
"pageName": "Home",
"title": "Home",
"content_base64": "dXBkYXRlZA==",
},
},
{
name: "delete",
args: map[string]string{"project-id": "123", "page": "Home"},
method: "DELETE",
path: "/wiki/deleteWiki.json",
payload: map[string]interface{}{
"owner": "owner",
"repo": "repo",
"projectId": float64(123),
"pageName": "Home",
},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
called := false
server := newWikiTestServer(t, func(w http.ResponseWriter, r *http.Request) {
if r.Method != tc.method || r.URL.Path != tc.path {
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
assertWikiQuery(t, r, tc.query)
if tc.payload != nil {
assertWikiPayload(t, r, tc.payload)
}
called = true
writeWikiJSON(t, w, map[string]interface{}{"message": "success", "data": map[string]interface{}{}})
})
defer server.Close()
err := runWikiShortcut(server, tc.name, tc.args)
if err != nil {
t.Fatalf("%s shortcut failed: %v", tc.name, err)
}
if !called {
t.Fatal("expected API request")
}
})
}
}
func TestWikiDeleteDryRunDoesNotCallAPI(t *testing.T) {
server := newWikiTestServer(t, func(w http.ResponseWriter, r *http.Request) {
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
})
defer server.Close()
err := runWikiShortcut(server, "delete", map[string]string{
"project-id": "123",
"page": "Home",
"dry-run": "true",
})
if err != nil {
t.Fatalf("delete dry-run failed: %v", err)
}
}
func TestWikiShortcutsValidateRequiredArgs(t *testing.T) {
server := newWikiTestServer(t, func(w http.ResponseWriter, r *http.Request) {
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
})
defer server.Close()
cases := []struct {
name string
args map[string]string
want string
}{
{name: "pages", args: map[string]string{}, want: "--project-id"},
{name: "pages", args: map[string]string{"project-id": "abc"}, want: "--project-id must be a positive integer"},
{name: "view", args: map[string]string{"project-id": "123"}, want: "--page"},
{name: "create", args: map[string]string{"project-id": "123", "page": "Home", "title": "Home"}, want: "--content"},
{name: "create", args: map[string]string{"project-id": "123", "page": "Home", "title": "Home", "content": "x", "content-base64": "eA=="}, want: "--content cannot be used with --content-base64"},
{name: "update", args: map[string]string{"project-id": "123", "page": "Home"}, want: "--title"},
{name: "delete", args: map[string]string{"project-id": "123"}, want: "--page"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
err := runWikiShortcut(server, tc.name, tc.args)
if err == nil {
t.Fatal("expected validation error")
}
if !strings.Contains(err.Error(), tc.want) {
t.Fatalf("error = %q, want it to mention %s", err.Error(), tc.want)
}
})
}
}
func runWikiShortcut(server *httptest.Server, name string, args map[string]string) error {
for _, shortcut := range Shortcuts() {
if shortcut.Name != name {
continue
}
ctx := &common.RuntimeContext{
Client: &client.Client{
HTTP: server.Client(),
BaseURL: server.URL,
},
Owner: "owner",
Repo: "repo",
Format: "json",
Args: args,
Tr: i18n.Default(),
}
return shortcut.Run(ctx)
}
return fmt.Errorf("shortcut %q not found", name)
}
func newWikiTestServer(t *testing.T, handler http.HandlerFunc) *httptest.Server {
t.Helper()
return httptest.NewServer(handler)
}
func assertWikiQuery(t *testing.T, r *http.Request, want map[string]string) {
t.Helper()
query := r.URL.Query()
if len(query) != len(want) {
t.Fatalf("query = %v, want %v", query, want)
}
for key, value := range want {
if got := query.Get(key); got != value {
t.Fatalf("query %s = %q, want %q", key, got, value)
}
}
}
func assertWikiPayload(t *testing.T, r *http.Request, want map[string]interface{}) {
t.Helper()
var got map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&got); err != nil {
t.Fatalf("decode request body: %v", err)
}
if len(got) != len(want) {
t.Fatalf("payload = %v, want %v", got, want)
}
for key, value := range want {
if got[key] != value {
t.Fatalf("payload %s = %v, want %v", key, got[key], value)
}
}
}
func writeWikiJSON(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)
}
}

View File

@ -0,0 +1,64 @@
---
name: gitlink-wiki
version: 1.0.0
description: "Wiki management: list, view, create, update, and delete GitLink wiki pages."
metadata:
requires:
bins: ["gitlink-cli"]
cliHelp: "gitlink-cli wiki --help"
---
# gitlink-wiki
Read `../gitlink-shared/SKILL.md` first for authentication, global flags, and safety rules.
Write and delete operations must be confirmed by the user before execution. Use
`wiki +delete --dry-run` before deleting a page.
## Shortcuts
| Shortcut | Purpose | Auth |
|----------|---------|------|
| `wiki +pages` | List wiki pages | No for public repos |
| `wiki +view` | Show one wiki page | No for public repos |
| `wiki +create` | Create a wiki page | Yes |
| `wiki +update` | Update a wiki page | Yes |
| `wiki +delete` | Delete a wiki page | Yes |
All shortcuts require `--project-id` because the Wiki OpenAPI requires the
GitLink project ID in addition to owner and repo.
`wiki +create` and `wiki +update` accept either `--content`, which the CLI
base64-encodes, or `--content-base64` for callers that already have encoded
content.
## Examples
```bash
gitlink-cli wiki +pages --owner Gitlink --repo forgeplus --project-id 123
gitlink-cli wiki +view --owner Gitlink --repo forgeplus --project-id 123 --page Home
gitlink-cli wiki +create --owner Gitlink --repo forgeplus \
--project-id 123 \
--page Home \
--title Home \
--content "Welcome to Wiki"
gitlink-cli wiki +update --owner Gitlink --repo forgeplus \
--project-id 123 \
--page Home \
--title Home \
--content "Updated content"
gitlink-cli wiki +delete --owner Gitlink --repo forgeplus --project-id 123 --page Home --dry-run
```
## API Mapping
| Shortcut | API |
|----------|-----|
| `wiki +pages` | `GET /api/wiki/wikiPages` |
| `wiki +view` | `GET /api/wiki/getWiki` |
| `wiki +create` | `POST /api/wiki/createWiki` |
| `wiki +update` | `PUT /api/wiki/updateWiki` |
| `wiki +delete` | `DELETE /api/wiki/deleteWiki` |