{"record":{"id":"02b5cdcba139010e","repo":"siyuan-note/siyuan","slug":"skill-already-exists-s","errorCode":null,"errorMessage":"skill already exists: %s","messagePattern":"skill already exists: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"kernel/util/skill.go","lineNumber":174,"sourceCode":"\t}\n\treturn os.RemoveAll(skillDir)\n}\n\nfunc RenameSkill(oldName, newName string) error {\n\tif err := validateSkillName(oldName); err != nil {\n\t\treturn err\n\t}\n\tif err := validateSkillName(newName); err != nil {\n\t\treturn err\n\t}\n\tdir := SkillsDir()\n\toldDir := filepath.Join(dir, oldName)\n\tnewDir := filepath.Join(dir, newName)\n\tif _, err := os.Stat(oldDir); os.IsNotExist(err) {\n\t\treturn fmt.Errorf(\"skill not found: %s\", oldName)\n\t}\n\tif _, err := os.Stat(newDir); err == nil {\n\t\treturn fmt.Errorf(\"skill already exists: %s\", newName)\n\t}\n\treturn os.Rename(oldDir, newDir)\n}\n\nfunc parseSkillFrontmatter(text string) (fm map[string]string, body string) {\n\tfm = map[string]string{}\n\ttext = strings.TrimSpace(text)\n\tif !strings.HasPrefix(text, \"---\") {\n\t\treturn fm, text\n\t}\n\tend := strings.Index(text[3:], \"\\n---\")\n\tif end < 0 {\n\t\treturn fm, text\n\t}\n\traw := text[3 : 3+end]\n\tbody = strings.TrimSpace(text[3+end+4:])\n\tfor line := range strings.SplitSeq(raw, \"\\n\") {\n\t\tline = strings.TrimSpace(line)","sourceCodeStart":156,"sourceCodeEnd":192,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/251596fc0de2f9528c00c224252fd073a99973f4/kernel/util/skill.go#L156-L192","documentation":"Thrown by RenameSkill when the destination (newName) directory already exists. The function stats newDir; if it exists the rename is refused to prevent silently overwriting an installed skill. Unlike install (which removes the old dir first), rename treats the target as occupied.","triggerScenarios":"Renaming a skill to a name already present in SkillsDir(); renaming to a case variant of an existing skill on a case-insensitive filesystem; attempting to 'restore' a skill whose name is taken.","commonSituations":"User picks a name already in use; a skill was partially installed and left a directory behind; case-insensitive OS (macOS/Windows) collides 'MySkill' with 'myskill'.","solutions":["Check DiscoverSkills for the target name before renaming; prompt the user to overwrite or pick another name.","If overwrite is intended, RemoveSkill(newName) before RenameSkill, mirroring the install path.","Normalize name case consistently and reject collisions at the UI layer."],"exampleFix":"// before\nerr := util.RenameSkill(oldName, newName)\n\n// after — explicit overwrite when the user confirms\nif _, serr := os.Stat(filepath.Join(util.SkillsDir(), newName)); serr == nil {\n    _ = util.RemoveSkill(newName)\n}\nerr := util.RenameSkill(oldName, newName)","handlingStrategy":"validation","validationCode":"// reject or confirm overwrite before renaming\nexists := false\nfor _, s := range util.DiscoverSkills() {\n    if strings.EqualFold(s.Name, newName) {\n        exists = true\n        break\n    }\n}\nif exists && !userConfirmedOverwrite {\n    return fmt.Errorf(\"target name %q already in use\", newName)\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Normalize skill names to a single case convention to avoid case-collision surprises.","Offer an explicit 'replace existing' choice when the target name is taken.","On case-insensitive OSes, treat name equality case-insensitively in collision checks."],"tags":["skill-management","filesystem","conflict"],"backgroundTag":null,"analyzedSha":"251596fc0de2f9528c00c224252fd073a99973f4","analyzedAt":"2026-08-12T21:18:37.123Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}