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

SKILL_DIRECTORY_CONFLICT

SKILL_DIRECTORY_CONFLICT

Error message

SKILL_DIRECTORY_CONFLICT

What it means

During skill install, the fast path reuse_existing_install finds an installed skill whose install_name (final directory segment) collides with the incoming one, but the existing record belongs to a different repository (owner/name mismatch), so it refuses to reuse or overwrite and returns a SKILL_DIRECTORY_CONFLICT error via format_skill_error. The payload is a JSON string with code, context (directory, existing_repo as 'owner/name'), and a suggestion key, which the frontend parses into a structured, actionable message. This protects the existing installation from being silently replaced by unrelated content that happens to share a directory name.

Solutions

  1. Uninstall the existing skill named in context.existing_repo first, then retry the install.
  2. Install the incoming skill from its original repo instead of the fork/second source so owner/name match and the reuse path applies.
  3. If both are wanted, change one skill's directory name at the source so install_name differs.

Example fix

# before
installed: { directory: "code-review", repo: "alice/skills" }
install({ directory: "code-review", repo_owner: "bob", repo_name: "skills", ... })
# -> SKILL_DIRECTORY_CONFLICT (directory=code-review, existing_repo=alice/skills)

# after
uninstall("code-review")   # remove alice/skills version
install({ directory: "code-review", repo_owner: "bob", repo_name: "skills", ... })  # succeeds
Defensive patterns

Strategy: try-catch

Validate before calling

// Before install, query for a same-name skill from another repo
let existing = db.find_skill_by_directory(&install_name)?;
if let Some(prev) = existing {
    if prev.repo_owner != skill.repo_owner || prev.repo_name != skill.repo_name {
        return plan_uninstall_first(&prev); // avoid the conflict deterministically
    }
}

Try / catch

match install(&db, &skill, &app).await {
    Err(e) if e.to_string().contains("SKILL_DIRECTORY_CONFLICT") => {
        let parsed: serde_json::Value = serde_json::from_str(&e.to_string())?;
        let existing = parsed["context"]["existing_repo"].as_str().unwrap_or("unknown");
        ui::confirm_uninstall(existing)?; // then retry install once
    }
    other => other?,
}

Prevention

When it happens

Trigger: Calling install() for a DiscoverableSkill from repo B whose directory field's last segment equals a skill already installed from repo A (e.g. both repos contain a top-level 'code-review' skill dir).

Common situations: Installing multiple skills collections that ship identically-named folders; a fork of a popular skills repo reinstalled under the fork's owner; renaming upstream changed the repo identity so the same logical skill now looks like a conflict.

Related errors


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

Appendix: source

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

                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 06082e189d)