UNPKG

design-agent

Version:

Universal AI Design Review Agent - CLI tool for scanning code for design drift

332 lines (284 loc) 9.71 kB
/** * 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; } }