Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
31 kB · 878 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879/** * Plugin and marketplace subcommand handlers — extracted from main.tsx for lazy loading. * These are dynamically imported only when `claude plugin *` or `claude plugin marketplace *` runs. *//* eslint-disable custom-rules/no-process-exit -- CLI subcommand handlers intentionally exit */import figures from 'figures'import { basename, dirname } from 'path'import { setUseCoworkPlugins } from '../../bootstrap/state.js'import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, type AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED, logEvent,} from '../../services/analytics/index.js'import { disableAllPlugins, disablePlugin, enablePlugin, installPlugin, uninstallPlugin, updatePluginCli, VALID_INSTALLABLE_SCOPES, VALID_UPDATE_SCOPES,} from '../../services/plugins/pluginCliCommands.js'import { getPluginErrorMessage } from '../../types/plugin.js'import { errorMessage } from '../../utils/errors.js'import { logError } from '../../utils/log.js'import { clearAllCaches } from '../../utils/plugins/cacheUtils.js'import { getInstallCounts } from '../../utils/plugins/installCounts.js'import { isPluginInstalled, loadInstalledPluginsV2,} from '../../utils/plugins/installedPluginsManager.js'import { createPluginId, loadMarketplacesWithGracefulDegradation,} from '../../utils/plugins/marketplaceHelpers.js'import { addMarketplaceSource, loadKnownMarketplacesConfig, refreshAllMarketplaces, refreshMarketplace, removeMarketplaceSource, saveMarketplaceToSettings,} from '../../utils/plugins/marketplaceManager.js'import { loadPluginMcpServers } from '../../utils/plugins/mcpPluginIntegration.js'import { parseMarketplaceInput } from '../../utils/plugins/parseMarketplaceInput.js'import { parsePluginIdentifier, scopeToSettingSource,} from '../../utils/plugins/pluginIdentifier.js'import { loadAllPlugins } from '../../utils/plugins/pluginLoader.js'import type { PluginSource } from '../../utils/plugins/schemas.js'import { type ValidationResult, validateManifest, validatePluginContents,} from '../../utils/plugins/validatePlugin.js'import { jsonStringify } from '../../utils/slowOperations.js'import { plural } from '../../utils/stringUtils.js'import { cliError, cliOk } from '../exit.js'
// Re-export for main.tsx to reference in option definitionsexport { VALID_INSTALLABLE_SCOPES, VALID_UPDATE_SCOPES }
/** * Helper function to handle marketplace command errors consistently. */export function handleMarketplaceError(error: unknown, action: string): never { logError(error) cliError(`${figures.cross} Failed to ${action}: ${errorMessage(error)}`)}
function printValidationResult(result: ValidationResult): void { if (result.errors.length > 0) { // biome-ignore lint/suspicious/noConsole:: intentional console output console.log( `${figures.cross} Found ${result.errors.length} ${plural(result.errors.length, 'error')}:\n`, ) result.errors.forEach(error => { // biome-ignore lint/suspicious/noConsole:: intentional console output console.log(` ${figures.pointer} ${error.path}: ${error.message}`) }) // biome-ignore lint/suspicious/noConsole:: intentional console output console.log('') } if (result.warnings.length > 0) { // biome-ignore lint/suspicious/noConsole:: intentional console output console.log( `${figures.warning} Found ${result.warnings.length} ${plural(result.warnings.length, 'warning')}:\n`, ) result.warnings.forEach(warning => { // biome-ignore lint/suspicious/noConsole:: intentional console output console.log(` ${figures.pointer} ${warning.path}: ${warning.message}`) }) // biome-ignore lint/suspicious/noConsole:: intentional console output console.log('') }}
// plugin validateexport async function pluginValidateHandler( manifestPath: string, options: { cowork?: boolean },): Promise<void> { if (options.cowork) setUseCoworkPlugins(true) try { const result = await validateManifest(manifestPath)
// biome-ignore lint/suspicious/noConsole:: intentional console output console.log(`Validating ${result.fileType} manifest: ${result.filePath}\n`) printValidationResult(result)
// If this is a plugin manifest located inside a .claude-plugin directory, // also validate the plugin's content files (skills, agents, commands, // hooks). Works whether the user passed a directory or the plugin.json // path directly. let contentResults: ValidationResult[] = [] if (result.fileType === 'plugin') { const manifestDir = dirname(result.filePath) if (basename(manifestDir) === '.claude-plugin') { contentResults = await validatePluginContents(dirname(manifestDir)) for (const r of contentResults) { // biome-ignore lint/suspicious/noConsole:: intentional console output console.log(`Validating ${r.fileType}: ${r.filePath}\n`) printValidationResult(r) } } }
const allSuccess = result.success && contentResults.every(r => r.success) const hasWarnings = result.warnings.length > 0 || contentResults.some(r => r.warnings.length > 0)
if (allSuccess) { cliOk( hasWarnings ? `${figures.tick} Validation passed with warnings` : `${figures.tick} Validation passed`, ) } else { // biome-ignore lint/suspicious/noConsole:: intentional console output console.log(`${figures.cross} Validation failed`) process.exit(1) } } catch (error) { logError(error) // biome-ignore lint/suspicious/noConsole:: intentional console output console.error( `${figures.cross} Unexpected error during validation: ${errorMessage(error)}`, ) process.exit(2) }}
// plugin list (lines 5217–5416)export async function pluginListHandler(options: { json?: boolean available?: boolean cowork?: boolean}): Promise<void> { if (options.cowork) setUseCoworkPlugins(true) logEvent('tengu_plugin_list_command', {})
const installedData = loadInstalledPluginsV2() const { getPluginEditableScopes } = await import( '../../utils/plugins/pluginStartupCheck.js' ) const enabledPlugins = getPluginEditableScopes()
const pluginIds = Object.keys(installedData.plugins)
// Load all plugins once. The JSON and human paths both need: // - loadErrors (to show load failures per plugin) // - inline plugins (session-only via --plugin-dir, source='name@inline') // which are NOT in installedData.plugins (V2 bookkeeping) — they must // be surfaced separately or `plugin list` silently ignores --plugin-dir. const { enabled: loadedEnabled, disabled: loadedDisabled, errors: loadErrors, } = await loadAllPlugins() const allLoadedPlugins = [...loadedEnabled, ...loadedDisabled] const inlinePlugins = allLoadedPlugins.filter(p => p.source.endsWith('@inline'), ) // Path-level inline failures (dir doesn't exist, parse error before // manifest is read) use source='inline[N]'. Plugin-level errors after // manifest read use source='name@inline'. Collect both for the session // section — these are otherwise invisible since they have no pluginId. const inlineLoadErrors = loadErrors.filter( e => e.source.endsWith('@inline') || e.source.startsWith('inline['), )
if (options.json) { // Create a map of plugin source to loaded plugin for quick lookup const loadedPluginMap = new Map(allLoadedPlugins.map(p => [p.source, p]))
const plugins: Array<{ id: string version: string scope: string enabled: boolean installPath: string installedAt?: string lastUpdated?: string projectPath?: string mcpServers?: Record<string, unknown> errors?: string[] }> = []
for (const pluginId of pluginIds.sort()) { const installations = installedData.plugins[pluginId] if (!installations || installations.length === 0) continue
// Find loading errors for this plugin const pluginName = parsePluginIdentifier(pluginId).name const pluginErrors = loadErrors .filter( e => e.source === pluginId || ('plugin' in e && e.plugin === pluginName), ) .map(getPluginErrorMessage)
for (const installation of installations) { // Try to find the loaded plugin to get MCP servers const loadedPlugin = loadedPluginMap.get(pluginId) let mcpServers: Record<string, unknown> | undefined
if (loadedPlugin) { // Load MCP servers if not already cached const servers = loadedPlugin.mcpServers || (await loadPluginMcpServers(loadedPlugin)) if (servers && Object.keys(servers).length > 0) { mcpServers = servers } }
plugins.push({ id: pluginId, version: installation.version || 'unknown', scope: installation.scope, enabled: enabledPlugins.has(pluginId), installPath: installation.installPath, installedAt: installation.installedAt, lastUpdated: installation.lastUpdated, projectPath: installation.projectPath, mcpServers, errors: pluginErrors.length > 0 ? pluginErrors : undefined, }) } }
// Session-only plugins: scope='session', no install metadata. // Filter from inlineLoadErrors (not loadErrors) so an installed plugin // with the same manifest name doesn't cross-contaminate via e.plugin. // The e.plugin fallback catches the dirName≠manifestName case: // createPluginFromPath tags errors with `${dirName}@inline` but // plugin.source is reassigned to `${manifest.name}@inline` afterward // (pluginLoader.ts loadInlinePlugins), so e.source !== p.source when // a dev checkout dir like ~/code/my-fork/ has manifest name 'cool-plugin'. for (const p of inlinePlugins) { const servers = p.mcpServers || (await loadPluginMcpServers(p)) const pErrors = inlineLoadErrors .filter( e => e.source === p.source || ('plugin' in e && e.plugin === p.name), ) .map(getPluginErrorMessage) plugins.push({ id: p.source, version: p.manifest.version ?? 'unknown', scope: 'session', enabled: p.enabled !== false, installPath: p.path, mcpServers: servers && Object.keys(servers).length > 0 ? servers : undefined, errors: pErrors.length > 0 ? pErrors : undefined, }) } // Path-level inline failures (--plugin-dir /nonexistent): no LoadedPlugin // exists so the loop above can't surface them. Mirror the human-path // handling so JSON consumers see the failure instead of silent omission. for (const e of inlineLoadErrors.filter(e => e.source.startsWith('inline['), )) { plugins.push({ id: e.source, version: 'unknown', scope: 'session', enabled: false, installPath: 'path' in e ? e.path : '', errors: [getPluginErrorMessage(e)], }) }
// If --available is set, also load available plugins from marketplaces if (options.available) { const available: Array<{ pluginId: string name: string description?: string marketplaceName: string version?: string source: PluginSource installCount?: number }> = []
try { const [config, installCounts] = await Promise.all([ loadKnownMarketplacesConfig(), getInstallCounts(), ]) const { marketplaces } = await loadMarketplacesWithGracefulDegradation(config)
for (const { name: marketplaceName, data: marketplace, } of marketplaces) { if (marketplace) { for (const entry of marketplace.plugins) { const pluginId = createPluginId(entry.name, marketplaceName) // Only include plugins that are not already installed if (!isPluginInstalled(pluginId)) { available.push({ pluginId, name: entry.name, description: entry.description, marketplaceName, version: entry.version, source: entry.source, installCount: installCounts?.get(pluginId), }) } } } } } catch { // Silently ignore marketplace loading errors }
cliOk(jsonStringify({ installed: plugins, available }, null, 2)) } else { cliOk(jsonStringify(plugins, null, 2)) } }
if (pluginIds.length === 0 && inlinePlugins.length === 0) { // inlineLoadErrors can exist with zero inline plugins (e.g. --plugin-dir // points at a nonexistent path). Don't early-exit over them — fall // through to the session section so the failure is visible. if (inlineLoadErrors.length === 0) { cliOk( 'No plugins installed. Use `claude plugin install` to install a plugin.', ) } }
if (pluginIds.length > 0) { // biome-ignore lint/suspicious/noConsole:: intentional console output console.log('Installed plugins:\n') }
for (const pluginId of pluginIds.sort()) { const installations = installedData.plugins[pluginId] if (!installations || installations.length === 0) continue
// Find loading errors for this plugin const pluginName = parsePluginIdentifier(pluginId).name const pluginErrors = loadErrors.filter( e => e.source === pluginId || ('plugin' in e && e.plugin === pluginName), )
for (const installation of installations) { const isEnabled = enabledPlugins.has(pluginId) const status = pluginErrors.length > 0 ? `${figures.cross} failed to load` : isEnabled ? `${figures.tick} enabled` : `${figures.cross} disabled` const version = installation.version || 'unknown' const scope = installation.scope
// biome-ignore lint/suspicious/noConsole:: intentional console output console.log(` ${figures.pointer} ${pluginId}`) // biome-ignore lint/suspicious/noConsole:: intentional console output console.log(` Version: ${version}`) // biome-ignore lint/suspicious/noConsole:: intentional console output console.log(` Scope: ${scope}`) // biome-ignore lint/suspicious/noConsole:: intentional console output console.log(` Status: ${status}`) for (const error of pluginErrors) { // biome-ignore lint/suspicious/noConsole:: intentional console output console.log(` Error: ${getPluginErrorMessage(error)}`) } // biome-ignore lint/suspicious/noConsole:: intentional console output console.log('') } }
if (inlinePlugins.length > 0 || inlineLoadErrors.length > 0) { // biome-ignore lint/suspicious/noConsole:: intentional console output console.log('Session-only plugins (--plugin-dir):\n') for (const p of inlinePlugins) { // Same dirName≠manifestName fallback as the JSON path above — error // sources use the dir basename but p.source uses the manifest name. const pErrors = inlineLoadErrors.filter( e => e.source === p.source || ('plugin' in e && e.plugin === p.name), ) const status = pErrors.length > 0 ? `${figures.cross} loaded with errors` : `${figures.tick} loaded` // biome-ignore lint/suspicious/noConsole:: intentional console output console.log(` ${figures.pointer} ${p.source}`) // biome-ignore lint/suspicious/noConsole:: intentional console output console.log(` Version: ${p.manifest.version ?? 'unknown'}`) // biome-ignore lint/suspicious/noConsole:: intentional console output console.log(` Path: ${p.path}`) // biome-ignore lint/suspicious/noConsole:: intentional console output console.log(` Status: ${status}`) for (const e of pErrors) { // biome-ignore lint/suspicious/noConsole:: intentional console output console.log(` Error: ${getPluginErrorMessage(e)}`) } // biome-ignore lint/suspicious/noConsole:: intentional console output console.log('') } // Path-level failures: no LoadedPlugin object exists. Show them so // `--plugin-dir /typo` doesn't just silently produce nothing. for (const e of inlineLoadErrors.filter(e => e.source.startsWith('inline['), )) { // biome-ignore lint/suspicious/noConsole:: intentional console output console.log( ` ${figures.pointer} ${e.source}: ${figures.cross} ${getPluginErrorMessage(e)}\n`, ) } }
cliOk()}
// marketplace add (lines 5433–5487)export async function marketplaceAddHandler( source: string, options: { cowork?: boolean; sparse?: string[]; scope?: string },): Promise<void> { if (options.cowork) setUseCoworkPlugins(true) try { const parsed = await parseMarketplaceInput(source)
if (!parsed) { cliError( `${figures.cross} Invalid marketplace source format. Try: owner/repo, https://..., or ./path`, ) }
if ('error' in parsed) { cliError(`${figures.cross} ${parsed.error}`) }
// Validate scope const scope = options.scope ?? 'user' if (scope !== 'user' && scope !== 'project' && scope !== 'local') { cliError( `${figures.cross} Invalid scope '${scope}'. Use: user, project, or local`, ) } const settingSource = scopeToSettingSource(scope)
let marketplaceSource = parsed
if (options.sparse && options.sparse.length > 0) { if ( marketplaceSource.source === 'github' || marketplaceSource.source === 'git' ) { marketplaceSource = { ...marketplaceSource, sparsePaths: options.sparse, } } else { cliError( `${figures.cross} --sparse is only supported for github and git marketplace sources (got: ${marketplaceSource.source})`, ) } }
// biome-ignore lint/suspicious/noConsole:: intentional console output console.log('Adding marketplace...')
const { name, alreadyMaterialized, resolvedSource } = await addMarketplaceSource(marketplaceSource, message => { // biome-ignore lint/suspicious/noConsole:: intentional console output console.log(message) })
// Write intent to settings at the requested scope saveMarketplaceToSettings(name, { source: resolvedSource }, settingSource)
clearAllCaches()
let sourceType = marketplaceSource.source if (marketplaceSource.source === 'github') { sourceType = marketplaceSource.repo as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } logEvent('tengu_marketplace_added', { source_type: sourceType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, })
cliOk( alreadyMaterialized ? `${figures.tick} Marketplace '${name}' already on disk — declared in ${scope} settings` : `${figures.tick} Successfully added marketplace: ${name} (declared in ${scope} settings)`, ) } catch (error) { handleMarketplaceError(error, 'add marketplace') }}
// marketplace list (lines 5497–5565)export async function marketplaceListHandler(options: { json?: boolean cowork?: boolean}): Promise<void> { if (options.cowork) setUseCoworkPlugins(true) try { const config = await loadKnownMarketplacesConfig() const names = Object.keys(config)
if (options.json) { const marketplaces = names.sort().map(name => { const marketplace = config[name] const source = marketplace?.source return { name, source: source?.source, ...(source?.source === 'github' && { repo: source.repo }), ...(source?.source === 'git' && { url: source.url }), ...(source?.source === 'url' && { url: source.url }), ...(source?.source === 'directory' && { path: source.path }), ...(source?.source === 'file' && { path: source.path }), installLocation: marketplace?.installLocation, } }) cliOk(jsonStringify(marketplaces, null, 2)) }
if (names.length === 0) { cliOk('No marketplaces configured') }
// biome-ignore lint/suspicious/noConsole:: intentional console output console.log('Configured marketplaces:\n') names.forEach(name => { const marketplace = config[name] // biome-ignore lint/suspicious/noConsole:: intentional console output console.log(` ${figures.pointer} ${name}`)
if (marketplace?.source) { const src = marketplace.source if (src.source === 'github') { // biome-ignore lint/suspicious/noConsole:: intentional console output console.log(` Source: GitHub (${src.repo})`) } else if (src.source === 'git') { // biome-ignore lint/suspicious/noConsole:: intentional console output console.log(` Source: Git (${src.url})`) } else if (src.source === 'url') { // biome-ignore lint/suspicious/noConsole:: intentional console output console.log(` Source: URL (${src.url})`) } else if (src.source === 'directory') { // biome-ignore lint/suspicious/noConsole:: intentional console output console.log(` Source: Directory (${src.path})`) } else if (src.source === 'file') { // biome-ignore lint/suspicious/noConsole:: intentional console output console.log(` Source: File (${src.path})`) } } // biome-ignore lint/suspicious/noConsole:: intentional console output console.log('') })
cliOk() } catch (error) { handleMarketplaceError(error, 'list marketplaces') }}
// marketplace remove (lines 5576–5598)export async function marketplaceRemoveHandler( name: string, options: { cowork?: boolean },): Promise<void> { if (options.cowork) setUseCoworkPlugins(true) try { await removeMarketplaceSource(name) clearAllCaches()
logEvent('tengu_marketplace_removed', { marketplace_name: name as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, })
cliOk(`${figures.tick} Successfully removed marketplace: ${name}`) } catch (error) { handleMarketplaceError(error, 'remove marketplace') }}
// marketplace update (lines 5609–5672)export async function marketplaceUpdateHandler( name: string | undefined, options: { cowork?: boolean },): Promise<void> { if (options.cowork) setUseCoworkPlugins(true) try { if (name) { // biome-ignore lint/suspicious/noConsole:: intentional console output console.log(`Updating marketplace: ${name}...`)
await refreshMarketplace(name, message => { // biome-ignore lint/suspicious/noConsole:: intentional console output console.log(message) })
clearAllCaches()
logEvent('tengu_marketplace_updated', { marketplace_name: name as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, })
cliOk(`${figures.tick} Successfully updated marketplace: ${name}`) } else { const config = await loadKnownMarketplacesConfig() const marketplaceNames = Object.keys(config)
if (marketplaceNames.length === 0) { cliOk('No marketplaces configured') }
// biome-ignore lint/suspicious/noConsole:: intentional console output console.log(`Updating ${marketplaceNames.length} marketplace(s)...`)
await refreshAllMarketplaces() clearAllCaches()
logEvent('tengu_marketplace_updated_all', { count: marketplaceNames.length as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, })
cliOk( `${figures.tick} Successfully updated ${marketplaceNames.length} marketplace(s)`, ) } } catch (error) { handleMarketplaceError(error, 'update marketplace(s)') }}
// plugin install (lines 5690–5721)export async function pluginInstallHandler( plugin: string, options: { scope?: string; cowork?: boolean },): Promise<void> { if (options.cowork) setUseCoworkPlugins(true) const scope = options.scope || 'user' if (options.cowork && scope !== 'user') { cliError('--cowork can only be used with user scope') } if ( !VALID_INSTALLABLE_SCOPES.includes( scope as (typeof VALID_INSTALLABLE_SCOPES)[number], ) ) { cliError( `Invalid scope: ${scope}. Must be one of: ${VALID_INSTALLABLE_SCOPES.join(', ')}.`, ) } // _PROTO_* routes to PII-tagged plugin_name/marketplace_name BQ columns. // Unredacted plugin arg was previously logged to general-access // additional_metadata for all users — dropped in favor of the privileged // column route. marketplace may be undefined (fires before resolution). const { name, marketplace } = parsePluginIdentifier(plugin) logEvent('tengu_plugin_install_command', { _PROTO_plugin_name: name as AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED, ...(marketplace && { _PROTO_marketplace_name: marketplace as AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED, }), scope: scope as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, })
await installPlugin(plugin, scope as 'user' | 'project' | 'local')}
// plugin uninstall (lines 5738–5769)export async function pluginUninstallHandler( plugin: string, options: { scope?: string; cowork?: boolean; keepData?: boolean },): Promise<void> { if (options.cowork) setUseCoworkPlugins(true) const scope = options.scope || 'user' if (options.cowork && scope !== 'user') { cliError('--cowork can only be used with user scope') } if ( !VALID_INSTALLABLE_SCOPES.includes( scope as (typeof VALID_INSTALLABLE_SCOPES)[number], ) ) { cliError( `Invalid scope: ${scope}. Must be one of: ${VALID_INSTALLABLE_SCOPES.join(', ')}.`, ) } const { name, marketplace } = parsePluginIdentifier(plugin) logEvent('tengu_plugin_uninstall_command', { _PROTO_plugin_name: name as AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED, ...(marketplace && { _PROTO_marketplace_name: marketplace as AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED, }), scope: scope as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, })
await uninstallPlugin( plugin, scope as 'user' | 'project' | 'local', options.keepData, )}
// plugin enable (lines 5783–5818)export async function pluginEnableHandler( plugin: string, options: { scope?: string; cowork?: boolean },): Promise<void> { if (options.cowork) setUseCoworkPlugins(true) let scope: (typeof VALID_INSTALLABLE_SCOPES)[number] | undefined if (options.scope) { if ( !VALID_INSTALLABLE_SCOPES.includes( options.scope as (typeof VALID_INSTALLABLE_SCOPES)[number], ) ) { cliError( `Invalid scope "${options.scope}". Valid scopes: ${VALID_INSTALLABLE_SCOPES.join(', ')}`, ) } scope = options.scope as (typeof VALID_INSTALLABLE_SCOPES)[number] } if (options.cowork && scope !== undefined && scope !== 'user') { cliError('--cowork can only be used with user scope') }
// --cowork always operates at user scope if (options.cowork && scope === undefined) { scope = 'user' }
const { name, marketplace } = parsePluginIdentifier(plugin) logEvent('tengu_plugin_enable_command', { _PROTO_plugin_name: name as AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED, ...(marketplace && { _PROTO_marketplace_name: marketplace as AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED, }), scope: (scope ?? 'auto') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, })
await enablePlugin(plugin, scope)}
// plugin disable (lines 5833–5902)export async function pluginDisableHandler( plugin: string | undefined, options: { scope?: string; cowork?: boolean; all?: boolean },): Promise<void> { if (options.all && plugin) { cliError('Cannot use --all with a specific plugin') }
if (!options.all && !plugin) { cliError('Please specify a plugin name or use --all to disable all plugins') }
if (options.cowork) setUseCoworkPlugins(true)
if (options.all) { if (options.scope) { cliError('Cannot use --scope with --all') }
// No _PROTO_plugin_name here — --all disables all plugins. // Distinguishable from the specific-plugin branch by plugin_name IS NULL. logEvent('tengu_plugin_disable_command', {})
await disableAllPlugins() return }
let scope: (typeof VALID_INSTALLABLE_SCOPES)[number] | undefined if (options.scope) { if ( !VALID_INSTALLABLE_SCOPES.includes( options.scope as (typeof VALID_INSTALLABLE_SCOPES)[number], ) ) { cliError( `Invalid scope "${options.scope}". Valid scopes: ${VALID_INSTALLABLE_SCOPES.join(', ')}`, ) } scope = options.scope as (typeof VALID_INSTALLABLE_SCOPES)[number] } if (options.cowork && scope !== undefined && scope !== 'user') { cliError('--cowork can only be used with user scope') }
// --cowork always operates at user scope if (options.cowork && scope === undefined) { scope = 'user' }
const { name, marketplace } = parsePluginIdentifier(plugin!) logEvent('tengu_plugin_disable_command', { _PROTO_plugin_name: name as AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED, ...(marketplace && { _PROTO_marketplace_name: marketplace as AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED, }), scope: (scope ?? 'auto') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, })
await disablePlugin(plugin!, scope)}
// plugin update (lines 5918–5948)export async function pluginUpdateHandler( plugin: string, options: { scope?: string; cowork?: boolean },): Promise<void> { if (options.cowork) setUseCoworkPlugins(true) const { name, marketplace } = parsePluginIdentifier(plugin) logEvent('tengu_plugin_update_command', { _PROTO_plugin_name: name as AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED, ...(marketplace && { _PROTO_marketplace_name: marketplace as AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED, }), })
let scope: (typeof VALID_UPDATE_SCOPES)[number] = 'user' if (options.scope) { if ( !VALID_UPDATE_SCOPES.includes( options.scope as (typeof VALID_UPDATE_SCOPES)[number], ) ) { cliError( `Invalid scope "${options.scope}". Valid scopes: ${VALID_UPDATE_SCOPES.join(', ')}`, ) } scope = options.scope as (typeof VALID_UPDATE_SCOPES)[number] } if (options.cowork && scope !== 'user') { cliError('--cowork can only be used with user scope') }
await updatePluginCli(plugin, scope)}