Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
5.6 kB · 208 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209/** * Flagged plugin tracking utilities * * Tracks plugins that were auto-removed because they were delisted from * their marketplace. Data is stored in ~/.claude/plugins/flagged-plugins.json. * Flagged plugins appear in a "Flagged" section in /plugins until the user * dismisses them. * * Uses a module-level cache so that getFlaggedPlugins() can be called * synchronously during React render. The cache is populated on the first * async call (loadFlaggedPlugins or addFlaggedPlugin) and kept in sync * with writes. */
import { randomBytes } from 'crypto'import { readFile, rename, unlink, writeFile } from 'fs/promises'import { join } from 'path'import { logForDebugging } from '../debug.js'import { getFsImplementation } from '../fsOperations.js'import { logError } from '../log.js'import { jsonParse, jsonStringify } from '../slowOperations.js'import { getPluginsDirectory } from './pluginDirectories.js'
const FLAGGED_PLUGINS_FILENAME = 'flagged-plugins.json'
export type FlaggedPlugin = { flaggedAt: string seenAt?: string}
const SEEN_EXPIRY_MS = 48 * 60 * 60 * 1000 // 48 hours
// Module-level cache — populated by loadFlaggedPlugins(), updated by writes.let cache: Record<string, FlaggedPlugin> | null = null
function getFlaggedPluginsPath(): string { return join(getPluginsDirectory(), FLAGGED_PLUGINS_FILENAME)}
function parsePluginsData(content: string): Record<string, FlaggedPlugin> { const parsed = jsonParse(content) as unknown if ( typeof parsed !== 'object' || parsed === null || !('plugins' in parsed) || typeof (parsed as { plugins: unknown }).plugins !== 'object' || (parsed as { plugins: unknown }).plugins === null ) { return {} } const plugins = (parsed as { plugins: Record<string, unknown> }).plugins const result: Record<string, FlaggedPlugin> = {} for (const [id, entry] of Object.entries(plugins)) { if ( entry && typeof entry === 'object' && 'flaggedAt' in entry && typeof (entry as { flaggedAt: unknown }).flaggedAt === 'string' ) { const parsed: FlaggedPlugin = { flaggedAt: (entry as { flaggedAt: string }).flaggedAt, } if ( 'seenAt' in entry && typeof (entry as { seenAt: unknown }).seenAt === 'string' ) { parsed.seenAt = (entry as { seenAt: string }).seenAt } result[id] = parsed } } return result}
async function readFromDisk(): Promise<Record<string, FlaggedPlugin>> { try { const content = await readFile(getFlaggedPluginsPath(), { encoding: 'utf-8', }) return parsePluginsData(content) } catch { return {} }}
async function writeToDisk( plugins: Record<string, FlaggedPlugin>,): Promise<void> { const filePath = getFlaggedPluginsPath() const tempPath = `${filePath}.${randomBytes(8).toString('hex')}.tmp`
try { await getFsImplementation().mkdir(getPluginsDirectory())
const content = jsonStringify({ plugins }, null, 2) await writeFile(tempPath, content, { encoding: 'utf-8', mode: 0o600, }) await rename(tempPath, filePath) cache = plugins } catch (error) { logError(error) try { await unlink(tempPath) } catch { // Ignore cleanup errors } }}
/** * Load flagged plugins from disk into the module cache. * Must be called (and awaited) before getFlaggedPlugins() returns * meaningful data. Called by useManagePlugins during plugin refresh. */export async function loadFlaggedPlugins(): Promise<void> { const all = await readFromDisk() const now = Date.now() let changed = false
for (const [id, entry] of Object.entries(all)) { if ( entry.seenAt && now - new Date(entry.seenAt).getTime() >= SEEN_EXPIRY_MS ) { delete all[id] changed = true } }
cache = all if (changed) { await writeToDisk(all) }}
/** * Get all flagged plugins from the in-memory cache. * Returns an empty object if loadFlaggedPlugins() has not been called yet. */export function getFlaggedPlugins(): Record<string, FlaggedPlugin> { return cache ?? {}}
/** * Add a plugin to the flagged list. * * @param pluginId "name@marketplace" format */export async function addFlaggedPlugin(pluginId: string): Promise<void> { if (cache === null) { cache = await readFromDisk() }
const updated = { ...cache, [pluginId]: { flaggedAt: new Date().toISOString(), }, }
await writeToDisk(updated) logForDebugging(`Flagged plugin: ${pluginId}`)}
/** * Mark flagged plugins as seen. Called when the Installed view renders * flagged plugins. Sets seenAt on entries that don't already have it. * After 48 hours from seenAt, entries are auto-cleared on next load. */export async function markFlaggedPluginsSeen( pluginIds: string[],): Promise<void> { if (cache === null) { cache = await readFromDisk() } const now = new Date().toISOString() let changed = false
const updated = { ...cache } for (const id of pluginIds) { const entry = updated[id] if (entry && !entry.seenAt) { updated[id] = { ...entry, seenAt: now } changed = true } }
if (changed) { await writeToDisk(updated) }}
/** * Remove a plugin from the flagged list. Called when the user dismisses * a flagged plugin notification in /plugins. */export async function removeFlaggedPlugin(pluginId: string): Promise<void> { if (cache === null) { cache = await readFromDisk() } if (!(pluginId in cache)) return
const { [pluginId]: _, ...rest } = cache cache = rest await writeToDisk(rest)}