fix: namespace claude managed install paths

This commit is contained in:
Affaan Mustafa
2026-04-30 07:43:20 -04:00
committed by Affaan Mustafa
parent 08d6c82989
commit e381c8d8a8
10 changed files with 243 additions and 53 deletions

View File

@@ -79,9 +79,11 @@ function writeManifestSourceFixture(root) {
kind: 'fixture',
description: 'Fixture module',
paths: [
'rules',
'src',
'standalone.txt',
'missing.txt',
'skills/demo',
path.join('runtime', 'ecc', 'install-state.json'),
'.claude-plugin',
],
@@ -107,6 +109,8 @@ function writeManifestSourceFixture(root) {
writeFile(root, path.join('src', 'node_modules', 'ignored.js'), 'console.log("ignored");\n');
writeFile(root, path.join('src', '.git', 'ignored.js'), 'console.log("ignored");\n');
writeFile(root, path.join('src', 'nested', 'ecc-install-state.json'), '{}\n');
writeFile(root, path.join('rules', 'common', 'coding-style.md'), '# Common\n');
writeFile(root, path.join('skills', 'demo', 'SKILL.md'), '# Demo\n');
writeFile(root, 'standalone.txt', 'standalone\n');
writeFile(root, path.join('runtime', 'ecc', 'install-state.json'), '{}\n');
writeJson(root, path.join('.claude-plugin', 'plugin.json'), { name: 'fixture' });
@@ -194,6 +198,35 @@ function runTests() {
}
})) passed++; else failed++;
if (test('plans Claude legacy rules under the default ECC-managed rules directory', () => {
const sourceRoot = createTempDir('install-executor-source-');
const homeDir = createTempDir('install-executor-home-');
const projectRoot = createTempDir('install-executor-project-');
try {
writeLegacySourceFixture(sourceRoot);
writeFile(homeDir, path.join('.claude', 'rules', 'common', 'coding-style.md'), '# User custom rule\n');
const plan = createLegacyInstallPlan({
sourceRoot,
homeDir,
projectRoot,
target: 'claude',
languages: ['typescript'],
});
const managedRulesDir = path.join(homeDir, '.claude', 'rules', 'ecc');
assert.strictEqual(plan.installRoot, managedRulesDir);
assert.ok(operationFor(plan, path.join('.claude', 'rules', 'ecc', 'common', 'coding-style.md')));
assert.ok(operationFor(plan, path.join('.claude', 'rules', 'ecc', 'typescript', 'testing.md')));
assert.ok(!operationFor(plan, path.join('.claude', 'rules', 'common', 'coding-style.md')));
assert.ok(!plan.warnings.some(warning => warning.includes('files may be overwritten')));
} finally {
cleanup(sourceRoot);
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('plans Cursor legacy assets and JSON merge payloads', () => {
const sourceRoot = createTempDir('install-executor-source-');
const projectRoot = createTempDir('install-executor-project-');
@@ -307,6 +340,8 @@ function runTests() {
));
assert.ok(normalizedSources.includes('src/app.js'));
assert.ok(normalizedSources.includes('src/nested/feature.js'));
assert.ok(normalizedSources.includes('rules/common/coding-style.md'));
assert.ok(normalizedSources.includes('skills/demo/SKILL.md'));
assert.ok(normalizedSources.includes('standalone.txt'));
assert.ok(normalizedSources.includes('.claude-plugin/plugin.json'));
assert.ok(!normalizedSources.includes('missing.txt'));
@@ -318,6 +353,14 @@ function runTests() {
operation.sourceRelativePath === path.join('.claude-plugin', 'plugin.json')
&& operation.destinationPath === path.join(homeDir, '.claude', 'plugin.json')
)));
assert.ok(plan.operations.some(operation => (
operation.sourceRelativePath === path.join('rules', 'common', 'coding-style.md')
&& operation.destinationPath === path.join(homeDir, '.claude', 'rules', 'ecc', 'common', 'coding-style.md')
)));
assert.ok(plan.operations.some(operation => (
operation.sourceRelativePath === path.join('skills', 'demo', 'SKILL.md')
&& operation.destinationPath === path.join(homeDir, '.claude', 'skills', 'ecc', 'demo', 'SKILL.md')
)));
assert.deepStrictEqual(plan.warnings, ['fixture warning']);
assert.strictEqual(plan.statePreview.request.profile, 'minimal');
assert.deepStrictEqual(plan.statePreview.request.includeComponents, ['capability:fixture']);
@@ -369,6 +412,8 @@ function runTests() {
const applied = applyInstallPlan(plan);
assert.strictEqual(applied.applied, true);
assert.ok(fs.existsSync(path.join(homeDir, '.claude', 'rules', 'ecc', 'common', 'coding-style.md')));
assert.ok(fs.existsSync(path.join(homeDir, '.claude', 'skills', 'ecc', 'demo', 'SKILL.md')));
assert.ok(fs.existsSync(path.join(homeDir, '.claude', 'src', 'app.js')));
assert.ok(fs.existsSync(path.join(homeDir, '.claude', 'standalone.txt')));
assert.ok(fs.existsSync(path.join(homeDir, '.claude', 'plugin.json')));

View File

@@ -65,6 +65,42 @@ function runTests() {
assert.strictEqual(statePath, path.join(homeDir, '.claude', 'ecc', 'install-state.json'));
})) passed++; else failed++;
if (test('plans claude rules and skills under ECC-managed subdirectories', () => {
const repoRoot = path.join(__dirname, '..', '..');
const homeDir = '/Users/example';
const plan = planInstallTargetScaffold({
target: 'claude',
repoRoot,
homeDir,
modules: [
{
id: 'rules-core',
paths: ['rules'],
},
{
id: 'workflow-quality',
paths: ['skills/tdd-workflow'],
},
],
});
assert.ok(
plan.operations.some(operation => (
normalizedRelativePath(operation.sourceRelativePath) === 'rules'
&& operation.destinationPath === path.join(homeDir, '.claude', 'rules', 'ecc')
)),
'Should install bundled Claude rules under rules/ecc'
);
assert.ok(
plan.operations.some(operation => (
normalizedRelativePath(operation.sourceRelativePath) === 'skills/tdd-workflow'
&& operation.destinationPath === path.join(homeDir, '.claude', 'skills', 'ecc', 'tdd-workflow')
)),
'Should install bundled Claude skills under skills/ecc'
);
})) passed++; else failed++;
if (test('plans scaffold operations and flattens native target roots', () => {
const repoRoot = path.join(__dirname, '..', '..');
const projectRoot = '/workspace/app';

View File

@@ -576,10 +576,10 @@ function runTests() {
const claudeRoot = path.join(homeDir, '.claude');
// Security skill should be installed (from --with)
assert.ok(fs.existsSync(path.join(claudeRoot, 'skills', 'security-review', 'SKILL.md')),
assert.ok(fs.existsSync(path.join(claudeRoot, 'skills', 'ecc', 'security-review', 'SKILL.md')),
'Should install security-review skill from --with');
// Core profile modules should be installed
assert.ok(fs.existsSync(path.join(claudeRoot, 'rules', 'common', 'coding-style.md')),
assert.ok(fs.existsSync(path.join(claudeRoot, 'rules', 'ecc', 'common', 'coding-style.md')),
'Should install core rules');
// Install state should record include/exclude
@@ -615,12 +615,12 @@ function runTests() {
const claudeRoot = path.join(homeDir, '.claude');
// Orchestration skills should NOT be installed (from --without)
assert.ok(!fs.existsSync(path.join(claudeRoot, 'skills', 'dmux-workflows', 'SKILL.md')),
assert.ok(!fs.existsSync(path.join(claudeRoot, 'skills', 'ecc', 'dmux-workflows', 'SKILL.md')),
'Should not install orchestration skills');
// Developer profile base modules should be installed
assert.ok(fs.existsSync(path.join(claudeRoot, 'rules', 'common', 'coding-style.md')),
assert.ok(fs.existsSync(path.join(claudeRoot, 'rules', 'ecc', 'common', 'coding-style.md')),
'Should install core rules');
assert.ok(fs.existsSync(path.join(claudeRoot, 'skills', 'tdd-workflow', 'SKILL.md')),
assert.ok(fs.existsSync(path.join(claudeRoot, 'skills', 'ecc', 'tdd-workflow', 'SKILL.md')),
'Should install workflow skills');
const statePath = path.join(claudeRoot, 'ecc', 'install-state.json');
@@ -653,10 +653,10 @@ function runTests() {
const claudeRoot = path.join(homeDir, '.claude');
// framework-language skill (from lang:typescript) should be installed
assert.ok(fs.existsSync(path.join(claudeRoot, 'skills', 'coding-standards', 'SKILL.md')),
assert.ok(fs.existsSync(path.join(claudeRoot, 'skills', 'ecc', 'coding-standards', 'SKILL.md')),
'Should install framework-language skills');
// Its dependencies should be installed
assert.ok(fs.existsSync(path.join(claudeRoot, 'rules', 'common', 'coding-style.md')),
assert.ok(fs.existsSync(path.join(claudeRoot, 'rules', 'ecc', 'common', 'coding-style.md')),
'Should install dependency rules-core');
const statePath = path.join(claudeRoot, 'ecc', 'install-state.json');

View File

@@ -94,13 +94,13 @@ function runTests() {
assert.strictEqual(result.code, 0, result.stderr);
const claudeRoot = path.join(homeDir, '.claude');
assert.ok(fs.existsSync(path.join(claudeRoot, 'rules', 'common', 'coding-style.md')));
assert.ok(fs.existsSync(path.join(claudeRoot, 'rules', 'typescript', 'testing.md')));
assert.ok(fs.existsSync(path.join(claudeRoot, 'rules', 'ecc', 'common', 'coding-style.md')));
assert.ok(fs.existsSync(path.join(claudeRoot, 'rules', 'ecc', 'typescript', 'testing.md')));
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', 'tdd-workflow', 'SKILL.md')));
assert.ok(fs.existsSync(path.join(claudeRoot, 'skills', 'coding-standards', 'SKILL.md')));
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, 'plugin.json')));
const statePath = path.join(homeDir, '.claude', 'ecc', 'install-state.json');
@@ -113,7 +113,7 @@ function runTests() {
assert.ok(state.resolution.selectedModules.includes('framework-language'));
assert.ok(
state.operations.some(operation => (
operation.destinationPath === path.join(claudeRoot, 'rules', 'common', 'coding-style.md')
operation.destinationPath === path.join(claudeRoot, 'rules', 'ecc', 'common', 'coding-style.md')
)),
'Should record common rule file operation'
);
@@ -299,7 +299,7 @@ function runTests() {
assert.strictEqual(result.code, 0, result.stderr);
const claudeRoot = path.join(homeDir, '.claude');
assert.ok(fs.existsSync(path.join(claudeRoot, 'rules', 'common', 'coding-style.md')));
assert.ok(fs.existsSync(path.join(claudeRoot, 'rules', 'ecc', 'common', 'coding-style.md')));
assert.ok(fs.existsSync(path.join(claudeRoot, 'agents', 'architect.md')));
assert.ok(fs.existsSync(path.join(claudeRoot, 'commands', 'plan.md')));
assert.ok(fs.existsSync(path.join(claudeRoot, 'hooks', 'hooks.json')));
@@ -324,6 +324,32 @@ function runTests() {
}
})) passed++; else failed++;
if (test('preserves existing top-level Claude rules and skills during managed install', () => {
const homeDir = createTempDir('install-apply-home-');
const projectDir = createTempDir('install-apply-project-');
try {
const claudeRoot = path.join(homeDir, '.claude');
const userRulePath = path.join(claudeRoot, 'rules', 'common', 'coding-style.md');
const userSkillPath = path.join(claudeRoot, 'skills', 'tdd-workflow', 'SKILL.md');
fs.mkdirSync(path.dirname(userRulePath), { recursive: true });
fs.mkdirSync(path.dirname(userSkillPath), { recursive: true });
fs.writeFileSync(userRulePath, '# User custom rule\n');
fs.writeFileSync(userSkillPath, '# User custom skill\n');
const result = run(['--profile', 'core'], { cwd: projectDir, homeDir });
assert.strictEqual(result.code, 0, result.stderr);
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')));
} finally {
cleanup(homeDir);
cleanup(projectDir);
}
})) passed++; else failed++;
if (test('installs antigravity manifest profiles while skipping only unsupported modules', () => {
const homeDir = createTempDir('install-apply-home-');
const projectDir = createTempDir('install-apply-project-');
@@ -727,8 +753,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', 'security-review', 'SKILL.md')));
assert.ok(!fs.existsSync(path.join(homeDir, '.claude', 'skills', 'dmux-workflows', 'SKILL.md')));
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')));
const state = readJson(path.join(homeDir, '.claude', 'ecc', 'install-state.json'));
assert.strictEqual(state.request.profile, 'developer');
@@ -759,8 +785,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', 'security-review', 'SKILL.md')));
assert.ok(!fs.existsSync(path.join(homeDir, '.claude', 'skills', 'dmux-workflows', 'SKILL.md')));
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')));
const state = readJson(path.join(homeDir, '.claude', 'ecc', 'install-state.json'));
assert.strictEqual(state.request.profile, 'developer');

View File

@@ -132,6 +132,10 @@ function runTests() {
readme.includes('Start with `rules/common` plus one language or framework pack you actually use.'),
'README should steer users away from copying every rules directory'
);
assert.ok(
readme.includes('~/.claude/rules/ecc/'),
'README should steer plugin-path rules into an ECC-owned namespace'
);
})) passed++; else failed++;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);