diff --git a/.agents/skills/impeccable/scripts/cleanup-deprecated.mjs b/.agents/skills/impeccable/scripts/cleanup-deprecated.mjs deleted file mode 100644 index bc6400e90..000000000 --- a/.agents/skills/impeccable/scripts/cleanup-deprecated.mjs +++ /dev/null @@ -1,284 +0,0 @@ -#!/usr/bin/env node -/** - * Cleans up deprecated Impeccable skill files, symlinks, and - * skills-lock.json entries left over from previous versions. - * - * Safe to run repeatedly -- it is a no-op when nothing needs cleaning. - * - * Usage (from the project root): - * node {{scripts_path}}/cleanup-deprecated.mjs - * - * What it does: - * 1. Finds every harness-specific skills directory (.claude/skills, - * .cursor/skills, .agents/skills, etc.). - * 2. For each deprecated skill name (with and without i- prefix), - * checks if the directory exists and its SKILL.md mentions - * "impeccable" (to avoid deleting unrelated user skills). - * 3. Deletes confirmed matches (files, directories, or symlinks). - * 4. Removes the corresponding entries from skills-lock.json. - */ - -import { existsSync, readFileSync, writeFileSync, rmSync, readdirSync, statSync, lstatSync, unlinkSync } from 'node:fs'; -import { join, resolve } from 'node:path'; - -// Skills that were renamed, merged, or folded in v2.0, v2.1, and v3.0. -const DEPRECATED_NAMES = [ - // v2.0 renames - 'frontend-design', // renamed to impeccable - 'teach-impeccable', // folded into /impeccable init - // v2.1 merges - 'arrange', // renamed to layout - 'normalize', // merged into polish - 'onboard', // merged into harden - 'extract', // merged into /impeccable extract - // v3.0 consolidation: all standalone skills -> /impeccable sub-commands - 'adapt', - 'animate', - 'audit', - 'bolder', - 'clarify', - 'colorize', - 'critique', - 'delight', - 'distill', - 'harden', - 'layout', - 'optimize', - 'overdrive', - 'polish', - 'quieter', - 'shape', - 'typeset', -]; - -// All known harness directories that may contain a skills/ subfolder. -const HARNESS_DIRS = [ - '.claude', '.cursor', '.gemini', '.codex', '.agents', - '.trae', '.trae-cn', '.pi', '.opencode', '.kiro', '.rovodev', -]; - -// Per-skill fingerprints for SKILL.md bodies that never mentioned -// "impeccable" in their v2.x source. Used as a last-resort match -// when no skills-lock.json exists and the word heuristic fails. -// The strings are lifted verbatim from the v2.x frontmatter -// descriptions, so collisions with hand-written user skills are -// vanishingly unlikely. -const SKILL_FINGERPRINTS = { - harden: 'Make interfaces production-ready: error handling, empty states', - optimize: 'Diagnoses and fixes UI performance across loading speed', -}; - -/** - * Walk up from startDir until we find a directory that looks like a - * project root (has package.json, .git, or skills-lock.json). - */ -export function findProjectRoot(startDir = process.cwd()) { - let dir = resolve(startDir); - const { root } = { root: '/' }; - while (dir !== root) { - if ( - existsSync(join(dir, 'package.json')) || - existsSync(join(dir, '.git')) || - existsSync(join(dir, 'skills-lock.json')) - ) { - return dir; - } - const parent = resolve(dir, '..'); - if (parent === dir) break; - dir = parent; - } - return resolve(startDir); -} - -/** - * Load skills-lock.json from the project root, or null if missing/unreadable. - */ -export function loadLock(projectRoot) { - const lockPath = join(projectRoot, 'skills-lock.json'); - if (!existsSync(lockPath)) return null; - try { - return JSON.parse(readFileSync(lockPath, 'utf-8')); - } catch { - return null; - } -} - -/** - * Check whether a skill directory belongs to Impeccable. Three layered - * signals, in order of reliability: - * 1. Lock source equals "pbakaus/impeccable" (authoritative). - * 2. SKILL.md body contains the word "impeccable". - * 3. SKILL.md body contains a per-skill fingerprint (for harden and - * optimize, whose v2.x SKILL.md never mentioned the pack name). - */ -export function isImpeccableSkill(skillDir, { skillName, lock } = {}) { - // 1. Authoritative: the lock file claims this skill is ours. - if (skillName && lock?.skills?.[skillName]?.source === 'pbakaus/impeccable') { - return true; - } - const skillMd = join(skillDir, 'SKILL.md'); - if (!existsSync(skillMd)) return false; - let content; - try { - content = readFileSync(skillMd, 'utf-8'); - } catch { - return false; - } - // 2. Word-level content heuristic. - if (/impeccable/i.test(content)) return true; - // 3. Per-skill fingerprint for old skills that never mentioned the pack. - // Strip the i- prefix so both `harden` and `i-harden` resolve to the - // same fingerprint entry. - const unprefixed = skillName?.startsWith('i-') ? skillName.slice(2) : skillName; - const fingerprint = unprefixed && SKILL_FINGERPRINTS[unprefixed]; - if (fingerprint && content.includes(fingerprint)) return true; - return false; -} - -/** - * Build the full list of names to check: each deprecated name, plus - * its i-prefixed variant. - */ -export function buildTargetNames() { - const names = []; - for (const name of DEPRECATED_NAMES) { - names.push(name); - names.push(`i-${name}`); - } - return names; -} - -/** - * Find every skills directory across all harness dirs in the project. - * Returns absolute paths that exist on disk. - */ -export function findSkillsDirs(projectRoot) { - const dirs = []; - for (const harness of HARNESS_DIRS) { - const candidate = join(projectRoot, harness, 'skills'); - if (existsSync(candidate)) { - dirs.push(candidate); - } - } - return dirs; -} - -/** - * Remove deprecated skill directories/symlinks from all harness dirs. - * Reads skills-lock.json so the authoritative "source" field can - * drive deletion even when SKILL.md never mentions impeccable. - * Returns an array of paths that were deleted. - */ -export function removeDeprecatedSkills(projectRoot, lock) { - if (lock === undefined) lock = loadLock(projectRoot); - const targets = buildTargetNames(); - const skillsDirs = findSkillsDirs(projectRoot); - const deleted = []; - - for (const skillsDir of skillsDirs) { - for (const name of targets) { - const skillPath = join(skillsDir, name); - - // Use lstat to detect symlinks (existsSync follows symlinks and - // returns false for dangling ones). - let stat; - try { - stat = lstatSync(skillPath); - } catch { - continue; // does not exist at all - } - - if (stat.isSymbolicLink()) { - // Symlink: check the target if it's alive, otherwise treat - // dangling symlinks to deprecated names as safe to remove. - const targetAlive = existsSync(skillPath); - const isMatch = targetAlive - ? isImpeccableSkill(skillPath, { skillName: name, lock }) - : true; - if (isMatch) { - unlinkSync(skillPath); - deleted.push(skillPath); - } - continue; - } - - // Regular directory -- verify it belongs to impeccable - if (isImpeccableSkill(skillPath, { skillName: name, lock })) { - rmSync(skillPath, { recursive: true, force: true }); - deleted.push(skillPath); - } - } - } - - return deleted; -} - -/** - * Remove deprecated entries from skills-lock.json. - * Only removes entries whose source is "pbakaus/impeccable". - * Returns the list of removed skill names. - */ -export function cleanSkillsLock(projectRoot) { - const lockPath = join(projectRoot, 'skills-lock.json'); - if (!existsSync(lockPath)) return []; - - let lock; - try { - lock = JSON.parse(readFileSync(lockPath, 'utf-8')); - } catch { - return []; - } - - if (!lock.skills || typeof lock.skills !== 'object') return []; - - const targets = buildTargetNames(); - const removed = []; - - for (const name of targets) { - const entry = lock.skills[name]; - if (!entry) continue; - // Only remove if it belongs to impeccable - if (entry.source === 'pbakaus/impeccable') { - delete lock.skills[name]; - removed.push(name); - } - } - - if (removed.length > 0) { - writeFileSync(lockPath, JSON.stringify(lock, null, 2) + '\n', 'utf-8'); - } - - return removed; -} - -/** - * Run the full cleanup. Returns a summary object. - * - * Order matters: read the lock and delete directories first, then - * strip lock entries. Otherwise the authoritative signal is gone by - * the time directory deletion runs. - */ -export function cleanup(projectRoot) { - const root = projectRoot || findProjectRoot(); - const lock = loadLock(root); - const deletedPaths = removeDeprecatedSkills(root, lock); - const removedLockEntries = cleanSkillsLock(root); - return { deletedPaths, removedLockEntries, projectRoot: root }; -} - -// CLI entry point -if (process.argv[1] && resolve(process.argv[1]) === resolve(new URL(import.meta.url).pathname)) { - const result = cleanup(); - if (result.deletedPaths.length === 0 && result.removedLockEntries.length === 0) { - console.log('No deprecated Impeccable skills found. Nothing to clean up.'); - } else { - if (result.deletedPaths.length > 0) { - console.log(`Removed ${result.deletedPaths.length} deprecated skill(s):`); - for (const p of result.deletedPaths) console.log(` - ${p}`); - } - if (result.removedLockEntries.length > 0) { - console.log(`Cleaned ${result.removedLockEntries.length} entry/entries from skills-lock.json:`); - for (const name of result.removedLockEntries) console.log(` - ${name}`); - } - } -} diff --git a/.claude/skills/impeccable/scripts/cleanup-deprecated.mjs b/.claude/skills/impeccable/scripts/cleanup-deprecated.mjs deleted file mode 100644 index bc6400e90..000000000 --- a/.claude/skills/impeccable/scripts/cleanup-deprecated.mjs +++ /dev/null @@ -1,284 +0,0 @@ -#!/usr/bin/env node -/** - * Cleans up deprecated Impeccable skill files, symlinks, and - * skills-lock.json entries left over from previous versions. - * - * Safe to run repeatedly -- it is a no-op when nothing needs cleaning. - * - * Usage (from the project root): - * node {{scripts_path}}/cleanup-deprecated.mjs - * - * What it does: - * 1. Finds every harness-specific skills directory (.claude/skills, - * .cursor/skills, .agents/skills, etc.). - * 2. For each deprecated skill name (with and without i- prefix), - * checks if the directory exists and its SKILL.md mentions - * "impeccable" (to avoid deleting unrelated user skills). - * 3. Deletes confirmed matches (files, directories, or symlinks). - * 4. Removes the corresponding entries from skills-lock.json. - */ - -import { existsSync, readFileSync, writeFileSync, rmSync, readdirSync, statSync, lstatSync, unlinkSync } from 'node:fs'; -import { join, resolve } from 'node:path'; - -// Skills that were renamed, merged, or folded in v2.0, v2.1, and v3.0. -const DEPRECATED_NAMES = [ - // v2.0 renames - 'frontend-design', // renamed to impeccable - 'teach-impeccable', // folded into /impeccable init - // v2.1 merges - 'arrange', // renamed to layout - 'normalize', // merged into polish - 'onboard', // merged into harden - 'extract', // merged into /impeccable extract - // v3.0 consolidation: all standalone skills -> /impeccable sub-commands - 'adapt', - 'animate', - 'audit', - 'bolder', - 'clarify', - 'colorize', - 'critique', - 'delight', - 'distill', - 'harden', - 'layout', - 'optimize', - 'overdrive', - 'polish', - 'quieter', - 'shape', - 'typeset', -]; - -// All known harness directories that may contain a skills/ subfolder. -const HARNESS_DIRS = [ - '.claude', '.cursor', '.gemini', '.codex', '.agents', - '.trae', '.trae-cn', '.pi', '.opencode', '.kiro', '.rovodev', -]; - -// Per-skill fingerprints for SKILL.md bodies that never mentioned -// "impeccable" in their v2.x source. Used as a last-resort match -// when no skills-lock.json exists and the word heuristic fails. -// The strings are lifted verbatim from the v2.x frontmatter -// descriptions, so collisions with hand-written user skills are -// vanishingly unlikely. -const SKILL_FINGERPRINTS = { - harden: 'Make interfaces production-ready: error handling, empty states', - optimize: 'Diagnoses and fixes UI performance across loading speed', -}; - -/** - * Walk up from startDir until we find a directory that looks like a - * project root (has package.json, .git, or skills-lock.json). - */ -export function findProjectRoot(startDir = process.cwd()) { - let dir = resolve(startDir); - const { root } = { root: '/' }; - while (dir !== root) { - if ( - existsSync(join(dir, 'package.json')) || - existsSync(join(dir, '.git')) || - existsSync(join(dir, 'skills-lock.json')) - ) { - return dir; - } - const parent = resolve(dir, '..'); - if (parent === dir) break; - dir = parent; - } - return resolve(startDir); -} - -/** - * Load skills-lock.json from the project root, or null if missing/unreadable. - */ -export function loadLock(projectRoot) { - const lockPath = join(projectRoot, 'skills-lock.json'); - if (!existsSync(lockPath)) return null; - try { - return JSON.parse(readFileSync(lockPath, 'utf-8')); - } catch { - return null; - } -} - -/** - * Check whether a skill directory belongs to Impeccable. Three layered - * signals, in order of reliability: - * 1. Lock source equals "pbakaus/impeccable" (authoritative). - * 2. SKILL.md body contains the word "impeccable". - * 3. SKILL.md body contains a per-skill fingerprint (for harden and - * optimize, whose v2.x SKILL.md never mentioned the pack name). - */ -export function isImpeccableSkill(skillDir, { skillName, lock } = {}) { - // 1. Authoritative: the lock file claims this skill is ours. - if (skillName && lock?.skills?.[skillName]?.source === 'pbakaus/impeccable') { - return true; - } - const skillMd = join(skillDir, 'SKILL.md'); - if (!existsSync(skillMd)) return false; - let content; - try { - content = readFileSync(skillMd, 'utf-8'); - } catch { - return false; - } - // 2. Word-level content heuristic. - if (/impeccable/i.test(content)) return true; - // 3. Per-skill fingerprint for old skills that never mentioned the pack. - // Strip the i- prefix so both `harden` and `i-harden` resolve to the - // same fingerprint entry. - const unprefixed = skillName?.startsWith('i-') ? skillName.slice(2) : skillName; - const fingerprint = unprefixed && SKILL_FINGERPRINTS[unprefixed]; - if (fingerprint && content.includes(fingerprint)) return true; - return false; -} - -/** - * Build the full list of names to check: each deprecated name, plus - * its i-prefixed variant. - */ -export function buildTargetNames() { - const names = []; - for (const name of DEPRECATED_NAMES) { - names.push(name); - names.push(`i-${name}`); - } - return names; -} - -/** - * Find every skills directory across all harness dirs in the project. - * Returns absolute paths that exist on disk. - */ -export function findSkillsDirs(projectRoot) { - const dirs = []; - for (const harness of HARNESS_DIRS) { - const candidate = join(projectRoot, harness, 'skills'); - if (existsSync(candidate)) { - dirs.push(candidate); - } - } - return dirs; -} - -/** - * Remove deprecated skill directories/symlinks from all harness dirs. - * Reads skills-lock.json so the authoritative "source" field can - * drive deletion even when SKILL.md never mentions impeccable. - * Returns an array of paths that were deleted. - */ -export function removeDeprecatedSkills(projectRoot, lock) { - if (lock === undefined) lock = loadLock(projectRoot); - const targets = buildTargetNames(); - const skillsDirs = findSkillsDirs(projectRoot); - const deleted = []; - - for (const skillsDir of skillsDirs) { - for (const name of targets) { - const skillPath = join(skillsDir, name); - - // Use lstat to detect symlinks (existsSync follows symlinks and - // returns false for dangling ones). - let stat; - try { - stat = lstatSync(skillPath); - } catch { - continue; // does not exist at all - } - - if (stat.isSymbolicLink()) { - // Symlink: check the target if it's alive, otherwise treat - // dangling symlinks to deprecated names as safe to remove. - const targetAlive = existsSync(skillPath); - const isMatch = targetAlive - ? isImpeccableSkill(skillPath, { skillName: name, lock }) - : true; - if (isMatch) { - unlinkSync(skillPath); - deleted.push(skillPath); - } - continue; - } - - // Regular directory -- verify it belongs to impeccable - if (isImpeccableSkill(skillPath, { skillName: name, lock })) { - rmSync(skillPath, { recursive: true, force: true }); - deleted.push(skillPath); - } - } - } - - return deleted; -} - -/** - * Remove deprecated entries from skills-lock.json. - * Only removes entries whose source is "pbakaus/impeccable". - * Returns the list of removed skill names. - */ -export function cleanSkillsLock(projectRoot) { - const lockPath = join(projectRoot, 'skills-lock.json'); - if (!existsSync(lockPath)) return []; - - let lock; - try { - lock = JSON.parse(readFileSync(lockPath, 'utf-8')); - } catch { - return []; - } - - if (!lock.skills || typeof lock.skills !== 'object') return []; - - const targets = buildTargetNames(); - const removed = []; - - for (const name of targets) { - const entry = lock.skills[name]; - if (!entry) continue; - // Only remove if it belongs to impeccable - if (entry.source === 'pbakaus/impeccable') { - delete lock.skills[name]; - removed.push(name); - } - } - - if (removed.length > 0) { - writeFileSync(lockPath, JSON.stringify(lock, null, 2) + '\n', 'utf-8'); - } - - return removed; -} - -/** - * Run the full cleanup. Returns a summary object. - * - * Order matters: read the lock and delete directories first, then - * strip lock entries. Otherwise the authoritative signal is gone by - * the time directory deletion runs. - */ -export function cleanup(projectRoot) { - const root = projectRoot || findProjectRoot(); - const lock = loadLock(root); - const deletedPaths = removeDeprecatedSkills(root, lock); - const removedLockEntries = cleanSkillsLock(root); - return { deletedPaths, removedLockEntries, projectRoot: root }; -} - -// CLI entry point -if (process.argv[1] && resolve(process.argv[1]) === resolve(new URL(import.meta.url).pathname)) { - const result = cleanup(); - if (result.deletedPaths.length === 0 && result.removedLockEntries.length === 0) { - console.log('No deprecated Impeccable skills found. Nothing to clean up.'); - } else { - if (result.deletedPaths.length > 0) { - console.log(`Removed ${result.deletedPaths.length} deprecated skill(s):`); - for (const p of result.deletedPaths) console.log(` - ${p}`); - } - if (result.removedLockEntries.length > 0) { - console.log(`Cleaned ${result.removedLockEntries.length} entry/entries from skills-lock.json:`); - for (const name of result.removedLockEntries) console.log(` - ${name}`); - } - } -} diff --git a/.cursor/skills/impeccable/scripts/cleanup-deprecated.mjs b/.cursor/skills/impeccable/scripts/cleanup-deprecated.mjs deleted file mode 100644 index bc6400e90..000000000 --- a/.cursor/skills/impeccable/scripts/cleanup-deprecated.mjs +++ /dev/null @@ -1,284 +0,0 @@ -#!/usr/bin/env node -/** - * Cleans up deprecated Impeccable skill files, symlinks, and - * skills-lock.json entries left over from previous versions. - * - * Safe to run repeatedly -- it is a no-op when nothing needs cleaning. - * - * Usage (from the project root): - * node {{scripts_path}}/cleanup-deprecated.mjs - * - * What it does: - * 1. Finds every harness-specific skills directory (.claude/skills, - * .cursor/skills, .agents/skills, etc.). - * 2. For each deprecated skill name (with and without i- prefix), - * checks if the directory exists and its SKILL.md mentions - * "impeccable" (to avoid deleting unrelated user skills). - * 3. Deletes confirmed matches (files, directories, or symlinks). - * 4. Removes the corresponding entries from skills-lock.json. - */ - -import { existsSync, readFileSync, writeFileSync, rmSync, readdirSync, statSync, lstatSync, unlinkSync } from 'node:fs'; -import { join, resolve } from 'node:path'; - -// Skills that were renamed, merged, or folded in v2.0, v2.1, and v3.0. -const DEPRECATED_NAMES = [ - // v2.0 renames - 'frontend-design', // renamed to impeccable - 'teach-impeccable', // folded into /impeccable init - // v2.1 merges - 'arrange', // renamed to layout - 'normalize', // merged into polish - 'onboard', // merged into harden - 'extract', // merged into /impeccable extract - // v3.0 consolidation: all standalone skills -> /impeccable sub-commands - 'adapt', - 'animate', - 'audit', - 'bolder', - 'clarify', - 'colorize', - 'critique', - 'delight', - 'distill', - 'harden', - 'layout', - 'optimize', - 'overdrive', - 'polish', - 'quieter', - 'shape', - 'typeset', -]; - -// All known harness directories that may contain a skills/ subfolder. -const HARNESS_DIRS = [ - '.claude', '.cursor', '.gemini', '.codex', '.agents', - '.trae', '.trae-cn', '.pi', '.opencode', '.kiro', '.rovodev', -]; - -// Per-skill fingerprints for SKILL.md bodies that never mentioned -// "impeccable" in their v2.x source. Used as a last-resort match -// when no skills-lock.json exists and the word heuristic fails. -// The strings are lifted verbatim from the v2.x frontmatter -// descriptions, so collisions with hand-written user skills are -// vanishingly unlikely. -const SKILL_FINGERPRINTS = { - harden: 'Make interfaces production-ready: error handling, empty states', - optimize: 'Diagnoses and fixes UI performance across loading speed', -}; - -/** - * Walk up from startDir until we find a directory that looks like a - * project root (has package.json, .git, or skills-lock.json). - */ -export function findProjectRoot(startDir = process.cwd()) { - let dir = resolve(startDir); - const { root } = { root: '/' }; - while (dir !== root) { - if ( - existsSync(join(dir, 'package.json')) || - existsSync(join(dir, '.git')) || - existsSync(join(dir, 'skills-lock.json')) - ) { - return dir; - } - const parent = resolve(dir, '..'); - if (parent === dir) break; - dir = parent; - } - return resolve(startDir); -} - -/** - * Load skills-lock.json from the project root, or null if missing/unreadable. - */ -export function loadLock(projectRoot) { - const lockPath = join(projectRoot, 'skills-lock.json'); - if (!existsSync(lockPath)) return null; - try { - return JSON.parse(readFileSync(lockPath, 'utf-8')); - } catch { - return null; - } -} - -/** - * Check whether a skill directory belongs to Impeccable. Three layered - * signals, in order of reliability: - * 1. Lock source equals "pbakaus/impeccable" (authoritative). - * 2. SKILL.md body contains the word "impeccable". - * 3. SKILL.md body contains a per-skill fingerprint (for harden and - * optimize, whose v2.x SKILL.md never mentioned the pack name). - */ -export function isImpeccableSkill(skillDir, { skillName, lock } = {}) { - // 1. Authoritative: the lock file claims this skill is ours. - if (skillName && lock?.skills?.[skillName]?.source === 'pbakaus/impeccable') { - return true; - } - const skillMd = join(skillDir, 'SKILL.md'); - if (!existsSync(skillMd)) return false; - let content; - try { - content = readFileSync(skillMd, 'utf-8'); - } catch { - return false; - } - // 2. Word-level content heuristic. - if (/impeccable/i.test(content)) return true; - // 3. Per-skill fingerprint for old skills that never mentioned the pack. - // Strip the i- prefix so both `harden` and `i-harden` resolve to the - // same fingerprint entry. - const unprefixed = skillName?.startsWith('i-') ? skillName.slice(2) : skillName; - const fingerprint = unprefixed && SKILL_FINGERPRINTS[unprefixed]; - if (fingerprint && content.includes(fingerprint)) return true; - return false; -} - -/** - * Build the full list of names to check: each deprecated name, plus - * its i-prefixed variant. - */ -export function buildTargetNames() { - const names = []; - for (const name of DEPRECATED_NAMES) { - names.push(name); - names.push(`i-${name}`); - } - return names; -} - -/** - * Find every skills directory across all harness dirs in the project. - * Returns absolute paths that exist on disk. - */ -export function findSkillsDirs(projectRoot) { - const dirs = []; - for (const harness of HARNESS_DIRS) { - const candidate = join(projectRoot, harness, 'skills'); - if (existsSync(candidate)) { - dirs.push(candidate); - } - } - return dirs; -} - -/** - * Remove deprecated skill directories/symlinks from all harness dirs. - * Reads skills-lock.json so the authoritative "source" field can - * drive deletion even when SKILL.md never mentions impeccable. - * Returns an array of paths that were deleted. - */ -export function removeDeprecatedSkills(projectRoot, lock) { - if (lock === undefined) lock = loadLock(projectRoot); - const targets = buildTargetNames(); - const skillsDirs = findSkillsDirs(projectRoot); - const deleted = []; - - for (const skillsDir of skillsDirs) { - for (const name of targets) { - const skillPath = join(skillsDir, name); - - // Use lstat to detect symlinks (existsSync follows symlinks and - // returns false for dangling ones). - let stat; - try { - stat = lstatSync(skillPath); - } catch { - continue; // does not exist at all - } - - if (stat.isSymbolicLink()) { - // Symlink: check the target if it's alive, otherwise treat - // dangling symlinks to deprecated names as safe to remove. - const targetAlive = existsSync(skillPath); - const isMatch = targetAlive - ? isImpeccableSkill(skillPath, { skillName: name, lock }) - : true; - if (isMatch) { - unlinkSync(skillPath); - deleted.push(skillPath); - } - continue; - } - - // Regular directory -- verify it belongs to impeccable - if (isImpeccableSkill(skillPath, { skillName: name, lock })) { - rmSync(skillPath, { recursive: true, force: true }); - deleted.push(skillPath); - } - } - } - - return deleted; -} - -/** - * Remove deprecated entries from skills-lock.json. - * Only removes entries whose source is "pbakaus/impeccable". - * Returns the list of removed skill names. - */ -export function cleanSkillsLock(projectRoot) { - const lockPath = join(projectRoot, 'skills-lock.json'); - if (!existsSync(lockPath)) return []; - - let lock; - try { - lock = JSON.parse(readFileSync(lockPath, 'utf-8')); - } catch { - return []; - } - - if (!lock.skills || typeof lock.skills !== 'object') return []; - - const targets = buildTargetNames(); - const removed = []; - - for (const name of targets) { - const entry = lock.skills[name]; - if (!entry) continue; - // Only remove if it belongs to impeccable - if (entry.source === 'pbakaus/impeccable') { - delete lock.skills[name]; - removed.push(name); - } - } - - if (removed.length > 0) { - writeFileSync(lockPath, JSON.stringify(lock, null, 2) + '\n', 'utf-8'); - } - - return removed; -} - -/** - * Run the full cleanup. Returns a summary object. - * - * Order matters: read the lock and delete directories first, then - * strip lock entries. Otherwise the authoritative signal is gone by - * the time directory deletion runs. - */ -export function cleanup(projectRoot) { - const root = projectRoot || findProjectRoot(); - const lock = loadLock(root); - const deletedPaths = removeDeprecatedSkills(root, lock); - const removedLockEntries = cleanSkillsLock(root); - return { deletedPaths, removedLockEntries, projectRoot: root }; -} - -// CLI entry point -if (process.argv[1] && resolve(process.argv[1]) === resolve(new URL(import.meta.url).pathname)) { - const result = cleanup(); - if (result.deletedPaths.length === 0 && result.removedLockEntries.length === 0) { - console.log('No deprecated Impeccable skills found. Nothing to clean up.'); - } else { - if (result.deletedPaths.length > 0) { - console.log(`Removed ${result.deletedPaths.length} deprecated skill(s):`); - for (const p of result.deletedPaths) console.log(` - ${p}`); - } - if (result.removedLockEntries.length > 0) { - console.log(`Cleaned ${result.removedLockEntries.length} entry/entries from skills-lock.json:`); - for (const name of result.removedLockEntries) console.log(` - ${name}`); - } - } -} diff --git a/.gemini/skills/impeccable/scripts/cleanup-deprecated.mjs b/.gemini/skills/impeccable/scripts/cleanup-deprecated.mjs deleted file mode 100644 index bc6400e90..000000000 --- a/.gemini/skills/impeccable/scripts/cleanup-deprecated.mjs +++ /dev/null @@ -1,284 +0,0 @@ -#!/usr/bin/env node -/** - * Cleans up deprecated Impeccable skill files, symlinks, and - * skills-lock.json entries left over from previous versions. - * - * Safe to run repeatedly -- it is a no-op when nothing needs cleaning. - * - * Usage (from the project root): - * node {{scripts_path}}/cleanup-deprecated.mjs - * - * What it does: - * 1. Finds every harness-specific skills directory (.claude/skills, - * .cursor/skills, .agents/skills, etc.). - * 2. For each deprecated skill name (with and without i- prefix), - * checks if the directory exists and its SKILL.md mentions - * "impeccable" (to avoid deleting unrelated user skills). - * 3. Deletes confirmed matches (files, directories, or symlinks). - * 4. Removes the corresponding entries from skills-lock.json. - */ - -import { existsSync, readFileSync, writeFileSync, rmSync, readdirSync, statSync, lstatSync, unlinkSync } from 'node:fs'; -import { join, resolve } from 'node:path'; - -// Skills that were renamed, merged, or folded in v2.0, v2.1, and v3.0. -const DEPRECATED_NAMES = [ - // v2.0 renames - 'frontend-design', // renamed to impeccable - 'teach-impeccable', // folded into /impeccable init - // v2.1 merges - 'arrange', // renamed to layout - 'normalize', // merged into polish - 'onboard', // merged into harden - 'extract', // merged into /impeccable extract - // v3.0 consolidation: all standalone skills -> /impeccable sub-commands - 'adapt', - 'animate', - 'audit', - 'bolder', - 'clarify', - 'colorize', - 'critique', - 'delight', - 'distill', - 'harden', - 'layout', - 'optimize', - 'overdrive', - 'polish', - 'quieter', - 'shape', - 'typeset', -]; - -// All known harness directories that may contain a skills/ subfolder. -const HARNESS_DIRS = [ - '.claude', '.cursor', '.gemini', '.codex', '.agents', - '.trae', '.trae-cn', '.pi', '.opencode', '.kiro', '.rovodev', -]; - -// Per-skill fingerprints for SKILL.md bodies that never mentioned -// "impeccable" in their v2.x source. Used as a last-resort match -// when no skills-lock.json exists and the word heuristic fails. -// The strings are lifted verbatim from the v2.x frontmatter -// descriptions, so collisions with hand-written user skills are -// vanishingly unlikely. -const SKILL_FINGERPRINTS = { - harden: 'Make interfaces production-ready: error handling, empty states', - optimize: 'Diagnoses and fixes UI performance across loading speed', -}; - -/** - * Walk up from startDir until we find a directory that looks like a - * project root (has package.json, .git, or skills-lock.json). - */ -export function findProjectRoot(startDir = process.cwd()) { - let dir = resolve(startDir); - const { root } = { root: '/' }; - while (dir !== root) { - if ( - existsSync(join(dir, 'package.json')) || - existsSync(join(dir, '.git')) || - existsSync(join(dir, 'skills-lock.json')) - ) { - return dir; - } - const parent = resolve(dir, '..'); - if (parent === dir) break; - dir = parent; - } - return resolve(startDir); -} - -/** - * Load skills-lock.json from the project root, or null if missing/unreadable. - */ -export function loadLock(projectRoot) { - const lockPath = join(projectRoot, 'skills-lock.json'); - if (!existsSync(lockPath)) return null; - try { - return JSON.parse(readFileSync(lockPath, 'utf-8')); - } catch { - return null; - } -} - -/** - * Check whether a skill directory belongs to Impeccable. Three layered - * signals, in order of reliability: - * 1. Lock source equals "pbakaus/impeccable" (authoritative). - * 2. SKILL.md body contains the word "impeccable". - * 3. SKILL.md body contains a per-skill fingerprint (for harden and - * optimize, whose v2.x SKILL.md never mentioned the pack name). - */ -export function isImpeccableSkill(skillDir, { skillName, lock } = {}) { - // 1. Authoritative: the lock file claims this skill is ours. - if (skillName && lock?.skills?.[skillName]?.source === 'pbakaus/impeccable') { - return true; - } - const skillMd = join(skillDir, 'SKILL.md'); - if (!existsSync(skillMd)) return false; - let content; - try { - content = readFileSync(skillMd, 'utf-8'); - } catch { - return false; - } - // 2. Word-level content heuristic. - if (/impeccable/i.test(content)) return true; - // 3. Per-skill fingerprint for old skills that never mentioned the pack. - // Strip the i- prefix so both `harden` and `i-harden` resolve to the - // same fingerprint entry. - const unprefixed = skillName?.startsWith('i-') ? skillName.slice(2) : skillName; - const fingerprint = unprefixed && SKILL_FINGERPRINTS[unprefixed]; - if (fingerprint && content.includes(fingerprint)) return true; - return false; -} - -/** - * Build the full list of names to check: each deprecated name, plus - * its i-prefixed variant. - */ -export function buildTargetNames() { - const names = []; - for (const name of DEPRECATED_NAMES) { - names.push(name); - names.push(`i-${name}`); - } - return names; -} - -/** - * Find every skills directory across all harness dirs in the project. - * Returns absolute paths that exist on disk. - */ -export function findSkillsDirs(projectRoot) { - const dirs = []; - for (const harness of HARNESS_DIRS) { - const candidate = join(projectRoot, harness, 'skills'); - if (existsSync(candidate)) { - dirs.push(candidate); - } - } - return dirs; -} - -/** - * Remove deprecated skill directories/symlinks from all harness dirs. - * Reads skills-lock.json so the authoritative "source" field can - * drive deletion even when SKILL.md never mentions impeccable. - * Returns an array of paths that were deleted. - */ -export function removeDeprecatedSkills(projectRoot, lock) { - if (lock === undefined) lock = loadLock(projectRoot); - const targets = buildTargetNames(); - const skillsDirs = findSkillsDirs(projectRoot); - const deleted = []; - - for (const skillsDir of skillsDirs) { - for (const name of targets) { - const skillPath = join(skillsDir, name); - - // Use lstat to detect symlinks (existsSync follows symlinks and - // returns false for dangling ones). - let stat; - try { - stat = lstatSync(skillPath); - } catch { - continue; // does not exist at all - } - - if (stat.isSymbolicLink()) { - // Symlink: check the target if it's alive, otherwise treat - // dangling symlinks to deprecated names as safe to remove. - const targetAlive = existsSync(skillPath); - const isMatch = targetAlive - ? isImpeccableSkill(skillPath, { skillName: name, lock }) - : true; - if (isMatch) { - unlinkSync(skillPath); - deleted.push(skillPath); - } - continue; - } - - // Regular directory -- verify it belongs to impeccable - if (isImpeccableSkill(skillPath, { skillName: name, lock })) { - rmSync(skillPath, { recursive: true, force: true }); - deleted.push(skillPath); - } - } - } - - return deleted; -} - -/** - * Remove deprecated entries from skills-lock.json. - * Only removes entries whose source is "pbakaus/impeccable". - * Returns the list of removed skill names. - */ -export function cleanSkillsLock(projectRoot) { - const lockPath = join(projectRoot, 'skills-lock.json'); - if (!existsSync(lockPath)) return []; - - let lock; - try { - lock = JSON.parse(readFileSync(lockPath, 'utf-8')); - } catch { - return []; - } - - if (!lock.skills || typeof lock.skills !== 'object') return []; - - const targets = buildTargetNames(); - const removed = []; - - for (const name of targets) { - const entry = lock.skills[name]; - if (!entry) continue; - // Only remove if it belongs to impeccable - if (entry.source === 'pbakaus/impeccable') { - delete lock.skills[name]; - removed.push(name); - } - } - - if (removed.length > 0) { - writeFileSync(lockPath, JSON.stringify(lock, null, 2) + '\n', 'utf-8'); - } - - return removed; -} - -/** - * Run the full cleanup. Returns a summary object. - * - * Order matters: read the lock and delete directories first, then - * strip lock entries. Otherwise the authoritative signal is gone by - * the time directory deletion runs. - */ -export function cleanup(projectRoot) { - const root = projectRoot || findProjectRoot(); - const lock = loadLock(root); - const deletedPaths = removeDeprecatedSkills(root, lock); - const removedLockEntries = cleanSkillsLock(root); - return { deletedPaths, removedLockEntries, projectRoot: root }; -} - -// CLI entry point -if (process.argv[1] && resolve(process.argv[1]) === resolve(new URL(import.meta.url).pathname)) { - const result = cleanup(); - if (result.deletedPaths.length === 0 && result.removedLockEntries.length === 0) { - console.log('No deprecated Impeccable skills found. Nothing to clean up.'); - } else { - if (result.deletedPaths.length > 0) { - console.log(`Removed ${result.deletedPaths.length} deprecated skill(s):`); - for (const p of result.deletedPaths) console.log(` - ${p}`); - } - if (result.removedLockEntries.length > 0) { - console.log(`Cleaned ${result.removedLockEntries.length} entry/entries from skills-lock.json:`); - for (const name of result.removedLockEntries) console.log(` - ${name}`); - } - } -} diff --git a/.github/skills/impeccable/scripts/cleanup-deprecated.mjs b/.github/skills/impeccable/scripts/cleanup-deprecated.mjs deleted file mode 100644 index bc6400e90..000000000 --- a/.github/skills/impeccable/scripts/cleanup-deprecated.mjs +++ /dev/null @@ -1,284 +0,0 @@ -#!/usr/bin/env node -/** - * Cleans up deprecated Impeccable skill files, symlinks, and - * skills-lock.json entries left over from previous versions. - * - * Safe to run repeatedly -- it is a no-op when nothing needs cleaning. - * - * Usage (from the project root): - * node {{scripts_path}}/cleanup-deprecated.mjs - * - * What it does: - * 1. Finds every harness-specific skills directory (.claude/skills, - * .cursor/skills, .agents/skills, etc.). - * 2. For each deprecated skill name (with and without i- prefix), - * checks if the directory exists and its SKILL.md mentions - * "impeccable" (to avoid deleting unrelated user skills). - * 3. Deletes confirmed matches (files, directories, or symlinks). - * 4. Removes the corresponding entries from skills-lock.json. - */ - -import { existsSync, readFileSync, writeFileSync, rmSync, readdirSync, statSync, lstatSync, unlinkSync } from 'node:fs'; -import { join, resolve } from 'node:path'; - -// Skills that were renamed, merged, or folded in v2.0, v2.1, and v3.0. -const DEPRECATED_NAMES = [ - // v2.0 renames - 'frontend-design', // renamed to impeccable - 'teach-impeccable', // folded into /impeccable init - // v2.1 merges - 'arrange', // renamed to layout - 'normalize', // merged into polish - 'onboard', // merged into harden - 'extract', // merged into /impeccable extract - // v3.0 consolidation: all standalone skills -> /impeccable sub-commands - 'adapt', - 'animate', - 'audit', - 'bolder', - 'clarify', - 'colorize', - 'critique', - 'delight', - 'distill', - 'harden', - 'layout', - 'optimize', - 'overdrive', - 'polish', - 'quieter', - 'shape', - 'typeset', -]; - -// All known harness directories that may contain a skills/ subfolder. -const HARNESS_DIRS = [ - '.claude', '.cursor', '.gemini', '.codex', '.agents', - '.trae', '.trae-cn', '.pi', '.opencode', '.kiro', '.rovodev', -]; - -// Per-skill fingerprints for SKILL.md bodies that never mentioned -// "impeccable" in their v2.x source. Used as a last-resort match -// when no skills-lock.json exists and the word heuristic fails. -// The strings are lifted verbatim from the v2.x frontmatter -// descriptions, so collisions with hand-written user skills are -// vanishingly unlikely. -const SKILL_FINGERPRINTS = { - harden: 'Make interfaces production-ready: error handling, empty states', - optimize: 'Diagnoses and fixes UI performance across loading speed', -}; - -/** - * Walk up from startDir until we find a directory that looks like a - * project root (has package.json, .git, or skills-lock.json). - */ -export function findProjectRoot(startDir = process.cwd()) { - let dir = resolve(startDir); - const { root } = { root: '/' }; - while (dir !== root) { - if ( - existsSync(join(dir, 'package.json')) || - existsSync(join(dir, '.git')) || - existsSync(join(dir, 'skills-lock.json')) - ) { - return dir; - } - const parent = resolve(dir, '..'); - if (parent === dir) break; - dir = parent; - } - return resolve(startDir); -} - -/** - * Load skills-lock.json from the project root, or null if missing/unreadable. - */ -export function loadLock(projectRoot) { - const lockPath = join(projectRoot, 'skills-lock.json'); - if (!existsSync(lockPath)) return null; - try { - return JSON.parse(readFileSync(lockPath, 'utf-8')); - } catch { - return null; - } -} - -/** - * Check whether a skill directory belongs to Impeccable. Three layered - * signals, in order of reliability: - * 1. Lock source equals "pbakaus/impeccable" (authoritative). - * 2. SKILL.md body contains the word "impeccable". - * 3. SKILL.md body contains a per-skill fingerprint (for harden and - * optimize, whose v2.x SKILL.md never mentioned the pack name). - */ -export function isImpeccableSkill(skillDir, { skillName, lock } = {}) { - // 1. Authoritative: the lock file claims this skill is ours. - if (skillName && lock?.skills?.[skillName]?.source === 'pbakaus/impeccable') { - return true; - } - const skillMd = join(skillDir, 'SKILL.md'); - if (!existsSync(skillMd)) return false; - let content; - try { - content = readFileSync(skillMd, 'utf-8'); - } catch { - return false; - } - // 2. Word-level content heuristic. - if (/impeccable/i.test(content)) return true; - // 3. Per-skill fingerprint for old skills that never mentioned the pack. - // Strip the i- prefix so both `harden` and `i-harden` resolve to the - // same fingerprint entry. - const unprefixed = skillName?.startsWith('i-') ? skillName.slice(2) : skillName; - const fingerprint = unprefixed && SKILL_FINGERPRINTS[unprefixed]; - if (fingerprint && content.includes(fingerprint)) return true; - return false; -} - -/** - * Build the full list of names to check: each deprecated name, plus - * its i-prefixed variant. - */ -export function buildTargetNames() { - const names = []; - for (const name of DEPRECATED_NAMES) { - names.push(name); - names.push(`i-${name}`); - } - return names; -} - -/** - * Find every skills directory across all harness dirs in the project. - * Returns absolute paths that exist on disk. - */ -export function findSkillsDirs(projectRoot) { - const dirs = []; - for (const harness of HARNESS_DIRS) { - const candidate = join(projectRoot, harness, 'skills'); - if (existsSync(candidate)) { - dirs.push(candidate); - } - } - return dirs; -} - -/** - * Remove deprecated skill directories/symlinks from all harness dirs. - * Reads skills-lock.json so the authoritative "source" field can - * drive deletion even when SKILL.md never mentions impeccable. - * Returns an array of paths that were deleted. - */ -export function removeDeprecatedSkills(projectRoot, lock) { - if (lock === undefined) lock = loadLock(projectRoot); - const targets = buildTargetNames(); - const skillsDirs = findSkillsDirs(projectRoot); - const deleted = []; - - for (const skillsDir of skillsDirs) { - for (const name of targets) { - const skillPath = join(skillsDir, name); - - // Use lstat to detect symlinks (existsSync follows symlinks and - // returns false for dangling ones). - let stat; - try { - stat = lstatSync(skillPath); - } catch { - continue; // does not exist at all - } - - if (stat.isSymbolicLink()) { - // Symlink: check the target if it's alive, otherwise treat - // dangling symlinks to deprecated names as safe to remove. - const targetAlive = existsSync(skillPath); - const isMatch = targetAlive - ? isImpeccableSkill(skillPath, { skillName: name, lock }) - : true; - if (isMatch) { - unlinkSync(skillPath); - deleted.push(skillPath); - } - continue; - } - - // Regular directory -- verify it belongs to impeccable - if (isImpeccableSkill(skillPath, { skillName: name, lock })) { - rmSync(skillPath, { recursive: true, force: true }); - deleted.push(skillPath); - } - } - } - - return deleted; -} - -/** - * Remove deprecated entries from skills-lock.json. - * Only removes entries whose source is "pbakaus/impeccable". - * Returns the list of removed skill names. - */ -export function cleanSkillsLock(projectRoot) { - const lockPath = join(projectRoot, 'skills-lock.json'); - if (!existsSync(lockPath)) return []; - - let lock; - try { - lock = JSON.parse(readFileSync(lockPath, 'utf-8')); - } catch { - return []; - } - - if (!lock.skills || typeof lock.skills !== 'object') return []; - - const targets = buildTargetNames(); - const removed = []; - - for (const name of targets) { - const entry = lock.skills[name]; - if (!entry) continue; - // Only remove if it belongs to impeccable - if (entry.source === 'pbakaus/impeccable') { - delete lock.skills[name]; - removed.push(name); - } - } - - if (removed.length > 0) { - writeFileSync(lockPath, JSON.stringify(lock, null, 2) + '\n', 'utf-8'); - } - - return removed; -} - -/** - * Run the full cleanup. Returns a summary object. - * - * Order matters: read the lock and delete directories first, then - * strip lock entries. Otherwise the authoritative signal is gone by - * the time directory deletion runs. - */ -export function cleanup(projectRoot) { - const root = projectRoot || findProjectRoot(); - const lock = loadLock(root); - const deletedPaths = removeDeprecatedSkills(root, lock); - const removedLockEntries = cleanSkillsLock(root); - return { deletedPaths, removedLockEntries, projectRoot: root }; -} - -// CLI entry point -if (process.argv[1] && resolve(process.argv[1]) === resolve(new URL(import.meta.url).pathname)) { - const result = cleanup(); - if (result.deletedPaths.length === 0 && result.removedLockEntries.length === 0) { - console.log('No deprecated Impeccable skills found. Nothing to clean up.'); - } else { - if (result.deletedPaths.length > 0) { - console.log(`Removed ${result.deletedPaths.length} deprecated skill(s):`); - for (const p of result.deletedPaths) console.log(` - ${p}`); - } - if (result.removedLockEntries.length > 0) { - console.log(`Cleaned ${result.removedLockEntries.length} entry/entries from skills-lock.json:`); - for (const name of result.removedLockEntries) console.log(` - ${name}`); - } - } -} diff --git a/.kiro/skills/impeccable/scripts/cleanup-deprecated.mjs b/.kiro/skills/impeccable/scripts/cleanup-deprecated.mjs deleted file mode 100644 index bc6400e90..000000000 --- a/.kiro/skills/impeccable/scripts/cleanup-deprecated.mjs +++ /dev/null @@ -1,284 +0,0 @@ -#!/usr/bin/env node -/** - * Cleans up deprecated Impeccable skill files, symlinks, and - * skills-lock.json entries left over from previous versions. - * - * Safe to run repeatedly -- it is a no-op when nothing needs cleaning. - * - * Usage (from the project root): - * node {{scripts_path}}/cleanup-deprecated.mjs - * - * What it does: - * 1. Finds every harness-specific skills directory (.claude/skills, - * .cursor/skills, .agents/skills, etc.). - * 2. For each deprecated skill name (with and without i- prefix), - * checks if the directory exists and its SKILL.md mentions - * "impeccable" (to avoid deleting unrelated user skills). - * 3. Deletes confirmed matches (files, directories, or symlinks). - * 4. Removes the corresponding entries from skills-lock.json. - */ - -import { existsSync, readFileSync, writeFileSync, rmSync, readdirSync, statSync, lstatSync, unlinkSync } from 'node:fs'; -import { join, resolve } from 'node:path'; - -// Skills that were renamed, merged, or folded in v2.0, v2.1, and v3.0. -const DEPRECATED_NAMES = [ - // v2.0 renames - 'frontend-design', // renamed to impeccable - 'teach-impeccable', // folded into /impeccable init - // v2.1 merges - 'arrange', // renamed to layout - 'normalize', // merged into polish - 'onboard', // merged into harden - 'extract', // merged into /impeccable extract - // v3.0 consolidation: all standalone skills -> /impeccable sub-commands - 'adapt', - 'animate', - 'audit', - 'bolder', - 'clarify', - 'colorize', - 'critique', - 'delight', - 'distill', - 'harden', - 'layout', - 'optimize', - 'overdrive', - 'polish', - 'quieter', - 'shape', - 'typeset', -]; - -// All known harness directories that may contain a skills/ subfolder. -const HARNESS_DIRS = [ - '.claude', '.cursor', '.gemini', '.codex', '.agents', - '.trae', '.trae-cn', '.pi', '.opencode', '.kiro', '.rovodev', -]; - -// Per-skill fingerprints for SKILL.md bodies that never mentioned -// "impeccable" in their v2.x source. Used as a last-resort match -// when no skills-lock.json exists and the word heuristic fails. -// The strings are lifted verbatim from the v2.x frontmatter -// descriptions, so collisions with hand-written user skills are -// vanishingly unlikely. -const SKILL_FINGERPRINTS = { - harden: 'Make interfaces production-ready: error handling, empty states', - optimize: 'Diagnoses and fixes UI performance across loading speed', -}; - -/** - * Walk up from startDir until we find a directory that looks like a - * project root (has package.json, .git, or skills-lock.json). - */ -export function findProjectRoot(startDir = process.cwd()) { - let dir = resolve(startDir); - const { root } = { root: '/' }; - while (dir !== root) { - if ( - existsSync(join(dir, 'package.json')) || - existsSync(join(dir, '.git')) || - existsSync(join(dir, 'skills-lock.json')) - ) { - return dir; - } - const parent = resolve(dir, '..'); - if (parent === dir) break; - dir = parent; - } - return resolve(startDir); -} - -/** - * Load skills-lock.json from the project root, or null if missing/unreadable. - */ -export function loadLock(projectRoot) { - const lockPath = join(projectRoot, 'skills-lock.json'); - if (!existsSync(lockPath)) return null; - try { - return JSON.parse(readFileSync(lockPath, 'utf-8')); - } catch { - return null; - } -} - -/** - * Check whether a skill directory belongs to Impeccable. Three layered - * signals, in order of reliability: - * 1. Lock source equals "pbakaus/impeccable" (authoritative). - * 2. SKILL.md body contains the word "impeccable". - * 3. SKILL.md body contains a per-skill fingerprint (for harden and - * optimize, whose v2.x SKILL.md never mentioned the pack name). - */ -export function isImpeccableSkill(skillDir, { skillName, lock } = {}) { - // 1. Authoritative: the lock file claims this skill is ours. - if (skillName && lock?.skills?.[skillName]?.source === 'pbakaus/impeccable') { - return true; - } - const skillMd = join(skillDir, 'SKILL.md'); - if (!existsSync(skillMd)) return false; - let content; - try { - content = readFileSync(skillMd, 'utf-8'); - } catch { - return false; - } - // 2. Word-level content heuristic. - if (/impeccable/i.test(content)) return true; - // 3. Per-skill fingerprint for old skills that never mentioned the pack. - // Strip the i- prefix so both `harden` and `i-harden` resolve to the - // same fingerprint entry. - const unprefixed = skillName?.startsWith('i-') ? skillName.slice(2) : skillName; - const fingerprint = unprefixed && SKILL_FINGERPRINTS[unprefixed]; - if (fingerprint && content.includes(fingerprint)) return true; - return false; -} - -/** - * Build the full list of names to check: each deprecated name, plus - * its i-prefixed variant. - */ -export function buildTargetNames() { - const names = []; - for (const name of DEPRECATED_NAMES) { - names.push(name); - names.push(`i-${name}`); - } - return names; -} - -/** - * Find every skills directory across all harness dirs in the project. - * Returns absolute paths that exist on disk. - */ -export function findSkillsDirs(projectRoot) { - const dirs = []; - for (const harness of HARNESS_DIRS) { - const candidate = join(projectRoot, harness, 'skills'); - if (existsSync(candidate)) { - dirs.push(candidate); - } - } - return dirs; -} - -/** - * Remove deprecated skill directories/symlinks from all harness dirs. - * Reads skills-lock.json so the authoritative "source" field can - * drive deletion even when SKILL.md never mentions impeccable. - * Returns an array of paths that were deleted. - */ -export function removeDeprecatedSkills(projectRoot, lock) { - if (lock === undefined) lock = loadLock(projectRoot); - const targets = buildTargetNames(); - const skillsDirs = findSkillsDirs(projectRoot); - const deleted = []; - - for (const skillsDir of skillsDirs) { - for (const name of targets) { - const skillPath = join(skillsDir, name); - - // Use lstat to detect symlinks (existsSync follows symlinks and - // returns false for dangling ones). - let stat; - try { - stat = lstatSync(skillPath); - } catch { - continue; // does not exist at all - } - - if (stat.isSymbolicLink()) { - // Symlink: check the target if it's alive, otherwise treat - // dangling symlinks to deprecated names as safe to remove. - const targetAlive = existsSync(skillPath); - const isMatch = targetAlive - ? isImpeccableSkill(skillPath, { skillName: name, lock }) - : true; - if (isMatch) { - unlinkSync(skillPath); - deleted.push(skillPath); - } - continue; - } - - // Regular directory -- verify it belongs to impeccable - if (isImpeccableSkill(skillPath, { skillName: name, lock })) { - rmSync(skillPath, { recursive: true, force: true }); - deleted.push(skillPath); - } - } - } - - return deleted; -} - -/** - * Remove deprecated entries from skills-lock.json. - * Only removes entries whose source is "pbakaus/impeccable". - * Returns the list of removed skill names. - */ -export function cleanSkillsLock(projectRoot) { - const lockPath = join(projectRoot, 'skills-lock.json'); - if (!existsSync(lockPath)) return []; - - let lock; - try { - lock = JSON.parse(readFileSync(lockPath, 'utf-8')); - } catch { - return []; - } - - if (!lock.skills || typeof lock.skills !== 'object') return []; - - const targets = buildTargetNames(); - const removed = []; - - for (const name of targets) { - const entry = lock.skills[name]; - if (!entry) continue; - // Only remove if it belongs to impeccable - if (entry.source === 'pbakaus/impeccable') { - delete lock.skills[name]; - removed.push(name); - } - } - - if (removed.length > 0) { - writeFileSync(lockPath, JSON.stringify(lock, null, 2) + '\n', 'utf-8'); - } - - return removed; -} - -/** - * Run the full cleanup. Returns a summary object. - * - * Order matters: read the lock and delete directories first, then - * strip lock entries. Otherwise the authoritative signal is gone by - * the time directory deletion runs. - */ -export function cleanup(projectRoot) { - const root = projectRoot || findProjectRoot(); - const lock = loadLock(root); - const deletedPaths = removeDeprecatedSkills(root, lock); - const removedLockEntries = cleanSkillsLock(root); - return { deletedPaths, removedLockEntries, projectRoot: root }; -} - -// CLI entry point -if (process.argv[1] && resolve(process.argv[1]) === resolve(new URL(import.meta.url).pathname)) { - const result = cleanup(); - if (result.deletedPaths.length === 0 && result.removedLockEntries.length === 0) { - console.log('No deprecated Impeccable skills found. Nothing to clean up.'); - } else { - if (result.deletedPaths.length > 0) { - console.log(`Removed ${result.deletedPaths.length} deprecated skill(s):`); - for (const p of result.deletedPaths) console.log(` - ${p}`); - } - if (result.removedLockEntries.length > 0) { - console.log(`Cleaned ${result.removedLockEntries.length} entry/entries from skills-lock.json:`); - for (const name of result.removedLockEntries) console.log(` - ${name}`); - } - } -} diff --git a/.opencode/skills/impeccable/scripts/cleanup-deprecated.mjs b/.opencode/skills/impeccable/scripts/cleanup-deprecated.mjs deleted file mode 100644 index bc6400e90..000000000 --- a/.opencode/skills/impeccable/scripts/cleanup-deprecated.mjs +++ /dev/null @@ -1,284 +0,0 @@ -#!/usr/bin/env node -/** - * Cleans up deprecated Impeccable skill files, symlinks, and - * skills-lock.json entries left over from previous versions. - * - * Safe to run repeatedly -- it is a no-op when nothing needs cleaning. - * - * Usage (from the project root): - * node {{scripts_path}}/cleanup-deprecated.mjs - * - * What it does: - * 1. Finds every harness-specific skills directory (.claude/skills, - * .cursor/skills, .agents/skills, etc.). - * 2. For each deprecated skill name (with and without i- prefix), - * checks if the directory exists and its SKILL.md mentions - * "impeccable" (to avoid deleting unrelated user skills). - * 3. Deletes confirmed matches (files, directories, or symlinks). - * 4. Removes the corresponding entries from skills-lock.json. - */ - -import { existsSync, readFileSync, writeFileSync, rmSync, readdirSync, statSync, lstatSync, unlinkSync } from 'node:fs'; -import { join, resolve } from 'node:path'; - -// Skills that were renamed, merged, or folded in v2.0, v2.1, and v3.0. -const DEPRECATED_NAMES = [ - // v2.0 renames - 'frontend-design', // renamed to impeccable - 'teach-impeccable', // folded into /impeccable init - // v2.1 merges - 'arrange', // renamed to layout - 'normalize', // merged into polish - 'onboard', // merged into harden - 'extract', // merged into /impeccable extract - // v3.0 consolidation: all standalone skills -> /impeccable sub-commands - 'adapt', - 'animate', - 'audit', - 'bolder', - 'clarify', - 'colorize', - 'critique', - 'delight', - 'distill', - 'harden', - 'layout', - 'optimize', - 'overdrive', - 'polish', - 'quieter', - 'shape', - 'typeset', -]; - -// All known harness directories that may contain a skills/ subfolder. -const HARNESS_DIRS = [ - '.claude', '.cursor', '.gemini', '.codex', '.agents', - '.trae', '.trae-cn', '.pi', '.opencode', '.kiro', '.rovodev', -]; - -// Per-skill fingerprints for SKILL.md bodies that never mentioned -// "impeccable" in their v2.x source. Used as a last-resort match -// when no skills-lock.json exists and the word heuristic fails. -// The strings are lifted verbatim from the v2.x frontmatter -// descriptions, so collisions with hand-written user skills are -// vanishingly unlikely. -const SKILL_FINGERPRINTS = { - harden: 'Make interfaces production-ready: error handling, empty states', - optimize: 'Diagnoses and fixes UI performance across loading speed', -}; - -/** - * Walk up from startDir until we find a directory that looks like a - * project root (has package.json, .git, or skills-lock.json). - */ -export function findProjectRoot(startDir = process.cwd()) { - let dir = resolve(startDir); - const { root } = { root: '/' }; - while (dir !== root) { - if ( - existsSync(join(dir, 'package.json')) || - existsSync(join(dir, '.git')) || - existsSync(join(dir, 'skills-lock.json')) - ) { - return dir; - } - const parent = resolve(dir, '..'); - if (parent === dir) break; - dir = parent; - } - return resolve(startDir); -} - -/** - * Load skills-lock.json from the project root, or null if missing/unreadable. - */ -export function loadLock(projectRoot) { - const lockPath = join(projectRoot, 'skills-lock.json'); - if (!existsSync(lockPath)) return null; - try { - return JSON.parse(readFileSync(lockPath, 'utf-8')); - } catch { - return null; - } -} - -/** - * Check whether a skill directory belongs to Impeccable. Three layered - * signals, in order of reliability: - * 1. Lock source equals "pbakaus/impeccable" (authoritative). - * 2. SKILL.md body contains the word "impeccable". - * 3. SKILL.md body contains a per-skill fingerprint (for harden and - * optimize, whose v2.x SKILL.md never mentioned the pack name). - */ -export function isImpeccableSkill(skillDir, { skillName, lock } = {}) { - // 1. Authoritative: the lock file claims this skill is ours. - if (skillName && lock?.skills?.[skillName]?.source === 'pbakaus/impeccable') { - return true; - } - const skillMd = join(skillDir, 'SKILL.md'); - if (!existsSync(skillMd)) return false; - let content; - try { - content = readFileSync(skillMd, 'utf-8'); - } catch { - return false; - } - // 2. Word-level content heuristic. - if (/impeccable/i.test(content)) return true; - // 3. Per-skill fingerprint for old skills that never mentioned the pack. - // Strip the i- prefix so both `harden` and `i-harden` resolve to the - // same fingerprint entry. - const unprefixed = skillName?.startsWith('i-') ? skillName.slice(2) : skillName; - const fingerprint = unprefixed && SKILL_FINGERPRINTS[unprefixed]; - if (fingerprint && content.includes(fingerprint)) return true; - return false; -} - -/** - * Build the full list of names to check: each deprecated name, plus - * its i-prefixed variant. - */ -export function buildTargetNames() { - const names = []; - for (const name of DEPRECATED_NAMES) { - names.push(name); - names.push(`i-${name}`); - } - return names; -} - -/** - * Find every skills directory across all harness dirs in the project. - * Returns absolute paths that exist on disk. - */ -export function findSkillsDirs(projectRoot) { - const dirs = []; - for (const harness of HARNESS_DIRS) { - const candidate = join(projectRoot, harness, 'skills'); - if (existsSync(candidate)) { - dirs.push(candidate); - } - } - return dirs; -} - -/** - * Remove deprecated skill directories/symlinks from all harness dirs. - * Reads skills-lock.json so the authoritative "source" field can - * drive deletion even when SKILL.md never mentions impeccable. - * Returns an array of paths that were deleted. - */ -export function removeDeprecatedSkills(projectRoot, lock) { - if (lock === undefined) lock = loadLock(projectRoot); - const targets = buildTargetNames(); - const skillsDirs = findSkillsDirs(projectRoot); - const deleted = []; - - for (const skillsDir of skillsDirs) { - for (const name of targets) { - const skillPath = join(skillsDir, name); - - // Use lstat to detect symlinks (existsSync follows symlinks and - // returns false for dangling ones). - let stat; - try { - stat = lstatSync(skillPath); - } catch { - continue; // does not exist at all - } - - if (stat.isSymbolicLink()) { - // Symlink: check the target if it's alive, otherwise treat - // dangling symlinks to deprecated names as safe to remove. - const targetAlive = existsSync(skillPath); - const isMatch = targetAlive - ? isImpeccableSkill(skillPath, { skillName: name, lock }) - : true; - if (isMatch) { - unlinkSync(skillPath); - deleted.push(skillPath); - } - continue; - } - - // Regular directory -- verify it belongs to impeccable - if (isImpeccableSkill(skillPath, { skillName: name, lock })) { - rmSync(skillPath, { recursive: true, force: true }); - deleted.push(skillPath); - } - } - } - - return deleted; -} - -/** - * Remove deprecated entries from skills-lock.json. - * Only removes entries whose source is "pbakaus/impeccable". - * Returns the list of removed skill names. - */ -export function cleanSkillsLock(projectRoot) { - const lockPath = join(projectRoot, 'skills-lock.json'); - if (!existsSync(lockPath)) return []; - - let lock; - try { - lock = JSON.parse(readFileSync(lockPath, 'utf-8')); - } catch { - return []; - } - - if (!lock.skills || typeof lock.skills !== 'object') return []; - - const targets = buildTargetNames(); - const removed = []; - - for (const name of targets) { - const entry = lock.skills[name]; - if (!entry) continue; - // Only remove if it belongs to impeccable - if (entry.source === 'pbakaus/impeccable') { - delete lock.skills[name]; - removed.push(name); - } - } - - if (removed.length > 0) { - writeFileSync(lockPath, JSON.stringify(lock, null, 2) + '\n', 'utf-8'); - } - - return removed; -} - -/** - * Run the full cleanup. Returns a summary object. - * - * Order matters: read the lock and delete directories first, then - * strip lock entries. Otherwise the authoritative signal is gone by - * the time directory deletion runs. - */ -export function cleanup(projectRoot) { - const root = projectRoot || findProjectRoot(); - const lock = loadLock(root); - const deletedPaths = removeDeprecatedSkills(root, lock); - const removedLockEntries = cleanSkillsLock(root); - return { deletedPaths, removedLockEntries, projectRoot: root }; -} - -// CLI entry point -if (process.argv[1] && resolve(process.argv[1]) === resolve(new URL(import.meta.url).pathname)) { - const result = cleanup(); - if (result.deletedPaths.length === 0 && result.removedLockEntries.length === 0) { - console.log('No deprecated Impeccable skills found. Nothing to clean up.'); - } else { - if (result.deletedPaths.length > 0) { - console.log(`Removed ${result.deletedPaths.length} deprecated skill(s):`); - for (const p of result.deletedPaths) console.log(` - ${p}`); - } - if (result.removedLockEntries.length > 0) { - console.log(`Cleaned ${result.removedLockEntries.length} entry/entries from skills-lock.json:`); - for (const name of result.removedLockEntries) console.log(` - ${name}`); - } - } -} diff --git a/.pi/skills/impeccable/scripts/cleanup-deprecated.mjs b/.pi/skills/impeccable/scripts/cleanup-deprecated.mjs deleted file mode 100644 index bc6400e90..000000000 --- a/.pi/skills/impeccable/scripts/cleanup-deprecated.mjs +++ /dev/null @@ -1,284 +0,0 @@ -#!/usr/bin/env node -/** - * Cleans up deprecated Impeccable skill files, symlinks, and - * skills-lock.json entries left over from previous versions. - * - * Safe to run repeatedly -- it is a no-op when nothing needs cleaning. - * - * Usage (from the project root): - * node {{scripts_path}}/cleanup-deprecated.mjs - * - * What it does: - * 1. Finds every harness-specific skills directory (.claude/skills, - * .cursor/skills, .agents/skills, etc.). - * 2. For each deprecated skill name (with and without i- prefix), - * checks if the directory exists and its SKILL.md mentions - * "impeccable" (to avoid deleting unrelated user skills). - * 3. Deletes confirmed matches (files, directories, or symlinks). - * 4. Removes the corresponding entries from skills-lock.json. - */ - -import { existsSync, readFileSync, writeFileSync, rmSync, readdirSync, statSync, lstatSync, unlinkSync } from 'node:fs'; -import { join, resolve } from 'node:path'; - -// Skills that were renamed, merged, or folded in v2.0, v2.1, and v3.0. -const DEPRECATED_NAMES = [ - // v2.0 renames - 'frontend-design', // renamed to impeccable - 'teach-impeccable', // folded into /impeccable init - // v2.1 merges - 'arrange', // renamed to layout - 'normalize', // merged into polish - 'onboard', // merged into harden - 'extract', // merged into /impeccable extract - // v3.0 consolidation: all standalone skills -> /impeccable sub-commands - 'adapt', - 'animate', - 'audit', - 'bolder', - 'clarify', - 'colorize', - 'critique', - 'delight', - 'distill', - 'harden', - 'layout', - 'optimize', - 'overdrive', - 'polish', - 'quieter', - 'shape', - 'typeset', -]; - -// All known harness directories that may contain a skills/ subfolder. -const HARNESS_DIRS = [ - '.claude', '.cursor', '.gemini', '.codex', '.agents', - '.trae', '.trae-cn', '.pi', '.opencode', '.kiro', '.rovodev', -]; - -// Per-skill fingerprints for SKILL.md bodies that never mentioned -// "impeccable" in their v2.x source. Used as a last-resort match -// when no skills-lock.json exists and the word heuristic fails. -// The strings are lifted verbatim from the v2.x frontmatter -// descriptions, so collisions with hand-written user skills are -// vanishingly unlikely. -const SKILL_FINGERPRINTS = { - harden: 'Make interfaces production-ready: error handling, empty states', - optimize: 'Diagnoses and fixes UI performance across loading speed', -}; - -/** - * Walk up from startDir until we find a directory that looks like a - * project root (has package.json, .git, or skills-lock.json). - */ -export function findProjectRoot(startDir = process.cwd()) { - let dir = resolve(startDir); - const { root } = { root: '/' }; - while (dir !== root) { - if ( - existsSync(join(dir, 'package.json')) || - existsSync(join(dir, '.git')) || - existsSync(join(dir, 'skills-lock.json')) - ) { - return dir; - } - const parent = resolve(dir, '..'); - if (parent === dir) break; - dir = parent; - } - return resolve(startDir); -} - -/** - * Load skills-lock.json from the project root, or null if missing/unreadable. - */ -export function loadLock(projectRoot) { - const lockPath = join(projectRoot, 'skills-lock.json'); - if (!existsSync(lockPath)) return null; - try { - return JSON.parse(readFileSync(lockPath, 'utf-8')); - } catch { - return null; - } -} - -/** - * Check whether a skill directory belongs to Impeccable. Three layered - * signals, in order of reliability: - * 1. Lock source equals "pbakaus/impeccable" (authoritative). - * 2. SKILL.md body contains the word "impeccable". - * 3. SKILL.md body contains a per-skill fingerprint (for harden and - * optimize, whose v2.x SKILL.md never mentioned the pack name). - */ -export function isImpeccableSkill(skillDir, { skillName, lock } = {}) { - // 1. Authoritative: the lock file claims this skill is ours. - if (skillName && lock?.skills?.[skillName]?.source === 'pbakaus/impeccable') { - return true; - } - const skillMd = join(skillDir, 'SKILL.md'); - if (!existsSync(skillMd)) return false; - let content; - try { - content = readFileSync(skillMd, 'utf-8'); - } catch { - return false; - } - // 2. Word-level content heuristic. - if (/impeccable/i.test(content)) return true; - // 3. Per-skill fingerprint for old skills that never mentioned the pack. - // Strip the i- prefix so both `harden` and `i-harden` resolve to the - // same fingerprint entry. - const unprefixed = skillName?.startsWith('i-') ? skillName.slice(2) : skillName; - const fingerprint = unprefixed && SKILL_FINGERPRINTS[unprefixed]; - if (fingerprint && content.includes(fingerprint)) return true; - return false; -} - -/** - * Build the full list of names to check: each deprecated name, plus - * its i-prefixed variant. - */ -export function buildTargetNames() { - const names = []; - for (const name of DEPRECATED_NAMES) { - names.push(name); - names.push(`i-${name}`); - } - return names; -} - -/** - * Find every skills directory across all harness dirs in the project. - * Returns absolute paths that exist on disk. - */ -export function findSkillsDirs(projectRoot) { - const dirs = []; - for (const harness of HARNESS_DIRS) { - const candidate = join(projectRoot, harness, 'skills'); - if (existsSync(candidate)) { - dirs.push(candidate); - } - } - return dirs; -} - -/** - * Remove deprecated skill directories/symlinks from all harness dirs. - * Reads skills-lock.json so the authoritative "source" field can - * drive deletion even when SKILL.md never mentions impeccable. - * Returns an array of paths that were deleted. - */ -export function removeDeprecatedSkills(projectRoot, lock) { - if (lock === undefined) lock = loadLock(projectRoot); - const targets = buildTargetNames(); - const skillsDirs = findSkillsDirs(projectRoot); - const deleted = []; - - for (const skillsDir of skillsDirs) { - for (const name of targets) { - const skillPath = join(skillsDir, name); - - // Use lstat to detect symlinks (existsSync follows symlinks and - // returns false for dangling ones). - let stat; - try { - stat = lstatSync(skillPath); - } catch { - continue; // does not exist at all - } - - if (stat.isSymbolicLink()) { - // Symlink: check the target if it's alive, otherwise treat - // dangling symlinks to deprecated names as safe to remove. - const targetAlive = existsSync(skillPath); - const isMatch = targetAlive - ? isImpeccableSkill(skillPath, { skillName: name, lock }) - : true; - if (isMatch) { - unlinkSync(skillPath); - deleted.push(skillPath); - } - continue; - } - - // Regular directory -- verify it belongs to impeccable - if (isImpeccableSkill(skillPath, { skillName: name, lock })) { - rmSync(skillPath, { recursive: true, force: true }); - deleted.push(skillPath); - } - } - } - - return deleted; -} - -/** - * Remove deprecated entries from skills-lock.json. - * Only removes entries whose source is "pbakaus/impeccable". - * Returns the list of removed skill names. - */ -export function cleanSkillsLock(projectRoot) { - const lockPath = join(projectRoot, 'skills-lock.json'); - if (!existsSync(lockPath)) return []; - - let lock; - try { - lock = JSON.parse(readFileSync(lockPath, 'utf-8')); - } catch { - return []; - } - - if (!lock.skills || typeof lock.skills !== 'object') return []; - - const targets = buildTargetNames(); - const removed = []; - - for (const name of targets) { - const entry = lock.skills[name]; - if (!entry) continue; - // Only remove if it belongs to impeccable - if (entry.source === 'pbakaus/impeccable') { - delete lock.skills[name]; - removed.push(name); - } - } - - if (removed.length > 0) { - writeFileSync(lockPath, JSON.stringify(lock, null, 2) + '\n', 'utf-8'); - } - - return removed; -} - -/** - * Run the full cleanup. Returns a summary object. - * - * Order matters: read the lock and delete directories first, then - * strip lock entries. Otherwise the authoritative signal is gone by - * the time directory deletion runs. - */ -export function cleanup(projectRoot) { - const root = projectRoot || findProjectRoot(); - const lock = loadLock(root); - const deletedPaths = removeDeprecatedSkills(root, lock); - const removedLockEntries = cleanSkillsLock(root); - return { deletedPaths, removedLockEntries, projectRoot: root }; -} - -// CLI entry point -if (process.argv[1] && resolve(process.argv[1]) === resolve(new URL(import.meta.url).pathname)) { - const result = cleanup(); - if (result.deletedPaths.length === 0 && result.removedLockEntries.length === 0) { - console.log('No deprecated Impeccable skills found. Nothing to clean up.'); - } else { - if (result.deletedPaths.length > 0) { - console.log(`Removed ${result.deletedPaths.length} deprecated skill(s):`); - for (const p of result.deletedPaths) console.log(` - ${p}`); - } - if (result.removedLockEntries.length > 0) { - console.log(`Cleaned ${result.removedLockEntries.length} entry/entries from skills-lock.json:`); - for (const name of result.removedLockEntries) console.log(` - ${name}`); - } - } -} diff --git a/.qoder/skills/impeccable/scripts/cleanup-deprecated.mjs b/.qoder/skills/impeccable/scripts/cleanup-deprecated.mjs deleted file mode 100644 index bc6400e90..000000000 --- a/.qoder/skills/impeccable/scripts/cleanup-deprecated.mjs +++ /dev/null @@ -1,284 +0,0 @@ -#!/usr/bin/env node -/** - * Cleans up deprecated Impeccable skill files, symlinks, and - * skills-lock.json entries left over from previous versions. - * - * Safe to run repeatedly -- it is a no-op when nothing needs cleaning. - * - * Usage (from the project root): - * node {{scripts_path}}/cleanup-deprecated.mjs - * - * What it does: - * 1. Finds every harness-specific skills directory (.claude/skills, - * .cursor/skills, .agents/skills, etc.). - * 2. For each deprecated skill name (with and without i- prefix), - * checks if the directory exists and its SKILL.md mentions - * "impeccable" (to avoid deleting unrelated user skills). - * 3. Deletes confirmed matches (files, directories, or symlinks). - * 4. Removes the corresponding entries from skills-lock.json. - */ - -import { existsSync, readFileSync, writeFileSync, rmSync, readdirSync, statSync, lstatSync, unlinkSync } from 'node:fs'; -import { join, resolve } from 'node:path'; - -// Skills that were renamed, merged, or folded in v2.0, v2.1, and v3.0. -const DEPRECATED_NAMES = [ - // v2.0 renames - 'frontend-design', // renamed to impeccable - 'teach-impeccable', // folded into /impeccable init - // v2.1 merges - 'arrange', // renamed to layout - 'normalize', // merged into polish - 'onboard', // merged into harden - 'extract', // merged into /impeccable extract - // v3.0 consolidation: all standalone skills -> /impeccable sub-commands - 'adapt', - 'animate', - 'audit', - 'bolder', - 'clarify', - 'colorize', - 'critique', - 'delight', - 'distill', - 'harden', - 'layout', - 'optimize', - 'overdrive', - 'polish', - 'quieter', - 'shape', - 'typeset', -]; - -// All known harness directories that may contain a skills/ subfolder. -const HARNESS_DIRS = [ - '.claude', '.cursor', '.gemini', '.codex', '.agents', - '.trae', '.trae-cn', '.pi', '.opencode', '.kiro', '.rovodev', -]; - -// Per-skill fingerprints for SKILL.md bodies that never mentioned -// "impeccable" in their v2.x source. Used as a last-resort match -// when no skills-lock.json exists and the word heuristic fails. -// The strings are lifted verbatim from the v2.x frontmatter -// descriptions, so collisions with hand-written user skills are -// vanishingly unlikely. -const SKILL_FINGERPRINTS = { - harden: 'Make interfaces production-ready: error handling, empty states', - optimize: 'Diagnoses and fixes UI performance across loading speed', -}; - -/** - * Walk up from startDir until we find a directory that looks like a - * project root (has package.json, .git, or skills-lock.json). - */ -export function findProjectRoot(startDir = process.cwd()) { - let dir = resolve(startDir); - const { root } = { root: '/' }; - while (dir !== root) { - if ( - existsSync(join(dir, 'package.json')) || - existsSync(join(dir, '.git')) || - existsSync(join(dir, 'skills-lock.json')) - ) { - return dir; - } - const parent = resolve(dir, '..'); - if (parent === dir) break; - dir = parent; - } - return resolve(startDir); -} - -/** - * Load skills-lock.json from the project root, or null if missing/unreadable. - */ -export function loadLock(projectRoot) { - const lockPath = join(projectRoot, 'skills-lock.json'); - if (!existsSync(lockPath)) return null; - try { - return JSON.parse(readFileSync(lockPath, 'utf-8')); - } catch { - return null; - } -} - -/** - * Check whether a skill directory belongs to Impeccable. Three layered - * signals, in order of reliability: - * 1. Lock source equals "pbakaus/impeccable" (authoritative). - * 2. SKILL.md body contains the word "impeccable". - * 3. SKILL.md body contains a per-skill fingerprint (for harden and - * optimize, whose v2.x SKILL.md never mentioned the pack name). - */ -export function isImpeccableSkill(skillDir, { skillName, lock } = {}) { - // 1. Authoritative: the lock file claims this skill is ours. - if (skillName && lock?.skills?.[skillName]?.source === 'pbakaus/impeccable') { - return true; - } - const skillMd = join(skillDir, 'SKILL.md'); - if (!existsSync(skillMd)) return false; - let content; - try { - content = readFileSync(skillMd, 'utf-8'); - } catch { - return false; - } - // 2. Word-level content heuristic. - if (/impeccable/i.test(content)) return true; - // 3. Per-skill fingerprint for old skills that never mentioned the pack. - // Strip the i- prefix so both `harden` and `i-harden` resolve to the - // same fingerprint entry. - const unprefixed = skillName?.startsWith('i-') ? skillName.slice(2) : skillName; - const fingerprint = unprefixed && SKILL_FINGERPRINTS[unprefixed]; - if (fingerprint && content.includes(fingerprint)) return true; - return false; -} - -/** - * Build the full list of names to check: each deprecated name, plus - * its i-prefixed variant. - */ -export function buildTargetNames() { - const names = []; - for (const name of DEPRECATED_NAMES) { - names.push(name); - names.push(`i-${name}`); - } - return names; -} - -/** - * Find every skills directory across all harness dirs in the project. - * Returns absolute paths that exist on disk. - */ -export function findSkillsDirs(projectRoot) { - const dirs = []; - for (const harness of HARNESS_DIRS) { - const candidate = join(projectRoot, harness, 'skills'); - if (existsSync(candidate)) { - dirs.push(candidate); - } - } - return dirs; -} - -/** - * Remove deprecated skill directories/symlinks from all harness dirs. - * Reads skills-lock.json so the authoritative "source" field can - * drive deletion even when SKILL.md never mentions impeccable. - * Returns an array of paths that were deleted. - */ -export function removeDeprecatedSkills(projectRoot, lock) { - if (lock === undefined) lock = loadLock(projectRoot); - const targets = buildTargetNames(); - const skillsDirs = findSkillsDirs(projectRoot); - const deleted = []; - - for (const skillsDir of skillsDirs) { - for (const name of targets) { - const skillPath = join(skillsDir, name); - - // Use lstat to detect symlinks (existsSync follows symlinks and - // returns false for dangling ones). - let stat; - try { - stat = lstatSync(skillPath); - } catch { - continue; // does not exist at all - } - - if (stat.isSymbolicLink()) { - // Symlink: check the target if it's alive, otherwise treat - // dangling symlinks to deprecated names as safe to remove. - const targetAlive = existsSync(skillPath); - const isMatch = targetAlive - ? isImpeccableSkill(skillPath, { skillName: name, lock }) - : true; - if (isMatch) { - unlinkSync(skillPath); - deleted.push(skillPath); - } - continue; - } - - // Regular directory -- verify it belongs to impeccable - if (isImpeccableSkill(skillPath, { skillName: name, lock })) { - rmSync(skillPath, { recursive: true, force: true }); - deleted.push(skillPath); - } - } - } - - return deleted; -} - -/** - * Remove deprecated entries from skills-lock.json. - * Only removes entries whose source is "pbakaus/impeccable". - * Returns the list of removed skill names. - */ -export function cleanSkillsLock(projectRoot) { - const lockPath = join(projectRoot, 'skills-lock.json'); - if (!existsSync(lockPath)) return []; - - let lock; - try { - lock = JSON.parse(readFileSync(lockPath, 'utf-8')); - } catch { - return []; - } - - if (!lock.skills || typeof lock.skills !== 'object') return []; - - const targets = buildTargetNames(); - const removed = []; - - for (const name of targets) { - const entry = lock.skills[name]; - if (!entry) continue; - // Only remove if it belongs to impeccable - if (entry.source === 'pbakaus/impeccable') { - delete lock.skills[name]; - removed.push(name); - } - } - - if (removed.length > 0) { - writeFileSync(lockPath, JSON.stringify(lock, null, 2) + '\n', 'utf-8'); - } - - return removed; -} - -/** - * Run the full cleanup. Returns a summary object. - * - * Order matters: read the lock and delete directories first, then - * strip lock entries. Otherwise the authoritative signal is gone by - * the time directory deletion runs. - */ -export function cleanup(projectRoot) { - const root = projectRoot || findProjectRoot(); - const lock = loadLock(root); - const deletedPaths = removeDeprecatedSkills(root, lock); - const removedLockEntries = cleanSkillsLock(root); - return { deletedPaths, removedLockEntries, projectRoot: root }; -} - -// CLI entry point -if (process.argv[1] && resolve(process.argv[1]) === resolve(new URL(import.meta.url).pathname)) { - const result = cleanup(); - if (result.deletedPaths.length === 0 && result.removedLockEntries.length === 0) { - console.log('No deprecated Impeccable skills found. Nothing to clean up.'); - } else { - if (result.deletedPaths.length > 0) { - console.log(`Removed ${result.deletedPaths.length} deprecated skill(s):`); - for (const p of result.deletedPaths) console.log(` - ${p}`); - } - if (result.removedLockEntries.length > 0) { - console.log(`Cleaned ${result.removedLockEntries.length} entry/entries from skills-lock.json:`); - for (const name of result.removedLockEntries) console.log(` - ${name}`); - } - } -} diff --git a/.rovodev/skills/impeccable/scripts/cleanup-deprecated.mjs b/.rovodev/skills/impeccable/scripts/cleanup-deprecated.mjs deleted file mode 100644 index bc6400e90..000000000 --- a/.rovodev/skills/impeccable/scripts/cleanup-deprecated.mjs +++ /dev/null @@ -1,284 +0,0 @@ -#!/usr/bin/env node -/** - * Cleans up deprecated Impeccable skill files, symlinks, and - * skills-lock.json entries left over from previous versions. - * - * Safe to run repeatedly -- it is a no-op when nothing needs cleaning. - * - * Usage (from the project root): - * node {{scripts_path}}/cleanup-deprecated.mjs - * - * What it does: - * 1. Finds every harness-specific skills directory (.claude/skills, - * .cursor/skills, .agents/skills, etc.). - * 2. For each deprecated skill name (with and without i- prefix), - * checks if the directory exists and its SKILL.md mentions - * "impeccable" (to avoid deleting unrelated user skills). - * 3. Deletes confirmed matches (files, directories, or symlinks). - * 4. Removes the corresponding entries from skills-lock.json. - */ - -import { existsSync, readFileSync, writeFileSync, rmSync, readdirSync, statSync, lstatSync, unlinkSync } from 'node:fs'; -import { join, resolve } from 'node:path'; - -// Skills that were renamed, merged, or folded in v2.0, v2.1, and v3.0. -const DEPRECATED_NAMES = [ - // v2.0 renames - 'frontend-design', // renamed to impeccable - 'teach-impeccable', // folded into /impeccable init - // v2.1 merges - 'arrange', // renamed to layout - 'normalize', // merged into polish - 'onboard', // merged into harden - 'extract', // merged into /impeccable extract - // v3.0 consolidation: all standalone skills -> /impeccable sub-commands - 'adapt', - 'animate', - 'audit', - 'bolder', - 'clarify', - 'colorize', - 'critique', - 'delight', - 'distill', - 'harden', - 'layout', - 'optimize', - 'overdrive', - 'polish', - 'quieter', - 'shape', - 'typeset', -]; - -// All known harness directories that may contain a skills/ subfolder. -const HARNESS_DIRS = [ - '.claude', '.cursor', '.gemini', '.codex', '.agents', - '.trae', '.trae-cn', '.pi', '.opencode', '.kiro', '.rovodev', -]; - -// Per-skill fingerprints for SKILL.md bodies that never mentioned -// "impeccable" in their v2.x source. Used as a last-resort match -// when no skills-lock.json exists and the word heuristic fails. -// The strings are lifted verbatim from the v2.x frontmatter -// descriptions, so collisions with hand-written user skills are -// vanishingly unlikely. -const SKILL_FINGERPRINTS = { - harden: 'Make interfaces production-ready: error handling, empty states', - optimize: 'Diagnoses and fixes UI performance across loading speed', -}; - -/** - * Walk up from startDir until we find a directory that looks like a - * project root (has package.json, .git, or skills-lock.json). - */ -export function findProjectRoot(startDir = process.cwd()) { - let dir = resolve(startDir); - const { root } = { root: '/' }; - while (dir !== root) { - if ( - existsSync(join(dir, 'package.json')) || - existsSync(join(dir, '.git')) || - existsSync(join(dir, 'skills-lock.json')) - ) { - return dir; - } - const parent = resolve(dir, '..'); - if (parent === dir) break; - dir = parent; - } - return resolve(startDir); -} - -/** - * Load skills-lock.json from the project root, or null if missing/unreadable. - */ -export function loadLock(projectRoot) { - const lockPath = join(projectRoot, 'skills-lock.json'); - if (!existsSync(lockPath)) return null; - try { - return JSON.parse(readFileSync(lockPath, 'utf-8')); - } catch { - return null; - } -} - -/** - * Check whether a skill directory belongs to Impeccable. Three layered - * signals, in order of reliability: - * 1. Lock source equals "pbakaus/impeccable" (authoritative). - * 2. SKILL.md body contains the word "impeccable". - * 3. SKILL.md body contains a per-skill fingerprint (for harden and - * optimize, whose v2.x SKILL.md never mentioned the pack name). - */ -export function isImpeccableSkill(skillDir, { skillName, lock } = {}) { - // 1. Authoritative: the lock file claims this skill is ours. - if (skillName && lock?.skills?.[skillName]?.source === 'pbakaus/impeccable') { - return true; - } - const skillMd = join(skillDir, 'SKILL.md'); - if (!existsSync(skillMd)) return false; - let content; - try { - content = readFileSync(skillMd, 'utf-8'); - } catch { - return false; - } - // 2. Word-level content heuristic. - if (/impeccable/i.test(content)) return true; - // 3. Per-skill fingerprint for old skills that never mentioned the pack. - // Strip the i- prefix so both `harden` and `i-harden` resolve to the - // same fingerprint entry. - const unprefixed = skillName?.startsWith('i-') ? skillName.slice(2) : skillName; - const fingerprint = unprefixed && SKILL_FINGERPRINTS[unprefixed]; - if (fingerprint && content.includes(fingerprint)) return true; - return false; -} - -/** - * Build the full list of names to check: each deprecated name, plus - * its i-prefixed variant. - */ -export function buildTargetNames() { - const names = []; - for (const name of DEPRECATED_NAMES) { - names.push(name); - names.push(`i-${name}`); - } - return names; -} - -/** - * Find every skills directory across all harness dirs in the project. - * Returns absolute paths that exist on disk. - */ -export function findSkillsDirs(projectRoot) { - const dirs = []; - for (const harness of HARNESS_DIRS) { - const candidate = join(projectRoot, harness, 'skills'); - if (existsSync(candidate)) { - dirs.push(candidate); - } - } - return dirs; -} - -/** - * Remove deprecated skill directories/symlinks from all harness dirs. - * Reads skills-lock.json so the authoritative "source" field can - * drive deletion even when SKILL.md never mentions impeccable. - * Returns an array of paths that were deleted. - */ -export function removeDeprecatedSkills(projectRoot, lock) { - if (lock === undefined) lock = loadLock(projectRoot); - const targets = buildTargetNames(); - const skillsDirs = findSkillsDirs(projectRoot); - const deleted = []; - - for (const skillsDir of skillsDirs) { - for (const name of targets) { - const skillPath = join(skillsDir, name); - - // Use lstat to detect symlinks (existsSync follows symlinks and - // returns false for dangling ones). - let stat; - try { - stat = lstatSync(skillPath); - } catch { - continue; // does not exist at all - } - - if (stat.isSymbolicLink()) { - // Symlink: check the target if it's alive, otherwise treat - // dangling symlinks to deprecated names as safe to remove. - const targetAlive = existsSync(skillPath); - const isMatch = targetAlive - ? isImpeccableSkill(skillPath, { skillName: name, lock }) - : true; - if (isMatch) { - unlinkSync(skillPath); - deleted.push(skillPath); - } - continue; - } - - // Regular directory -- verify it belongs to impeccable - if (isImpeccableSkill(skillPath, { skillName: name, lock })) { - rmSync(skillPath, { recursive: true, force: true }); - deleted.push(skillPath); - } - } - } - - return deleted; -} - -/** - * Remove deprecated entries from skills-lock.json. - * Only removes entries whose source is "pbakaus/impeccable". - * Returns the list of removed skill names. - */ -export function cleanSkillsLock(projectRoot) { - const lockPath = join(projectRoot, 'skills-lock.json'); - if (!existsSync(lockPath)) return []; - - let lock; - try { - lock = JSON.parse(readFileSync(lockPath, 'utf-8')); - } catch { - return []; - } - - if (!lock.skills || typeof lock.skills !== 'object') return []; - - const targets = buildTargetNames(); - const removed = []; - - for (const name of targets) { - const entry = lock.skills[name]; - if (!entry) continue; - // Only remove if it belongs to impeccable - if (entry.source === 'pbakaus/impeccable') { - delete lock.skills[name]; - removed.push(name); - } - } - - if (removed.length > 0) { - writeFileSync(lockPath, JSON.stringify(lock, null, 2) + '\n', 'utf-8'); - } - - return removed; -} - -/** - * Run the full cleanup. Returns a summary object. - * - * Order matters: read the lock and delete directories first, then - * strip lock entries. Otherwise the authoritative signal is gone by - * the time directory deletion runs. - */ -export function cleanup(projectRoot) { - const root = projectRoot || findProjectRoot(); - const lock = loadLock(root); - const deletedPaths = removeDeprecatedSkills(root, lock); - const removedLockEntries = cleanSkillsLock(root); - return { deletedPaths, removedLockEntries, projectRoot: root }; -} - -// CLI entry point -if (process.argv[1] && resolve(process.argv[1]) === resolve(new URL(import.meta.url).pathname)) { - const result = cleanup(); - if (result.deletedPaths.length === 0 && result.removedLockEntries.length === 0) { - console.log('No deprecated Impeccable skills found. Nothing to clean up.'); - } else { - if (result.deletedPaths.length > 0) { - console.log(`Removed ${result.deletedPaths.length} deprecated skill(s):`); - for (const p of result.deletedPaths) console.log(` - ${p}`); - } - if (result.removedLockEntries.length > 0) { - console.log(`Cleaned ${result.removedLockEntries.length} entry/entries from skills-lock.json:`); - for (const name of result.removedLockEntries) console.log(` - ${name}`); - } - } -} diff --git a/.trae-cn/skills/impeccable/scripts/cleanup-deprecated.mjs b/.trae-cn/skills/impeccable/scripts/cleanup-deprecated.mjs deleted file mode 100644 index bc6400e90..000000000 --- a/.trae-cn/skills/impeccable/scripts/cleanup-deprecated.mjs +++ /dev/null @@ -1,284 +0,0 @@ -#!/usr/bin/env node -/** - * Cleans up deprecated Impeccable skill files, symlinks, and - * skills-lock.json entries left over from previous versions. - * - * Safe to run repeatedly -- it is a no-op when nothing needs cleaning. - * - * Usage (from the project root): - * node {{scripts_path}}/cleanup-deprecated.mjs - * - * What it does: - * 1. Finds every harness-specific skills directory (.claude/skills, - * .cursor/skills, .agents/skills, etc.). - * 2. For each deprecated skill name (with and without i- prefix), - * checks if the directory exists and its SKILL.md mentions - * "impeccable" (to avoid deleting unrelated user skills). - * 3. Deletes confirmed matches (files, directories, or symlinks). - * 4. Removes the corresponding entries from skills-lock.json. - */ - -import { existsSync, readFileSync, writeFileSync, rmSync, readdirSync, statSync, lstatSync, unlinkSync } from 'node:fs'; -import { join, resolve } from 'node:path'; - -// Skills that were renamed, merged, or folded in v2.0, v2.1, and v3.0. -const DEPRECATED_NAMES = [ - // v2.0 renames - 'frontend-design', // renamed to impeccable - 'teach-impeccable', // folded into /impeccable init - // v2.1 merges - 'arrange', // renamed to layout - 'normalize', // merged into polish - 'onboard', // merged into harden - 'extract', // merged into /impeccable extract - // v3.0 consolidation: all standalone skills -> /impeccable sub-commands - 'adapt', - 'animate', - 'audit', - 'bolder', - 'clarify', - 'colorize', - 'critique', - 'delight', - 'distill', - 'harden', - 'layout', - 'optimize', - 'overdrive', - 'polish', - 'quieter', - 'shape', - 'typeset', -]; - -// All known harness directories that may contain a skills/ subfolder. -const HARNESS_DIRS = [ - '.claude', '.cursor', '.gemini', '.codex', '.agents', - '.trae', '.trae-cn', '.pi', '.opencode', '.kiro', '.rovodev', -]; - -// Per-skill fingerprints for SKILL.md bodies that never mentioned -// "impeccable" in their v2.x source. Used as a last-resort match -// when no skills-lock.json exists and the word heuristic fails. -// The strings are lifted verbatim from the v2.x frontmatter -// descriptions, so collisions with hand-written user skills are -// vanishingly unlikely. -const SKILL_FINGERPRINTS = { - harden: 'Make interfaces production-ready: error handling, empty states', - optimize: 'Diagnoses and fixes UI performance across loading speed', -}; - -/** - * Walk up from startDir until we find a directory that looks like a - * project root (has package.json, .git, or skills-lock.json). - */ -export function findProjectRoot(startDir = process.cwd()) { - let dir = resolve(startDir); - const { root } = { root: '/' }; - while (dir !== root) { - if ( - existsSync(join(dir, 'package.json')) || - existsSync(join(dir, '.git')) || - existsSync(join(dir, 'skills-lock.json')) - ) { - return dir; - } - const parent = resolve(dir, '..'); - if (parent === dir) break; - dir = parent; - } - return resolve(startDir); -} - -/** - * Load skills-lock.json from the project root, or null if missing/unreadable. - */ -export function loadLock(projectRoot) { - const lockPath = join(projectRoot, 'skills-lock.json'); - if (!existsSync(lockPath)) return null; - try { - return JSON.parse(readFileSync(lockPath, 'utf-8')); - } catch { - return null; - } -} - -/** - * Check whether a skill directory belongs to Impeccable. Three layered - * signals, in order of reliability: - * 1. Lock source equals "pbakaus/impeccable" (authoritative). - * 2. SKILL.md body contains the word "impeccable". - * 3. SKILL.md body contains a per-skill fingerprint (for harden and - * optimize, whose v2.x SKILL.md never mentioned the pack name). - */ -export function isImpeccableSkill(skillDir, { skillName, lock } = {}) { - // 1. Authoritative: the lock file claims this skill is ours. - if (skillName && lock?.skills?.[skillName]?.source === 'pbakaus/impeccable') { - return true; - } - const skillMd = join(skillDir, 'SKILL.md'); - if (!existsSync(skillMd)) return false; - let content; - try { - content = readFileSync(skillMd, 'utf-8'); - } catch { - return false; - } - // 2. Word-level content heuristic. - if (/impeccable/i.test(content)) return true; - // 3. Per-skill fingerprint for old skills that never mentioned the pack. - // Strip the i- prefix so both `harden` and `i-harden` resolve to the - // same fingerprint entry. - const unprefixed = skillName?.startsWith('i-') ? skillName.slice(2) : skillName; - const fingerprint = unprefixed && SKILL_FINGERPRINTS[unprefixed]; - if (fingerprint && content.includes(fingerprint)) return true; - return false; -} - -/** - * Build the full list of names to check: each deprecated name, plus - * its i-prefixed variant. - */ -export function buildTargetNames() { - const names = []; - for (const name of DEPRECATED_NAMES) { - names.push(name); - names.push(`i-${name}`); - } - return names; -} - -/** - * Find every skills directory across all harness dirs in the project. - * Returns absolute paths that exist on disk. - */ -export function findSkillsDirs(projectRoot) { - const dirs = []; - for (const harness of HARNESS_DIRS) { - const candidate = join(projectRoot, harness, 'skills'); - if (existsSync(candidate)) { - dirs.push(candidate); - } - } - return dirs; -} - -/** - * Remove deprecated skill directories/symlinks from all harness dirs. - * Reads skills-lock.json so the authoritative "source" field can - * drive deletion even when SKILL.md never mentions impeccable. - * Returns an array of paths that were deleted. - */ -export function removeDeprecatedSkills(projectRoot, lock) { - if (lock === undefined) lock = loadLock(projectRoot); - const targets = buildTargetNames(); - const skillsDirs = findSkillsDirs(projectRoot); - const deleted = []; - - for (const skillsDir of skillsDirs) { - for (const name of targets) { - const skillPath = join(skillsDir, name); - - // Use lstat to detect symlinks (existsSync follows symlinks and - // returns false for dangling ones). - let stat; - try { - stat = lstatSync(skillPath); - } catch { - continue; // does not exist at all - } - - if (stat.isSymbolicLink()) { - // Symlink: check the target if it's alive, otherwise treat - // dangling symlinks to deprecated names as safe to remove. - const targetAlive = existsSync(skillPath); - const isMatch = targetAlive - ? isImpeccableSkill(skillPath, { skillName: name, lock }) - : true; - if (isMatch) { - unlinkSync(skillPath); - deleted.push(skillPath); - } - continue; - } - - // Regular directory -- verify it belongs to impeccable - if (isImpeccableSkill(skillPath, { skillName: name, lock })) { - rmSync(skillPath, { recursive: true, force: true }); - deleted.push(skillPath); - } - } - } - - return deleted; -} - -/** - * Remove deprecated entries from skills-lock.json. - * Only removes entries whose source is "pbakaus/impeccable". - * Returns the list of removed skill names. - */ -export function cleanSkillsLock(projectRoot) { - const lockPath = join(projectRoot, 'skills-lock.json'); - if (!existsSync(lockPath)) return []; - - let lock; - try { - lock = JSON.parse(readFileSync(lockPath, 'utf-8')); - } catch { - return []; - } - - if (!lock.skills || typeof lock.skills !== 'object') return []; - - const targets = buildTargetNames(); - const removed = []; - - for (const name of targets) { - const entry = lock.skills[name]; - if (!entry) continue; - // Only remove if it belongs to impeccable - if (entry.source === 'pbakaus/impeccable') { - delete lock.skills[name]; - removed.push(name); - } - } - - if (removed.length > 0) { - writeFileSync(lockPath, JSON.stringify(lock, null, 2) + '\n', 'utf-8'); - } - - return removed; -} - -/** - * Run the full cleanup. Returns a summary object. - * - * Order matters: read the lock and delete directories first, then - * strip lock entries. Otherwise the authoritative signal is gone by - * the time directory deletion runs. - */ -export function cleanup(projectRoot) { - const root = projectRoot || findProjectRoot(); - const lock = loadLock(root); - const deletedPaths = removeDeprecatedSkills(root, lock); - const removedLockEntries = cleanSkillsLock(root); - return { deletedPaths, removedLockEntries, projectRoot: root }; -} - -// CLI entry point -if (process.argv[1] && resolve(process.argv[1]) === resolve(new URL(import.meta.url).pathname)) { - const result = cleanup(); - if (result.deletedPaths.length === 0 && result.removedLockEntries.length === 0) { - console.log('No deprecated Impeccable skills found. Nothing to clean up.'); - } else { - if (result.deletedPaths.length > 0) { - console.log(`Removed ${result.deletedPaths.length} deprecated skill(s):`); - for (const p of result.deletedPaths) console.log(` - ${p}`); - } - if (result.removedLockEntries.length > 0) { - console.log(`Cleaned ${result.removedLockEntries.length} entry/entries from skills-lock.json:`); - for (const name of result.removedLockEntries) console.log(` - ${name}`); - } - } -} diff --git a/.trae/skills/impeccable/scripts/cleanup-deprecated.mjs b/.trae/skills/impeccable/scripts/cleanup-deprecated.mjs deleted file mode 100644 index bc6400e90..000000000 --- a/.trae/skills/impeccable/scripts/cleanup-deprecated.mjs +++ /dev/null @@ -1,284 +0,0 @@ -#!/usr/bin/env node -/** - * Cleans up deprecated Impeccable skill files, symlinks, and - * skills-lock.json entries left over from previous versions. - * - * Safe to run repeatedly -- it is a no-op when nothing needs cleaning. - * - * Usage (from the project root): - * node {{scripts_path}}/cleanup-deprecated.mjs - * - * What it does: - * 1. Finds every harness-specific skills directory (.claude/skills, - * .cursor/skills, .agents/skills, etc.). - * 2. For each deprecated skill name (with and without i- prefix), - * checks if the directory exists and its SKILL.md mentions - * "impeccable" (to avoid deleting unrelated user skills). - * 3. Deletes confirmed matches (files, directories, or symlinks). - * 4. Removes the corresponding entries from skills-lock.json. - */ - -import { existsSync, readFileSync, writeFileSync, rmSync, readdirSync, statSync, lstatSync, unlinkSync } from 'node:fs'; -import { join, resolve } from 'node:path'; - -// Skills that were renamed, merged, or folded in v2.0, v2.1, and v3.0. -const DEPRECATED_NAMES = [ - // v2.0 renames - 'frontend-design', // renamed to impeccable - 'teach-impeccable', // folded into /impeccable init - // v2.1 merges - 'arrange', // renamed to layout - 'normalize', // merged into polish - 'onboard', // merged into harden - 'extract', // merged into /impeccable extract - // v3.0 consolidation: all standalone skills -> /impeccable sub-commands - 'adapt', - 'animate', - 'audit', - 'bolder', - 'clarify', - 'colorize', - 'critique', - 'delight', - 'distill', - 'harden', - 'layout', - 'optimize', - 'overdrive', - 'polish', - 'quieter', - 'shape', - 'typeset', -]; - -// All known harness directories that may contain a skills/ subfolder. -const HARNESS_DIRS = [ - '.claude', '.cursor', '.gemini', '.codex', '.agents', - '.trae', '.trae-cn', '.pi', '.opencode', '.kiro', '.rovodev', -]; - -// Per-skill fingerprints for SKILL.md bodies that never mentioned -// "impeccable" in their v2.x source. Used as a last-resort match -// when no skills-lock.json exists and the word heuristic fails. -// The strings are lifted verbatim from the v2.x frontmatter -// descriptions, so collisions with hand-written user skills are -// vanishingly unlikely. -const SKILL_FINGERPRINTS = { - harden: 'Make interfaces production-ready: error handling, empty states', - optimize: 'Diagnoses and fixes UI performance across loading speed', -}; - -/** - * Walk up from startDir until we find a directory that looks like a - * project root (has package.json, .git, or skills-lock.json). - */ -export function findProjectRoot(startDir = process.cwd()) { - let dir = resolve(startDir); - const { root } = { root: '/' }; - while (dir !== root) { - if ( - existsSync(join(dir, 'package.json')) || - existsSync(join(dir, '.git')) || - existsSync(join(dir, 'skills-lock.json')) - ) { - return dir; - } - const parent = resolve(dir, '..'); - if (parent === dir) break; - dir = parent; - } - return resolve(startDir); -} - -/** - * Load skills-lock.json from the project root, or null if missing/unreadable. - */ -export function loadLock(projectRoot) { - const lockPath = join(projectRoot, 'skills-lock.json'); - if (!existsSync(lockPath)) return null; - try { - return JSON.parse(readFileSync(lockPath, 'utf-8')); - } catch { - return null; - } -} - -/** - * Check whether a skill directory belongs to Impeccable. Three layered - * signals, in order of reliability: - * 1. Lock source equals "pbakaus/impeccable" (authoritative). - * 2. SKILL.md body contains the word "impeccable". - * 3. SKILL.md body contains a per-skill fingerprint (for harden and - * optimize, whose v2.x SKILL.md never mentioned the pack name). - */ -export function isImpeccableSkill(skillDir, { skillName, lock } = {}) { - // 1. Authoritative: the lock file claims this skill is ours. - if (skillName && lock?.skills?.[skillName]?.source === 'pbakaus/impeccable') { - return true; - } - const skillMd = join(skillDir, 'SKILL.md'); - if (!existsSync(skillMd)) return false; - let content; - try { - content = readFileSync(skillMd, 'utf-8'); - } catch { - return false; - } - // 2. Word-level content heuristic. - if (/impeccable/i.test(content)) return true; - // 3. Per-skill fingerprint for old skills that never mentioned the pack. - // Strip the i- prefix so both `harden` and `i-harden` resolve to the - // same fingerprint entry. - const unprefixed = skillName?.startsWith('i-') ? skillName.slice(2) : skillName; - const fingerprint = unprefixed && SKILL_FINGERPRINTS[unprefixed]; - if (fingerprint && content.includes(fingerprint)) return true; - return false; -} - -/** - * Build the full list of names to check: each deprecated name, plus - * its i-prefixed variant. - */ -export function buildTargetNames() { - const names = []; - for (const name of DEPRECATED_NAMES) { - names.push(name); - names.push(`i-${name}`); - } - return names; -} - -/** - * Find every skills directory across all harness dirs in the project. - * Returns absolute paths that exist on disk. - */ -export function findSkillsDirs(projectRoot) { - const dirs = []; - for (const harness of HARNESS_DIRS) { - const candidate = join(projectRoot, harness, 'skills'); - if (existsSync(candidate)) { - dirs.push(candidate); - } - } - return dirs; -} - -/** - * Remove deprecated skill directories/symlinks from all harness dirs. - * Reads skills-lock.json so the authoritative "source" field can - * drive deletion even when SKILL.md never mentions impeccable. - * Returns an array of paths that were deleted. - */ -export function removeDeprecatedSkills(projectRoot, lock) { - if (lock === undefined) lock = loadLock(projectRoot); - const targets = buildTargetNames(); - const skillsDirs = findSkillsDirs(projectRoot); - const deleted = []; - - for (const skillsDir of skillsDirs) { - for (const name of targets) { - const skillPath = join(skillsDir, name); - - // Use lstat to detect symlinks (existsSync follows symlinks and - // returns false for dangling ones). - let stat; - try { - stat = lstatSync(skillPath); - } catch { - continue; // does not exist at all - } - - if (stat.isSymbolicLink()) { - // Symlink: check the target if it's alive, otherwise treat - // dangling symlinks to deprecated names as safe to remove. - const targetAlive = existsSync(skillPath); - const isMatch = targetAlive - ? isImpeccableSkill(skillPath, { skillName: name, lock }) - : true; - if (isMatch) { - unlinkSync(skillPath); - deleted.push(skillPath); - } - continue; - } - - // Regular directory -- verify it belongs to impeccable - if (isImpeccableSkill(skillPath, { skillName: name, lock })) { - rmSync(skillPath, { recursive: true, force: true }); - deleted.push(skillPath); - } - } - } - - return deleted; -} - -/** - * Remove deprecated entries from skills-lock.json. - * Only removes entries whose source is "pbakaus/impeccable". - * Returns the list of removed skill names. - */ -export function cleanSkillsLock(projectRoot) { - const lockPath = join(projectRoot, 'skills-lock.json'); - if (!existsSync(lockPath)) return []; - - let lock; - try { - lock = JSON.parse(readFileSync(lockPath, 'utf-8')); - } catch { - return []; - } - - if (!lock.skills || typeof lock.skills !== 'object') return []; - - const targets = buildTargetNames(); - const removed = []; - - for (const name of targets) { - const entry = lock.skills[name]; - if (!entry) continue; - // Only remove if it belongs to impeccable - if (entry.source === 'pbakaus/impeccable') { - delete lock.skills[name]; - removed.push(name); - } - } - - if (removed.length > 0) { - writeFileSync(lockPath, JSON.stringify(lock, null, 2) + '\n', 'utf-8'); - } - - return removed; -} - -/** - * Run the full cleanup. Returns a summary object. - * - * Order matters: read the lock and delete directories first, then - * strip lock entries. Otherwise the authoritative signal is gone by - * the time directory deletion runs. - */ -export function cleanup(projectRoot) { - const root = projectRoot || findProjectRoot(); - const lock = loadLock(root); - const deletedPaths = removeDeprecatedSkills(root, lock); - const removedLockEntries = cleanSkillsLock(root); - return { deletedPaths, removedLockEntries, projectRoot: root }; -} - -// CLI entry point -if (process.argv[1] && resolve(process.argv[1]) === resolve(new URL(import.meta.url).pathname)) { - const result = cleanup(); - if (result.deletedPaths.length === 0 && result.removedLockEntries.length === 0) { - console.log('No deprecated Impeccable skills found. Nothing to clean up.'); - } else { - if (result.deletedPaths.length > 0) { - console.log(`Removed ${result.deletedPaths.length} deprecated skill(s):`); - for (const p of result.deletedPaths) console.log(` - ${p}`); - } - if (result.removedLockEntries.length > 0) { - console.log(`Cleaned ${result.removedLockEntries.length} entry/entries from skills-lock.json:`); - for (const name of result.removedLockEntries) console.log(` - ${name}`); - } - } -} diff --git a/AGENTS.md b/AGENTS.md index 9b5b7e08d..94184019f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,9 +1,5 @@ # Repository Guidelines -## Skill bootstrap: skip the post-update-cleanup step - -If you load the Impeccable skill from `.agents/skills/impeccable/SKILL.md` (or any sibling harness directory under this repo), it begins with a `` block that runs `scripts/cleanup-deprecated.mjs` and then self-deletes. **Do not run it here, and do not delete the block.** This repo is the source of truth for the skill; cleanup-deprecated assumes a consumer project and would touch legitimate source files. The block is regenerated from `skill/SKILL.src.md` whenever `bun run build:release` syncs tracked harness output, so any deletion churns back. Skip it silently and proceed with the task. - ## Project Structure & Module Organization `skill/` is the source of truth for the Impeccable skill: `SKILL.src.md`, `reference/`, `scripts/`, and `agents/`. Build logic lives in `scripts/`, with provider configs in `scripts/lib/transformers/`. The CLI and anti-pattern detector live in `cli/`, the browser extension in `extension/`, the Astro website in `site/`, Cloudflare Pages Functions in `functions/`, and regression coverage in `tests/` with fixtures under `tests/fixtures/`. `dist/` and `build/` are generated and gitignored. The root harness folders (`.agents/`, `.claude/`, `.cursor/`, etc.) and `plugin/` are generated distribution artifacts that are tracked for direct repo installs, not hand-authored source. diff --git a/CLAUDE.md b/CLAUDE.md index 1337592b8..e53854307 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,11 +1,5 @@ # Project Instructions for Claude -## Skill bootstrap: skip the post-update-cleanup step - -The `` block at the top of `.claude/skills/impeccable/SKILL.md` runs `scripts/cleanup-deprecated.mjs` and then self-deletes. **Do not run it in this repo, and do not delete the block.** This repo is the source of truth for the Impeccable skill; the cleanup script scans for "leftover" skill files to remove and its judgment doesn't fit a repo where the skill lives in `skill/`. The block is also regenerated from `skill/SKILL.src.md` whenever `bun run build:release` syncs tracked harness output, so any deletion churns back on the next sync. Skip the step silently and proceed. - -Same rule for AGENTS.md and every other harness-specific instruction file: treat post-update-cleanup as a no-op in this repo. - ## Architecture (v3.0+) There is **one** user-invocable skill, `impeccable`, with **23 commands** underneath it. Users type `/impeccable polish`, `/impeccable audit`, etc. The skill is defined in `skill/`: @@ -15,7 +9,6 @@ There is **one** user-invocable skill, `impeccable`, with **23 commands** undern - `reference/brand.md` and `reference/product.md` — the two register references. SKILL.md's Setup section selects one based on the task cue, the surface in focus, or the `register` field in PRODUCT.md (first match wins). - `scripts/command-metadata.json` — single source of truth for each command's description, argument hint, and (eventually) category. Both the build and `pin.mjs` read from this. - `scripts/pin.mjs` — creates/removes lightweight redirect shims so users can have `/audit` as a standalone shortcut that delegates to `/impeccable audit`. -- `scripts/cleanup-deprecated.mjs` — runs once after an update to remove leftover files from renamed/merged commands. **Do not add standalone skills** unless there's a strong reason. The consolidation was deliberate: the `/` menu pollution problem is real and gets worse as users install more plugins. diff --git a/cli/bin/commands/skills.mjs b/cli/bin/commands/skills.mjs index c3c87e5eb..46dfacd7d 100644 --- a/cli/bin/commands/skills.mjs +++ b/cli/bin/commands/skills.mjs @@ -316,8 +316,8 @@ function migrateUnprefixImpeccable(root) { let entries; try { entries = readdirSync(skillsDir); } catch { continue; } for (const name of entries) { - // A prefixed impeccable skill is `impeccable` -- not the canonical - // `impeccable`, and not the legacy `teach-impeccable` (cleanup handles that). + // A prefixed impeccable skill is `impeccable`, not the canonical + // `impeccable` and not an unrelated legacy skill name. if (name === 'impeccable' || name === 'teach-impeccable') continue; if (!name.endsWith('-impeccable')) continue; if (!isRealSkillDir(skillsDir, name)) continue; @@ -609,18 +609,6 @@ async function install(flags) { } console.log(`Installed impeccable into: ${targets.join(', ')}`); - // Clean up deprecated skills from previous versions - try { - const { cleanup } = await import('../../../skill/scripts/cleanup-deprecated.mjs'); - const result = cleanup(root); - const total = result.deletedPaths.length + result.removedLockEntries.length; - if (total > 0) { - console.log(`Cleaned up ${total} deprecated skill(s) from previous versions.`); - } - } catch { - // Cleanup script not available -- skip - } - console.log('\nDone! Run /impeccable init in your AI harness to set up design context.\n'); } @@ -705,19 +693,6 @@ function downloadFile(url, dest) { async function update(flags = []) { const yes = flags.includes('-y') || flags.includes('--yes'); - // Clean up deprecated skills from previous versions. - try { - const { cleanup } = await import('../../../skill/scripts/cleanup-deprecated.mjs'); - const root = findProjectRoot(); - const result = cleanup(root); - const total = result.deletedPaths.length + result.removedLockEntries.length; - if (total > 0) { - console.log(`Cleaned up ${total} deprecated skill(s) from previous versions.\n`); - } - } catch { - // Cleanup script not available (e.g. running from npm package) -- skip - } - // Download the latest skills directly from impeccable.style. // We skip `npx skills update` because it has a known upstream bug // (vercel-labs/skills#775) where it can't find the lock file. @@ -797,14 +772,6 @@ async function update(flags = []) { rmSync(tmpDir, { recursive: true, force: true }); - // Run cleanup to remove deprecated stubs from the fresh download - try { - const { cleanup: postCleanup } = await import('../../../skill/scripts/cleanup-deprecated.mjs'); - postCleanup(root); - } catch { - // Not available -- skip - } - const v = getSkillsVersion(root); console.log(`Updated ${updated} skill(s)${v ? ` to v${v}` : ''}.`); console.log('Done!\n'); diff --git a/plugin/skills/impeccable/scripts/cleanup-deprecated.mjs b/plugin/skills/impeccable/scripts/cleanup-deprecated.mjs deleted file mode 100644 index bc6400e90..000000000 --- a/plugin/skills/impeccable/scripts/cleanup-deprecated.mjs +++ /dev/null @@ -1,284 +0,0 @@ -#!/usr/bin/env node -/** - * Cleans up deprecated Impeccable skill files, symlinks, and - * skills-lock.json entries left over from previous versions. - * - * Safe to run repeatedly -- it is a no-op when nothing needs cleaning. - * - * Usage (from the project root): - * node {{scripts_path}}/cleanup-deprecated.mjs - * - * What it does: - * 1. Finds every harness-specific skills directory (.claude/skills, - * .cursor/skills, .agents/skills, etc.). - * 2. For each deprecated skill name (with and without i- prefix), - * checks if the directory exists and its SKILL.md mentions - * "impeccable" (to avoid deleting unrelated user skills). - * 3. Deletes confirmed matches (files, directories, or symlinks). - * 4. Removes the corresponding entries from skills-lock.json. - */ - -import { existsSync, readFileSync, writeFileSync, rmSync, readdirSync, statSync, lstatSync, unlinkSync } from 'node:fs'; -import { join, resolve } from 'node:path'; - -// Skills that were renamed, merged, or folded in v2.0, v2.1, and v3.0. -const DEPRECATED_NAMES = [ - // v2.0 renames - 'frontend-design', // renamed to impeccable - 'teach-impeccable', // folded into /impeccable init - // v2.1 merges - 'arrange', // renamed to layout - 'normalize', // merged into polish - 'onboard', // merged into harden - 'extract', // merged into /impeccable extract - // v3.0 consolidation: all standalone skills -> /impeccable sub-commands - 'adapt', - 'animate', - 'audit', - 'bolder', - 'clarify', - 'colorize', - 'critique', - 'delight', - 'distill', - 'harden', - 'layout', - 'optimize', - 'overdrive', - 'polish', - 'quieter', - 'shape', - 'typeset', -]; - -// All known harness directories that may contain a skills/ subfolder. -const HARNESS_DIRS = [ - '.claude', '.cursor', '.gemini', '.codex', '.agents', - '.trae', '.trae-cn', '.pi', '.opencode', '.kiro', '.rovodev', -]; - -// Per-skill fingerprints for SKILL.md bodies that never mentioned -// "impeccable" in their v2.x source. Used as a last-resort match -// when no skills-lock.json exists and the word heuristic fails. -// The strings are lifted verbatim from the v2.x frontmatter -// descriptions, so collisions with hand-written user skills are -// vanishingly unlikely. -const SKILL_FINGERPRINTS = { - harden: 'Make interfaces production-ready: error handling, empty states', - optimize: 'Diagnoses and fixes UI performance across loading speed', -}; - -/** - * Walk up from startDir until we find a directory that looks like a - * project root (has package.json, .git, or skills-lock.json). - */ -export function findProjectRoot(startDir = process.cwd()) { - let dir = resolve(startDir); - const { root } = { root: '/' }; - while (dir !== root) { - if ( - existsSync(join(dir, 'package.json')) || - existsSync(join(dir, '.git')) || - existsSync(join(dir, 'skills-lock.json')) - ) { - return dir; - } - const parent = resolve(dir, '..'); - if (parent === dir) break; - dir = parent; - } - return resolve(startDir); -} - -/** - * Load skills-lock.json from the project root, or null if missing/unreadable. - */ -export function loadLock(projectRoot) { - const lockPath = join(projectRoot, 'skills-lock.json'); - if (!existsSync(lockPath)) return null; - try { - return JSON.parse(readFileSync(lockPath, 'utf-8')); - } catch { - return null; - } -} - -/** - * Check whether a skill directory belongs to Impeccable. Three layered - * signals, in order of reliability: - * 1. Lock source equals "pbakaus/impeccable" (authoritative). - * 2. SKILL.md body contains the word "impeccable". - * 3. SKILL.md body contains a per-skill fingerprint (for harden and - * optimize, whose v2.x SKILL.md never mentioned the pack name). - */ -export function isImpeccableSkill(skillDir, { skillName, lock } = {}) { - // 1. Authoritative: the lock file claims this skill is ours. - if (skillName && lock?.skills?.[skillName]?.source === 'pbakaus/impeccable') { - return true; - } - const skillMd = join(skillDir, 'SKILL.md'); - if (!existsSync(skillMd)) return false; - let content; - try { - content = readFileSync(skillMd, 'utf-8'); - } catch { - return false; - } - // 2. Word-level content heuristic. - if (/impeccable/i.test(content)) return true; - // 3. Per-skill fingerprint for old skills that never mentioned the pack. - // Strip the i- prefix so both `harden` and `i-harden` resolve to the - // same fingerprint entry. - const unprefixed = skillName?.startsWith('i-') ? skillName.slice(2) : skillName; - const fingerprint = unprefixed && SKILL_FINGERPRINTS[unprefixed]; - if (fingerprint && content.includes(fingerprint)) return true; - return false; -} - -/** - * Build the full list of names to check: each deprecated name, plus - * its i-prefixed variant. - */ -export function buildTargetNames() { - const names = []; - for (const name of DEPRECATED_NAMES) { - names.push(name); - names.push(`i-${name}`); - } - return names; -} - -/** - * Find every skills directory across all harness dirs in the project. - * Returns absolute paths that exist on disk. - */ -export function findSkillsDirs(projectRoot) { - const dirs = []; - for (const harness of HARNESS_DIRS) { - const candidate = join(projectRoot, harness, 'skills'); - if (existsSync(candidate)) { - dirs.push(candidate); - } - } - return dirs; -} - -/** - * Remove deprecated skill directories/symlinks from all harness dirs. - * Reads skills-lock.json so the authoritative "source" field can - * drive deletion even when SKILL.md never mentions impeccable. - * Returns an array of paths that were deleted. - */ -export function removeDeprecatedSkills(projectRoot, lock) { - if (lock === undefined) lock = loadLock(projectRoot); - const targets = buildTargetNames(); - const skillsDirs = findSkillsDirs(projectRoot); - const deleted = []; - - for (const skillsDir of skillsDirs) { - for (const name of targets) { - const skillPath = join(skillsDir, name); - - // Use lstat to detect symlinks (existsSync follows symlinks and - // returns false for dangling ones). - let stat; - try { - stat = lstatSync(skillPath); - } catch { - continue; // does not exist at all - } - - if (stat.isSymbolicLink()) { - // Symlink: check the target if it's alive, otherwise treat - // dangling symlinks to deprecated names as safe to remove. - const targetAlive = existsSync(skillPath); - const isMatch = targetAlive - ? isImpeccableSkill(skillPath, { skillName: name, lock }) - : true; - if (isMatch) { - unlinkSync(skillPath); - deleted.push(skillPath); - } - continue; - } - - // Regular directory -- verify it belongs to impeccable - if (isImpeccableSkill(skillPath, { skillName: name, lock })) { - rmSync(skillPath, { recursive: true, force: true }); - deleted.push(skillPath); - } - } - } - - return deleted; -} - -/** - * Remove deprecated entries from skills-lock.json. - * Only removes entries whose source is "pbakaus/impeccable". - * Returns the list of removed skill names. - */ -export function cleanSkillsLock(projectRoot) { - const lockPath = join(projectRoot, 'skills-lock.json'); - if (!existsSync(lockPath)) return []; - - let lock; - try { - lock = JSON.parse(readFileSync(lockPath, 'utf-8')); - } catch { - return []; - } - - if (!lock.skills || typeof lock.skills !== 'object') return []; - - const targets = buildTargetNames(); - const removed = []; - - for (const name of targets) { - const entry = lock.skills[name]; - if (!entry) continue; - // Only remove if it belongs to impeccable - if (entry.source === 'pbakaus/impeccable') { - delete lock.skills[name]; - removed.push(name); - } - } - - if (removed.length > 0) { - writeFileSync(lockPath, JSON.stringify(lock, null, 2) + '\n', 'utf-8'); - } - - return removed; -} - -/** - * Run the full cleanup. Returns a summary object. - * - * Order matters: read the lock and delete directories first, then - * strip lock entries. Otherwise the authoritative signal is gone by - * the time directory deletion runs. - */ -export function cleanup(projectRoot) { - const root = projectRoot || findProjectRoot(); - const lock = loadLock(root); - const deletedPaths = removeDeprecatedSkills(root, lock); - const removedLockEntries = cleanSkillsLock(root); - return { deletedPaths, removedLockEntries, projectRoot: root }; -} - -// CLI entry point -if (process.argv[1] && resolve(process.argv[1]) === resolve(new URL(import.meta.url).pathname)) { - const result = cleanup(); - if (result.deletedPaths.length === 0 && result.removedLockEntries.length === 0) { - console.log('No deprecated Impeccable skills found. Nothing to clean up.'); - } else { - if (result.deletedPaths.length > 0) { - console.log(`Removed ${result.deletedPaths.length} deprecated skill(s):`); - for (const p of result.deletedPaths) console.log(` - ${p}`); - } - if (result.removedLockEntries.length > 0) { - console.log(`Cleaned ${result.removedLockEntries.length} entry/entries from skills-lock.json:`); - for (const name of result.removedLockEntries) console.log(` - ${name}`); - } - } -} diff --git a/scripts/build.js b/scripts/build.js index 17de4d249..d06712f76 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -658,24 +658,6 @@ async function build() { } } - // Remove deprecated skill stubs from local harness dirs. They exist - // in dist/ so the cleanup script can redirect users, but they should - // not clutter the repo's own skill directories. - const deprecatedLocalSkills = [ - 'frontend-design', 'teach-impeccable', - 'arrange', 'normalize', 'onboard', 'extract', - // v3.0 consolidation: standalone skills -> /impeccable sub-commands - 'adapt', 'animate', 'audit', 'bolder', 'clarify', 'colorize', - 'critique', 'delight', 'distill', 'harden', 'layout', 'optimize', - 'overdrive', 'polish', 'quieter', 'shape', 'typeset', - ]; - for (const { configDir } of syncConfigs) { - for (const name of deprecatedLocalSkills) { - const p = path.join(ROOT_DIR, configDir, 'skills', name); - if (fs.existsSync(p)) fs.rmSync(p, { recursive: true, force: true }); - } - } - console.log(`📋 Synced skills to: ${syncConfigs.map(p => p.configDir).join(', ')}`); // Build the Claude Code plugin subtree at ./plugin/. diff --git a/scripts/test-suites.mjs b/scripts/test-suites.mjs index be8f12948..82530f352 100644 --- a/scripts/test-suites.mjs +++ b/scripts/test-suites.mjs @@ -25,11 +25,11 @@ export const SUITES = { triggers: [ ...COMMON_INFRA_PATTERNS, /^scripts\/(?!benchmark-detector|build-browser-detector|build-extension)/, - /^skill\/(SKILL\.src\.md|agents\/|reference\/|scripts\/(cleanup-deprecated|context|context-signals|critique-storage|design-parser|impeccable-paths|is-generated))/, + /^skill\/(SKILL\.src\.md|agents\/|reference\/|scripts\/(context|context-signals|critique-storage|design-parser|impeccable-paths|is-generated))/, /^site\/(pages|content|components|layouts)\//, /^README(\.npm)?\.md$/, /^cli\/bin\//, - /^tests\/(build|cleanup-deprecated|context|context-signals|critique-storage|design-parser|docs-integrity|impeccable-paths|skills-cli|test-suites|windows-path-fix)\.test\.(js|mjs)$/, + /^tests\/(build|context|context-signals|critique-storage|design-parser|docs-integrity|impeccable-paths|skills-cli|test-suites|windows-path-fix)\.test\.(js|mjs)$/, /^tests\/lib\//, ], commands: [ @@ -51,7 +51,6 @@ export const SUITES = { runner: 'node', files: [ 'tests/ci-test-plan.test.mjs', - 'tests/cleanup-deprecated.test.mjs', 'tests/context.test.mjs', 'tests/context-signals.test.mjs', 'tests/critique-storage.test.mjs', diff --git a/skill/scripts/cleanup-deprecated.mjs b/skill/scripts/cleanup-deprecated.mjs deleted file mode 100644 index bc6400e90..000000000 --- a/skill/scripts/cleanup-deprecated.mjs +++ /dev/null @@ -1,284 +0,0 @@ -#!/usr/bin/env node -/** - * Cleans up deprecated Impeccable skill files, symlinks, and - * skills-lock.json entries left over from previous versions. - * - * Safe to run repeatedly -- it is a no-op when nothing needs cleaning. - * - * Usage (from the project root): - * node {{scripts_path}}/cleanup-deprecated.mjs - * - * What it does: - * 1. Finds every harness-specific skills directory (.claude/skills, - * .cursor/skills, .agents/skills, etc.). - * 2. For each deprecated skill name (with and without i- prefix), - * checks if the directory exists and its SKILL.md mentions - * "impeccable" (to avoid deleting unrelated user skills). - * 3. Deletes confirmed matches (files, directories, or symlinks). - * 4. Removes the corresponding entries from skills-lock.json. - */ - -import { existsSync, readFileSync, writeFileSync, rmSync, readdirSync, statSync, lstatSync, unlinkSync } from 'node:fs'; -import { join, resolve } from 'node:path'; - -// Skills that were renamed, merged, or folded in v2.0, v2.1, and v3.0. -const DEPRECATED_NAMES = [ - // v2.0 renames - 'frontend-design', // renamed to impeccable - 'teach-impeccable', // folded into /impeccable init - // v2.1 merges - 'arrange', // renamed to layout - 'normalize', // merged into polish - 'onboard', // merged into harden - 'extract', // merged into /impeccable extract - // v3.0 consolidation: all standalone skills -> /impeccable sub-commands - 'adapt', - 'animate', - 'audit', - 'bolder', - 'clarify', - 'colorize', - 'critique', - 'delight', - 'distill', - 'harden', - 'layout', - 'optimize', - 'overdrive', - 'polish', - 'quieter', - 'shape', - 'typeset', -]; - -// All known harness directories that may contain a skills/ subfolder. -const HARNESS_DIRS = [ - '.claude', '.cursor', '.gemini', '.codex', '.agents', - '.trae', '.trae-cn', '.pi', '.opencode', '.kiro', '.rovodev', -]; - -// Per-skill fingerprints for SKILL.md bodies that never mentioned -// "impeccable" in their v2.x source. Used as a last-resort match -// when no skills-lock.json exists and the word heuristic fails. -// The strings are lifted verbatim from the v2.x frontmatter -// descriptions, so collisions with hand-written user skills are -// vanishingly unlikely. -const SKILL_FINGERPRINTS = { - harden: 'Make interfaces production-ready: error handling, empty states', - optimize: 'Diagnoses and fixes UI performance across loading speed', -}; - -/** - * Walk up from startDir until we find a directory that looks like a - * project root (has package.json, .git, or skills-lock.json). - */ -export function findProjectRoot(startDir = process.cwd()) { - let dir = resolve(startDir); - const { root } = { root: '/' }; - while (dir !== root) { - if ( - existsSync(join(dir, 'package.json')) || - existsSync(join(dir, '.git')) || - existsSync(join(dir, 'skills-lock.json')) - ) { - return dir; - } - const parent = resolve(dir, '..'); - if (parent === dir) break; - dir = parent; - } - return resolve(startDir); -} - -/** - * Load skills-lock.json from the project root, or null if missing/unreadable. - */ -export function loadLock(projectRoot) { - const lockPath = join(projectRoot, 'skills-lock.json'); - if (!existsSync(lockPath)) return null; - try { - return JSON.parse(readFileSync(lockPath, 'utf-8')); - } catch { - return null; - } -} - -/** - * Check whether a skill directory belongs to Impeccable. Three layered - * signals, in order of reliability: - * 1. Lock source equals "pbakaus/impeccable" (authoritative). - * 2. SKILL.md body contains the word "impeccable". - * 3. SKILL.md body contains a per-skill fingerprint (for harden and - * optimize, whose v2.x SKILL.md never mentioned the pack name). - */ -export function isImpeccableSkill(skillDir, { skillName, lock } = {}) { - // 1. Authoritative: the lock file claims this skill is ours. - if (skillName && lock?.skills?.[skillName]?.source === 'pbakaus/impeccable') { - return true; - } - const skillMd = join(skillDir, 'SKILL.md'); - if (!existsSync(skillMd)) return false; - let content; - try { - content = readFileSync(skillMd, 'utf-8'); - } catch { - return false; - } - // 2. Word-level content heuristic. - if (/impeccable/i.test(content)) return true; - // 3. Per-skill fingerprint for old skills that never mentioned the pack. - // Strip the i- prefix so both `harden` and `i-harden` resolve to the - // same fingerprint entry. - const unprefixed = skillName?.startsWith('i-') ? skillName.slice(2) : skillName; - const fingerprint = unprefixed && SKILL_FINGERPRINTS[unprefixed]; - if (fingerprint && content.includes(fingerprint)) return true; - return false; -} - -/** - * Build the full list of names to check: each deprecated name, plus - * its i-prefixed variant. - */ -export function buildTargetNames() { - const names = []; - for (const name of DEPRECATED_NAMES) { - names.push(name); - names.push(`i-${name}`); - } - return names; -} - -/** - * Find every skills directory across all harness dirs in the project. - * Returns absolute paths that exist on disk. - */ -export function findSkillsDirs(projectRoot) { - const dirs = []; - for (const harness of HARNESS_DIRS) { - const candidate = join(projectRoot, harness, 'skills'); - if (existsSync(candidate)) { - dirs.push(candidate); - } - } - return dirs; -} - -/** - * Remove deprecated skill directories/symlinks from all harness dirs. - * Reads skills-lock.json so the authoritative "source" field can - * drive deletion even when SKILL.md never mentions impeccable. - * Returns an array of paths that were deleted. - */ -export function removeDeprecatedSkills(projectRoot, lock) { - if (lock === undefined) lock = loadLock(projectRoot); - const targets = buildTargetNames(); - const skillsDirs = findSkillsDirs(projectRoot); - const deleted = []; - - for (const skillsDir of skillsDirs) { - for (const name of targets) { - const skillPath = join(skillsDir, name); - - // Use lstat to detect symlinks (existsSync follows symlinks and - // returns false for dangling ones). - let stat; - try { - stat = lstatSync(skillPath); - } catch { - continue; // does not exist at all - } - - if (stat.isSymbolicLink()) { - // Symlink: check the target if it's alive, otherwise treat - // dangling symlinks to deprecated names as safe to remove. - const targetAlive = existsSync(skillPath); - const isMatch = targetAlive - ? isImpeccableSkill(skillPath, { skillName: name, lock }) - : true; - if (isMatch) { - unlinkSync(skillPath); - deleted.push(skillPath); - } - continue; - } - - // Regular directory -- verify it belongs to impeccable - if (isImpeccableSkill(skillPath, { skillName: name, lock })) { - rmSync(skillPath, { recursive: true, force: true }); - deleted.push(skillPath); - } - } - } - - return deleted; -} - -/** - * Remove deprecated entries from skills-lock.json. - * Only removes entries whose source is "pbakaus/impeccable". - * Returns the list of removed skill names. - */ -export function cleanSkillsLock(projectRoot) { - const lockPath = join(projectRoot, 'skills-lock.json'); - if (!existsSync(lockPath)) return []; - - let lock; - try { - lock = JSON.parse(readFileSync(lockPath, 'utf-8')); - } catch { - return []; - } - - if (!lock.skills || typeof lock.skills !== 'object') return []; - - const targets = buildTargetNames(); - const removed = []; - - for (const name of targets) { - const entry = lock.skills[name]; - if (!entry) continue; - // Only remove if it belongs to impeccable - if (entry.source === 'pbakaus/impeccable') { - delete lock.skills[name]; - removed.push(name); - } - } - - if (removed.length > 0) { - writeFileSync(lockPath, JSON.stringify(lock, null, 2) + '\n', 'utf-8'); - } - - return removed; -} - -/** - * Run the full cleanup. Returns a summary object. - * - * Order matters: read the lock and delete directories first, then - * strip lock entries. Otherwise the authoritative signal is gone by - * the time directory deletion runs. - */ -export function cleanup(projectRoot) { - const root = projectRoot || findProjectRoot(); - const lock = loadLock(root); - const deletedPaths = removeDeprecatedSkills(root, lock); - const removedLockEntries = cleanSkillsLock(root); - return { deletedPaths, removedLockEntries, projectRoot: root }; -} - -// CLI entry point -if (process.argv[1] && resolve(process.argv[1]) === resolve(new URL(import.meta.url).pathname)) { - const result = cleanup(); - if (result.deletedPaths.length === 0 && result.removedLockEntries.length === 0) { - console.log('No deprecated Impeccable skills found. Nothing to clean up.'); - } else { - if (result.deletedPaths.length > 0) { - console.log(`Removed ${result.deletedPaths.length} deprecated skill(s):`); - for (const p of result.deletedPaths) console.log(` - ${p}`); - } - if (result.removedLockEntries.length > 0) { - console.log(`Cleaned ${result.removedLockEntries.length} entry/entries from skills-lock.json:`); - for (const name of result.removedLockEntries) console.log(` - ${name}`); - } - } -} diff --git a/tests/cleanup-deprecated.test.mjs b/tests/cleanup-deprecated.test.mjs deleted file mode 100644 index 7bee75365..000000000 --- a/tests/cleanup-deprecated.test.mjs +++ /dev/null @@ -1,344 +0,0 @@ -import { describe, it, beforeEach, afterEach } from 'node:test'; -import assert from 'node:assert/strict'; -import { mkdirSync, writeFileSync, readFileSync, existsSync, symlinkSync, rmSync } from 'node:fs'; -import { join } from 'node:path'; -import { mkdtempSync } from 'node:fs'; -import { tmpdir } from 'node:os'; - -import { - findProjectRoot, - isImpeccableSkill, - buildTargetNames, - findSkillsDirs, - removeDeprecatedSkills, - cleanSkillsLock, - cleanup, - loadLock, -} from '../skill/scripts/cleanup-deprecated.mjs'; - -function makeTmpDir() { - return mkdtempSync(join(tmpdir(), 'impeccable-cleanup-test-')); -} - -function writeSkill(root, harness, name, content) { - const dir = join(root, harness, 'skills', name); - mkdirSync(dir, { recursive: true }); - writeFileSync(join(dir, 'SKILL.md'), content, 'utf-8'); - return dir; -} - -describe('cleanup-deprecated', () => { - let tmp; - - beforeEach(() => { - tmp = makeTmpDir(); - // Mark as project root - writeFileSync(join(tmp, 'package.json'), '{}', 'utf-8'); - }); - - afterEach(() => { - rmSync(tmp, { recursive: true, force: true }); - }); - - describe('findProjectRoot', () => { - it('finds directory with package.json', () => { - const sub = join(tmp, 'a', 'b', 'c'); - mkdirSync(sub, { recursive: true }); - assert.equal(findProjectRoot(sub), tmp); - }); - - it('finds directory with skills-lock.json', () => { - const root2 = makeTmpDir(); - writeFileSync(join(root2, 'skills-lock.json'), '{}', 'utf-8'); - assert.equal(findProjectRoot(root2), root2); - rmSync(root2, { recursive: true, force: true }); - }); - }); - - describe('isImpeccableSkill', () => { - it('returns true when SKILL.md mentions impeccable', () => { - const dir = writeSkill(tmp, '.claude', 'arrange', 'Invoke /impeccable first.'); - assert.equal(isImpeccableSkill(dir), true); - }); - - it('returns false when SKILL.md does not mention impeccable', () => { - const dir = writeSkill(tmp, '.claude', 'arrange', 'This is my custom arrange skill.'); - assert.equal(isImpeccableSkill(dir), false); - }); - - it('returns false for non-existent directory', () => { - assert.equal(isImpeccableSkill(join(tmp, 'nope')), false); - }); - - it('returns true when lock source says pbakaus/impeccable, even if SKILL.md never mentions it', () => { - const dir = writeSkill(tmp, '.claude', 'harden', 'A custom skill with no pack mention.'); - const lock = { - skills: { harden: { source: 'pbakaus/impeccable' } }, - }; - assert.equal(isImpeccableSkill(dir, { skillName: 'harden', lock }), true); - }); - - it('returns false when lock source is a different pack', () => { - const dir = writeSkill(tmp, '.claude', 'harden', 'A custom skill with no pack mention.'); - const lock = { - skills: { harden: { source: 'someone-else/pack' } }, - }; - assert.equal(isImpeccableSkill(dir, { skillName: 'harden', lock }), false); - }); - - it('falls back to SKILL.md content when no lock entry exists', () => { - const dir = writeSkill(tmp, '.claude', 'harden', 'Invoke /impeccable to harden.'); - const lock = { skills: {} }; - assert.equal(isImpeccableSkill(dir, { skillName: 'harden', lock }), true); - }); - }); - - describe('loadLock', () => { - it('returns null when skills-lock.json is missing', () => { - assert.equal(loadLock(tmp), null); - }); - - it('parses skills-lock.json when present', () => { - const lock = { version: 1, skills: { arrange: { source: 'pbakaus/impeccable' } } }; - writeFileSync(join(tmp, 'skills-lock.json'), JSON.stringify(lock), 'utf-8'); - assert.deepEqual(loadLock(tmp), lock); - }); - - it('returns null on malformed JSON', () => { - writeFileSync(join(tmp, 'skills-lock.json'), '{not json', 'utf-8'); - assert.equal(loadLock(tmp), null); - }); - }); - - describe('buildTargetNames', () => { - it('includes both unprefixed and i-prefixed names', () => { - const names = buildTargetNames(); - assert.ok(names.includes('arrange')); - assert.ok(names.includes('i-arrange')); - assert.ok(names.includes('frontend-design')); - assert.ok(names.includes('i-frontend-design')); - assert.equal(names.length, 46); // 23 deprecated * 2 - }); - }); - - describe('findSkillsDirs', () => { - it('finds existing harness skill directories', () => { - mkdirSync(join(tmp, '.claude', 'skills'), { recursive: true }); - mkdirSync(join(tmp, '.agents', 'skills'), { recursive: true }); - const dirs = findSkillsDirs(tmp); - assert.equal(dirs.length, 2); - }); - - it('ignores non-existent harness directories', () => { - const dirs = findSkillsDirs(tmp); - assert.equal(dirs.length, 0); - }); - }); - - describe('removeDeprecatedSkills', () => { - it('deletes impeccable-owned deprecated skill directories', () => { - writeSkill(tmp, '.claude', 'arrange', 'Invoke /impeccable first.'); - writeSkill(tmp, '.claude', 'normalize', 'Run impeccable teach.'); - const deleted = removeDeprecatedSkills(tmp); - assert.equal(deleted.length, 2); - assert.equal(existsSync(join(tmp, '.claude', 'skills', 'arrange')), false); - assert.equal(existsSync(join(tmp, '.claude', 'skills', 'normalize')), false); - }); - - it('does NOT delete skills that do not mention impeccable', () => { - writeSkill(tmp, '.claude', 'arrange', 'My custom layout organizer.'); - const deleted = removeDeprecatedSkills(tmp); - assert.equal(deleted.length, 0); - assert.equal(existsSync(join(tmp, '.claude', 'skills', 'arrange')), true); - }); - - it('deletes i-prefixed variants', () => { - writeSkill(tmp, '.cursor', 'i-normalize', 'Invoke /impeccable first.'); - const deleted = removeDeprecatedSkills(tmp); - assert.equal(deleted.length, 1); - assert.equal(existsSync(join(tmp, '.cursor', 'skills', 'i-normalize')), false); - }); - - it('cleans across multiple harness directories', () => { - writeSkill(tmp, '.claude', 'onboard', 'Run impeccable teach first.'); - writeSkill(tmp, '.agents', 'onboard', 'Run impeccable teach first.'); - writeSkill(tmp, '.cursor', 'onboard', 'Run impeccable teach first.'); - const deleted = removeDeprecatedSkills(tmp); - assert.equal(deleted.length, 3); - }); - - it('leaves non-deprecated skills alone', () => { - writeSkill(tmp, '.claude', 'my-custom-skill', 'Invoke /impeccable first.'); - writeSkill(tmp, '.claude', 'arrange', 'Invoke /impeccable first.'); - const deleted = removeDeprecatedSkills(tmp); - assert.equal(deleted.length, 1); // only arrange - assert.equal(existsSync(join(tmp, '.claude', 'skills', 'my-custom-skill')), true); - }); - - it('deletes a stock v2.x harden skill with no lock file via the fingerprint fallback', () => { - // Reproduces the no-lock-file install path: user installed via - // submodule or manual copy, so skills-lock.json never existed. The - // v2.x harden SKILL.md never contained the word "impeccable", but - // does contain the distinctive description fingerprint. - const body = [ - '---', - 'name: harden', - 'description: "Make interfaces production-ready: error handling, empty states, onboarding flows, i18n, text overflow, and edge case management."', - '---', - '', - 'Strengthen interfaces against edge cases.', - ].join('\n'); - writeSkill(tmp, '.claude', 'harden', body); - const deleted = removeDeprecatedSkills(tmp); - assert.equal(deleted.length, 1); - assert.equal(existsSync(join(tmp, '.claude', 'skills', 'harden')), false); - }); - - it('deletes a stock v2.x optimize skill with no lock file via the fingerprint fallback', () => { - const body = [ - '---', - 'name: optimize', - 'description: "Diagnoses and fixes UI performance across loading speed, rendering, animations, images, and bundle size."', - '---', - '', - 'Identify and fix performance issues.', - ].join('\n'); - writeSkill(tmp, '.claude', 'optimize', body); - const deleted = removeDeprecatedSkills(tmp); - assert.equal(deleted.length, 1); - }); - - it('does NOT delete a user-written harden skill that lacks both the pack word and the fingerprint', () => { - writeSkill(tmp, '.claude', 'harden', 'My custom skill for hardening cookies against CSRF.'); - const deleted = removeDeprecatedSkills(tmp); - assert.equal(deleted.length, 0); - assert.equal(existsSync(join(tmp, '.claude', 'skills', 'harden')), true); - }); - - it('deletes skills whose SKILL.md never mentions impeccable when the lock claims them', () => { - // Reproduces the "orphan dir" bug: the old SKILL.md bodies described - // each skill on its own merits and never said the word "impeccable", - // so the content heuristic returned false. The lock source is the - // authoritative signal. - writeSkill(tmp, '.claude', 'harden', '# Harden\n\nA custom skill with zero pack-name mentions.'); - const lock = { - version: 1, - skills: { harden: { source: 'pbakaus/impeccable', sourceType: 'github', computedHash: 'x' } }, - }; - writeFileSync(join(tmp, 'skills-lock.json'), JSON.stringify(lock), 'utf-8'); - const deleted = removeDeprecatedSkills(tmp); - assert.equal(deleted.length, 1); - assert.equal(existsSync(join(tmp, '.claude', 'skills', 'harden')), false); - }); - - it('does NOT delete a same-named skill owned by a different pack', () => { - writeSkill(tmp, '.claude', 'extract', 'Some user-written extract skill.'); - const lock = { - version: 1, - skills: { extract: { source: 'someone-else/pack' } }, - }; - writeFileSync(join(tmp, 'skills-lock.json'), JSON.stringify(lock), 'utf-8'); - const deleted = removeDeprecatedSkills(tmp); - assert.equal(deleted.length, 0); - assert.equal(existsSync(join(tmp, '.claude', 'skills', 'extract')), true); - }); - - it('handles symlinks to deprecated skills', () => { - // Create the canonical skill in .agents - const canonical = writeSkill(tmp, '.agents', 'extract', 'Use impeccable extract.'); - // Create a symlink in .claude - mkdirSync(join(tmp, '.claude', 'skills'), { recursive: true }); - symlinkSync(canonical, join(tmp, '.claude', 'skills', 'extract')); - const deleted = removeDeprecatedSkills(tmp); - assert.equal(deleted.length, 2); // both canonical and symlink - }); - }); - - describe('cleanSkillsLock', () => { - it('removes impeccable-owned deprecated entries', () => { - const lock = { - version: 1, - skills: { - arrange: { source: 'pbakaus/impeccable', sourceType: 'github', computedHash: 'abc' }, - impeccable: { source: 'pbakaus/impeccable', sourceType: 'github', computedHash: 'def' }, - 'resolve-reviews': { source: 'pbakaus/agent-reviews', sourceType: 'github', computedHash: 'ghi' }, - }, - }; - writeFileSync(join(tmp, 'skills-lock.json'), JSON.stringify(lock), 'utf-8'); - const removed = cleanSkillsLock(tmp); - assert.deepEqual(removed, ['arrange']); - const updated = JSON.parse(readFileSync(join(tmp, 'skills-lock.json'), 'utf-8')); - assert.equal(updated.skills.arrange, undefined); - assert.ok(updated.skills.impeccable); // not deprecated - assert.ok(updated.skills['resolve-reviews']); // different source - }); - - it('does NOT remove entries from other sources', () => { - const lock = { - version: 1, - skills: { - extract: { source: 'some-other/package', sourceType: 'github', computedHash: 'xyz' }, - }, - }; - writeFileSync(join(tmp, 'skills-lock.json'), JSON.stringify(lock), 'utf-8'); - const removed = cleanSkillsLock(tmp); - assert.equal(removed.length, 0); - }); - - it('handles missing skills-lock.json gracefully', () => { - const removed = cleanSkillsLock(tmp); - assert.equal(removed.length, 0); - }); - - it('removes i-prefixed entries', () => { - const lock = { - version: 1, - skills: { - 'i-arrange': { source: 'pbakaus/impeccable', sourceType: 'github', computedHash: 'abc' }, - 'i-normalize': { source: 'pbakaus/impeccable', sourceType: 'github', computedHash: 'def' }, - }, - }; - writeFileSync(join(tmp, 'skills-lock.json'), JSON.stringify(lock), 'utf-8'); - const removed = cleanSkillsLock(tmp); - assert.equal(removed.length, 2); - }); - }); - - describe('cleanup (integration)', () => { - it('cleans both files and lock entries in one pass', () => { - // Set up deprecated skills in two harness dirs - writeSkill(tmp, '.claude', 'arrange', 'Invoke /impeccable.'); - writeSkill(tmp, '.agents', 'arrange', 'Invoke /impeccable.'); - writeSkill(tmp, '.claude', 'extract', 'Run impeccable extract.'); - - // Set up lock file - const lock = { - version: 1, - skills: { - arrange: { source: 'pbakaus/impeccable', sourceType: 'github', computedHash: 'a' }, - extract: { source: 'pbakaus/impeccable', sourceType: 'github', computedHash: 'b' }, - impeccable: { source: 'pbakaus/impeccable', sourceType: 'github', computedHash: 'c' }, - }, - }; - writeFileSync(join(tmp, 'skills-lock.json'), JSON.stringify(lock), 'utf-8'); - - const result = cleanup(tmp); - assert.equal(result.deletedPaths.length, 3); - assert.equal(result.removedLockEntries.length, 2); // arrange + extract - assert.equal(existsSync(join(tmp, '.claude', 'skills', 'arrange')), false); - assert.equal(existsSync(join(tmp, '.agents', 'skills', 'arrange')), false); - - const updated = JSON.parse(readFileSync(join(tmp, 'skills-lock.json'), 'utf-8')); - assert.ok(updated.skills.impeccable); // not deprecated - assert.equal(updated.skills.arrange, undefined); - assert.equal(updated.skills.extract, undefined); - }); - - it('is a no-op when nothing needs cleaning', () => { - writeSkill(tmp, '.claude', 'my-custom-skill', 'Invoke /impeccable.'); - const result = cleanup(tmp); - assert.equal(result.deletedPaths.length, 0); - assert.equal(result.removedLockEntries.length, 0); - }); - }); -}); diff --git a/tests/skills-cli.test.js b/tests/skills-cli.test.js index cc3cf8275..693fce88f 100644 --- a/tests/skills-cli.test.js +++ b/tests/skills-cli.test.js @@ -299,7 +299,7 @@ describe('skills: unprefix migration', () => { rmSync(tmp, { recursive: true, force: true }); }); - test('leaves the legacy teach-impeccable name alone (cleanup owns that)', () => { + test('leaves unrelated legacy skill names alone', () => { const tmp = mkdtempSync(join(tmpdir(), 'imp-test-mig-legacy-')); createFakeSkills(tmp, ['teach-impeccable'], ['.claude']);