fix: flatten Claude skill installs (#2582)

Flatten managed Claude skill destinations, preserve user-owned conflicts, and migrate legacy nested installs through the lifecycle tooling.
This commit is contained in:
Affaan Mustafa
2026-07-26 03:20:06 -07:00
committed by GitHub
parent 71438391e8
commit f3afd59045
18 changed files with 1620 additions and 119 deletions
+102 -12
View File
@@ -100,8 +100,8 @@ function runTests() {
assert.ok(fs.existsSync(path.join(claudeRoot, 'commands', 'plan.md')));
assert.ok(fs.existsSync(path.join(claudeRoot, 'scripts', 'hooks', 'session-end.js')));
assert.ok(fs.existsSync(path.join(claudeRoot, 'scripts', 'lib', 'utils.js')));
assert.ok(fs.existsSync(path.join(claudeRoot, 'skills', 'ecc', 'tdd-workflow', 'SKILL.md')));
assert.ok(fs.existsSync(path.join(claudeRoot, 'skills', 'ecc', 'coding-standards', 'SKILL.md')));
assert.ok(fs.existsSync(path.join(claudeRoot, 'skills', 'tdd-workflow', 'SKILL.md')));
assert.ok(fs.existsSync(path.join(claudeRoot, 'skills', 'coding-standards', 'SKILL.md')));
assert.ok(fs.existsSync(path.join(claudeRoot, 'plugin.json')));
const statePath = path.join(homeDir, '.claude', 'ecc', 'install-state.json');
@@ -133,23 +133,23 @@ function runTests() {
assert.strictEqual(result.code, 0, result.stderr);
const claudeRoot = path.join(homeDir, '.claude');
const skillPath = path.join(claudeRoot, 'skills', 'ecc', 'react-patterns', 'SKILL.md');
const skillPath = path.join(claudeRoot, 'skills', 'react-patterns', 'SKILL.md');
assert.ok(fs.existsSync(skillPath), 'react-patterns SKILL.md should be installed');
const content = fs.readFileSync(skillPath, 'utf8');
assert.ok(
content.includes('../../../rules/ecc/react/'),
content.includes('../../rules/ecc/react/'),
'source-relative rules link should be rewritten for the ecc/ namespace'
);
assert.ok(
!content.includes('](../../rules/'),
'no un-namespaced ](../../rules/ links should remain'
!content.includes('](../../rules/react/'),
'no un-namespaced ](../../rules/react/ links should remain'
);
// The rewritten link must resolve to a file that actually exists on disk.
const linkTarget = path.join(
path.dirname(skillPath),
'../../../rules/ecc/react/hooks.md'
'../../rules/ecc/react/hooks.md'
);
assert.ok(fs.existsSync(linkTarget), 'rewritten link target should exist');
} finally {
@@ -468,11 +468,101 @@ function runTests() {
const result = run(['--profile', 'core'], { cwd: projectDir, homeDir });
assert.strictEqual(result.code, 0, result.stderr);
assert.ok(result.stdout.includes('user-owned'), result.stdout);
assert.ok(result.stdout.includes('Skipped operations:'), result.stdout);
assert.strictEqual(fs.readFileSync(userRulePath, 'utf8'), '# User custom rule\n');
assert.strictEqual(fs.readFileSync(userSkillPath, 'utf8'), '# User custom skill\n');
assert.ok(fs.existsSync(path.join(claudeRoot, 'rules', 'ecc', 'common', 'coding-style.md')));
assert.ok(fs.existsSync(path.join(claudeRoot, 'skills', 'ecc', 'tdd-workflow', 'SKILL.md')));
assert.ok(fs.existsSync(path.join(claudeRoot, 'skills', 'verification-loop', 'SKILL.md')));
const state = readJson(path.join(claudeRoot, 'ecc', 'install-state.json'));
assert.ok(!state.operations.some(operation => (
operation.destinationPath.startsWith(path.join(claudeRoot, 'skills', 'tdd-workflow'))
)));
} finally {
cleanup(homeDir);
cleanup(projectDir);
}
})) passed++; else failed++;
if (test('reports applied and skipped user-owned Claude skill operations in JSON', () => {
const homeDir = createTempDir('install-apply-home-');
const projectDir = createTempDir('install-apply-project-');
try {
const userSkillPath = path.join(
homeDir,
'.claude',
'skills',
'tdd-workflow',
'SKILL.md'
);
fs.mkdirSync(path.dirname(userSkillPath), { recursive: true });
fs.writeFileSync(userSkillPath, '# User custom skill\n');
const result = run(['--skills', 'tdd-workflow', '--json'], {
cwd: projectDir,
homeDir,
});
assert.strictEqual(result.code, 0, result.stderr);
const payload = JSON.parse(result.stdout);
assert.strictEqual(payload.dryRun, false);
assert.ok(payload.result.plannedOperations.length > 0);
assert.ok(payload.result.operations.length > 0);
assert.ok(payload.result.skippedOperations.length > 0);
assert.strictEqual(
payload.result.operations.length + payload.result.skippedOperations.length,
payload.result.plannedOperations.length
);
assert.ok(payload.result.skippedOperations.every(operation => (
operation.destinationPath.startsWith(path.dirname(userSkillPath))
)));
assert.ok(!payload.result.operations.some(operation => (
operation.destinationPath.startsWith(path.dirname(userSkillPath))
)));
assert.ok(payload.result.warnings.some(warning => warning.includes('user-owned')));
assert.strictEqual(fs.readFileSync(userSkillPath, 'utf8'), '# User custom skill\n');
} finally {
cleanup(homeDir);
cleanup(projectDir);
}
})) passed++; else failed++;
if (test('dry-run reports the same user-owned Claude skill conflicts as apply', () => {
const homeDir = createTempDir('install-apply-home-');
const projectDir = createTempDir('install-apply-project-');
try {
const userSkillRoot = path.join(
homeDir,
'.claude',
'skills',
'tdd-workflow'
);
const userSkillPath = path.join(userSkillRoot, 'SKILL.md');
fs.mkdirSync(userSkillRoot, { recursive: true });
fs.writeFileSync(userSkillPath, '# User custom skill\n');
const result = run(
['--skills', 'tdd-workflow', '--dry-run', '--json'],
{ cwd: projectDir, homeDir }
);
assert.strictEqual(result.code, 0, result.stderr);
const payload = JSON.parse(result.stdout);
assert.strictEqual(payload.dryRun, true);
assert.ok(payload.plan.plannedOperations.length > 0);
assert.ok(payload.plan.skippedOperations.length > 0);
assert.ok(payload.plan.warnings.some(warning => warning.includes('user-owned')));
assert.ok(payload.plan.skippedOperations.every(operation => (
operation.destinationPath.startsWith(userSkillRoot)
)));
assert.ok(!payload.plan.operations.some(operation => (
operation.destinationPath.startsWith(userSkillRoot)
)));
assert.strictEqual(fs.readFileSync(userSkillPath, 'utf8'), '# User custom skill\n');
assert.ok(!fs.existsSync(path.join(homeDir, '.claude', 'ecc', 'install-state.json')));
} finally {
cleanup(homeDir);
cleanup(projectDir);
@@ -893,8 +983,8 @@ function runTests() {
const result = run(['--config', configPath], { cwd: projectDir, homeDir });
assert.strictEqual(result.code, 0, result.stderr);
assert.ok(fs.existsSync(path.join(homeDir, '.claude', 'skills', 'ecc', 'security-review', 'SKILL.md')));
assert.ok(!fs.existsSync(path.join(homeDir, '.claude', 'skills', 'ecc', 'dmux-workflows', 'SKILL.md')));
assert.ok(fs.existsSync(path.join(homeDir, '.claude', 'skills', 'security-review', 'SKILL.md')));
assert.ok(!fs.existsSync(path.join(homeDir, '.claude', 'skills', 'dmux-workflows', 'SKILL.md')));
const state = readJson(path.join(homeDir, '.claude', 'ecc', 'install-state.json'));
assert.strictEqual(state.request.profile, 'developer');
@@ -925,8 +1015,8 @@ function runTests() {
const result = run([], { cwd: projectDir, homeDir });
assert.strictEqual(result.code, 0, result.stderr);
assert.ok(fs.existsSync(path.join(homeDir, '.claude', 'skills', 'ecc', 'security-review', 'SKILL.md')));
assert.ok(!fs.existsSync(path.join(homeDir, '.claude', 'skills', 'ecc', 'dmux-workflows', 'SKILL.md')));
assert.ok(fs.existsSync(path.join(homeDir, '.claude', 'skills', 'security-review', 'SKILL.md')));
assert.ok(!fs.existsSync(path.join(homeDir, '.claude', 'skills', 'dmux-workflows', 'SKILL.md')));
const state = readJson(path.join(homeDir, '.claude', 'ecc', 'install-state.json'));
assert.strictEqual(state.request.profile, 'developer');