(filePath: string)
| 112 | * Uses lightweight regex to read `id:` and `title:` from frontmatter. |
| 113 | */ |
| 114 | export function scanTaskIds(filePath: string): TaskEntry[] { |
| 115 | const taskDir = resolveTaskDir(filePath); |
| 116 | if (!taskDir || !fs.existsSync(taskDir)) return []; |
| 117 | |
| 118 | let files: string[]; |
| 119 | try { |
| 120 | files = fs.readdirSync(taskDir, { recursive: true }) as string[]; |
| 121 | } catch { |
| 122 | return []; |
| 123 | } |
| 124 | |
| 125 | const entries: TaskEntry[] = []; |
| 126 | for (const rel of files) { |
| 127 | if (!String(rel).endsWith(".md")) continue; |
| 128 | const fullPath = path.join(taskDir, String(rel)); |
| 129 | try { |
| 130 | const content = fs.readFileSync(fullPath, "utf-8"); |
| 131 | const fmMatch = content.match(/^---\n([\s\S]*?)\n---/); |
| 132 | if (!fmMatch) continue; |
| 133 | const fm = fmMatch[1]; |
| 134 | const idMatch = fm.match(/^id:\s*"?([^"\n]+)"?\s*$/m); |
| 135 | const titleMatch = fm.match(/^title:\s*"?([^"\n]+)"?\s*$/m); |
| 136 | if (idMatch) { |
| 137 | entries.push({ |
| 138 | id: idMatch[1].trim(), |
| 139 | title: titleMatch ? titleMatch[1].trim() : "", |
| 140 | }); |
| 141 | } |
| 142 | } catch { |
| 143 | // skip unreadable files |
| 144 | } |
| 145 | } |
| 146 | return entries; |
| 147 | } |
| 148 | |
| 149 | /** A scope entry with its name and optional description. */ |
| 150 | export interface ScopeEntry { |
no test coverage detected