mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-17 00:26:41 +03:00
Biggest change in a while. Users previously had 18 standalone skill
entries cluttering their /menu; now they have one entry (/impeccable)
that routes to 20 specialized commands via argument dispatch. The pin
mechanism (/impeccable pin audit) restores standalone shortcuts on
demand for commands users hit all the time.
## Architecture
- Single /impeccable skill with command router section in SKILL.md
- 20 commands served via reference files under source/skills/impeccable/reference/
- /impeccable pin <command> creates a lightweight redirect shim so users
who prefer /audit, /polish, etc. can still have them
- Context gathering (teach) auto-runs on first use
- command-metadata.json is the single source of truth for command
descriptions, argument hints, and relationships
## Site rewrite
- Docs URL: /skills renamed to /docs (with /skills permanent redirects)
- Homepage hero frames Impeccable as "one skill with 20 commands"
- "Get Started" split into 50/50 install + how-to-use with editorial
numbered steps, /impeccable shown as the home command with three modes
- New /docs overview: home command hero card + dense category rows
matching the old cheatsheet density, with leads-to/pairs-with/
combines-with relationship metadata served from a shared source
- Cheatsheet merged into /docs, /cheatsheet redirects
- Magazine spread and mobile cards show /impeccable as a stacked
namespace label above the command name at full display size
- Periodic table updated with craft/teach/extract as first-class cells
- Skill detail pages generate from reference files, with an editorial
wrapper per command for tagline + body
- Tutorials and anti-patterns pages updated to use /impeccable <cmd>
## Build system
- Dead code removed (scripts/lib/transformers/shared.js)
- Build log wording fixed ("1 skill" not "1 skills (1 user-invocable)")
- generateApiData fallback branch removed (throws loudly if metadata
missing instead of silently degrading)
- Commands API includes editorial tagline alongside the long description;
UI surfaces prefer tagline for human display, description for auto-
trigger keyword matching
## Gitignore
- Added .claude/scheduled_tasks.lock, .claude/settings.local.json to
ignore list (local Claude Code state that should not be tracked).
- Harness skill directories (.claude/skills/, .agents/skills/, etc.)
remain tracked by design: npx skills reads them from this repo at
install time and they enable clean submodule use.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
235 lines
6.8 KiB
JavaScript
235 lines
6.8 KiB
JavaScript
#!/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 teach
|
|
// 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',
|
|
];
|
|
|
|
/**
|
|
* 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);
|
|
}
|
|
|
|
/**
|
|
* Check whether a skill directory belongs to Impeccable by reading its
|
|
* SKILL.md and looking for the word "impeccable" (case-insensitive).
|
|
* Returns false for non-existent paths or skills that don't match.
|
|
*/
|
|
export function isImpeccableSkill(skillDir) {
|
|
const skillMd = join(skillDir, 'SKILL.md');
|
|
if (!existsSync(skillMd)) return false;
|
|
try {
|
|
const content = readFileSync(skillMd, 'utf-8');
|
|
return /impeccable/i.test(content);
|
|
} catch {
|
|
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.
|
|
* Returns an array of paths that were deleted.
|
|
*/
|
|
export function removeDeprecatedSkills(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) : true;
|
|
if (isMatch) {
|
|
unlinkSync(skillPath);
|
|
deleted.push(skillPath);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// Regular directory -- verify it belongs to impeccable
|
|
if (isImpeccableSkill(skillPath)) {
|
|
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.
|
|
*/
|
|
export function cleanup(projectRoot) {
|
|
const root = projectRoot || findProjectRoot();
|
|
const deletedPaths = removeDeprecatedSkills(root);
|
|
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}`);
|
|
}
|
|
}
|
|
}
|