( raw: string, name: string, filePath: string, origin: 'project' | 'user' | 'plugin', )
| 105 | |
| 106 | /** Parse a Markdown file into a CustomCommandSpec. */ |
| 107 | export function parseSpec( |
| 108 | raw: string, |
| 109 | name: string, |
| 110 | filePath: string, |
| 111 | origin: 'project' | 'user' | 'plugin', |
| 112 | ): CustomCommandSpec { |
| 113 | let description: string | undefined; |
| 114 | let argumentHint: string | undefined; |
| 115 | let allowedTools: string[] | undefined; |
| 116 | let model: string | undefined; |
| 117 | let mode: 'plan' | 'normal' | undefined; |
| 118 | let body = raw; |
| 119 | |
| 120 | // Frontmatter must be at the very top, delimited by --- on its own line. |
| 121 | const fmMatch = raw.match(/^---\s*\n([\s\S]*?)\n---\s*\r?\n?([\s\S]*)$/); |
| 122 | if (fmMatch) { |
| 123 | const fm = fmMatch[1] ?? ''; |
| 124 | body = fmMatch[2] ?? ''; |
| 125 | const fmLines = fm.split('\n'); |
| 126 | let i = 0; |
| 127 | while (i < fmLines.length) { |
| 128 | const line = fmLines[i]!; |
| 129 | // Skip empty/comment lines |
| 130 | if (/^\s*(#.*)?$/.test(line)) { i++; continue; } |
| 131 | const m = line.match(/^([a-zA-Z][\w-]*)\s*:\s*(.*)$/); |
| 132 | if (!m) { i++; continue; } |
| 133 | const key = m[1]!.toLowerCase(); |
| 134 | const rawValue = m[2]!.trim(); |
| 135 | |
| 136 | let value: string | string[]; |
| 137 | if (rawValue === '') { |
| 138 | // Look ahead for ` - item` lines (YAML multiline list form) |
| 139 | const items: string[] = []; |
| 140 | i++; |
| 141 | while (i < fmLines.length && /^\s+-\s+/.test(fmLines[i]!)) { |
| 142 | items.push(stripQuotes(fmLines[i]!.replace(/^\s+-\s+/, '').trim())); |
| 143 | i++; |
| 144 | } |
| 145 | value = items; |
| 146 | } else if (rawValue.startsWith('[') && rawValue.endsWith(']')) { |
| 147 | // Inline array |
| 148 | value = rawValue |
| 149 | .slice(1, -1) |
| 150 | .split(',') |
| 151 | .map(s => stripQuotes(s.trim())) |
| 152 | .filter(Boolean); |
| 153 | i++; |
| 154 | } else { |
| 155 | value = stripQuotes(rawValue); |
| 156 | i++; |
| 157 | } |
| 158 | |
| 159 | // Apply the key. Validate types loosely. |
| 160 | switch (key) { |
| 161 | case 'description': |
| 162 | if (typeof value === 'string') description = value; |
| 163 | break; |
| 164 | case 'argument-hint': |
no test coverage detected