From e381c8d8a8bbfef1432e8222c5ef1f7676fe4d8c Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Thu, 30 Apr 2026 07:43:20 -0400 Subject: [PATCH] fix: namespace claude managed install paths --- README.md | 63 ++++++++-------- docs/SELECTIVE-INSTALL-ARCHITECTURE.md | 4 +- scripts/install-apply.js | 2 +- scripts/lib/install-executor.js | 3 +- scripts/lib/install-targets/claude-home.js | 79 +++++++++++++++++++- tests/lib/install-executor.test.js | 45 +++++++++++ tests/lib/install-targets.test.js | 36 +++++++++ tests/lib/selective-install.test.js | 14 ++-- tests/scripts/install-apply.test.js | 46 +++++++++--- tests/scripts/install-readme-clarity.test.js | 4 + 10 files changed, 243 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index 6d89fc49..d4559b28 100644 --- a/README.md +++ b/README.md @@ -245,7 +245,7 @@ This is intentional. Anthropic marketplace/plugin installs are keyed by a canoni > > If you already installed ECC via `/plugin install`, **do not run `./install.sh --profile full`, `.\install.ps1 --profile full`, or `npx ecc-install --profile full` afterward**. The plugin already loads ECC skills, commands, and hooks. Running the full installer after a plugin install copies those same surfaces into your user directories and can create duplicate skills plus duplicate runtime behavior. > -> For plugin installs, manually copy only the `rules/` directories you want. Start with `rules/common` plus one language or framework pack you actually use. Do not copy every rules directory unless you explicitly want all of that context in Claude. +> For plugin installs, manually copy only the `rules/` directories you want under `~/.claude/rules/ecc/`. Start with `rules/common` plus one language or framework pack you actually use. Do not copy every rules directory unless you explicitly want all of that context in Claude. > > Use the full installer only when you are doing a fully manual ECC install instead of the plugin path. > @@ -259,10 +259,10 @@ cd everything-claude-code # Install dependencies (pick your package manager) npm install # or: pnpm install | yarn install | bun install -# Plugin install path: copy only rules -mkdir -p ~/.claude/rules -cp -R rules/common ~/.claude/rules/ -cp -R rules/typescript ~/.claude/rules/ +# Plugin install path: copy only ECC rules into an ECC-owned namespace +mkdir -p ~/.claude/rules/ecc +cp -R rules/common ~/.claude/rules/ecc/ +cp -R rules/typescript ~/.claude/rules/ecc/ # Fully manual ECC install path (use this instead of /plugin install) # ./install.sh --profile full @@ -271,10 +271,10 @@ cp -R rules/typescript ~/.claude/rules/ ```powershell # Windows PowerShell -# Plugin install path: copy only rules -New-Item -ItemType Directory -Force -Path "$HOME/.claude/rules" | Out-Null -Copy-Item -Recurse rules/common "$HOME/.claude/rules/" -Copy-Item -Recurse rules/typescript "$HOME/.claude/rules/" +# Plugin install path: copy only ECC rules into an ECC-owned namespace +New-Item -ItemType Directory -Force -Path "$HOME/.claude/rules/ecc" | Out-Null +Copy-Item -Recurse rules/common "$HOME/.claude/rules/ecc/" +Copy-Item -Recurse rules/typescript "$HOME/.claude/rules/ecc/" # Fully manual ECC install path (use this instead of /plugin install) # .\install.ps1 --profile full @@ -303,7 +303,7 @@ If you choose this path, stop there. Do not also run `/plugin install`. If ECC feels duplicated, intrusive, or broken, do not keep reinstalling it on top of itself. -- **Plugin path:** remove the plugin from Claude Code, then delete the specific rule folders you manually copied under `~/.claude/rules/`. +- **Plugin path:** remove the plugin from Claude Code, then delete the specific rule folders you manually copied under `~/.claude/rules/ecc/`. - **Manual installer / CLI path:** from the repo root, preview removal first: ```bash @@ -574,7 +574,7 @@ everything-claude-code/ | |-- verify.md # /verify - Prefer the verification-loop skill | |-- orchestrate.md # /orchestrate - Prefer dmux-workflows or multi-workflow | -|-- rules/ # Always-follow guidelines (copy to ~/.claude/rules/) +|-- rules/ # Always-follow guidelines (copy to ~/.claude/rules/ecc/) | |-- README.md # Structure overview and installation guide | |-- common/ # Language-agnostic principles | | |-- coding-style.md # Immutability, file organization @@ -791,17 +791,17 @@ This gives you instant access to all commands, agents, skills, and hooks. > git clone https://github.com/affaan-m/everything-claude-code.git > > # Option A: User-level rules (applies to all projects) -> mkdir -p ~/.claude/rules -> cp -r everything-claude-code/rules/common ~/.claude/rules/ -> cp -r everything-claude-code/rules/typescript ~/.claude/rules/ # pick your stack -> cp -r everything-claude-code/rules/python ~/.claude/rules/ -> cp -r everything-claude-code/rules/golang ~/.claude/rules/ -> cp -r everything-claude-code/rules/php ~/.claude/rules/ +> mkdir -p ~/.claude/rules/ecc +> cp -r everything-claude-code/rules/common ~/.claude/rules/ecc/ +> cp -r everything-claude-code/rules/typescript ~/.claude/rules/ecc/ # pick your stack +> cp -r everything-claude-code/rules/python ~/.claude/rules/ecc/ +> cp -r everything-claude-code/rules/golang ~/.claude/rules/ecc/ +> cp -r everything-claude-code/rules/php ~/.claude/rules/ecc/ > > # Option B: Project-level rules (applies to current project only) -> mkdir -p .claude/rules -> cp -r everything-claude-code/rules/common .claude/rules/ -> cp -r everything-claude-code/rules/typescript .claude/rules/ # pick your stack +> mkdir -p .claude/rules/ecc +> cp -r everything-claude-code/rules/common .claude/rules/ecc/ +> cp -r everything-claude-code/rules/typescript .claude/rules/ecc/ # pick your stack > ``` --- @@ -818,21 +818,22 @@ git clone https://github.com/affaan-m/everything-claude-code.git cp everything-claude-code/agents/*.md ~/.claude/agents/ # Copy rules directories (common + language-specific) -mkdir -p ~/.claude/rules -cp -r everything-claude-code/rules/common ~/.claude/rules/ -cp -r everything-claude-code/rules/typescript ~/.claude/rules/ # pick your stack -cp -r everything-claude-code/rules/python ~/.claude/rules/ -cp -r everything-claude-code/rules/golang ~/.claude/rules/ -cp -r everything-claude-code/rules/php ~/.claude/rules/ +mkdir -p ~/.claude/rules/ecc +cp -r everything-claude-code/rules/common ~/.claude/rules/ecc/ +cp -r everything-claude-code/rules/typescript ~/.claude/rules/ecc/ # pick your stack +cp -r everything-claude-code/rules/python ~/.claude/rules/ecc/ +cp -r everything-claude-code/rules/golang ~/.claude/rules/ecc/ +cp -r everything-claude-code/rules/php ~/.claude/rules/ecc/ # Copy skills first (primary workflow surface) # Recommended (new users): core/general skills only -cp -r everything-claude-code/.agents/skills/* ~/.claude/skills/ -cp -r everything-claude-code/skills/search-first ~/.claude/skills/ +mkdir -p ~/.claude/skills/ecc +cp -r everything-claude-code/.agents/skills/* ~/.claude/skills/ecc/ +cp -r everything-claude-code/skills/search-first ~/.claude/skills/ecc/ # Optional: add niche/framework-specific skills only when needed # for s in django-patterns django-tdd laravel-patterns springboot-patterns; do -# cp -r everything-claude-code/skills/$s ~/.claude/skills/ +# cp -r everything-claude-code/skills/$s ~/.claude/skills/ecc/ # done # Optional: keep maintained slash-command compatibility during migration @@ -1059,8 +1060,8 @@ Yes. Use Option 2 (manual installation) and copy only what you need: cp everything-claude-code/agents/*.md ~/.claude/agents/ # Just rules -mkdir -p ~/.claude/rules/ -cp -r everything-claude-code/rules/common ~/.claude/rules/ +mkdir -p ~/.claude/rules/ecc/ +cp -r everything-claude-code/rules/common ~/.claude/rules/ecc/ ``` Each component is fully independent. diff --git a/docs/SELECTIVE-INSTALL-ARCHITECTURE.md b/docs/SELECTIVE-INSTALL-ARCHITECTURE.md index 1e9f2bc2..c412cb25 100644 --- a/docs/SELECTIVE-INSTALL-ARCHITECTURE.md +++ b/docs/SELECTIVE-INSTALL-ARCHITECTURE.md @@ -640,7 +640,7 @@ Suggested operation shape: "kind": "copy", "moduleId": "rules-core", "source": "rules/common/coding-style.md", - "destination": "/Users/example/.claude/rules/common/coding-style.md", + "destination": "/Users/example/.claude/rules/ecc/common/coding-style.md", "ownership": "managed", "overwritePolicy": "replace" } @@ -711,7 +711,7 @@ Suggested payload: { "kind": "copy", "moduleId": "rules-core", - "destination": "/Users/example/.claude/rules/common/coding-style.md", + "destination": "/Users/example/.claude/rules/ecc/common/coding-style.md", "digest": "sha256:..." } ] diff --git a/scripts/install-apply.js b/scripts/install-apply.js index 57d5e775..c4a531b4 100755 --- a/scripts/install-apply.js +++ b/scripts/install-apply.js @@ -27,7 +27,7 @@ Usage: install.sh [--target <${LEGACY_INSTALL_TARGETS.join('|')}>] [--dry-run] [ install.sh [--dry-run] [--json] --config Targets: - claude (default) - Install ECC into ~/.claude/ (hooks, commands, agents, rules, skills) + claude (default) - Install ECC into ~/.claude/ with managed rules/skills under rules/ecc and skills/ecc cursor - Install rules, hooks, and bundled Cursor configs to ./.cursor/ antigravity - Install rules, workflows, skills, and agents to ./.agent/ diff --git a/scripts/lib/install-executor.js b/scripts/lib/install-executor.js index cee65085..d7a5e4ee 100644 --- a/scripts/lib/install-executor.js +++ b/scripts/lib/install-executor.js @@ -14,6 +14,7 @@ const { const { getInstallTargetAdapter } = require('./install-targets/registry'); const LANGUAGE_NAME_PATTERN = /^[a-zA-Z0-9_-]+$/; +const CLAUDE_ECC_NAMESPACE = 'ecc'; const EXCLUDED_GENERATED_SOURCE_SUFFIXES = [ '/ecc-install-state.json', '/ecc/install-state.json', @@ -264,7 +265,7 @@ function isDirectoryNonEmpty(dirPath) { function planClaudeLegacyInstall(context) { const adapter = getInstallTargetAdapter('claude'); const targetRoot = adapter.resolveRoot({ homeDir: context.homeDir }); - const rulesDir = context.claudeRulesDir || path.join(targetRoot, 'rules'); + const rulesDir = context.claudeRulesDir || path.join(targetRoot, 'rules', CLAUDE_ECC_NAMESPACE); const installStatePath = adapter.getInstallStatePath({ homeDir: context.homeDir }); const operations = []; const warnings = []; diff --git a/scripts/lib/install-targets/claude-home.js b/scripts/lib/install-targets/claude-home.js index 03e0b4ef..230c0b7b 100644 --- a/scripts/lib/install-targets/claude-home.js +++ b/scripts/lib/install-targets/claude-home.js @@ -1,4 +1,46 @@ -const { createInstallTargetAdapter } = require('./helpers'); +const path = require('path'); + +const { + createInstallTargetAdapter, + createRemappedOperation, + isForeignPlatformPath, + normalizeRelativePath, +} = require('./helpers'); + +const CLAUDE_ECC_NAMESPACE = 'ecc'; + +function getClaudeManagedDestinationPath(adapter, sourceRelativePath, input) { + const normalizedSourcePath = normalizeRelativePath(sourceRelativePath); + const targetRoot = adapter.resolveRoot(input); + + if (normalizedSourcePath === 'rules') { + return path.join(targetRoot, 'rules', CLAUDE_ECC_NAMESPACE); + } + + if (normalizedSourcePath.startsWith('rules/')) { + return path.join( + targetRoot, + 'rules', + CLAUDE_ECC_NAMESPACE, + normalizedSourcePath.slice('rules/'.length) + ); + } + + if (normalizedSourcePath === 'skills') { + return path.join(targetRoot, 'skills', CLAUDE_ECC_NAMESPACE); + } + + if (normalizedSourcePath.startsWith('skills/')) { + return path.join( + targetRoot, + 'skills', + CLAUDE_ECC_NAMESPACE, + normalizedSourcePath.slice('skills/'.length) + ); + } + + return null; +} module.exports = createInstallTargetAdapter({ id: 'claude-home', @@ -7,4 +49,39 @@ module.exports = createInstallTargetAdapter({ rootSegments: ['.claude'], installStatePathSegments: ['ecc', 'install-state.json'], nativeRootRelativePath: '.claude-plugin', + planOperations(input, adapter) { + const modules = Array.isArray(input.modules) + ? input.modules + : (input.module ? [input.module] : []); + const planningInput = { + repoRoot: input.repoRoot, + projectRoot: input.projectRoot, + homeDir: input.homeDir, + }; + + return modules.flatMap(module => { + const paths = Array.isArray(module.paths) ? module.paths : []; + return paths + .filter(p => !isForeignPlatformPath(p, adapter.target)) + .map(sourceRelativePath => { + const managedDestinationPath = getClaudeManagedDestinationPath( + adapter, + sourceRelativePath, + planningInput + ); + + if (managedDestinationPath) { + return createRemappedOperation( + adapter, + module.id, + sourceRelativePath, + managedDestinationPath, + { strategy: 'preserve-relative-path' } + ); + } + + return adapter.createScaffoldOperation(module.id, sourceRelativePath, planningInput); + }); + }); + }, }); diff --git a/tests/lib/install-executor.test.js b/tests/lib/install-executor.test.js index 3bc9e698..e9f5ee75 100644 --- a/tests/lib/install-executor.test.js +++ b/tests/lib/install-executor.test.js @@ -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'))); diff --git a/tests/lib/install-targets.test.js b/tests/lib/install-targets.test.js index 60476e33..fe9bc9b2 100644 --- a/tests/lib/install-targets.test.js +++ b/tests/lib/install-targets.test.js @@ -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'; diff --git a/tests/lib/selective-install.test.js b/tests/lib/selective-install.test.js index 1b9075b8..58fc363d 100644 --- a/tests/lib/selective-install.test.js +++ b/tests/lib/selective-install.test.js @@ -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'); diff --git a/tests/scripts/install-apply.test.js b/tests/scripts/install-apply.test.js index 8fcf0c70..fcedff0b 100644 --- a/tests/scripts/install-apply.test.js +++ b/tests/scripts/install-apply.test.js @@ -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'); diff --git a/tests/scripts/install-readme-clarity.test.js b/tests/scripts/install-readme-clarity.test.js index 3a0725db..913b200a 100644 --- a/tests/scripts/install-readme-clarity.test.js +++ b/tests/scripts/install-readme-clarity.test.js @@ -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}`);