π File detail
utils/plugins/pluginVersioning.ts
π― Use case
This file lives under βutils/β, which covers cross-cutting helpers (shell, tempfiles, settings, messages, process input, β¦). On the API surface it exposes calculatePluginVersion, getGitCommitSha, getVersionFromPath, and isVersionedPath β mainly functions, hooks, or classes. Dependencies touch crypto. It composes internal code from debug, git, and schemas (relative imports). What the file header says: Plugin Version Calculation Module Handles version calculation for plugins from various sources. Versions are used for versioned cache paths and update detection. Version sources (in order of preference): 1. Explicit version from plugin.json 2. Git commit SHA (for git/github sourc.
Generated from folder role, exports, dependency roots, and inline comments β not hand-reviewed for every path.
π§ Inline summary
Plugin Version Calculation Module Handles version calculation for plugins from various sources. Versions are used for versioned cache paths and update detection. Version sources (in order of preference): 1. Explicit version from plugin.json 2. Git commit SHA (for git/github sources) 3. Fallback timestamp for local sources
π€ Exports (heuristic)
calculatePluginVersiongetGitCommitShagetVersionFromPathisVersionedPath
π External import roots
Package roots from from "β¦" (relative paths omitted).
crypto
π₯οΈ Source preview
/**
* Plugin Version Calculation Module
*
* Handles version calculation for plugins from various sources.
* Versions are used for versioned cache paths and update detection.
*
* Version sources (in order of preference):
* 1. Explicit version from plugin.json
* 2. Git commit SHA (for git/github sources)
* 3. Fallback timestamp for local sources
*/
import { createHash } from 'crypto'
import { logForDebugging } from '../debug.js'
import { getHeadForDir } from '../git/gitFilesystem.js'
import type { PluginManifest, PluginSource } from './schemas.js'
/**
* Calculate the version for a plugin based on its source.
*
* Version sources (in order of priority):
* 1. plugin.json version field (highest priority)
* 2. Provided version (typically from marketplace entry)
* 3. Git commit SHA from install path
* 4. 'unknown' as last resort
*
* @param pluginId - Plugin identifier (e.g., "plugin@marketplace")
* @param source - Plugin source configuration (used for git-subdir path hashing)
* @param manifest - Optional plugin manifest with version field
* @param installPath - Optional path to installed plugin (for git SHA extraction)
* @param providedVersion - Optional version from marketplace entry or caller
* @param gitCommitSha - Optional pre-resolved git SHA (for sources like
* git-subdir where the clone is discarded and the install path has no .git)
* @returns Version string (semver, short SHA, or 'unknown')
*/
export async function calculatePluginVersion(
pluginId: string,
source: PluginSource,
manifest?: PluginManifest,
installPath?: string,
providedVersion?: string,
gitCommitSha?: string,
): Promise<string> {
// 1. Use explicit version from plugin.json if available
if (manifest?.version) {
logForDebugging(
`Using manifest version for ${pluginId}: ${manifest.version}`,
)
return manifest.version
}
// 2. Use provided version (typically from marketplace entry)
if (providedVersion) {
logForDebugging(
`Using provided version for ${pluginId}: ${providedVersion}`,
)
return providedVersion
}
// 3. Use pre-resolved git SHA if caller captured it before discarding the clone
if (gitCommitSha) {
const shortSha = gitCommitSha.substring(0, 12)
if (typeof source === 'object' && source.source === 'git-subdir') {
// Encode the subdir path in the version so cache keys differ when
// marketplace.json's `path` changes but the monorepo SHA doesn't.
// Without this, two plugins at different subdirs of the same commit
// collide at cache/<m>/<p>/<sha>/ and serve each other's trees.
//
// Normalization MUST match the squashfs cron byte-for-byte:
// 1. backslash β forward slash
// 2. strip one leading `./`
// 3. strip all trailing `/`
// 4. UTF-8 sha256, first 8 hex chars
// See api/β¦/plugins_official_squashfs/job.py _validate_subdir().
const normPath = source.path
.replace(/\\/g, '/')
.replace(/^\.\//, '')
.replace(/\/+$/, '')
const pathHash = createHash('sha256')
.update(normPath)
.digest('hex')
.substring(0, 8)
const v = `${shortSha}-${pathHash}`
logForDebugging(
`Using git-subdir SHA+path version for ${pluginId}: ${v} (path=${normPath})`,
)
return v
}
logForDebugging(`Using pre-resolved git SHA for ${pluginId}: ${shortSha}`)
return shortSha
}
// 4. Try to get git SHA from install path
if (installPath) {
const sha = await getGitCommitSha(installPath)
if (sha) {
const shortSha = sha.substring(0, 12)
logForDebugging(`Using git SHA for ${pluginId}: ${shortSha}`)
return shortSha
}
}
// 5. Return 'unknown' as last resort
logForDebugging(`No version found for ${pluginId}, using 'unknown'`)
return 'unknown'
}
/**
* Get the git commit SHA for a directory.
*
* @param dirPath - Path to directory (should be a git repository)
* @returns Full commit SHA or null if not a git repo
*/
export function getGitCommitSha(dirPath: string): Promise<string | null> {
return getHeadForDir(dirPath)
}
/**
* Extract version from a versioned cache path.
*
* Given a path like `~/.claude/plugins/cache/marketplace/plugin/1.0.0`,
* extracts and returns `1.0.0`.
*
* @param installPath - Full path to plugin installation
* @returns Version string from path, or null if not a versioned path
*/
export function getVersionFromPath(installPath: string): string | null {
// Versioned paths have format: .../plugins/cache/marketplace/plugin/version/
const parts = installPath.split('/').filter(Boolean)
// Find 'cache' index to determine depth
const cacheIndex = parts.findIndex(
(part, i) => part === 'cache' && parts[i - 1] === 'plugins',
)
if (cacheIndex === -1) {
return null
}
// Versioned path has 3 components after 'cache': marketplace/plugin/version
const componentsAfterCache = parts.slice(cacheIndex + 1)
if (componentsAfterCache.length >= 3) {
return componentsAfterCache[2] || null
}
return null
}
/**
* Check if a path is a versioned plugin path.
*
* @param path - Path to check
* @returns True if path follows versioned structure
*/
export function isVersionedPath(path: string): boolean {
return getVersionFromPath(path) !== null
}