This commit is contained in:
parent
739a534ac2
commit
f237916291
165 changed files with 79237 additions and 0 deletions
284
.codex/skills/impeccable/scripts/cleanup-deprecated.mjs
Normal file
284
.codex/skills/impeccable/scripts/cleanup-deprecated.mjs
Normal file
|
|
@ -0,0 +1,284 @@
|
|||
#!/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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue