Merge PR #65: feat(wiki): add OpenAPI shortcuts

# Conflicts:
#	README.md
#	README.zh-CN.md
#	shortcuts/register.go
#	shortcuts/wiki/wiki.go
#	shortcuts/wiki/wiki_test.go
#	skills/gitlink-wiki/SKILL.md
This commit is contained in:
wbtiger 2026-07-14 22:50:11 +08:00
commit 32f9c027ba
8 changed files with 439 additions and 622 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 |
@ -106,9 +107,8 @@ The official [GitLink](https://www.gitlink.org.cn) CLI tool — built for humans
| 🔧 CI | View builds, logs, CI/CD operations |
| ⚙️ Pipeline | Run, inspect, enable, disable, delete pipeline workflows and logs |
| 🔔 Webhook | Manage repo webhooks and test deliveries |
| 📚 Catalog | List GitLink license and .gitignore templates for repository bootstrap |
| 🔍 Search | Search repositories, users |
| 👤 User | View profiles, manage public keys, inspect user statistics |
| 👤 User | View user profiles and info |
| 📋 PM | Sprint management, kanban boards, weekly reports |
| 🤖 Workflow | AI-powered issue triage, PR review, release notes |
@ -247,17 +247,21 @@ gitlink-cli repo +create -n my-project -d "Project description"
gitlink-cli repo +fork --owner Gitlink --repo forgeplus
```
### Platform Catalogs
### Wiki Management
```bash
# List license templates
gitlink-cli catalog +licenses
# 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
# Filter license templates by name
gitlink-cli catalog +licenses --name MIT
# 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"
# List .gitignore templates
gitlink-cli catalog +ignores --name Go
# Preview wiki page deletion
gitlink-cli wiki +delete --owner Gitlink --repo forgeplus --project-id 123 --page Home --dry-run
```
### Webhook Management
@ -297,16 +301,6 @@ gitlink-cli member +role --owner Gitlink --repo forgeplus --user-id 101 --role D
# Create an invite link
gitlink-cli member +invite-link --owner Gitlink --repo forgeplus --role developer --apply true
# List pending project membership applications
gitlink-cli member +applications --user Mengz --page 1 --per-page 20
# Accept or refuse a membership application by applied_projects[].id
gitlink-cli member +accept-application --user Mengz --id 42 --dry-run
gitlink-cli member +refuse-application --user Mengz --id 43 --dry-run
# Apply to join a project by application code
gitlink-cli member +apply --code <application_code> --role developer --dry-run
```
### Issue Management
@ -456,30 +450,6 @@ gitlink-cli release +update --owner Gitlink --repo forgeplus -i <version_id> -b
gitlink-cli release +delete --owner Gitlink --repo forgeplus -i <version_id> --dry-run
```
### Attachment Upload & Download
`attachment` gives a scriptable path for large-file transfer instead of the web UI. The uploaded attachment id can be fed to `release +create --attachment-ids`.
```bash
# Upload a local file as a platform attachment (returns the attachment id)
gitlink-cli attachment +upload -f ./dist/app-v1.0.0.tar.gz -d "v1.0.0 release asset"
# Upload several files concurrently (comma-separated; -c sets the worker count, default 3)
gitlink-cli attachment +upload -f ./dist/app.tar.gz,./dist/app.sha256,./dist/CHANGELOG.md -c 3
# Download an attachment by id to a local file
gitlink-cli attachment +download -i <attachment_id> -o ./app-v1.0.0.tar.gz
# Download all attachments of a release by tag (mirrors `gh release download`)
gitlink-cli release +download -i v1.0.0 -o ./assets
# Delete an attachment by id
gitlink-cli attachment +delete -i <attachment_id>
# Or do it in one step: upload local files and attach them to a new release
gitlink-cli release +create -t v1.0.0 -n "v1.0.0" --attachment-files ./dist/app.tar.gz,./dist/app.sha256
```
### CI/CD Operations
```bash
@ -525,24 +495,6 @@ gitlink-cli search +repos -k "machine learning"
gitlink-cli search +users -k "zhangsan"
```
### User Operations
```bash
# Current authenticated user and detailed profile
gitlink-cli user +me
gitlink-cli user +current --format json
# Public key management
gitlink-cli user +keys --page 1 --limit 20
gitlink-cli user +key-create --title "work laptop" --key-file ~/.ssh/id_rsa.pub --dry-run
gitlink-cli user +key-delete --id 7 --dry-run
# User statistics
gitlink-cli user +activity --login zhangsan
gitlink-cli user +headmap --login zhangsan --year 2026
gitlink-cli user +develop --login zhangsan --start-time 1704067200 --end-time 1735689599
```
### Workflow Agent Commands
`workflow` provides rule-based repository analysis for maintainers and AI Agents. It currently supports:
@ -704,7 +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-catalog` | License and .gitignore template lookup |
| `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 |
@ -714,7 +666,7 @@ See [skills/README.md](skills/README.md) for details.
| `gitlink-pipeline` | Pipeline workflow operations (runs, logs, enable, disable, delete, etc.) |
| `gitlink-search` | Search (repositories, users, etc.) |
| `gitlink-org` | Organization management (members, teams, etc.) |
| `gitlink-user` | User management (profiles, public keys, statistics, etc.) |
| `gitlink-user` | User management (profile info, etc.) |
| `gitlink-pm` | Project management (sprints, kanban, weekly reports, etc.) |
| `gitlink-workflow` | AI-powered workflows (issue triage, PR review, release notes, etc.) |
| `gitlink-health` | Project health analysis (PR/Issue metrics aggregation, health reports) |

View File

@ -96,6 +96,7 @@
| 分类 | 能力 |
|------|------|
| 📦 仓库 | 列出、创建、Fork、删除仓库查看仓库信息、洞察数据和互动状态 |
| Wiki | 创建、更新、查看、列出和删除 Wiki 页面 |
| 🐛 Issue | 创建、更新、关闭、批量关闭、评论 Issue |
| 🔖 标签 | 创建、列出、更新、删除 Issue 标签 |
| 🔀 PR | 创建、合并、Review Pull Request查看变更文件 |
@ -105,9 +106,8 @@
| 🏢 组织 | 管理组织、成员、团队 |
| 🔧 CI | 查看构建、日志、CI/CD 操作 |
| ⚙️ Pipeline | 运行、查看、启停、删除流水线工作流并查询日志 |
| 📚 模板目录 | 查询 GitLink 许可证和 .gitignore 模板,用于仓库初始化 |
| 🔍 搜索 | 搜索仓库、用户 |
| 👤 用户 | 查看资料、管理 Public Keys、查询用户统计 |
| 👤 用户 | 查看用户资料和信息 |
| 📋 项目管理 | Sprint 管理、看板、周报 |
| 🤖 工作流 | AI 驱动的 Issue 分类、PR Review、Release Notes |
@ -258,17 +258,21 @@ gitlink-cli repo +create -n my-project -d "项目描述"
gitlink-cli repo +fork --owner Gitlink --repo forgeplus
```
### 平台模板目录
### Wiki 管理
```bash
# 列出许可证模板
gitlink-cli catalog +licenses
# 列出和查看 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
# 按名称筛选许可证模板
gitlink-cli catalog +licenses --name MIT
# 创建或更新 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"
# 查询 .gitignore 模板
gitlink-cli catalog +ignores --name Go
# 预览删除 Wiki 页面
gitlink-cli wiki +delete --owner Gitlink --repo forgeplus --project-id 123 --page Home --dry-run
```
### Webhook 管理
@ -308,16 +312,6 @@ gitlink-cli member +role --owner Gitlink --repo forgeplus --user-id 101 --role D
# 生成邀请链接
gitlink-cli member +invite-link --owner Gitlink --repo forgeplus --role developer --apply true
# 查看待处理的项目成员申请
gitlink-cli member +applications --user Mengz --page 1 --per-page 20
# 按 applied_projects[].id 接受或拒绝成员申请
gitlink-cli member +accept-application --user Mengz --id 42 --dry-run
gitlink-cli member +refuse-application --user Mengz --id 43 --dry-run
# 通过申请码申请加入项目
gitlink-cli member +apply --code <application_code> --role developer --dry-run
```
### Issue 管理
@ -447,30 +441,6 @@ gitlink-cli release +update --owner Gitlink --repo forgeplus -i <version_id> -b
gitlink-cli release +delete --owner Gitlink --repo forgeplus -i <version_id> --dry-run
```
### 附件上传与下载
`attachment` 为大文件传输提供可脚本化的 CLI 通道(不必走网页端)。上传返回的附件 id 可直接用于 `release +create --attachment-ids`
```bash
# 上传本地文件为平台附件(返回附件 id
gitlink-cli attachment +upload -f ./dist/app-v1.0.0.tar.gz -d "v1.0.0 发布产物"
# 多文件并发上传(逗号分隔;-c 指定并发数,默认 3
gitlink-cli attachment +upload -f ./dist/app.tar.gz,./dist/app.sha256,./dist/CHANGELOG.md -c 3
# 按 id 下载附件到本地文件
gitlink-cli attachment +download -i <attachment_id> -o ./app-v1.0.0.tar.gz
# 按 tag 一步下载发行版全部附件(对标 `gh release download`
gitlink-cli release +download -i v1.0.0 -o ./assets
# 按 id 删除附件
gitlink-cli attachment +delete -i <attachment_id>
# 一步到位:上传本地文件并附加到新发行版
gitlink-cli release +create -t v1.0.0 -n "v1.0.0" --attachment-files ./dist/app.tar.gz,./dist/app.sha256
```
### 流水线管理
```bash
@ -503,24 +473,6 @@ gitlink-cli search +repos -k "machine learning"
gitlink-cli search +users -k "zhangsan"
```
### 用户操作
```bash
# 当前登录用户和详细资料
gitlink-cli user +me
gitlink-cli user +current --format json
# Public Key 管理
gitlink-cli user +keys --page 1 --limit 20
gitlink-cli user +key-create --title "work laptop" --key-file ~/.ssh/id_rsa.pub --dry-run
gitlink-cli user +key-delete --id 7 --dry-run
# 用户统计
gitlink-cli user +activity --login zhangsan
gitlink-cli user +headmap --login zhangsan --year 2026
gitlink-cli user +develop --login zhangsan --start-time 1704067200 --end-time 1735689599
```
### Raw API
Shortcuts 未覆盖的接口可通过 Raw API 直接调用:
@ -583,7 +535,7 @@ git push gitlink
|-------|------|
| `gitlink-shared` | 认证、全局参数、安全规则、API 注意事项 |
| `gitlink-repo` | 仓库操作创建、查看、删除、Fork、洞察数据等 |
| `gitlink-catalog` | 许可证和 .gitignore 模板查询 |
| `gitlink-wiki` | Wiki 操作(创建、更新、查看、列表、删除) |
| `gitlink-issue` | Issue 操作(创建、更新、关闭、评论等) |
| `gitlink-pr` | Pull Request 操作创建、合并、Review 等) |
| `gitlink-member` | 仓库成员与邀请链接管理 |
@ -592,7 +544,7 @@ git push gitlink
| `gitlink-ci` | CI/CD 操作(构建、日志等) |
| `gitlink-pipeline` | 流水线工作流操作(运行、日志、启停、删除等) |
| `gitlink-search` | 搜索功能(仓库、用户等) |
| `gitlink-user` | 用户管理(资料、Public Keys、统计等) |
| `gitlink-user` | 用户管理(个人信息等) |
| `gitlink-pm` | 项目管理Sprint、看板、周报等 |
| `gitlink-workflow` | AI 自动化工作流Issue 分类、PR Review、Release Notes 等) |

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

@ -5,13 +5,10 @@ import (
"github.com/gitlink-org/gitlink-cli/internal/i18n"
"github.com/gitlink-org/gitlink-cli/shortcuts/branch"
"github.com/gitlink-org/gitlink-cli/shortcuts/catalog"
"github.com/gitlink-org/gitlink-cli/shortcuts/ci"
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
"github.com/gitlink-org/gitlink-cli/shortcuts/compare"
"github.com/gitlink-org/gitlink-cli/shortcuts/dataset"
"github.com/gitlink-org/gitlink-cli/shortcuts/health"
"github.com/gitlink-org/gitlink-cli/shortcuts/ignore"
"github.com/gitlink-org/gitlink-cli/shortcuts/issue"
"github.com/gitlink-org/gitlink-cli/shortcuts/label"
"github.com/gitlink-org/gitlink-cli/shortcuts/license"
@ -20,7 +17,6 @@ import (
"github.com/gitlink-org/gitlink-cli/shortcuts/org"
"github.com/gitlink-org/gitlink-cli/shortcuts/pipeline"
"github.com/gitlink-org/gitlink-cli/shortcuts/pr"
"github.com/gitlink-org/gitlink-cli/shortcuts/profile"
"github.com/gitlink-org/gitlink-cli/shortcuts/release"
"github.com/gitlink-org/gitlink-cli/shortcuts/repo"
"github.com/gitlink-org/gitlink-cli/shortcuts/search"
@ -38,54 +34,46 @@ func RegisterAll(root *cobra.Command, translators ...*i18n.Translator) {
}
groups := map[string][]*common.Shortcut{
"repo": repo.Shortcuts(tr),
"catalog": catalog.Shortcuts(tr),
"issue": issue.Shortcuts(tr),
"label": label.Shortcuts(tr),
"license": license.Shortcuts(tr),
"member": member.Shortcuts(tr),
"milestone": milestone.Shortcuts(tr),
"pipeline": pipeline.Shortcuts(tr),
"label": label.Shortcuts(),
"license": license.Shortcuts(),
"member": member.Shortcuts(),
"milestone": milestone.Shortcuts(),
"pipeline": pipeline.Shortcuts(),
"pr": pr.Shortcuts(tr),
"profile": profile.Shortcuts(tr),
"release": release.Shortcuts(tr),
"branch": branch.Shortcuts(tr),
"org": org.Shortcuts(tr),
"user": user.Shortcuts(tr),
"search": search.Shortcuts(tr),
"ci": ci.Shortcuts(tr),
"compare": compare.Shortcuts(tr),
"dataset": dataset.Shortcuts(tr),
"compare": compare.Shortcuts(),
"webhook": webhook.Shortcuts(tr),
"wiki": wiki.Shortcuts(tr),
"health": health.Shortcuts(tr),
"ignore": ignore.Shortcuts(tr),
"workflow": workflow.Shortcuts(tr),
"wiki": wiki.Shortcuts(),
"workflow": workflow.Shortcuts(),
}
descriptions := map[string]string{
"repo": tr.T("cmd.repo.short"),
"catalog": tr.T("cmd.catalog.short"),
"issue": tr.T("cmd.issue.short"),
"label": tr.T("cmd.label.short"),
"license": tr.T("cmd.license.short"),
"member": tr.T("cmd.member.short"),
"milestone": tr.T("cmd.milestone.short"),
"pipeline": tr.T("cmd.pipeline.short"),
"label": "Issue label operations",
"license": "License operations",
"member": "Repository member operations",
"milestone": "Milestone operations",
"pipeline": "Pipeline operations",
"pr": tr.T("cmd.pr.short"),
"profile": tr.T("cmd.profile.short"),
"release": tr.T("cmd.release.short"),
"branch": tr.T("cmd.branch.short"),
"org": tr.T("cmd.org.short"),
"user": tr.T("cmd.user.short"),
"search": tr.T("cmd.search.short"),
"ci": tr.T("cmd.ci.short"),
"compare": tr.T("cmd.compare.short"),
"dataset": tr.T("cmd.dataset.short"),
"compare": "Compare branches, tags, or commits",
"webhook": tr.T("cmd.webhook.short"),
"wiki": tr.T("cmd.wiki.short"),
"health": tr.T("cmd.health.short"),
"ignore": tr.T("cmd.ignore.short"),
"workflow": tr.T("cmd.workflow.short"),
"health": "Project health data collection",
"wiki": "Wiki operations",
"workflow": "AI agent workflow analysis",
}
for name, shortcuts := range groups {

View File

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

View File

@ -4,55 +4,26 @@ import (
"encoding/base64"
"fmt"
"net/url"
"strconv"
"github.com/gitlink-org/gitlink-cli/internal/config"
"github.com/gitlink-org/gitlink-cli/internal/i18n"
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
)
// switchToGateway overrides the client base URL with the gateway URL from config.
func switchToGateway(ctx *common.RuntimeContext) error {
cfg, err := config.Load()
if err != nil {
return err
}
if cfg.GatewayURL == "" {
cfg.GatewayURL = config.DefaultGatewayURL
}
ctx.Client.BaseURL = cfg.GatewayURL
return nil
}
// gatewayFlag returns the common --gateway flag definition.
func gatewayFlag(tr *i18n.Translator) common.Flag {
return common.Flag{Name: "gateway", Short: "g", Usage: tr.T("flag.wiki.gateway"), Bool: true}
}
// Shortcuts returns all wiki shortcuts.
func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
tr := shortcutTranslator(translators...)
func Shortcuts() []*common.Shortcut {
return []*common.Shortcut{
{
Name: "list",
Description: tr.T("cmd.wiki.list.short"),
Flags: []common.Flag{
{Name: "project-id", Usage: tr.T("flag.wiki.project_id"), Required: true},
gatewayFlag(tr),
},
Name: "pages",
Description: "List repository wiki pages",
Flags: wikiProjectFlags(),
Run: func(ctx *common.RuntimeContext) error {
if ctx.Arg("gateway") == "true" {
if err := switchToGateway(ctx); err != nil {
return err
}
}
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
q := url.Values{}
q.Set("owner", ctx.Owner)
q.Set("repo", ctx.Repo)
q.Set("projectId", ctx.Arg("project-id"))
env, err := ctx.CallAPIWithQuery("GET", "/wiki/open/wikiPages", q)
q, err := wikiProjectQuery(ctx)
if err != nil {
return err
}
env, err := ctx.CallAPIWithQuery("GET", "/wiki/wikiPages", q)
if err != nil {
return err
}
@ -61,27 +32,24 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
},
{
Name: "view",
Description: tr.T("cmd.wiki.view.short"),
Flags: []common.Flag{
{Name: "project-id", Usage: tr.T("flag.wiki.project_id"), Required: true},
{Name: "page-name", Short: "n", Usage: tr.T("flag.wiki.page_name"), Required: true},
gatewayFlag(tr),
},
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 ctx.Arg("gateway") == "true" {
if err := switchToGateway(ctx); err != nil {
return err
}
}
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
q := url.Values{}
q.Set("owner", ctx.Owner)
q.Set("repo", ctx.Repo)
q.Set("projectId", ctx.Arg("project-id"))
q.Set("pageName", ctx.Arg("page-name"))
env, err := ctx.CallAPIWithQuery("GET", "/wiki/open/getWiki", q)
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
}
@ -90,35 +58,17 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
},
{
Name: "create",
Description: tr.T("cmd.wiki.create.short"),
Flags: []common.Flag{
{Name: "project-id", Usage: tr.T("flag.wiki.project_id"), Required: true},
{Name: "page-name", Short: "n", Usage: tr.T("flag.wiki.page_name"), Required: true},
{Name: "title", Short: "t", Usage: tr.T("flag.wiki.title"), Required: true},
{Name: "content", Short: "c", Usage: tr.T("flag.wiki.content"), Required: true},
{Name: "message", Short: "m", Usage: tr.T("flag.wiki.message")},
gatewayFlag(tr),
},
Description: "Create a wiki page",
Flags: wikiWriteFlags(true),
Run: func(ctx *common.RuntimeContext) error {
if ctx.Arg("gateway") == "true" {
if err := switchToGateway(ctx); err != nil {
return err
}
}
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
content := ctx.Arg("content")
payload := map[string]interface{}{
"owner": ctx.Owner,
"repo": ctx.Repo,
"projectId": ctx.Arg("project-id"),
"pageName": ctx.Arg("page-name"),
"title": ctx.Arg("title"),
"content_base64": base64.StdEncoding.EncodeToString([]byte(content)),
"message": ctx.Arg("message"),
payload, err := wikiWritePayload(ctx, true)
if err != nil {
return err
}
env, err := ctx.CallAPI("POST", "/wiki/open/createWiki", payload)
env, err := ctx.CallAPI("POST", "/wiki/createWiki", payload)
if err != nil {
return err
}
@ -127,41 +77,17 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
},
{
Name: "update",
Description: tr.T("cmd.wiki.update.short"),
Flags: []common.Flag{
{Name: "project-id", Usage: tr.T("flag.wiki.project_id"), Required: true},
{Name: "page-name", Short: "n", Usage: tr.T("flag.wiki.page_name"), Required: true},
{Name: "title", Short: "t", Usage: tr.T("flag.wiki.title"), Required: true},
{Name: "content", Short: "c", Usage: tr.T("flag.wiki.content")},
{Name: "message", Short: "m", Usage: tr.T("flag.wiki.message")},
gatewayFlag(tr),
},
Description: "Update a wiki page",
Flags: wikiWriteFlags(false),
Run: func(ctx *common.RuntimeContext) error {
if ctx.Arg("gateway") == "true" {
if err := switchToGateway(ctx); err != nil {
return err
}
}
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
title := ctx.Arg("title")
if title == "" {
return fmt.Errorf("--title is required")
payload, err := wikiWritePayload(ctx, false)
if err != nil {
return err
}
content := ctx.Arg("content")
payload := map[string]interface{}{
"owner": ctx.Owner,
"repo": ctx.Repo,
"projectId": ctx.Arg("project-id"),
"pageName": ctx.Arg("page-name"),
"title": title,
"message": ctx.Arg("message"),
}
if content != "" {
payload["content_base64"] = base64.StdEncoding.EncodeToString([]byte(content))
}
env, err := ctx.CallAPI("PUT", "/wiki/open/updateWiki", payload)
env, err := ctx.CallAPI("PUT", "/wiki/updateWiki", payload)
if err != nil {
return err
}
@ -170,28 +96,34 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
},
{
Name: "delete",
Description: tr.T("cmd.wiki.delete.short"),
Flags: []common.Flag{
{Name: "project-id", Usage: tr.T("flag.wiki.project_id"), Required: true},
{Name: "page-name", Short: "n", Usage: tr.T("flag.wiki.page_name"), Required: true},
gatewayFlag(tr),
},
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 ctx.Arg("gateway") == "true" {
if err := switchToGateway(ctx); err != nil {
return err
}
}
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
payload := map[string]interface{}{
"owner": ctx.Owner,
"repo": ctx.Repo,
"projectId": ctx.Arg("project-id"),
"pageName": ctx.Arg("page-name"),
payload, err := wikiBasePayload(ctx)
if err != nil {
return err
}
env, err := ctx.CallAPI("DELETE", "/wiki/open/deleteWiki", payload)
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
}
@ -201,9 +133,100 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
}
}
func shortcutTranslator(translators ...*i18n.Translator) *i18n.Translator {
if len(translators) > 0 && translators[0] != nil {
return translators[0]
func wikiProjectFlags() []common.Flag {
return []common.Flag{
{Name: "project-id", Usage: "GitLink project ID", Required: true},
}
return i18n.Default()
}
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
}

View File

@ -1,7 +1,6 @@
package wiki
import (
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
@ -10,316 +9,210 @@ import (
"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"
)
// --- list ---
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",
},
},
}
func TestWikiList(t *testing.T) {
callCount := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
callCount++
switch {
case r.Method == "GET" && r.URL.Path == "/owner/repo.json":
writeJSON(t, w, map[string]interface{}{"id": float64(42)})
case r.Method == "GET" && r.URL.Path == "/wiki/open/wikiPages":
if r.URL.Query().Get("projectId") != "42" {
t.Fatalf("expected projectId=42, got %s", r.URL.Query().Get("projectId"))
}
writeJSON(t, w, map[string]interface{}{
"data": []interface{}{
map[string]interface{}{"title": "Home", "sub_url": "Home"},
map[string]interface{}{"title": "Guide", "sub_url": "Guide"},
},
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{}{}})
})
default:
t.Fatalf("unexpected request #%d: %s %s", callCount, r.Method, r.URL.Path)
}
}))
defer server.Close()
defer server.Close()
if err := runWikiShortcut(t, server, "list", map[string]string{}); err != nil {
t.Fatalf("list failed: %v", err)
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 TestWikiListWithProjectID(t *testing.T) {
callCount := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
callCount++
switch {
case r.Method == "GET" && r.URL.Path == "/owner/repo.json":
writeJSON(t, w, map[string]interface{}{"project_id": float64(99)})
case r.Method == "GET" && r.URL.Path == "/wiki/open/wikiPages":
if r.URL.Query().Get("projectId") != "99" {
t.Fatalf("expected projectId=99, got %s", r.URL.Query().Get("projectId"))
}
writeJSON(t, w, map[string]interface{}{"data": []interface{}{}})
default:
t.Fatalf("unexpected request #%d: %s %s", callCount, r.Method, r.URL.Path)
}
}))
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()
if err := runWikiShortcut(t, server, "list", map[string]string{}); err != nil {
t.Fatalf("list with project_id failed: %v", err)
}
}
// --- view ---
func TestWikiView(t *testing.T) {
callCount := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
callCount++
switch {
case r.Method == "GET" && r.URL.Path == "/owner/repo.json":
writeJSON(t, w, map[string]interface{}{"id": float64(42)})
case r.Method == "GET" && r.URL.Path == "/wiki/open/getWiki":
if r.URL.Query().Get("pageName") != "Home" {
t.Fatalf("expected pageName=Home, got %s", r.URL.Query().Get("pageName"))
}
writeJSON(t, w, map[string]interface{}{
"data": map[string]interface{}{
"title": "Home",
"content_base64": base64.StdEncoding.EncodeToString([]byte("Welcome to wiki")),
},
})
default:
t.Fatalf("unexpected request #%d: %s %s", callCount, r.Method, r.URL.Path)
}
}))
defer server.Close()
err := runWikiShortcut(t, server, "view", map[string]string{"name": "Home"})
if err != nil {
t.Fatalf("view failed: %v", err)
}
}
func TestWikiViewRequiresName(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Fatal("no request should be made without --name")
}))
defer server.Close()
err := runWikiShortcut(t, server, "view", map[string]string{})
if err == nil {
t.Fatal("expected error for missing --name")
}
}
// --- create ---
func TestWikiCreate(t *testing.T) {
callCount := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
callCount++
switch {
case r.Method == "GET" && r.URL.Path == "/owner/repo.json":
writeJSON(t, w, map[string]interface{}{"id": float64(42)})
case r.Method == "POST" && r.URL.Path == "/wiki/open/createWiki":
var payload map[string]interface{}
json.NewDecoder(r.Body).Decode(&payload)
if payload["pageName"] != "NewPage" {
t.Fatalf("expected pageName=NewPage, got %v", payload["pageName"])
}
if payload["title"] != "NewPage" {
t.Fatalf("expected title=NewPage, got %v", payload["title"])
}
if payload["owner"] != "owner" {
t.Fatalf("expected owner=owner, got %v", payload["owner"])
}
if payload["repo"] != "repo" {
t.Fatalf("expected repo=repo, got %v", payload["repo"])
}
if payload["projectId"].(float64) != 42 {
t.Fatalf("expected projectId=42, got %v", payload["projectId"])
}
expectedContent := base64.StdEncoding.EncodeToString([]byte("Hello Wiki!"))
if payload["content_base64"] != expectedContent {
t.Fatalf("content_base64 mismatch: got %v", payload["content_base64"])
}
writeJSON(t, w, map[string]interface{}{
"code": 201,
"data": map[string]interface{}{"title": "NewPage"},
})
default:
t.Fatalf("unexpected request #%d: %s %s", callCount, r.Method, r.URL.Path)
}
}))
defer server.Close()
err := runWikiShortcut(t, server, "create", map[string]string{
"name": "NewPage", "content": "Hello Wiki!", "message": "create page",
err := runWikiShortcut(server, "delete", map[string]string{
"project-id": "123",
"page": "Home",
"dry-run": "true",
})
if err != nil {
t.Fatalf("create failed: %v", err)
t.Fatalf("delete dry-run failed: %v", err)
}
}
func TestWikiCreateRequiresName(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Fatal("no request should be made without --name")
}))
defer server.Close()
err := runWikiShortcut(t, server, "create", map[string]string{"content": "test"})
if err == nil {
t.Fatal("expected error for missing --name")
}
}
func TestWikiCreateRequiresContent(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Fatal("no request should be made without --content")
}))
defer server.Close()
err := runWikiShortcut(t, server, "create", map[string]string{"name": "Test"})
if err == nil {
t.Fatal("expected error for missing --content")
}
}
// --- update ---
func TestWikiUpdate(t *testing.T) {
callCount := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
callCount++
switch {
case r.Method == "GET" && r.URL.Path == "/owner/repo.json":
writeJSON(t, w, map[string]interface{}{"id": float64(42)})
case r.Method == "PUT" && r.URL.Path == "/wiki/open/updateWiki":
var payload map[string]interface{}
json.NewDecoder(r.Body).Decode(&payload)
if payload["pageName"] != "Home" {
t.Fatalf("expected pageName=Home, got %v", payload["pageName"])
}
if payload["title"] != "Home" {
t.Fatalf("expected title=Home, got %v", payload["title"])
}
if payload["message"] != "update page" {
t.Fatalf("expected message=update page, got %v", payload["message"])
}
writeJSON(t, w, map[string]interface{}{"code": 200})
default:
t.Fatalf("unexpected request #%d: %s %s", callCount, r.Method, r.URL.Path)
}
}))
defer server.Close()
err := runWikiShortcut(t, server, "update", map[string]string{
"name": "Home", "content": "Updated content", "message": "update page",
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)
})
if err != nil {
t.Fatalf("update failed: %v", err)
}
}
func TestWikiUpdateRequiresName(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Fatal("no request should be made without --name")
}))
defer server.Close()
err := runWikiShortcut(t, server, "update", map[string]string{"content": "test"})
if err == nil {
t.Fatal("expected error for missing --name")
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)
}
})
}
}
// --- delete ---
func TestWikiDelete(t *testing.T) {
callCount := 0
sidebarContent := "[[Home]]\n[[OldPage]]\n[[Guide]]"
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
callCount++
switch {
case r.Method == "GET" && r.URL.Path == "/owner/repo.json":
writeJSON(t, w, map[string]interface{}{"id": float64(42)})
case r.Method == "DELETE" && r.URL.Path == "/wiki/open/deleteWiki":
var payload map[string]interface{}
json.NewDecoder(r.Body).Decode(&payload)
if payload["pageName"] != "OldPage" {
t.Fatalf("expected pageName=OldPage, got %v", payload["pageName"])
}
writeJSON(t, w, map[string]interface{}{"code": 200})
case r.Method == "GET" && r.URL.Path == "/wiki/open/getWiki":
if r.URL.Query().Get("pageName") != "_Sidebar" {
t.Fatalf("expected pageName=_Sidebar, got %s", r.URL.Query().Get("pageName"))
}
// Return sidebar with the page still in it
writeJSON(t, w, map[string]interface{}{
"code": 200,
"data": fmt.Sprintf(`{"content_base64":"%s"}`, base64.StdEncoding.EncodeToString([]byte(sidebarContent))),
})
case r.Method == "PUT" && r.URL.Path == "/wiki/open/updateWiki":
var payload map[string]interface{}
json.NewDecoder(r.Body).Decode(&payload)
if payload["pageName"] != "_Sidebar" {
t.Fatalf("expected pageName=_Sidebar, got %v", payload["pageName"])
}
// Verify OldPage is removed from sidebar
updated, _ := base64.StdEncoding.DecodeString(payload["content_base64"].(string))
if strings.Contains(string(updated), "[[OldPage]]") {
t.Fatal("sidebar should not contain [[OldPage]] after delete")
}
if !strings.Contains(string(updated), "[[Home]]") {
t.Fatal("sidebar should still contain [[Home]]")
}
writeJSON(t, w, map[string]interface{}{"code": 200})
default:
t.Fatalf("unexpected request #%d: %s %s", callCount, r.Method, r.URL.Path)
func runWikiShortcut(server *httptest.Server, name string, args map[string]string) error {
for _, shortcut := range Shortcuts() {
if shortcut.Name != name {
continue
}
}))
defer server.Close()
err := runWikiShortcut(t, server, "delete", map[string]string{"name": "OldPage"})
if err != nil {
t.Fatalf("delete failed: %v", err)
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 TestWikiDeleteRequiresName(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Fatal("no request should be made without --name")
}))
defer server.Close()
err := runWikiShortcut(t, server, "delete", map[string]string{})
if err == nil {
t.Fatal("expected error for missing --name")
}
}
// --- helpers ---
func runWikiShortcut(t *testing.T, server *httptest.Server, name string, args map[string]string) error {
func newWikiTestServer(t *testing.T, handler http.HandlerFunc) *httptest.Server {
t.Helper()
shortcut := findWikiShortcut(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)
return httptest.NewServer(handler)
}
func findWikiShortcut(t *testing.T, name string) *common.Shortcut {
func assertWikiQuery(t *testing.T, r *http.Request, want map[string]string) {
t.Helper()
for _, s := range Shortcuts() {
if s.Name == name {
return s
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)
}
}
t.Fatalf("shortcut %q not found", name)
return nil
}
func writeJSON(t *testing.T, w http.ResponseWriter, payload interface{}) {
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")
json.NewEncoder(w).Encode(payload)
if err := json.NewEncoder(w).Encode(payload); err != nil {
t.Fatalf("failed to write response: %v", err)
}
}

View File

@ -1,7 +1,7 @@
---
name: gitlink-wiki
version: 2.0.0
description: "Wiki 页面管理:查看目录、查看、创建、更新和删除 GitLink Wiki 页面。"
version: 1.0.0
description: "Wiki management: list, view, create, update, and delete GitLink wiki pages."
metadata:
requires:
bins: ["gitlink-cli"]
@ -10,70 +10,55 @@ metadata:
# gitlink-wiki
**重要**: 开始操作前请先阅读 [`../gitlink-shared/SKILL.md`](../gitlink-shared/SKILL.md),其中涵盖认证、权限、全局参数和 GitLink API 行为说明。
**重要**: 执行写入或破坏性操作(如 `+create`、`+update` 或 `+delete`)前,请先确认用户意图。
**重要**: 操作 GitLink 资源请使用 `gitlink-cli`,不要使用 `gh` 等 GitHub 专用工具。
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.
| 快捷命令 | 说明 | 操作类型 |
|----------|------|----------|
| `wiki +list` | 列出 Wiki 页面(目录结构) | 只读 |
| `wiki +view` | 按页面名称查看 Wiki 页面详情 | 只读 |
| `wiki +create` | 创建新的 Wiki 页面 | 写入 |
| `wiki +update` | 更新 Wiki 页面标题和/或内容 | 写入 |
| `wiki +delete` | 删除 Wiki 页面 | 破坏性 |
## 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
# 列出 Wiki 目录
gitlink-cli wiki +list --owner Gitlink --repo forgeplus --project-id 12345
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 +view --owner Gitlink --repo forgeplus --project-id 12345 -n home
gitlink-cli wiki +create --owner Gitlink --repo forgeplus \
--project-id 123 \
--page Home \
--title Home \
--content "Welcome to Wiki"
# 创建 Wiki 页面
gitlink-cli wiki +create --owner Gitlink --repo forgeplus --project-id 12345 \
-n getting-started -t "快速开始" -c "# 快速开始指南\n\n这是入门文档。"
gitlink-cli wiki +update --owner Gitlink --repo forgeplus \
--project-id 123 \
--page Home \
--title Home \
--content "Updated content"
# 创建时附带提交信息
gitlink-cli wiki +create --owner Gitlink --repo forgeplus --project-id 12345 \
-n api-guide -t "API 指南" -c "# API 指南" -m "Add API guide"
# 仅更新页面标题
gitlink-cli wiki +update --owner Gitlink --repo forgeplus --project-id 12345 -n home -t "新标题"
# 仅更新页面内容
gitlink-cli wiki +update --owner Gitlink --repo forgeplus --project-id 12345 -n home -c "# 更新后的内容"
# 同时更新标题和内容
gitlink-cli wiki +update --owner Gitlink --repo forgeplus --project-id 12345 \
-n home -t "新标题" -c "新内容"
# 删除 Wiki 页面
gitlink-cli wiki +delete --owner Gitlink --repo forgeplus --project-id 12345 -n old-page
gitlink-cli wiki +delete --owner Gitlink --repo forgeplus --project-id 123 --page Home --dry-run
```
## 参数说明
## API Mapping
| 命令 | 关键参数 |
|------|----------|
| `+list` | `--project-id` |
| `+view` | `--project-id`、`--page-name` (`-n`) |
| `+create` | `--project-id`、`--page-name` (`-n`)、`--title` (`-t`)、`--content` (`-c`)、`--message` (`-m`) |
| `+update` | `--project-id`、`--page-name` (`-n`)、`--title` (`-t`),可选 `--content` (`-c`)、`--message` (`-m`) |
| `+delete` | `--project-id`、`--page-name` (`-n`) |
## API 说明
- 所有 Wiki 端点使用 `/api/wiki/{action}` 扁平路径结构(非 REST 嵌套路径)。
- 目录列表: `GET /api/wiki/wikiPages`查询参数owner, repo, projectId
- 查看详情: `GET /api/wiki/getWiki`查询参数owner, repo, projectId, pageName
- 创建页面: `POST /api/wiki/createWiki`JSON bodycontent 需 base64 编码)
- 更新页面: `PUT /api/wiki/updateWiki`JSON bodycontent 需 base64 编码)
- 删除页面: `DELETE /api/wiki/deleteWiki`JSON bodyowner, repo, projectId, pageName
- Wiki 页面通过 `pageName`slug标识而非数字 ID。
- 所有操作都需要 `--project-id`GitLink 项目数字 ID
- 创建和更新时,内容自动进行 base64 编码后以 `content_base64` 字段发送。
- `+update` 要求必须提供 `--title``--page-name``--content` 为可选。
| 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` |