* Parse hook metadata from a hook folder (similar to skills) * @param {string} hookPath - Path to the hook folder * @returns {object|null} Hook metadata or null on error
(hookPath)
| 183 | * @returns {object|null} Hook metadata or null on error |
| 184 | */ |
| 185 | function parseHookMetadata(hookPath) { |
| 186 | return safeFileOperation( |
| 187 | () => { |
| 188 | const readmeFile = path.join(hookPath, "README.md"); |
| 189 | if (!fs.existsSync(readmeFile)) { |
| 190 | return null; |
| 191 | } |
| 192 | |
| 193 | const frontmatter = parseFrontmatter(readmeFile); |
| 194 | |
| 195 | // Validate required fields |
| 196 | if (!frontmatter?.name || !frontmatter?.description) { |
| 197 | console.warn( |
| 198 | `Invalid hook at ${hookPath}: missing name or description in frontmatter` |
| 199 | ); |
| 200 | return null; |
| 201 | } |
| 202 | |
| 203 | // Extract hook events from hooks.json if it exists |
| 204 | let hookEvents = []; |
| 205 | const hooksJsonPath = path.join(hookPath, "hooks.json"); |
| 206 | if (fs.existsSync(hooksJsonPath)) { |
| 207 | try { |
| 208 | const hooksJsonContent = fs.readFileSync(hooksJsonPath, "utf8"); |
| 209 | const hooksConfig = JSON.parse(hooksJsonContent); |
| 210 | // Extract all hook event names from the hooks object |
| 211 | if (hooksConfig.hooks && typeof hooksConfig.hooks === "object") { |
| 212 | hookEvents = Object.keys(hooksConfig.hooks); |
| 213 | } |
| 214 | } catch (error) { |
| 215 | console.warn( |
| 216 | `Failed to parse hooks.json at ${hookPath}: ${error.message}` |
| 217 | ); |
| 218 | } |
| 219 | } |
| 220 | |
| 221 | // List bundled assets (all files except README.md), recursing through subdirectories |
| 222 | const getAllFiles = (dirPath, arrayOfFiles = []) => { |
| 223 | const files = fs.readdirSync(dirPath); |
| 224 | |
| 225 | files.forEach((file) => { |
| 226 | const filePath = path.join(dirPath, file); |
| 227 | if (fs.statSync(filePath).isDirectory()) { |
| 228 | arrayOfFiles = getAllFiles(filePath, arrayOfFiles); |
| 229 | } else { |
| 230 | const relativePath = path.relative(hookPath, filePath); |
| 231 | if (relativePath !== "README.md") { |
| 232 | // Normalize path separators to forward slashes for cross-platform consistency |
| 233 | arrayOfFiles.push(relativePath.replace(/\\/g, "/")); |
| 234 | } |
| 235 | } |
| 236 | }); |
| 237 | |
| 238 | return arrayOfFiles; |
| 239 | }; |
| 240 | |
| 241 | const assets = getAllFiles(hookPath).sort(); |
| 242 |
no test coverage detected