mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 12:14:19 +00:00
fix(shared-manager): prune transient and malformed marketplace entries
This commit is contained in:
1 parent
5b262f0139
commit
2293368e31
2 files changed
+55
-14
No files matched your search
@@ -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,
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
Reference in new issue
Block a user