design-agent
Version:
Universal AI Design Review Agent - CLI tool for scanning code for design drift
332 lines (284 loc) • 9.71 kB
JavaScript
/**
* Configuration validation utilities
* Validates design agent configuration files and environment
*/
import { validateConfig, validateEnvironmentVariables } from './validation.mjs';
export function validateDesignAgentConfig(config) {
const errors = [];
const warnings = [];
// Validate project configuration
if (!config.project || typeof config.project !== 'string') {
errors.push('Project name must be a non-empty string');
}
// Validate mode configuration
if (config.mode) {
if (typeof config.mode !== 'object') {
errors.push('Mode configuration must be an object');
} else {
if (config.mode.on_pr !== undefined && typeof config.mode.on_pr !== 'boolean') {
errors.push('mode.on_pr must be a boolean');
}
if (config.mode.nightly !== undefined && typeof config.mode.nightly !== 'boolean') {
errors.push('mode.nightly must be a boolean');
}
}
}
// Validate code configuration
if (config.code) {
if (!Array.isArray(config.code.include)) {
errors.push('code.include must be an array');
} else if (config.code.include.length === 0) {
warnings.push('code.include is empty - no files will be scanned');
}
if (config.code.exclude && !Array.isArray(config.code.exclude)) {
errors.push('code.exclude must be an array');
}
} else {
errors.push('Code configuration is required');
}
// Validate design system configuration
if (config.designSystem) {
if (!Array.isArray(config.designSystem.sources)) {
errors.push('designSystem.sources must be an array');
} else {
config.designSystem.sources.forEach((source, index) => {
if (!source.kind) {
errors.push(`designSystem.sources[${index}].kind is required`);
}
if (!source.path) {
errors.push(`designSystem.sources[${index}].path is required`);
}
// Validate source kinds
const validKinds = [
'tailwind', 'tokens', 'storybook', 'vercel', 'openai',
'anthropic', 'langchain', 'llamaindex', 'aws', 'docker',
'vite', 'nodejs'
];
if (source.kind && !validKinds.includes(source.kind)) {
errors.push(`Invalid design system source kind: ${source.kind}`);
}
});
}
if (config.designSystem.themes && !Array.isArray(config.designSystem.themes)) {
errors.push('designSystem.themes must be an array');
}
} else {
errors.push('Design system configuration is required');
}
// Validate checks configuration
if (config.checks) {
const validChecks = [
'tokenDrift', 'utilities', 'inlineStyles', 'typography',
'contrastStatic', 'storybookProps', 'accessibility', 'security',
'testing', 'performance'
];
Object.keys(config.checks).forEach(check => {
if (!validChecks.includes(check)) {
warnings.push(`Unknown check type: ${check}`);
}
if (typeof config.checks[check] !== 'boolean') {
errors.push(`check.${check} must be a boolean`);
}
});
}
// Validate privacy configuration
if (config.privacy) {
if (config.privacy.sendSourceToAI !== undefined && typeof config.privacy.sendSourceToAI !== 'boolean') {
errors.push('privacy.sendSourceToAI must be a boolean');
}
if (config.privacy.aiSummaryFromFindingsOnly !== undefined && typeof config.privacy.aiSummaryFromFindingsOnly !== 'boolean') {
errors.push('privacy.aiSummaryFromFindingsOnly must be a boolean');
}
}
// Validate integrations configuration
if (config.integrations) {
if (config.integrations.slackWebhook !== null && typeof config.integrations.slackWebhook !== 'string') {
errors.push('integrations.slackWebhook must be a string or null');
}
if (config.integrations.slackWebhook && !isValidUrl(config.integrations.slackWebhook)) {
errors.push('integrations.slackWebhook must be a valid URL');
}
}
// Validate output configuration
if (config.output) {
if (config.output.report && typeof config.output.report !== 'string') {
errors.push('output.report must be a string');
}
if (config.output.json && typeof config.output.json !== 'string') {
errors.push('output.json must be a string');
}
}
return { errors, warnings };
}
export function validateEnvironmentSetup() {
const errors = [];
const warnings = [];
// Check Node.js version
const nodeVersion = process.version;
const majorVersion = parseInt(nodeVersion.slice(1).split('.')[0]);
if (majorVersion < 20) {
errors.push(`Node.js version ${nodeVersion} is not supported. Required: >=20.0.0`);
}
// Check for required environment variables
const requiredEnvVars = ['NODE_ENV'];
requiredEnvVars.forEach(envVar => {
if (!process.env[envVar]) {
errors.push(`Required environment variable missing: ${envVar}`);
}
});
// Check for optional but recommended environment variables
const recommendedEnvVars = ['ANTHROPIC_API_KEY', 'SLACK_WEBHOOK_URL'];
recommendedEnvVars.forEach(envVar => {
if (!process.env[envVar]) {
warnings.push(`Recommended environment variable not set: ${envVar}`);
}
});
// Validate API keys if present
if (process.env.ANTHROPIC_API_KEY) {
if (!process.env.ANTHROPIC_API_KEY.startsWith('sk-ant-')) {
errors.push('Invalid Anthropic API key format');
}
}
if (process.env.OPENAI_API_KEY) {
if (!process.env.OPENAI_API_KEY.startsWith('sk-')) {
errors.push('Invalid OpenAI API key format');
}
}
if (process.env.SLACK_WEBHOOK_URL) {
if (!isValidUrl(process.env.SLACK_WEBHOOK_URL)) {
errors.push('Invalid Slack webhook URL format');
}
}
return { errors, warnings };
}
export function validateFileSystemAccess(config) {
const errors = [];
const warnings = [];
// Check if scan paths exist
if (config.code?.include) {
for (const path of config.code.include) {
try {
const fs = require('fs');
if (!fs.existsSync(path)) {
warnings.push(`Scan path does not exist: ${path}`);
}
} catch (error) {
// Skip if fs is not available
}
}
}
// Check if design system sources exist
if (config.designSystem?.sources) {
for (const source of config.designSystem.sources) {
if (source.path) {
try {
const fs = require('fs');
if (!fs.existsSync(source.path)) {
warnings.push(`Design system source does not exist: ${source.path}`);
}
} catch (error) {
// Skip if fs is not available
}
}
}
}
return { errors, warnings };
}
export function validateAdapterConfiguration(config) {
const errors = [];
const warnings = [];
if (!config.designSystem?.sources) {
return { errors, warnings };
}
const adapters = config.designSystem.sources.map(s => s.kind);
const uniqueAdapters = [...new Set(adapters)];
// Check for duplicate adapters
if (adapters.length !== uniqueAdapters.length) {
warnings.push('Duplicate adapters detected in design system sources');
}
// Check for conflicting adapters
const conflictingPairs = [
['openai', 'anthropic'],
['vercel', 'aws'],
['vite', 'nodejs']
];
for (const [adapter1, adapter2] of conflictingPairs) {
if (adapters.includes(adapter1) && adapters.includes(adapter2)) {
warnings.push(`Potentially conflicting adapters: ${adapter1} and ${adapter2}`);
}
}
// Check for missing required files
const requiredFiles = {
tailwind: 'tailwind.config.js',
tokens: 'design-tokens/tokens.json',
storybook: 'storybook-static/stories.json',
vercel: 'vercel.json',
vite: 'vite.config.js',
nodejs: 'package.json'
};
for (const adapter of uniqueAdapters) {
if (requiredFiles[adapter]) {
const source = config.designSystem.sources.find(s => s.kind === adapter);
if (source && source.path !== requiredFiles[adapter]) {
warnings.push(`Consider using standard path for ${adapter}: ${requiredFiles[adapter]}`);
}
}
}
return { errors, warnings };
}
export function generateConfigValidationReport(validationResults) {
let report = '## Configuration Validation Report\n\n';
const { errors, warnings } = validationResults;
if (errors.length === 0 && warnings.length === 0) {
report += '✅ Configuration is valid!\n';
return report;
}
if (errors.length > 0) {
report += '### Errors (Must Fix)\n\n';
errors.forEach(error => {
report += `- ❌ ${error}\n`;
});
report += '\n';
}
if (warnings.length > 0) {
report += '### Warnings (Consider Fixing)\n\n';
warnings.forEach(warning => {
report += `- ⚠️ ${warning}\n`;
});
}
return report;
}
export function validateCompleteSetup(config) {
const results = {
config: validateDesignAgentConfig(config),
environment: validateEnvironmentSetup(),
fileSystem: validateFileSystemAccess(config),
adapters: validateAdapterConfiguration(config)
};
const allErrors = [
...results.config.errors,
...results.environment.errors,
...results.fileSystem.errors,
...results.adapters.errors
];
const allWarnings = [
...results.config.warnings,
...results.environment.warnings,
...results.fileSystem.warnings,
...results.adapters.warnings
];
return {
valid: allErrors.length === 0,
errors: allErrors,
warnings: allWarnings,
details: results
};
}
function isValidUrl(url) {
try {
new URL(url);
return true;
} catch {
return false;
}
}