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

Skill directory changed during install; please retry

Error message

Skill directory changed during install; please retry

What it means

Thrown during skill installation when the SSOT destination directory did not exist at the final copy step but no downloaded source is available. The install flow skips the network download when dest already exists (src-tauri/src/services/skill.rs:812); downloaded_source is only populated in that download branch. A concurrent install/uninstall removed dest between the fast-path check and re-acquiring the write guard, so the code has neither an existing directory nor a downloaded copy to install from.

Source

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

            &skill.directory,
        );

        let readme_url =
            Self::build_skill_doc_url(&skill.repo_owner, &skill.repo_name, &repo_branch, &doc_path);

        // Re-check after the network download: another install/uninstall may have
        // completed while the lock was intentionally released around `.await`.
        let _state_guard = skill_state_write_guard();
        if let Some(existing) = Self::reuse_existing_install(db, skill, &install_name, current_app)?
        {
            return Ok(existing);
        }

        if !dest.exists() {
            let source = downloaded_source
                .as_ref()
                .map(|(_, source)| source)
                .ok_or_else(|| anyhow!("Skill directory changed during install; please retry"))?;
            Self::preflight_install_destination(source, &install_name, current_app)?;
            Self::copy_dir_recursive(source, &dest)?;
        }

        // 创建 InstalledSkill 记录
        // 计算内容哈希
        let content_hash = Self::compute_dir_hash(&dest).map(Some).unwrap_or_else(|e| {
            log::warn!("Failed to compute content hash for {}: {e}", install_name);
            None
        });

        let installed_skill = InstalledSkill {
            id: skill.key.clone(),
            name: skill.name.clone(),
            description: if skill.description.is_empty() {
                None
            } else {
                Some(skill.description.clone())

View on GitHub (pinned to a2e22f3302)

Solutions

  1. Retry the install command — the second attempt takes the download branch and succeeds
  2. Avoid firing install and uninstall for the same skill concurrently from the frontend (disable the button while a command is in flight)
  3. If it persists, inspect the SSOT dir for a half-deleted directory and remove it, then retry

Example fix

// before
const skill = await invoke('install_skill', { repo });

// after
async function installWithRetry(repo: SkillRepo) {
  try { return await invoke('install_skill', { repo }); }
  catch (e) {
    if (String(e).includes('changed during install'))
      return invoke('install_skill', { repo }); // second pass downloads
    throw e;
  }
}
Defensive patterns

Strategy: retry

Validate before calling

const skills = await invoke('list_installed_skills');
const busy = skills.find(s => s.directory === skill.directory);
// ensure no uninstall/install for this directory is currently in flight
if (inFlight.has(skill.directory)) return; // serialize per-directory

Try / catch

try { await invoke('install_skill', { repo }); }
catch (e) {
  if (String(e).includes('changed during install'))
    return invoke('install_skill', { repo }); // second pass downloads
  throw e;
}

Prevention

When it happens

Trigger: Calling install_skill for a skill whose SSOT directory already exists (no download occurs), while a concurrent uninstall of the same skill (or a cloud-sync snapshot import that wipes SSOT) deletes that directory during the window where the state write guard is released around .await. On re-check reuse_existing_install returns None and dest.exists() is false with downloaded_source == None.

Common situations: Double-clicking install in the UI firing two commands, uninstall and install racing from different windows, or a background cloud-sync import mutating SSOT while the user reinstalls. Typically self-healing on the next attempt because the retry path sees dest missing and downloads.

Related errors


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