mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-22 01:25:10 +02:00
fix: isolate Claude project hooks from ESM hosts (#3184)
* fix: isolate Claude project hooks from ESM hosts * fix: handle user-owned Claude scripts package
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "commonjs"
|
||||
}
|
||||
@@ -9,6 +9,7 @@ const {
|
||||
} = require('./helpers');
|
||||
|
||||
const CLAUDE_ECC_NAMESPACE = 'ecc';
|
||||
const CLAUDE_PROJECT_COMMONJS_PACKAGE = 'manifests/install-assets/claude-project-scripts-package.json';
|
||||
|
||||
function getClaudeManagedDestinationPath(adapter, sourceRelativePath, input) {
|
||||
const normalizedSourcePath = normalizeRelativePath(sourceRelativePath);
|
||||
@@ -65,7 +66,7 @@ module.exports = createInstallTargetAdapter({
|
||||
|
||||
return modules.flatMap(module => {
|
||||
const paths = Array.isArray(module.paths) ? module.paths : [];
|
||||
return paths
|
||||
const operations = paths
|
||||
.filter(p => !isForeignPlatformPath(p, 'claude'))
|
||||
.flatMap(sourceRelativePath => {
|
||||
if (
|
||||
@@ -93,6 +94,21 @@ module.exports = createInstallTargetAdapter({
|
||||
|
||||
return [adapter.createScaffoldOperation(module.id, sourceRelativePath, planningInput)];
|
||||
});
|
||||
|
||||
if (module.id !== 'hooks-runtime') {
|
||||
return operations;
|
||||
}
|
||||
|
||||
return [
|
||||
...['hooks', 'lib'].map(directory => createRemappedOperation(
|
||||
adapter,
|
||||
module.id,
|
||||
CLAUDE_PROJECT_COMMONJS_PACKAGE,
|
||||
path.join(adapter.resolveRoot(planningInput), 'scripts', directory, 'package.json'),
|
||||
{ strategy: 'preserve-relative-path' }
|
||||
)),
|
||||
...operations,
|
||||
];
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1056,6 +1056,74 @@ function runTests() {
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('isolates project hooks from ESM package scopes without overwriting user Claude package data', () => {
|
||||
const homeDir = createTempDir('install-apply-claude-project-esm-home-');
|
||||
const projectDir = createTempDir('install-apply-claude-project-esm-');
|
||||
const claudeRoot = path.join(projectDir, '.claude');
|
||||
const userPackagePath = path.join(claudeRoot, 'package.json');
|
||||
const scriptsPackagePath = path.join(claudeRoot, 'scripts', 'package.json');
|
||||
const hooksPackagePath = path.join(claudeRoot, 'scripts', 'hooks', 'package.json');
|
||||
const libPackagePath = path.join(claudeRoot, 'scripts', 'lib', 'package.json');
|
||||
const userPackage = '{"name":"user-claude-config","type":"module"}\n';
|
||||
const userScriptsPackage = '{"name":"user-claude-scripts","type":"module"}\n';
|
||||
|
||||
try {
|
||||
fs.writeFileSync(path.join(projectDir, 'package.json'), '{"type":"module"}\n');
|
||||
fs.mkdirSync(path.dirname(scriptsPackagePath), { recursive: true });
|
||||
fs.writeFileSync(userPackagePath, userPackage);
|
||||
fs.writeFileSync(scriptsPackagePath, userScriptsPackage);
|
||||
|
||||
const firstInstall = run(
|
||||
['--target', 'claude-project', '--profile', 'core', '--enable-hooks'],
|
||||
{ cwd: projectDir, homeDir }
|
||||
);
|
||||
assert.strictEqual(firstInstall.code, 0, firstInstall.stderr);
|
||||
assert.strictEqual(fs.readFileSync(userPackagePath, 'utf8'), userPackage);
|
||||
assert.strictEqual(fs.readFileSync(scriptsPackagePath, 'utf8'), userScriptsPackage);
|
||||
assert.deepStrictEqual(readJson(hooksPackagePath), { type: 'commonjs' });
|
||||
assert.deepStrictEqual(readJson(libPackagePath), { type: 'commonjs' });
|
||||
|
||||
const hookResult = spawnSync(
|
||||
process.execPath,
|
||||
[path.join(claudeRoot, 'scripts', 'hooks', 'block-no-verify.js')],
|
||||
{
|
||||
input: JSON.stringify({ tool_input: { command: 'git commit --no-verify' } }),
|
||||
encoding: 'utf8',
|
||||
cwd: projectDir,
|
||||
}
|
||||
);
|
||||
assert.strictEqual(hookResult.status, 2, hookResult.stderr);
|
||||
assert.match(hookResult.stderr, /no-verify/i);
|
||||
|
||||
const secondInstall = run(
|
||||
['--target', 'claude-project', '--profile', 'core', '--enable-hooks'],
|
||||
{ cwd: projectDir, homeDir }
|
||||
);
|
||||
assert.strictEqual(secondInstall.code, 0, secondInstall.stderr);
|
||||
assert.strictEqual(fs.readFileSync(userPackagePath, 'utf8'), userPackage);
|
||||
assert.strictEqual(fs.readFileSync(scriptsPackagePath, 'utf8'), userScriptsPackage);
|
||||
|
||||
const state = readJson(path.join(claudeRoot, 'ecc', 'install-state.json'));
|
||||
const boundaryPaths = [hooksPackagePath, libPackagePath];
|
||||
const packageBoundaryOperations = state.operations.filter(operation => (
|
||||
boundaryPaths.includes(operation.destinationPath)
|
||||
));
|
||||
assert.deepStrictEqual(
|
||||
packageBoundaryOperations.map(operation => operation.destinationPath).sort(),
|
||||
[...boundaryPaths].sort()
|
||||
);
|
||||
assert.ok(packageBoundaryOperations.every(operation => operation.moduleId === 'hooks-runtime'));
|
||||
assert.ok(packageBoundaryOperations.every(operation => (
|
||||
/^[a-f0-9]{64}$/i.test(operation.contentSha256)
|
||||
)));
|
||||
assert.ok(!state.operations.some(operation => operation.destinationPath === userPackagePath));
|
||||
assert.ok(!state.operations.some(operation => operation.destinationPath === scriptsPackagePath));
|
||||
} finally {
|
||||
cleanup(homeDir);
|
||||
cleanup(projectDir);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('preserves existing settings.json while disabling Claude co-author attribution', () => {
|
||||
const homeDir = createTempDir('install-apply-home-');
|
||||
const projectDir = createTempDir('install-apply-project-');
|
||||
|
||||
@@ -132,6 +132,59 @@ function runTests() {
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('uninstalls the project hook module boundary and preserves user Claude package data', () => {
|
||||
const homeDir = createTempDir('uninstall-claude-project-esm-home-');
|
||||
const projectRoot = createTempDir('uninstall-claude-project-esm-');
|
||||
const claudeRoot = path.join(projectRoot, '.claude');
|
||||
const userPackagePath = path.join(claudeRoot, 'package.json');
|
||||
const scriptsPackagePath = path.join(claudeRoot, 'scripts', 'package.json');
|
||||
const hooksPackagePath = path.join(claudeRoot, 'scripts', 'hooks', 'package.json');
|
||||
const libPackagePath = path.join(claudeRoot, 'scripts', 'lib', 'package.json');
|
||||
const statePath = path.join(claudeRoot, 'ecc', 'install-state.json');
|
||||
const userPackage = '{"name":"user-claude-config","type":"module"}\n';
|
||||
const userScriptsPackage = '{"name":"user-claude-scripts","type":"module"}\n';
|
||||
|
||||
try {
|
||||
fs.writeFileSync(path.join(projectRoot, 'package.json'), '{"type":"module"}\n');
|
||||
fs.mkdirSync(path.dirname(scriptsPackagePath), { recursive: true });
|
||||
fs.writeFileSync(userPackagePath, userPackage);
|
||||
fs.writeFileSync(scriptsPackagePath, userScriptsPackage);
|
||||
|
||||
execFileSync(
|
||||
'node',
|
||||
[INSTALL_SCRIPT, '--target', 'claude-project', '--profile', 'core', '--enable-hooks'],
|
||||
{
|
||||
cwd: projectRoot,
|
||||
env: {
|
||||
...process.env,
|
||||
HOME: homeDir,
|
||||
USERPROFILE: homeDir,
|
||||
},
|
||||
encoding: 'utf8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
timeout: CLI_TIMEOUT_MS,
|
||||
}
|
||||
);
|
||||
assert.deepStrictEqual(JSON.parse(fs.readFileSync(hooksPackagePath, 'utf8')), { type: 'commonjs' });
|
||||
assert.deepStrictEqual(JSON.parse(fs.readFileSync(libPackagePath, 'utf8')), { type: 'commonjs' });
|
||||
assert.strictEqual(fs.readFileSync(scriptsPackagePath, 'utf8'), userScriptsPackage);
|
||||
|
||||
const uninstallResult = run(['--target', 'claude-project'], {
|
||||
cwd: projectRoot,
|
||||
homeDir,
|
||||
});
|
||||
assert.strictEqual(uninstallResult.code, 0, uninstallResult.stderr);
|
||||
assert.strictEqual(fs.readFileSync(userPackagePath, 'utf8'), userPackage);
|
||||
assert.strictEqual(fs.readFileSync(scriptsPackagePath, 'utf8'), userScriptsPackage);
|
||||
assert.ok(!fs.existsSync(hooksPackagePath));
|
||||
assert.ok(!fs.existsSync(libPackagePath));
|
||||
assert.ok(!fs.existsSync(statePath));
|
||||
} finally {
|
||||
cleanup(homeDir);
|
||||
cleanup(projectRoot);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('reverses non-copy operations and keeps unrelated files', () => {
|
||||
const homeDir = createTempDir('uninstall-home-');
|
||||
const projectRoot = createTempDir('uninstall-project-');
|
||||
|
||||
Reference in New Issue
Block a user