Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
18 kB · 592 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593import isEqual from 'lodash-es/isEqual.js'import { toError } from '../errors.js'import { logError } from '../log.js'import { getSettingsForSource } from '../settings/settings.js'import { plural } from '../stringUtils.js'import { checkGitAvailable } from './gitAvailability.js'import { getMarketplace } from './marketplaceManager.js'import type { KnownMarketplace, MarketplaceSource } from './schemas.js'
/** * Format plugin failure details for user display * @param failures - Array of failures with names and reasons * @param includeReasons - Whether to include failure reasons (true for full errors, false for summaries) * @returns Formatted string like "plugin-a (reason); plugin-b (reason)" or "plugin-a, plugin-b" */export function formatFailureDetails( failures: Array<{ name: string; reason?: string; error?: string }>, includeReasons: boolean,): string { const maxShow = 2 const details = failures .slice(0, maxShow) .map(f => { const reason = f.reason || f.error || 'unknown error' return includeReasons ? `${f.name} (${reason})` : f.name }) .join(includeReasons ? '; ' : ', ')
const remaining = failures.length - maxShow const moreText = remaining > 0 ? ` and ${remaining} more` : ''
return `${details}${moreText}`}
/** * Extract source display string from marketplace configuration */export function getMarketplaceSourceDisplay(source: MarketplaceSource): string { switch (source.source) { case 'github': return source.repo case 'url': return source.url case 'git': return source.url case 'directory': return source.path case 'file': return source.path case 'settings': return `settings:${source.name}` default: return 'Unknown source' }}
/** * Create a plugin ID from plugin name and marketplace name */export function createPluginId( pluginName: string, marketplaceName: string,): string { return `${pluginName}@${marketplaceName}`}
/** * Load marketplaces with graceful degradation for individual failures. * Blocked marketplaces (per enterprise policy) are excluded from the results. */export async function loadMarketplacesWithGracefulDegradation( config: Record<string, KnownMarketplace>,): Promise<{ marketplaces: Array<{ name: string config: KnownMarketplace data: Awaited<ReturnType<typeof getMarketplace>> | null }> failures: Array<{ name: string; error: string }>}> { const marketplaces: Array<{ name: string config: KnownMarketplace data: Awaited<ReturnType<typeof getMarketplace>> | null }> = [] const failures: Array<{ name: string; error: string }> = []
for (const [name, marketplaceConfig] of Object.entries(config)) { // Skip marketplaces blocked by enterprise policy if (!isSourceAllowedByPolicy(marketplaceConfig.source)) { continue }
let data = null try { data = await getMarketplace(name) } catch (err) { // Track individual marketplace failures but continue loading others const errorMessage = err instanceof Error ? err.message : String(err) failures.push({ name, error: errorMessage })
// Log for monitoring logError(toError(err)) }
marketplaces.push({ name, config: marketplaceConfig, data, }) }
return { marketplaces, failures }}
/** * Format marketplace loading failures into appropriate user messages */export function formatMarketplaceLoadingErrors( failures: Array<{ name: string; error: string }>, successCount: number,): { type: 'warning' | 'error'; message: string } | null { if (failures.length === 0) { return null }
// If some marketplaces succeeded, show warning if (successCount > 0) { const message = failures.length === 1 ? `Warning: Failed to load marketplace '${failures[0]!.name}': ${failures[0]!.error}` : `Warning: Failed to load ${failures.length} marketplaces: ${formatFailureNames(failures)}` return { type: 'warning', message } }
// All marketplaces failed - this is a critical error return { type: 'error', message: `Failed to load all marketplaces. Errors: ${formatFailureErrors(failures)}`, }}
function formatFailureNames( failures: Array<{ name: string; error: string }>,): string { return failures.map(f => f.name).join(', ')}
function formatFailureErrors( failures: Array<{ name: string; error: string }>,): string { return failures.map(f => `${f.name}: ${f.error}`).join('; ')}
/** * Get the strict marketplace source allowlist from policy settings. * Returns null if no restriction is in place, or an array of allowed sources. */export function getStrictKnownMarketplaces(): MarketplaceSource[] | null { const policySettings = getSettingsForSource('policySettings') if (!policySettings?.strictKnownMarketplaces) { return null // No restrictions } return policySettings.strictKnownMarketplaces}
/** * Get the marketplace source blocklist from policy settings. * Returns null if no blocklist is in place, or an array of blocked sources. */export function getBlockedMarketplaces(): MarketplaceSource[] | null { const policySettings = getSettingsForSource('policySettings') if (!policySettings?.blockedMarketplaces) { return null // No blocklist } return policySettings.blockedMarketplaces}
/** * Get the custom plugin trust message from policy settings. * Returns undefined if not configured. */export function getPluginTrustMessage(): string | undefined { return getSettingsForSource('policySettings')?.pluginTrustMessage}
/** * Compare two MarketplaceSource objects for equality. * Sources are equal if they have the same type and all relevant fields match. */function areSourcesEqual(a: MarketplaceSource, b: MarketplaceSource): boolean { if (a.source !== b.source) return false
switch (a.source) { case 'url': return a.url === (b as typeof a).url case 'github': return ( a.repo === (b as typeof a).repo && (a.ref || undefined) === ((b as typeof a).ref || undefined) && (a.path || undefined) === ((b as typeof a).path || undefined) ) case 'git': return ( a.url === (b as typeof a).url && (a.ref || undefined) === ((b as typeof a).ref || undefined) && (a.path || undefined) === ((b as typeof a).path || undefined) ) case 'npm': return a.package === (b as typeof a).package case 'file': return a.path === (b as typeof a).path case 'directory': return a.path === (b as typeof a).path case 'settings': return ( a.name === (b as typeof a).name && isEqual(a.plugins, (b as typeof a).plugins) ) default: return false }}
/** * Extract the host/domain from a marketplace source. * Used for hostPattern matching in strictKnownMarketplaces. * * Currently only supports github, git, and url sources. * npm, file, and directory sources are not supported for hostPattern matching. * * @param source - The marketplace source to extract host from * @returns The hostname string, or null if extraction fails or source type not supported */export function extractHostFromSource( source: MarketplaceSource,): string | null { switch (source.source) { case 'github': // GitHub shorthand always means github.com return 'github.com'
case 'git': { // SSH format: user@HOST:path (e.g., git@github.com:owner/repo.git) const sshMatch = source.url.match(/^[^@]+@([^:]+):/) if (sshMatch?.[1]) { return sshMatch[1] } // HTTPS format: extract hostname from URL try { return new URL(source.url).hostname } catch { return null } }
case 'url': try { return new URL(source.url).hostname } catch { return null }
// npm, file, directory, hostPattern, pathPattern sources are not supported for hostPattern matching default: return null }}
/** * Check if a source matches a hostPattern entry. * Extracts the host from the source and tests it against the regex pattern. * * @param source - The marketplace source to check * @param pattern - The hostPattern entry from strictKnownMarketplaces * @returns true if the source's host matches the pattern */function doesSourceMatchHostPattern( source: MarketplaceSource, pattern: MarketplaceSource & { source: 'hostPattern' },): boolean { const host = extractHostFromSource(source) if (!host) { return false }
try { const regex = new RegExp(pattern.hostPattern) return regex.test(host) } catch { // Invalid regex - log and return false logError(new Error(`Invalid hostPattern regex: ${pattern.hostPattern}`)) return false }}
/** * Check if a source matches a pathPattern entry. * Tests the source's .path (file and directory sources only) against the regex pattern. * * @param source - The marketplace source to check * @param pattern - The pathPattern entry from strictKnownMarketplaces * @returns true if the source's path matches the pattern */function doesSourceMatchPathPattern( source: MarketplaceSource, pattern: MarketplaceSource & { source: 'pathPattern' },): boolean { // Only file and directory sources have a .path to match against if (source.source !== 'file' && source.source !== 'directory') { return false }
try { const regex = new RegExp(pattern.pathPattern) return regex.test(source.path) } catch { logError(new Error(`Invalid pathPattern regex: ${pattern.pathPattern}`)) return false }}
/** * Get hosts from hostPattern entries in the allowlist. * Used to provide helpful error messages. */export function getHostPatternsFromAllowlist(): string[] { const allowlist = getStrictKnownMarketplaces() if (!allowlist) return []
return allowlist .filter( (entry): entry is MarketplaceSource & { source: 'hostPattern' } => entry.source === 'hostPattern', ) .map(entry => entry.hostPattern)}
/** * Extract GitHub owner/repo from a git URL if it's a GitHub URL. * Returns null if not a GitHub URL. * * Handles: * - git@github.com:owner/repo.git * - https://github.com/owner/repo.git * - https://github.com/owner/repo */function extractGitHubRepoFromGitUrl(url: string): string | null { // SSH format: git@github.com:owner/repo.git const sshMatch = url.match(/^git@github\.com:([^/]+\/[^/]+?)(?:\.git)?$/) if (sshMatch && sshMatch[1]) { return sshMatch[1] }
// HTTPS format: https://github.com/owner/repo.git or https://github.com/owner/repo const httpsMatch = url.match( /^https?:\/\/github\.com\/([^/]+\/[^/]+?)(?:\.git)?$/, ) if (httpsMatch && httpsMatch[1]) { return httpsMatch[1] }
return null}
/** * Check if a blocked ref/path constraint matches a source. * If the blocklist entry has no ref/path, it matches ALL refs/paths (wildcard). * If the blocklist entry has a specific ref/path, it only matches that exact value. */function blockedConstraintMatches( blockedValue: string | undefined, sourceValue: string | undefined,): boolean { // If blocklist doesn't specify a constraint, it's a wildcard - matches anything if (!blockedValue) { return true } // If blocklist specifies a constraint, source must match exactly return (blockedValue || undefined) === (sourceValue || undefined)}
/** * Check if two sources refer to the same GitHub repository, even if using * different source types (github vs git with GitHub URL). * * Blocklist matching is asymmetric: * - If blocklist entry has no ref/path, it blocks ALL refs/paths (wildcard) * - If blocklist entry has a specific ref/path, only that exact value is blocked */function areSourcesEquivalentForBlocklist( source: MarketplaceSource, blocked: MarketplaceSource,): boolean { // Check exact same source type if (source.source === blocked.source) { switch (source.source) { case 'github': { const b = blocked as typeof source if (source.repo !== b.repo) return false return ( blockedConstraintMatches(b.ref, source.ref) && blockedConstraintMatches(b.path, source.path) ) } case 'git': { const b = blocked as typeof source if (source.url !== b.url) return false return ( blockedConstraintMatches(b.ref, source.ref) && blockedConstraintMatches(b.path, source.path) ) } case 'url': return source.url === (blocked as typeof source).url case 'npm': return source.package === (blocked as typeof source).package case 'file': return source.path === (blocked as typeof source).path case 'directory': return source.path === (blocked as typeof source).path case 'settings': return source.name === (blocked as typeof source).name default: return false } }
// Check if a git source matches a github blocklist entry if (source.source === 'git' && blocked.source === 'github') { const extractedRepo = extractGitHubRepoFromGitUrl(source.url) if (extractedRepo === blocked.repo) { return ( blockedConstraintMatches(blocked.ref, source.ref) && blockedConstraintMatches(blocked.path, source.path) ) } }
// Check if a github source matches a git blocklist entry (GitHub URL) if (source.source === 'github' && blocked.source === 'git') { const extractedRepo = extractGitHubRepoFromGitUrl(blocked.url) if (extractedRepo === source.repo) { return ( blockedConstraintMatches(blocked.ref, source.ref) && blockedConstraintMatches(blocked.path, source.path) ) } }
return false}
/** * Check if a marketplace source is explicitly in the blocklist. * Used for error message differentiation. * * This also catches attempts to bypass a github blocklist entry by using * git URLs (e.g., git@github.com:owner/repo.git or https://github.com/owner/repo.git). */export function isSourceInBlocklist(source: MarketplaceSource): boolean { const blocklist = getBlockedMarketplaces() if (blocklist === null) { return false } return blocklist.some(blocked => areSourcesEquivalentForBlocklist(source, blocked), )}
/** * Check if a marketplace source is allowed by enterprise policy. * Returns true if allowed (or no policy), false if blocked. * This check happens BEFORE downloading, so blocked sources never touch the filesystem. * * Policy precedence: * 1. blockedMarketplaces (blocklist) - if source matches, it's blocked * 2. strictKnownMarketplaces (allowlist) - if set, source must be in the list */export function isSourceAllowedByPolicy(source: MarketplaceSource): boolean { // Check blocklist first (takes precedence) if (isSourceInBlocklist(source)) { return false }
// Then check allowlist const allowlist = getStrictKnownMarketplaces() if (allowlist === null) { return true // No restrictions }
// Check each entry in the allowlist return allowlist.some(allowed => { // Handle hostPattern entries - match by extracted host if (allowed.source === 'hostPattern') { return doesSourceMatchHostPattern(source, allowed) } // Handle pathPattern entries - match file/directory .path by regex if (allowed.source === 'pathPattern') { return doesSourceMatchPathPattern(source, allowed) } // Handle regular source entries - exact match return areSourcesEqual(source, allowed) })}
/** * Format a MarketplaceSource for display in error messages */export function formatSourceForDisplay(source: MarketplaceSource): string { switch (source.source) { case 'github': return `github:${source.repo}${source.ref ? `@${source.ref}` : ''}` case 'url': return source.url case 'git': return `git:${source.url}${source.ref ? `@${source.ref}` : ''}` case 'npm': return `npm:${source.package}` case 'file': return `file:${source.path}` case 'directory': return `dir:${source.path}` case 'hostPattern': return `hostPattern:${source.hostPattern}` case 'pathPattern': return `pathPattern:${source.pathPattern}` case 'settings': return `settings:${source.name} (${source.plugins.length} ${plural(source.plugins.length, 'plugin')})` default: return 'unknown source' }}
/** * Reasons why no marketplaces are available in the Discover screen */export type EmptyMarketplaceReason = | 'git-not-installed' | 'all-blocked-by-policy' | 'policy-restricts-sources' | 'all-marketplaces-failed' | 'no-marketplaces-configured' | 'all-plugins-installed'
/** * Detect why no marketplaces are available. * Checks in order of priority: git availability → policy restrictions → config state → failures */export async function detectEmptyMarketplaceReason({ configuredMarketplaceCount, failedMarketplaceCount,}: { configuredMarketplaceCount: number failedMarketplaceCount: number}): Promise<EmptyMarketplaceReason> { // Check if git is installed (required for most marketplace sources) const gitAvailable = await checkGitAvailable() if (!gitAvailable) { return 'git-not-installed' }
// Check policy restrictions const allowlist = getStrictKnownMarketplaces() if (allowlist !== null) { if (allowlist.length === 0) { // Policy explicitly blocks all marketplaces return 'all-blocked-by-policy' } // Policy restricts which sources can be used if (configuredMarketplaceCount === 0) { return 'policy-restricts-sources' } }
// Check if any marketplaces are configured if (configuredMarketplaceCount === 0) { return 'no-marketplaces-configured' }
// Check if all configured marketplaces failed to load if ( failedMarketplaceCount > 0 && failedMarketplaceCount === configuredMarketplaceCount ) { return 'all-marketplaces-failed' }
// Marketplaces are configured and loaded, but no plugins available // This typically means all plugins are already installed return 'all-plugins-installed'}