( pluginPath: string, source: string, enabled: boolean, fallbackName: string, strict = true, )
| 1347 | * @returns Object containing the LoadedPlugin and any errors encountered |
| 1348 | */ |
| 1349 | export async function createPluginFromPath( |
| 1350 | pluginPath: string, |
| 1351 | source: string, |
| 1352 | enabled: boolean, |
| 1353 | fallbackName: string, |
| 1354 | strict = true, |
| 1355 | ): Promise<{ plugin: LoadedPlugin; errors: PluginError[] }> { |
| 1356 | const errors: PluginError[] = [] |
| 1357 | |
| 1358 | // Step 1: Load or create the plugin manifest |
| 1359 | // This provides metadata about the plugin (name, version, etc.) |
| 1360 | const manifestPath = join(pluginPath, '.claude-plugin', 'plugin.json') |
| 1361 | const manifest = await loadPluginManifest(manifestPath, fallbackName, source) |
| 1362 | |
| 1363 | // Step 2: Create the base plugin object |
| 1364 | // Start with required fields from manifest and parameters |
| 1365 | const plugin: LoadedPlugin = { |
| 1366 | name: manifest.name, // Use name from manifest (or fallback) |
| 1367 | manifest, // Store full manifest for later use |
| 1368 | path: pluginPath, // Absolute path to plugin directory |
| 1369 | source, // Source identifier (e.g., "git:repo" or ".claude-plugin/name") |
| 1370 | repository: source, // For backward compatibility with Plugin Repository |
| 1371 | enabled, // Current enabled state |
| 1372 | } |
| 1373 | |
| 1374 | // Step 3: Auto-detect optional directories in parallel |
| 1375 | const [ |
| 1376 | commandsDirExists, |
| 1377 | agentsDirExists, |
| 1378 | skillsDirExists, |
| 1379 | outputStylesDirExists, |
| 1380 | ] = await Promise.all([ |
| 1381 | !manifest.commands ? pathExists(join(pluginPath, 'commands')) : false, |
| 1382 | !manifest.agents ? pathExists(join(pluginPath, 'agents')) : false, |
| 1383 | !manifest.skills ? pathExists(join(pluginPath, 'skills')) : false, |
| 1384 | !manifest.outputStyles |
| 1385 | ? pathExists(join(pluginPath, 'output-styles')) |
| 1386 | : false, |
| 1387 | ]) |
| 1388 | |
| 1389 | const commandsPath = join(pluginPath, 'commands') |
| 1390 | if (commandsDirExists) { |
| 1391 | plugin.commandsPath = commandsPath |
| 1392 | } |
| 1393 | |
| 1394 | // Step 3a: Process additional command paths from manifest |
| 1395 | if (manifest.commands) { |
| 1396 | // Check if it's an object mapping (record of command name → metadata) |
| 1397 | const firstValue = Object.values(manifest.commands)[0] |
| 1398 | if ( |
| 1399 | typeof manifest.commands === 'object' && |
| 1400 | !Array.isArray(manifest.commands) && |
| 1401 | firstValue && |
| 1402 | typeof firstValue === 'object' && |
| 1403 | ('source' in firstValue || 'content' in firstValue) |
| 1404 | ) { |
| 1405 | // Object mapping format: { "about": { "source": "./README.md", ... } } |
| 1406 | const commandsMetadata: Record<string, CommandMetadata> = {} |
no test coverage detected