mirror of
https://github.com/affaan-m/ECC.git
synced 2026-10-23 03:27:50 +09:00
fix: recover plugin migration at the effective settings scope
This commit is contained in:
2 files changed
+119
-19
No files matched your search
@@ -46,48 +46,60 @@ function recoveryCommands(sourceScope, destinationScope) {
|
||||
return commands;
|
||||
}
|
||||
|
||||
function enableRecoveryCommands(destinationScope) {
|
||||
function enableRecoveryCommands(destinationScope, enableScope = destinationScope) {
|
||||
return [
|
||||
`claude plugin enable ${CURRENT_PLUGIN_ID} --scope ${destinationScope}`,
|
||||
`claude plugin enable ${CURRENT_PLUGIN_ID} --scope ${enableScope}`,
|
||||
...recoveryCommands(null, destinationScope),
|
||||
];
|
||||
}
|
||||
|
||||
function destinationSettingsPaths(paths, destinationScope) {
|
||||
function destinationSettingsPaths(paths) {
|
||||
const projectSettingsDir = path.join(paths.projectRoot, '.claude');
|
||||
if (destinationScope === 'user') {
|
||||
return [path.join(paths.configDir, 'settings.json')];
|
||||
}
|
||||
if (destinationScope === 'project') {
|
||||
return [path.join(projectSettingsDir, 'settings.json')];
|
||||
}
|
||||
return [
|
||||
path.join(projectSettingsDir, 'settings.local.json'),
|
||||
path.join(projectSettingsDir, 'settings.json'),
|
||||
{ scope: 'local', settingsPath: path.join(projectSettingsDir, 'settings.local.json') },
|
||||
{ scope: 'project', settingsPath: path.join(projectSettingsDir, 'settings.json') },
|
||||
{ scope: 'user', settingsPath: path.join(paths.configDir, 'settings.json') },
|
||||
];
|
||||
}
|
||||
|
||||
function readExplicitlyDisabledSettings(settingsPath) {
|
||||
function readPluginEnabledSetting(settingsPath) {
|
||||
try {
|
||||
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
||||
return settings?.enabledPlugins?.[CURRENT_PLUGIN_ID] === false;
|
||||
const enabled = settings?.enabledPlugins?.[CURRENT_PLUGIN_ID];
|
||||
return typeof enabled === 'boolean' ? enabled : null;
|
||||
} catch {
|
||||
// Missing or unreadable settings are not an explicit user choice.
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function effectiveDestinationSetting(paths) {
|
||||
for (const setting of destinationSettingsPaths(paths)) {
|
||||
const enabled = readPluginEnabledSetting(setting.settingsPath);
|
||||
if (enabled !== null) return { ...setting, enabled };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function destinationEnableRecoveryCommands(paths, destinationScope, setting = effectiveDestinationSetting(paths)) {
|
||||
const scopes = ['user', 'project', 'local'];
|
||||
const enableScope = setting?.enabled === false
|
||||
&& scopes.indexOf(setting.scope) > scopes.indexOf(destinationScope)
|
||||
? setting.scope : destinationScope;
|
||||
return enableRecoveryCommands(destinationScope, enableScope);
|
||||
}
|
||||
|
||||
function destinationExplicitlyDisabledError(paths, plugins, destinationScope) {
|
||||
const installed = currentEccPlugins(plugins);
|
||||
const destination = installed.find(plugin => plugin.scope === destinationScope);
|
||||
if (!destination || destination.enabled === true) {
|
||||
return null;
|
||||
}
|
||||
const disabledSettingsPaths = destinationSettingsPaths(paths, destinationScope)
|
||||
.filter(readExplicitlyDisabledSettings);
|
||||
if (disabledSettingsPaths.length === 0) {
|
||||
const effectiveSetting = effectiveDestinationSetting(paths);
|
||||
if (!effectiveSetting || effectiveSetting.enabled !== false) {
|
||||
return null;
|
||||
}
|
||||
const disabledSettingsPaths = [effectiveSetting.settingsPath];
|
||||
return migrationError(
|
||||
'DESTINATION_EXPLICITLY_DISABLED',
|
||||
`${CURRENT_PLUGIN_ID} is installed at ${destinationScope} scope but explicitly `
|
||||
@@ -98,7 +110,7 @@ function destinationExplicitlyDisabledError(paths, plugins, destinationScope) {
|
||||
phase: 'destination-verification',
|
||||
observedScopes: installed.map(plugin => plugin.scope),
|
||||
settingsPaths: disabledSettingsPaths,
|
||||
recovery: enableRecoveryCommands(destinationScope),
|
||||
recovery: destinationEnableRecoveryCommands(paths, destinationScope, effectiveSetting),
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -244,7 +256,7 @@ function verifySourceAndDestination(run, paths, migration, destinationScope, pha
|
||||
: `Could not verify ${CURRENT_PLUGIN_ID} at the destination before source cleanup.`,
|
||||
phase,
|
||||
recovery: destination && destination.enabled !== true
|
||||
? enableRecoveryCommands(destinationScope)
|
||||
? destinationEnableRecoveryCommands(paths, destinationScope)
|
||||
: recoveryCommands(null, destinationScope),
|
||||
}
|
||||
);
|
||||
|
||||
@@ -836,5 +836,93 @@ test('a two-scope disabled destination offers enable recovery without removing t
|
||||
});
|
||||
});
|
||||
|
||||
for (const destinationScope of ['project', 'user']) {
|
||||
test(`local settings control recovery for a disabled ${destinationScope} destination`, () => {
|
||||
const sourceScope = destinationScope === 'project' ? 'user' : 'project';
|
||||
withFixture({ plugins: [plugin(sourceScope), plugin(destinationScope, { enabled: false })] }, fixture => {
|
||||
const localSettings = path.join(fixture.projectRoot, '.claude', 'settings.local.json');
|
||||
writeExplicitDisable(localSettings);
|
||||
const before = fs.readFileSync(localSettings, 'utf8');
|
||||
const error = captureError(() => migrateClaudePluginScope(migrationOptions(fixture, destinationScope)));
|
||||
assert.strictEqual(error.code, 'DESTINATION_EXPLICITLY_DISABLED');
|
||||
assert.ok(error.message.includes(localSettings));
|
||||
assert.strictEqual(error.recovery[0], 'claude plugin enable ecc@ecc --scope local');
|
||||
assert.strictEqual(error.recovery[1], `ecc setup --mode claude-plugin --scope ${destinationScope} --move-scope --yes`);
|
||||
assert.deepStrictEqual(mutationCalls(fixture), []);
|
||||
assert.strictEqual(fs.readFileSync(localSettings, 'utf8'), before);
|
||||
assert.strictEqual(readState(fixture).plugins.length, 2);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
test('a newly installed disabled destination points recovery at the local override before cleanup', () => {
|
||||
withFixture({
|
||||
plugins: [plugin('user')],
|
||||
marketplaces: [marketplace('user')],
|
||||
pluginListResponses: [
|
||||
[plugin('user')],
|
||||
[plugin('user'), plugin('project', { enabled: false })],
|
||||
],
|
||||
}, fixture => {
|
||||
const localSettings = path.join(fixture.projectRoot, '.claude', 'settings.local.json');
|
||||
writeExplicitDisable(localSettings);
|
||||
const before = fs.readFileSync(localSettings, 'utf8');
|
||||
const error = captureError(() => migrateClaudePluginScope(migrationOptions(fixture, 'project')));
|
||||
assert.strictEqual(error.code, 'DESTINATION_VERIFICATION_FAILED');
|
||||
assert.strictEqual(error.recovery[0], 'claude plugin enable ecc@ecc --scope local');
|
||||
assert.ok(!readCalls(fixture).some(argv => argv[1] === 'uninstall'));
|
||||
assert.strictEqual(fs.readFileSync(localSettings, 'utf8'), before);
|
||||
assert.ok(readState(fixture).plugins.some(entry => entry.scope === 'user'));
|
||||
});
|
||||
});
|
||||
|
||||
test('a higher-precedence true does not blame an overridden false', () => {
|
||||
withFixture({ plugins: [plugin('project', { enabled: false })] }, fixture => {
|
||||
const projectSettings = path.join(fixture.projectRoot, '.claude', 'settings.json');
|
||||
const localSettings = path.join(fixture.projectRoot, '.claude', 'settings.local.json');
|
||||
writeExplicitDisable(projectSettings);
|
||||
fs.writeFileSync(localSettings, JSON.stringify({ enabledPlugins: { 'ecc@ecc': true } }));
|
||||
const error = captureError(() => migrateClaudePluginScope(migrationOptions(fixture, 'project')));
|
||||
assert.strictEqual(error.code, 'DESTINATION_VERIFICATION_FAILED');
|
||||
assert.deepStrictEqual(mutationCalls(fixture), []);
|
||||
});
|
||||
});
|
||||
|
||||
test('an unrelated local plugin entry leaves the project choice effective', () => {
|
||||
withFixture({ plugins: [plugin('project', { enabled: false })] }, fixture => {
|
||||
const projectSettings = path.join(fixture.projectRoot, '.claude', 'settings.json');
|
||||
const localSettings = path.join(fixture.projectRoot, '.claude', 'settings.local.json');
|
||||
writeExplicitDisable(projectSettings);
|
||||
writeExplicitDisable(fixture.settingsPath);
|
||||
fs.writeFileSync(localSettings, JSON.stringify({ enabledPlugins: { 'other@marketplace': true } }));
|
||||
const error = captureError(() => migrateClaudePluginScope(migrationOptions(fixture, 'project')));
|
||||
assert.strictEqual(error.code, 'DESTINATION_EXPLICITLY_DISABLED');
|
||||
assert.ok(error.message.includes(projectSettings));
|
||||
assert.ok(!error.message.includes(fixture.settingsPath));
|
||||
assert.strictEqual(error.recovery[0], 'claude plugin enable ecc@ecc --scope project');
|
||||
assert.deepStrictEqual(mutationCalls(fixture), []);
|
||||
});
|
||||
});
|
||||
|
||||
test('the public setup CLI preserves locally disabled project migration and names local recovery', () => {
|
||||
withFixture({ plugins: [plugin('user'), plugin('project', { enabled: false })] }, fixture => {
|
||||
const localSettings = path.join(fixture.projectRoot, '.claude', 'settings.local.json');
|
||||
writeExplicitDisable(localSettings);
|
||||
const before = fs.readFileSync(localSettings, 'utf8');
|
||||
const result = require('child_process').spawnSync(process.execPath, [
|
||||
path.join(repoRoot, 'scripts/ecc.js'), 'setup', '--mode', 'claude-plugin',
|
||||
'--scope', 'project', '--move-scope', '--yes', '--json',
|
||||
], { cwd: fixture.projectRoot, env: process.env, encoding: 'utf8', timeout: 15000 });
|
||||
assert.strictEqual(result.error, undefined);
|
||||
assert.strictEqual(result.status, 1, result.stderr);
|
||||
const error = JSON.parse(result.stderr).error;
|
||||
assert.strictEqual(error.code, 'DESTINATION_EXPLICITLY_DISABLED');
|
||||
assert.strictEqual(error.recovery[0], 'claude plugin enable ecc@ecc --scope local');
|
||||
assert.deepStrictEqual(mutationCalls(fixture), []);
|
||||
assert.strictEqual(fs.readFileSync(localSettings, 'utf8'), before);
|
||||
assert.strictEqual(readState(fixture).plugins.length, 2);
|
||||
});
|
||||
});
|
||||
|
||||
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
Reference in new issue
Block a user