/**
* Custom Theme Loader
* Loads user-defined theme standards from JSON files.
* Default directory: ~/.magento-mcp/themes/
* Override with MAGENTO_MCP_THEME_DIR environment variable.
*/
import { readFileSync, readdirSync, existsSync } from 'fs';
import { join } from 'path';
import { homedir } from 'os';
import { ThemeStandard, ThemeValidationRule } from '../types.js';
/**
* Get the custom themes directory path.
*/
export function getCustomThemeDir(): string {
if (process.env.MAGENTO_MCP_THEME_DIR) {
return process.env.MAGENTO_MCP_THEME_DIR;
}
return join(homedir(), '.magento-mcp', 'themes');
}
/**
* Validate a single validation rule from user JSON.
* Returns null if invalid (silently skipped).
*/
function validateRule(rule: unknown): ThemeValidationRule | null {
if (!rule || typeof rule !== 'object') return null;
const r = rule as Record<string, unknown>;
// Required fields
if (typeof r.pattern !== 'string' || !r.pattern) return null;
if (!Array.isArray(r.fileTypes) || r.fileTypes.length === 0) return null;
if (typeof r.severity !== 'number' || r.severity < 1 || r.severity > 10) return null;
if (r.type !== 'error' && r.type !== 'warning') return null;
if (typeof r.message !== 'string' || !r.message) return null;
if (typeof r.rule !== 'string' || !r.rule) return null;
// Validate the regex pattern
try {
new RegExp(r.pattern);
} catch {
return null; // Invalid regex — skip silently
}
// Validate fileTypes
const validFileTypes = ['php', 'phtml', 'js', 'less', 'css'];
const fileTypes = r.fileTypes.filter(
(ft: unknown) => typeof ft === 'string' && validFileTypes.includes(ft)
);
if (fileTypes.length === 0) return null;
return {
pattern: r.pattern,
fileTypes: fileTypes as ThemeValidationRule['fileTypes'],
severity: r.severity,
type: r.type,
message: r.message,
rule: r.rule,
suggestion: typeof r.suggestion === 'string' ? r.suggestion : undefined,
mode: 'discourage',
};
}
/**
* Load a single custom theme from a JSON file.
* Returns null if the file is invalid.
*/
function loadThemeFile(filePath: string): ThemeStandard | null {
try {
const content = readFileSync(filePath, 'utf-8');
const data = JSON.parse(content);
// Validate required top-level fields
if (!data.id || typeof data.id !== 'string') return null;
if (!data.name || typeof data.name !== 'string') return null;
const theme: ThemeStandard = {
id: data.id,
name: data.name,
version: typeof data.version === 'string' ? data.version : '1.0.0',
description: typeof data.description === 'string' ? data.description : '',
technologies: {
use: Array.isArray(data.technologies?.use)
? data.technologies.use.filter((s: unknown) => typeof s === 'string')
: [],
avoid: Array.isArray(data.technologies?.avoid)
? data.technologies.avoid.filter((s: unknown) => typeof s === 'string')
: [],
},
validationRules: [],
patternOverrides: Array.isArray(data.patternOverrides)
? data.patternOverrides.filter((po: unknown) => {
if (!po || typeof po !== 'object') return false;
const p = po as Record<string, unknown>;
return typeof p.taskKeyword === 'string' &&
typeof p.correctPattern === 'string' &&
typeof p.example === 'string';
})
: [],
bestPractices: Array.isArray(data.bestPractices)
? data.bestPractices.filter((s: unknown) => typeof s === 'string')
: [],
};
// Validate each rule individually
if (Array.isArray(data.validationRules)) {
for (const rawRule of data.validationRules) {
const rule = validateRule(rawRule);
if (rule) {
theme.validationRules.push(rule);
}
}
}
return theme;
} catch {
return null; // Parse error or file read error — skip silently
}
}
/**
* Load all custom themes from the themes directory.
* Returns a record of theme ID → ThemeStandard.
*/
export function loadCustomThemes(): Record<string, ThemeStandard> {
const dir = getCustomThemeDir();
const themes: Record<string, ThemeStandard> = {};
if (!existsSync(dir)) {
return themes;
}
try {
const files = readdirSync(dir).filter(f => f.endsWith('.json'));
for (const file of files) {
const theme = loadThemeFile(join(dir, file));
if (theme) {
themes[theme.id] = theme;
}
}
} catch {
// Directory read error — return empty
}
return themes;
}