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) <noreply@anthropic.com>
This commit is contained in:
Paul Bakaus
2026-04-10 11:04:20 -07:00
co-authored by Claude Opus 4.6
parent 5071d85a4b
commit bd775e414e
2 changed files with 27 additions and 16 deletions
+26 -15
View File
@@ -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;
}
+1 -1
View File
@@ -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": [