From bd775e414ef07933dd1aee158232560d63592de5 Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Fri, 10 Apr 2026 11:04:20 -0700 Subject: [PATCH] Fix: normalize provider paths before comparing skill hashes Skills installed via npx skills add resolve {{scripts_path}} to .agents/skills/... while our bundle resolves it per-provider (.claude/skills/..., .cursor/skills/..., etc). Without normalizing, identical content always shows as different. Also compare only one provider instead of all (they have the same content, just different path prefixes). CLI bumped to v2.1.4. Co-Authored-By: Claude Opus 4.6 (1M context) --- bin/commands/skills.mjs | 41 ++++++++++++++++++++++++++--------------- package.json | 2 +- 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/bin/commands/skills.mjs b/bin/commands/skills.mjs index 9f6493089..900254cf5 100644 --- a/bin/commands/skills.mjs +++ b/bin/commands/skills.mjs @@ -112,30 +112,41 @@ async function downloadAndExtractBundle() { return tmpDir; } +/** + * Normalize a SKILL.md's content for comparison by stripping + * provider-specific paths. Different install methods (npx skills add + * vs our bundle) resolve {{scripts_path}} to different provider dirs + * (e.g. .agents vs .claude), so we strip those differences. + */ +function normalizeForHash(content) { + return content.replace(/\.(claude|cursor|agents|gemini|codex|kiro|opencode|pi|trae|trae-cn|rovodev)\/skills\//g, '.PROVIDER/skills/'); +} + /** * Compare local skills against a downloaded bundle. * Only checks skills that exist in the bundle (ignores user's custom - * skills that aren't part of impeccable). + * skills that aren't part of impeccable). Uses a single provider from + * the bundle as canonical and normalizes provider-specific paths. * Returns true if every bundle skill matches the local copy. */ function isUpToDate(root, providers, bundleDir) { - for (const provider of providers) { - const bundleSkillsDir = join(bundleDir, provider, 'skills'); - const localSkillsDir = join(root, provider, 'skills'); - if (!existsSync(bundleSkillsDir)) continue; + // Use the first installed provider for comparison + const provider = providers[0]; + const bundleSkillsDir = join(bundleDir, provider, 'skills'); + const localSkillsDir = join(root, provider, 'skills'); + if (!existsSync(bundleSkillsDir)) return false; - for (const name of readdirSync(bundleSkillsDir)) { - const bundleMd = join(bundleSkillsDir, name, 'SKILL.md'); - const localMd = join(localSkillsDir, name, 'SKILL.md'); - if (!existsSync(bundleMd)) continue; + for (const name of readdirSync(bundleSkillsDir)) { + const bundleMd = join(bundleSkillsDir, name, 'SKILL.md'); + const localMd = join(localSkillsDir, name, 'SKILL.md'); + if (!existsSync(bundleMd)) continue; - // Missing locally = needs update - if (!existsSync(localMd)) return false; + // Missing locally = needs update + if (!existsSync(localMd)) return false; - const bundleHash = createHash('sha256').update(readFileSync(bundleMd)).digest('hex'); - const localHash = createHash('sha256').update(readFileSync(localMd)).digest('hex'); - if (bundleHash !== localHash) return false; - } + const bundleHash = createHash('sha256').update(normalizeForHash(readFileSync(bundleMd, 'utf-8'))).digest('hex'); + const localHash = createHash('sha256').update(normalizeForHash(readFileSync(localMd, 'utf-8'))).digest('hex'); + if (bundleHash !== localHash) return false; } return true; } diff --git a/package.json b/package.json index 1bcc0bca1..5afcdbad9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "impeccable", - "version": "2.1.3", + "version": "2.1.4", "author": "Paul Bakaus", "description": "Design skills, commands, and anti-pattern detection for AI coding agents", "keywords": [