source dump of claude code
at main 344 lines 11 kB view raw
1/** 2 * CLI command wrappers for plugin operations 3 * 4 * This module provides thin wrappers around the core plugin operations 5 * that handle CLI-specific concerns like console output and process exit. 6 * 7 * For the core operations (without CLI side effects), see pluginOperations.ts 8 */ 9import figures from 'figures' 10import { errorMessage } from '../../utils/errors.js' 11import { gracefulShutdown } from '../../utils/gracefulShutdown.js' 12import { logError } from '../../utils/log.js' 13import { getManagedPluginNames } from '../../utils/plugins/managedPlugins.js' 14import { parsePluginIdentifier } from '../../utils/plugins/pluginIdentifier.js' 15import type { PluginScope } from '../../utils/plugins/schemas.js' 16import { writeToStdout } from '../../utils/process.js' 17import { 18 buildPluginTelemetryFields, 19 classifyPluginCommandError, 20} from '../../utils/telemetry/pluginTelemetry.js' 21import { 22 type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 23 type AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED, 24 logEvent, 25} from '../analytics/index.js' 26import { 27 disableAllPluginsOp, 28 disablePluginOp, 29 enablePluginOp, 30 type InstallableScope, 31 installPluginOp, 32 uninstallPluginOp, 33 updatePluginOp, 34 VALID_INSTALLABLE_SCOPES, 35 VALID_UPDATE_SCOPES, 36} from './pluginOperations.js' 37 38export { VALID_INSTALLABLE_SCOPES, VALID_UPDATE_SCOPES } 39 40type PluginCliCommand = 41 | 'install' 42 | 'uninstall' 43 | 'enable' 44 | 'disable' 45 | 'disable-all' 46 | 'update' 47 48/** 49 * Generic error handler for plugin CLI commands. Emits 50 * tengu_plugin_command_failed before exit so dashboards can compute a 51 * success rate against the corresponding success events. 52 */ 53function handlePluginCommandError( 54 error: unknown, 55 command: PluginCliCommand, 56 plugin?: string, 57): never { 58 logError(error) 59 const operation = plugin 60 ? `${command} plugin "${plugin}"` 61 : command === 'disable-all' 62 ? 'disable all plugins' 63 : `${command} plugins` 64 // biome-ignore lint/suspicious/noConsole:: intentional console output 65 console.error( 66 `${figures.cross} Failed to ${operation}: ${errorMessage(error)}`, 67 ) 68 const telemetryFields = plugin 69 ? (() => { 70 const { name, marketplace } = parsePluginIdentifier(plugin) 71 return { 72 _PROTO_plugin_name: 73 name as AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED, 74 ...(marketplace && { 75 _PROTO_marketplace_name: 76 marketplace as AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED, 77 }), 78 ...buildPluginTelemetryFields( 79 name, 80 marketplace, 81 getManagedPluginNames(), 82 ), 83 } 84 })() 85 : {} 86 logEvent('tengu_plugin_command_failed', { 87 command: 88 command as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 89 error_category: classifyPluginCommandError( 90 error, 91 ) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 92 ...telemetryFields, 93 }) 94 // eslint-disable-next-line custom-rules/no-process-exit 95 process.exit(1) 96} 97 98/** 99 * CLI command: Install a plugin non-interactively 100 * @param plugin Plugin identifier (name or plugin@marketplace) 101 * @param scope Installation scope: user, project, or local (defaults to 'user') 102 */ 103export async function installPlugin( 104 plugin: string, 105 scope: InstallableScope = 'user', 106): Promise<void> { 107 try { 108 // biome-ignore lint/suspicious/noConsole:: intentional console output 109 console.log(`Installing plugin "${plugin}"...`) 110 111 const result = await installPluginOp(plugin, scope) 112 113 if (!result.success) { 114 throw new Error(result.message) 115 } 116 117 // biome-ignore lint/suspicious/noConsole:: intentional console output 118 console.log(`${figures.tick} ${result.message}`) 119 120 // _PROTO_* routes to PII-tagged plugin_name/marketplace_name BQ columns. 121 // Unredacted plugin_id was previously logged to general-access 122 // additional_metadata for all users — dropped in favor of the privileged 123 // column route. 124 const { name, marketplace } = parsePluginIdentifier( 125 result.pluginId || plugin, 126 ) 127 logEvent('tengu_plugin_installed_cli', { 128 _PROTO_plugin_name: 129 name as AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED, 130 ...(marketplace && { 131 _PROTO_marketplace_name: 132 marketplace as AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED, 133 }), 134 scope: (result.scope || 135 scope) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 136 install_source: 137 'cli-explicit' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 138 ...buildPluginTelemetryFields(name, marketplace, getManagedPluginNames()), 139 }) 140 141 // eslint-disable-next-line custom-rules/no-process-exit 142 process.exit(0) 143 } catch (error) { 144 handlePluginCommandError(error, 'install', plugin) 145 } 146} 147 148/** 149 * CLI command: Uninstall a plugin non-interactively 150 * @param plugin Plugin name or plugin@marketplace identifier 151 * @param scope Uninstall from scope: user, project, or local (defaults to 'user') 152 */ 153export async function uninstallPlugin( 154 plugin: string, 155 scope: InstallableScope = 'user', 156 keepData = false, 157): Promise<void> { 158 try { 159 const result = await uninstallPluginOp(plugin, scope, !keepData) 160 161 if (!result.success) { 162 throw new Error(result.message) 163 } 164 165 // biome-ignore lint/suspicious/noConsole:: intentional console output 166 console.log(`${figures.tick} ${result.message}`) 167 168 const { name, marketplace } = parsePluginIdentifier( 169 result.pluginId || plugin, 170 ) 171 logEvent('tengu_plugin_uninstalled_cli', { 172 _PROTO_plugin_name: 173 name as AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED, 174 ...(marketplace && { 175 _PROTO_marketplace_name: 176 marketplace as AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED, 177 }), 178 scope: (result.scope || 179 scope) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 180 ...buildPluginTelemetryFields(name, marketplace, getManagedPluginNames()), 181 }) 182 183 // eslint-disable-next-line custom-rules/no-process-exit 184 process.exit(0) 185 } catch (error) { 186 handlePluginCommandError(error, 'uninstall', plugin) 187 } 188} 189 190/** 191 * CLI command: Enable a plugin non-interactively 192 * @param plugin Plugin name or plugin@marketplace identifier 193 * @param scope Optional scope. If not provided, finds the most specific scope for the current project. 194 */ 195export async function enablePlugin( 196 plugin: string, 197 scope?: InstallableScope, 198): Promise<void> { 199 try { 200 const result = await enablePluginOp(plugin, scope) 201 202 if (!result.success) { 203 throw new Error(result.message) 204 } 205 206 // biome-ignore lint/suspicious/noConsole:: intentional console output 207 console.log(`${figures.tick} ${result.message}`) 208 209 const { name, marketplace } = parsePluginIdentifier( 210 result.pluginId || plugin, 211 ) 212 logEvent('tengu_plugin_enabled_cli', { 213 _PROTO_plugin_name: 214 name as AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED, 215 ...(marketplace && { 216 _PROTO_marketplace_name: 217 marketplace as AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED, 218 }), 219 scope: 220 result.scope as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 221 ...buildPluginTelemetryFields(name, marketplace, getManagedPluginNames()), 222 }) 223 224 // eslint-disable-next-line custom-rules/no-process-exit 225 process.exit(0) 226 } catch (error) { 227 handlePluginCommandError(error, 'enable', plugin) 228 } 229} 230 231/** 232 * CLI command: Disable a plugin non-interactively 233 * @param plugin Plugin name or plugin@marketplace identifier 234 * @param scope Optional scope. If not provided, finds the most specific scope for the current project. 235 */ 236export async function disablePlugin( 237 plugin: string, 238 scope?: InstallableScope, 239): Promise<void> { 240 try { 241 const result = await disablePluginOp(plugin, scope) 242 243 if (!result.success) { 244 throw new Error(result.message) 245 } 246 247 // biome-ignore lint/suspicious/noConsole:: intentional console output 248 console.log(`${figures.tick} ${result.message}`) 249 250 const { name, marketplace } = parsePluginIdentifier( 251 result.pluginId || plugin, 252 ) 253 logEvent('tengu_plugin_disabled_cli', { 254 _PROTO_plugin_name: 255 name as AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED, 256 ...(marketplace && { 257 _PROTO_marketplace_name: 258 marketplace as AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED, 259 }), 260 scope: 261 result.scope as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 262 ...buildPluginTelemetryFields(name, marketplace, getManagedPluginNames()), 263 }) 264 265 // eslint-disable-next-line custom-rules/no-process-exit 266 process.exit(0) 267 } catch (error) { 268 handlePluginCommandError(error, 'disable', plugin) 269 } 270} 271 272/** 273 * CLI command: Disable all enabled plugins non-interactively 274 */ 275export async function disableAllPlugins(): Promise<void> { 276 try { 277 const result = await disableAllPluginsOp() 278 279 if (!result.success) { 280 throw new Error(result.message) 281 } 282 283 // biome-ignore lint/suspicious/noConsole:: intentional console output 284 console.log(`${figures.tick} ${result.message}`) 285 286 logEvent('tengu_plugin_disabled_all_cli', {}) 287 288 // eslint-disable-next-line custom-rules/no-process-exit 289 process.exit(0) 290 } catch (error) { 291 handlePluginCommandError(error, 'disable-all') 292 } 293} 294 295/** 296 * CLI command: Update a plugin non-interactively 297 * @param plugin Plugin name or plugin@marketplace identifier 298 * @param scope Scope to update 299 */ 300export async function updatePluginCli( 301 plugin: string, 302 scope: PluginScope, 303): Promise<void> { 304 try { 305 writeToStdout( 306 `Checking for updates for plugin "${plugin}" at ${scope} scope…\n`, 307 ) 308 309 const result = await updatePluginOp(plugin, scope) 310 311 if (!result.success) { 312 throw new Error(result.message) 313 } 314 315 writeToStdout(`${figures.tick} ${result.message}\n`) 316 317 if (!result.alreadyUpToDate) { 318 const { name, marketplace } = parsePluginIdentifier( 319 result.pluginId || plugin, 320 ) 321 logEvent('tengu_plugin_updated_cli', { 322 _PROTO_plugin_name: 323 name as AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED, 324 ...(marketplace && { 325 _PROTO_marketplace_name: 326 marketplace as AnalyticsMetadata_I_VERIFIED_THIS_IS_PII_TAGGED, 327 }), 328 old_version: (result.oldVersion || 329 'unknown') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 330 new_version: (result.newVersion || 331 'unknown') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 332 ...buildPluginTelemetryFields( 333 name, 334 marketplace, 335 getManagedPluginNames(), 336 ), 337 }) 338 } 339 340 await gracefulShutdown(0) 341 } catch (error) { 342 handlePluginCommandError(error, 'update', plugin) 343 } 344}