Download src/utils/plugins/pluginFlagging.ts from chenbhao/codev: direct link, hf CLI and curl.
- Browser
- Download file 5.62 kB
-
https://huggingface.co/chenbhao/codev/resolve/main/src/utils/plugins/pluginFlagging.ts
- Command line
-
hf download hf://chenbhao/codev/src/utils/plugins/pluginFlagging.ts
-
curl -L -o pluginFlagging.ts https://huggingface.co/chenbhao/codev/resolve/main/src/utils/plugins/pluginFlagging.ts
5.62 kB
| /** | |
| * 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) | |
| } | |