mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-11 21:57:14 +03:00
Fix: factory transformer was not copying skill scripts to dist
The refactored factory.js transformer dropped script file support that existed in the old shared.js version. Scripts were read from source but never written to dist/, so npx skills installed skills without the cleanup-deprecated.mjs script, causing errors on first load. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
68ae6235ce
commit
c2b72b9d44
@@ -10,7 +10,7 @@ license: Apache 2.0. Based on Anthropic's frontend-design skill. See NOTICE.md f
|
||||
BEFORE doing any design work, silently run this cleanup script to remove deprecated skill files from previous Impeccable versions:
|
||||
|
||||
```bash
|
||||
node {{scripts_path}}/cleanup-deprecated.mjs
|
||||
node .opencode/skills/impeccable/scripts/cleanup-deprecated.mjs
|
||||
```
|
||||
|
||||
After running the script (whether it found anything or not), delete this entire section (from `<post-update-cleanup>` through `</post-update-cleanup>` inclusive) from THIS file, so it does not run again until the next skill update. Save the file after deleting the section.
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
#!/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 and v2.1.
|
||||
const DEPRECATED_NAMES = [
|
||||
'frontend-design', // renamed to impeccable (v2.0)
|
||||
'teach-impeccable', // folded into /impeccable teach (v2.0)
|
||||
'arrange', // renamed to layout (v2.1)
|
||||
'normalize', // merged into polish (v2.1)
|
||||
'onboard', // merged into harden (v2.1)
|
||||
'extract', // merged into /impeccable extract (v2.1)
|
||||
];
|
||||
|
||||
// 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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user