364 lines
11 KiB
TypeScript
364 lines
11 KiB
TypeScript
import type { LspServerConfig } from '../services/lsp/types.js'
|
|
import type { McpServerConfig } from '../services/mcp/types.js'
|
|
import type { BundledSkillDefinition } from '../skills/bundledSkills.js'
|
|
import type {
|
|
CommandMetadata,
|
|
PluginAuthor,
|
|
PluginManifest,
|
|
} from '../utils/plugins/schemas.js'
|
|
import type { HooksSettings } from '../utils/settings/types.js'
|
|
|
|
export type { PluginAuthor, PluginManifest, CommandMetadata }
|
|
|
|
/**
|
|
* Definition for a built-in plugin that ships with the CLI.
|
|
* Built-in plugins appear in the /plugin UI and can be enabled/disabled by
|
|
* users (persisted to user settings).
|
|
*/
|
|
export type BuiltinPluginDefinition = {
|
|
/** Plugin name (used in `{name}@builtin` identifier) */
|
|
name: string
|
|
/** Description shown in the /plugin UI */
|
|
description: string
|
|
/** Optional version string */
|
|
version?: string
|
|
/** Skills provided by this plugin */
|
|
skills?: BundledSkillDefinition[]
|
|
/** Hooks provided by this plugin */
|
|
hooks?: HooksSettings
|
|
/** MCP servers provided by this plugin */
|
|
mcpServers?: Record<string, McpServerConfig>
|
|
/** Whether this plugin is available (e.g. based on system capabilities). Unavailable plugins are hidden entirely. */
|
|
isAvailable?: () => boolean
|
|
/** Default enabled state before the user sets a preference (defaults to true) */
|
|
defaultEnabled?: boolean
|
|
}
|
|
|
|
export type PluginRepository = {
|
|
url: string
|
|
branch: string
|
|
lastUpdated?: string
|
|
commitSha?: string
|
|
}
|
|
|
|
export type PluginConfig = {
|
|
repositories: Record<string, PluginRepository>
|
|
}
|
|
|
|
export type LoadedPlugin = {
|
|
name: string
|
|
manifest: PluginManifest
|
|
path: string
|
|
source: string
|
|
repository: string // Repository identifier, usually same as source
|
|
enabled?: boolean
|
|
isBuiltin?: boolean // true for built-in plugins that ship with the CLI
|
|
sha?: string // Git commit SHA for version pinning (from marketplace entry source)
|
|
commandsPath?: string
|
|
commandsPaths?: string[] // Additional command paths from manifest
|
|
commandsMetadata?: Record<string, CommandMetadata> // Metadata for named commands from object-mapping format
|
|
agentsPath?: string
|
|
agentsPaths?: string[] // Additional agent paths from manifest
|
|
skillsPath?: string
|
|
skillsPaths?: string[] // Additional skill paths from manifest
|
|
outputStylesPath?: string
|
|
outputStylesPaths?: string[] // Additional output style paths from manifest
|
|
hooksConfig?: HooksSettings
|
|
mcpServers?: Record<string, McpServerConfig>
|
|
lspServers?: Record<string, LspServerConfig>
|
|
settings?: Record<string, unknown>
|
|
}
|
|
|
|
export type PluginComponent =
|
|
| 'commands'
|
|
| 'agents'
|
|
| 'skills'
|
|
| 'hooks'
|
|
| 'output-styles'
|
|
|
|
/**
|
|
* Discriminated union of plugin error types.
|
|
* Each error type has specific contextual data for better debugging and user guidance.
|
|
*
|
|
* This replaces the previous string-based error matching approach with type-safe
|
|
* error handling that can't break when error messages change.
|
|
*
|
|
* IMPLEMENTATION STATUS:
|
|
* Currently used in production (2 types):
|
|
* - generic-error: Used for various plugin loading failures
|
|
* - plugin-not-found: Used when plugin not found in marketplace
|
|
*
|
|
* Planned for future use (10 types - see TODOs in pluginLoader.ts):
|
|
* - path-not-found, git-auth-failed, git-timeout, network-error
|
|
* - manifest-parse-error, manifest-validation-error
|
|
* - marketplace-not-found, marketplace-load-failed
|
|
* - mcp-config-invalid, hook-load-failed, component-load-failed
|
|
*
|
|
* These unused types support UI formatting and provide a clear roadmap for
|
|
* improving error specificity. They can be incrementally implemented as
|
|
* error creation sites are refactored.
|
|
*/
|
|
export type PluginError =
|
|
| {
|
|
type: 'path-not-found'
|
|
source: string
|
|
plugin?: string
|
|
path: string
|
|
component: PluginComponent
|
|
}
|
|
| {
|
|
type: 'git-auth-failed'
|
|
source: string
|
|
plugin?: string
|
|
gitUrl: string
|
|
authType: 'ssh' | 'https'
|
|
}
|
|
| {
|
|
type: 'git-timeout'
|
|
source: string
|
|
plugin?: string
|
|
gitUrl: string
|
|
operation: 'clone' | 'pull'
|
|
}
|
|
| {
|
|
type: 'network-error'
|
|
source: string
|
|
plugin?: string
|
|
url: string
|
|
details?: string
|
|
}
|
|
| {
|
|
type: 'manifest-parse-error'
|
|
source: string
|
|
plugin?: string
|
|
manifestPath: string
|
|
parseError: string
|
|
}
|
|
| {
|
|
type: 'manifest-validation-error'
|
|
source: string
|
|
plugin?: string
|
|
manifestPath: string
|
|
validationErrors: string[]
|
|
}
|
|
| {
|
|
type: 'plugin-not-found'
|
|
source: string
|
|
pluginId: string
|
|
marketplace: string
|
|
}
|
|
| {
|
|
type: 'marketplace-not-found'
|
|
source: string
|
|
marketplace: string
|
|
availableMarketplaces: string[]
|
|
}
|
|
| {
|
|
type: 'marketplace-load-failed'
|
|
source: string
|
|
marketplace: string
|
|
reason: string
|
|
}
|
|
| {
|
|
type: 'mcp-config-invalid'
|
|
source: string
|
|
plugin: string
|
|
serverName: string
|
|
validationError: string
|
|
}
|
|
| {
|
|
type: 'mcp-server-suppressed-duplicate'
|
|
source: string
|
|
plugin: string
|
|
serverName: string
|
|
duplicateOf: string
|
|
}
|
|
| {
|
|
type: 'lsp-config-invalid'
|
|
source: string
|
|
plugin: string
|
|
serverName: string
|
|
validationError: string
|
|
}
|
|
| {
|
|
type: 'hook-load-failed'
|
|
source: string
|
|
plugin: string
|
|
hookPath: string
|
|
reason: string
|
|
}
|
|
| {
|
|
type: 'component-load-failed'
|
|
source: string
|
|
plugin: string
|
|
component: PluginComponent
|
|
path: string
|
|
reason: string
|
|
}
|
|
| {
|
|
type: 'mcpb-download-failed'
|
|
source: string
|
|
plugin: string
|
|
url: string
|
|
reason: string
|
|
}
|
|
| {
|
|
type: 'mcpb-extract-failed'
|
|
source: string
|
|
plugin: string
|
|
mcpbPath: string
|
|
reason: string
|
|
}
|
|
| {
|
|
type: 'mcpb-invalid-manifest'
|
|
source: string
|
|
plugin: string
|
|
mcpbPath: string
|
|
validationError: string
|
|
}
|
|
| {
|
|
type: 'lsp-config-invalid'
|
|
source: string
|
|
plugin: string
|
|
serverName: string
|
|
validationError: string
|
|
}
|
|
| {
|
|
type: 'lsp-server-start-failed'
|
|
source: string
|
|
plugin: string
|
|
serverName: string
|
|
reason: string
|
|
}
|
|
| {
|
|
type: 'lsp-server-crashed'
|
|
source: string
|
|
plugin: string
|
|
serverName: string
|
|
exitCode: number | null
|
|
signal?: string
|
|
}
|
|
| {
|
|
type: 'lsp-request-timeout'
|
|
source: string
|
|
plugin: string
|
|
serverName: string
|
|
method: string
|
|
timeoutMs: number
|
|
}
|
|
| {
|
|
type: 'lsp-request-failed'
|
|
source: string
|
|
plugin: string
|
|
serverName: string
|
|
method: string
|
|
error: string
|
|
}
|
|
| {
|
|
type: 'marketplace-blocked-by-policy'
|
|
source: string
|
|
plugin?: string
|
|
marketplace: string
|
|
blockedByBlocklist?: boolean // true if blocked by blockedMarketplaces, false if not in strictKnownMarketplaces
|
|
allowedSources: string[] // Formatted source strings (e.g., "github:owner/repo")
|
|
}
|
|
| {
|
|
type: 'dependency-unsatisfied'
|
|
source: string
|
|
plugin: string
|
|
dependency: string
|
|
reason: 'not-enabled' | 'not-found'
|
|
}
|
|
| {
|
|
type: 'plugin-cache-miss'
|
|
source: string
|
|
plugin: string
|
|
installPath: string
|
|
}
|
|
| {
|
|
type: 'generic-error'
|
|
source: string
|
|
plugin?: string
|
|
error: string
|
|
}
|
|
|
|
export type PluginLoadResult = {
|
|
enabled: LoadedPlugin[]
|
|
disabled: LoadedPlugin[]
|
|
errors: PluginError[]
|
|
}
|
|
|
|
/**
|
|
* Helper function to get a display message from any PluginError
|
|
* Useful for logging and simple error displays
|
|
*/
|
|
export function getPluginErrorMessage(error: PluginError): string {
|
|
switch (error.type) {
|
|
case 'generic-error':
|
|
return error.error
|
|
case 'path-not-found':
|
|
return `Path not found: ${error.path} (${error.component})`
|
|
case 'git-auth-failed':
|
|
return `Git authentication failed (${error.authType}): ${error.gitUrl}`
|
|
case 'git-timeout':
|
|
return `Git ${error.operation} timeout: ${error.gitUrl}`
|
|
case 'network-error':
|
|
return `Network error: ${error.url}${error.details ? ` - ${error.details}` : ''}`
|
|
case 'manifest-parse-error':
|
|
return `Manifest parse error: ${error.parseError}`
|
|
case 'manifest-validation-error':
|
|
return `Manifest validation failed: ${error.validationErrors.join(', ')}`
|
|
case 'plugin-not-found':
|
|
return `Plugin ${error.pluginId} not found in marketplace ${error.marketplace}`
|
|
case 'marketplace-not-found':
|
|
return `Marketplace ${error.marketplace} not found`
|
|
case 'marketplace-load-failed':
|
|
return `Marketplace ${error.marketplace} failed to load: ${error.reason}`
|
|
case 'mcp-config-invalid':
|
|
return `MCP server ${error.serverName} invalid: ${error.validationError}`
|
|
case 'mcp-server-suppressed-duplicate': {
|
|
const dup = error.duplicateOf.startsWith('plugin:')
|
|
? `server provided by plugin "${error.duplicateOf.split(':')[1] ?? '?'}"`
|
|
: `already-configured "${error.duplicateOf}"`
|
|
return `MCP server "${error.serverName}" skipped — same command/URL as ${dup}`
|
|
}
|
|
case 'hook-load-failed':
|
|
return `Hook load failed: ${error.reason}`
|
|
case 'component-load-failed':
|
|
return `${error.component} load failed from ${error.path}: ${error.reason}`
|
|
case 'mcpb-download-failed':
|
|
return `Failed to download MCPB from ${error.url}: ${error.reason}`
|
|
case 'mcpb-extract-failed':
|
|
return `Failed to extract MCPB ${error.mcpbPath}: ${error.reason}`
|
|
case 'mcpb-invalid-manifest':
|
|
return `MCPB manifest invalid at ${error.mcpbPath}: ${error.validationError}`
|
|
case 'lsp-config-invalid':
|
|
return `Plugin "${error.plugin}" has invalid LSP server config for "${error.serverName}": ${error.validationError}`
|
|
case 'lsp-server-start-failed':
|
|
return `Plugin "${error.plugin}" failed to start LSP server "${error.serverName}": ${error.reason}`
|
|
case 'lsp-server-crashed':
|
|
if (error.signal) {
|
|
return `Plugin "${error.plugin}" LSP server "${error.serverName}" crashed with signal ${error.signal}`
|
|
}
|
|
return `Plugin "${error.plugin}" LSP server "${error.serverName}" crashed with exit code ${error.exitCode ?? 'unknown'}`
|
|
case 'lsp-request-timeout':
|
|
return `Plugin "${error.plugin}" LSP server "${error.serverName}" timed out on ${error.method} request after ${error.timeoutMs}ms`
|
|
case 'lsp-request-failed':
|
|
return `Plugin "${error.plugin}" LSP server "${error.serverName}" ${error.method} request failed: ${error.error}`
|
|
case 'marketplace-blocked-by-policy':
|
|
if (error.blockedByBlocklist) {
|
|
return `Marketplace '${error.marketplace}' is blocked by enterprise policy`
|
|
}
|
|
return `Marketplace '${error.marketplace}' is not in the allowed marketplace list`
|
|
case 'dependency-unsatisfied': {
|
|
const hint =
|
|
error.reason === 'not-enabled'
|
|
? 'disabled — enable it or remove the dependency'
|
|
: 'not found in any configured marketplace'
|
|
return `Dependency "${error.dependency}" is ${hint}`
|
|
}
|
|
case 'plugin-cache-miss':
|
|
return `Plugin "${error.plugin}" not cached at ${error.installPath} — run /plugins to refresh`
|
|
}
|
|
}
|