{"record":{"id":"eec79cb668b5317a","repo":"farion1231/cc-switch","slug":"skill-already-exists-please-uninstall-the-current","errorCode":null,"errorMessage":"Skill already exists, please uninstall the current one first: {}","messagePattern":"Skill already exists, please uninstall the current one first: (.+?)","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"src-tauri/src/services/skill.rs","lineNumber":1782,"sourceCode":"        let backup_path = Self::backup_path_for_id(backup_id)?;\n        let metadata = Self::read_backup_metadata(&backup_path)?;\n        let backup_skill_dir = backup_path.join(\"skill\");\n        if !backup_skill_dir.join(\"SKILL.md\").exists() {\n            return Err(anyhow!(\n                \"Skill backup is invalid or missing SKILL.md: {}\",\n                backup_path.display()\n            ));\n        }\n\n        let existing_skills = db.get_all_installed_skills()?;\n        if existing_skills.contains_key(&metadata.skill.id)\n            || existing_skills.values().any(|skill| {\n                skill\n                    .directory\n                    .eq_ignore_ascii_case(&metadata.skill.directory)\n            })\n        {\n            return Err(anyhow!(\n                \"Skill already exists, please uninstall the current one first: {}\",\n                metadata.skill.directory\n            ));\n        }\n\n        // meta.json 是文件内容（可能来自手工放置或不可信备份），directory 此前\n        // 未经任何校验就直接 join——可穿越出 SSOT 目录写任意位置。必须先校验。\n        let directory = Self::require_valid_directory(&metadata.skill.directory)?;\n\n        let ssot_dir = Self::get_ssot_dir()?;\n        let restore_path = ssot_dir.join(&directory);\n        if restore_path.exists() || Self::is_symlink(&restore_path) {\n            return Err(anyhow!(\n                \"Restore target already exists: {}\",\n                restore_path.display()\n            ));\n        }\n","sourceCodeStart":1764,"sourceCodeEnd":1800,"githubUrl":"https://github.com/farion1231/cc-switch/blob/0b5da510168914b251481654a568c3ffacd62cf4/src-tauri/src/services/skill.rs#L1764-L1800","documentation":"restore_from_backup is insert-only: it refuses to run when metadata.skill.id is already in installed_skills, or when any installed skill occupies the same directory name (case-insensitive). Restoring over an existing skill could clobber live files, so the user must uninstall first.","triggerScenarios":"Restoring a backup while the original skill is still installed; restoring after installing a different skill whose directory name matches the backup's (case differences count); duplicate ids from synced databases.","commonSituations":"Restore-before-uninstall workflows; name collisions after reinstalling a skill from a fork with the same directory name; backups restored onto machines that already have the skill.","solutions":["Uninstall the existing skill named in the error message, then retry the restore","If a different skill holds the same directory name, uninstall that one (or pick another backup)","Check the installed list for case-variant directory collisions before restoring"],"exampleFix":"// before\nawait invoke('restore_skill_backup', { backupId, currentApp: 'claude' });\n\n// after: clear the collision first\nconst skills = await invoke<Record<string, { id: string; directory: string }>>('get_all_installed_skills');\nconst collides = Object.values(skills).some(s =>\n  s.id === backup.skillId || s.directory.toLowerCase() === backup.directory.toLowerCase());\nif (collides) await invoke('uninstall_skill_unified', { id: backup.skillId });\nawait invoke('restore_skill_backup', { backupId, currentApp: 'claude' });","handlingStrategy":"validation","validationCode":"const skills = await invoke<Record<string, { id: string; directory: string }>>('get_all_installed_skills');\nconst clash = Object.values(skills).some(s =>\n  s.id === backupSkillId ||\n  s.directory.toLowerCase() === backupDirectory.toLowerCase());\nif (clash) {\n  await invoke('uninstall_skill_unified', { id: backupSkillId });\n}\nawait invoke('restore_skill_backup', { backupId, currentApp });","typeGuard":null,"tryCatchPattern":"try {\n  await invoke('restore_skill_backup', { backupId, currentApp });\n} catch (e) {\n  if (String(e).includes('already exists, please uninstall')) {\n    notify('Uninstall the current skill first, then restore');\n    return;\n  }\n  throw e;\n}","preventionTips":["Always uninstall before restoring the same skill","Check for case-insensitive directory-name collisions before restoring","Remember restore is insert-only by design — it never overwrites"],"tags":["restore","backup","conflict","duplicate","skill-manager"],"backgroundTag":"duplicate-resource-conflict","analyzedSha":"0b5da510168914b251481654a568c3ffacd62cf4","analyzedAt":"2026-08-20T14:29:00.113Z","contentChangedAt":"2026-08-20T14:29:00.113Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}