diff --git a/manifests/install-modules.json b/manifests/install-modules.json index 0edaf272..c8330f35 100644 --- a/manifests/install-modules.json +++ b/manifests/install-modules.json @@ -10,6 +10,7 @@ ], "targets": [ "claude", + "claude-project", "cursor", "antigravity", "codebuddy", @@ -33,6 +34,7 @@ ], "targets": [ "claude", + "claude-project", "cursor", "antigravity", "codex", @@ -55,6 +57,7 @@ ], "targets": [ "claude", + "claude-project", "cursor", "antigravity", "opencode", @@ -79,6 +82,7 @@ ], "targets": [ "claude", + "claude-project", "cursor", "opencode", "codebuddy" @@ -106,6 +110,7 @@ ], "targets": [ "claude", + "claude-project", "cursor", "antigravity", "codex", @@ -177,6 +182,7 @@ ], "targets": [ "claude", + "claude-project", "cursor", "antigravity", "codex", @@ -210,6 +216,7 @@ ], "targets": [ "claude", + "claude-project", "cursor", "antigravity", "codex", @@ -255,6 +262,7 @@ ], "targets": [ "claude", + "claude-project", "cursor", "antigravity", "codex", @@ -294,6 +302,7 @@ ], "targets": [ "claude", + "claude-project", "cursor", "antigravity", "codex", @@ -326,6 +335,7 @@ ], "targets": [ "claude", + "claude-project", "cursor", "antigravity", "codex", @@ -360,6 +370,7 @@ ], "targets": [ "claude", + "claude-project", "cursor", "antigravity", "codex", @@ -402,6 +413,7 @@ ], "targets": [ "claude", + "claude-project", "cursor", "antigravity", "codex", @@ -428,6 +440,7 @@ ], "targets": [ "claude", + "claude-project", "cursor", "antigravity", "codex", @@ -459,6 +472,7 @@ ], "targets": [ "claude", + "claude-project", "cursor", "codex", "opencode", @@ -490,6 +504,7 @@ ], "targets": [ "claude", + "claude-project", "codex", "opencode" ], @@ -515,6 +530,7 @@ ], "targets": [ "claude", + "claude-project", "cursor", "antigravity", "codex", @@ -559,6 +575,7 @@ ], "targets": [ "claude", + "claude-project", "cursor", "antigravity", "codex", @@ -592,6 +609,7 @@ ], "targets": [ "claude", + "claude-project", "cursor", "antigravity", "codex", @@ -617,6 +635,7 @@ ], "targets": [ "claude", + "claude-project", "cursor", "antigravity", "codex", @@ -653,6 +672,7 @@ ], "targets": [ "claude", + "claude-project", "cursor", "antigravity", "codex", @@ -679,6 +699,7 @@ ], "targets": [ "claude", + "claude-project", "cursor", "antigravity", "codex", @@ -703,7 +724,8 @@ "docs/ja-JP" ], "targets": [ - "claude" + "claude", + "claude-project" ], "dependencies": [], "defaultInstall": false, @@ -718,7 +740,8 @@ "docs/zh-CN" ], "targets": [ - "claude" + "claude", + "claude-project" ], "dependencies": [], "defaultInstall": false, @@ -733,7 +756,8 @@ "docs/ko-KR" ], "targets": [ - "claude" + "claude", + "claude-project" ], "dependencies": [], "defaultInstall": false, @@ -748,7 +772,8 @@ "docs/pt-BR" ], "targets": [ - "claude" + "claude", + "claude-project" ], "dependencies": [], "defaultInstall": false, @@ -763,7 +788,8 @@ "docs/ru" ], "targets": [ - "claude" + "claude", + "claude-project" ], "dependencies": [], "defaultInstall": false, @@ -778,7 +804,8 @@ "docs/tr" ], "targets": [ - "claude" + "claude", + "claude-project" ], "dependencies": [], "defaultInstall": false, @@ -793,7 +820,8 @@ "docs/vi-VN" ], "targets": [ - "claude" + "claude", + "claude-project" ], "dependencies": [], "defaultInstall": false, @@ -808,7 +836,8 @@ "docs/zh-TW" ], "targets": [ - "claude" + "claude", + "claude-project" ], "dependencies": [], "defaultInstall": false, diff --git a/schemas/ecc-install-config.schema.json b/schemas/ecc-install-config.schema.json index 65131fb8..dc5923d0 100644 --- a/schemas/ecc-install-config.schema.json +++ b/schemas/ecc-install-config.schema.json @@ -19,6 +19,7 @@ "type": "string", "enum": [ "claude", + "claude-project", "cursor", "antigravity", "codex", diff --git a/scripts/install-apply.js b/scripts/install-apply.js index eb48bb06..2e3009dd 100755 --- a/scripts/install-apply.js +++ b/scripts/install-apply.js @@ -27,11 +27,12 @@ Usage: install.sh [--target <${LEGACY_INSTALL_TARGETS.join('|')}>] [--dry-run] [ install.sh [--target <${SUPPORTED_INSTALL_TARGETS.join('|')}>] [--dry-run] [--json] --profile [--with ]... [--without ]... install.sh [--target <${SUPPORTED_INSTALL_TARGETS.join('|')}>] [--dry-run] [--json] --modules [--with ]... [--without ]... install.sh [--target <${SUPPORTED_INSTALL_TARGETS.join('|')}>] [--dry-run] [--json] --skills - install.sh [--target claude] [--dry-run] [--json] --locale + install.sh [--target claude|claude-project] [--dry-run] [--json] --locale install.sh [--dry-run] [--json] --config Targets: claude (default) - Install ECC into ~/.claude/ with managed rules/skills under rules/ecc and skills/ecc + claude-project - Install ECC into ./.claude/ (per-project) 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/ codex - Install shared agents/config into ~/.codex/ @@ -49,8 +50,8 @@ Options: --skills Install one or more skill directories by ID, e.g. continuous-learning-v2 --without Exclude a user-facing install component - --locale Install translated docs to ~/.claude/docs// - (claude target only; can be combined with --profile or --with) + --locale Install translated docs to ~/.claude/docs// (or ./.claude/docs// for claude-project) + (claude or claude-project target only; can be combined with --profile or --with) --config Load install intent from ecc-install.json --dry-run Show the install plan without copying files --json Emit machine-readable plan/result JSON diff --git a/scripts/lib/install-manifests.js b/scripts/lib/install-manifests.js index c27d5e23..0224b37d 100644 --- a/scripts/lib/install-manifests.js +++ b/scripts/lib/install-manifests.js @@ -4,7 +4,7 @@ const path = require('path'); const { getInstallTargetAdapter, planInstallTargetScaffold } = require('./install-targets/registry'); const DEFAULT_REPO_ROOT = path.join(__dirname, '../..'); -const SUPPORTED_INSTALL_TARGETS = ['claude', 'cursor', 'antigravity', 'codex', 'gemini', 'opencode', 'codebuddy', 'joycode', 'qwen', 'zed']; +const SUPPORTED_INSTALL_TARGETS = ['claude', 'claude-project', 'cursor', 'antigravity', 'codex', 'gemini', 'opencode', 'codebuddy', 'joycode', 'qwen', 'zed']; const COMPONENT_FAMILY_PREFIXES = { baseline: 'baseline:', language: 'lang:', @@ -43,6 +43,14 @@ const LEGACY_COMPAT_BASE_MODULE_IDS_BY_TARGET = Object.freeze({ 'platform-configs', 'workflow-quality', ], + 'claude-project': [ + 'rules-core', + 'agents-core', + 'commands-core', + 'hooks-runtime', + 'platform-configs', + 'workflow-quality', + ], cursor: [ 'rules-core', 'agents-core', diff --git a/scripts/lib/install-targets/claude-project.js b/scripts/lib/install-targets/claude-project.js new file mode 100644 index 00000000..150df276 --- /dev/null +++ b/scripts/lib/install-targets/claude-project.js @@ -0,0 +1,91 @@ +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) + ); + } + + if (normalizedSourcePath === 'docs' || normalizedSourcePath.startsWith('docs/')) { + return path.join(targetRoot, normalizedSourcePath); + } + + return null; +} + +module.exports = createInstallTargetAdapter({ + id: 'claude-project', + target: 'claude-project', + kind: 'project', + 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, 'claude')) + .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/scripts/lib/install-targets/registry.js b/scripts/lib/install-targets/registry.js index 8e444a0e..e4e3e7c4 100644 --- a/scripts/lib/install-targets/registry.js +++ b/scripts/lib/install-targets/registry.js @@ -1,5 +1,6 @@ const antigravityProject = require('./antigravity-project'); const claudeHome = require('./claude-home'); +const claudeProject = require('./claude-project'); const codebuddyProject = require('./codebuddy-project'); const codexHome = require('./codex-home'); const cursorProject = require('./cursor-project'); @@ -11,6 +12,7 @@ const zedProject = require('./zed-project'); const ADAPTERS = Object.freeze([ claudeHome, + claudeProject, cursorProject, antigravityProject, codexHome, diff --git a/scripts/lib/install/apply.js b/scripts/lib/install/apply.js index 42497c42..6c473685 100644 --- a/scripts/lib/install/apply.js +++ b/scripts/lib/install/apply.js @@ -89,7 +89,7 @@ function isMcpConfigPath(filePath) { } function buildResolvedClaudeHooks(plan) { - if (!plan.adapter || plan.adapter.target !== 'claude') { + if (!plan.adapter || (plan.adapter.target !== 'claude' && plan.adapter.target !== 'claude-project')) { return null; } diff --git a/scripts/lib/install/request.js b/scripts/lib/install/request.js index 4cf5c043..5b69d67c 100644 --- a/scripts/lib/install/request.js +++ b/scripts/lib/install/request.js @@ -100,8 +100,8 @@ function normalizeInstallRequest(options = {}) { `Unsupported locale: "${locale}". Supported locales: ${listSupportedLocales().join(', ')}` ); } - if (locale && target !== 'claude') { - throw new Error('--locale can only be used with --target claude'); + if (locale && target !== 'claude' && target !== 'claude-project') { + throw new Error('--locale can only be used with --target claude or --target claude-project'); } const requestedIncludeComponentIds = dedupeStrings([ ...(config?.includeComponentIds || []), diff --git a/tests/lib/install-targets.test.js b/tests/lib/install-targets.test.js index daa2e722..6fa7b1bf 100644 --- a/tests/lib/install-targets.test.js +++ b/tests/lib/install-targets.test.js @@ -37,6 +37,7 @@ function runTests() { const adapters = listInstallTargetAdapters(); const targets = adapters.map(adapter => adapter.target); assert.ok(targets.includes('claude'), 'Should include claude target'); + assert.ok(targets.includes('claude-project'), 'Should include claude-project target'); assert.ok(targets.includes('cursor'), 'Should include cursor target'); assert.ok(targets.includes('antigravity'), 'Should include antigravity target'); assert.ok(targets.includes('codex'), 'Should include codex target'); @@ -865,6 +866,99 @@ function runTests() { } })) passed++; else failed++; + if (test('resolves claude-project adapter root and install-state path from project root', () => { + const adapter = getInstallTargetAdapter('claude-project'); + const projectRoot = '/workspace/app'; + const root = adapter.resolveRoot({ projectRoot }); + const statePath = adapter.getInstallStatePath({ projectRoot }); + + assert.strictEqual(adapter.id, 'claude-project'); + assert.strictEqual(adapter.target, 'claude-project'); + assert.strictEqual(adapter.kind, 'project'); + assert.strictEqual(root, path.join(projectRoot, '.claude')); + assert.strictEqual(statePath, path.join(projectRoot, '.claude', 'ecc', 'install-state.json')); + })) passed++; else failed++; + + if (test('claude-project adapter supports lookup by target and adapter id', () => { + const byTarget = getInstallTargetAdapter('claude-project'); + const byId = getInstallTargetAdapter('claude-project'); + + assert.strictEqual(byTarget.id, 'claude-project'); + assert.strictEqual(byId.id, 'claude-project'); + assert.ok(byTarget.supports('claude-project')); + })) passed++; else failed++; + + if (test('plans claude-project rules and skills under project-scope ECC-managed subdirectories', () => { + const repoRoot = path.join(__dirname, '..', '..'); + const projectRoot = '/workspace/app'; + + const plan = planInstallTargetScaffold({ + target: 'claude-project', + repoRoot, + projectRoot, + modules: [ + { + id: 'rules-core', + paths: ['rules'], + }, + { + id: 'workflow-quality', + paths: ['skills/tdd-workflow'], + }, + ], + }); + + assert.strictEqual(plan.adapter.id, 'claude-project'); + assert.strictEqual(plan.targetRoot, path.join(projectRoot, '.claude')); + assert.strictEqual(plan.installStatePath, path.join(projectRoot, '.claude', 'ecc', 'install-state.json')); + assert.ok( + plan.operations.some(operation => ( + normalizedRelativePath(operation.sourceRelativePath) === 'rules' + && operation.destinationPath === path.join(projectRoot, '.claude', 'rules', 'ecc') + )), + 'Should install bundled rules under project-scope rules/ecc' + ); + assert.ok( + plan.operations.some(operation => ( + normalizedRelativePath(operation.sourceRelativePath) === 'skills/tdd-workflow' + && operation.destinationPath === path.join(projectRoot, '.claude', 'skills', 'ecc', 'tdd-workflow') + )), + 'Should install bundled skills under project-scope skills/ecc' + ); + })) passed++; else failed++; + + if (test('claude-project skips foreign platform source paths', () => { + const repoRoot = path.join(__dirname, '..', '..'); + const projectRoot = '/workspace/app'; + + const plan = planInstallTargetScaffold({ + target: 'claude-project', + repoRoot, + projectRoot, + modules: [ + { + id: 'platform-configs', + paths: ['.cursor', '.zed', 'rules'], + }, + ], + }); + + assert.ok( + !plan.operations.some(operation => ( + normalizedRelativePath(operation.sourceRelativePath) === '.cursor' + || normalizedRelativePath(operation.sourceRelativePath).startsWith('.cursor/') + )), + 'Should skip foreign Cursor platform paths' + ); + assert.ok( + !plan.operations.some(operation => ( + normalizedRelativePath(operation.sourceRelativePath) === '.zed' + || normalizedRelativePath(operation.sourceRelativePath).startsWith('.zed/') + )), + 'Should skip foreign Zed platform paths' + ); + })) passed++; else failed++; + console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`); process.exit(failed > 0 ? 1 : 0); }