/ types / plugin.ts
plugin.ts
  1  import type { LspServerConfig } from '../services/lsp/types.js'
  2  import type { McpServerConfig } from '../services/mcp/types.js'
  3  import type { BundledSkillDefinition } from '../skills/bundledSkills.js'
  4  import type {
  5    CommandMetadata,
  6    PluginAuthor,
  7    PluginManifest,
  8  } from '../utils/plugins/schemas.js'
  9  import type { HooksSettings } from '../utils/settings/types.js'
 10  
 11  export type { PluginAuthor, PluginManifest, CommandMetadata }
 12  
 13  /**
 14   * Definition for a built-in plugin that ships with the CLI.
 15   * Built-in plugins appear in the /plugin UI and can be enabled/disabled by
 16   * users (persisted to user settings).
 17   */
 18  export type BuiltinPluginDefinition = {
 19    /** Plugin name (used in `{name}@builtin` identifier) */
 20    name: string
 21    /** Description shown in the /plugin UI */
 22    description: string
 23    /** Optional version string */
 24    version?: string
 25    /** Skills provided by this plugin */
 26    skills?: BundledSkillDefinition[]
 27    /** Hooks provided by this plugin */
 28    hooks?: HooksSettings
 29    /** MCP servers provided by this plugin */
 30    mcpServers?: Record<string, McpServerConfig>
 31    /** Whether this plugin is available (e.g. based on system capabilities). Unavailable plugins are hidden entirely. */
 32    isAvailable?: () => boolean
 33    /** Default enabled state before the user sets a preference (defaults to true) */
 34    defaultEnabled?: boolean
 35  }
 36  
 37  export type PluginRepository = {
 38    url: string
 39    branch: string
 40    lastUpdated?: string
 41    commitSha?: string
 42  }
 43  
 44  export type PluginConfig = {
 45    repositories: Record<string, PluginRepository>
 46  }
 47  
 48  export type LoadedPlugin = {
 49    name: string
 50    manifest: PluginManifest
 51    path: string
 52    source: string
 53    repository: string // Repository identifier, usually same as source
 54    enabled?: boolean
 55    isBuiltin?: boolean // true for built-in plugins that ship with the CLI
 56    sha?: string // Git commit SHA for version pinning (from marketplace entry source)
 57    commandsPath?: string
 58    commandsPaths?: string[] // Additional command paths from manifest
 59    commandsMetadata?: Record<string, CommandMetadata> // Metadata for named commands from object-mapping format
 60    agentsPath?: string
 61    agentsPaths?: string[] // Additional agent paths from manifest
 62    skillsPath?: string
 63    skillsPaths?: string[] // Additional skill paths from manifest
 64    outputStylesPath?: string
 65    outputStylesPaths?: string[] // Additional output style paths from manifest
 66    hooksConfig?: HooksSettings
 67    mcpServers?: Record<string, McpServerConfig>
 68    lspServers?: Record<string, LspServerConfig>
 69    settings?: Record<string, unknown>
 70  }
 71  
 72  export type PluginComponent =
 73    | 'commands'
 74    | 'agents'
 75    | 'skills'
 76    | 'hooks'
 77    | 'output-styles'
 78  
 79  /**
 80   * Discriminated union of plugin error types.
 81   * Each error type has specific contextual data for better debugging and user guidance.
 82   *
 83   * This replaces the previous string-based error matching approach with type-safe
 84   * error handling that can't break when error messages change.
 85   *
 86   * IMPLEMENTATION STATUS:
 87   * Currently used in production (2 types):
 88   * - generic-error: Used for various plugin loading failures
 89   * - plugin-not-found: Used when plugin not found in marketplace
 90   *
 91   * Planned for future use (10 types - see TODOs in pluginLoader.ts):
 92   * - path-not-found, git-auth-failed, git-timeout, network-error
 93   * - manifest-parse-error, manifest-validation-error
 94   * - marketplace-not-found, marketplace-load-failed
 95   * - mcp-config-invalid, hook-load-failed, component-load-failed
 96   *
 97   * These unused types support UI formatting and provide a clear roadmap for
 98   * improving error specificity. They can be incrementally implemented as
 99   * error creation sites are refactored.
100   */
101  export type PluginError =
102    | {
103        type: 'path-not-found'
104        source: string
105        plugin?: string
106        path: string
107        component: PluginComponent
108      }
109    | {
110        type: 'git-auth-failed'
111        source: string
112        plugin?: string
113        gitUrl: string
114        authType: 'ssh' | 'https'
115      }
116    | {
117        type: 'git-timeout'
118        source: string
119        plugin?: string
120        gitUrl: string
121        operation: 'clone' | 'pull'
122      }
123    | {
124        type: 'network-error'
125        source: string
126        plugin?: string
127        url: string
128        details?: string
129      }
130    | {
131        type: 'manifest-parse-error'
132        source: string
133        plugin?: string
134        manifestPath: string
135        parseError: string
136      }
137    | {
138        type: 'manifest-validation-error'
139        source: string
140        plugin?: string
141        manifestPath: string
142        validationErrors: string[]
143      }
144    | {
145        type: 'plugin-not-found'
146        source: string
147        pluginId: string
148        marketplace: string
149      }
150    | {
151        type: 'marketplace-not-found'
152        source: string
153        marketplace: string
154        availableMarketplaces: string[]
155      }
156    | {
157        type: 'marketplace-load-failed'
158        source: string
159        marketplace: string
160        reason: string
161      }
162    | {
163        type: 'mcp-config-invalid'
164        source: string
165        plugin: string
166        serverName: string
167        validationError: string
168      }
169    | {
170        type: 'mcp-server-suppressed-duplicate'
171        source: string
172        plugin: string
173        serverName: string
174        duplicateOf: string
175      }
176    | {
177        type: 'lsp-config-invalid'
178        source: string
179        plugin: string
180        serverName: string
181        validationError: string
182      }
183    | {
184        type: 'hook-load-failed'
185        source: string
186        plugin: string
187        hookPath: string
188        reason: string
189      }
190    | {
191        type: 'component-load-failed'
192        source: string
193        plugin: string
194        component: PluginComponent
195        path: string
196        reason: string
197      }
198    | {
199        type: 'mcpb-download-failed'
200        source: string
201        plugin: string
202        url: string
203        reason: string
204      }
205    | {
206        type: 'mcpb-extract-failed'
207        source: string
208        plugin: string
209        mcpbPath: string
210        reason: string
211      }
212    | {
213        type: 'mcpb-invalid-manifest'
214        source: string
215        plugin: string
216        mcpbPath: string
217        validationError: string
218      }
219    | {
220        type: 'lsp-config-invalid'
221        source: string
222        plugin: string
223        serverName: string
224        validationError: string
225      }
226    | {
227        type: 'lsp-server-start-failed'
228        source: string
229        plugin: string
230        serverName: string
231        reason: string
232      }
233    | {
234        type: 'lsp-server-crashed'
235        source: string
236        plugin: string
237        serverName: string
238        exitCode: number | null
239        signal?: string
240      }
241    | {
242        type: 'lsp-request-timeout'
243        source: string
244        plugin: string
245        serverName: string
246        method: string
247        timeoutMs: number
248      }
249    | {
250        type: 'lsp-request-failed'
251        source: string
252        plugin: string
253        serverName: string
254        method: string
255        error: string
256      }
257    | {
258        type: 'marketplace-blocked-by-policy'
259        source: string
260        plugin?: string
261        marketplace: string
262        blockedByBlocklist?: boolean // true if blocked by blockedMarketplaces, false if not in strictKnownMarketplaces
263        allowedSources: string[] // Formatted source strings (e.g., "github:owner/repo")
264      }
265    | {
266        type: 'dependency-unsatisfied'
267        source: string
268        plugin: string
269        dependency: string
270        reason: 'not-enabled' | 'not-found'
271      }
272    | {
273        type: 'plugin-cache-miss'
274        source: string
275        plugin: string
276        installPath: string
277      }
278    | {
279        type: 'generic-error'
280        source: string
281        plugin?: string
282        error: string
283      }
284  
285  export type PluginLoadResult = {
286    enabled: LoadedPlugin[]
287    disabled: LoadedPlugin[]
288    errors: PluginError[]
289  }
290  
291  /**
292   * Helper function to get a display message from any PluginError
293   * Useful for logging and simple error displays
294   */
295  export function getPluginErrorMessage(error: PluginError): string {
296    switch (error.type) {
297      case 'generic-error':
298        return error.error
299      case 'path-not-found':
300        return `Path not found: ${error.path} (${error.component})`
301      case 'git-auth-failed':
302        return `Git authentication failed (${error.authType}): ${error.gitUrl}`
303      case 'git-timeout':
304        return `Git ${error.operation} timeout: ${error.gitUrl}`
305      case 'network-error':
306        return `Network error: ${error.url}${error.details ? ` - ${error.details}` : ''}`
307      case 'manifest-parse-error':
308        return `Manifest parse error: ${error.parseError}`
309      case 'manifest-validation-error':
310        return `Manifest validation failed: ${error.validationErrors.join(', ')}`
311      case 'plugin-not-found':
312        return `Plugin ${error.pluginId} not found in marketplace ${error.marketplace}`
313      case 'marketplace-not-found':
314        return `Marketplace ${error.marketplace} not found`
315      case 'marketplace-load-failed':
316        return `Marketplace ${error.marketplace} failed to load: ${error.reason}`
317      case 'mcp-config-invalid':
318        return `MCP server ${error.serverName} invalid: ${error.validationError}`
319      case 'mcp-server-suppressed-duplicate': {
320        const dup = error.duplicateOf.startsWith('plugin:')
321          ? `server provided by plugin "${error.duplicateOf.split(':')[1] ?? '?'}"`
322          : `already-configured "${error.duplicateOf}"`
323        return `MCP server "${error.serverName}" skipped — same command/URL as ${dup}`
324      }
325      case 'hook-load-failed':
326        return `Hook load failed: ${error.reason}`
327      case 'component-load-failed':
328        return `${error.component} load failed from ${error.path}: ${error.reason}`
329      case 'mcpb-download-failed':
330        return `Failed to download MCPB from ${error.url}: ${error.reason}`
331      case 'mcpb-extract-failed':
332        return `Failed to extract MCPB ${error.mcpbPath}: ${error.reason}`
333      case 'mcpb-invalid-manifest':
334        return `MCPB manifest invalid at ${error.mcpbPath}: ${error.validationError}`
335      case 'lsp-config-invalid':
336        return `Plugin "${error.plugin}" has invalid LSP server config for "${error.serverName}": ${error.validationError}`
337      case 'lsp-server-start-failed':
338        return `Plugin "${error.plugin}" failed to start LSP server "${error.serverName}": ${error.reason}`
339      case 'lsp-server-crashed':
340        if (error.signal) {
341          return `Plugin "${error.plugin}" LSP server "${error.serverName}" crashed with signal ${error.signal}`
342        }
343        return `Plugin "${error.plugin}" LSP server "${error.serverName}" crashed with exit code ${error.exitCode ?? 'unknown'}`
344      case 'lsp-request-timeout':
345        return `Plugin "${error.plugin}" LSP server "${error.serverName}" timed out on ${error.method} request after ${error.timeoutMs}ms`
346      case 'lsp-request-failed':
347        return `Plugin "${error.plugin}" LSP server "${error.serverName}" ${error.method} request failed: ${error.error}`
348      case 'marketplace-blocked-by-policy':
349        if (error.blockedByBlocklist) {
350          return `Marketplace '${error.marketplace}' is blocked by enterprise policy`
351        }
352        return `Marketplace '${error.marketplace}' is not in the allowed marketplace list`
353      case 'dependency-unsatisfied': {
354        const hint =
355          error.reason === 'not-enabled'
356            ? 'disabled — enable it or remove the dependency'
357            : 'not found in any configured marketplace'
358        return `Dependency "${error.dependency}" is ${hint}`
359      }
360      case 'plugin-cache-miss':
361        return `Plugin "${error.plugin}" not cached at ${error.installPath} — run /plugins to refresh`
362    }
363  }