mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-17 16:16:37 +00:00
fix(shared-manager): normalize marketplace registry paths
This commit is contained in:
@@ -60,6 +60,7 @@ class InstanceManager {
|
|||||||
|
|
||||||
// Sync MCP servers from global ~/.claude.json (unless bare)
|
// Sync MCP servers from global ~/.claude.json (unless bare)
|
||||||
if (!options.bare) {
|
if (!options.bare) {
|
||||||
|
this.sharedManager.normalizeSharedPluginMetadataPaths();
|
||||||
this.syncMcpServers(instancePath);
|
this.syncMcpServers(instancePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -210,8 +210,7 @@ class SharedManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalize plugin registry paths after linking
|
this.normalizeSharedPluginMetadataPaths();
|
||||||
this.normalizePluginRegistryPaths();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -539,6 +538,14 @@ class SharedManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize shared plugin metadata files to canonical ~/.claude/ paths.
|
||||||
|
*/
|
||||||
|
normalizeSharedPluginMetadataPaths(): void {
|
||||||
|
this.normalizePluginRegistryPaths();
|
||||||
|
this.normalizeMarketplaceRegistryPaths();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalize plugin registry paths to use canonical ~/.claude/ paths
|
* Normalize plugin registry paths to use canonical ~/.claude/ paths
|
||||||
* instead of instance-specific ~/.ccs/instances/<name>/ paths.
|
* instead of instance-specific ~/.ccs/instances/<name>/ paths.
|
||||||
@@ -547,9 +554,33 @@ class SharedManager {
|
|||||||
* which CCS instance installed the plugin.
|
* which CCS instance installed the plugin.
|
||||||
*/
|
*/
|
||||||
normalizePluginRegistryPaths(): void {
|
normalizePluginRegistryPaths(): void {
|
||||||
const registryPath = path.join(this.claudeDir, 'plugins', 'installed_plugins.json');
|
this.normalizePluginMetadataFile(
|
||||||
|
path.join(this.claudeDir, 'plugins', 'installed_plugins.json'),
|
||||||
|
'Normalized plugin registry paths',
|
||||||
|
'plugin registry'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Skip if registry doesn't exist
|
/**
|
||||||
|
* Normalize marketplace registry paths to use canonical ~/.claude/ paths
|
||||||
|
* instead of instance-specific ~/.ccs/instances/<name>/ paths.
|
||||||
|
*
|
||||||
|
* This ensures known_marketplaces.json is consistent regardless of
|
||||||
|
* which CCS instance added the marketplace.
|
||||||
|
*/
|
||||||
|
normalizeMarketplaceRegistryPaths(): void {
|
||||||
|
this.normalizePluginMetadataFile(
|
||||||
|
path.join(this.claudeDir, 'plugins', 'known_marketplaces.json'),
|
||||||
|
'Normalized marketplace registry paths',
|
||||||
|
'marketplace registry'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalizePluginMetadataFile(
|
||||||
|
registryPath: string,
|
||||||
|
successMessage: string,
|
||||||
|
warningLabel: string
|
||||||
|
): void {
|
||||||
if (!fs.existsSync(registryPath)) {
|
if (!fs.existsSync(registryPath)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -557,20 +588,16 @@ class SharedManager {
|
|||||||
try {
|
try {
|
||||||
const original = fs.readFileSync(registryPath, 'utf8');
|
const original = fs.readFileSync(registryPath, 'utf8');
|
||||||
|
|
||||||
// Replace instance paths with canonical claude path
|
|
||||||
// Pattern: /.ccs/instances/<instance-name>/ -> /.claude/
|
// Pattern: /.ccs/instances/<instance-name>/ -> /.claude/
|
||||||
const normalized = original.replace(/\/\.ccs\/instances\/[^/]+\//g, '/.claude/');
|
const normalized = original.replace(/\/\.ccs\/instances\/[^/]+\//g, '/.claude/');
|
||||||
|
|
||||||
// Only write if changes were made
|
|
||||||
if (normalized !== original) {
|
if (normalized !== original) {
|
||||||
// Validate JSON before writing
|
|
||||||
JSON.parse(normalized);
|
JSON.parse(normalized);
|
||||||
fs.writeFileSync(registryPath, normalized, 'utf8');
|
fs.writeFileSync(registryPath, normalized, 'utf8');
|
||||||
console.log(ok('Normalized plugin registry paths'));
|
console.log(ok(successMessage));
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Log warning but don't fail - registry may be malformed
|
console.log(warn(`Could not normalize ${warningLabel}: ${(err as Error).message}`));
|
||||||
console.log(warn(`Could not normalize plugin registry: ${(err as Error).message}`));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -101,6 +101,10 @@ describe('InstanceManager MCP sync', () => {
|
|||||||
);
|
);
|
||||||
spyOn(SharedManager.prototype, 'syncProjectContext').mockResolvedValue(undefined);
|
spyOn(SharedManager.prototype, 'syncProjectContext').mockResolvedValue(undefined);
|
||||||
spyOn(SharedManager.prototype, 'syncAdvancedContinuityArtifacts').mockResolvedValue(undefined);
|
spyOn(SharedManager.prototype, 'syncAdvancedContinuityArtifacts').mockResolvedValue(undefined);
|
||||||
|
const normalizeSharedPluginMetadataSpy = spyOn(
|
||||||
|
SharedManager.prototype,
|
||||||
|
'normalizeSharedPluginMetadataPaths'
|
||||||
|
).mockImplementation(() => {});
|
||||||
const syncMcpSpy = spyOn(InstanceManager.prototype, 'syncMcpServers').mockImplementation(
|
const syncMcpSpy = spyOn(InstanceManager.prototype, 'syncMcpServers').mockImplementation(
|
||||||
() => false
|
() => false
|
||||||
);
|
);
|
||||||
@@ -109,6 +113,28 @@ describe('InstanceManager MCP sync', () => {
|
|||||||
await manager.ensureInstance('sandbox', { mode: 'isolated' }, { bare: true });
|
await manager.ensureInstance('sandbox', { mode: 'isolated' }, { bare: true });
|
||||||
|
|
||||||
expect(linkSharedSpy).not.toHaveBeenCalled();
|
expect(linkSharedSpy).not.toHaveBeenCalled();
|
||||||
|
expect(normalizeSharedPluginMetadataSpy).not.toHaveBeenCalled();
|
||||||
expect(syncMcpSpy).not.toHaveBeenCalled();
|
expect(syncMcpSpy).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('normalizes shared plugin metadata for existing non-bare instances', async () => {
|
||||||
|
spyOn(SharedManager.prototype, 'syncProjectContext').mockResolvedValue(undefined);
|
||||||
|
spyOn(SharedManager.prototype, 'syncAdvancedContinuityArtifacts').mockResolvedValue(undefined);
|
||||||
|
const normalizeSharedPluginMetadataSpy = spyOn(
|
||||||
|
SharedManager.prototype,
|
||||||
|
'normalizeSharedPluginMetadataPaths'
|
||||||
|
).mockImplementation(() => {});
|
||||||
|
const syncMcpSpy = spyOn(InstanceManager.prototype, 'syncMcpServers').mockImplementation(
|
||||||
|
() => false
|
||||||
|
);
|
||||||
|
|
||||||
|
const manager = new InstanceManager();
|
||||||
|
const instancePath = manager.getInstancePath('work');
|
||||||
|
fs.mkdirSync(instancePath, { recursive: true });
|
||||||
|
|
||||||
|
await manager.ensureInstance('work', { mode: 'isolated' });
|
||||||
|
|
||||||
|
expect(normalizeSharedPluginMetadataSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(syncMcpSpy).toHaveBeenCalledWith(instancePath);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
/**
|
/**
|
||||||
* Unit tests for SharedManager - plugin registry path normalization
|
* Unit tests for SharedManager - plugin registry path normalization
|
||||||
*/
|
*/
|
||||||
import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
|
import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from 'bun:test';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
|
import SharedManager from '../../src/management/shared-manager';
|
||||||
|
|
||||||
// Test the normalization regex pattern directly
|
// Test the normalization regex pattern directly
|
||||||
const normalizePluginPaths = (content: string): string => {
|
const normalizePluginPaths = (content: string): string => {
|
||||||
@@ -12,6 +13,40 @@ const normalizePluginPaths = (content: string): string => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
describe('SharedManager', () => {
|
describe('SharedManager', () => {
|
||||||
|
let tempRoot = '';
|
||||||
|
let originalHome: string | undefined;
|
||||||
|
let originalCcsHome: string | undefined;
|
||||||
|
let originalCcsDir: string | undefined;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-shared-manager-test-'));
|
||||||
|
originalHome = process.env.HOME;
|
||||||
|
originalCcsHome = process.env.CCS_HOME;
|
||||||
|
originalCcsDir = process.env.CCS_DIR;
|
||||||
|
|
||||||
|
spyOn(os, 'homedir').mockReturnValue(tempRoot);
|
||||||
|
process.env.HOME = tempRoot;
|
||||||
|
process.env.CCS_HOME = tempRoot;
|
||||||
|
delete process.env.CCS_DIR;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
mock.restore();
|
||||||
|
|
||||||
|
if (originalHome !== undefined) process.env.HOME = originalHome;
|
||||||
|
else delete process.env.HOME;
|
||||||
|
|
||||||
|
if (originalCcsHome !== undefined) process.env.CCS_HOME = originalCcsHome;
|
||||||
|
else delete process.env.CCS_HOME;
|
||||||
|
|
||||||
|
if (originalCcsDir !== undefined) process.env.CCS_DIR = originalCcsDir;
|
||||||
|
else delete process.env.CCS_DIR;
|
||||||
|
|
||||||
|
if (tempRoot && fs.existsSync(tempRoot)) {
|
||||||
|
fs.rmSync(tempRoot, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
describe('normalizePluginRegistryPaths', () => {
|
describe('normalizePluginRegistryPaths', () => {
|
||||||
describe('regex pattern', () => {
|
describe('regex pattern', () => {
|
||||||
it('should replace instance paths with canonical claude path', () => {
|
it('should replace instance paths with canonical claude path', () => {
|
||||||
@@ -82,6 +117,24 @@ describe('SharedManager', () => {
|
|||||||
'/home/kai/.claude/plugins/cache/claude-hud/claude-hud/0.0.2'
|
'/home/kai/.claude/plugins/cache/claude-hud/claude-hud/0.0.2'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should normalize marketplace installLocation values', () => {
|
||||||
|
const original = {
|
||||||
|
'claude-code-plugins': {
|
||||||
|
installLocation:
|
||||||
|
'/home/kai/.ccs/instances/work/plugins/marketplaces/claude-code-plugins',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const input = JSON.stringify(original, null, 2);
|
||||||
|
const result = normalizePluginPaths(input);
|
||||||
|
|
||||||
|
expect(() => JSON.parse(result)).not.toThrow();
|
||||||
|
|
||||||
|
const parsed = JSON.parse(result);
|
||||||
|
expect(parsed['claude-code-plugins'].installLocation).toBe(
|
||||||
|
'/home/kai/.claude/plugins/marketplaces/claude-code-plugins'
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('edge cases', () => {
|
describe('edge cases', () => {
|
||||||
@@ -102,4 +155,35 @@ describe('SharedManager', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('normalizeMarketplaceRegistryPaths', () => {
|
||||||
|
it('rewrites known_marketplaces.json on disk', () => {
|
||||||
|
const pluginsDir = path.join(tempRoot, '.claude', 'plugins');
|
||||||
|
fs.mkdirSync(pluginsDir, { recursive: true });
|
||||||
|
|
||||||
|
const registryPath = path.join(pluginsDir, 'known_marketplaces.json');
|
||||||
|
fs.writeFileSync(
|
||||||
|
registryPath,
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
'claude-code-plugins': {
|
||||||
|
installLocation:
|
||||||
|
'/home/kai/.ccs/instances/work/plugins/marketplaces/claude-code-plugins',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
),
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
|
|
||||||
|
const manager = new SharedManager();
|
||||||
|
manager.normalizeMarketplaceRegistryPaths();
|
||||||
|
|
||||||
|
const normalized = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
|
||||||
|
expect(normalized['claude-code-plugins'].installLocation).toBe(
|
||||||
|
'/home/kai/.claude/plugins/marketplaces/claude-code-plugins'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user