mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-11 20:27:58 +02:00
fix: disable Claude co-author attribution by default
This commit is contained in:
@@ -18,6 +18,7 @@ const OFFICIAL_MARKETPLACE_NAME = 'ecc';
|
||||
const OFFICIAL_MARKETPLACE_REPO = 'affaan-m/ecc';
|
||||
const OFFICIAL_MARKETPLACE_URL = 'https://github.com/affaan-m/ECC';
|
||||
const PROVIDER_COMMAND_TIMEOUT_MS = 120 * 1000;
|
||||
const CLAUDE_COAUTHOR_SETTING_KEY = 'includeCoAuthoredBy';
|
||||
const VALID_SCOPES = new Set(['user', 'project', 'local']);
|
||||
const VALID_HOOK_MODES = new Set(['off', 'minimal', 'standard', 'strict']);
|
||||
|
||||
@@ -320,21 +321,39 @@ function deriveHookMode(settings) {
|
||||
return options.hooks_enabled ? options.hook_profile : 'off';
|
||||
}
|
||||
|
||||
function withClaudeCommitAttributionPreference(settings) {
|
||||
if (settings?.[CLAUDE_COAUTHOR_SETTING_KEY] === true) {
|
||||
return settings;
|
||||
}
|
||||
return {
|
||||
...settings,
|
||||
[CLAUDE_COAUTHOR_SETTING_KEY]: false,
|
||||
};
|
||||
}
|
||||
|
||||
function needsClaudeCommitAttributionPreferenceWrite(settings) {
|
||||
return settings?.[CLAUDE_COAUTHOR_SETTING_KEY] !== false
|
||||
&& settings?.[CLAUDE_COAUTHOR_SETTING_KEY] !== true;
|
||||
}
|
||||
|
||||
function writeClaudePluginOptions(settingsPath, hooks) {
|
||||
const settings = readSettings(settingsPath);
|
||||
const pluginConfigs = settings.pluginConfigs || {};
|
||||
const eccConfig = pluginConfigs[CURRENT_PLUGIN_ID] || {};
|
||||
const options = eccConfig.options || {};
|
||||
const nextOptions = hooks === undefined
|
||||
? { ...options }
|
||||
: {
|
||||
...options,
|
||||
...hookOptions(hooks),
|
||||
};
|
||||
const nextSettings = {
|
||||
...settings,
|
||||
...withClaudeCommitAttributionPreference(settings),
|
||||
pluginConfigs: {
|
||||
...pluginConfigs,
|
||||
[CURRENT_PLUGIN_ID]: {
|
||||
...eccConfig,
|
||||
options: {
|
||||
...options,
|
||||
...hookOptions(hooks),
|
||||
},
|
||||
options: nextOptions,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -609,8 +628,15 @@ function setupClaudePlugin(options = {}, dependencies = {}) {
|
||||
run,
|
||||
scope: inventory.scope,
|
||||
});
|
||||
if (options.hooks !== undefined || !inventory.installed) {
|
||||
writeClaudePluginOptions(settingsPath, hooks);
|
||||
const hooksToPersist = options.hooks !== undefined || !inventory.installed
|
||||
? hooks
|
||||
: undefined;
|
||||
if (
|
||||
options.hooks !== undefined
|
||||
|| !inventory.installed
|
||||
|| needsClaudeCommitAttributionPreferenceWrite(initialSettings)
|
||||
) {
|
||||
writeClaudePluginOptions(settingsPath, hooksToPersist);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -648,5 +674,7 @@ module.exports = {
|
||||
runClaude,
|
||||
setupClaudePlugin,
|
||||
verifyPluginAtScope,
|
||||
needsClaudeCommitAttributionPreferenceWrite,
|
||||
withClaudeCommitAttributionPreference,
|
||||
writeClaudePluginOptions,
|
||||
};
|
||||
|
||||
@@ -16,6 +16,7 @@ const {
|
||||
ensurePluginAtScope,
|
||||
hookOptions,
|
||||
isOfficialMarketplace,
|
||||
needsClaudeCommitAttributionPreferenceWrite,
|
||||
parseMarketplaceList,
|
||||
parsePluginList,
|
||||
readSettings,
|
||||
@@ -264,6 +265,7 @@ function migrateClaudePluginScope(options = {}, dependencies = {}) {
|
||||
const hookConfiguration = options.hooks === undefined
|
||||
? readStoredHookOptions(settings)
|
||||
: hookOptions(options.hooks);
|
||||
const needsCommitAttributionPreference = needsClaudeCommitAttributionPreferenceWrite(settings);
|
||||
|
||||
const marketplaces = parseMarketplaceList(
|
||||
run(
|
||||
@@ -296,14 +298,23 @@ function migrateClaudePluginScope(options = {}, dependencies = {}) {
|
||||
...result,
|
||||
dryRun: true,
|
||||
preferencesUpdated: false,
|
||||
plannedActions: options.hooks === undefined ? [] : [{
|
||||
action: 'write-hook-preferences',
|
||||
...hookConfiguration,
|
||||
}],
|
||||
plannedActions: [
|
||||
...(options.hooks === undefined ? [] : [{
|
||||
action: 'write-hook-preferences',
|
||||
...hookConfiguration,
|
||||
}]),
|
||||
...(needsCommitAttributionPreference ? [{
|
||||
action: 'write-commit-attribution-preference',
|
||||
includeCoAuthoredBy: false,
|
||||
}] : []),
|
||||
],
|
||||
};
|
||||
}
|
||||
if (options.hooks !== undefined) {
|
||||
writeClaudePluginOptions(settingsPath, options.hooks);
|
||||
if (options.hooks !== undefined || needsCommitAttributionPreference) {
|
||||
writeClaudePluginOptions(
|
||||
settingsPath,
|
||||
options.hooks !== undefined ? options.hooks : undefined
|
||||
);
|
||||
return { ...result, preferencesUpdated: true };
|
||||
}
|
||||
return result;
|
||||
@@ -371,8 +382,11 @@ function migrateClaudePluginScope(options = {}, dependencies = {}) {
|
||||
const warnings = uninstallSource(run, paths, migration, options.scope);
|
||||
verifyFinalState(run, paths, options.scope);
|
||||
|
||||
if (options.hooks !== undefined) {
|
||||
writeClaudePluginOptions(settingsPath, options.hooks);
|
||||
if (options.hooks !== undefined || needsCommitAttributionPreference) {
|
||||
writeClaudePluginOptions(
|
||||
settingsPath,
|
||||
options.hooks !== undefined ? options.hooks : undefined
|
||||
);
|
||||
}
|
||||
|
||||
const result = {
|
||||
|
||||
@@ -105,6 +105,46 @@ function formatJson(value) {
|
||||
return `${JSON.stringify(value, null, 2)}\n`;
|
||||
}
|
||||
|
||||
function shouldSetClaudeCommitAttributionPreference(plan) {
|
||||
if (!plan?.adapter || !['claude', 'claude-project'].includes(plan.adapter.target)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return plan.operations.some(operation => {
|
||||
if (typeof operation?.destinationPath !== 'string') {
|
||||
return false;
|
||||
}
|
||||
const relativePath = path.relative(plan.targetRoot, operation.destinationPath);
|
||||
return relativePath && !relativePath.startsWith(`docs${path.sep}`) && relativePath !== 'docs';
|
||||
});
|
||||
}
|
||||
|
||||
function writeClaudeCommitAttributionPreference(settingsPath) {
|
||||
let settings = {};
|
||||
if (fs.existsSync(settingsPath)) {
|
||||
try {
|
||||
settings = readJsonObject(settingsPath, 'Claude settings');
|
||||
} catch (_error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.includeCoAuthoredBy === true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
settingsPath,
|
||||
formatJson({
|
||||
...settings,
|
||||
includeCoAuthoredBy: false,
|
||||
}),
|
||||
'utf8'
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
function replacePluginRootPlaceholders(value, pluginRoot) {
|
||||
if (!pluginRoot) {
|
||||
return value;
|
||||
@@ -325,6 +365,11 @@ function applyInstallPlan(plan, dependencies = {}) {
|
||||
if (hasLegacyMigration) {
|
||||
removeLegacyClaudeSkillFiles(migration, plan.targetRoot);
|
||||
}
|
||||
|
||||
if (shouldSetClaudeCommitAttributionPreference(appliedPlan)) {
|
||||
writeClaudeCommitAttributionPreference(path.join(plan.targetRoot, 'settings.json'));
|
||||
}
|
||||
|
||||
const finalState = stateWithContentDigests(migration.finalState);
|
||||
if (typeof beforeInstallStateWrite === 'function') {
|
||||
beforeInstallStateWrite({ plan: appliedPlan, state: finalState });
|
||||
|
||||
@@ -260,6 +260,7 @@ test('an existing single-scope install defaults to its detected scope', () => {
|
||||
['plugin', 'list', '--json'],
|
||||
]);
|
||||
const settings = JSON.parse(fs.readFileSync(fixture.settingsPath, 'utf8'));
|
||||
assert.strictEqual(settings.includeCoAuthoredBy, false);
|
||||
assert.strictEqual(settings.pluginConfigs['ecc@ecc'].options.hook_profile, 'minimal');
|
||||
});
|
||||
});
|
||||
@@ -345,6 +346,7 @@ test('same-scope repeat setup updates ECC and changes durable user hook preferen
|
||||
setupClaudePlugin(setupOptions(fixture, { scope: 'local', hooks: 'off' }));
|
||||
const settings = JSON.parse(fs.readFileSync(fixture.settingsPath, 'utf8'));
|
||||
assert.strictEqual(settings.theme, 'dark');
|
||||
assert.strictEqual(settings.includeCoAuthoredBy, false);
|
||||
assert.deepStrictEqual(settings.pluginConfigs['another@market'], { enabled: false });
|
||||
assert.deepStrictEqual(settings.pluginConfigs['ecc@ecc'].futureKey, { keep: true });
|
||||
assert.strictEqual(settings.pluginConfigs['ecc@ecc'].options.unknown, 'keep');
|
||||
@@ -373,11 +375,36 @@ test('repeat setup preserves the current hook preference when --hooks is omitted
|
||||
const result = setupClaudePlugin(setupOptions(fixture, { hooks: undefined }));
|
||||
const settings = JSON.parse(fs.readFileSync(fixture.settingsPath, 'utf8'));
|
||||
assert.strictEqual(result.hooks, 'off');
|
||||
assert.strictEqual(settings.includeCoAuthoredBy, false);
|
||||
assert.strictEqual(settings.pluginConfigs['ecc@ecc'].options.hooks_enabled, false);
|
||||
assert.strictEqual(settings.pluginConfigs['ecc@ecc'].options.hook_profile, 'strict');
|
||||
});
|
||||
});
|
||||
|
||||
test('setup preserves an explicit includeCoAuthoredBy opt-in', () => {
|
||||
withFixture({
|
||||
plugins: [installedPlugin('user')],
|
||||
marketplaces: [officialMarketplace('user')],
|
||||
}, fixture => {
|
||||
fs.writeFileSync(fixture.settingsPath, `${JSON.stringify({
|
||||
includeCoAuthoredBy: true,
|
||||
pluginConfigs: {
|
||||
'ecc@ecc': {
|
||||
options: {
|
||||
hooks_enabled: true,
|
||||
hook_profile: 'minimal',
|
||||
},
|
||||
},
|
||||
},
|
||||
}, null, 2)}\n`);
|
||||
|
||||
setupClaudePlugin(setupOptions(fixture, { hooks: 'strict' }));
|
||||
const settings = JSON.parse(fs.readFileSync(fixture.settingsPath, 'utf8'));
|
||||
assert.strictEqual(settings.includeCoAuthoredBy, true);
|
||||
assert.strictEqual(settings.pluginConfigs['ecc@ecc'].options.hook_profile, 'strict');
|
||||
});
|
||||
});
|
||||
|
||||
test('malformed user settings fail preflight without provider mutation or corruption', () => {
|
||||
withFixture({}, fixture => {
|
||||
const malformed = '{"theme":';
|
||||
|
||||
@@ -321,11 +321,17 @@ test('destination-only migration honors explicit hook preferences and reports dr
|
||||
assert.strictEqual(result.action, 'already-migrated');
|
||||
assert.strictEqual(result.dryRun, true);
|
||||
assert.strictEqual(result.preferencesUpdated, false);
|
||||
assert.deepStrictEqual(result.plannedActions, [{
|
||||
action: 'write-hook-preferences',
|
||||
hooks_enabled: false,
|
||||
hook_profile: 'standard',
|
||||
}]);
|
||||
assert.deepStrictEqual(result.plannedActions, [
|
||||
{
|
||||
action: 'write-hook-preferences',
|
||||
hooks_enabled: false,
|
||||
hook_profile: 'standard',
|
||||
},
|
||||
{
|
||||
action: 'write-commit-attribution-preference',
|
||||
includeCoAuthoredBy: false,
|
||||
},
|
||||
]);
|
||||
assert.ok(!fs.existsSync(fixture.settingsPath));
|
||||
});
|
||||
});
|
||||
@@ -614,7 +620,10 @@ test('migration preserves hook preferences unless --hooks is explicit', () => {
|
||||
assert.strictEqual(result.hooks, 'off');
|
||||
assert.deepStrictEqual(
|
||||
JSON.parse(fs.readFileSync(fixture.settingsPath, 'utf8')),
|
||||
original
|
||||
{
|
||||
...original,
|
||||
includeCoAuthoredBy: false,
|
||||
}
|
||||
);
|
||||
assert.ok(readCalls(fixture).some(argv => (
|
||||
JSON.stringify(argv) === JSON.stringify(installArgv('project', 'off', 'strict'))
|
||||
@@ -637,6 +646,7 @@ test('migration preserves hook preferences unless --hooks is explicit', () => {
|
||||
migrateClaudePluginScope(migrationOptions(fixture, 'project', { hooks: 'strict' }));
|
||||
const settings = JSON.parse(fs.readFileSync(fixture.settingsPath, 'utf8'));
|
||||
assert.strictEqual(settings.theme, 'dark');
|
||||
assert.strictEqual(settings.includeCoAuthoredBy, false);
|
||||
assert.strictEqual(settings.pluginConfigs['ecc@ecc'].futureKey, true);
|
||||
assert.strictEqual(settings.pluginConfigs['ecc@ecc'].options.unknown, 'keep');
|
||||
assert.strictEqual(settings.pluginConfigs['ecc@ecc'].options.hooks_enabled, true);
|
||||
@@ -644,5 +654,26 @@ test('migration preserves hook preferences unless --hooks is explicit', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('migration preserves an explicit includeCoAuthoredBy opt-in', () => {
|
||||
withFixture({
|
||||
plugins: [plugin('user')],
|
||||
marketplaces: [marketplace('user')],
|
||||
}, fixture => {
|
||||
fs.writeFileSync(fixture.settingsPath, `${JSON.stringify({
|
||||
includeCoAuthoredBy: true,
|
||||
pluginConfigs: {
|
||||
'ecc@ecc': {
|
||||
options: { hooks_enabled: true, hook_profile: 'minimal' },
|
||||
},
|
||||
},
|
||||
}, null, 2)}\n`);
|
||||
|
||||
migrateClaudePluginScope(migrationOptions(fixture, 'project', { hooks: 'strict' }));
|
||||
const settings = JSON.parse(fs.readFileSync(fixture.settingsPath, 'utf8'));
|
||||
assert.strictEqual(settings.includeCoAuthoredBy, true);
|
||||
assert.strictEqual(settings.pluginConfigs['ecc@ecc'].options.hook_profile, 'strict');
|
||||
});
|
||||
});
|
||||
|
||||
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
|
||||
@@ -681,7 +681,7 @@ function runTests() {
|
||||
assert.ok(result.stderr.includes('Unknown install module: ghost-module'));
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('installs claude hooks without generating settings.json', () => {
|
||||
if (test('installs claude hooks and defaults commit attribution off', () => {
|
||||
const homeDir = createTempDir('install-apply-home-');
|
||||
const projectDir = createTempDir('install-apply-project-');
|
||||
|
||||
@@ -691,7 +691,10 @@ function runTests() {
|
||||
|
||||
const claudeRoot = path.join(homeDir, '.claude');
|
||||
assert.ok(fs.existsSync(path.join(claudeRoot, 'hooks', 'hooks.json')), 'hooks.json should be copied');
|
||||
assert.ok(!fs.existsSync(path.join(claudeRoot, 'settings.json')), 'settings.json should not be created just to install managed hooks');
|
||||
assert.deepStrictEqual(
|
||||
readJson(path.join(claudeRoot, 'settings.json')),
|
||||
{ includeCoAuthoredBy: false }
|
||||
);
|
||||
} finally {
|
||||
cleanup(homeDir);
|
||||
cleanup(projectDir);
|
||||
@@ -742,7 +745,7 @@ function runTests() {
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('preserves existing settings.json without mutating it during claude install', () => {
|
||||
if (test('preserves existing settings.json while disabling Claude co-author attribution', () => {
|
||||
const homeDir = createTempDir('install-apply-home-');
|
||||
const projectDir = createTempDir('install-apply-project-');
|
||||
|
||||
@@ -766,6 +769,7 @@ function runTests() {
|
||||
|
||||
const settings = readJson(path.join(claudeRoot, 'settings.json'));
|
||||
assert.strictEqual(settings.effortLevel, 'high', 'existing effortLevel should be preserved');
|
||||
assert.strictEqual(settings.includeCoAuthoredBy, false, 'Claude co-author attribution should be disabled by default');
|
||||
assert.deepStrictEqual(settings.env, { MY_VAR: '1' }, 'existing env should be preserved');
|
||||
assert.deepStrictEqual(
|
||||
settings.hooks.UserPromptSubmit,
|
||||
@@ -857,7 +861,7 @@ function runTests() {
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('reinstall does not create settings.json when only managed hooks are installed', () => {
|
||||
if (test('reinstall keeps commit attribution disabled when only managed hooks are installed', () => {
|
||||
const homeDir = createTempDir('install-apply-home-');
|
||||
const projectDir = createTempDir('install-apply-project-');
|
||||
|
||||
@@ -868,14 +872,17 @@ function runTests() {
|
||||
const secondInstall = run(['--profile', 'core'], { cwd: projectDir, homeDir });
|
||||
assert.strictEqual(secondInstall.code, 0, secondInstall.stderr);
|
||||
|
||||
assert.ok(!fs.existsSync(path.join(homeDir, '.claude', 'settings.json')));
|
||||
assert.deepStrictEqual(
|
||||
readJson(path.join(homeDir, '.claude', 'settings.json')),
|
||||
{ includeCoAuthoredBy: false }
|
||||
);
|
||||
} finally {
|
||||
cleanup(homeDir);
|
||||
cleanup(projectDir);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('reinstall leaves pre-existing hook-based settings.json untouched', () => {
|
||||
if (test('reinstall leaves pre-existing hook-based settings.json untouched apart from co-author preference', () => {
|
||||
const homeDir = createTempDir('install-apply-home-');
|
||||
const projectDir = createTempDir('install-apply-project-');
|
||||
|
||||
@@ -894,7 +901,35 @@ function runTests() {
|
||||
assert.strictEqual(secondInstall.code, 0, secondInstall.stderr);
|
||||
|
||||
const afterSecondInstall = readJson(settingsPath);
|
||||
assert.deepStrictEqual(afterSecondInstall, legacySettings);
|
||||
assert.deepStrictEqual(afterSecondInstall, {
|
||||
...legacySettings,
|
||||
includeCoAuthoredBy: false,
|
||||
});
|
||||
} finally {
|
||||
cleanup(homeDir);
|
||||
cleanup(projectDir);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('reinstall preserves an explicit includeCoAuthoredBy opt-in', () => {
|
||||
const homeDir = createTempDir('install-apply-home-');
|
||||
const projectDir = createTempDir('install-apply-project-');
|
||||
|
||||
try {
|
||||
const claudeRoot = path.join(homeDir, '.claude');
|
||||
fs.mkdirSync(claudeRoot, { recursive: true });
|
||||
const settingsPath = path.join(claudeRoot, 'settings.json');
|
||||
const customSettings = {
|
||||
includeCoAuthoredBy: true,
|
||||
theme: 'dark',
|
||||
};
|
||||
fs.writeFileSync(settingsPath, JSON.stringify(customSettings, null, 2));
|
||||
|
||||
const install = run(['--profile', 'core'], { cwd: projectDir, homeDir });
|
||||
assert.strictEqual(install.code, 0, install.stderr);
|
||||
|
||||
const afterInstall = readJson(settingsPath);
|
||||
assert.deepStrictEqual(afterInstall, customSettings);
|
||||
} finally {
|
||||
cleanup(homeDir);
|
||||
cleanup(projectDir);
|
||||
|
||||
Reference in New Issue
Block a user