π File detail
utils/plugins/headlessPluginInstall.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 installPluginsForHeadless β mainly functions, hooks, or classes. It composes internal code from services, cleanupRegistry, debug, diagLogs, and fsOperations (relative imports). What the file header says: Plugin installation for headless/CCR mode. This module provides plugin installation without AppState updates, suitable for non-interactive environments like CCR. When CLAUDE_CODE_PLUGIN_USE_ZIP_CACHE is enabled, plugins are stored as ZIPs on a mounted volume. The storage layer (p.
Generated from folder role, exports, dependency roots, and inline comments β not hand-reviewed for every path.
π§ Inline summary
Plugin installation for headless/CCR mode. This module provides plugin installation without AppState updates, suitable for non-interactive environments like CCR. When CLAUDE_CODE_PLUGIN_USE_ZIP_CACHE is enabled, plugins are stored as ZIPs on a mounted volume. The storage layer (pluginLoader.ts) handles ZIP creation on install and extraction on load transparently.
π€ Exports (heuristic)
installPluginsForHeadless
π₯οΈ Source preview
/**
* Plugin installation for headless/CCR mode.
*
* This module provides plugin installation without AppState updates,
* suitable for non-interactive environments like CCR.
*
* When CLAUDE_CODE_PLUGIN_USE_ZIP_CACHE is enabled, plugins are stored as
* ZIPs on a mounted volume. The storage layer (pluginLoader.ts) handles
* ZIP creation on install and extraction on load transparently.
*/
import { logEvent } from '../../services/analytics/index.js'
import { registerCleanup } from '../cleanupRegistry.js'
import { logForDebugging } from '../debug.js'
import { withDiagnosticsTiming } from '../diagLogs.js'
import { getFsImplementation } from '../fsOperations.js'
import { logError } from '../log.js'
import {
clearMarketplacesCache,
getDeclaredMarketplaces,
registerSeedMarketplaces,
} from './marketplaceManager.js'
import { detectAndUninstallDelistedPlugins } from './pluginBlocklist.js'
import { clearPluginCache } from './pluginLoader.js'
import { reconcileMarketplaces } from './reconciler.js'
import {
cleanupSessionPluginCache,
getZipCacheMarketplacesDir,
getZipCachePluginsDir,
isMarketplaceSourceSupportedByZipCache,
isPluginZipCacheEnabled,
} from './zipCache.js'
import { syncMarketplacesToZipCache } from './zipCacheAdapters.js'
/**
* Install plugins for headless/CCR mode.
*
* This is the headless equivalent of performBackgroundPluginInstallations(),
* but without AppState updates (no UI to update in headless mode).
*
* @returns true if any plugins were installed (caller should refresh MCP)
*/
export async function installPluginsForHeadless(): Promise<boolean> {
const zipCacheMode = isPluginZipCacheEnabled()
logForDebugging(
`installPluginsForHeadless: starting${zipCacheMode ? ' (zip cache mode)' : ''}`,
)
// Register seed marketplaces (CLAUDE_CODE_PLUGIN_SEED_DIR) before diffing.
// Idempotent; no-op if seed not configured. Without this, findMissingMarketplaces
// would see seed entries as missing β clone β defeats seed's purpose.
//
// If registration changed state, clear caches so the early plugin-load pass
// (which runs during CLI startup before this function) doesn't keep stale
// "marketplace not found" results. Without this clear, a first-boot headless
// run with a seed-cached plugin would show 0 plugin commands/agents/skills
// in the init message even though the seed has everything.
const seedChanged = await registerSeedMarketplaces()
if (seedChanged) {
clearMarketplacesCache()
clearPluginCache('headlessPluginInstall: seed marketplaces registered')
}
// Ensure zip cache directory structure exists
if (zipCacheMode) {
await getFsImplementation().mkdir(getZipCacheMarketplacesDir())
await getFsImplementation().mkdir(getZipCachePluginsDir())
}
// Declared now includes an implicit claude-plugins-official entry when any
// enabled plugin references it (see getDeclaredMarketplaces). This routes
// the official marketplace through the same reconciler path as any other β
// which composes correctly with CLAUDE_CODE_PLUGIN_SEED_DIR: seed registers
// it in known_marketplaces.json, reconciler diff sees it as upToDate, no clone.
const declaredCount = Object.keys(getDeclaredMarketplaces()).length
const metrics = {
marketplaces_installed: 0,
delisted_count: 0,
}
// Initialize from seedChanged so the caller (print.ts) calls
// refreshPluginState() β clearCommandsCache/clearAgentDefinitionsCache
// when seed registration added marketplaces. Without this, the caller
// only refreshes when an actual plugin install happened.
let pluginsChanged = seedChanged
try {
if (declaredCount === 0) {
logForDebugging('installPluginsForHeadless: no marketplaces declared')
} else {
// Reconcile declared marketplaces (settings intent + implicit official)
// with materialized state. Zip cache: skip unsupported source types.
const reconcileResult = await withDiagnosticsTiming(
'headless_marketplace_reconcile',
() =>
reconcileMarketplaces({
skip: zipCacheMode
? (_name, source) =>
!isMarketplaceSourceSupportedByZipCache(source)
: undefined,
onProgress: event => {
if (event.type === 'installed') {
logForDebugging(
`installPluginsForHeadless: installed marketplace ${event.name}`,
)
} else if (event.type === 'failed') {
logForDebugging(
`installPluginsForHeadless: failed to install marketplace ${event.name}: ${event.error}`,
)
}
},
}),
r => ({
installed_count: r.installed.length,
updated_count: r.updated.length,
failed_count: r.failed.length,
skipped_count: r.skipped.length,
}),
)
if (reconcileResult.skipped.length > 0) {
logForDebugging(
`installPluginsForHeadless: skipped ${reconcileResult.skipped.length} marketplace(s) unsupported by zip cache: ${reconcileResult.skipped.join(', ')}`,
)
}
const marketplacesChanged =
reconcileResult.installed.length + reconcileResult.updated.length
// Clear caches so newly-installed marketplace plugins are discoverable.
// Plugin caching is the loader's job β after caches clear, the caller's
// refreshPluginState() β loadAllPlugins() will cache any missing plugins
// from the newly-materialized marketplaces.
if (marketplacesChanged > 0) {
clearMarketplacesCache()
clearPluginCache('headlessPluginInstall: marketplaces reconciled')
pluginsChanged = true
}
metrics.marketplaces_installed = marketplacesChanged
}
// Zip cache: save marketplace JSONs for offline access on ephemeral containers.
// Runs unconditionally so that steady-state containers (all plugins installed)
// still sync marketplace data that may have been cloned in a previous run.
if (zipCacheMode) {
await syncMarketplacesToZipCache()
}
// Delisting enforcement
const newlyDelisted = await detectAndUninstallDelistedPlugins()
metrics.delisted_count = newlyDelisted.length
if (newlyDelisted.length > 0) {
pluginsChanged = true
}
if (pluginsChanged) {
clearPluginCache('headlessPluginInstall: plugins changed')
}
// Zip cache: register session cleanup for extracted plugin temp dirs
if (zipCacheMode) {
registerCleanup(cleanupSessionPluginCache)
}
return pluginsChanged
} catch (error) {
logError(error)
return false
} finally {
logEvent('tengu_headless_plugin_install', metrics)
}
}