/ utils / plugins / loadPluginOutputStyles.ts
loadPluginOutputStyles.ts
  1  import memoize from 'lodash-es/memoize.js'
  2  import { basename } from 'path'
  3  import type { OutputStyleConfig } from '../../constants/outputStyles.js'
  4  import { getPluginErrorMessage } from '../../types/plugin.js'
  5  import { logForDebugging } from '../debug.js'
  6  import {
  7    coerceDescriptionToString,
  8    parseFrontmatter,
  9  } from '../frontmatterParser.js'
 10  import { getFsImplementation, isDuplicatePath } from '../fsOperations.js'
 11  import { extractDescriptionFromMarkdown } from '../markdownConfigLoader.js'
 12  import { loadAllPluginsCacheOnly } from './pluginLoader.js'
 13  import { walkPluginMarkdown } from './walkPluginMarkdown.js'
 14  
 15  async function loadOutputStylesFromDirectory(
 16    outputStylesPath: string,
 17    pluginName: string,
 18    loadedPaths: Set<string>,
 19  ): Promise<OutputStyleConfig[]> {
 20    const styles: OutputStyleConfig[] = []
 21    await walkPluginMarkdown(
 22      outputStylesPath,
 23      async fullPath => {
 24        const style = await loadOutputStyleFromFile(
 25          fullPath,
 26          pluginName,
 27          loadedPaths,
 28        )
 29        if (style) styles.push(style)
 30      },
 31      { logLabel: 'output-styles' },
 32    )
 33    return styles
 34  }
 35  
 36  async function loadOutputStyleFromFile(
 37    filePath: string,
 38    pluginName: string,
 39    loadedPaths: Set<string>,
 40  ): Promise<OutputStyleConfig | null> {
 41    const fs = getFsImplementation()
 42    if (isDuplicatePath(fs, filePath, loadedPaths)) {
 43      return null
 44    }
 45    try {
 46      const content = await fs.readFile(filePath, { encoding: 'utf-8' })
 47      const { frontmatter, content: markdownContent } = parseFrontmatter(
 48        content,
 49        filePath,
 50      )
 51  
 52      const fileName = basename(filePath, '.md')
 53      const baseStyleName = (frontmatter.name as string) || fileName
 54      // Namespace output styles with plugin name, consistent with commands and agents
 55      const name = `${pluginName}:${baseStyleName}`
 56      const description =
 57        coerceDescriptionToString(frontmatter.description, name) ??
 58        extractDescriptionFromMarkdown(
 59          markdownContent,
 60          `Output style from ${pluginName} plugin`,
 61        )
 62  
 63      // Parse forceForPlugin flag (supports both boolean and string values)
 64      const forceRaw = frontmatter['force-for-plugin']
 65      const forceForPlugin =
 66        forceRaw === true || forceRaw === 'true'
 67          ? true
 68          : forceRaw === false || forceRaw === 'false'
 69            ? false
 70            : undefined
 71  
 72      return {
 73        name,
 74        description,
 75        prompt: markdownContent.trim(),
 76        source: 'plugin',
 77        forceForPlugin,
 78      }
 79    } catch (error) {
 80      logForDebugging(`Failed to load output style from ${filePath}: ${error}`, {
 81        level: 'error',
 82      })
 83      return null
 84    }
 85  }
 86  
 87  export const loadPluginOutputStyles = memoize(
 88    async (): Promise<OutputStyleConfig[]> => {
 89      // Only load output styles from enabled plugins
 90      const { enabled, errors } = await loadAllPluginsCacheOnly()
 91      const allStyles: OutputStyleConfig[] = []
 92  
 93      if (errors.length > 0) {
 94        logForDebugging(
 95          `Plugin loading errors: ${errors.map(e => getPluginErrorMessage(e)).join(', ')}`,
 96        )
 97      }
 98  
 99      for (const plugin of enabled) {
100        // Track loaded file paths to prevent duplicates within this plugin
101        const loadedPaths = new Set<string>()
102  
103        // Load output styles from default output-styles directory
104        if (plugin.outputStylesPath) {
105          try {
106            const styles = await loadOutputStylesFromDirectory(
107              plugin.outputStylesPath,
108              plugin.name,
109              loadedPaths,
110            )
111            allStyles.push(...styles)
112  
113            if (styles.length > 0) {
114              logForDebugging(
115                `Loaded ${styles.length} output styles from plugin ${plugin.name} default directory`,
116              )
117            }
118          } catch (error) {
119            logForDebugging(
120              `Failed to load output styles from plugin ${plugin.name} default directory: ${error}`,
121              { level: 'error' },
122            )
123          }
124        }
125  
126        // Load output styles from additional paths specified in manifest
127        if (plugin.outputStylesPaths) {
128          for (const stylePath of plugin.outputStylesPaths) {
129            try {
130              const fs = getFsImplementation()
131              const stats = await fs.stat(stylePath)
132  
133              if (stats.isDirectory()) {
134                // Load all .md files from directory
135                const styles = await loadOutputStylesFromDirectory(
136                  stylePath,
137                  plugin.name,
138                  loadedPaths,
139                )
140                allStyles.push(...styles)
141  
142                if (styles.length > 0) {
143                  logForDebugging(
144                    `Loaded ${styles.length} output styles from plugin ${plugin.name} custom path: ${stylePath}`,
145                  )
146                }
147              } else if (stats.isFile() && stylePath.endsWith('.md')) {
148                // Load single output style file
149                const style = await loadOutputStyleFromFile(
150                  stylePath,
151                  plugin.name,
152                  loadedPaths,
153                )
154                if (style) {
155                  allStyles.push(style)
156                  logForDebugging(
157                    `Loaded output style from plugin ${plugin.name} custom file: ${stylePath}`,
158                  )
159                }
160              }
161            } catch (error) {
162              logForDebugging(
163                `Failed to load output styles from plugin ${plugin.name} custom path ${stylePath}: ${error}`,
164                { level: 'error' },
165              )
166            }
167          }
168        }
169      }
170  
171      logForDebugging(`Total plugin output styles loaded: ${allStyles.length}`)
172      return allStyles
173    },
174  )
175  
176  export function clearPluginOutputStyleCache(): void {
177    loadPluginOutputStyles.cache?.clear?.()
178  }