source dump of claude code
at main 157 lines 5.3 kB view raw
1/** 2 * Plugin Version Calculation Module 3 * 4 * Handles version calculation for plugins from various sources. 5 * Versions are used for versioned cache paths and update detection. 6 * 7 * Version sources (in order of preference): 8 * 1. Explicit version from plugin.json 9 * 2. Git commit SHA (for git/github sources) 10 * 3. Fallback timestamp for local sources 11 */ 12 13import { createHash } from 'crypto' 14import { logForDebugging } from '../debug.js' 15import { getHeadForDir } from '../git/gitFilesystem.js' 16import type { PluginManifest, PluginSource } from './schemas.js' 17 18/** 19 * Calculate the version for a plugin based on its source. 20 * 21 * Version sources (in order of priority): 22 * 1. plugin.json version field (highest priority) 23 * 2. Provided version (typically from marketplace entry) 24 * 3. Git commit SHA from install path 25 * 4. 'unknown' as last resort 26 * 27 * @param pluginId - Plugin identifier (e.g., "plugin@marketplace") 28 * @param source - Plugin source configuration (used for git-subdir path hashing) 29 * @param manifest - Optional plugin manifest with version field 30 * @param installPath - Optional path to installed plugin (for git SHA extraction) 31 * @param providedVersion - Optional version from marketplace entry or caller 32 * @param gitCommitSha - Optional pre-resolved git SHA (for sources like 33 * git-subdir where the clone is discarded and the install path has no .git) 34 * @returns Version string (semver, short SHA, or 'unknown') 35 */ 36export async function calculatePluginVersion( 37 pluginId: string, 38 source: PluginSource, 39 manifest?: PluginManifest, 40 installPath?: string, 41 providedVersion?: string, 42 gitCommitSha?: string, 43): Promise<string> { 44 // 1. Use explicit version from plugin.json if available 45 if (manifest?.version) { 46 logForDebugging( 47 `Using manifest version for ${pluginId}: ${manifest.version}`, 48 ) 49 return manifest.version 50 } 51 52 // 2. Use provided version (typically from marketplace entry) 53 if (providedVersion) { 54 logForDebugging( 55 `Using provided version for ${pluginId}: ${providedVersion}`, 56 ) 57 return providedVersion 58 } 59 60 // 3. Use pre-resolved git SHA if caller captured it before discarding the clone 61 if (gitCommitSha) { 62 const shortSha = gitCommitSha.substring(0, 12) 63 if (typeof source === 'object' && source.source === 'git-subdir') { 64 // Encode the subdir path in the version so cache keys differ when 65 // marketplace.json's `path` changes but the monorepo SHA doesn't. 66 // Without this, two plugins at different subdirs of the same commit 67 // collide at cache/<m>/<p>/<sha>/ and serve each other's trees. 68 // 69 // Normalization MUST match the squashfs cron byte-for-byte: 70 // 1. backslash → forward slash 71 // 2. strip one leading `./` 72 // 3. strip all trailing `/` 73 // 4. UTF-8 sha256, first 8 hex chars 74 // See api/…/plugins_official_squashfs/job.py _validate_subdir(). 75 const normPath = source.path 76 .replace(/\\/g, '/') 77 .replace(/^\.\//, '') 78 .replace(/\/+$/, '') 79 const pathHash = createHash('sha256') 80 .update(normPath) 81 .digest('hex') 82 .substring(0, 8) 83 const v = `${shortSha}-${pathHash}` 84 logForDebugging( 85 `Using git-subdir SHA+path version for ${pluginId}: ${v} (path=${normPath})`, 86 ) 87 return v 88 } 89 logForDebugging(`Using pre-resolved git SHA for ${pluginId}: ${shortSha}`) 90 return shortSha 91 } 92 93 // 4. Try to get git SHA from install path 94 if (installPath) { 95 const sha = await getGitCommitSha(installPath) 96 if (sha) { 97 const shortSha = sha.substring(0, 12) 98 logForDebugging(`Using git SHA for ${pluginId}: ${shortSha}`) 99 return shortSha 100 } 101 } 102 103 // 5. Return 'unknown' as last resort 104 logForDebugging(`No version found for ${pluginId}, using 'unknown'`) 105 return 'unknown' 106} 107 108/** 109 * Get the git commit SHA for a directory. 110 * 111 * @param dirPath - Path to directory (should be a git repository) 112 * @returns Full commit SHA or null if not a git repo 113 */ 114export function getGitCommitSha(dirPath: string): Promise<string | null> { 115 return getHeadForDir(dirPath) 116} 117 118/** 119 * Extract version from a versioned cache path. 120 * 121 * Given a path like `~/.claude/plugins/cache/marketplace/plugin/1.0.0`, 122 * extracts and returns `1.0.0`. 123 * 124 * @param installPath - Full path to plugin installation 125 * @returns Version string from path, or null if not a versioned path 126 */ 127export function getVersionFromPath(installPath: string): string | null { 128 // Versioned paths have format: .../plugins/cache/marketplace/plugin/version/ 129 const parts = installPath.split('/').filter(Boolean) 130 131 // Find 'cache' index to determine depth 132 const cacheIndex = parts.findIndex( 133 (part, i) => part === 'cache' && parts[i - 1] === 'plugins', 134 ) 135 136 if (cacheIndex === -1) { 137 return null 138 } 139 140 // Versioned path has 3 components after 'cache': marketplace/plugin/version 141 const componentsAfterCache = parts.slice(cacheIndex + 1) 142 if (componentsAfterCache.length >= 3) { 143 return componentsAfterCache[2] || null 144 } 145 146 return null 147} 148 149/** 150 * Check if a path is a versioned plugin path. 151 * 152 * @param path - Path to check 153 * @returns True if path follows versioned structure 154 */ 155export function isVersionedPath(path: string): boolean { 156 return getVersionFromPath(path) !== null 157}