UNPKG

claude-git-hooks

Version:

Git hooks with Claude CLI for code analysis and automatic commit messages

215 lines (186 loc) 6.35 kB
/** * File: preset-loader.js * Purpose: Loads preset configurations and metadata * * Key responsibilities: * - Load preset.json (metadata) * - Load config.json (overrides) * - Resolve template paths * - Replace placeholders in templates * * Dependencies: * - fs/promises: Async file operations * - path: Cross-platform path handling * - git-operations: For getRepoRoot() * - logger: Debug and error logging * - installation-diagnostics: Error formatting with remediation steps */ import fs from 'fs/promises'; import path from 'path'; import { getRepoRoot } from './git-operations.js'; import logger from './logger.js'; import { formatError } from './installation-diagnostics.js'; /** * Custom error for preset loading failures */ class PresetError extends Error { constructor(message, { presetName, cause } = {}) { super(message); this.name = 'PresetError'; this.presetName = presetName; this.cause = cause; } } /** * Loads preset metadata + config * @param {string} presetName - Name of preset (backend, frontend, etc.) * @returns {Promise<Object>} { metadata, config, templates } */ export async function loadPreset(presetName) { logger.debug( 'preset-loader - loadPreset', 'Loading preset', { presetName } ); const repoRoot = getRepoRoot(); // Only try .claude/presets/{name} (installed by claude-hooks install) const presetDir = path.join(repoRoot, '.claude', 'presets', presetName); logger.debug('preset-loader - loadPreset', 'Loading preset from', { presetDir }); try { // Load preset.json (metadata) const presetJsonPath = path.join(presetDir, 'preset.json'); const metadataRaw = await fs.readFile(presetJsonPath, 'utf8'); const metadata = JSON.parse(metadataRaw); // Load config.json (overrides) const configJsonPath = path.join(presetDir, 'config.json'); const configRaw = await fs.readFile(configJsonPath, 'utf8'); const config = JSON.parse(configRaw); // Resolve template paths const templates = {}; for (const [key, templatePath] of Object.entries(metadata.templates)) { templates[key] = path.join(presetDir, templatePath); } logger.debug( 'preset-loader - loadPreset', 'Preset loaded successfully', { presetName, displayName: metadata.displayName, fileExtensions: metadata.fileExtensions, templateCount: Object.keys(templates).length } ); return { metadata, config, templates }; } catch (error) { logger.error( 'preset-loader - loadPreset', `Failed to load preset: ${presetName}`, error ); throw new PresetError(`Preset "${presetName}" not found or invalid`, { presetName, cause: error }); } } /** * Loads a template file and replaces placeholders * @param {string} templatePath - Absolute path to template file * @param {Object} metadata - Preset metadata for placeholder replacement * @returns {Promise<string>} Template content with placeholders replaced */ export async function loadTemplate(templatePath, metadata) { logger.debug( 'preset-loader - loadTemplate', 'Loading template', { templatePath } ); try { let template = await fs.readFile(templatePath, 'utf8'); // Replace placeholders template = template.replace( /{{TECH_STACK}}/g, metadata.techStack.join(', ') ); template = template.replace( /{{FOCUS_AREAS}}/g, metadata.focusAreas.join(', ') ); template = template.replace( /{{FILE_EXTENSIONS}}/g, metadata.fileExtensions.join(', ') ); template = template.replace( /{{PRESET_NAME}}/g, metadata.name ); logger.debug( 'preset-loader - loadTemplate', 'Template loaded and processed', { templatePath, originalLength: template.length } ); return template; } catch (error) { logger.error( 'preset-loader - loadTemplate', `Failed to load template: ${templatePath}`, error ); throw new PresetError(`Template not found: ${templatePath}`, { cause: error }); } } /** * Lists all available presets in .claude/presets/ * @returns {Promise<Array<Object>>} Array of preset metadata * Returns: [{ name, displayName, description }] */ export async function listPresets() { logger.debug('preset-loader - listPresets', 'Listing available presets'); const repoRoot = getRepoRoot(); const presets = []; // Load all presets from .claude/presets/ (installed by claude-hooks install) const presetsDir = path.join(repoRoot, '.claude', 'presets'); try { const presetNames = await fs.readdir(presetsDir); for (const name of presetNames) { const presetJsonPath = path.join(presetsDir, name, 'preset.json'); try { const metadataRaw = await fs.readFile(presetJsonPath, 'utf8'); const metadata = JSON.parse(metadataRaw); presets.push({ name: metadata.name, displayName: metadata.displayName, description: metadata.description }); } catch (error) { logger.debug( 'preset-loader - listPresets', `Skipping invalid preset: ${name}`, error ); } } } catch (error) { const errorMsg = formatError('No presets directory found', [ `Expected location: ${presetsDir}` ]); logger.warning(errorMsg); logger.debug( 'preset-loader - listPresets', 'Failed to read presets directory', error ); } logger.debug( 'preset-loader - listPresets', 'Presets listed', { count: presets.length } ); return presets; } export { PresetError };