diff --git a/README.md b/README.md index 67f1643..d526660 100644 --- a/README.md +++ b/README.md @@ -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 --role developer --dry-run ``` ### Issue Management @@ -456,30 +450,6 @@ gitlink-cli release +update --owner Gitlink --repo forgeplus -i -b gitlink-cli release +delete --owner Gitlink --repo forgeplus -i --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 -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 - -# 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) | diff --git a/README.zh-CN.md b/README.zh-CN.md index 45faad4..85fda2c 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -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 --role developer --dry-run ``` ### Issue 管理 @@ -447,30 +441,6 @@ gitlink-cli release +update --owner Gitlink --repo forgeplus -i -b gitlink-cli release +delete --owner Gitlink --repo forgeplus -i --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 -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 - -# 一步到位:上传本地文件并附加到新发行版 -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 等) | diff --git a/doc/changes/wiki-openapi-shortcuts.md b/doc/changes/wiki-openapi-shortcuts.md new file mode 100644 index 0000000..9a7d272 --- /dev/null +++ b/doc/changes/wiki-openapi-shortcuts.md @@ -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 diff --git a/shortcuts/register.go b/shortcuts/register.go index 039e62b..f32312a 100644 --- a/shortcuts/register.go +++ b/shortcuts/register.go @@ -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 { diff --git a/shortcuts/register_test.go b/shortcuts/register_test.go index 27ad1ac..6c2795b 100644 --- a/shortcuts/register_test.go +++ b/shortcuts/register_test.go @@ -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{} diff --git a/shortcuts/wiki/wiki.go b/shortcuts/wiki/wiki.go index 261c697..6915173 100644 --- a/shortcuts/wiki/wiki.go +++ b/shortcuts/wiki/wiki.go @@ -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 } diff --git a/shortcuts/wiki/wiki_test.go b/shortcuts/wiki/wiki_test.go index ed8b0f7..a1d1b62 100644 --- a/shortcuts/wiki/wiki_test.go +++ b/shortcuts/wiki/wiki_test.go @@ -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) + } } diff --git a/skills/gitlink-wiki/SKILL.md b/skills/gitlink-wiki/SKILL.md index b64d5be..444e129 100644 --- a/skills/gitlink-wiki/SKILL.md +++ b/skills/gitlink-wiki/SKILL.md @@ -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 body,content 需 base64 编码) -- 更新页面: `PUT /api/wiki/updateWiki`(JSON body,content 需 base64 编码) -- 删除页面: `DELETE /api/wiki/deleteWiki`(JSON body:owner, 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` |