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

Skill not found: {id}

Error message

Skill not found: {id}

What it means

Uninstall looks up the skill row by id in SQLite via db.get_installed_skill before touching the filesystem; when the query returns None there is nothing to uninstall and the error is raised. It is a plain not-found guard at the top of SkillService::uninstall, before any directory is validated or deleted.

Source

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

            current_app
        );

        Ok(installed_skill)
    }

    /// 卸载 Skill
    ///
    /// 流程:
    /// 1. 从所有应用目录删除
    /// 2. 从 SSOT 删除
    /// 3. 从数据库删除
    pub fn uninstall(db: &Arc<Database>, id: &str) -> Result<SkillUninstallResult> {
        let _state_guard = skill_state_write_guard();

        // 获取 skill 信息
        let skill = db
            .get_installed_skill(id)?
            .ok_or_else(|| anyhow!("Skill not found: {id}"))?;

        // DB 行可能被同步导入污染(远端快照 raw SQL 直接灌库,绕过安装期校验),
        // 也可能是 v3.11.0 引入 sanitize_install_name 之前留下的存量脏值
        // (当年扫描不过滤点开头目录,`.github/SKILL.md` 会存成 `.github`)。
        //
        // 守卫失败时**跳过全部文件系统操作、但仍删除 DB 行**:`db.delete_skill`
        // 全项目只有这一处调用且未暴露为命令,若在此直接返回 Err,用户就再也无法
        // 从界面删掉这条记录,只能手改 SQLite。安全目标是「不碰危险路径」,
        // 不是「把用户锁在坏状态里」。
        let (backup_path, preserved_pi_path, pi_cleanup_incomplete) =
            match Self::require_valid_directory(&skill.directory) {
                Ok(directory) => {
                    let ssot_dir = Self::get_ssot_dir()?;
                    let source = ssot_dir.join(&directory);
                    let mut preserved_pi_path: Option<PathBuf> = None;
                    let mut pi_cleanup_incomplete = false;
                    let mut pi_removal_path = None;

View on GitHub (pinned to a2e22f3302)

Solutions

  1. Refresh the installed-skills list and re-render before retrying
  2. Confirm you are passing the installed skill's id (InstalledSkill.id), not the marketplace/repo identifier
  3. If the row keeps disappearing, check cloud-sync import logs — a remote snapshot may be deleting rows under you

Example fix

// before
await invoke('uninstall_skill', { id });

// after
const skills = await invoke('list_installed_skills');
if (!skills.some(s => s.id === id)) {
  await refreshSkillList(); // stale view — nothing to uninstall
} else {
  await invoke('uninstall_skill', { id });
}
Defensive patterns

Strategy: validation

Validate before calling

const skills = await invoke('list_installed_skills');
if (!skills.some(s => s.id === id)) {
  await refreshSkillList();
  return; // nothing to uninstall
}

Type guard

const isInstalledSkill = (s: unknown, id: string): s is InstalledSkill =>
  typeof s === 'object' && s !== null && (s as any).id === id;

Try / catch

try { await invoke('uninstall_skill', { id }); }
catch (e) {
  if (String(e).includes('Skill not found')) { await refreshSkillList(); return; }
  throw e;
}

Prevention

When it happens

Trigger: Calling the uninstall command with an id that is not in the installed_skills table: the skill was already uninstalled from another window/device, the UI list is stale after a cloud-sync import replaced rows, or the id string is malformed/truncated.

Common situations: Stale list rendered before a background sync import wiped and re-inserted rows; double-click on uninstall where the first call already removed the row; passing a marketplace skill id instead of the installed skill id.

Related errors


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