Files
pbakaus_impeccable/tests/cleanup-deprecated.test.mjs
T
Paul BakausandClaude Opus 4.6 b0f44f83c6 Consolidate 18 skills into 1 /impeccable skill with 20 commands
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>
2026-04-10 19:45:17 -07:00

237 lines
9.1 KiB
JavaScript

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,
} from '../source/skills/impeccable/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);
});
});
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('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);
});
});
});