mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 08:17:11 +00:00
fix(shared-manager): normalize plugin registry paths to canonical ~/.claude/
Replaces instance-specific paths (/.ccs/instances/<name>/) with canonical /.claude/ paths in installed_plugins.json after linking. Fixes #276
This commit is contained in:
@@ -194,6 +194,44 @@ class SharedManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize plugin registry paths after linking
|
||||
this.normalizePluginRegistryPaths();
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize plugin registry paths to use canonical ~/.claude/ paths
|
||||
* instead of instance-specific ~/.ccs/instances/<name>/ paths.
|
||||
*
|
||||
* This ensures installed_plugins.json is consistent regardless of
|
||||
* which CCS instance installed the plugin.
|
||||
*/
|
||||
normalizePluginRegistryPaths(): void {
|
||||
const registryPath = path.join(this.claudeDir, 'plugins', 'installed_plugins.json');
|
||||
|
||||
// Skip if registry doesn't exist
|
||||
if (!fs.existsSync(registryPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const original = fs.readFileSync(registryPath, 'utf8');
|
||||
|
||||
// Replace instance paths with canonical claude path
|
||||
// Pattern: /.ccs/instances/<instance-name>/ -> /.claude/
|
||||
const normalized = original.replace(/\/\.ccs\/instances\/[^/]+\//g, '/.claude/');
|
||||
|
||||
// Only write if changes were made
|
||||
if (normalized !== original) {
|
||||
// Validate JSON before writing
|
||||
JSON.parse(normalized);
|
||||
fs.writeFileSync(registryPath, normalized, 'utf8');
|
||||
console.log(ok('Normalized plugin registry paths'));
|
||||
}
|
||||
} catch (err) {
|
||||
// Log warning but don't fail - registry may be malformed
|
||||
console.log(warn(`Could not normalize plugin registry: ${(err as Error).message}`));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* Unit tests for SharedManager - plugin registry path normalization
|
||||
*/
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
|
||||
// Test the normalization regex pattern directly
|
||||
const normalizePluginPaths = (content: string): string => {
|
||||
return content.replace(/\/\.ccs\/instances\/[^/]+\//g, '/.claude/');
|
||||
};
|
||||
|
||||
describe('SharedManager', () => {
|
||||
describe('normalizePluginRegistryPaths', () => {
|
||||
describe('regex pattern', () => {
|
||||
it('should replace instance paths with canonical claude path', () => {
|
||||
const input = '/home/user/.ccs/instances/ck/plugins/cache/plugin/0.0.2';
|
||||
const expected = '/home/user/.claude/plugins/cache/plugin/0.0.2';
|
||||
expect(normalizePluginPaths(input)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should handle different instance names', () => {
|
||||
const inputs = [
|
||||
'/home/user/.ccs/instances/work/plugins/cache/plugin/1.0.0',
|
||||
'/home/user/.ccs/instances/personal/plugins/cache/plugin/1.0.0',
|
||||
'/home/user/.ccs/instances/test-account/plugins/cache/plugin/1.0.0',
|
||||
];
|
||||
for (const input of inputs) {
|
||||
expect(normalizePluginPaths(input)).toContain('/.claude/');
|
||||
expect(normalizePluginPaths(input)).not.toContain('/.ccs/instances/');
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle multiple occurrences', () => {
|
||||
const input = JSON.stringify({
|
||||
plugins: {
|
||||
'plugin-a': [{ installPath: '/home/user/.ccs/instances/ck/plugins/a' }],
|
||||
'plugin-b': [{ installPath: '/home/user/.ccs/instances/work/plugins/b' }],
|
||||
},
|
||||
});
|
||||
const result = normalizePluginPaths(input);
|
||||
expect(result).not.toContain('/.ccs/instances/');
|
||||
expect(result.match(/\.claude/g)?.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should not modify already-canonical paths', () => {
|
||||
const input = '/home/user/.claude/plugins/cache/plugin/0.0.2';
|
||||
expect(normalizePluginPaths(input)).toBe(input);
|
||||
});
|
||||
|
||||
it('should be idempotent', () => {
|
||||
const input = '/home/user/.ccs/instances/ck/plugins/cache/plugin/0.0.2';
|
||||
const first = normalizePluginPaths(input);
|
||||
const second = normalizePluginPaths(first);
|
||||
expect(first).toBe(second);
|
||||
});
|
||||
|
||||
it('should preserve JSON structure', () => {
|
||||
const original = {
|
||||
version: 2,
|
||||
plugins: {
|
||||
'claude-hud@claude-hud': [
|
||||
{
|
||||
scope: 'user',
|
||||
installPath: '/home/kai/.ccs/instances/ck/plugins/cache/claude-hud/claude-hud/0.0.2',
|
||||
version: '0.0.2',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const input = JSON.stringify(original, null, 2);
|
||||
const result = normalizePluginPaths(input);
|
||||
|
||||
// Should be valid JSON
|
||||
expect(() => JSON.parse(result)).not.toThrow();
|
||||
|
||||
// Should have normalized path
|
||||
const parsed = JSON.parse(result);
|
||||
expect(parsed.plugins['claude-hud@claude-hud'][0].installPath).toBe(
|
||||
'/home/kai/.claude/plugins/cache/claude-hud/claude-hud/0.0.2'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle empty object', () => {
|
||||
const input = JSON.stringify({});
|
||||
expect(normalizePluginPaths(input)).toBe(input);
|
||||
});
|
||||
|
||||
it('should handle plugins without installPath', () => {
|
||||
const input = JSON.stringify({ plugins: {} });
|
||||
expect(normalizePluginPaths(input)).toBe(input);
|
||||
});
|
||||
|
||||
it('should handle Windows-style paths (backslash)', () => {
|
||||
// Windows paths use backslashes, regex should not match
|
||||
const input = 'C:\\Users\\user\\.ccs\\instances\\ck\\plugins\\cache';
|
||||
expect(normalizePluginPaths(input)).toBe(input);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user