Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
One-time setup that gathers your project's design context and saves it to CLAUDE.md for future sessions.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
scripts/cleanup-deprecated.mjs
1#!/usr/bin/env node2/**3* Cleans up deprecated Impeccable skill files, symlinks, and4* skills-lock.json entries left over from previous versions.5*6* Safe to run repeatedly -- it is a no-op when nothing needs cleaning.7*8* Usage (from the project root):9* node {{scripts_path}}/cleanup-deprecated.mjs10*11* What it does:12* 1. Finds every harness-specific skills directory (.claude/skills,13* .cursor/skills, .agents/skills, etc.).14* 2. For each deprecated skill name (with and without i- prefix),15* checks if the directory exists and its SKILL.md mentions16* "impeccable" (to avoid deleting unrelated user skills).17* 3. Deletes confirmed matches (files, directories, or symlinks).18* 4. Removes the corresponding entries from skills-lock.json.19*/2021import { existsSync, readFileSync, writeFileSync, rmSync, readdirSync, statSync, lstatSync, unlinkSync } from 'node:fs';22import { join, resolve } from 'node:path';2324// Skills that were renamed, merged, or folded in v2.0, v2.1, and v3.0.25const DEPRECATED_NAMES = [26// v2.0 renames27'frontend-design', // renamed to impeccable28'teach-impeccable', // folded into /impeccable teach29// v2.1 merges30'arrange', // renamed to layout31'normalize', // merged into polish32'onboard', // merged into harden33'extract', // merged into /impeccable extract34// v3.0 consolidation: all standalone skills -> /impeccable sub-commands35'adapt',36'animate',37'audit',38'bolder',39'clarify',40'colorize',41'critique',42'delight',43'distill',44'harden',45'layout',46'optimize',47'overdrive',48'polish',49'quieter',50'shape',51'typeset',52];5354// All known harness directories that may contain a skills/ subfolder.55const HARNESS_DIRS = [56'.claude', '.cursor', '.gemini', '.codex', '.agents',57'.trae', '.trae-cn', '.pi', '.opencode', '.kiro', '.rovodev',58];5960// Per-skill fingerprints for SKILL.md bodies that never mentioned61// "impeccable" in their v2.x source. Used as a last-resort match62// when no skills-lock.json exists and the word heuristic fails.63// The strings are lifted verbatim from the v2.x frontmatter64// descriptions, so collisions with hand-written user skills are65// vanishingly unlikely.66const SKILL_FINGERPRINTS = {67harden: 'Make interfaces production-ready: error handling, empty states',68optimize: 'Diagnoses and fixes UI performance across loading speed',69};7071/**72* Walk up from startDir until we find a directory that looks like a73* project root (has package.json, .git, or skills-lock.json).74*/75export function findProjectRoot(startDir = process.cwd()) {76let dir = resolve(startDir);77const { root } = { root: '/' };78while (dir !== root) {79if (80existsSync(join(dir, 'package.json')) ||81existsSync(join(dir, '.git')) ||82existsSync(join(dir, 'skills-lock.json'))83) {84return dir;85}86const parent = resolve(dir, '..');87if (parent === dir) break;88dir = parent;89}90return resolve(startDir);91}9293/**94* Load skills-lock.json from the project root, or null if missing/unreadable.95*/96export function loadLock(projectRoot) {97const lockPath = join(projectRoot, 'skills-lock.json');98if (!existsSync(lockPath)) return null;99try {100return JSON.parse(readFileSync(lockPath, 'utf-8'));101} catch {102return null;103}104}105106/**107* Check whether a skill directory belongs to Impeccable. Three layered108* signals, in order of reliability:109* 1. Lock source equals "pbakaus/impeccable" (authoritative).110* 2. SKILL.md body contains the word "impeccable".111* 3. SKILL.md body contains a per-skill fingerprint (for harden and112* optimize, whose v2.x SKILL.md never mentioned the pack name).113*/114export function isImpeccableSkill(skillDir, { skillName, lock } = {}) {115// 1. Authoritative: the lock file claims this skill is ours.116if (skillName && lock?.skills?.[skillName]?.source === 'pbakaus/impeccable') {117return true;118}119const skillMd = join(skillDir, 'SKILL.md');120if (!existsSync(skillMd)) return false;121let content;122try {123content = readFileSync(skillMd, 'utf-8');124} catch {125return false;126}127// 2. Word-level content heuristic.128if (/impeccable/i.test(content)) return true;129// 3. Per-skill fingerprint for old skills that never mentioned the pack.130// Strip the i- prefix so both `harden` and `i-harden` resolve to the131// same fingerprint entry.132const unprefixed = skillName?.startsWith('i-') ? skillName.slice(2) : skillName;133const fingerprint = unprefixed && SKILL_FINGERPRINTS[unprefixed];134if (fingerprint && content.includes(fingerprint)) return true;135return false;136}137138/**139* Build the full list of names to check: each deprecated name, plus140* its i-prefixed variant.141*/142export function buildTargetNames() {143const names = [];144for (const name of DEPRECATED_NAMES) {145names.push(name);146names.push(`i-${name}`);147}148return names;149}150151/**152* Find every skills directory across all harness dirs in the project.153* Returns absolute paths that exist on disk.154*/155export function findSkillsDirs(projectRoot) {156const dirs = [];157for (const harness of HARNESS_DIRS) {158const candidate = join(projectRoot, harness, 'skills');159if (existsSync(candidate)) {160dirs.push(candidate);161}162}163return dirs;164}165166/**167* Remove deprecated skill directories/symlinks from all harness dirs.168* Reads skills-lock.json so the authoritative "source" field can169* drive deletion even when SKILL.md never mentions impeccable.170* Returns an array of paths that were deleted.171*/172export function removeDeprecatedSkills(projectRoot, lock) {173if (lock === undefined) lock = loadLock(projectRoot);174const targets = buildTargetNames();175const skillsDirs = findSkillsDirs(projectRoot);176const deleted = [];177178for (const skillsDir of skillsDirs) {179for (const name of targets) {180const skillPath = join(skillsDir, name);181182// Use lstat to detect symlinks (existsSync follows symlinks and183// returns false for dangling ones).184let stat;185try {186stat = lstatSync(skillPath);187} catch {188continue; // does not exist at all189}190191if (stat.isSymbolicLink()) {192// Symlink: check the target if it's alive, otherwise treat193// dangling symlinks to deprecated names as safe to remove.194const targetAlive = existsSync(skillPath);195const isMatch = targetAlive196? isImpeccableSkill(skillPath, { skillName: name, lock })197: true;198if (isMatch) {199unlinkSync(skillPath);200deleted.push(skillPath);201}202continue;203}204205// Regular directory -- verify it belongs to impeccable206if (isImpeccableSkill(skillPath, { skillName: name, lock })) {207rmSync(skillPath, { recursive: true, force: true });208deleted.push(skillPath);209}210}211}212213return deleted;214}215216/**217* Remove deprecated entries from skills-lock.json.218* Only removes entries whose source is "pbakaus/impeccable".219* Returns the list of removed skill names.220*/221export function cleanSkillsLock(projectRoot) {222const lockPath = join(projectRoot, 'skills-lock.json');223if (!existsSync(lockPath)) return [];224225let lock;226try {227lock = JSON.parse(readFileSync(lockPath, 'utf-8'));228} catch {229return [];230}231232if (!lock.skills || typeof lock.skills !== 'object') return [];233234const targets = buildTargetNames();235const removed = [];236237for (const name of targets) {238const entry = lock.skills[name];239if (!entry) continue;240// Only remove if it belongs to impeccable241if (entry.source === 'pbakaus/impeccable') {242delete lock.skills[name];243removed.push(name);244}245}246247if (removed.length > 0) {248writeFileSync(lockPath, JSON.stringify(lock, null, 2) + '\n', 'utf-8');249}250251return removed;252}253254/**255* Run the full cleanup. Returns a summary object.256*257* Order matters: read the lock and delete directories first, then258* strip lock entries. Otherwise the authoritative signal is gone by259* the time directory deletion runs.260*/261export function cleanup(projectRoot) {262const root = projectRoot || findProjectRoot();263const lock = loadLock(root);264const deletedPaths = removeDeprecatedSkills(root, lock);265const removedLockEntries = cleanSkillsLock(root);266return { deletedPaths, removedLockEntries, projectRoot: root };267}268269// CLI entry point270if (process.argv[1] && resolve(process.argv[1]) === resolve(new URL(import.meta.url).pathname)) {271const result = cleanup();272if (result.deletedPaths.length === 0 && result.removedLockEntries.length === 0) {273console.log('No deprecated Impeccable skills found. Nothing to clean up.');274} else {275if (result.deletedPaths.length > 0) {276console.log(`Removed ${result.deletedPaths.length} deprecated skill(s):`);277for (const p of result.deletedPaths) console.log(` - ${p}`);278}279if (result.removedLockEntries.length > 0) {280console.log(`Cleaned ${result.removedLockEntries.length} entry/entries from skills-lock.json:`);281for (const name of result.removedLockEntries) console.log(` - ${name}`);282}283}284}285