From c9d0aad59a534ca3f2865cf5af49c746ffb07a99 Mon Sep 17 00:00:00 2001 From: Mengz <2567587994@qq.com> Date: Tue, 9 Jun 2026 19:21:49 +0800 Subject: [PATCH] =?UTF-8?q?feat(trace):=20=E6=96=B0=E5=A2=9E=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E6=BA=AF=E6=BA=90=E5=88=86=E6=9E=90=E5=BF=AB=E6=8D=B7?= =?UTF-8?q?=E5=91=BD=E4=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 21 ++++ README.zh-CN.md | 21 ++++ doc/changes/trace-shortcuts.md | 13 ++ internal/i18n/locales/en-US.json | 9 ++ internal/i18n/locales/zh-CN.json | 9 ++ shortcuts/register.go | 3 + shortcuts/register_test.go | 2 +- shortcuts/trace/trace.go | 176 +++++++++++++++++++++++++++ shortcuts/trace/trace_test.go | 200 +++++++++++++++++++++++++++++++ skills/README.md | 3 + skills/gitlink-trace/SKILL.md | 52 ++++++++ 11 files changed, 508 insertions(+), 1 deletion(-) create mode 100644 doc/changes/trace-shortcuts.md create mode 100644 shortcuts/trace/trace.go create mode 100644 shortcuts/trace/trace_test.go create mode 100644 skills/gitlink-trace/SKILL.md diff --git a/README.md b/README.md index e5e4318..410fd93 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,7 @@ The official [GitLink](https://www.gitlink.org.cn) CLI tool — built for humans | ⚙️ Pipeline | Run, inspect, enable, disable, delete pipeline workflows and logs | | 🔔 Webhook | Manage repo webhooks and test deliveries | | 📖 Wiki | List, view, create, update, and delete wiki pages | +| 🧭 Trace | Initialize code trace analysis, start scans, list results, fetch reports | | 🔍 Search | Search repositories, users | | 📊 Dataset | Query research datasets by project | | 👤 User | View user profiles and info | @@ -296,6 +297,24 @@ gitlink-cli wiki +update --owner Gitlink --repo forgeplus --project-id 12345 -n gitlink-cli wiki +delete --owner Gitlink --repo forgeplus --project-id 12345 -n old-page ``` +### Code Trace Analysis + +```bash +# Initialize code trace analysis for the current account +gitlink-cli trace +init + +# Start a trace scan for a repository branch +gitlink-cli trace +start --owner Gitlink --repo forgeplus --branch master --dry-run +gitlink-cli trace +start --owner Gitlink --repo forgeplus --branch master + +# List scan results and fetch a generated report +gitlink-cli trace +results --owner Gitlink --repo forgeplus --page 1 --limit 20 +gitlink-cli trace +report --owner Gitlink --repo forgeplus --task-id 12345 + +# Re-run analysis for an existing project result +gitlink-cli trace +rescan --owner Gitlink --repo forgeplus --project-id 67890 --dry-run +``` + ### Member Management ```bash @@ -745,6 +764,7 @@ See [skills/README.md](./skills/README.md) for details. | `gitlink-release` | Release management (create, edit, update, view, delete, etc.) | | `gitlink-ci` | CI/CD operations (builds, logs, etc.) | | `gitlink-pipeline` | Pipeline workflow operations (runs, logs, enable, disable, delete, etc.) | +| `gitlink-trace` | Code trace analysis (initialize account, start scans, results, reports) | | `gitlink-search` | Search (repositories, users, etc.) | | `gitlink-org` | Organization management (members, teams, etc.) | | `gitlink-user` | User management (profile info, etc.) | @@ -779,6 +799,7 @@ gitlink-cli/ │ ├── org/ # Organization shortcuts │ ├── ci/ # CI shortcuts │ ├── pipeline/ # Pipeline shortcuts +│ ├── trace/ # Code trace analysis shortcuts │ ├── search/ # Search shortcuts │ ├── user/ # User shortcuts │ └── register.go # Registration entry point diff --git a/README.zh-CN.md b/README.zh-CN.md index 6a8879d..d4f84f9 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -114,6 +114,7 @@ | 🔧 CI | 查看构建、日志、CI/CD 操作 | | ⚙️ Pipeline | 运行、查看、启停、删除流水线工作流并查询日志 | | 📖 Wiki | 列出、查看、创建、更新、删除 Wiki 页面 | +| 🧭 代码溯源 | 初始化代码溯源分析、发起扫描、查看结果并获取报告 | | 🔍 搜索 | 搜索仓库、用户 | | 📊 数据集 | 按项目查询科研数据集 | | 👤 用户 | 查看用户资料和信息 | @@ -307,6 +308,24 @@ gitlink-cli wiki +update --owner Gitlink --repo forgeplus --project-id 12345 -n gitlink-cli wiki +delete --owner Gitlink --repo forgeplus --project-id 12345 -n old-page ``` +### 代码溯源分析 + +```bash +# 初始化当前账号的代码溯源分析能力 +gitlink-cli trace +init + +# 对仓库分支发起代码溯源扫描 +gitlink-cli trace +start --owner Gitlink --repo forgeplus --branch master --dry-run +gitlink-cli trace +start --owner Gitlink --repo forgeplus --branch master + +# 查看扫描结果并获取报告 +gitlink-cli trace +results --owner Gitlink --repo forgeplus --page 1 --limit 20 +gitlink-cli trace +report --owner Gitlink --repo forgeplus --task-id 12345 + +# 对已有项目结果重新扫描 +gitlink-cli trace +rescan --owner Gitlink --repo forgeplus --project-id 67890 --dry-run +``` + ### 成员管理 ```bash @@ -619,6 +638,7 @@ git push gitlink | `gitlink-org` | 组织管理(成员、团队等) | | `gitlink-ci` | CI/CD 操作(构建、日志等) | | `gitlink-pipeline` | 流水线工作流操作(运行、日志、启停、删除等) | +| `gitlink-trace` | 代码溯源分析(初始化、发起扫描、查看结果、获取报告) | | `gitlink-search` | 搜索功能(仓库、用户等) | | `gitlink-user` | 用户管理(个人信息等) | | `gitlink-pm` | 项目管理(Sprint、看板、周报等) | @@ -651,6 +671,7 @@ gitlink-cli/ │ ├── org/ # 组织 shortcuts │ ├── ci/ # CI shortcuts │ ├── pipeline/ # Pipeline shortcuts +│ ├── trace/ # 代码溯源 shortcuts │ ├── search/ # 搜索 shortcuts │ ├── user/ # 用户 shortcuts │ └── register.go # 注册入口 diff --git a/doc/changes/trace-shortcuts.md b/doc/changes/trace-shortcuts.md new file mode 100644 index 0000000..8e4f1a6 --- /dev/null +++ b/doc/changes/trace-shortcuts.md @@ -0,0 +1,13 @@ +# Trace Shortcuts + +新增 `trace` Shortcut 组,封装 GitLink 代码溯源分析相关接口,支持: + +- `trace +init` +- `trace +results` +- `trace +start` +- `trace +rescan` +- `trace +report` + +该能力覆盖账号初始化、按分支发起分析、分页查看分析结果、按项目重新扫描以及获取分析报告。`trace +start` 和 `trace +rescan` 支持 `--dry-run`,便于在自动化脚本或 AI Agent 执行前预览实际请求,降低误触发平台任务的风险。 + +同时补充了单元测试、README 示例、AI Agent Skill 文档和 i18n 文案,确保命令帮助、中文/英文提示和自动化使用场景保持一致。 diff --git a/internal/i18n/locales/en-US.json b/internal/i18n/locales/en-US.json index 0739395..40d3cff 100644 --- a/internal/i18n/locales/en-US.json +++ b/internal/i18n/locales/en-US.json @@ -93,6 +93,12 @@ "cmd.search.repos.short": "Search repositories", "cmd.search.short": "Search operations", "cmd.search.users.short": "Search users", + "cmd.trace.init.short": "Initialize code trace analysis account", + "cmd.trace.report.short": "Fetch a code trace analysis report", + "cmd.trace.rescan.short": "Rescan a code trace analysis result", + "cmd.trace.results.short": "List code trace analysis results", + "cmd.trace.short": "Code trace analysis operations", + "cmd.trace.start.short": "Start code trace analysis for a branch", "cmd.user.info.short": "Show user profile", "cmd.user.me.short": "Show current authenticated user", "cmd.user.short": "User operations", @@ -217,6 +223,9 @@ "flag.search.keyword": "Search keyword", "flag.sort_by": "Sort field", "flag.sort_direction": "Sort direction: asc, desc", + "flag.trace.branch": "Branch name to analyze", + "flag.trace.project_id": "project_id from trace analysis results", + "flag.trace.task_id": "task_id from trace analysis results", "flag.user": "User login (default: current user)", "flag.user.login": "User login name", "flag.webhook.active": "Whether the webhook is active: true or false", diff --git a/internal/i18n/locales/zh-CN.json b/internal/i18n/locales/zh-CN.json index 2e6fc4d..475b18a 100644 --- a/internal/i18n/locales/zh-CN.json +++ b/internal/i18n/locales/zh-CN.json @@ -93,6 +93,12 @@ "cmd.search.repos.short": "搜索仓库", "cmd.search.short": "搜索操作", "cmd.search.users.short": "搜索用户", + "cmd.trace.init.short": "初始化代码溯源分析账号", + "cmd.trace.report.short": "获取代码溯源分析报告", + "cmd.trace.rescan.short": "重新扫描代码溯源分析结果", + "cmd.trace.results.short": "列出代码溯源分析结果", + "cmd.trace.short": "代码溯源分析操作", + "cmd.trace.start.short": "对指定分支发起代码溯源分析", "cmd.user.info.short": "显示用户资料", "cmd.user.me.short": "显示当前认证用户", "cmd.user.short": "用户操作", @@ -217,6 +223,9 @@ "flag.search.keyword": "搜索关键词", "flag.sort_by": "排序字段", "flag.sort_direction": "排序方向:asc、desc", + "flag.trace.branch": "要分析的分支名称", + "flag.trace.project_id": "代码溯源分析结果中的 project_id", + "flag.trace.task_id": "代码溯源分析结果中的 task_id", "flag.user": "用户登录名(默认:当前用户)", "flag.user.login": "用户登录名", "flag.webhook.active": "Webhook 是否启用:true 或 false", diff --git a/shortcuts/register.go b/shortcuts/register.go index 1fedc7e..7c8be0c 100644 --- a/shortcuts/register.go +++ b/shortcuts/register.go @@ -23,6 +23,7 @@ import ( "github.com/gitlink-org/gitlink-cli/shortcuts/release" "github.com/gitlink-org/gitlink-cli/shortcuts/repo" "github.com/gitlink-org/gitlink-cli/shortcuts/search" + "github.com/gitlink-org/gitlink-cli/shortcuts/trace" "github.com/gitlink-org/gitlink-cli/shortcuts/user" "github.com/gitlink-org/gitlink-cli/shortcuts/webhook" "github.com/gitlink-org/gitlink-cli/shortcuts/wiki" @@ -57,6 +58,7 @@ func RegisterAll(root *cobra.Command, translators ...*i18n.Translator) { "wiki": wiki.Shortcuts(), "health": health.Shortcuts(tr), "ignore": ignore.Shortcuts(), + "trace": trace.Shortcuts(tr), "workflow": workflow.Shortcuts(), } @@ -82,6 +84,7 @@ func RegisterAll(root *cobra.Command, translators ...*i18n.Translator) { "wiki": "Wiki page management", "health": "Project health data collection", "ignore": tr.T("cmd.ignore.short"), + "trace": tr.T("cmd.trace.short"), "workflow": "AI agent workflow analysis", } diff --git a/shortcuts/register_test.go b/shortcuts/register_test.go index 00f4c57..f38624b 100644 --- a/shortcuts/register_test.go +++ b/shortcuts/register_test.go @@ -14,7 +14,7 @@ func TestRegisterAll(t *testing.T) { "repo", "issue", "label", "license", "pr", "profile", "release", "branch", "org", "user", "search", "ci", "workflow", "compare", "member", "milestone", "pipeline", "webhook", - "dataset", "health", "ignore", "wiki", + "dataset", "health", "ignore", "wiki", "trace", } groupSet := map[string]bool{} diff --git a/shortcuts/trace/trace.go b/shortcuts/trace/trace.go new file mode 100644 index 0000000..305b2df --- /dev/null +++ b/shortcuts/trace/trace.go @@ -0,0 +1,176 @@ +package trace + +import ( + "fmt" + "net/url" + "strings" + + "github.com/gitlink-org/gitlink-cli/internal/i18n" + "github.com/gitlink-org/gitlink-cli/shortcuts/common" +) + +// Shortcuts returns code trace analysis shortcuts. +func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut { + tr := shortcutTranslator(translators...) + return []*common.Shortcut{ + { + Name: "init", + Description: tr.T("cmd.trace.init.short"), + Run: func(ctx *common.RuntimeContext) error { + env, err := ctx.CallAPI("POST", "/api/traces/trace_users", nil) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + { + Name: "results", + Description: tr.T("cmd.trace.results.short"), + Flags: []common.Flag{ + {Name: "page", Short: "p", Usage: tr.T("flag.page"), Default: "1"}, + {Name: "limit", Short: "l", Usage: tr.T("flag.limit"), Default: "15"}, + }, + Run: func(ctx *common.RuntimeContext) error { + if err := ctx.ResolveOwnerRepo(); err != nil { + return err + } + q := url.Values{} + setQueryIfPresent(q, "page", ctx.Arg("page")) + setQueryIfPresent(q, "limit", ctx.Arg("limit")) + env, err := ctx.CallAPIWithQuery("GET", traceRepoPath(ctx)+"/task_results", q) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + { + Name: "start", + Description: tr.T("cmd.trace.start.short"), + Flags: []common.Flag{ + {Name: "branch", Short: "b", Usage: tr.T("flag.trace.branch"), Required: true}, + {Name: "dry-run", Usage: tr.T("flag.dry_run"), Bool: true, Default: "false"}, + }, + Run: runStart, + }, + { + Name: "rescan", + Description: tr.T("cmd.trace.rescan.short"), + Flags: []common.Flag{ + {Name: "project-id", Usage: tr.T("flag.trace.project_id"), Required: true}, + {Name: "dry-run", Usage: tr.T("flag.dry_run"), Bool: true, Default: "false"}, + }, + Run: runRescan, + }, + { + Name: "report", + Description: tr.T("cmd.trace.report.short"), + Flags: []common.Flag{ + {Name: "task-id", Usage: tr.T("flag.trace.task_id"), Required: true}, + }, + Run: func(ctx *common.RuntimeContext) error { + if err := ctx.ResolveOwnerRepo(); err != nil { + return err + } + taskID, err := ctx.RequireArg("task-id") + if err != nil { + return err + } + q := url.Values{} + q.Set("task_id", taskID) + env, err := ctx.CallAPIWithQuery("GET", traceRepoPath(ctx)+"/task_pdf", q) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + } +} + +func shortcutTranslator(translators ...*i18n.Translator) *i18n.Translator { + if len(translators) > 0 && translators[0] != nil { + return translators[0] + } + return i18n.Default() +} + +func runStart(ctx *common.RuntimeContext) error { + if err := ctx.ResolveOwnerRepo(); err != nil { + return err + } + branch, err := requiredTrimmedArg(ctx, "branch") + if err != nil { + return err + } + payload := map[string]interface{}{"branch_name": branch} + path := traceRepoPath(ctx) + "/tasks" + if ctx.Arg("dry-run") == "true" { + return ctx.OutputData(traceDryRun(ctx, "POST", path, payload, nil)) + } + env, err := ctx.CallAPI("POST", path, payload) + if err != nil { + return err + } + return ctx.Output(env) +} + +func runRescan(ctx *common.RuntimeContext) error { + if err := ctx.ResolveOwnerRepo(); err != nil { + return err + } + projectID, err := requiredTrimmedArg(ctx, "project-id") + if err != nil { + return err + } + path := traceRepoPath(ctx) + "/reload_task" + q := url.Values{} + q.Set("project_id", projectID) + if ctx.Arg("dry-run") == "true" { + return ctx.OutputData(traceDryRun(ctx, "GET", path, nil, q)) + } + env, err := ctx.CallAPIWithQuery("GET", path, q) + if err != nil { + return err + } + return ctx.Output(env) +} + +func traceRepoPath(ctx *common.RuntimeContext) string { + return fmt.Sprintf("/api/traces/%s/%s", ctx.Owner, ctx.Repo) +} + +func requiredTrimmedArg(ctx *common.RuntimeContext, name string) (string, error) { + value, err := ctx.RequireArg(name) + if err != nil { + return "", err + } + value = strings.TrimSpace(value) + if value == "" { + return "", fmt.Errorf("required flag --%s is missing", name) + } + return value, nil +} + +func traceDryRun(ctx *common.RuntimeContext, method, path string, payload interface{}, query url.Values) map[string]interface{} { + result := map[string]interface{}{ + "repository": fmt.Sprintf("%s/%s", ctx.Owner, ctx.Repo), + "dry_run": true, + "method": method, + "path": path, + } + if payload != nil { + result["payload"] = payload + } + if len(query) > 0 { + result["query"] = query.Encode() + } + return result +} + +func setQueryIfPresent(q url.Values, key, value string) { + if strings.TrimSpace(value) != "" { + q.Set(key, strings.TrimSpace(value)) + } +} diff --git a/shortcuts/trace/trace_test.go b/shortcuts/trace/trace_test.go new file mode 100644 index 0000000..b9e08e4 --- /dev/null +++ b/shortcuts/trace/trace_test.go @@ -0,0 +1,200 @@ +package trace + +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 runTraceShortcut(t *testing.T, server *httptest.Server, name string, args map[string]string) error { + t.Helper() + s := findTraceShortcut(t, name) + ctx := &common.RuntimeContext{ + Client: &client.Client{ + HTTP: server.Client(), + BaseURL: server.URL, + }, + Owner: "owner", + Repo: "repo", + Format: "json", + Args: args, + } + if ctx.Args == nil { + ctx.Args = map[string]string{} + } + return s.Run(ctx) +} + +func findTraceShortcut(t *testing.T, name string) *common.Shortcut { + t.Helper() + for _, s := range Shortcuts() { + if s.Name == name { + return s + } + } + t.Fatalf("shortcut %q not found", name) + return nil +} + +func writeJSON(t *testing.T, w http.ResponseWriter, value interface{}) { + t.Helper() + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(value); err != nil { + t.Fatalf("write response: %v", err) + } +} + +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 { + t.Fatalf("decode request: %v", err) + } + return payload +} + +func TestTraceInit(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + t.Fatalf("expected POST, got %s", r.Method) + } + if r.URL.Path != "/api/traces/trace_users.json" { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + writeJSON(t, w, map[string]interface{}{"status": float64(0), "message": "success"}) + })) + defer server.Close() + + if err := runTraceShortcut(t, server, "init", nil); err != nil { + t.Fatalf("init failed: %v", err) + } +} + +func TestTraceResults(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + t.Fatalf("expected GET, got %s", r.Method) + } + if r.URL.Path != "/api/traces/owner/repo/task_results.json" { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + if r.URL.Query().Get("page") != "2" { + t.Fatalf("expected page=2, got %s", r.URL.Query().Get("page")) + } + if r.URL.Query().Get("limit") != "50" { + t.Fatalf("expected limit=50, got %s", r.URL.Query().Get("limit")) + } + writeJSON(t, w, map[string]interface{}{"data": []interface{}{map[string]interface{}{"task_id": "task-1"}}}) + })) + defer server.Close() + + err := runTraceShortcut(t, server, "results", map[string]string{"page": "2", "limit": "50"}) + if err != nil { + t.Fatalf("results failed: %v", err) + } +} + +func TestTraceStart(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.Fatalf("expected POST, got %s", r.Method) + } + if r.URL.Path != "/api/traces/owner/repo/tasks.json" { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + payload = decodeJSON(t, r) + writeJSON(t, w, map[string]interface{}{"status": float64(0), "message": "success"}) + })) + defer server.Close() + + err := runTraceShortcut(t, server, "start", map[string]string{"branch": " feature/scan "}) + if err != nil { + t.Fatalf("start failed: %v", err) + } + if payload["branch_name"] != "feature/scan" { + t.Fatalf("unexpected branch_name: %#v", payload["branch_name"]) + } +} + +func TestTraceStartDryRunDoesNotCallAPI(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Fatalf("dry-run should not call API: %s %s", r.Method, r.URL.Path) + })) + defer server.Close() + + err := runTraceShortcut(t, server, "start", map[string]string{"branch": "main", "dry-run": "true"}) + if err != nil { + t.Fatalf("start dry-run failed: %v", err) + } +} + +func TestTraceStartRejectsBlankBranch(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Fatalf("blank branch should not call API") + })) + defer server.Close() + + err := runTraceShortcut(t, server, "start", map[string]string{"branch": " "}) + if err == nil { + t.Fatal("expected blank branch error, got nil") + } +} + +func TestTraceRescan(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + t.Fatalf("expected GET, got %s", r.Method) + } + if r.URL.Path != "/api/traces/owner/repo/reload_task.json" { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + if r.URL.Query().Get("project_id") != "project-1" { + t.Fatalf("unexpected project_id: %s", r.URL.Query().Get("project_id")) + } + writeJSON(t, w, map[string]interface{}{"status": float64(0), "message": "success"}) + })) + defer server.Close() + + err := runTraceShortcut(t, server, "rescan", map[string]string{"project-id": " project-1 "}) + if err != nil { + t.Fatalf("rescan failed: %v", err) + } +} + +func TestTraceRescanDryRunDoesNotCallAPI(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Fatalf("dry-run should not call API: %s %s", r.Method, r.URL.Path) + })) + defer server.Close() + + err := runTraceShortcut(t, server, "rescan", map[string]string{"project-id": "project-1", "dry-run": "true"}) + if err != nil { + t.Fatalf("rescan dry-run failed: %v", err) + } +} + +func TestTraceReport(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + t.Fatalf("expected GET, got %s", r.Method) + } + if r.URL.Path != "/api/traces/owner/repo/task_pdf.json" { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + if r.URL.Query().Get("task_id") != "task-1" { + t.Fatalf("unexpected task_id: %s", r.URL.Query().Get("task_id")) + } + writeJSON(t, w, map[string]interface{}{"status": float64(0), "message": "success"}) + })) + defer server.Close() + + err := runTraceShortcut(t, server, "report", map[string]string{"task-id": "task-1"}) + if err != nil { + t.Fatalf("report failed: %v", err) + } +} diff --git a/skills/README.md b/skills/README.md index d507074..d33ba4b 100644 --- a/skills/README.md +++ b/skills/README.md @@ -103,6 +103,8 @@ skills/ │ └── SKILL.md # Pipeline 操作指南 ├── gitlink-wiki/ # Wiki 页面管理 │ └── SKILL.md # Wiki 操作指南 +├── gitlink-trace/ # 代码溯源分析 +│ └── SKILL.md # 代码溯源分析操作指南 ├── gitlink-pm/ # 项目管理 │ └── SKILL.md # PM 操作指南 ├── gitlink-health/ # 项目健康度分析 @@ -144,6 +146,7 @@ skills/ | **gitlink-ci** | CI/CD | `ci +builds`, `ci +logs` | | **gitlink-pipeline** | 流水线工作流 | `pipeline +runs`, `pipeline +run`, `pipeline +logs` | | **gitlink-wiki** | Wiki 页面管理 | `wiki +list`, `wiki +view`, `wiki +create`, `wiki +update`, `wiki +delete` | +| **gitlink-trace** | 代码溯源分析 | `trace +init`, `trace +start`, `trace +results`, `trace +report` | | **gitlink-pm** | 项目管理 | 通过 Raw API 访问 | | **gitlink-workflow** | AI 工作流 | Issue 分类、PR Review、Release Notes | | **gitlink-health** | 开源项目健康度 | 详情见SKILL.md | diff --git a/skills/gitlink-trace/SKILL.md b/skills/gitlink-trace/SKILL.md new file mode 100644 index 0000000..e1884cb --- /dev/null +++ b/skills/gitlink-trace/SKILL.md @@ -0,0 +1,52 @@ +--- +name: gitlink-trace +version: 1.0.0 +description: "代码溯源分析:初始化账号、发起分支扫描、查看分析结果、重新扫描并获取报告。" +metadata: + requires: + bins: ["gitlink-cli"] + cliHelp: "gitlink-cli trace --help" +--- + +# gitlink-trace(代码溯源分析) + +Read [`../gitlink-shared/SKILL.md`](../gitlink-shared/SKILL.md) first for authentication, global flags, owner/repo resolution, and output formatting. + +**CRITICAL**: `trace +start` and `trace +rescan` trigger platform-side analysis jobs. Use `--dry-run` first when the user is not explicitly asking to start or rerun a scan. + +## Shortcuts + +| Shortcut | 说明 | +|----------|------| +| `trace +init` | 初始化当前账号的代码溯源分析能力 | +| `trace +results` | 查看仓库代码溯源分析结果,支持 `--page` 和 `--limit` | +| `trace +start` | 对指定分支发起代码溯源分析 | +| `trace +rescan` | 按 `project_id` 重新扫描已有代码溯源结果 | +| `trace +report` | 按 `task_id` 获取代码溯源分析报告 | + +## Usage + +```bash +# 初始化当前账号 +gitlink-cli trace +init + +# 查看最近的分析结果 +gitlink-cli trace +results --owner Gitlink --repo forgeplus --page 1 --limit 20 --format json + +# 先预览,再对 master 分支发起扫描 +gitlink-cli trace +start --owner Gitlink --repo forgeplus --branch master --dry-run +gitlink-cli trace +start --owner Gitlink --repo forgeplus --branch master + +# 重新扫描已有结果 +gitlink-cli trace +rescan --owner Gitlink --repo forgeplus --project-id 67890 --dry-run +gitlink-cli trace +rescan --owner Gitlink --repo forgeplus --project-id 67890 + +# 获取分析报告 +gitlink-cli trace +report --owner Gitlink --repo forgeplus --task-id 12345 --format json +``` + +## Agent Notes + +- Prefer `--format json` when consuming result IDs for a later `trace +report` or `trace +rescan` command. +- Treat `task_id` and `project_id` as platform-provided identifiers from `trace +results`; do not invent them. +- If owner/repo are omitted, the CLI can resolve them from the current Git remote, but explicit flags are safer in automation.