/ outputStyles / loadOutputStylesDir.ts
loadOutputStylesDir.ts
 1  import memoize from 'lodash-es/memoize.js'
 2  import { basename } from 'path'
 3  import type { OutputStyleConfig } from '../constants/outputStyles.js'
 4  import { logForDebugging } from '../utils/debug.js'
 5  import { coerceDescriptionToString } from '../utils/frontmatterParser.js'
 6  import { logError } from '../utils/log.js'
 7  import {
 8    extractDescriptionFromMarkdown,
 9    loadMarkdownFilesForSubdir,
10  } from '../utils/markdownConfigLoader.js'
11  import { clearPluginOutputStyleCache } from '../utils/plugins/loadPluginOutputStyles.js'
12  
13  /**
14   * Loads markdown files from .claude/output-styles directories throughout the project
15   * and from ~/.claude/output-styles directory and converts them to output styles.
16   *
17   * Each filename becomes a style name, and the file content becomes the style prompt.
18   * The frontmatter provides name and description.
19   *
20   * Structure:
21   * - Project .claude/output-styles/*.md -> project styles
22   * - User ~/.claude/output-styles/*.md -> user styles (overridden by project styles)
23   *
24   * @param cwd Current working directory for project directory traversal
25   */
26  export const getOutputStyleDirStyles = memoize(
27    async (cwd: string): Promise<OutputStyleConfig[]> => {
28      try {
29        const markdownFiles = await loadMarkdownFilesForSubdir(
30          'output-styles',
31          cwd,
32        )
33  
34        const styles = markdownFiles
35          .map(({ filePath, frontmatter, content, source }) => {
36            try {
37              const fileName = basename(filePath)
38              const styleName = fileName.replace(/\.md$/, '')
39  
40              // Get style configuration from frontmatter
41              const name = (frontmatter['name'] || styleName) as string
42              const description =
43                coerceDescriptionToString(
44                  frontmatter['description'],
45                  styleName,
46                ) ??
47                extractDescriptionFromMarkdown(
48                  content,
49                  `Custom ${styleName} output style`,
50                )
51  
52              // Parse keep-coding-instructions flag (supports both boolean and string values)
53              const keepCodingInstructionsRaw =
54                frontmatter['keep-coding-instructions']
55              const keepCodingInstructions =
56                keepCodingInstructionsRaw === true ||
57                keepCodingInstructionsRaw === 'true'
58                  ? true
59                  : keepCodingInstructionsRaw === false ||
60                      keepCodingInstructionsRaw === 'false'
61                    ? false
62                    : undefined
63  
64              // Warn if force-for-plugin is set on non-plugin output style
65              if (frontmatter['force-for-plugin'] !== undefined) {
66                logForDebugging(
67                  `Output style "${name}" has force-for-plugin set, but this option only applies to plugin output styles. Ignoring.`,
68                  { level: 'warn' },
69                )
70              }
71  
72              return {
73                name,
74                description,
75                prompt: content.trim(),
76                source,
77                keepCodingInstructions,
78              }
79            } catch (error) {
80              logError(error)
81              return null
82            }
83          })
84          .filter(style => style !== null)
85  
86        return styles
87      } catch (error) {
88        logError(error)
89        return []
90      }
91    },
92  )
93  
94  export function clearOutputStyleCaches(): void {
95    getOutputStyleDirStyles.cache?.clear?.()
96    loadMarkdownFilesForSubdir.cache?.clear?.()
97    clearPluginOutputStyleCache()
98  }