headlessPluginInstall.ts
utils/plugins/headlessPluginInstall.ts
No strong subsystem tag
175
Lines
6775
Bytes
1
Exports
12
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 175 lines, 12 detected imports, and 1 detected exports.
Important relationships
Detected exports
installPluginsForHeadless
Keywords
lengthpluginseedmarketplacepluginscachemarketplaceslogfordebugginginstalledinstallpluginsforheadless
Detected imports
../../services/analytics/index.js../cleanupRegistry.js../debug.js../diagLogs.js../fsOperations.js../log.js./marketplaceManager.js./pluginBlocklist.js./pluginLoader.js./reconciler.js./zipCache.js./zipCacheAdapters.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 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)
}
}