Download src/utils/plugins/installCounts.ts from chenbhao/codev: direct link, hf CLI and curl.
- Browser
- Download file 8.32 kB
-
https://huggingface.co/chenbhao/codev/resolve/714ab7d15ffd78992a4c3d4e2bf153c2f62f4841/src/utils/plugins/installCounts.ts
- Command line
-
hf download hf://chenbhao/codev@714ab7d15ffd78992a4c3d4e2bf153c2f62f4841/src/utils/plugins/installCounts.ts
-
curl -L -o installCounts.ts https://huggingface.co/chenbhao/codev/resolve/714ab7d15ffd78992a4c3d4e2bf153c2f62f4841/src/utils/plugins/installCounts.ts
8.32 kB
| /** | |
| * 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 = 1 | |
| const 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` | |
| } | |