pluginVersioning.ts
utils/plugins/pluginVersioning.ts
No strong subsystem tag
158
Lines
5340
Bytes
4
Exports
4
Imports
10
Keywords
What this is
This page documents one file from the repository and includes its full source so you can read it without leaving the docs site.
Beginner explanation
This file is one piece of the larger system. Its name, directory, imports, and exports show where it fits. Start by reading the exports and related files first.
How it is used
Start from the exports list and related files. Those are the easiest clues for where this file fits into the system.
Expert explanation
Architecturally, this file intersects with general runtime concerns. It contains 158 lines, 4 detected imports, and 4 detected exports.
Important relationships
Detected exports
calculatePluginVersiongetGitCommitShagetVersionFromPathisVersionedPath
Keywords
versionpathplugincacheparamsourcemarketplacepluginidversionedlogfordebugging
Detected imports
crypto../debug.js../git/gitFilesystem.js./schemas.js
Source notes
This page embeds the full file contents. Small or leaf files are still indexed honestly instead of being over-explained.
Full source
/**
* 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
}