fix(shared-manager): adopt diverged plugin registry files in linkInstancePlugins

The same atomic-rename divergence occurs in the plugins subtree: a
plugin install inside a session rewrites plugins/installed_plugins.json,
replacing the instance-level symlink with a regular file. The per-launch
relink then discarded it, so the plugin was effectively uninstalled on
every relaunch while settings.json still marked it enabled — sessions
then fail with 'Unknown skill: <plugin>:<skill>'.

Move adoptDivergedFileContent to fs-helpers (avoids a circular import)
and apply it to file-type plugin entries before re-linking.
This commit is contained in:
Nelson Melo committed 2026-07-31 13:40:23 -04:00
1 parent 481f013ec2
commit fa6b27f159
4 files changed
+70 -35

No files matched your search

+23
View File
@@ -347,6 +347,29 @@ describe('SharedManager', () => {
expect(readJson(claudeSettingsPath)).toEqual({ theme: 'dark' });
});
it('adopts a diverged instance plugin registry during instance linking', () => {
const manager = new SharedManager();
const instancePath = instanceDir('work');
const claudeRegistryPath = path.join(claudeDir(), 'plugins', 'installed_plugins.json');
const instanceRegistryPath = path.join(instancePath, 'plugins', 'installed_plugins.json');
fs.mkdirSync(claudeDir(), { recursive: true });
fs.mkdirSync(path.join(instancePath, 'plugins'), { recursive: true });
writeJson(claudeRegistryPath, { version: 2, plugins: {} });
// Simulate Claude Code's atomic save replacing the managed symlink with
// a regular file that records a plugin installed inside a session.
writeJson(instanceRegistryPath, {
version: 2,
plugins: { 'demo@demo-market': [{ scope: 'user', version: '1.0.0' }] },
});
manager.linkSharedDirectories(instancePath);
expect(fs.lstatSync(instanceRegistryPath).isSymbolicLink()).toBe(true);
const adopted = readJson(claudeRegistryPath) as { plugins: Record<string, unknown> };
expect(Object.keys(adopted.plugins)).toContain('demo@demo-market');
});
it('does not create a backup when the diverged copy matches the canonical file', () => {
const manager = new SharedManager();
const claudeSettingsPath = path.join(claudeDir(), 'settings.json');