import { readFileSync, existsSync } from 'fs';
import { resolve } from 'path';
import yaml from 'js-yaml';
import { ZodError } from 'zod';
import { Config, ConfigSchema, ValidationError } from './types.js';
export class ConfigError extends Error {
constructor(message: string) {
super(message);
this.name = 'ConfigError';
}
}
export function loadConfig(configPath: string = 'instruction.yaml'): Config {
const resolvedPath = resolve(configPath);
if (!existsSync(resolvedPath)) {
throw new ConfigError(`Config file not found: ${resolvedPath}`);
}
let raw: string;
try {
raw = readFileSync(resolvedPath, 'utf-8');
} catch (err) {
throw new ConfigError(`Failed to read config file: ${(err as Error).message}`);
}
let parsed: unknown;
try {
parsed = yaml.load(raw);
} catch (err) {
throw new ConfigError(`Invalid YAML syntax: ${(err as Error).message}`);
}
try {
return ConfigSchema.parse(parsed);
} catch (err) {
if (err instanceof ZodError) {
const issues = err.issues.map((i) => ` - ${i.path.join('.')}: ${i.message}`);
throw new ConfigError(`Invalid config schema:\n${issues.join('\n')}`);
}
throw err;
}
}
export function validateConfig(config: Config, basePath: string = '.'): ValidationError[] {
const errors: ValidationError[] = [];
const baseDir = resolve(basePath);
for (const [name, tool] of Object.entries(config.tools)) {
const instructionKeys = Object.keys(tool.instructions);
// Check not empty
if (instructionKeys.length === 0) {
errors.push({
tool: name,
message: 'Must have at least one instruction',
});
continue;
}
// Check default is valid
if (tool.default && !tool.instructions[tool.default]) {
errors.push({
tool: name,
message: `Invalid default "${tool.default}". Available: ${instructionKeys.join(', ')}`,
});
}
// Check files exist
for (const [key, filepath] of Object.entries(tool.instructions)) {
const resolvedFilePath = resolve(baseDir, filepath);
if (!existsSync(resolvedFilePath)) {
errors.push({
tool: name,
message: `File not found for instruction "${key}": ${filepath}`,
});
}
}
}
return errors;
}
export function getConfigDir(configPath: string): string {
const resolvedPath = resolve(configPath);
return resolve(resolvedPath, '..');
}