mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 10:15:40 +00:00
refactor: extract plugin path normalizer
This commit is contained in:
1 parent
7569c6ce2d
commit
3330229686
3 files changed
+121
-80
No files matched your search
@@ -0,0 +1,81 @@
|
|||||||
|
import * as os from 'os';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
function getPluginPathModule(
|
||||||
|
targetConfigDir: string,
|
||||||
|
input: string
|
||||||
|
): typeof path.posix | typeof path.win32 {
|
||||||
|
return targetConfigDir.includes('\\') || input.includes('\\') ? path.win32 : path.posix;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeTargetConfigDir(targetConfigDir: string, input: string): string {
|
||||||
|
const pathModule = getPluginPathModule(targetConfigDir, input);
|
||||||
|
return pathModule.normalize(
|
||||||
|
pathModule === path.win32
|
||||||
|
? targetConfigDir.replace(/\//g, '\\')
|
||||||
|
: targetConfigDir.replace(/\\/g, '/')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizePluginMetadataPathString(
|
||||||
|
input: string,
|
||||||
|
targetConfigDir = path.join(os.homedir(), '.claude')
|
||||||
|
): string {
|
||||||
|
const match = input.match(
|
||||||
|
/^(.*?)([\\/])(?:\.claude|\.ccs\2shared|\.ccs\2instances\2[^\\/]+)\2plugins(?:(\2.*))?$/
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!match) {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathModule = getPluginPathModule(targetConfigDir, input);
|
||||||
|
const normalizedTargetConfigDir = normalizeTargetConfigDir(targetConfigDir, input);
|
||||||
|
const suffix = match[3] ?? '';
|
||||||
|
const suffixSegments = suffix.split(/[\\/]+/).filter(Boolean);
|
||||||
|
|
||||||
|
return pathModule.join(normalizedTargetConfigDir, 'plugins', ...suffixSegments);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizePluginMetadataValue(
|
||||||
|
value: unknown,
|
||||||
|
targetConfigDir: string
|
||||||
|
): { normalized: unknown; changed: boolean } {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const normalized = normalizePluginMetadataPathString(value, targetConfigDir);
|
||||||
|
return { normalized, changed: normalized !== value };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
let changed = false;
|
||||||
|
const normalized = value.map((item) => {
|
||||||
|
const result = normalizePluginMetadataValue(item, targetConfigDir);
|
||||||
|
changed = changed || result.changed;
|
||||||
|
return result.normalized;
|
||||||
|
});
|
||||||
|
return { normalized, changed };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value && typeof value === 'object') {
|
||||||
|
let changed = false;
|
||||||
|
const normalized = Object.fromEntries(
|
||||||
|
Object.entries(value as Record<string, unknown>).map(([key, item]) => {
|
||||||
|
const result = normalizePluginMetadataValue(item, targetConfigDir);
|
||||||
|
changed = changed || result.changed;
|
||||||
|
return [key, result.normalized];
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return { normalized, changed };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { normalized: value, changed: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizePluginMetadataContent(
|
||||||
|
original: string,
|
||||||
|
targetConfigDir = path.join(os.homedir(), '.claude')
|
||||||
|
): string {
|
||||||
|
const parsed = JSON.parse(original) as unknown;
|
||||||
|
const result = normalizePluginMetadataValue(parsed, targetConfigDir);
|
||||||
|
return result.changed ? JSON.stringify(result.normalized, null, 2) : original;
|
||||||
|
}
|
||||||
@@ -14,6 +14,14 @@ import { ok, info, warn } from '../utils/ui';
|
|||||||
import { DEFAULT_ACCOUNT_CONTEXT_GROUP } from '../auth/account-context';
|
import { DEFAULT_ACCOUNT_CONTEXT_GROUP } from '../auth/account-context';
|
||||||
import type { AccountContextPolicy } from '../auth/account-context';
|
import type { AccountContextPolicy } from '../auth/account-context';
|
||||||
import { getCcsDir } from '../utils/config-manager';
|
import { getCcsDir } from '../utils/config-manager';
|
||||||
|
import {
|
||||||
|
normalizePluginMetadataContent,
|
||||||
|
normalizePluginMetadataValue,
|
||||||
|
} from './plugin-path-normalizer';
|
||||||
|
export {
|
||||||
|
normalizePluginMetadataContent,
|
||||||
|
normalizePluginMetadataPathString,
|
||||||
|
} from './plugin-path-normalizer';
|
||||||
|
|
||||||
interface SharedItem {
|
interface SharedItem {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -29,85 +37,6 @@ const DEFAULT_INSTALLED_PLUGIN_REGISTRY = JSON.stringify(
|
|||||||
2
|
2
|
||||||
);
|
);
|
||||||
|
|
||||||
function getPluginPathModule(
|
|
||||||
targetConfigDir: string,
|
|
||||||
input: string
|
|
||||||
): typeof path.posix | typeof path.win32 {
|
|
||||||
return targetConfigDir.includes('\\') || input.includes('\\') ? path.win32 : path.posix;
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeTargetConfigDir(targetConfigDir: string, input: string): string {
|
|
||||||
const pathModule = getPluginPathModule(targetConfigDir, input);
|
|
||||||
return pathModule.normalize(
|
|
||||||
pathModule === path.win32
|
|
||||||
? targetConfigDir.replace(/\//g, '\\')
|
|
||||||
: targetConfigDir.replace(/\\/g, '/')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function normalizePluginMetadataPathString(
|
|
||||||
input: string,
|
|
||||||
targetConfigDir = path.join(os.homedir(), '.claude')
|
|
||||||
): string {
|
|
||||||
const match = input.match(
|
|
||||||
/^(.*?)([\\/])(?:\.claude|\.ccs\2shared|\.ccs\2instances\2[^\\/]+)\2plugins(?:(\2.*))?$/
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!match) {
|
|
||||||
return input;
|
|
||||||
}
|
|
||||||
|
|
||||||
const pathModule = getPluginPathModule(targetConfigDir, input);
|
|
||||||
const normalizedTargetConfigDir = normalizeTargetConfigDir(targetConfigDir, input);
|
|
||||||
const suffix = match[3] ?? '';
|
|
||||||
const suffixSegments = suffix.split(/[\\/]+/).filter(Boolean);
|
|
||||||
|
|
||||||
return pathModule.join(normalizedTargetConfigDir, 'plugins', ...suffixSegments);
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizePluginMetadataValue(
|
|
||||||
value: unknown,
|
|
||||||
targetConfigDir: string
|
|
||||||
): { normalized: unknown; changed: boolean } {
|
|
||||||
if (typeof value === 'string') {
|
|
||||||
const normalized = normalizePluginMetadataPathString(value, targetConfigDir);
|
|
||||||
return { normalized, changed: normalized !== value };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
let changed = false;
|
|
||||||
const normalized = value.map((item) => {
|
|
||||||
const result = normalizePluginMetadataValue(item, targetConfigDir);
|
|
||||||
changed = changed || result.changed;
|
|
||||||
return result.normalized;
|
|
||||||
});
|
|
||||||
return { normalized, changed };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value && typeof value === 'object') {
|
|
||||||
let changed = false;
|
|
||||||
const normalized = Object.fromEntries(
|
|
||||||
Object.entries(value as Record<string, unknown>).map(([key, item]) => {
|
|
||||||
const result = normalizePluginMetadataValue(item, targetConfigDir);
|
|
||||||
changed = changed || result.changed;
|
|
||||||
return [key, result.normalized];
|
|
||||||
})
|
|
||||||
);
|
|
||||||
return { normalized, changed };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { normalized: value, changed: false };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function normalizePluginMetadataContent(
|
|
||||||
original: string,
|
|
||||||
targetConfigDir = path.join(os.homedir(), '.claude')
|
|
||||||
): string {
|
|
||||||
const parsed = JSON.parse(original) as unknown;
|
|
||||||
const result = normalizePluginMetadataValue(parsed, targetConfigDir);
|
|
||||||
return result.changed ? JSON.stringify(result.normalized, null, 2) : original;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SharedManager Class
|
* SharedManager Class
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -2,9 +2,13 @@ import { afterEach, beforeEach, describe, expect, it, mock, spyOn } from 'bun:te
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import SharedManager, {
|
import {
|
||||||
normalizePluginMetadataContent,
|
normalizePluginMetadataContent,
|
||||||
normalizePluginMetadataPathString,
|
normalizePluginMetadataPathString,
|
||||||
|
} from '../../src/management/plugin-path-normalizer';
|
||||||
|
import SharedManager, {
|
||||||
|
normalizePluginMetadataContent as normalizeSharedManagerPluginMetadataContent,
|
||||||
|
normalizePluginMetadataPathString as normalizeSharedManagerPluginMetadataPathString,
|
||||||
} from '../../src/management/shared-manager';
|
} from '../../src/management/shared-manager';
|
||||||
|
|
||||||
describe('SharedManager', () => {
|
describe('SharedManager', () => {
|
||||||
@@ -144,6 +148,12 @@ describe('SharedManager', () => {
|
|||||||
expect(normalizePluginMetadataPathString(input, targetConfigDir)).toBe(input);
|
expect(normalizePluginMetadataPathString(input, targetConfigDir)).toBe(input);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('preserves original JSON content when no plugin path changes are needed', () => {
|
||||||
|
const original = JSON.stringify({ plugins: { 'plugin-a': { enabled: true } } }, null, 4);
|
||||||
|
|
||||||
|
expect(normalizePluginMetadataContent(original, instanceDir('work'))).toBe(original);
|
||||||
|
});
|
||||||
|
|
||||||
it('handles Windows path separators', () => {
|
it('handles Windows path separators', () => {
|
||||||
const targetConfigDir = 'C:\\Users\\user\\.claude';
|
const targetConfigDir = 'C:\\Users\\user\\.claude';
|
||||||
const input = 'C:\\Users\\user\\.ccs\\instances\\work\\plugins\\marketplaces\\official';
|
const input = 'C:\\Users\\user\\.ccs\\instances\\work\\plugins\\marketplaces\\official';
|
||||||
@@ -152,6 +162,27 @@ describe('SharedManager', () => {
|
|||||||
'C:\\Users\\user\\.claude\\plugins\\marketplaces\\official'
|
'C:\\Users\\user\\.claude\\plugins\\marketplaces\\official'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('uses the current home directory when target config dir is omitted', () => {
|
||||||
|
const input = path.join(tempRoot, '.ccs', 'shared', 'plugins', 'cache', 'plugin-a');
|
||||||
|
|
||||||
|
expect(normalizePluginMetadataPathString(input)).toBe(
|
||||||
|
path.join(tempRoot, '.claude', 'plugins', 'cache', 'plugin-a')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps the legacy shared-manager helper export compatible', () => {
|
||||||
|
const targetConfigDir = instanceDir('work');
|
||||||
|
const input = path.join(tempRoot, '.ccs', 'shared', 'plugins', 'cache', 'plugin-a');
|
||||||
|
const content = JSON.stringify({ installPath: input }, null, 2);
|
||||||
|
|
||||||
|
expect(normalizeSharedManagerPluginMetadataPathString(input, targetConfigDir)).toBe(
|
||||||
|
normalizePluginMetadataPathString(input, targetConfigDir)
|
||||||
|
);
|
||||||
|
expect(normalizeSharedManagerPluginMetadataContent(content, targetConfigDir)).toBe(
|
||||||
|
normalizePluginMetadataContent(content, targetConfigDir)
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('shared symlink lifecycle', () => {
|
describe('shared symlink lifecycle', () => {
|
||||||
|
|||||||
Reference in new issue
Block a user