claude-git-hooks
Version:
Git hooks with Claude CLI for code analysis and automatic commit messages
215 lines (186 loc) • 6.35 kB
JavaScript
/**
* 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 };