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

@@ -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. > 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. > 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) # Install dependencies (pick your package manager)
npm install # or: pnpm install | yarn install | bun install npm install # or: pnpm install | yarn install | bun install
# Plugin install path: copy only rules # Plugin install path: copy only ECC rules into an ECC-owned namespace
mkdir -p ~/.claude/rules mkdir -p ~/.claude/rules/ecc
cp -R rules/common ~/.claude/rules/ cp -R rules/common ~/.claude/rules/ecc/
cp -R rules/typescript ~/.claude/rules/ cp -R rules/typescript ~/.claude/rules/ecc/
# Fully manual ECC install path (use this instead of /plugin install) # Fully manual ECC install path (use this instead of /plugin install)
# ./install.sh --profile full # ./install.sh --profile full
@@ -271,10 +271,10 @@ cp -R rules/typescript ~/.claude/rules/
```powershell ```powershell
# Windows PowerShell # Windows PowerShell
# Plugin install path: copy only rules # Plugin install path: copy only ECC rules into an ECC-owned namespace
New-Item -ItemType Directory -Force -Path "$HOME/.claude/rules" | Out-Null New-Item -ItemType Directory -Force -Path "$HOME/.claude/rules/ecc" | Out-Null
Copy-Item -Recurse rules/common "$HOME/.claude/rules/" Copy-Item -Recurse rules/common "$HOME/.claude/rules/ecc/"
Copy-Item -Recurse rules/typescript "$HOME/.claude/rules/" Copy-Item -Recurse rules/typescript "$HOME/.claude/rules/ecc/"
# Fully manual ECC install path (use this instead of /plugin install) # Fully manual ECC install path (use this instead of /plugin install)
# .\install.ps1 --profile full # .\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. 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: - **Manual installer / CLI path:** from the repo root, preview removal first:
```bash ```bash
@@ -574,7 +574,7 @@ everything-claude-code/
| |-- verify.md # /verify - Prefer the verification-loop skill | |-- verify.md # /verify - Prefer the verification-loop skill
| |-- orchestrate.md # /orchestrate - Prefer dmux-workflows or multi-workflow | |-- 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 | |-- README.md # Structure overview and installation guide
| |-- common/ # Language-agnostic principles | |-- common/ # Language-agnostic principles
| | |-- coding-style.md # Immutability, file organization | | |-- 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 > git clone https://github.com/affaan-m/everything-claude-code.git
> >
> # Option A: User-level rules (applies to all projects) > # Option A: User-level rules (applies to all projects)
> mkdir -p ~/.claude/rules > mkdir -p ~/.claude/rules/ecc
> cp -r everything-claude-code/rules/common ~/.claude/rules/ > cp -r everything-claude-code/rules/common ~/.claude/rules/ecc/
> cp -r everything-claude-code/rules/typescript ~/.claude/rules/ # pick your stack > cp -r everything-claude-code/rules/typescript ~/.claude/rules/ecc/ # pick your stack
> cp -r everything-claude-code/rules/python ~/.claude/rules/ > cp -r everything-claude-code/rules/python ~/.claude/rules/ecc/
> cp -r everything-claude-code/rules/golang ~/.claude/rules/ > cp -r everything-claude-code/rules/golang ~/.claude/rules/ecc/
> cp -r everything-claude-code/rules/php ~/.claude/rules/ > cp -r everything-claude-code/rules/php ~/.claude/rules/ecc/
> >
> # Option B: Project-level rules (applies to current project only) > # Option B: Project-level rules (applies to current project only)
> mkdir -p .claude/rules > mkdir -p .claude/rules/ecc
> cp -r everything-claude-code/rules/common .claude/rules/ > cp -r everything-claude-code/rules/common .claude/rules/ecc/
> cp -r everything-claude-code/rules/typescript .claude/rules/ # pick your stack > 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/ cp everything-claude-code/agents/*.md ~/.claude/agents/
# Copy rules directories (common + language-specific) # Copy rules directories (common + language-specific)
mkdir -p ~/.claude/rules mkdir -p ~/.claude/rules/ecc
cp -r everything-claude-code/rules/common ~/.claude/rules/ cp -r everything-claude-code/rules/common ~/.claude/rules/ecc/
cp -r everything-claude-code/rules/typescript ~/.claude/rules/ # pick your stack cp -r everything-claude-code/rules/typescript ~/.claude/rules/ecc/ # pick your stack
cp -r everything-claude-code/rules/python ~/.claude/rules/ cp -r everything-claude-code/rules/python ~/.claude/rules/ecc/
cp -r everything-claude-code/rules/golang ~/.claude/rules/ cp -r everything-claude-code/rules/golang ~/.claude/rules/ecc/
cp -r everything-claude-code/rules/php ~/.claude/rules/ cp -r everything-claude-code/rules/php ~/.claude/rules/ecc/
# Copy skills first (primary workflow surface) # Copy skills first (primary workflow surface)
# Recommended (new users): core/general skills only # Recommended (new users): core/general skills only
cp -r everything-claude-code/.agents/skills/* ~/.claude/skills/ mkdir -p ~/.claude/skills/ecc
cp -r everything-claude-code/skills/search-first ~/.claude/skills/ 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 # Optional: add niche/framework-specific skills only when needed
# for s in django-patterns django-tdd laravel-patterns springboot-patterns; do # 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 # done
# Optional: keep maintained slash-command compatibility during migration # 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/ cp everything-claude-code/agents/*.md ~/.claude/agents/
# Just rules # Just rules
mkdir -p ~/.claude/rules/ mkdir -p ~/.claude/rules/ecc/
cp -r everything-claude-code/rules/common ~/.claude/rules/ cp -r everything-claude-code/rules/common ~/.claude/rules/ecc/
``` ```
Each component is fully independent. Each component is fully independent.

View File

@@ -640,7 +640,7 @@ Suggested operation shape:
"kind": "copy", "kind": "copy",
"moduleId": "rules-core", "moduleId": "rules-core",
"source": "rules/common/coding-style.md", "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", "ownership": "managed",
"overwritePolicy": "replace" "overwritePolicy": "replace"
} }
@@ -711,7 +711,7 @@ Suggested payload:
{ {
"kind": "copy", "kind": "copy",
"moduleId": "rules-core", "moduleId": "rules-core",
"destination": "/Users/example/.claude/rules/common/coding-style.md", "destination": "/Users/example/.claude/rules/ecc/common/coding-style.md",
"digest": "sha256:..." "digest": "sha256:..."
} }
] ]

View File

@@ -27,7 +27,7 @@ Usage: install.sh [--target <${LEGACY_INSTALL_TARGETS.join('|')}>] [--dry-run] [
install.sh [--dry-run] [--json] --config <path> install.sh [--dry-run] [--json] --config <path>
Targets: 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/ cursor - Install rules, hooks, and bundled Cursor configs to ./.cursor/
antigravity - Install rules, workflows, skills, and agents to ./.agent/ antigravity - Install rules, workflows, skills, and agents to ./.agent/

View File

@@ -14,6 +14,7 @@ const {
const { getInstallTargetAdapter } = require('./install-targets/registry'); const { getInstallTargetAdapter } = require('./install-targets/registry');
const LANGUAGE_NAME_PATTERN = /^[a-zA-Z0-9_-]+$/; const LANGUAGE_NAME_PATTERN = /^[a-zA-Z0-9_-]+$/;
const CLAUDE_ECC_NAMESPACE = 'ecc';
const EXCLUDED_GENERATED_SOURCE_SUFFIXES = [ const EXCLUDED_GENERATED_SOURCE_SUFFIXES = [
'/ecc-install-state.json', '/ecc-install-state.json',
'/ecc/install-state.json', '/ecc/install-state.json',
@@ -264,7 +265,7 @@ function isDirectoryNonEmpty(dirPath) {
function planClaudeLegacyInstall(context) { function planClaudeLegacyInstall(context) {
const adapter = getInstallTargetAdapter('claude'); const adapter = getInstallTargetAdapter('claude');
const targetRoot = adapter.resolveRoot({ homeDir: context.homeDir }); 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 installStatePath = adapter.getInstallStatePath({ homeDir: context.homeDir });
const operations = []; const operations = [];
const warnings = []; const warnings = [];

View File

@@ -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({ module.exports = createInstallTargetAdapter({
id: 'claude-home', id: 'claude-home',
@@ -7,4 +49,39 @@ module.exports = createInstallTargetAdapter({
rootSegments: ['.claude'], rootSegments: ['.claude'],
installStatePathSegments: ['ecc', 'install-state.json'], installStatePathSegments: ['ecc', 'install-state.json'],
nativeRootRelativePath: '.claude-plugin', 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);
});
});
},
}); });

View File

@@ -79,9 +79,11 @@ function writeManifestSourceFixture(root) {
kind: 'fixture', kind: 'fixture',
description: 'Fixture module', description: 'Fixture module',
paths: [ paths: [
'rules',
'src', 'src',
'standalone.txt', 'standalone.txt',
'missing.txt', 'missing.txt',
'skills/demo',
path.join('runtime', 'ecc', 'install-state.json'), path.join('runtime', 'ecc', 'install-state.json'),
'.claude-plugin', '.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', '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', '.git', 'ignored.js'), 'console.log("ignored");\n');
writeFile(root, path.join('src', 'nested', 'ecc-install-state.json'), '{}\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, 'standalone.txt', 'standalone\n');
writeFile(root, path.join('runtime', 'ecc', 'install-state.json'), '{}\n'); writeFile(root, path.join('runtime', 'ecc', 'install-state.json'), '{}\n');
writeJson(root, path.join('.claude-plugin', 'plugin.json'), { name: 'fixture' }); writeJson(root, path.join('.claude-plugin', 'plugin.json'), { name: 'fixture' });
@@ -194,6 +198,35 @@ function runTests() {
} }
})) passed++; else failed++; })) 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', () => { if (test('plans Cursor legacy assets and JSON merge payloads', () => {
const sourceRoot = createTempDir('install-executor-source-'); const sourceRoot = createTempDir('install-executor-source-');
const projectRoot = createTempDir('install-executor-project-'); const projectRoot = createTempDir('install-executor-project-');
@@ -307,6 +340,8 @@ function runTests() {
)); ));
assert.ok(normalizedSources.includes('src/app.js')); assert.ok(normalizedSources.includes('src/app.js'));
assert.ok(normalizedSources.includes('src/nested/feature.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('standalone.txt'));
assert.ok(normalizedSources.includes('.claude-plugin/plugin.json')); assert.ok(normalizedSources.includes('.claude-plugin/plugin.json'));
assert.ok(!normalizedSources.includes('missing.txt')); assert.ok(!normalizedSources.includes('missing.txt'));
@@ -318,6 +353,14 @@ function runTests() {
operation.sourceRelativePath === path.join('.claude-plugin', 'plugin.json') operation.sourceRelativePath === path.join('.claude-plugin', 'plugin.json')
&& operation.destinationPath === path.join(homeDir, '.claude', '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.deepStrictEqual(plan.warnings, ['fixture warning']);
assert.strictEqual(plan.statePreview.request.profile, 'minimal'); assert.strictEqual(plan.statePreview.request.profile, 'minimal');
assert.deepStrictEqual(plan.statePreview.request.includeComponents, ['capability:fixture']); assert.deepStrictEqual(plan.statePreview.request.includeComponents, ['capability:fixture']);
@@ -369,6 +412,8 @@ function runTests() {
const applied = applyInstallPlan(plan); const applied = applyInstallPlan(plan);
assert.strictEqual(applied.applied, true); 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', 'src', 'app.js')));
assert.ok(fs.existsSync(path.join(homeDir, '.claude', 'standalone.txt'))); assert.ok(fs.existsSync(path.join(homeDir, '.claude', 'standalone.txt')));
assert.ok(fs.existsSync(path.join(homeDir, '.claude', 'plugin.json'))); 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')); assert.strictEqual(statePath, path.join(homeDir, '.claude', 'ecc', 'install-state.json'));
})) passed++; else failed++; })) 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', () => { if (test('plans scaffold operations and flattens native target roots', () => {
const repoRoot = path.join(__dirname, '..', '..'); const repoRoot = path.join(__dirname, '..', '..');
const projectRoot = '/workspace/app'; const projectRoot = '/workspace/app';

View File

@@ -576,10 +576,10 @@ function runTests() {
const claudeRoot = path.join(homeDir, '.claude'); const claudeRoot = path.join(homeDir, '.claude');
// Security skill should be installed (from --with) // 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'); 'Should install security-review skill from --with');
// Core profile modules should be installed // 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'); 'Should install core rules');
// Install state should record include/exclude // Install state should record include/exclude
@@ -615,12 +615,12 @@ function runTests() {
const claudeRoot = path.join(homeDir, '.claude'); const claudeRoot = path.join(homeDir, '.claude');
// Orchestration skills should NOT be installed (from --without) // 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'); 'Should not install orchestration skills');
// Developer profile base modules should be installed // 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'); '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'); 'Should install workflow skills');
const statePath = path.join(claudeRoot, 'ecc', 'install-state.json'); const statePath = path.join(claudeRoot, 'ecc', 'install-state.json');
@@ -653,10 +653,10 @@ function runTests() {
const claudeRoot = path.join(homeDir, '.claude'); const claudeRoot = path.join(homeDir, '.claude');
// framework-language skill (from lang:typescript) should be installed // 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'); 'Should install framework-language skills');
// Its dependencies should be installed // 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'); 'Should install dependency rules-core');
const statePath = path.join(claudeRoot, 'ecc', 'install-state.json'); const statePath = path.join(claudeRoot, 'ecc', 'install-state.json');

View File

@@ -94,13 +94,13 @@ function runTests() {
assert.strictEqual(result.code, 0, result.stderr); assert.strictEqual(result.code, 0, result.stderr);
const claudeRoot = path.join(homeDir, '.claude'); 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, 'rules', 'typescript', 'testing.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, 'commands', 'plan.md')));
assert.ok(fs.existsSync(path.join(claudeRoot, 'scripts', 'hooks', 'session-end.js'))); 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, 'scripts', 'lib', 'utils.js')));
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')));
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')));
assert.ok(fs.existsSync(path.join(claudeRoot, 'plugin.json'))); assert.ok(fs.existsSync(path.join(claudeRoot, 'plugin.json')));
const statePath = path.join(homeDir, '.claude', 'ecc', 'install-state.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.resolution.selectedModules.includes('framework-language'));
assert.ok( assert.ok(
state.operations.some(operation => ( 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' 'Should record common rule file operation'
); );
@@ -299,7 +299,7 @@ function runTests() {
assert.strictEqual(result.code, 0, result.stderr); assert.strictEqual(result.code, 0, result.stderr);
const claudeRoot = path.join(homeDir, '.claude'); 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, 'agents', 'architect.md')));
assert.ok(fs.existsSync(path.join(claudeRoot, 'commands', 'plan.md'))); assert.ok(fs.existsSync(path.join(claudeRoot, 'commands', 'plan.md')));
assert.ok(fs.existsSync(path.join(claudeRoot, 'hooks', 'hooks.json'))); assert.ok(fs.existsSync(path.join(claudeRoot, 'hooks', 'hooks.json')));
@@ -324,6 +324,32 @@ function runTests() {
} }
})) passed++; else failed++; })) 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', () => { if (test('installs antigravity manifest profiles while skipping only unsupported modules', () => {
const homeDir = createTempDir('install-apply-home-'); const homeDir = createTempDir('install-apply-home-');
const projectDir = createTempDir('install-apply-project-'); const projectDir = createTempDir('install-apply-project-');
@@ -727,8 +753,8 @@ function runTests() {
const result = run(['--config', configPath], { cwd: projectDir, homeDir }); const result = run(['--config', configPath], { cwd: projectDir, homeDir });
assert.strictEqual(result.code, 0, result.stderr); 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', 'ecc', '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', 'dmux-workflows', 'SKILL.md')));
const state = readJson(path.join(homeDir, '.claude', 'ecc', 'install-state.json')); const state = readJson(path.join(homeDir, '.claude', 'ecc', 'install-state.json'));
assert.strictEqual(state.request.profile, 'developer'); assert.strictEqual(state.request.profile, 'developer');
@@ -759,8 +785,8 @@ function runTests() {
const result = run([], { cwd: projectDir, homeDir }); const result = run([], { cwd: projectDir, homeDir });
assert.strictEqual(result.code, 0, result.stderr); 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', 'ecc', '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', 'dmux-workflows', 'SKILL.md')));
const state = readJson(path.join(homeDir, '.claude', 'ecc', 'install-state.json')); const state = readJson(path.join(homeDir, '.claude', 'ecc', 'install-state.json'));
assert.strictEqual(state.request.profile, 'developer'); 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.includes('Start with `rules/common` plus one language or framework pack you actually use.'),
'README should steer users away from copying every rules directory' '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++; })) passed++; else failed++;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`); console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);