agent-ecosystem/src/shared/utils/extensionNormalizers.ts
iliya bec8a6184a fix: refine regex patterns and improve utility functions for mention handling
- Updated regex patterns in chipUtils and mentionLinkify to enhance boundary detection for mentions.
- Refactored taskChangeRequest to simplify earliest date calculation using array destructuring.
- Improved taskReferenceUtils by replacing character boundary checks with a more concise regex.
- Enhanced teamMessageFiltering to ensure boolean checks for message filtering conditions.
- Adjusted urlMatchUtils to refine URL matching regex for better accuracy.
- Updated crossTeam constants to include comments for regex patterns, improving code clarity.
- Removed unused CommentAttachmentPayload type from api.ts to clean up type definitions.
- Introduced McpInstallScope type for better type safety in mcp.ts.
- Enhanced extensionNormalizers to improve URL normalization and added tests for parseGitHubOwnerRepo function.
- Cleaned up pricing.ts by removing unnecessary eslint disable comments.
- Added tests for new functionality in chipUtils and crossTeam constants, ensuring robust coverage.
2026-03-19 13:35:51 +02:00

130 lines
3.7 KiB
TypeScript

/**
* Pure-function normalizers for Extension Store data.
*/
import type { PluginCapability, PluginCatalogItem } from '@shared/types/extensions';
/**
* Normalize a repository URL for dedup comparison.
* Lowercases, strips `.git` suffix, strips trailing `/`.
*/
export function normalizeRepoUrl(url: string): string {
return url
.toLowerCase()
.replace(/\.git$/, '')
.replace(
/* eslint-disable-next-line sonarjs/slow-regex -- trailing slashes only, URL length bounded */
/\/+$/,
''
);
}
/**
* Derive UI-visible capability labels from plugin capability flags.
*/
export function inferCapabilities(item: PluginCatalogItem): PluginCapability[] {
const caps: PluginCapability[] = [];
if (item.hasLspServers) caps.push('lsp');
if (item.hasMcpServers) caps.push('mcp');
if (item.hasAgents) caps.push('agent');
if (item.hasCommands) caps.push('command');
if (item.hasHooks) caps.push('hook');
if (caps.length === 0) caps.push('skill');
return caps;
}
const CAPABILITY_LABELS: Record<PluginCapability, string> = {
lsp: 'LSP',
mcp: 'MCP',
agent: 'Agent',
command: 'Command',
hook: 'Hook',
skill: 'Skill',
};
/**
* Get a human-readable label for the primary capability.
*/
export function getPrimaryCapabilityLabel(capabilities: PluginCapability[]): string {
if (capabilities.length === 0) return 'Skill';
return CAPABILITY_LABELS[capabilities[0]];
}
/**
* Get human-readable label for a capability.
*/
export function getCapabilityLabel(capability: PluginCapability): string {
return CAPABILITY_LABELS[capability];
}
/**
* Format large install counts for display.
* 277472 → "277K", 1200000 → "1.2M", 42 → "42"
*/
export function formatInstallCount(count: number): string {
if (count >= 1_000_000) {
const millions = count / 1_000_000;
return millions >= 10
? `${Math.round(millions)}M`
: `${millions.toFixed(1).replace(/\.0$/, '')}M`;
}
if (count >= 1_000) {
const thousands = count / 1_000;
return thousands >= 10
? `${Math.round(thousands)}K`
: `${thousands.toFixed(1).replace(/\.0$/, '')}K`;
}
return String(count);
}
/**
* Normalize a category string for consistent comparison/display.
* Lowercases, trims, falls back to "other".
*/
export function normalizeCategory(raw: string | undefined): string {
if (!raw) return 'other';
const normalized = raw.trim().toLowerCase();
return normalized || 'other';
}
/**
* Build a pluginId (= qualifiedName) from marketplace plugin name + marketplace name.
*/
export function buildPluginId(pluginName: string, marketplaceName: string): string {
return `${pluginName}@${marketplaceName}`;
}
/**
* Sanitize an MCP server display name into a CLI-safe server name.
* Must match the regex /^[\w.-]{1,100}$/ required by McpInstallService.
*/
export function sanitizeMcpServerName(displayName: string): string {
return displayName
.toLowerCase()
.replace(/\s+/g, '-')
.replace(/[^\w.-]/g, '');
}
/**
* Extract owner/repo from a GitHub URL. Returns null for non-GitHub URLs.
* Handles: https://github.com/owner/repo, https://github.com/owner/repo.git, trailing slashes.
*/
export function parseGitHubOwnerRepo(url: string): { owner: string; repo: string } | null {
try {
const parsed = new URL(url);
if (parsed.hostname !== 'github.com') return null;
const parts = parsed.pathname
.replace(/^\//, '')
.replace(/\.git$/, '')
.replace(
/* eslint-disable-next-line sonarjs/slow-regex -- trailing slashes only, pathname bounded */
/\/+$/,
''
)
.split('/');
if (parts.length < 2 || !parts[0] || !parts[1]) return null;
return { owner: parts[0], repo: parts[1] };
} catch {
return null;
}
}