Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
8.3 kB · 292 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293/** * Plugin install counts data layer * * This module fetches and caches plugin install counts from the official * Claude plugins statistics repository. The cache is refreshed if older * than 24 hours. * * Cache location: ~/.claude/plugins/install-counts-cache.json */
import axios from 'axios'import { randomBytes } from 'crypto'import { readFile, rename, unlink, writeFile } from 'fs/promises'import { join } from 'path'import { logForDebugging } from '../debug.js'import { errorMessage, getErrnoCode } from '../errors.js'import { getFsImplementation } from '../fsOperations.js'import { logError } from '../log.js'import { jsonParse, jsonStringify } from '../slowOperations.js'import { classifyFetchError, logPluginFetch } from './fetchTelemetry.js'import { getPluginsDirectory } from './pluginDirectories.js'
const INSTALL_COUNTS_CACHE_VERSION = 1const INSTALL_COUNTS_CACHE_FILENAME = 'install-counts-cache.json'const INSTALL_COUNTS_URL = 'https://raw.githubusercontent.com/anthropics/claude-plugins-official/refs/heads/stats/stats/plugin-installs.json'const CACHE_TTL_MS = 24 * 60 * 60 * 1000 // 24 hours in milliseconds
/** * Structure of the install counts cache file */type InstallCountsCache = { version: number fetchedAt: string // ISO timestamp counts: Array<{ plugin: string // "pluginName@marketplace" unique_installs: number }>}
/** * Expected structure of the GitHub stats response */type GitHubStatsResponse = { plugins: Array<{ plugin: string unique_installs: number }>}
/** * Get the path to the install counts cache file */function getInstallCountsCachePath(): string { return join(getPluginsDirectory(), INSTALL_COUNTS_CACHE_FILENAME)}
/** * Load the install counts cache from disk. * Returns null if the file doesn't exist, is invalid, or is stale (>24h old). */async function loadInstallCountsCache(): Promise<InstallCountsCache | null> { const cachePath = getInstallCountsCachePath()
try { const content = await readFile(cachePath, { encoding: 'utf-8' }) const parsed = jsonParse(content) as unknown
// Validate basic structure if ( typeof parsed !== 'object' || parsed === null || !('version' in parsed) || !('fetchedAt' in parsed) || !('counts' in parsed) ) { logForDebugging('Install counts cache has invalid structure') return null }
const cache = parsed as { version: unknown fetchedAt: unknown counts: unknown }
// Validate version if (cache.version !== INSTALL_COUNTS_CACHE_VERSION) { logForDebugging( `Install counts cache version mismatch (got ${cache.version}, expected ${INSTALL_COUNTS_CACHE_VERSION})`, ) return null }
// Validate fetchedAt and counts if (typeof cache.fetchedAt !== 'string' || !Array.isArray(cache.counts)) { logForDebugging('Install counts cache has invalid structure') return null }
// Validate fetchedAt is a valid date const fetchedAt = new Date(cache.fetchedAt).getTime() if (Number.isNaN(fetchedAt)) { logForDebugging('Install counts cache has invalid fetchedAt timestamp') return null }
// Validate count entries have required fields const validCounts = cache.counts.every( (entry): entry is { plugin: string; unique_installs: number } => typeof entry === 'object' && entry !== null && typeof entry.plugin === 'string' && typeof entry.unique_installs === 'number', ) if (!validCounts) { logForDebugging('Install counts cache has malformed entries') return null }
// Check if cache is stale (>24 hours old) const now = Date.now() if (now - fetchedAt > CACHE_TTL_MS) { logForDebugging('Install counts cache is stale (>24h old)') return null }
// Return validated cache return { version: cache.version as number, fetchedAt: cache.fetchedAt, counts: cache.counts, } } catch (error) { const code = getErrnoCode(error) if (code !== 'ENOENT') { logForDebugging( `Failed to load install counts cache: ${errorMessage(error)}`, ) } return null }}
/** * Save the install counts cache to disk atomically. * Uses a temp file + rename pattern to prevent corruption. */async function saveInstallCountsCache( cache: InstallCountsCache,): Promise<void> { const cachePath = getInstallCountsCachePath() const tempPath = `${cachePath}.${randomBytes(8).toString('hex')}.tmp`
try { // Ensure the plugins directory exists const pluginsDir = getPluginsDirectory() await getFsImplementation().mkdir(pluginsDir)
// Write to temp file const content = jsonStringify(cache, null, 2) await writeFile(tempPath, content, { encoding: 'utf-8', mode: 0o600, })
// Atomic rename await rename(tempPath, cachePath) logForDebugging('Install counts cache saved successfully') } catch (error) { logError(error) // Clean up temp file if it exists try { await unlink(tempPath) } catch { // Ignore cleanup errors } }}
/** * Fetch install counts from GitHub stats repository */async function fetchInstallCountsFromGitHub(): Promise< Array<{ plugin: string; unique_installs: number }>> { logForDebugging(`Fetching install counts from ${INSTALL_COUNTS_URL}`)
const started = performance.now() try { const response = await axios.get<GitHubStatsResponse>(INSTALL_COUNTS_URL, { timeout: 10000, })
if (!response.data?.plugins || !Array.isArray(response.data.plugins)) { throw new Error('Invalid response format from install counts API') }
logPluginFetch( 'install_counts', INSTALL_COUNTS_URL, 'success', performance.now() - started, ) return response.data.plugins } catch (error) { logPluginFetch( 'install_counts', INSTALL_COUNTS_URL, 'failure', performance.now() - started, classifyFetchError(error), ) throw error }}
/** * Get plugin install counts as a Map. * Uses cached data if available and less than 24 hours old. * Returns null on errors so UI can hide counts rather than show misleading zeros. * * @returns Map of plugin ID (name@marketplace) to install count, or null if unavailable */export async function getInstallCounts(): Promise<Map<string, number> | null> { // Try to load from cache first const cache = await loadInstallCountsCache() if (cache) { logForDebugging('Using cached install counts') logPluginFetch('install_counts', INSTALL_COUNTS_URL, 'cache_hit', 0) const map = new Map<string, number>() for (const entry of cache.counts) { map.set(entry.plugin, entry.unique_installs) } return map }
// Cache miss or stale - fetch from GitHub try { const counts = await fetchInstallCountsFromGitHub()
// Save to cache const newCache: InstallCountsCache = { version: INSTALL_COUNTS_CACHE_VERSION, fetchedAt: new Date().toISOString(), counts, } await saveInstallCountsCache(newCache)
// Convert to Map const map = new Map<string, number>() for (const entry of counts) { map.set(entry.plugin, entry.unique_installs) } return map } catch (error) { // Log error and return null so UI can hide counts logError(error) logForDebugging(`Failed to fetch install counts: ${errorMessage(error)}`) return null }}
/** * Format an install count for display. * * @param count - The raw install count * @returns Formatted string: * - <1000: raw number (e.g., "42") * - >=1000: K suffix with 1 decimal (e.g., "1.2K", "36.2K") * - >=1000000: M suffix with 1 decimal (e.g., "1.2M") */export function formatInstallCount(count: number): string { if (count < 1000) { return String(count) }
if (count < 1000000) { const k = count / 1000 // Use toFixed(1) but remove trailing .0 const formatted = k.toFixed(1) return formatted.endsWith('.0') ? `${formatted.slice(0, -2)}K` : `${formatted}K` }
const m = count / 1000000 const formatted = m.toFixed(1) return formatted.endsWith('.0') ? `${formatted.slice(0, -2)}M` : `${formatted}M`}