πŸ“„ File detail

utils/plugins/orphanedPluginFilter.ts

🧩 .tsπŸ“ 115 linesπŸ’Ύ 3,980 bytesπŸ“ text
← Back to All Files

🎯 Use case

This file lives under β€œutils/”, which covers cross-cutting helpers (shell, tempfiles, settings, messages, process input, …). On the API surface it exposes getGlobExclusionsForPluginCache and clearPluginCacheExclusions β€” mainly functions, hooks, or classes. Dependencies touch Node path helpers. It composes internal code from ripgrep and pluginDirectories (relative imports). What the file header says: Provides ripgrep glob exclusion patterns for orphaned plugin versions. When plugin versions are updated, old versions are marked with a `.orphaned_at` file but kept on disk for 7 days (since concurrent sessions might still reference them). During this window, Grep/Glob could retu.

Generated from folder role, exports, dependency roots, and inline comments β€” not hand-reviewed for every path.

🧠 Inline summary

Provides ripgrep glob exclusion patterns for orphaned plugin versions. When plugin versions are updated, old versions are marked with a `.orphaned_at` file but kept on disk for 7 days (since concurrent sessions might still reference them). During this window, Grep/Glob could return files from orphaned versions, causing Claude to use outdated plugin code. We find `.orphaned_at` markers via a single ripgrep call and generate `--glob '!<dir>/**'` patterns for their parent directories. The cache is warmed in main.tsx AFTER cleanupOrphanedPluginVersionsInBackground settles disk state. Once populated, the exclusion list is frozen for the session unless /reload-plugins is called; subsequent disk mutations (autoupdate, concurrent sessions) don't affect it.

πŸ“€ Exports (heuristic)

  • getGlobExclusionsForPluginCache
  • clearPluginCacheExclusions

πŸ“š External import roots

Package roots from from "…" (relative paths omitted).

  • path

πŸ–₯️ Source preview

/**
 * Provides ripgrep glob exclusion patterns for orphaned plugin versions.
 *
 * When plugin versions are updated, old versions are marked with a
 * `.orphaned_at` file but kept on disk for 7 days (since concurrent
 * sessions might still reference them). During this window, Grep/Glob
 * could return files from orphaned versions, causing Claude to use
 * outdated plugin code.
 *
 * We find `.orphaned_at` markers via a single ripgrep call and generate
 * `--glob '!<dir>/**'` patterns for their parent directories. The cache
 * is warmed in main.tsx AFTER cleanupOrphanedPluginVersionsInBackground
 * settles disk state. Once populated, the exclusion list is frozen for
 * the session unless /reload-plugins is called; subsequent disk mutations
 * (autoupdate, concurrent sessions) don't affect it.
 */

import { dirname, isAbsolute, join, normalize, relative, sep } from 'path'
import { ripGrep } from '../ripgrep.js'
import { getPluginsDirectory } from './pluginDirectories.js'

// Inlined from cacheUtils.ts to avoid a circular dep through commands.js.
const ORPHANED_AT_FILENAME = '.orphaned_at'

/** Session-scoped cache. Frozen once computed β€” only cleared by explicit /reload-plugins. */
let cachedExclusions: string[] | null = null

/**
 * Get ripgrep glob exclusion patterns for orphaned plugin versions.
 *
 * @param searchPath - When provided, exclusions are only returned if the
 *   search overlaps the plugin cache directory (avoids unnecessary --glob
 *   args for searches outside the cache).
 *
 * Warmed eagerly in main.tsx after orphan GC; the lazy-compute path here
 * is a fallback. Best-effort: returns empty array if anything goes wrong.
 */
export async function getGlobExclusionsForPluginCache(
  searchPath?: string,
): Promise<string[]> {
  const cachePath = normalize(join(getPluginsDirectory(), 'cache'))

  if (searchPath && !pathsOverlap(searchPath, cachePath)) {
    return []
  }

  if (cachedExclusions !== null) {
    return cachedExclusions
  }

  try {
    // Find all .orphaned_at files within the plugin cache directory.
    // --hidden: marker is a dotfile. --no-ignore: don't let a stray
    // .gitignore hide it. --max-depth 4: marker is always at
    // cache/<marketplace>/<plugin>/<version>/.orphaned_at β€” don't recurse
    // into plugin contents (node_modules, etc.). Never-aborts signal: no
    // caller signal to thread.
    const markers = await ripGrep(
      [
        '--files',
        '--hidden',
        '--no-ignore',
        '--max-depth',
        '4',
        '--glob',
        ORPHANED_AT_FILENAME,
      ],
      cachePath,
      new AbortController().signal,
    )

    cachedExclusions = markers.map(markerPath => {
      // ripgrep may return absolute or relative β€” normalize to relative.
      const versionDir = dirname(markerPath)
      const rel = isAbsolute(versionDir)
        ? relative(cachePath, versionDir)
        : versionDir
      // ripgrep glob patterns always use forward slashes, even on Windows
      const posixRelative = rel.replace(/\\/g, '/')
      return `!**/${posixRelative}/**`
    })
    return cachedExclusions
  } catch {
    // Best-effort β€” don't break core search tools if ripgrep fails here
    cachedExclusions = []
    return cachedExclusions
  }
}

export function clearPluginCacheExclusions(): void {
  cachedExclusions = null
}

/**
 * One path is a prefix of the other. Special-cases root (normalize('/') + sep
 * = '//'). Case-insensitive on win32 since normalize() doesn't lowercase
 * drive letters and CLAUDE_CODE_PLUGIN_CACHE_DIR may disagree with resolved.
 */
function pathsOverlap(a: string, b: string): boolean {
  const na = normalizeForCompare(a)
  const nb = normalizeForCompare(b)
  return (
    na === nb ||
    na === sep ||
    nb === sep ||
    na.startsWith(nb + sep) ||
    nb.startsWith(na + sep)
  )
}

function normalizeForCompare(p: string): string {
  const n = normalize(p)
  return process.platform === 'win32' ? n.toLowerCase() : n
}