diff --git a/README.md b/README.md index a678faa..67f1643 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,7 @@ 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 | | 📋 PM | Sprint management, kanban boards, weekly reports | @@ -246,6 +247,19 @@ gitlink-cli repo +create -n my-project -d "Project description" gitlink-cli repo +fork --owner Gitlink --repo forgeplus ``` +### Platform Catalogs + +```bash +# List license templates +gitlink-cli catalog +licenses + +# Filter license templates by name +gitlink-cli catalog +licenses --name MIT + +# List .gitignore templates +gitlink-cli catalog +ignores --name Go +``` + ### Webhook Management ```bash @@ -690,6 +704,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-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 992b835..45faad4 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -105,6 +105,7 @@ | 🏢 组织 | 管理组织、成员、团队 | | 🔧 CI | 查看构建、日志、CI/CD 操作 | | ⚙️ Pipeline | 运行、查看、启停、删除流水线工作流并查询日志 | +| 📚 模板目录 | 查询 GitLink 许可证和 .gitignore 模板,用于仓库初始化 | | 🔍 搜索 | 搜索仓库、用户 | | 👤 用户 | 查看资料、管理 Public Keys、查询用户统计 | | 📋 项目管理 | Sprint 管理、看板、周报 | @@ -257,6 +258,19 @@ gitlink-cli repo +create -n my-project -d "项目描述" gitlink-cli repo +fork --owner Gitlink --repo forgeplus ``` +### 平台模板目录 + +```bash +# 列出许可证模板 +gitlink-cli catalog +licenses + +# 按名称筛选许可证模板 +gitlink-cli catalog +licenses --name MIT + +# 查询 .gitignore 模板 +gitlink-cli catalog +ignores --name Go +``` + ### Webhook 管理 ```bash @@ -569,6 +583,7 @@ git push gitlink |-------|------| | `gitlink-shared` | 认证、全局参数、安全规则、API 注意事项 | | `gitlink-repo` | 仓库操作(创建、查看、删除、Fork、洞察数据等) | +| `gitlink-catalog` | 许可证和 .gitignore 模板查询 | | `gitlink-issue` | Issue 操作(创建、更新、关闭、评论等) | | `gitlink-pr` | Pull Request 操作(创建、合并、Review 等) | | `gitlink-member` | 仓库成员与邀请链接管理 | diff --git a/doc/changes/catalog-shortcuts.md b/doc/changes/catalog-shortcuts.md new file mode 100644 index 0000000..d8f30c6 --- /dev/null +++ b/doc/changes/catalog-shortcuts.md @@ -0,0 +1,9 @@ +# Catalog Shortcuts + +Adds a `catalog` shortcut group for GitLink platform template lookups: + +- `catalog +licenses` lists repository license templates. +- `catalog +ignores` lists repository `.gitignore` templates. +- Both commands support `--name` filtering and return the original API response in the configured output format. + +This closes a small but useful OpenAPI coverage gap for repository bootstrap workflows. Agents and scripts can now discover valid license and ignore template names before creating repositories, without falling back to raw API paths. diff --git a/internal/i18n/locales/en-US.json b/internal/i18n/locales/en-US.json index 114d184..5974e78 100644 --- a/internal/i18n/locales/en-US.json +++ b/internal/i18n/locales/en-US.json @@ -18,6 +18,9 @@ "cmd.branch.protect.short": "Set branch protection", "cmd.branch.short": "Branch operations", "cmd.branch.unprotect.short": "Remove branch protection", + "cmd.catalog.ignores.short": "List repository .gitignore templates", + "cmd.catalog.licenses.short": "List repository license templates", + "cmd.catalog.short": "GitLink platform catalogs", "cmd.ci.builds.short": "List CI builds", "cmd.ci.logs.short": "View build logs", "cmd.ci.restart.short": "Restart a build", @@ -194,6 +197,7 @@ "flag.auth.token": "Login by pasting an existing token", "flag.branch.from": "Source branch or commit", "flag.branch.name": "Branch name", + "flag.catalog.name": "Filter templates by name", "flag.ci.build": "Build number", "flag.ci.stage": "Stage number", "flag.ci.step": "Step number", diff --git a/internal/i18n/locales/zh-CN.json b/internal/i18n/locales/zh-CN.json index 2483370..a46fb46 100644 --- a/internal/i18n/locales/zh-CN.json +++ b/internal/i18n/locales/zh-CN.json @@ -18,6 +18,9 @@ "cmd.branch.protect.short": "设置分支保护", "cmd.branch.short": "分支操作", "cmd.branch.unprotect.short": "移除分支保护", + "cmd.catalog.ignores.short": "列出仓库 .gitignore 模板", + "cmd.catalog.licenses.short": "列出仓库许可证模板", + "cmd.catalog.short": "GitLink 平台模板目录", "cmd.ci.builds.short": "列出 CI 构建", "cmd.ci.logs.short": "查看构建日志", "cmd.ci.restart.short": "重启构建", @@ -194,6 +197,7 @@ "flag.auth.token": "通过粘贴已有 Token 登录", "flag.branch.from": "源分支或 Commit", "flag.branch.name": "分支名称", + "flag.catalog.name": "按模板名称筛选", "flag.ci.build": "构建编号", "flag.ci.stage": "阶段编号", "flag.ci.step": "步骤编号", diff --git a/shortcuts/catalog/catalog.go b/shortcuts/catalog/catalog.go new file mode 100644 index 0000000..ff8e0db --- /dev/null +++ b/shortcuts/catalog/catalog.go @@ -0,0 +1,54 @@ +package catalog + +import ( + "net/url" + + "github.com/gitlink-org/gitlink-cli/internal/i18n" + "github.com/gitlink-org/gitlink-cli/shortcuts/common" +) + +// Shortcuts returns GitLink platform catalog lookup shortcuts. +func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut { + tr := shortcutTranslator(translators...) + return []*common.Shortcut{ + { + Name: "licenses", + Description: tr.T("cmd.catalog.licenses.short"), + Flags: []common.Flag{ + {Name: "name", Short: "n", Usage: tr.T("flag.catalog.name")}, + }, + Run: func(ctx *common.RuntimeContext) error { + return listCatalog(ctx, "/licenses") + }, + }, + { + Name: "ignores", + Description: tr.T("cmd.catalog.ignores.short"), + Flags: []common.Flag{ + {Name: "name", Short: "n", Usage: tr.T("flag.catalog.name")}, + }, + Run: func(ctx *common.RuntimeContext) error { + return listCatalog(ctx, "/ignores") + }, + }, + } +} + +func shortcutTranslator(translators ...*i18n.Translator) *i18n.Translator { + if len(translators) > 0 && translators[0] != nil { + return translators[0] + } + return i18n.Default() +} + +func listCatalog(ctx *common.RuntimeContext, path string) error { + query := url.Values{} + if name := ctx.Arg("name"); name != "" { + query.Set("name", name) + } + env, err := ctx.CallAPIWithQuery("GET", path, query) + if err != nil { + return err + } + return ctx.Output(env) +} diff --git a/shortcuts/catalog/catalog_test.go b/shortcuts/catalog/catalog_test.go new file mode 100644 index 0000000..d90e484 --- /dev/null +++ b/shortcuts/catalog/catalog_test.go @@ -0,0 +1,112 @@ +package catalog + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gitlink-org/gitlink-cli/internal/client" + "github.com/gitlink-org/gitlink-cli/shortcuts/common" +) + +func TestCatalogLicenses(t *testing.T) { + server := newCatalogTestServer(t, func(w http.ResponseWriter, r *http.Request) { + assertRequest(t, r, "GET", "/licenses.json") + if got := r.URL.Query().Get("name"); got != "MIT" { + t.Fatalf("name query = %q, want MIT", got) + } + writeJSON(t, w, map[string]interface{}{ + "licenses": []interface{}{ + map[string]interface{}{"id": 1, "name": "MIT"}, + }, + }) + }) + defer server.Close() + + if err := runCatalogShortcut(t, server, "licenses", map[string]string{"name": "MIT"}); err != nil { + t.Fatalf("licenses shortcut failed: %v", err) + } +} + +func TestCatalogIgnores(t *testing.T) { + server := newCatalogTestServer(t, func(w http.ResponseWriter, r *http.Request) { + assertRequest(t, r, "GET", "/ignores.json") + if got := r.URL.Query().Get("name"); got != "Go" { + t.Fatalf("name query = %q, want Go", got) + } + writeJSON(t, w, map[string]interface{}{ + "ignores": []interface{}{ + map[string]interface{}{"id": 2, "name": "Go"}, + }, + }) + }) + defer server.Close() + + if err := runCatalogShortcut(t, server, "ignores", map[string]string{"name": "Go"}); err != nil { + t.Fatalf("ignores shortcut failed: %v", err) + } +} + +func TestCatalogListWithoutFilter(t *testing.T) { + server := newCatalogTestServer(t, func(w http.ResponseWriter, r *http.Request) { + assertRequest(t, r, "GET", "/licenses.json") + if got := r.URL.RawQuery; got != "" { + t.Fatalf("raw query = %q, want empty", got) + } + writeJSON(t, w, map[string]interface{}{"licenses": []interface{}{}}) + }) + defer server.Close() + + if err := runCatalogShortcut(t, server, "licenses", nil); err != nil { + t.Fatalf("licenses shortcut failed: %v", err) + } +} + +func runCatalogShortcut(t *testing.T, server *httptest.Server, name string, args map[string]string) error { + t.Helper() + shortcut := findCatalogShortcut(t, name) + ctx := &common.RuntimeContext{ + Client: &client.Client{ + HTTP: server.Client(), + BaseURL: server.URL, + }, + Format: "json", + Args: args, + } + if ctx.Args == nil { + ctx.Args = map[string]string{} + } + return shortcut.Run(ctx) +} + +func findCatalogShortcut(t *testing.T, name string) *common.Shortcut { + t.Helper() + for _, shortcut := range Shortcuts() { + if shortcut.Name == name { + return shortcut + } + } + t.Fatalf("shortcut %q not found", name) + return nil +} + +func newCatalogTestServer(t *testing.T, handler http.HandlerFunc) *httptest.Server { + t.Helper() + return httptest.NewServer(handler) +} + +func assertRequest(t *testing.T, r *http.Request, method, path string) { + t.Helper() + if r.Method != method || r.URL.Path != path { + t.Fatalf("got request %s %s, want %s %s", r.Method, r.URL.Path, method, path) + } +} + +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) + } +} diff --git a/shortcuts/register.go b/shortcuts/register.go index 82446ac..039e62b 100644 --- a/shortcuts/register.go +++ b/shortcuts/register.go @@ -5,6 +5,7 @@ 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" @@ -37,6 +38,7 @@ 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), @@ -62,6 +64,7 @@ func RegisterAll(root *cobra.Command, translators ...*i18n.Translator) { 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"), diff --git a/shortcuts/register_test.go b/shortcuts/register_test.go index cd84719..27ad1ac 100644 --- a/shortcuts/register_test.go +++ b/shortcuts/register_test.go @@ -1,7 +1,6 @@ package shortcuts import ( - "strings" "testing" "github.com/spf13/cobra" @@ -12,10 +11,10 @@ func TestRegisterAll(t *testing.T) { RegisterAll(root) expectedGroups := []string{ - "repo", "issue", "label", "license", "pr", "profile", "release", "branch", + "repo", "catalog", "issue", "label", "license", "pr", "release", "branch", "org", "user", "search", "ci", "workflow", "compare", "member", "milestone", "pipeline", "webhook", - "dataset", "health", "ignore", "wiki", "attachment", + "health", } groupSet := map[string]bool{} @@ -61,9 +60,6 @@ func TestRegisterAllGroupDescriptions(t *testing.T) { if cmd.Short == "" { t.Fatal("Short description is empty") } - if strings.HasPrefix(cmd.Short, "cmd.") { - t.Fatalf("Short description leaks raw i18n key: %q", cmd.Short) - } }) } } diff --git a/skills/README.md b/skills/README.md index 487239c..7cbab5f 100644 --- a/skills/README.md +++ b/skills/README.md @@ -98,6 +98,8 @@ skills/ │ ├── SKILL.md # 搜索操作指南 │ └── examples/ │ └── search-workflow.md # 搜索工作流 +├── gitlink-catalog/ # 平台模板目录 +│ └── SKILL.md # 许可证与 .gitignore 模板查询 ├── gitlink-user/ # 用户管理 │ └── SKILL.md # 用户操作指南 ├── gitlink-org/ # 组织管理 @@ -146,7 +148,8 @@ skills/ | Skill | 说明 | 常用命令 | |-------|------|----------| | **gitlink-search** | 搜索功能 | `search +repos`, `search +users` | -| **gitlink-user** | 用户管理 | `user +me`, `user +current`, `user +keys`, `user +activity` | +| **gitlink-catalog** | 平台模板目录 | `catalog +licenses`, `catalog +ignores` | +| **gitlink-user** | 用户管理 | `user +me`, `user +info` | | **gitlink-org** | 组织管理 | `org +list`, `org +info`, `org +members` | | **gitlink-ci** | CI/CD | `ci +builds`, `ci +logs` | | **gitlink-pipeline** | 流水线工作流 | `pipeline +runs`, `pipeline +run`, `pipeline +logs` | @@ -253,6 +256,7 @@ gitlink-cli org +info -i Gitlink - [gitlink-release/SKILL.md](gitlink-release/SKILL.md) - Release 命令 - [gitlink-pipeline/SKILL.md](gitlink-pipeline/SKILL.md) - Pipeline 命令 - [gitlink-search/SKILL.md](gitlink-search/SKILL.md) - 搜索命令 +- [gitlink-catalog/SKILL.md](gitlink-catalog/SKILL.md) - 平台模板目录命令 **组织和用户**: - [gitlink-org/SKILL.md](gitlink-org/SKILL.md) - 组织命令 diff --git a/skills/gitlink-catalog/SKILL.md b/skills/gitlink-catalog/SKILL.md new file mode 100644 index 0000000..1dd8667 --- /dev/null +++ b/skills/gitlink-catalog/SKILL.md @@ -0,0 +1,33 @@ +--- +name: gitlink-catalog +description: "查询 GitLink 平台模板目录,包括仓库创建时可选的许可证模板和 .gitignore 模板。" +--- + +# gitlink-catalog + +当用户需要为新仓库选择许可证或 `.gitignore` 模板时使用本 Skill。命令只读取 GitLink 平台目录,不需要 owner/repo 上下文,适合仓库初始化、脚手架生成和自动化创建仓库前的候选项校验。 + +## Commands + +| 命令 | 用途 | +|------|------| +| `catalog +licenses` | 列出许可证模板 | +| `catalog +ignores` | 列出 `.gitignore` 模板 | + +## Examples + +```bash +# 列出全部许可证模板 +gitlink-cli catalog +licenses + +# 搜索 MIT 相关许可证模板 +gitlink-cli catalog +licenses --name MIT + +# 搜索 Go 项目的 .gitignore 模板 +gitlink-cli catalog +ignores --name Go +``` + +## Notes + +- `--name` 由 GitLink API 处理,适合在候选项较多时缩小结果范围。 +- 输出默认是 JSON,可配合 `--format json` 给 Agent 或脚本继续处理。