{"record":{"id":"3bc7f51707c6f0b8","repo":"farion1231/cc-switch","slug":"skill-directory-conflict","errorCode":"SKILL_DIRECTORY_CONFLICT","errorMessage":"{\"code\":\"SKILL_DIRECTORY_CONFLICT\",\"context\":{\"directory\":\"{directory}\",\"existing_repo\":\"{existing_repo}\",\"new_repo\":\"{new_repo}\"},\"suggestion\":\"uninstallFirst\"}","messagePattern":"(.+?)\",\"existing_repo\":\"(.+?)\",\"new_repo\":\"(.+?)\"\\},\"suggestion\":\"uninstallFirst\"\\}","errorType":"error_code","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"src-tauri/src/services/skill.rs","lineNumber":735,"sourceCode":"                continue;\n            }\n\n            let same_repo = existing.repo_owner.as_deref() == Some(&skill.repo_owner)\n                && existing.repo_name.as_deref() == Some(&skill.repo_name);\n            if same_repo {\n                let mut updated = existing.clone();\n                updated.apps.set_enabled_for(current_app, true);\n                db.save_skill(&updated)?;\n                Self::sync_to_app_dir(&updated.directory, current_app)?;\n                log::info!(\n                    \"Skill {} 已存在，更新 {:?} 启用状态\",\n                    updated.name,\n                    current_app\n                );\n                return Ok(Some(updated));\n            }\n\n            return Err(anyhow!(format_skill_error(\n                \"SKILL_DIRECTORY_CONFLICT\",\n                &[\n                    (\"directory\", install_name),\n                    (\n                        \"existing_repo\",\n                        &format!(\n                            \"{}/{}\",\n                            existing.repo_owner.as_deref().unwrap_or(\"unknown\"),\n                            existing.repo_name.as_deref().unwrap_or(\"unknown\")\n                        )\n                    ),\n                    (\n                        \"new_repo\",\n                        &format!(\"{}/{}\", skill.repo_owner, skill.repo_name)\n                    ),\n                ],\n                Some(\"uninstallFirst\"),\n            )));","sourceCodeStart":717,"sourceCodeEnd":753,"githubUrl":"https://github.com/farion1231/cc-switch/blob/a2e22f330273a5b6ffa87cb8b82b624601bac562/src-tauri/src/services/skill.rs#L717-L753","documentation":"Structured skill-install error from CC Switch's Rust backend. During install(), reuse_existing_install (src-tauri/src/services/skill.rs:708-754) finds an already-installed skill whose install directory matches (case-insensitive eq_ignore_ascii_case) but which came from a DIFFERENT GitHub repo (repo_owner/repo_name mismatch). Same-repo reinstalls are idempotent (they just enable the app and sync); cross-repo ones return this JSON error built by format_skill_error (src-tauri/src/error.rs:127) with code SKILL_DIRECTORY_CONFLICT, context {directory, existing_repo, new_repo}, and suggestion 'uninstallFirst'.","triggerScenarios":"Installing a skills.sh entry named e.g. 'pdf' when a 'pdf' directory already exists in the SSOT installed by another owner/repo; case variants like 'PDF' vs 'pdf' also collide because the comparison ignores ASCII case.","commonSituations":"Popular skills forked across many repos (agents skills collections); users migrating from one fork to another without uninstalling; two marketplace entries with the same skillId from different repos (dedup key is owner/repo:directory, so both are listed).","solutions":["Uninstall the existing skill first, then retry the install (the error's suggestion: uninstallFirst)","Or install from the original repo reported in context.existing_repo instead of the new one","In UI code, parse the JSON and offer a one-click 'uninstall and reinstall' flow keyed on suggestion === 'uninstallFirst'"],"exampleFix":"// before\nawait invoke(\"install_skill\", { skill }); // SKILL_DIRECTORY_CONFLICT\n\n// after\nawait invoke(\"uninstall_skill\", { directory: conflict.context.directory });\nawait invoke(\"install_skill\", { skill });","handlingStrategy":"try-catch","validationCode":"// Frontend: before install, check for a same-directory skill from another repo\nconst installed = await invoke<InstalledSkill[]>(\"get_all_installed_skills\");\nconst clash = installed.find(\n  (s) => s.directory.toLowerCase() === skill.directory.split(\"/\").pop()?.toLowerCase(),\n);\nif (clash && `${clash.repoOwner}/${clash.repoName}` !== `${skill.repoOwner}/${skill.repoName}`) {\n  promptUninstallFirst(clash.directory); // SKILL_DIRECTORY_CONFLICT would be thrown\n}","typeGuard":"interface SkillError {\n  code: string;\n  context: Record<string, string>;\n  suggestion?: string;\n}\nfunction isSkillError(value: unknown): value is SkillError {\n  if (typeof value !== \"string\") return false;\n  try {\n    const parsed = JSON.parse(value);\n    return typeof parsed?.code === \"string\" && typeof parsed?.context === \"object\";\n  } catch {\n    return false;\n  }\n}","tryCatchPattern":"try {\n  await install(skill);\n} catch (e) {\n  if (isSkillError(e) && e.code === \"SKILL_DIRECTORY_CONFLICT\") {\n    // e.context.existing_repo vs e.context.new_repo; e.suggestion === \"uninstallFirst\"\n    await offerUninstallAndReinstall(e.context.directory);\n    return;\n  }\n  throw e;\n}","preventionTips":["Check the installed list for directory collisions (case-insensitive) before installing","Remember same-repo reinstalls are idempotent — conflicts only occur across different owner/repo","When switching a skill to a fork, uninstall the original first"],"tags":["skills","install","conflict","github"],"backgroundTag":null,"analyzedSha":"a2e22f330273a5b6ffa87cb8b82b624601bac562","analyzedAt":"2026-08-16T03:46:07.889Z","schemaVersion":2},"datasetVersion":"2026-08-16T08:17:34.114Z"}