YangZhiqiang98 asked (#1449) that the Library find which skills have updates and update them. magpie already checked skills from GitHub (a market install among them) by hand and could update them, but an update deleted the library's folder, so a skill the user had changed (by hand, through an agent's link, or in an agent's copy not yet taken in) was lost without a word, and nothing checked unless the user pressed Check. - A skill is "changed here" when its library folder no longer hashes to what was fetched (Finder's .DS_Store aside), or an agent's copy was edited since its mark and not yet taken in (editState, skill_changed.go). The view says so (SkillView.edited) and the row shows "Changed here". - UpdateSkill refuses such a skill with *EditedError unless told to replace it. The row's Update asks in the page first (Keep mine / Update; Update sends replace). Update N, the group's Update N and the CLI's update-all leave it as it is and say so. The CLI takes `magpie library skill update <name> --replace-edits`. - The version an update replaces is moved to the backups when it was changed, or when it can't be told (a skill with no hash from before). - View.checkDue: no check since magpie started, or none for 12 hours. The page then runs the check once by itself as the skills open, and says only what there is to update or add. Not checked, by design: skills the skills CLI manages (`npx skills update` updates them; magpie only groups them), linked local folders, skills imported from an agent with no source, and From-only skills have no remote; CC Switch-installed skills can still be updated by hand. Verified: - go test -tags nogui ./... (sandbox HOME); new TestUpdateKeepsASkillChangedHere, TestUpdateKeepsACopyEditedInAnAgent, TestUpdateKeepsWhatItCantTell, TestCheckDue, TestCheckSkillsMarksItDone, all from httptest. With editState forced to "not changed" the first three fail. - gofmt -l, go vet, GOOS=linux and GOOS=windows go vet -tags nogui on ./internal/library ./internal/gui ./internal/tui . - GUI: new skill-update-edited.test.cjs (Chromium and WebKit, 860 and 400px, en and zh; no scroll on click; fails with the row's ask removed), plus the 22 other library skill tests, gui-ja, gui-de, gui-zh-tw, i18n-keys and click-scroll: 262 pass. - Real use in a sandbox HOME with github.com/anthropics/skills: installed pdf and docx, the real check said both current; an edit through Claude Code's link was refused by `skill update pdf`, skipped by `skill update` (docx updated), replaced by `--replace-edits` with the edit in backups/<time>/library/skills/pdf; docx edited later shows edited, a .DS_Store doesn't.
83 lines
2.8 KiB
Go
83 lines
2.8 KiB
Go
package library
|
|
|
|
import (
|
|
"database/sql"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
_ "modernc.org/sqlite"
|
|
)
|
|
|
|
// A skill the library links to in CC Switch's folder is updated from the
|
|
// GitHub repository CC Switch installed it from, and becomes the library's
|
|
// own; CC Switch's folder is left as it was.
|
|
func TestCCSwitchSkillUpdates(t *testing.T) {
|
|
h := sandbox(t)
|
|
ccs := filepath.Join(h, ".cc-switch")
|
|
write(t, filepath.Join(ccs, "skills/pdf/SKILL.md"), "---\nname: pdf\ndescription: old\n---\n")
|
|
write(t, filepath.Join(ccs, "skills/mine/SKILL.md"), "---\nname: mine\n---\n")
|
|
db, err := sql.Open("sqlite", filepath.Join(ccs, "cc-switch.db"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, q := range []string{
|
|
`CREATE TABLE skills (id TEXT PRIMARY KEY, name TEXT NOT NULL, description TEXT, directory TEXT NOT NULL, repo_owner TEXT, repo_name TEXT, repo_branch TEXT DEFAULT 'main')`,
|
|
`INSERT INTO skills VALUES ('1','pdf','','pdf','owner','repo','main'), ('2','mine','','mine',NULL,NULL,NULL)`,
|
|
} {
|
|
if _, err := db.Exec(q); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
db.Close()
|
|
var asked []string
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
asked = append(asked, r.URL.Path)
|
|
w.Write(tarball(t, map[string]string{
|
|
"skills/pdf/SKILL.md": "---\nname: pdf\ndescription: new\n---\n",
|
|
"skills/pdf/forms.md": "forms",
|
|
}))
|
|
}))
|
|
defer srv.Close()
|
|
old := tarballURL
|
|
tarballURL = func(repo, ref string) string { return srv.URL + "/" + repo + "/" + ref }
|
|
defer func() { tarballURL = old }()
|
|
|
|
ok(t)(InstallSkills(filepath.Join(ccs, "skills"), []string{"pdf", "mine"}, []string{"claude"}))
|
|
v, err := Read(nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
origins := map[string]string{}
|
|
for _, s := range v.Skills {
|
|
origins[s.Name] = s.Origin
|
|
}
|
|
if origins["pdf"] != "https://github.com/owner/repo/tree/main" || origins["mine"] != "" {
|
|
t.Fatalf("origins %v", origins)
|
|
}
|
|
if _, err := UpdateSkill("mine", false); err == nil {
|
|
t.Error("one CC Switch has no repository for was updated")
|
|
}
|
|
ok(t)(UpdateSkill("pdf", false))
|
|
if asked[len(asked)-1] != "/owner/repo/main" {
|
|
t.Errorf("asked %v", asked)
|
|
}
|
|
if s := read(t, filepath.Join(h, ".claude/skills/pdf/forms.md")); s != "forms" {
|
|
t.Errorf("claude's pdf: %q", s)
|
|
}
|
|
if s := read(t, filepath.Join(ccs, "skills/pdf/SKILL.md")); !strings.Contains(s, "old") {
|
|
t.Errorf("CC Switch's folder changed: %q", s)
|
|
}
|
|
if fi, err := os.Lstat(skillDir("pdf")); err != nil || linkEntry(fi) {
|
|
t.Errorf("the library's pdf is still a link: %v", err)
|
|
}
|
|
l, _ := load()
|
|
if s := l.skill("pdf"); s.Source == nil || s.Source.Kind != "github" || s.Source.Repo != "owner/repo" || s.Source.Path != "skills/pdf" {
|
|
t.Errorf("source %+v", s.Source)
|
|
}
|
|
ok(t)(UpdateSkill("pdf", false)) // from GitHub now, like any other
|
|
}
|