diff --git a/README.md b/README.md index 3fba5d0..d48d41a 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![Go Version](https://img.shields.io/badge/Go-1.26%2B-blue.svg)](https://golang.org) [![npm version](https://img.shields.io/npm/v/@gitlink-ai/cli.svg)](https://www.npmjs.com/package/@gitlink-ai/cli) -The official [GitLink](https://www.gitlink.org.cn) CLI tool — built for humans and AI Agents. Supports **macOS, Linux, and Windows**. Covers repository management, issue tracking, pull requests, webhooks, member collaboration, CI/CD, and AI-powered workflows, with 40+ commands and AI Agent [Skills](./skills/). +The official [GitLink](https://www.gitlink.org.cn) CLI tool — built for humans and AI Agents. Supports **macOS, Linux, and Windows**. Covers repository management, wiki pages, issue tracking, pull requests, webhooks, member collaboration, CI/CD, and AI-powered workflows, with 40+ commands and AI Agent [Skills](./skills/). **[中文文档](./README.zh-CN.md)** @@ -83,7 +83,7 @@ The official [GitLink](https://www.gitlink.org.cn) CLI tool — built for humans ## Why gitlink-cli? - **Agent-Native Design** — Structured [Skills](./skills/) out of the box, compatible with Claude Code, OpenClaw, and other AI platforms — Agents can operate GitLink with zero extra setup -- **Wide Coverage** — Repository, Issue, PR, Webhook, Member, Branch, Release, CI, Pipeline, Org, Search, and User workflows are covered by high-level commands +- **Wide Coverage** — Repository, Wiki, Issue, PR, Webhook, Member, Branch, Release, CI, Pipeline, Org, Search, and User workflows are covered by high-level commands - **AI-Friendly & Optimized** — Every command is tested with real Agents, featuring concise parameters, smart defaults, and structured output - **Cross-Platform** — Runs on macOS, Linux, and Windows (x64/arm64), install via `npm install -g @gitlink-ai/cli` in one command, binary auto-downloaded - **Open Source, Zero Barriers** — MulanPSL-2.0 license, ready to use, just `npm install` @@ -96,7 +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 | -| ⭐ Reaction | Follow, unfollow, like, unlike, list watchers and stargazers | +| 📚 Wiki | List, view, create, update, 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 | @@ -107,7 +107,6 @@ 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 | -| 📖 Wiki | List, view, create, update, delete, and export wiki pages | | 🔍 Search | Search repositories, users | | 👤 User | View user profiles and info | | 📋 PM | Sprint management, kanban boards, weekly reports | @@ -248,22 +247,25 @@ gitlink-cli repo +create -n my-project -d "Project description" gitlink-cli repo +fork --owner Gitlink --repo forgeplus ``` -### Repository Reactions +### Wiki Management ```bash -# List repository watchers -gitlink-cli reaction +watchers --owner Gitlink --repo forgeplus +# List wiki pages +gitlink-cli wiki +list --owner Gitlink --repo forgeplus -# List repository stargazers -gitlink-cli reaction +stargazers --owner Gitlink --repo forgeplus +# View a wiki page +gitlink-cli wiki +view --owner Gitlink --repo forgeplus --page Home -# Follow or unfollow a repository -gitlink-cli reaction +follow --owner Gitlink --repo forgeplus -gitlink-cli reaction +unfollow --owner Gitlink --repo forgeplus +# Create a wiki page from inline content +gitlink-cli wiki +create --owner Gitlink --repo forgeplus \ + --page Home --title Home --content "Welcome to the project wiki" -# Like or unlike a repository -gitlink-cli reaction +like --owner Gitlink --repo forgeplus -gitlink-cli reaction +unlike --owner Gitlink --repo forgeplus +# Update a wiki page from a Markdown file +gitlink-cli wiki +update --owner Gitlink --repo forgeplus \ + --page Home --file docs/wiki-home.md --message "Update Home" + +# Delete a wiki page +gitlink-cli wiki +delete --owner Gitlink --repo forgeplus --page Home ``` ### Webhook Management @@ -311,22 +313,6 @@ gitlink-cli member +invite-link --owner Gitlink --repo forgeplus --role develope # List issues gitlink-cli issue +list --owner Gitlink --repo forgeplus -# Fetch all pages automatically (works on all paginated list commands: -# issue/pr/branch/release/milestone/org/repo/label/member/webhook/tag/commit +list, -# issue +comments, repo +watchers/+stargazers/+forks, search +repos/+users) -gitlink-cli issue +list --owner Gitlink --repo forgeplus --all - -# Commit history and single commit details -gitlink-cli commit +list --owner Gitlink --repo forgeplus --ref develop --all -gitlink-cli commit +view --owner Gitlink --repo forgeplus --sha - -# Git tags -gitlink-cli tag +list --owner Gitlink --repo forgeplus --all - -# Show a single tag by name (falls back to a list scan when the -# show endpoint's existence precheck misfires) -gitlink-cli tag +view --owner Gitlink --repo forgeplus -n v4.0.0 - # Create an issue gitlink-cli issue +create --owner Gitlink --repo forgeplus -t "Bug: Login failed" -b "Steps to reproduce..." @@ -594,14 +580,6 @@ gitlink-cli workflow +pr-summary --from shortcuts/workflow/testdata/pr_summary.j # Repository workflow report by read-only GitLink fetch gitlink-cli workflow +repo-report --owner Gitlink --repo gitlink-cli --format markdown -# Optional full PR review attribution. This deep-fetches formal reviews and -# PR-associated Issue journals for analyzed PRs, so keep it explicit. -gitlink-cli workflow +repo-report --owner Gitlink --repo gitlink-cli --include-pr-review-audit --format json > report.review-audit.json - -# Limit analysis only when an intentional sample is needed. -# By default, repo-report paginates through all open issues and pull requests. -gitlink-cli workflow +repo-report --owner Gitlink --repo gitlink-cli --issue-limit 20 --pr-limit 50 --format markdown - # Repository workflow report from a local JSON file gitlink-cli workflow +repo-report --from shortcuts/workflow/testdata/repo_report.json --format json ``` @@ -619,86 +597,6 @@ Safety: - They do not depend on LLM APIs. - `workflow +pr-summary` does not comment, approve, reject, or merge pull requests. - `workflow +repo-report` aggregates health, issue triage, and PR review summary signals without remote writes. -- `workflow +repo-report --include-pr-review-audit` remains read-only. It treats formal review objects as authoritative review evidence and keeps submitter, reviewer, participant, and system journal activity separate. - -### Feishu Collaboration Export - -`feishu` turns `workflow +repo-report` JSON into Feishu collaboration outputs. - -`workflow +repo-report` paginates through all open issues and pull requests by -default. Feishu cards label these values as analyzed counts. Passing -`--issue-limit` or `--pr-limit` intentionally limits the analysis and the -resulting values must not be interpreted as repository totals. - -Stable usage: - -```bash -gitlink-cli workflow +repo-report --owner "$GITLINK_OWNER" --repo "$GITLINK_REPO" --format json > report.json -gitlink-cli workflow +repo-report --owner "$GITLINK_OWNER" --repo "$GITLINK_REPO" --include-pr-review-audit --format json > report.review-audit.json - -gitlink-cli feishu +notify --from-workflow-json report.json --format json -gitlink-cli feishu +notify --from-workflow-json report.json --send --format table -gitlink-cli feishu +owner-digest --from-workflow-json report.review-audit.json --format table - -gitlink-cli feishu +weekly-report --from-workflow-json report.json --format markdown -gitlink-cli feishu +owner-digest --from-workflow-json report.json --format markdown -gitlink-cli feishu +contributor-digest --from-workflow-json report.json --format markdown -gitlink-cli feishu +bitable-records --from-workflow-json report.json --format json -gitlink-cli feishu +task-preview --from-workflow-json report.json --format markdown -``` - -Experimental Open Platform usage: - -```bash -gitlink-cli feishu +doc-export --from-workflow-json report.json --wiki-url "$FEISHU_WIKI_URL" --send --format table -gitlink-cli feishu +bitable-sync --from-workflow-json report.json --tables reports,issues,prs,tasks --send --format table -gitlink-cli feishu +task-create --from-workflow-json report.json --send --format table -``` - -GitLink write operations are not implemented in this branch. Feishu card buttons are navigation-only. Open Platform commands require explicit `--send` and a self-built app with resource permissions. Whether these experimental capabilities should be enabled in official deployments is left to GitLink maintainers and deployment administrators. - -Details: - -- [Feishu integration](./docs/feishu-integration.md) -- [Feishu capability layers](./docs/FEISHU_CAPABILITY_LAYERS.md) -- [Feishu environment variables](./docs/FEISHU_ENVIRONMENT.md) -- [Feishu permission matrix](./reports/FEISHU_PERMISSION_MATRIX.md) - -Local setup and smoke testing: - -```powershell -.\scripts\feishu-gitlink-setup.ps1 -.\scripts\feishu-gitlink-env-check.ps1 -Layer stable -.\scripts\feishu-gitlink-smoke.ps1 -Mode preview -``` - -The setup script stores real values only in `.local/feishu-gitlink.env.ps1`, which is ignored. - -### Wiki - -`wiki` manages a repository's wiki pages. The numeric GitLink project ID is -resolved from `--owner/--repo` automatically, or pass `--project-id`. - -```bash -# List and view wiki pages -gitlink-cli wiki +list --owner Gitlink --repo gitlink-cli -gitlink-cli wiki +view --owner Gitlink --repo gitlink-cli --page Home - -# Create a page (content is base64-encoded automatically) -gitlink-cli wiki +create --owner Gitlink --repo gitlink-cli --page Home --title Home --content "# Welcome" - -# Create from a file -gitlink-cli wiki +create --owner Gitlink --repo gitlink-cli --page Guide --content-file guide.md - -# Update (content optional) and preview with --dry-run -gitlink-cli wiki +update --owner Gitlink --repo gitlink-cli --page Home --title "Home Page" --dry-run - -# Delete a page -gitlink-cli wiki +delete --owner Gitlink --repo gitlink-cli --page Home - -# Export the wiki (markdown, pdf, or html) -gitlink-cli wiki +export --owner Gitlink --repo gitlink-cli --type markdown -``` ### Raw API @@ -762,7 +660,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-reaction` | Repository reactions (follow, like, watchers, stargazers) | +| `gitlink-wiki` | Wiki operations (list, view, create, update, 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 | diff --git a/README.zh-CN.md b/README.zh-CN.md index de688bb..1412fc5 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -5,7 +5,7 @@ [![Go Version](https://img.shields.io/badge/Go-1.26%2B-blue.svg)](https://golang.org) [![npm version](https://img.shields.io/npm/v/@gitlink-ai/cli.svg)](https://www.npmjs.com/package/@gitlink-ai/cli) -[GitLink(确实开源)](https://www.gitlink.org.cn) 官方 CLI 工具 — 为人类和 AI Agent 双重设计。支持 **macOS、Linux、Windows**,覆盖仓库管理、Issue 追踪、Pull Request、Webhook、成员协作、CI/CD 和 AI 自动化工作流,包含 40+ 命令和 AI Agent [Skills](./skills/README.md)。 +[GitLink(确实开源)](https://www.gitlink.org.cn) 官方 CLI 工具 — 为人类和 AI Agent 双重设计。支持 **macOS、Linux、Windows**,覆盖仓库管理、Wiki、Issue 追踪、Pull Request、Webhook、成员协作、CI/CD 和 AI 自动化工作流,包含 40+ 命令和 AI Agent [Skills](./skills/)。 **[English](./README.md)** @@ -78,20 +78,12 @@ jiangtx
jiangtx -
- luwanzhou -
luwanzhou -
-
- whale_hihihi -
whale_hihihi -
## 为什么选择 gitlink-cli? -- **Agent-Native 设计** — 开箱即用结构化 [Skills](./skills/README.md),兼容 Claude Code — Agent 零配置即可操作 GitLink -- **广泛覆盖** — 仓库、Issue、PR、Webhook、成员、分支、Release、CI、Pipeline、组织、搜索、用户等常用工作流均提供高层命令 +- **Agent-Native 设计** — 开箱即用结构化 [Skills](./skills/),兼容 Claude Code — Agent 零配置即可操作 GitLink +- **广泛覆盖** — 仓库、Wiki、Issue、PR、Webhook、成员、分支、Release、CI、Pipeline、组织、搜索、用户等常用工作流均提供高层命令 - **AI 友好 & 优化** — 每条命令都经过真实 Agent 测试,简洁参数、智能默认值、结构化输出 - **跨平台** — macOS、Linux、Windows (x64/arm64) 全支持,`npm` 一条命令安装 - **开源零门槛** — 木兰宽松许可证第2版(MulanPSL-2.0),`npm install` 即用 @@ -104,7 +96,8 @@ | 分类 | 能力 | |------|------| | 📦 仓库 | 列出、创建、Fork、删除仓库,查看仓库信息、洞察数据和互动状态 | -| 🐛 Issue | 创建、更新、关闭、批量关闭/更新/删除、评论 Issue | +| 📚 Wiki | 列出、查看、创建、更新、删除 Wiki 页面 | +| 🐛 Issue | 创建、更新、关闭、批量关闭、评论 Issue | | 🔖 标签 | 创建、列出、更新、删除 Issue 标签 | | 🔀 PR | 创建、合并、Review Pull Request,查看变更文件 | | 👥 成员 | 列出、添加、移除仓库成员,调整角色,生成和接受邀请链接 | @@ -113,11 +106,8 @@ | 🏢 组织 | 管理组织、成员、团队 | | 🔧 CI | 查看构建、日志、CI/CD 操作 | | ⚙️ Pipeline | 运行、查看、启停、删除流水线工作流并查询日志 | -| 📖 Wiki | 列出、查看、创建、更新、删除 Wiki 页面 | | 🔍 搜索 | 搜索仓库、用户 | -| 📊 数据集 | 按项目查询科研数据集 | | 👤 用户 | 查看用户资料和信息 | -| 📊 画像 | 用户开发能力、角色定位、专业定位、近期活动、贡献热力图统计 | | 📋 项目管理 | Sprint 管理、看板、周报 | | 🤖 工作流 | AI 驱动的 Issue 分类、PR Review、Release Notes | @@ -268,6 +258,27 @@ gitlink-cli repo +create -n my-project -d "项目描述" gitlink-cli repo +fork --owner Gitlink --repo forgeplus ``` +### Wiki 管理 + +```bash +# 列出 Wiki 页面 +gitlink-cli wiki +list --owner Gitlink --repo forgeplus + +# 查看 Wiki 页面 +gitlink-cli wiki +view --owner Gitlink --repo forgeplus --page Home + +# 使用命令行内容创建 Wiki 页面 +gitlink-cli wiki +create --owner Gitlink --repo forgeplus \ + --page Home --title Home --content "欢迎来到项目 Wiki" + +# 使用 Markdown 文件更新 Wiki 页面 +gitlink-cli wiki +update --owner Gitlink --repo forgeplus \ + --page Home --file docs/wiki-home.md --message "更新 Home" + +# 删除 Wiki 页面 +gitlink-cli wiki +delete --owner Gitlink --repo forgeplus --page Home +``` + ### Webhook 管理 ```bash @@ -285,28 +296,6 @@ gitlink-cli webhook +test --owner Gitlink --repo forgeplus --id 68 gitlink-cli webhook +tasks --owner Gitlink --repo forgeplus --id 68 ``` -### Wiki 管理 - -```bash -# 列出 Wiki 页面(目录结构) -gitlink-cli wiki +list --owner Gitlink --repo forgeplus --project-id 12345 - -# 查看 Wiki 页面 -gitlink-cli wiki +view --owner Gitlink --repo forgeplus --project-id 12345 -n home - -# 创建 Wiki 页面 -gitlink-cli wiki +create --owner Gitlink --repo forgeplus --project-id 12345 \ - -n getting-started -t "快速开始" -c "# 快速开始指南" - -# 更新 Wiki 页面标题和/或内容 -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 -``` - ### 成员管理 ```bash @@ -335,18 +324,6 @@ gitlink-cli member +invite-link --owner Gitlink --repo forgeplus --role develope # 列出 Issue gitlink-cli issue +list --owner Gitlink --repo forgeplus -# 自动翻页拉取全部(适用于所有可分页 list 命令: -# issue/pr/branch/release/milestone/org/repo/label/member/webhook/tag/commit +list、 -# issue +comments、repo +watchers/+stargazers/+forks、search +repos/+users) -gitlink-cli issue +list --owner Gitlink --repo forgeplus --all - -# 提交历史与单个提交详情 -gitlink-cli commit +list --owner Gitlink --repo forgeplus --ref develop --all -gitlink-cli commit +view --owner Gitlink --repo forgeplus --sha - -# Git 标签 -gitlink-cli tag +list --owner Gitlink --repo forgeplus --all - # 创建 Issue gitlink-cli issue +create --owner Gitlink --repo forgeplus -t "Bug: 登录失败" -b "复现步骤..." @@ -368,14 +345,6 @@ gitlink-cli issue +batch-close --owner Gitlink --repo forgeplus --numbers 123,12 # 从 CSV 文件批量关闭 Issue gitlink-cli issue +batch-close --owner Gitlink --repo forgeplus --from issues.csv -# 按 API issue id 预览批量更新元数据 -# 注意:--ids 是 API issue id,不是网页 URL 中的 Issue 编号。 -gitlink-cli issue +batch-update --owner Gitlink --repo forgeplus --ids 101,102 --status-id 3 --priority-id 2 --dry-run - -# 危险批量删除必须先 dry-run,真实执行还要显式 --yes -gitlink-cli issue +batch-delete --owner Gitlink --repo forgeplus --ids 101,102 --dry-run -gitlink-cli issue +batch-delete --owner Gitlink --repo forgeplus --ids 101,102 --yes - # 添加评论 gitlink-cli issue +comment --owner Gitlink --repo forgeplus -i 123 -b "已修复" @@ -498,16 +467,6 @@ gitlink-cli pipeline +disable --owner Gitlink --repo forgeplus --id 7 --workflow gitlink-cli pipeline +delete --owner Gitlink --repo forgeplus --id 7 --dry-run ``` -### 忽略文件模板 - -```bash -# 列出所有可用的 .gitignore 模板 -gitlink-cli ignore +list - -# 按名称筛选模板 -gitlink-cli ignore +list --name Go -``` - ### 搜索 ```bash @@ -518,143 +477,6 @@ gitlink-cli search +repos -k "machine learning" gitlink-cli search +users -k "zhangsan" ``` -### 用户画像 - -`profile` 暴露 GitLink 原生的用户画像统计(开发能力、角色定位、专业定位、近期活动、贡献热力图)。 -省略 `--user` 时默认使用当前认证用户。 - -```bash -# 开发能力评分 + 语言分布 -gitlink-cli profile +ability --user zhangsan - -# 角色定位 / 专业(学科)定位 -gitlink-cli profile +role --user zhangsan -gitlink-cli profile +major --user zhangsan - -# 指定时间范围的开发能力(Unix 时间戳) -gitlink-cli profile +ability --user zhangsan --start-time 1704067200 --end-time 1735689600 - -# 当前用户的近期活动(每日 疑修 / 合并请求 / 提交) -gitlink-cli profile +activity - -# 指定年份的贡献热力图 -gitlink-cli profile +contribution --user zhangsan --year 2025 -``` - -### 数据集 - -`dataset` 管理并查询 GitLink 科研数据集(标题、描述、论文内容、许可证、所属项目)。 - -```bash -# 按数字项目 ID 列出一个或多个项目的数据集 -gitlink-cli dataset +list --ids 5988 - -# 查看仓库的数据集及其附件 -gitlink-cli dataset +view --owner Gitlink --repo forgeplus - -# 创建 / 更新仓库数据集(先用 --dry-run 预览) -gitlink-cli dataset +create --owner me --repo proj -t "我的数据集" -d "..." --license-id 359 --dry-run -gitlink-cli dataset +update --owner me --repo proj -t "我的数据集" -d "更新" - -# 删除数据集附件(破坏性:先预览,再用 --yes 确认) -gitlink-cli dataset +delete-attachment --owner me --repo proj --uuid --dry-run -gitlink-cli dataset +delete-attachment --owner me --repo proj --uuid --yes -``` - -> 注意:`dataset +list`(平台数据集查询)已在生产 gitlink.org.cn 验证可用。按仓库的 `+view`/`+create`/`+update` 遵循已发布的 OpenAPI 契约,但生产环境尚未部署(当前返回 404),待平台上线后即可生效。 - -### 飞书协作导出 - -`feishu` 将 `workflow +repo-report` JSON 转成飞书协作内容。 - -`workflow +repo-report` 默认分页读取并分析全部开放 Issue 和 PR。飞书卡片会把这些值明确标为“已分析数量”。只有显式传 `--issue-limit` 或 `--pr-limit` 时才会采样,此时结果不能解释为仓库总量。 - -#### 稳定层:自定义机器人通知 - -稳定层只依赖飞书群自定义机器人。它适合把 GitLink 项目状态、周报、Owner 摘要和贡献者摘要推送到群里。默认只预览,真实发送必须显式传 `--send`。 - -```bash -gitlink-cli workflow +repo-report --owner "$GITLINK_OWNER" --repo "$GITLINK_REPO" --format json > report.json - -# 仅在明确需要采样时设置上限 -gitlink-cli workflow +repo-report --owner "$GITLINK_OWNER" --repo "$GITLINK_REPO" --issue-limit 20 --pr-limit 50 --format json > report.sample.json - -gitlink-cli feishu +notify --from-workflow-json report.json --format json -gitlink-cli feishu +notify --from-workflow-json report.json --send --format table - -gitlink-cli feishu +weekly-report --from-workflow-json report.json --format markdown -gitlink-cli feishu +owner-digest --from-workflow-json report.json --format markdown -gitlink-cli feishu +contributor-digest --from-workflow-json report.json --format markdown -gitlink-cli feishu +bitable-records --from-workflow-json report.json --format json -gitlink-cli feishu +task-preview --from-workflow-json report.json --format markdown -``` - -中文输出建议同时给 workflow 和 feishu 命令传 `--lang zh-CN`: - -```bash -gitlink-cli workflow +repo-report --owner "$GITLINK_OWNER" --repo "$GITLINK_REPO" --lang zh-CN --format json > report.zh-CN.json - -gitlink-cli feishu +notify --from-workflow-json report.zh-CN.json --lang zh-CN --send --format table -gitlink-cli feishu +owner-digest --from-workflow-json report.zh-CN.json --lang zh-CN --send --format table -gitlink-cli feishu +contributor-digest --from-workflow-json report.zh-CN.json --lang zh-CN --send --format table -``` - -#### 配置诊断层:先检查再写入 - -诊断命令用于降低飞书开放平台配置成本。默认只检查本地变量和目标配置;传 `--remote` 后会调用飞书只读/检查接口,例如获取 `tenant_access_token`、解析 Wiki node、搜索 Bitable sentinel `unique_key`。这些命令不会创建文档、不会写入多维表格、不会创建任务,也不会修改 GitLink。 - -```bash -gitlink-cli feishu +app-check --format table -gitlink-cli feishu +doc-check --remote --format table -gitlink-cli feishu +bitable-check --tables reports,issues,prs,tasks --remote --format table -gitlink-cli feishu +task-check --remote --format table -``` - -#### 实验层:飞书开放平台写入 - -实验层使用飞书开放平台自建应用。当前已在测试企业中验证 DocX 追加、多维表格写入和飞书任务创建,但这部分不是零配置稳定能力。真实写入仍然必须显式传 `--send`,并且要求自建应用有对应 API scope 和目标资源权限。 - -```bash -gitlink-cli feishu +doc-export --from-workflow-json report.json --wiki-url "$FEISHU_WIKI_URL" --send --format table -gitlink-cli feishu +bitable-sync --from-workflow-json report.json --tables reports,issues,prs,tasks --send --format table -gitlink-cli feishu +task-create --from-workflow-json report.json --send --format table -``` - -为了完成端到端验证,测试企业里的自建应用授予了较宽的权限。正式部署时不建议照搬测试权限,应由维护者或管理员按命令实际需要开最小权限。 - -多维表格已完成两类真实验证: - -- 单表多视图验证:把 `reports/issues/prs/contributors/tasks` 写入同一张测试表,证明字段和写入链路可用。 -- 独立表验证:拆成 `gitlink_reports`、`gitlink_issues`、`gitlink_prs`、`gitlink_contributors`、`gitlink_tasks` 五张表,分别写入 `1/5/2/1/7` 条记录,证明每类记录都能写入独立表。 - -#### 当前边界 - -本分支不实现 GitLink 写操作。飞书卡片按钮仅用于跳转。开放平台能力必须显式传 `--send`,并要求自建应用具备对应资源权限。是否在正式部署中启用这些实验能力,由 GitLink 维护者和部署管理员决定。 - -下一阶段再考虑: - -- 飞书任务项目/分组归属。 -- 飞书任务执行者/关注人。 -- 飞书侧任务去重或搜索。 -- 多维表格自动建 Base、建表、建字段、建视图。 -- 飞书卡片回调和 GitLink 低风险写动作。 - -详细文档: - -- [飞书集成](./docs/feishu-integration.md) -- [飞书能力分层](./docs/FEISHU_CAPABILITY_LAYERS.md) -- [飞书环境变量](./docs/FEISHU_ENVIRONMENT.md) -- [飞书权限矩阵](./reports/FEISHU_PERMISSION_MATRIX.md) - -本地配置和 smoke 测试: - -```powershell -.\scripts\feishu-gitlink-setup.ps1 -.\scripts\feishu-gitlink-env-check.ps1 -Layer stable -.\scripts\feishu-gitlink-smoke.ps1 -Mode preview -``` - -真实值只会写入被忽略的 `.local/feishu-gitlink.env.ps1`,不要提交。 ### Raw API Shortcuts 未覆盖的接口可通过 Raw API 直接调用: @@ -711,13 +533,14 @@ git push gitlink `skills/` 目录包含 Claude Code Agent Skill 文件,支持 AI 自动化操作 GitLink 平台。 -详见 [skills/README.md](./skills/README.md) +详见 [skills/README.md](skills/README.md) | Skill | 说明 | |-------|------| | `gitlink-shared` | 认证、全局参数、安全规则、API 注意事项 | | `gitlink-repo` | 仓库操作(创建、查看、删除、Fork、洞察数据等) | -| `gitlink-issue` | Issue 操作(创建、更新、关闭、批量更新/删除、评论等) | +| `gitlink-wiki` | Wiki 操作(列出、查看、创建、更新、删除) | +| `gitlink-issue` | Issue 操作(创建、更新、关闭、评论等) | | `gitlink-pr` | Pull Request 操作(创建、合并、Review 等) | | `gitlink-member` | 仓库成员与邀请链接管理 | | `gitlink-release` | 发布管理(创建、编辑、更新、查看、删除等) | @@ -779,7 +602,7 @@ gitlink-cli/ ## 文档 -- [Skills 使用指南](./skills/README.md) — AI Agent Skills 详细说明 +- [Skills 使用指南](skills/README.md) — AI Agent Skills 详细说明 - [设计文档](doc/design.md) — 架构设计和开发计划 ## 常见问题 @@ -842,7 +665,7 @@ gitlink-cli 使用 Windows Credential Manager 安全存储 Token。如果 Creden ### Q: 如何查看完整的 API 参考? -查看 [skills/gitlink-shared/references/api-reference.md](./skills/gitlink-shared/references/api-reference.md) +查看 [skills/gitlink-shared/REFERENCE.md](skills/gitlink-shared/REFERENCE.md) ## 许可证 diff --git a/doc/changes/wiki-shortcut.md b/doc/changes/wiki-shortcut.md index b48af22..f54c78c 100644 --- a/doc/changes/wiki-shortcut.md +++ b/doc/changes/wiki-shortcut.md @@ -1,23 +1,13 @@ # Wiki Shortcut -新增 `wiki` Shortcut 组,支持 Wiki 页面管理: +Added a new `wiki` shortcut group for repository wiki management. -- `wiki +list` - 列出 Wiki 页面(目录结构) -- `wiki +view` - 按页面名称查看 Wiki 页面详情 -- `wiki +create` - 创建新的 Wiki 页面 -- `wiki +update` - 更新 Wiki 页面标题和/或内容 -- `wiki +delete` - 删除 Wiki 页面 +Included commands: -## 实现要点 - -- **API 端点**:基于 `/api/wiki/open/{action}` 扁平路径结构,覆盖 5 个 Wiki 管理接口: - - `GET /api/wiki/open/wikiPages` — 目录列表 - - `GET /api/wiki/open/getWiki` — 查看页面 - - `POST /api/wiki/open/createWiki` — 创建页面 - - `PUT /api/wiki/open/updateWiki` — 更新页面 - - `DELETE /api/wiki/open/deleteWiki` — 删除页面 -- **标识方式**:Wiki 页面通过 `pageName`(slug)标识,所有操作需要 `projectId`(GitLink 项目数字 ID) -- **内容编码**:创建和更新时,内容自动进行 base64 编码后以 `content_base64` 字段发送 -- **更新保护**:`+update` 要求必须提供 `--title` 和 `--page-name`;`--content` 为可选 -- **Shortcut 模式**:使用 `common.Shortcut` + `RuntimeContext` 框架,与其他模块保持一致 +- `wiki +list` +- `wiki +view` +- `wiki +create` +- `wiki +update` +- `wiki +delete` +The shortcut auto-resolves GitLink `projectId` from repository metadata when `--project-id` is omitted, supports both inline content and file-backed Markdown content, base64-encodes wiki content for the API, and validates conflicting or incomplete input before sending requests. diff --git a/internal/i18n/locales/en-US.json b/internal/i18n/locales/en-US.json index 83a373d..79c20fa 100644 --- a/internal/i18n/locales/en-US.json +++ b/internal/i18n/locales/en-US.json @@ -21,17 +21,6 @@ "cmd.config.list.short": "List all configuration values", "cmd.config.set.short": "Set a configuration value", "cmd.config.short": "Manage gitlink-cli configuration", - "cmd.dataset.create.long": "Create the dataset of a repository with a title, description, optional license and research paper content.", - "cmd.dataset.create.short": "Create a repository's dataset", - "cmd.dataset.delete_attachment.long": "Delete a dataset attachment by its UUID. This is destructive: preview with --dry-run, then pass --yes to confirm.", - "cmd.dataset.delete_attachment.short": "Delete a dataset attachment by UUID", - "cmd.dataset.list.long": "List datasets for one or more GitLink projects by their numeric project IDs.", - "cmd.dataset.list.short": "List datasets by project IDs", - "cmd.dataset.short": "Dataset operations", - "cmd.dataset.update.long": "Update the dataset of a repository (title, description, optional license and research paper content).", - "cmd.dataset.update.short": "Update a repository's dataset", - "cmd.dataset.view.long": "View a repository's dataset and its attachments. Use --page/--limit to paginate attachments.", - "cmd.dataset.view.short": "View a repository's dataset", "cmd.doctor.long": "Run local diagnostics for gitlink-cli configuration, authentication, repository context and API connectivity.", "cmd.doctor.short": "Diagnose gitlink-cli environment problems", "cmd.issue.batch_close.long": "Close filtered issues in bulk.\n\nThis command defaults to dry-run mode and only prints matching issues.\nPass --yes to execute remote close operations. Use restrictive filters and a small limit.\n\nExamples:\n gitlink-cli issue +batch-close --owner Gitlink --repo gitlink-cli --older-than-days 60 --limit 20\n gitlink-cli issue +batch-close --owner Gitlink --repo gitlink-cli --older-than-days 60 --limit 20 --yes", @@ -42,7 +31,6 @@ "cmd.issue.batch_list.short": "List issue batch maintenance candidates without changing remote data", "cmd.issue.close.short": "Close an issue", "cmd.issue.comment.short": "Add a comment to an issue", - "cmd.issue.comments.short": "List comments on an issue", "cmd.issue.create.short": "Create a new issue", "cmd.issue.list.short": "List issues", "cmd.issue.short": "Issue operations", @@ -52,12 +40,9 @@ "cmd.org.info.short": "Show organization details", "cmd.org.list.short": "List organizations", "cmd.org.members.short": "List organization members", - "cmd.org.repos.short": "List repositories of an organization", "cmd.org.short": "Organization operations", "cmd.pr.close.short": "Close a pull request", "cmd.pr.comment.short": "Add a comment to a pull request", - "cmd.pr.comments.short": "List comments on a pull request", - "cmd.pr.commits.short": "List commits on a pull request", "cmd.pr.create.short": "Create a pull request", "cmd.pr.diff.short": "Show diff for a pull request", "cmd.pr.files.short": "List changed files in a pull request", @@ -69,17 +54,6 @@ "cmd.pr.version_diff.short": "Show diff for a pull request patchset version", "cmd.pr.versions.short": "List pull request patchset versions", "cmd.pr.view.short": "View pull request details", - "cmd.profile.ability.long": "Show a user's development ability scores (influence, contribution, activity, experience, language) and language breakdown, sourced from the GitLink platform statistics API.", - "cmd.profile.ability.short": "Show a user's development ability scores", - "cmd.profile.activity.long": "Show a user's recent activity statistics: daily issues, pull requests, and commits over a time window.", - "cmd.profile.activity.short": "Show a user's recent activity statistics", - "cmd.profile.contribution.long": "Show a user's contribution heatmap (daily contribution counts) for a given year.", - "cmd.profile.contribution.short": "Show a user's contribution heatmap", - "cmd.profile.major.long": "Show a user's major/discipline categories (e.g. deep learning, quantum computing) inferred from their projects.", - "cmd.profile.major.short": "Show a user's major/discipline categories", - "cmd.profile.role.long": "Show a user's role positioning derived from the GitLink platform statistics API.", - "cmd.profile.role.short": "Show a user's role positioning", - "cmd.profile.short": "User profile and statistics operations", "cmd.release.create.short": "Create a release", "cmd.release.delete.short": "Delete a release", "cmd.release.list.short": "List releases", @@ -109,16 +83,19 @@ "cmd.webhook.test.short": "Trigger a test delivery for a webhook", "cmd.webhook.update.short": "Update a repository webhook while preserving unspecified fields when available", "cmd.webhook.view.short": "View webhook details", + "cmd.wiki.create.short": "Create a wiki page", + "cmd.wiki.delete.short": "Delete a wiki page", + "cmd.wiki.list.short": "List wiki pages", + "cmd.wiki.short": "Wiki operations", + "cmd.wiki.update.short": "Update a wiki page", + "cmd.wiki.view.short": "View a wiki page", "error.auth.delete_token_failed": "failed to delete token: {message}", "error.auth.login_failed": "login failed: {message}", "error.auth.store_token_failed": "failed to store token: {message}", "error.auth.token_empty": "token cannot be empty", "error.config.save_failed": "failed to save config: {message}", - "error.dataset.delete_confirm": "dataset attachment deletion is destructive; run --dry-run first, then pass --yes to confirm", "error.missing_required_flag": "required flag --{name} is missing", - "error.profile.user_required": "could not determine target user; pass --user or run gitlink-cli auth login", "error.unsupported_language": "unsupported language: {lang}", - "flag.all": "Fetch all pages automatically (ignores --page)", "flag.api.batch_continue_on_error": "Continue running remaining batch requests after a failure", "flag.api.batch_dry_run": "Preview batch requests without sending remote requests", "flag.api.batch_file": "Read an API batch plan from a JSON file", @@ -127,7 +104,6 @@ "flag.api.body_file": "Read request body JSON from a file", "flag.api.body_stdin": "Read request body JSON from stdin", "flag.api.header": "Additional headers (key:value)", - "flag.api.paginate": "Fetch all pages of a GET list endpoint and merge the results", "flag.api.query": "Query parameters (key=val&key2=val2)", "flag.auth.token": "Login by pasting an existing token", "flag.branch.from": "Source branch or commit", @@ -136,17 +112,6 @@ "flag.ci.stage": "Stage number", "flag.ci.step": "Step number", "flag.comment.body": "Comment body", - "flag.dataset.description": "Dataset description", - "flag.dataset.dry_run": "Preview the request without writing the dataset", - "flag.dataset.dry_run_delete": "Preview the request without deleting the attachment", - "flag.dataset.ids": "Comma-separated project IDs to query datasets for", - "flag.dataset.license_id": "License ID", - "flag.dataset.limit": "Attachment page size", - "flag.dataset.page": "Attachment page number", - "flag.dataset.paper_content": "Research paper content", - "flag.dataset.title": "Dataset title", - "flag.dataset.uuid": "Attachment UUID", - "flag.dataset.yes": "Confirm the destructive deletion", "flag.debug": "Enable debug output", "flag.description": "Description", "flag.doctor.skip_network": "Skip authenticated API connectivity checks", @@ -202,10 +167,6 @@ "flag.pr.tag_id": "Issue tag ID", "flag.pr.title": "PR title", "flag.pr.version_id": "Patchset version ID", - "flag.profile.end_time": "End time (Unix timestamp)", - "flag.profile.start_time": "Start time (Unix timestamp)", - "flag.profile.user": "Target user login (defaults to the authenticated user)", - "flag.profile.year": "Year for the contribution heatmap (e.g. 2025)", "flag.release.body": "Release notes", "flag.release.id": "Release ID", "flag.release.id_or_tag": "Release ID or tag", @@ -235,6 +196,12 @@ "flag.webhook.secret_update": "Webhook secret. Pass it again if the server does not return existing secrets.", "flag.webhook.type": "Webhook type: gitea/slack/discord/dingtalk/telegram/msteams/feishu/matrix/jianmu/softbot", "flag.webhook.url": "Webhook target URL", + "flag.wiki.content": "Wiki page content", + "flag.wiki.file": "Read wiki page content from file", + "flag.wiki.message": "Commit message", + "flag.wiki.page": "Wiki page name", + "flag.wiki.project_id": "GitLink project ID (auto-resolved when omitted)", + "flag.wiki.title": "Wiki page title (defaults to --page)", "output.auth.env_hint": " Or set {env} environment variable", "output.auth.login_hint": " Run: gitlink-cli auth login", "output.config.file": "Config file: {path}", diff --git a/internal/i18n/locales/zh-CN.json b/internal/i18n/locales/zh-CN.json index fa505e0..381ad2e 100644 --- a/internal/i18n/locales/zh-CN.json +++ b/internal/i18n/locales/zh-CN.json @@ -21,17 +21,6 @@ "cmd.config.list.short": "列出所有配置项", "cmd.config.set.short": "设置配置项", "cmd.config.short": "管理 gitlink-cli 配置", - "cmd.dataset.create.long": "为仓库创建数据集,包含标题、描述、可选许可证和研究论文内容。", - "cmd.dataset.create.short": "创建仓库数据集", - "cmd.dataset.delete_attachment.long": "按 UUID 删除数据集附件。该操作具有破坏性:先用 --dry-run 预览,再传 --yes 确认。", - "cmd.dataset.delete_attachment.short": "按 UUID 删除数据集附件", - "cmd.dataset.list.long": "按数字项目 ID 列出一个或多个 GitLink 项目的数据集。", - "cmd.dataset.list.short": "按项目 ID 列出数据集", - "cmd.dataset.short": "数据集操作", - "cmd.dataset.update.long": "更新仓库数据集(标题、描述、可选许可证和研究论文内容)。", - "cmd.dataset.update.short": "更新仓库数据集", - "cmd.dataset.view.long": "查看仓库的数据集及其附件。用 --page/--limit 对附件分页。", - "cmd.dataset.view.short": "查看仓库数据集", "cmd.doctor.long": "诊断 gitlink-cli 的配置、认证、仓库上下文和 API 连通性问题。", "cmd.doctor.short": "诊断 gitlink-cli 环境问题", "cmd.issue.batch_close.long": "批量关闭筛选后的议题。\n\n该命令默认处于 dry-run 模式,只打印匹配的议题。\n传入 --yes 后执行远端关闭操作。请使用严格筛选条件和较小 limit。\n\n示例:\n gitlink-cli issue +batch-close --owner Gitlink --repo gitlink-cli --older-than-days 60 --limit 20\n gitlink-cli issue +batch-close --owner Gitlink --repo gitlink-cli --older-than-days 60 --limit 20 --yes", @@ -42,7 +31,6 @@ "cmd.issue.batch_list.short": "列出议题批量维护候选项,不修改远端数据", "cmd.issue.close.short": "关闭议题", "cmd.issue.comment.short": "给议题添加评论", - "cmd.issue.comments.short": "列出 Issue 的评论", "cmd.issue.create.short": "创建新议题", "cmd.issue.list.short": "列出议题", "cmd.issue.short": "议题操作", @@ -52,12 +40,9 @@ "cmd.org.info.short": "显示组织详情", "cmd.org.list.short": "列出组织", "cmd.org.members.short": "列出组织成员", - "cmd.org.repos.short": "列出组织下的仓库", "cmd.org.short": "组织操作", "cmd.pr.close.short": "关闭拉取请求", "cmd.pr.comment.short": "给拉取请求添加评论", - "cmd.pr.comments.short": "列出 PR 的评论", - "cmd.pr.commits.short": "列出 PR 的提交", "cmd.pr.create.short": "创建拉取请求", "cmd.pr.diff.short": "显示拉取请求 diff", "cmd.pr.files.short": "列出拉取请求中的变更文件", @@ -69,17 +54,6 @@ "cmd.pr.version_diff.short": "显示拉取请求补丁集版本 diff", "cmd.pr.versions.short": "列出拉取请求补丁集版本", "cmd.pr.view.short": "查看拉取请求详情", - "cmd.profile.ability.long": "显示用户的开发能力评分(影响力、贡献度、活跃度、项目经验、语言能力)及语言分布,数据来自 GitLink 平台统计接口。", - "cmd.profile.ability.short": "显示用户开发能力评分", - "cmd.profile.activity.long": "显示用户近期活动统计:时间范围内每日的疑修、合并请求和提交数量。", - "cmd.profile.activity.short": "显示用户近期活动统计", - "cmd.profile.contribution.long": "显示用户在指定年份的贡献热力图(每日贡献次数)。", - "cmd.profile.contribution.short": "显示用户贡献热力图", - "cmd.profile.major.long": "显示根据用户项目推断的专业/学科领域(如深度学习、量子计算)。", - "cmd.profile.major.short": "显示用户专业/学科定位", - "cmd.profile.role.long": "显示来自 GitLink 平台统计接口的用户角色定位。", - "cmd.profile.role.short": "显示用户角色定位", - "cmd.profile.short": "用户画像与统计操作", "cmd.release.create.short": "创建发布", "cmd.release.delete.short": "删除发布", "cmd.release.list.short": "列出发布", @@ -109,16 +83,19 @@ "cmd.webhook.test.short": "触发 Webhook 测试投递", "cmd.webhook.update.short": "更新仓库 Webhook,并在可用时保留未指定字段", "cmd.webhook.view.short": "查看 Webhook 详情", + "cmd.wiki.create.short": "创建 Wiki 页面", + "cmd.wiki.delete.short": "删除 Wiki 页面", + "cmd.wiki.list.short": "列出 Wiki 页面", + "cmd.wiki.short": "Wiki 操作", + "cmd.wiki.update.short": "更新 Wiki 页面", + "cmd.wiki.view.short": "查看 Wiki 页面", "error.auth.delete_token_failed": "删除 Token 失败:{message}", "error.auth.login_failed": "登录失败:{message}", "error.auth.store_token_failed": "保存 Token 失败:{message}", "error.auth.token_empty": "Token 不能为空", "error.config.save_failed": "保存配置失败:{message}", - "error.dataset.delete_confirm": "删除数据集附件具有破坏性;请先 --dry-run 预览,再传 --yes 确认", "error.missing_required_flag": "缺少必需参数 --{name}", - "error.profile.user_required": "无法确定目标用户;请通过 --user 指定,或先运行 gitlink-cli auth login 登录", "error.unsupported_language": "不支持的语言:{lang}", - "flag.all": "自动获取全部分页(忽略 --page)", "flag.api.batch_continue_on_error": "批处理请求失败后继续执行后续请求", "flag.api.batch_dry_run": "预览批处理请求,不发送远端请求", "flag.api.batch_file": "从 JSON 文件读取 API 批处理计划", @@ -127,7 +104,6 @@ "flag.api.body_file": "从文件读取 JSON 请求体", "flag.api.body_stdin": "从标准输入读取 JSON 请求体", "flag.api.header": "附加请求头(key:value)", - "flag.api.paginate": "自动获取 GET 列表接口的全部分页并合并结果", "flag.api.query": "查询参数(key=val&key2=val2)", "flag.auth.token": "通过粘贴已有 Token 登录", "flag.branch.from": "源分支或 Commit", @@ -136,17 +112,6 @@ "flag.ci.stage": "阶段编号", "flag.ci.step": "步骤编号", "flag.comment.body": "评论内容", - "flag.dataset.description": "数据集描述", - "flag.dataset.dry_run": "预览请求,不写入数据集", - "flag.dataset.dry_run_delete": "预览请求,不删除附件", - "flag.dataset.ids": "用于查询数据集的项目 ID,逗号分隔", - "flag.dataset.license_id": "许可证 ID", - "flag.dataset.limit": "附件每页数量", - "flag.dataset.page": "附件页码", - "flag.dataset.paper_content": "研究论文内容", - "flag.dataset.title": "数据集标题", - "flag.dataset.uuid": "附件 UUID", - "flag.dataset.yes": "确认执行破坏性删除", "flag.debug": "启用调试输出", "flag.description": "描述", "flag.doctor.skip_network": "跳过需要访问 GitLink 的认证连通性检查", @@ -202,10 +167,6 @@ "flag.pr.tag_id": "议题标签 ID", "flag.pr.title": "PR 标题", "flag.pr.version_id": "补丁集版本 ID", - "flag.profile.end_time": "结束时间(Unix 时间戳)", - "flag.profile.start_time": "开始时间(Unix 时间戳)", - "flag.profile.user": "目标用户登录名(默认为当前认证用户)", - "flag.profile.year": "贡献热力图的年份(如 2025)", "flag.release.body": "发布说明", "flag.release.id": "发布 ID", "flag.release.id_or_tag": "发布 ID 或标签", @@ -235,6 +196,12 @@ "flag.webhook.secret_update": "Webhook 密钥。如果服务端不返回已有密钥,请再次传入。", "flag.webhook.type": "Webhook 类型:gitea/slack/discord/dingtalk/telegram/msteams/feishu/matrix/jianmu/softbot", "flag.webhook.url": "Webhook 目标 URL", + "flag.wiki.content": "Wiki 页面内容", + "flag.wiki.file": "从文件读取 Wiki 页面内容", + "flag.wiki.message": "提交说明", + "flag.wiki.page": "Wiki 页面名称", + "flag.wiki.project_id": "GitLink 项目 ID(未传入时自动解析)", + "flag.wiki.title": "Wiki 页面标题(默认使用 --page)", "output.auth.env_hint": " 或设置 {env} 环境变量", "output.auth.login_hint": " 运行:gitlink-cli auth login", "output.config.file": "配置文件:{path}", diff --git a/shortcuts/register.go b/shortcuts/register.go index 85ac32d..1840b06 100644 --- a/shortcuts/register.go +++ b/shortcuts/register.go @@ -54,6 +54,7 @@ func RegisterAll(root *cobra.Command, translators ...*i18n.Translator) { "webhook": webhook.Shortcuts(tr), "wiki": wiki.Shortcuts(), "workflow": workflow.Shortcuts(), + "wiki": wiki.Shortcuts(tr), } descriptions := map[string]string{ @@ -77,6 +78,7 @@ func RegisterAll(root *cobra.Command, translators ...*i18n.Translator) { "webhook": tr.T("cmd.webhook.short"), "wiki": "Wiki page operations", "workflow": "AI agent workflow analysis", + "wiki": tr.T("cmd.wiki.short"), } for name, shortcuts := range groups { diff --git a/shortcuts/register_test.go b/shortcuts/register_test.go index 3e633cf..8ea2cc2 100644 --- a/shortcuts/register_test.go +++ b/shortcuts/register_test.go @@ -11,10 +11,10 @@ func TestRegisterAll(t *testing.T) { RegisterAll(root) expectedGroups := []string{ - "repo", "issue", "label", "license", "pr", "profile", "release", "branch", - "org", "user", "search", "tag", "commit", "ci", "workflow", + "repo", "issue", "label", "license", "pr", "release", "branch", + "org", "user", "search", "ci", "workflow", "compare", "member", "milestone", "pipeline", "webhook", - "dataset", "health", "ignore", "wiki", "feishu", + "health", "wiki", } groupSet := map[string]bool{} diff --git a/shortcuts/wiki/wiki.go b/shortcuts/wiki/wiki.go index 870f51a..18b814c 100644 --- a/shortcuts/wiki/wiki.go +++ b/shortcuts/wiki/wiki.go @@ -1,129 +1,297 @@ package wiki import ( + "encoding/base64" + "errors" "fmt" + "net/url" + "os" + "strconv" + "strings" + "github.com/gitlink-org/gitlink-cli/internal/client" + "github.com/gitlink-org/gitlink-cli/internal/i18n" + "github.com/gitlink-org/gitlink-cli/internal/output" "github.com/gitlink-org/gitlink-cli/shortcuts/common" ) -// Shortcuts returns wiki management shortcuts for GitLink. -// -// The wiki domain provides commands for listing, viewing, creating, -// updating, and deleting wiki pages within a repository. -func Shortcuts() []*common.Shortcut { +const ( + wikiListPath = "/api/wiki/wikiPages" + wikiViewPath = "/api/wiki/getWiki" + wikiCreatePath = "/api/wiki/createWiki" + wikiUpdatePath = "/api/wiki/updateWiki" + wikiDeletePath = "/api/wiki/deleteWiki" +) + +func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut { + tr := shortcutTranslator(translators...) return []*common.Shortcut{ { - Name: "pages", - Description: "列出 Wiki 页面", - Run: func(ctx *common.RuntimeContext) error { - if err := ctx.ResolveOwnerRepo(); err != nil { - return err - } - env, err := ctx.CallAPI("GET", "/api/wiki/wikiPages", nil) - if err != nil { - return err - } - return ctx.Output(env) + Name: "list", + Description: tr.T("cmd.wiki.list.short"), + Flags: []common.Flag{ + {Name: "project-id", Usage: tr.T("flag.wiki.project_id")}, }, + Run: runListWikiPages, }, { - Name: "get", - Description: "获取 Wiki 页面内容", + Name: "view", + Description: tr.T("cmd.wiki.view.short"), Flags: []common.Flag{ - {Name: "id", Short: "i", Usage: "Wiki 页面 ID", Required: true}, - }, - Run: func(ctx *common.RuntimeContext) error { - id, err := ctx.RequireArg("id") - if err != nil { - return err - } - path := fmt.Sprintf("/api/wiki/getWiki?id=%s", id) - env, err := ctx.CallAPI("GET", path, nil) - if err != nil { - return err - } - return ctx.Output(env) + {Name: "page", Short: "p", Usage: tr.T("flag.wiki.page"), Required: true}, + {Name: "project-id", Usage: tr.T("flag.wiki.project_id")}, }, + Run: runViewWikiPage, }, { Name: "create", - Description: "创建 Wiki 页面", + Description: tr.T("cmd.wiki.create.short"), Flags: []common.Flag{ - {Name: "title", Short: "t", Usage: "页面标题", Required: true}, - {Name: "content", Short: "c", Usage: "页面内容(Markdown)", Required: true}, - {Name: "project", Usage: "项目 ID"}, - }, - Run: func(ctx *common.RuntimeContext) error { - if err := ctx.ResolveOwnerRepo(); err != nil { - return err - } - title, err := ctx.RequireArg("title") - if err != nil { - return err - } - content, err := ctx.RequireArg("content") - if err != nil { - return err - } - body := map[string]interface{}{ - "title": title, - "content": content, - } - if project := ctx.Arg("project"); project != "" { - body["project_id"] = project - } - env, err := ctx.CallAPI("POST", "/api/wiki/createWiki", body) - if err != nil { - return err - } - return ctx.Output(env) + {Name: "page", Short: "p", Usage: tr.T("flag.wiki.page"), Required: true}, + {Name: "title", Short: "t", Usage: tr.T("flag.wiki.title")}, + {Name: "content", Short: "c", Usage: tr.T("flag.wiki.content")}, + {Name: "file", Short: "f", Usage: tr.T("flag.wiki.file")}, + {Name: "message", Short: "m", Usage: tr.T("flag.wiki.message")}, + {Name: "project-id", Usage: tr.T("flag.wiki.project_id")}, }, + Run: runCreateWikiPage, }, { Name: "update", - Description: "更新 Wiki 页面", + Description: tr.T("cmd.wiki.update.short"), Flags: []common.Flag{ - {Name: "id", Short: "i", Usage: "Wiki 页面 ID", Required: true}, - {Name: "title", Short: "t", Usage: "新标题"}, - {Name: "content", Short: "c", Usage: "新内容(Markdown)"}, - }, - Run: func(ctx *common.RuntimeContext) error { - id, err := ctx.RequireArg("id") - if err != nil { - return err - } - body := map[string]interface{}{"id": id} - if t := ctx.Arg("title"); t != "" { - body["title"] = t - } - if c := ctx.Arg("content"); c != "" { - body["content"] = c - } - env, err := ctx.CallAPI("PUT", "/api/wiki/updateWiki", body) - if err != nil { - return err - } - return ctx.Output(env) + {Name: "page", Short: "p", Usage: tr.T("flag.wiki.page"), Required: true}, + {Name: "title", Short: "t", Usage: tr.T("flag.wiki.title")}, + {Name: "content", Short: "c", Usage: tr.T("flag.wiki.content")}, + {Name: "file", Short: "f", Usage: tr.T("flag.wiki.file")}, + {Name: "message", Short: "m", Usage: tr.T("flag.wiki.message")}, + {Name: "project-id", Usage: tr.T("flag.wiki.project_id")}, }, + Run: runUpdateWikiPage, }, { Name: "delete", - Description: "删除 Wiki 页面", + Description: tr.T("cmd.wiki.delete.short"), Flags: []common.Flag{ - {Name: "id", Short: "i", Usage: "Wiki 页面 ID", Required: true}, - }, - Run: func(ctx *common.RuntimeContext) error { - id, err := ctx.RequireArg("id") - if err != nil { - return err - } - body := map[string]interface{}{"id": id} - env, err := ctx.CallAPI("POST", "/api/wiki/deleteWiki", body) - if err != nil { - return err - } - return ctx.Output(env) + {Name: "page", Short: "p", Usage: tr.T("flag.wiki.page"), Required: true}, + {Name: "project-id", Usage: tr.T("flag.wiki.project_id")}, }, + Run: runDeleteWikiPage, }, } } + +func shortcutTranslator(translators ...*i18n.Translator) *i18n.Translator { + if len(translators) > 0 && translators[0] != nil { + return translators[0] + } + return i18n.Default() +} + +func runListWikiPages(ctx *common.RuntimeContext) error { + if err := ctx.ResolveOwnerRepo(); err != nil { + return err + } + query, err := buildWikiQuery(ctx, "") + if err != nil { + return err + } + env, err := callWikiAPIWithQuery(ctx, "GET", wikiListPath, query) + if err != nil { + return err + } + return ctx.Output(env) +} + +func runViewWikiPage(ctx *common.RuntimeContext) error { + if err := ctx.ResolveOwnerRepo(); err != nil { + return err + } + page, err := ctx.RequireArg("page") + if err != nil { + return err + } + query, err := buildWikiQuery(ctx, page) + if err != nil { + return err + } + env, err := callWikiAPIWithQuery(ctx, "GET", wikiViewPath, query) + if err != nil { + return err + } + return ctx.Output(env) +} + +func runCreateWikiPage(ctx *common.RuntimeContext) error { + if err := ctx.ResolveOwnerRepo(); err != nil { + return err + } + payload, err := buildWikiWritePayload(ctx, true) + if err != nil { + return err + } + env, err := callWikiAPI(ctx, "POST", wikiCreatePath, payload) + if err != nil { + return err + } + return ctx.Output(env) +} + +func runUpdateWikiPage(ctx *common.RuntimeContext) error { + if err := ctx.ResolveOwnerRepo(); err != nil { + return err + } + payload, err := buildWikiWritePayload(ctx, false) + if err != nil { + return err + } + env, err := callWikiAPI(ctx, "PUT", wikiUpdatePath, payload) + if err != nil { + return err + } + return ctx.Output(env) +} + +func runDeleteWikiPage(ctx *common.RuntimeContext) error { + if err := ctx.ResolveOwnerRepo(); err != nil { + return err + } + page, err := ctx.RequireArg("page") + if err != nil { + return err + } + projectID, err := resolveProjectID(ctx) + if err != nil { + return err + } + payload := map[string]interface{}{ + "owner": ctx.Owner, + "repo": ctx.Repo, + "projectId": projectID, + "pageName": page, + } + env, err := callWikiAPI(ctx, "DELETE", wikiDeletePath, payload) + if err != nil { + return err + } + return ctx.Output(env) +} + +func buildWikiQuery(ctx *common.RuntimeContext, page string) (url.Values, error) { + projectID, err := resolveProjectID(ctx) + if err != nil { + return nil, err + } + query := url.Values{} + query.Set("owner", ctx.Owner) + query.Set("repo", ctx.Repo) + query.Set("projectId", strconv.Itoa(projectID)) + if page != "" { + query.Set("pageName", page) + } + return query, nil +} + +func callWikiAPI(ctx *common.RuntimeContext, method, path string, body interface{}) (*output.Envelope, error) { + env, err := ctx.CallAPI(method, path, body) + if err != nil { + return nil, wrapWikiAPIError(path, err) + } + return env, nil +} + +func callWikiAPIWithQuery(ctx *common.RuntimeContext, method, path string, query url.Values) (*output.Envelope, error) { + env, err := ctx.CallAPIWithQuery(method, path, query) + if err != nil { + return nil, wrapWikiAPIError(path, err) + } + return env, nil +} + +func wrapWikiAPIError(path string, err error) error { + var apiErr *client.APIError + if errors.As(err, &apiErr) && apiErr.StatusCode == 404 { + return fmt.Errorf("GitLink Wiki API %s returned 404; confirm the repository has Wiki enabled and the Wiki OpenAPI is available: %w", path, err) + } + return err +} + +func buildWikiWritePayload(ctx *common.RuntimeContext, requireContent bool) (map[string]interface{}, error) { + page, err := ctx.RequireArg("page") + if err != nil { + return nil, err + } + projectID, err := resolveProjectID(ctx) + if err != nil { + return nil, err + } + title := ctx.Arg("title") + if title == "" { + title = page + } + + payload := map[string]interface{}{ + "owner": ctx.Owner, + "repo": ctx.Repo, + "projectId": projectID, + "pageName": page, + "title": title, + } + if message := ctx.Arg("message"); message != "" { + payload["message"] = message + } + contentBase64, ok, err := readWikiContent(ctx) + if err != nil { + return nil, err + } + if ok { + payload["content_base64"] = contentBase64 + } else if requireContent { + return nil, fmt.Errorf("one of --content or --file is required") + } + return payload, nil +} + +func readWikiContent(ctx *common.RuntimeContext) (string, bool, error) { + content := ctx.Arg("content") + filePath := ctx.Arg("file") + if content != "" && filePath != "" { + return "", false, fmt.Errorf("use only one of --content or --file") + } + if filePath != "" { + data, err := os.ReadFile(filePath) + if err != nil { + return "", false, fmt.Errorf("read wiki file: %w", err) + } + return base64.StdEncoding.EncodeToString(data), true, nil + } + if content != "" { + return base64.StdEncoding.EncodeToString([]byte(content)), true, nil + } + return "", false, nil +} + +func resolveProjectID(ctx *common.RuntimeContext) (int, error) { + if value := strings.TrimSpace(ctx.Arg("project-id")); value != "" { + projectID, err := strconv.Atoi(value) + if err != nil || projectID <= 0 { + return 0, fmt.Errorf("invalid --project-id %q", value) + } + return projectID, nil + } + + env, err := ctx.CallAPI("GET", ctx.RepoPath(), nil) + if err != nil { + return 0, fmt.Errorf("resolve project ID: %w", err) + } + data, ok := env.Data.(map[string]interface{}) + if !ok { + return 0, fmt.Errorf("unexpected repository response format") + } + for _, key := range []string{"project_id", "id"} { + if value, ok := data[key].(float64); ok && value > 0 { + return int(value), nil + } + } + return 0, fmt.Errorf("repository response missing project_id") +} diff --git a/shortcuts/wiki/wiki_test.go b/shortcuts/wiki/wiki_test.go index 7712815..0db3cb4 100644 --- a/shortcuts/wiki/wiki_test.go +++ b/shortcuts/wiki/wiki_test.go @@ -1,9 +1,13 @@ package wiki import ( + "encoding/base64" "encoding/json" + "fmt" "net/http" "net/http/httptest" + "os" + "path/filepath" "strings" "testing" @@ -11,253 +15,216 @@ import ( "github.com/gitlink-org/gitlink-cli/shortcuts/common" ) -func TestWikiPages(t *testing.T) { - tests := []struct { - name string - mockStatus int - mockBody string - wantErr bool - errContains string - }{ - {"正常返回", 200, `{"wikiPages": []}`, false, ""}, - {"API 404", 404, `{"error": "not found"}`, true, "404"}, - {"返回 HTML", 200, `Login`, true, "HTML"}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method != "GET" { - t.Errorf("expected GET, got %s", r.Method) - } - w.WriteHeader(tt.mockStatus) - w.Write([]byte(tt.mockBody)) - })) - defer server.Close() - - shortcut := findWikiShortcut(t, "pages") - ctx := &common.RuntimeContext{ - Client: &client.Client{HTTP: server.Client(), BaseURL: server.URL}, - Owner: "test", Repo: "test", Format: "json", - Args: map[string]string{}, - } - err := shortcut.Run(ctx) - - if tt.wantErr && err == nil { - t.Fatal("期望错误但为 nil") - } - if !tt.wantErr && err != nil { - t.Fatalf("不期望错误: %v", err) - } - if tt.wantErr && tt.errContains != "" && err != nil { - if !strings.Contains(err.Error(), tt.errContains) { - t.Errorf("错误应包含 %q: %s", tt.errContains, err.Error()) - } - } - }) - } -} - -func TestWikiGet(t *testing.T) { - tests := []struct { - name string - args map[string]string - mockStatus int - mockBody string - wantErr bool - errContains string - }{ - {"正常获取", map[string]string{"id": "42"}, 200, `{"id": 42, "title": "Home"}`, false, ""}, - {"缺少 id", map[string]string{}, 200, `{}`, true, ""}, - {"API 404", map[string]string{"id": "999"}, 404, `{"error": "not found"}`, true, "404"}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if !strings.Contains(r.URL.Path, "/api/wiki/getWiki") { - t.Errorf("expected path containing /api/wiki/getWiki, got %s", r.URL.Path) - } - w.WriteHeader(tt.mockStatus) - w.Write([]byte(tt.mockBody)) - })) - defer server.Close() - - shortcut := findWikiShortcut(t, "get") - ctx := &common.RuntimeContext{ - Client: &client.Client{HTTP: server.Client(), BaseURL: server.URL}, - Owner: "test", Repo: "test", Format: "json", - Args: tt.args, - } - err := shortcut.Run(ctx) - - if tt.wantErr && err == nil { - t.Fatal("期望错误但为 nil") - } - if !tt.wantErr && err != nil { - t.Fatalf("不期望错误: %v", err) - } - }) - } -} - -func TestWikiCreate(t *testing.T) { - var payload map[string]interface{} +func TestWikiListAutoResolvesProjectID(t *testing.T) { + called := false server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method != "POST" { - t.Errorf("expected POST, got %s", r.Method) + switch { + case r.Method == "GET" && r.URL.Path == "/owner/repo.json": + writeJSON(t, w, map[string]interface{}{"project_id": float64(1546652)}) + case r.Method == "GET" && r.URL.Path == "/api/wiki/wikiPages.json": + called = true + query := r.URL.Query() + assertEqual(t, query.Get("owner"), "owner") + assertEqual(t, query.Get("repo"), "repo") + assertEqual(t, query.Get("projectId"), "1546652") + writeJSON(t, w, map[string]interface{}{"message": "success", "data": []interface{}{}}) + default: + t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path) } - if !strings.Contains(r.URL.Path, "/api/wiki/createWiki") { - t.Errorf("expected path containing /api/wiki/createWiki, got %s", r.URL.Path) - } - payload = decodeWikiJSON(t, r) - w.WriteHeader(200) - w.Write([]byte(`{"status": 0, "message": "success"}`)) })) defer server.Close() - shortcut := findWikiShortcut(t, "create") - ctx := &common.RuntimeContext{ - Client: &client.Client{HTTP: server.Client(), BaseURL: server.URL}, - Owner: "test", Repo: "test", Format: "json", - Args: map[string]string{ - "title": "Getting Started", - "content": "# Hello\nWelcome to the wiki", - }, + err := runWikiShortcut(t, server, "list", map[string]string{}) + if err != nil { + t.Fatalf("list shortcut failed: %v", err) } - if err := shortcut.Run(ctx); err != nil { + if !called { + t.Fatal("wiki list endpoint was not called") + } +} + +func TestWikiViewUsesExplicitProjectID(t *testing.T) { + called := false + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" || r.URL.Path != "/api/wiki/getWiki.json" { + t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path) + } + called = true + query := r.URL.Query() + assertEqual(t, query.Get("owner"), "owner") + assertEqual(t, query.Get("repo"), "repo") + assertEqual(t, query.Get("projectId"), "42") + assertEqual(t, query.Get("pageName"), "Home") + writeJSON(t, w, map[string]interface{}{"message": "success", "data": map[string]interface{}{"title": "Home"}}) + })) + defer server.Close() + + err := runWikiShortcut(t, server, "view", map[string]string{ + "page": "Home", + "project-id": "42", + }) + if err != nil { + t.Fatalf("view shortcut failed: %v", err) + } + if !called { + t.Fatal("wiki view endpoint was not called") + } +} + +func TestWikiCreateBuildsPayloadFromContent(t *testing.T) { + var payload map[string]interface{} + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == "GET" && r.URL.Path == "/owner/repo.json": + writeJSON(t, w, map[string]interface{}{"project_id": float64(1546652)}) + case r.Method == "POST" && r.URL.Path == "/api/wiki/createWiki.json": + payload = decodeJSON(t, r) + writeJSON(t, w, map[string]interface{}{"message": "201"}) + default: + t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path) + } + })) + defer server.Close() + + err := runWikiShortcut(t, server, "create", map[string]string{ + "page": "Home", + "title": "Welcome", + "content": "hello wiki", + "message": "Add Home", + }) + if err != nil { t.Fatalf("create shortcut failed: %v", err) } - if payload["title"] != "Getting Started" { - t.Errorf("expected title 'Getting Started', got %v", payload["title"]) - } - if payload["content"] != "# Hello\nWelcome to the wiki" { - t.Errorf("unexpected content: %v", payload["content"]) - } + + assertEqual(t, payload["owner"], "owner") + assertEqual(t, payload["repo"], "repo") + assertEqual(t, payload["projectId"], float64(1546652)) + assertEqual(t, payload["pageName"], "Home") + assertEqual(t, payload["title"], "Welcome") + assertEqual(t, payload["message"], "Add Home") + assertEqual(t, payload["content_base64"], base64.StdEncoding.EncodeToString([]byte("hello wiki"))) } -func TestWikiCreateWithProject(t *testing.T) { +func TestWikiUpdateBuildsPayloadFromFile(t *testing.T) { + tempDir := t.TempDir() + filePath := filepath.Join(tempDir, "wiki.md") + if err := os.WriteFile(filePath, []byte("# Updated\n"), 0o644); err != nil { + t.Fatalf("write wiki file: %v", err) + } + var payload map[string]interface{} server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - payload = decodeWikiJSON(t, r) - w.WriteHeader(200) - w.Write([]byte(`{"status": 0}`)) - })) - defer server.Close() - - shortcut := findWikiShortcut(t, "create") - ctx := &common.RuntimeContext{ - Client: &client.Client{HTTP: server.Client(), BaseURL: server.URL}, - Owner: "test", Repo: "test", Format: "json", - Args: map[string]string{ - "title": "Test", - "content": "Body", - "project": "123", - }, - } - if err := shortcut.Run(ctx); err != nil { - t.Fatalf("create shortcut failed: %v", err) - } - if payload["project_id"] != "123" { - t.Errorf("expected project_id '123', got %v", payload["project_id"]) - } -} - -func TestWikiCreateMissingTitle(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - t.Fatal("should not call API when --title is missing") - })) - defer server.Close() - - shortcut := findWikiShortcut(t, "create") - ctx := &common.RuntimeContext{ - Client: &client.Client{HTTP: server.Client(), BaseURL: server.URL}, - Owner: "test", Repo: "test", Format: "json", - Args: map[string]string{"content": "only content"}, - } - if err := shortcut.Run(ctx); err == nil { - t.Fatal("expected error when --title is missing") - } -} - -func TestWikiUpdate(t *testing.T) { - var payload map[string]interface{} - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method != "PUT" { - t.Errorf("expected PUT, got %s", r.Method) + if r.Method != "PUT" || r.URL.Path != "/api/wiki/updateWiki.json" { + t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path) } - if !strings.Contains(r.URL.Path, "/api/wiki/updateWiki") { - t.Errorf("expected path containing /api/wiki/updateWiki, got %s", r.URL.Path) - } - payload = decodeWikiJSON(t, r) - w.WriteHeader(200) - w.Write([]byte(`{"status": 0, "message": "success"}`)) + payload = decodeJSON(t, r) + writeJSON(t, w, map[string]interface{}{"message": "success"}) })) defer server.Close() - shortcut := findWikiShortcut(t, "update") - ctx := &common.RuntimeContext{ - Client: &client.Client{HTTP: server.Client(), BaseURL: server.URL}, - Owner: "test", Repo: "test", Format: "json", - Args: map[string]string{ - "id": "42", - "title": "Updated Title", - "content": "Updated content", - }, - } - if err := shortcut.Run(ctx); err != nil { + err := runWikiShortcut(t, server, "update", map[string]string{ + "page": "Home", + "file": filePath, + "project-id": "42", + }) + if err != nil { t.Fatalf("update shortcut failed: %v", err) } - if payload["id"] != "42" { - t.Errorf("expected id '42', got %v", payload["id"]) - } - if payload["title"] != "Updated Title" { - t.Errorf("expected title 'Updated Title', got %v", payload["title"]) - } + + assertEqual(t, payload["projectId"], float64(42)) + assertEqual(t, payload["pageName"], "Home") + assertEqual(t, payload["title"], "Home") + assertEqual(t, payload["content_base64"], base64.StdEncoding.EncodeToString([]byte("# Updated\n"))) } -func TestWikiDelete(t *testing.T) { +func TestWikiDeleteSendsBody(t *testing.T) { var payload map[string]interface{} server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method != "POST" { - t.Errorf("expected POST, got %s", r.Method) + if r.Method != "DELETE" || r.URL.Path != "/api/wiki/deleteWiki.json" { + t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path) } - if !strings.Contains(r.URL.Path, "/api/wiki/deleteWiki") { - t.Errorf("expected path containing /api/wiki/deleteWiki, got %s", r.URL.Path) - } - payload = decodeWikiJSON(t, r) - w.WriteHeader(200) - w.Write([]byte(`{"status": 0, "message": "success"}`)) + payload = decodeJSON(t, r) + writeJSON(t, w, map[string]interface{}{"message": "success"}) })) defer server.Close() - shortcut := findWikiShortcut(t, "delete") - ctx := &common.RuntimeContext{ - Client: &client.Client{HTTP: server.Client(), BaseURL: server.URL}, - Owner: "test", Repo: "test", Format: "json", - Args: map[string]string{"id": "42"}, - } - if err := shortcut.Run(ctx); err != nil { + err := runWikiShortcut(t, server, "delete", map[string]string{ + "page": "Home", + "project-id": "42", + }) + if err != nil { t.Fatalf("delete shortcut failed: %v", err) } - if payload["id"] != "42" { - t.Errorf("expected id '42', got %v", payload["id"]) + + assertEqual(t, payload["owner"], "owner") + assertEqual(t, payload["repo"], "repo") + assertEqual(t, payload["projectId"], float64(42)) + assertEqual(t, payload["pageName"], "Home") +} + +func TestWikiOpenAPI404HasActionableError(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" || r.URL.Path != "/api/wiki/wikiPages.json" { + t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path) + } + writeJSON(t, w, map[string]interface{}{"status": 404, "message": "not found"}) + })) + defer server.Close() + + err := runWikiShortcut(t, server, "list", map[string]string{ + "project-id": "42", + }) + if err == nil { + t.Fatal("expected wiki API 404 to fail") + } + if !strings.Contains(err.Error(), "GitLink Wiki API /api/wiki/wikiPages returned 404") { + t.Fatalf("unexpected error: %v", err) + } + if !strings.Contains(err.Error(), "Wiki OpenAPI is available") { + t.Fatalf("expected actionable wiki API hint, got: %v", err) } } -func TestWikiDeleteMissingId(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - t.Fatal("should not call API when --id is missing") - })) +func TestWikiRejectsInvalidInput(t *testing.T) { + server := httptest.NewServer(http.NotFoundHandler()) defer server.Close() - shortcut := findWikiShortcut(t, "delete") + if err := runWikiShortcut(t, server, "view", map[string]string{ + "page": "Home", + "project-id": "abc", + }); err == nil { + t.Fatal("expected invalid project-id to fail") + } + if err := runWikiShortcut(t, server, "create", map[string]string{ + "page": "Home", + "project-id": "42", + }); err == nil { + t.Fatal("expected missing content to fail") + } + if err := runWikiShortcut(t, server, "create", map[string]string{ + "page": "Home", + "project-id": "42", + "content": "hello", + "file": "wiki.md", + }); err == nil { + t.Fatal("expected content/file conflict to fail") + } +} + +func runWikiShortcut(t *testing.T, server *httptest.Server, name string, args map[string]string) error { + t.Helper() + shortcut := findWikiShortcut(t, name) ctx := &common.RuntimeContext{ - Client: &client.Client{HTTP: server.Client(), BaseURL: server.URL}, - Owner: "test", Repo: "test", Format: "json", - Args: map[string]string{}, - } - if err := shortcut.Run(ctx); err == nil { - t.Fatal("expected error when --id is missing") + Client: &client.Client{ + HTTP: server.Client(), + BaseURL: server.URL, + }, + Owner: "owner", + Repo: "repo", + Format: "json", + Args: args, } + return shortcut.Run(ctx) } func findWikiShortcut(t *testing.T, name string) *common.Shortcut { @@ -271,7 +238,7 @@ func findWikiShortcut(t *testing.T, name string) *common.Shortcut { return nil } -func decodeWikiJSON(t *testing.T, r *http.Request) map[string]interface{} { +func decodeJSON(t *testing.T, r *http.Request) map[string]interface{} { t.Helper() var payload map[string]interface{} if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { @@ -279,3 +246,18 @@ func decodeWikiJSON(t *testing.T, r *http.Request) map[string]interface{} { } return payload } + +func writeJSON(t *testing.T, w http.ResponseWriter, payload interface{}) { + t.Helper() + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(payload); err != nil { + t.Fatalf("failed to write response: %v", err) + } +} + +func assertEqual(t *testing.T, got interface{}, want interface{}) { + t.Helper() + if fmt.Sprintf("%v", got) != fmt.Sprintf("%v", want) { + t.Fatalf("got %v (%T), want %v (%T)", got, got, want, want) + } +} diff --git a/skills/gitlink-wiki/SKILL.md b/skills/gitlink-wiki/SKILL.md index c022e23..def9c6a 100644 --- a/skills/gitlink-wiki/SKILL.md +++ b/skills/gitlink-wiki/SKILL.md @@ -1,52 +1,43 @@ --- name: gitlink-wiki version: 1.0.0 -description: "Wiki 操作:查看、创建、更新、删除 Wiki 页面。当用户需要管理 GitLink 仓库 Wiki 时触发。" +description: "Wiki management: list, view, create, update, and delete repository wiki pages." metadata: requires: bins: ["gitlink-cli"] cliHelp: "gitlink-cli wiki --help" + shortcuts: + - "wiki +list" + - "wiki +view" + - "wiki +create" + - "wiki +update" + - "wiki +delete" --- -# gitlink-wiki(Wiki 操作) - -**CRITICAL — 开始前必须先阅读 [`../gitlink-shared/SKILL.md`](../gitlink-shared/SKILL.md),其中包含认证、权限处理和 API 注意事项。** -**CRITICAL — 所有 Shortcuts 在执行写入/删除操作前,务必先确认用户意图。** -**CRITICAL — GitLink 操作只能用 `gitlink-cli`。禁止用 `gh`(GitHub CLI)操作 GitLink 资源。`gh` 仅适用于 GitHub 平台。** - -> **前置条件:** 先阅读 [`../gitlink-shared/SKILL.md`](../gitlink-shared/SKILL.md) 了解认证和全局参数。 +# gitlink-wiki ## Shortcuts -| Shortcut | 说明 | 需要认证 | -|----------|------|----------| -| `wiki +list` | 列出 Wiki 页面 | 是 | -| `wiki +view` | 查看 Wiki 页面内容 | 是 | -| `wiki +create` | 创建 Wiki 页面 | 是 | -| `wiki +update` | 更新 Wiki 页面 | 是 | -| `wiki +delete` | 删除 Wiki 页面 | 是 | +| Shortcut | Description | +|----------|-------------| +| `wiki +list` | List repository wiki pages | +| `wiki +view` | View a wiki page | +| `wiki +create` | Create a wiki page | +| `wiki +update` | Update a wiki page | +| `wiki +delete` | Delete a wiki page | -## 使用示例 +## Examples ```bash -# 列出所有 Wiki 页面 -gitlink-cli wiki +list --owner myuser --repo myrepo - -# 查看 Wiki 页面 -gitlink-cli wiki +view --name Home - -# 创建 Wiki 页面 -gitlink-cli wiki +create --name Guide --content "使用指南内容" - -# 更新 Wiki 页面(带提交信息) -gitlink-cli wiki +update --name Guide --content "更新后的内容" --message "更新使用指南" - -# 删除 Wiki 页面 -gitlink-cli wiki +delete --name OldPage +gitlink-cli wiki +list --owner Gitlink --repo forgeplus +gitlink-cli wiki +view --owner Gitlink --repo forgeplus --page Home +gitlink-cli wiki +create --owner Gitlink --repo forgeplus \ + --page Home --title Home --content "Welcome to the project wiki" +gitlink-cli wiki +update --owner Gitlink --repo forgeplus \ + --page Home --file docs/wiki-home.md --message "Update Home" +gitlink-cli wiki +delete --owner Gitlink --repo forgeplus --page Home ``` -## 注意事项 +## Notes -- Wiki 命令会自动从仓库信息中获取 `projectId`,无需手动指定 -- `--content` 参数的内容会自动进行 base64 编码 -- 在 git 仓库目录下执行时,`--owner` 和 `--repo` 会自动解析 +`--project-id` is optional. When omitted, the CLI reads repository metadata and uses the returned `project_id`.