/*** Centralized plugin directory configuration.** This module provides the single source of truth for the plugins directory path.* It supports switching between 'plugins' and 'cowork_plugins' directories via:* - CLI flag: --cowork* - Environment variable: CLAUDE_CODE_USE_COWORK_PLUGINS** The base directory can be overridden via CLAUDE_CODE_PLUGIN_CACHE_DIR.*/import { mkdirSync } from 'fs'import { readdir, rm, stat } from 'fs/promises'import { delimiter, join } from 'path'import { getUseCoworkPlugins } from '../../bootstrap/state.js'import { logForDebugging } from '../debug.js'import { getClaudeConfigHomeDir, isEnvTruthy } from '../envUtils.js'import { errorMessage, isFsInaccessible } from '../errors.js'import { formatFileSize } from '../format.js'import { expandTilde } from '../permissions/pathValidation.js'const PLUGINS_DIR = 'plugins'const COWORK_PLUGINS_DIR = 'cowork_plugins'/*** Get the plugins directory name based on current mode.* Uses session state (from --cowork flag) or env var.** Priority:* 1. Session state (set by CLI flag --cowork)* 2. Environment variable CLAUDE_CODE_USE_COWORK_PLUGINS* 3. Default: 'plugins'*/function getPluginsDirectoryName(): string {// Session state takes precedence (set by CLI flag)if (getUseCoworkPlugins()) {return COWORK_PLUGINS_DIR}// Fall back to env varif (isEnvTruthy(process.env.CLAUDE_CODE_USE_COWORK_PLUGINS)) {return COWORK_PLUGINS_DIR}return PLUGINS_DIR}/*** Get the full path to the plugins directory.** Priority:* 1. CLAUDE_CODE_PLUGIN_CACHE_DIR env var (explicit override)* 2. Default: ~/.claude/plugins or ~/.claude/cowork_plugins*/export function getPluginsDirectory(): string {// expandTilde: when CLAUDE_CODE_PLUGIN_CACHE_DIR is set via settings.json// `env` (not shell), ~ is not expanded by the shell. Without this, a value// like "~/.claude/plugins" becomes a literal `~` directory created in the// cwd of every project (gh-30794 / CC-212).const envOverride = process.env.CLAUDE_CODE_PLUGIN_CACHE_DIRif (envOverride) {return expandTilde(envOverride)}return join(getClaudeConfigHomeDir(), getPluginsDirectoryName())}/*** Get the read-only plugin seed directories, if configured.** Customers can pre-bake a populated plugins directory into their container* image and point CLAUDE_CODE_PLUGIN_SEED_DIR at it. CC will use it as a* read-only fallback layer under the primary plugins directory — marketplaces* and plugin caches found in the seed are used in place without re-cloning.** Multiple seed directories can be layered using the platform path delimiter* (':' on Unix, ';' on Windows), in PATH-like precedence order — the first* seed that contains a given marketplace or plugin cache wins.** Seed structure mirrors the primary plugins directory:* $CLAUDE_CODE_PLUGIN_SEED_DIR/* known_marketplaces.json* marketplaces/<name>/...* cache/<marketplace>/<plugin>/<version>/...** @returns Absolute paths to seed dirs in precedence order (empty if unset)*/export function getPluginSeedDirs(): string[] {// Same tilde-expansion rationale as getPluginsDirectory (gh-30794).const raw = process.env.CLAUDE_CODE_PLUGIN_SEED_DIRif (!raw) return []return raw.split(delimiter).filter(Boolean).map(expandTilde)}function sanitizePluginId(pluginId: string): string {// Same character class as the install-cache sanitizer (pluginLoader.ts)return pluginId.replace(/[^a-zA-Z0-9\-_]/g, '-')}/** Pure path — no mkdir. For display (e.g. uninstall dialog). */export function pluginDataDirPath(pluginId: string): string {return join(getPluginsDirectory(), 'data', sanitizePluginId(pluginId))}/*** Persistent per-plugin data directory, exposed to plugins as* ${CLAUDE_PLUGIN_DATA}. Unlike the version-scoped install cache* (${CLAUDE_PLUGIN_ROOT}, which is orphaned and GC'd on every update),* this survives plugin updates — only removed on last-scope uninstall.** Creates the directory on call (mkdir). The *lazy* behavior is at the* substitutePluginVariables call site — the DATA pattern uses function-form* .replace() so this isn't invoked unless ${CLAUDE_PLUGIN_DATA} is present* (ROOT also uses function-form, but for $-pattern safety, not laziness).* Env-var export sites (MCP/LSP server env, hook env) call this eagerly* since subprocesses may expect the dir to exist before writing to it.** Sync because it's called from substitutePluginVariables (sync, inside* String.replace) — making this async would cascade through 6 call sites* and their sync iteration loops. One mkdir in plugin-load path is cheap.*/export function getPluginDataDir(pluginId: string): string {const dir = pluginDataDirPath(pluginId)mkdirSync(dir, { recursive: true })return dir}/*** Size of the data dir for the uninstall confirmation prompt. Returns null* when the dir is absent or empty so callers can skip the prompt entirely.* Recursive walk — not hot-path (only on uninstall).*/export async function getPluginDataDirSize(pluginId: string,): Promise<{ bytes: number; human: string } | null> {const dir = pluginDataDirPath(pluginId)let bytes = 0const walk = async (p: string) => {for (const entry of await readdir(p, { withFileTypes: true })) {const full = join(p, entry.name)if (entry.isDirectory()) {await walk(full)} else {// Per-entry catch: a broken symlink makes stat() throw ENOENT.// Without this, one broken link bubbles to the outer catch →// returns null → dialog skipped → data silently deleted.try {bytes += (await stat(full)).size} catch {// Broken symlink / raced delete — skip this entry, keep walking}}}}try {await walk(dir)} catch (e) {if (isFsInaccessible(e)) return nullthrow e}if (bytes === 0) return nullreturn { bytes, human: formatFileSize(bytes) }}/*** Best-effort cleanup on last-scope uninstall. Failure is logged but does* not throw — the uninstall itself already succeeded; we don't want a* cleanup side-effect surfacing as "uninstall failed". Same rationale as* deletePluginOptions (pluginOptionsStorage.ts).*/export async function deletePluginDataDir(pluginId: string): Promise<void> {const dir = pluginDataDirPath(pluginId)try {await rm(dir, { recursive: true, force: true })} catch (e) {logForDebugging(`Failed to delete plugin data dir ${dir}: ${errorMessage(e)}`,{ level: 'warn' },)}}
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。