source dump of claude code
at main 114 lines 4.0 kB view raw
1/** 2 * Provides ripgrep glob exclusion patterns for orphaned plugin versions. 3 * 4 * When plugin versions are updated, old versions are marked with a 5 * `.orphaned_at` file but kept on disk for 7 days (since concurrent 6 * sessions might still reference them). During this window, Grep/Glob 7 * could return files from orphaned versions, causing Claude to use 8 * outdated plugin code. 9 * 10 * We find `.orphaned_at` markers via a single ripgrep call and generate 11 * `--glob '!<dir>/**'` patterns for their parent directories. The cache 12 * is warmed in main.tsx AFTER cleanupOrphanedPluginVersionsInBackground 13 * settles disk state. Once populated, the exclusion list is frozen for 14 * the session unless /reload-plugins is called; subsequent disk mutations 15 * (autoupdate, concurrent sessions) don't affect it. 16 */ 17 18import { dirname, isAbsolute, join, normalize, relative, sep } from 'path' 19import { ripGrep } from '../ripgrep.js' 20import { getPluginsDirectory } from './pluginDirectories.js' 21 22// Inlined from cacheUtils.ts to avoid a circular dep through commands.js. 23const ORPHANED_AT_FILENAME = '.orphaned_at' 24 25/** Session-scoped cache. Frozen once computed — only cleared by explicit /reload-plugins. */ 26let cachedExclusions: string[] | null = null 27 28/** 29 * Get ripgrep glob exclusion patterns for orphaned plugin versions. 30 * 31 * @param searchPath - When provided, exclusions are only returned if the 32 * search overlaps the plugin cache directory (avoids unnecessary --glob 33 * args for searches outside the cache). 34 * 35 * Warmed eagerly in main.tsx after orphan GC; the lazy-compute path here 36 * is a fallback. Best-effort: returns empty array if anything goes wrong. 37 */ 38export async function getGlobExclusionsForPluginCache( 39 searchPath?: string, 40): Promise<string[]> { 41 const cachePath = normalize(join(getPluginsDirectory(), 'cache')) 42 43 if (searchPath && !pathsOverlap(searchPath, cachePath)) { 44 return [] 45 } 46 47 if (cachedExclusions !== null) { 48 return cachedExclusions 49 } 50 51 try { 52 // Find all .orphaned_at files within the plugin cache directory. 53 // --hidden: marker is a dotfile. --no-ignore: don't let a stray 54 // .gitignore hide it. --max-depth 4: marker is always at 55 // cache/<marketplace>/<plugin>/<version>/.orphaned_at — don't recurse 56 // into plugin contents (node_modules, etc.). Never-aborts signal: no 57 // caller signal to thread. 58 const markers = await ripGrep( 59 [ 60 '--files', 61 '--hidden', 62 '--no-ignore', 63 '--max-depth', 64 '4', 65 '--glob', 66 ORPHANED_AT_FILENAME, 67 ], 68 cachePath, 69 new AbortController().signal, 70 ) 71 72 cachedExclusions = markers.map(markerPath => { 73 // ripgrep may return absolute or relative — normalize to relative. 74 const versionDir = dirname(markerPath) 75 const rel = isAbsolute(versionDir) 76 ? relative(cachePath, versionDir) 77 : versionDir 78 // ripgrep glob patterns always use forward slashes, even on Windows 79 const posixRelative = rel.replace(/\\/g, '/') 80 return `!**/${posixRelative}/**` 81 }) 82 return cachedExclusions 83 } catch { 84 // Best-effort — don't break core search tools if ripgrep fails here 85 cachedExclusions = [] 86 return cachedExclusions 87 } 88} 89 90export function clearPluginCacheExclusions(): void { 91 cachedExclusions = null 92} 93 94/** 95 * One path is a prefix of the other. Special-cases root (normalize('/') + sep 96 * = '//'). Case-insensitive on win32 since normalize() doesn't lowercase 97 * drive letters and CLAUDE_CODE_PLUGIN_CACHE_DIR may disagree with resolved. 98 */ 99function pathsOverlap(a: string, b: string): boolean { 100 const na = normalizeForCompare(a) 101 const nb = normalizeForCompare(b) 102 return ( 103 na === nb || 104 na === sep || 105 nb === sep || 106 na.startsWith(nb + sep) || 107 nb.startsWith(na + sep) 108 ) 109} 110 111function normalizeForCompare(p: string): string { 112 const n = normalize(p) 113 return process.platform === 'win32' ? n.toLowerCase() : n 114}