fix(shared-manager): prune transient and malformed marketplace entries

This commit is contained in:
Tam Nhu Tran committed 2026-04-15 16:29:25 -04:00
1 parent 5b262f0139
commit 2293368e31
2 files changed
+55 -14

No files matched your search

+18 -5
View File
@@ -936,6 +936,10 @@ class SharedManager {
} }
for (const [name, value] of Object.entries(parsed as Record<string, unknown>)) { for (const [name, value] of Object.entries(parsed as Record<string, unknown>)) {
if (!this.isMarketplaceRegistryEntry(value)) {
continue;
}
merged[name] = normalizePluginMetadataValue(value, targetConfigDir).normalized; merged[name] = normalizePluginMetadataValue(value, targetConfigDir).normalized;
} }
} catch (err) { } catch (err) {
@@ -954,13 +958,14 @@ class SharedManager {
const entry = merged[name]; const entry = merged[name];
if (!(name in discoveredEntries)) { if (!(name in discoveredEntries)) {
delete merged[name]; delete merged[name];
} else if (entry && typeof entry === 'object' && !Array.isArray(entry)) { } else if (this.isMarketplaceRegistryEntry(entry)) {
merged[name] = { merged[name] = {
...(entry as Record<string, unknown>), ...entry,
installLocation: discoveredEntries[name].installLocation, installLocation: discoveredEntries[name].installLocation,
}; };
} else {
delete merged[name];
} }
// else: entry is in discoveredEntries but has a malformed value — preserve as-is
} }
return JSON.stringify(merged, null, 2); return JSON.stringify(merged, null, 2);
@@ -981,8 +986,8 @@ class SharedManager {
continue; continue;
} }
// Skip hidden dirs and Claude Code's .staging rename-dance work trees. // Skip hidden dirs and Claude Code rename-dance leftovers (.staging/.bak).
if (entry.name.startsWith('.') || entry.name.endsWith('.staging')) { if (this.isTransientMarketplaceDirectory(entry.name)) {
continue; continue;
} }
@@ -994,6 +999,14 @@ class SharedManager {
return discovered; return discovered;
} }
private isTransientMarketplaceDirectory(name: string): boolean {
return name.startsWith('.') || name.endsWith('.staging') || name.endsWith('.bak');
}
private isMarketplaceRegistryEntry(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
}
private writePluginMetadataFile( private writePluginMetadataFile(
registryPath: string, registryPath: string,
content: string, content: string,
+37 -9
View File
@@ -360,7 +360,7 @@ describe('SharedManager', () => {
expect(reconciled.stale).toBeUndefined(); expect(reconciled.stale).toBeUndefined();
}); });
it('does not register .staging directories left behind by interrupted marketplace auto-updates', () => { it('does not register transient marketplace directories left behind by interrupted auto-updates', () => {
// Regression: CCS used to write bare { installLocation } entries for marketplace // Regression: CCS used to write bare { installLocation } entries for marketplace
// directories with no registry record. Claude Code requires source + lastUpdated, // directories with no registry record. Claude Code requires source + lastUpdated,
// so those entries corrupted known_marketplaces.json and broke /plugin. // so those entries corrupted known_marketplaces.json and broke /plugin.
@@ -369,24 +369,29 @@ describe('SharedManager', () => {
fs.mkdirSync(instancePath, { recursive: true }); fs.mkdirSync(instancePath, { recursive: true });
manager.linkSharedDirectories(instancePath); manager.linkSharedDirectories(instancePath);
// Simulate Claude Code leaving a .staging dir behind in both the global claude dir // Simulate Claude Code leaving rename-dance temp dirs behind in both the
// and the instance dir (discoverMarketplaceEntries scans each independently). // global claude dir and the instance dir (discoverMarketplaceEntries scans
fs.mkdirSync(marketplacePath(claudeDir(), 'claude-plugins-official.staging'), { // each independently).
recursive: true, for (const suffix of ['.staging', '.bak']) {
}); fs.mkdirSync(marketplacePath(claudeDir(), `claude-plugins-official${suffix}`), {
fs.mkdirSync(marketplacePath(instancePath, 'claude-plugins-official.staging'), { recursive: true,
recursive: true, });
}); fs.mkdirSync(marketplacePath(instancePath, `claude-plugins-official${suffix}`), {
recursive: true,
});
}
manager.normalizeMarketplaceRegistryPaths(instancePath); manager.normalizeMarketplaceRegistryPaths(instancePath);
const globalRegistryPath = path.join(claudeDir(), 'plugins', 'known_marketplaces.json'); const globalRegistryPath = path.join(claudeDir(), 'plugins', 'known_marketplaces.json');
const global = readJson(globalRegistryPath) as Record<string, unknown>; const global = readJson(globalRegistryPath) as Record<string, unknown>;
expect(global['claude-plugins-official.staging']).toBeUndefined(); expect(global['claude-plugins-official.staging']).toBeUndefined();
expect(global['claude-plugins-official.bak']).toBeUndefined();
const instanceRegistryPath = path.join(instancePath, 'plugins', 'known_marketplaces.json'); const instanceRegistryPath = path.join(instancePath, 'plugins', 'known_marketplaces.json');
const instance = readJson(instanceRegistryPath) as Record<string, unknown>; const instance = readJson(instanceRegistryPath) as Record<string, unknown>;
expect(instance['claude-plugins-official.staging']).toBeUndefined(); expect(instance['claude-plugins-official.staging']).toBeUndefined();
expect(instance['claude-plugins-official.bak']).toBeUndefined();
}); });
it('removes registry entries whose physical marketplace directory no longer exists', () => { it('removes registry entries whose physical marketplace directory no longer exists', () => {
@@ -421,6 +426,29 @@ describe('SharedManager', () => {
expect(instance['vanished-marketplace']).toBeUndefined(); expect(instance['vanished-marketplace']).toBeUndefined();
}); });
it('drops malformed marketplace entries even when the payload directory still exists', () => {
const manager = new SharedManager();
const instancePath = instanceDir('work');
fs.mkdirSync(instancePath, { recursive: true });
manager.linkSharedDirectories(instancePath);
fs.mkdirSync(marketplacePath(claudeDir(), 'claude-code-plugins'), { recursive: true });
const globalRegistryPath = path.join(claudeDir(), 'plugins', 'known_marketplaces.json');
writeJson(globalRegistryPath, {
'claude-code-plugins': 'bad-entry',
});
manager.normalizeMarketplaceRegistryPaths(instancePath);
const global = readJson(globalRegistryPath) as Record<string, unknown>;
expect(global['claude-code-plugins']).toBeUndefined();
const instanceRegistryPath = path.join(instancePath, 'plugins', 'known_marketplaces.json');
const instance = readJson(instanceRegistryPath) as Record<string, unknown>;
expect(instance['claude-code-plugins']).toBeUndefined();
});
it('warns and skips malformed marketplace registries while keeping valid sources', () => { it('warns and skips malformed marketplace registries while keeping valid sources', () => {
const manager = new SharedManager(); const manager = new SharedManager();
const instancePath = instanceDir('work'); const instancePath = instanceDir('work');