From 34eeaf5899a426e81d7982cfd218940f21c309bd Mon Sep 17 00:00:00 2001 From: lxowalle Date: Fri, 10 Apr 2026 14:56:29 +0800 Subject: [PATCH] fix cli skills install origin metadata --- cmd/picoclaw/internal/skills/helpers.go | 35 +++++++++++ cmd/picoclaw/internal/skills/helpers_test.go | 62 ++++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 cmd/picoclaw/internal/skills/helpers_test.go diff --git a/cmd/picoclaw/internal/skills/helpers.go b/cmd/picoclaw/internal/skills/helpers.go index d115bd384..d0ad6c962 100644 --- a/cmd/picoclaw/internal/skills/helpers.go +++ b/cmd/picoclaw/internal/skills/helpers.go @@ -2,6 +2,7 @@ package skills import ( "context" + "encoding/json" "fmt" "io" "os" @@ -11,12 +12,23 @@ import ( "github.com/sipeed/picoclaw/cmd/picoclaw/internal" "github.com/sipeed/picoclaw/pkg/config" + "github.com/sipeed/picoclaw/pkg/fileutil" "github.com/sipeed/picoclaw/pkg/skills" "github.com/sipeed/picoclaw/pkg/utils" ) const skillsSearchMaxResults = 20 +type installedSkillOriginMeta struct { + Version int `json:"version"` + OriginKind string `json:"origin_kind,omitempty"` + Registry string `json:"registry,omitempty"` + Slug string `json:"slug,omitempty"` + RegistryURL string `json:"registry_url,omitempty"` + InstalledVersion string `json:"installed_version,omitempty"` + InstalledAt int64 `json:"installed_at"` +} + func skillsListCmd(loader *skills.SkillsLoader) { allSkills := loader.ListSkills() @@ -92,6 +104,21 @@ func skillsInstallFromRegistry(cfg *config.Config, registryName, target string) fmt.Printf("\u26a0\ufe0f Warning: skill '%s' is flagged as suspicious.\n", target) } + normalizedSlug := skills.NormalizeInstallTargetForRegistry(cfg.Tools.Skills, registry.Name(), target) + installedAt := time.Now().UnixMilli() + if err := writeInstalledSkillOriginMeta(targetDir, installedSkillOriginMeta{ + Version: 1, + OriginKind: "third_party", + Registry: registry.Name(), + Slug: normalizedSlug, + RegistryURL: registry.SkillURL(normalizedSlug, result.Version), + InstalledVersion: result.Version, + InstalledAt: installedAt, + }); err != nil { + _ = os.RemoveAll(targetDir) + return fmt.Errorf("✗ failed to persist skill metadata: %w", err) + } + fmt.Printf("\u2713 Skill '%s' v%s installed successfully!\n", dirName, result.Version) if result.Summary != "" { fmt.Printf(" %s\n", result.Summary) @@ -100,6 +127,14 @@ func skillsInstallFromRegistry(cfg *config.Config, registryName, target string) return nil } +func writeInstalledSkillOriginMeta(targetDir string, meta installedSkillOriginMeta) error { + data, err := json.MarshalIndent(meta, "", " ") + if err != nil { + return err + } + return fileutil.WriteFileAtomic(filepath.Join(targetDir, ".skill-origin.json"), data, 0o600) +} + func skillsRemoveFromWorkspace(workspace, skillName string) error { name := strings.TrimSpace(skillName) name = strings.Trim(name, "/") diff --git a/cmd/picoclaw/internal/skills/helpers_test.go b/cmd/picoclaw/internal/skills/helpers_test.go new file mode 100644 index 000000000..13310e7be --- /dev/null +++ b/cmd/picoclaw/internal/skills/helpers_test.go @@ -0,0 +1,62 @@ +package skills + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/sipeed/picoclaw/pkg/config" +) + +func TestSkillsInstallFromRegistryWritesOriginMetadata(t *testing.T) { + workspace := t.TempDir() + cfg := config.DefaultConfig() + cfg.Agents.Defaults.Workspace = workspace + + var server *httptest.Server + server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/api/v3/repos/foo/bar": + require.NoError(t, json.NewEncoder(w).Encode(map[string]any{"default_branch": "master"})) + case "/api/v3/repos/foo/bar/contents/.agents/skills/pr-review": + assert.Equal(t, "ref=master", r.URL.RawQuery) + require.NoError(t, json.NewEncoder(w).Encode([]map[string]any{{ + "type": "file", + "name": "SKILL.md", + "download_url": server.URL + "/raw/foo/bar/master/.agents/skills/pr-review/SKILL.md", + }})) + case "/raw/foo/bar/master/.agents/skills/pr-review/SKILL.md": + _, _ = w.Write([]byte("---\nname: pr-review\ndescription: PR review skill\n---\n# PR Review\n")) + default: + http.NotFound(w, r) + } + })) + defer server.Close() + + githubRegistry, ok := cfg.Tools.Skills.Registries.Get("github") + require.True(t, ok) + githubRegistry.BaseURL = server.URL + cfg.Tools.Skills.Registries.Set("github", githubRegistry) + + target := server.URL + "/foo/bar/tree/master/.agents/skills/pr-review" + require.NoError(t, skillsInstallFromRegistry(cfg, "github", target)) + + metaPath := filepath.Join(workspace, "skills", "pr-review", ".skill-origin.json") + data, err := os.ReadFile(metaPath) + require.NoError(t, err) + + var meta installedSkillOriginMeta + require.NoError(t, json.Unmarshal(data, &meta)) + assert.Equal(t, "third_party", meta.OriginKind) + assert.Equal(t, "github", meta.Registry) + assert.Equal(t, "foo/bar/.agents/skills/pr-review", meta.Slug) + assert.Equal(t, server.URL+"/foo/bar/tree/master/.agents/skills/pr-review", meta.RegistryURL) + assert.Equal(t, "master", meta.InstalledVersion) + assert.NotZero(t, meta.InstalledAt) +}