farion1231/cc-switch · error · anyhow::Error

SKILL_DIRECTORY_CONFLICT

SKILL_DIRECTORY_CONFLICT

Error message

{"code":"SKILL_DIRECTORY_CONFLICT","context":{"directory":"{directory}","existing_repo":"{existing_repo}","new_repo":"{new_repo}"},"suggestion":"uninstallFirst"}

What it means

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'.

Source

Thrown at src-tauri/src/services/skill.rs:735

                continue;
            }

            let same_repo = existing.repo_owner.as_deref() == Some(&skill.repo_owner)
                && existing.repo_name.as_deref() == Some(&skill.repo_name);
            if same_repo {
                let mut updated = existing.clone();
                updated.apps.set_enabled_for(current_app, true);
                db.save_skill(&updated)?;
                Self::sync_to_app_dir(&updated.directory, current_app)?;
                log::info!(
                    "Skill {} 已存在,更新 {:?} 启用状态",
                    updated.name,
                    current_app
                );
                return Ok(Some(updated));
            }

            return Err(anyhow!(format_skill_error(
                "SKILL_DIRECTORY_CONFLICT",
                &[
                    ("directory", install_name),
                    (
                        "existing_repo",
                        &format!(
                            "{}/{}",
                            existing.repo_owner.as_deref().unwrap_or("unknown"),
                            existing.repo_name.as_deref().unwrap_or("unknown")
                        )
                    ),
                    (
                        "new_repo",
                        &format!("{}/{}", skill.repo_owner, skill.repo_name)
                    ),
                ],
                Some("uninstallFirst"),
            )));

View on GitHub (pinned to a2e22f3302)

Solutions

  1. Uninstall the existing skill first, then retry the install (the error's suggestion: uninstallFirst)
  2. Or install from the original repo reported in context.existing_repo instead of the new one
  3. In UI code, parse the JSON and offer a one-click 'uninstall and reinstall' flow keyed on suggestion === 'uninstallFirst'

Example fix

// before
await invoke("install_skill", { skill }); // SKILL_DIRECTORY_CONFLICT

// after
await invoke("uninstall_skill", { directory: conflict.context.directory });
await invoke("install_skill", { skill });
Defensive patterns

Strategy: try-catch

Validate before calling

// Frontend: before install, check for a same-directory skill from another repo
const installed = await invoke<InstalledSkill[]>("get_all_installed_skills");
const clash = installed.find(
  (s) => s.directory.toLowerCase() === skill.directory.split("/").pop()?.toLowerCase(),
);
if (clash && `${clash.repoOwner}/${clash.repoName}` !== `${skill.repoOwner}/${skill.repoName}`) {
  promptUninstallFirst(clash.directory); // SKILL_DIRECTORY_CONFLICT would be thrown
}

Type guard

interface SkillError {
  code: string;
  context: Record<string, string>;
  suggestion?: string;
}
function isSkillError(value: unknown): value is SkillError {
  if (typeof value !== "string") return false;
  try {
    const parsed = JSON.parse(value);
    return typeof parsed?.code === "string" && typeof parsed?.context === "object";
  } catch {
    return false;
  }
}

Try / catch

try {
  await install(skill);
} catch (e) {
  if (isSkillError(e) && e.code === "SKILL_DIRECTORY_CONFLICT") {
    // e.context.existing_repo vs e.context.new_repo; e.suggestion === "uninstallFirst"
    await offerUninstallAndReinstall(e.context.directory);
    return;
  }
  throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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).

Related errors


AI-assisted analysis of farion1231/cc-switch@a2e22f3302 (2026-08-16). Data as JSON: /api/errors/3bc7f51707c6f0b8. Report an issue: GitHub.