Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
AI-powered brand identity skill for generating cohesive brand guidelines, color palettes, and visual identity.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
scripts/validate-asset.cjs
1#!/usr/bin/env node2/**3* validate-asset.cjs4*5* Validates marketing assets against brand guidelines.6* Checks: file naming, dimensions, file size, metadata.7*8* Usage:9* node validate-asset.cjs <asset-path>10* node validate-asset.cjs <asset-path> --json11* node validate-asset.cjs <asset-path> --fix12*13* For color validation of images, use with extract-colors.cjs14*/1516const fs = require("fs");17const path = require("path");1819// Validation rules20const RULES = {21naming: {22pattern: /^[a-z]+_[a-z0-9-]+_[a-z0-9-]+_\d{8}(_[a-z0-9-]+)?\.[a-z]+$/,23description:24"{type}_{campaign}_{description}_{timestamp}_{variant}.{ext}",25examples: [26"banner_claude-launch_hero-image_20251209.png",27"logo_brand-refresh_horizontal_20251209_dark.svg",28],29},30dimensions: {31banner: { minWidth: 600, minHeight: 300 },32logo: { minWidth: 100, minHeight: 100 },33design: { minWidth: 800, minHeight: 600 },34video: { minWidth: 640, minHeight: 480 },35default: { minWidth: 100, minHeight: 100 },36},37fileSize: {38image: { max: 5 * 1024 * 1024, recommended: 1 * 1024 * 1024 },39video: { max: 100 * 1024 * 1024, recommended: 50 * 1024 * 1024 },40svg: { max: 500 * 1024, recommended: 100 * 1024 },41},42formats: {43image: ["png", "jpg", "jpeg", "webp", "gif"],44vector: ["svg"],45video: ["mp4", "mov", "webm"],46document: ["pdf", "psd", "ai", "fig"],47},48};4950/**51* Parse asset filename52*/53function parseFilename(filename) {54const parts = filename.replace(/\.[^.]+$/, "").split("_");5556if (parts.length < 4) {57return null;58}5960return {61type: parts[0],62campaign: parts[1],63description: parts[2],64timestamp: parts[3],65variant: parts.length > 4 ? parts[4] : null,66extension: path.extname(filename).slice(1).toLowerCase(),67};68}6970/**71* Validate filename convention72*/73function validateFilename(filename) {74const issues = [];75const suggestions = [];7677// Check pattern match78if (!RULES.naming.pattern.test(filename)) {79issues.push("Filename does not match naming convention");80suggestions.push(`Expected format: ${RULES.naming.description}`);81suggestions.push(`Examples: ${RULES.naming.examples.join(", ")}`);82}8384// Parse and check components85const parsed = parseFilename(filename);86if (parsed) {87// Check timestamp format88if (!/^\d{8}$/.test(parsed.timestamp)) {89issues.push("Timestamp should be YYYYMMDD format");90}9192// Check kebab-case for campaign and description93if (parsed.campaign && !/^[a-z0-9-]+$/.test(parsed.campaign)) {94issues.push("Campaign name should be kebab-case");95}9697if (parsed.description && !/^[a-z0-9-]+$/.test(parsed.description)) {98issues.push("Description should be kebab-case");99}100101// Check valid type102const validTypes = [103"banner",104"logo",105"design",106"video",107"infographic",108"icon",109"photo",110];111if (!validTypes.includes(parsed.type)) {112suggestions.push(`Consider using type: ${validTypes.join(", ")}`);113}114}115116return { valid: issues.length === 0, issues, suggestions, parsed };117}118119/**120* Validate file size121*/122function validateFileSize(filepath, extension) {123const issues = [];124const warnings = [];125126const stats = fs.statSync(filepath);127const size = stats.size;128129let limits;130if (RULES.formats.video.includes(extension)) {131limits = RULES.fileSize.video;132} else if (extension === "svg") {133limits = RULES.fileSize.svg;134} else {135limits = RULES.fileSize.image;136}137138if (size > limits.max) {139issues.push(140`File size (${formatBytes(size)}) exceeds maximum (${formatBytes(141limits.max142)})`143);144} else if (size > limits.recommended) {145warnings.push(146`File size (${formatBytes(size)}) exceeds recommended (${formatBytes(147limits.recommended148)})`149);150}151152return { valid: issues.length === 0, issues, warnings, size };153}154155/**156* Validate file format157*/158function validateFormat(extension) {159const issues = [];160const info = { category: null };161162const allFormats = [163...RULES.formats.image,164...RULES.formats.vector,165...RULES.formats.video,166...RULES.formats.document,167];168169if (!allFormats.includes(extension)) {170issues.push(`Unsupported file format: .${extension}`);171return { valid: false, issues, info };172}173174// Determine category175if (RULES.formats.image.includes(extension)) info.category = "image";176else if (RULES.formats.vector.includes(extension)) info.category = "vector";177else if (RULES.formats.video.includes(extension)) info.category = "video";178else if (RULES.formats.document.includes(extension))179info.category = "document";180181return { valid: true, issues, info };182}183184/**185* Check if asset exists in manifest186*/187function checkManifest(filepath) {188const manifestPath = path.join(process.cwd(), ".assets", "manifest.json");189190if (!fs.existsSync(manifestPath)) {191return { registered: false, message: "Manifest not found" };192}193194try {195const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));196const relativePath = path.relative(process.cwd(), filepath);197const found = manifest.assets?.find(198(a) => a.path === relativePath || a.path === filepath199);200201return {202registered: !!found,203message: found ? "Asset registered in manifest" : "Asset not in manifest",204asset: found,205};206} catch {207return { registered: false, message: "Error reading manifest" };208}209}210211/**212* Generate suggested filename213*/214function suggestFilename(original, parsed) {215if (!parsed) return null;216217const today = new Date().toISOString().slice(0, 10).replace(/-/g, "");218const type = parsed.type || "asset";219const campaign = parsed.campaign || "general";220const description = parsed.description || "untitled";221const ext = parsed.extension || "png";222223return `${type}_${campaign}_${description}_${today}.${ext}`;224}225226/**227* Format bytes to human readable228*/229function formatBytes(bytes) {230if (bytes === 0) return "0 Bytes";231const k = 1024;232const sizes = ["Bytes", "KB", "MB", "GB"];233const i = Math.floor(Math.log(bytes) / Math.log(k));234return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];235}236237/**238* Main validation function239*/240function validateAsset(assetPath) {241const results = {242path: assetPath,243filename: path.basename(assetPath),244valid: true,245issues: [],246warnings: [],247suggestions: [],248checks: {},249};250251// Check file exists252if (!fs.existsSync(assetPath)) {253results.valid = false;254results.issues.push(`File not found: ${assetPath}`);255return results;256}257258const filename = path.basename(assetPath);259const extension = path.extname(filename).slice(1).toLowerCase();260261// 1. Validate filename262const filenameResult = validateFilename(filename);263results.checks.filename = filenameResult;264if (!filenameResult.valid) {265results.issues.push(...filenameResult.issues);266results.suggestions.push(...filenameResult.suggestions);267}268269// 2. Validate format270const formatResult = validateFormat(extension);271results.checks.format = formatResult;272if (!formatResult.valid) {273results.issues.push(...formatResult.issues);274}275276// 3. Validate file size277const sizeResult = validateFileSize(assetPath, extension);278results.checks.fileSize = sizeResult;279if (!sizeResult.valid) {280results.issues.push(...sizeResult.issues);281}282results.warnings.push(...sizeResult.warnings);283284// 4. Check manifest registration285const manifestResult = checkManifest(assetPath);286results.checks.manifest = manifestResult;287if (!manifestResult.registered) {288results.warnings.push("Asset not registered in manifest.json");289results.suggestions.push(290"Register asset in .assets/manifest.json for tracking"291);292}293294// 5. Suggest corrected filename if needed295if (!filenameResult.valid && filenameResult.parsed) {296const suggested = suggestFilename(filename, filenameResult.parsed);297if (suggested) {298results.suggestions.push(`Suggested filename: ${suggested}`);299}300}301302// Overall validity303results.valid = results.issues.length === 0;304305return results;306}307308/**309* Format output for console310*/311function formatOutput(results) {312const lines = [];313314lines.push("\n" + "=".repeat(60));315lines.push(`ASSET VALIDATION: ${results.filename}`);316lines.push("=".repeat(60));317318lines.push(`\nStatus: ${results.valid ? "PASS" : "FAIL"}`);319lines.push(`Path: ${results.path}`);320321if (results.issues.length > 0) {322lines.push("\nISSUES:");323results.issues.forEach((issue) => lines.push(` - ${issue}`));324}325326if (results.warnings.length > 0) {327lines.push("\nWARNINGS:");328results.warnings.forEach((warning) => lines.push(` - ${warning}`));329}330331if (results.suggestions.length > 0) {332lines.push("\nSUGGESTIONS:");333results.suggestions.forEach((suggestion) =>334lines.push(` - ${suggestion}`)335);336}337338// File size info339if (results.checks.fileSize?.size) {340lines.push(`\nFile Size: ${formatBytes(results.checks.fileSize.size)}`);341}342343lines.push("\n" + "=".repeat(60));344345return lines.join("\n");346}347348/**349* Main350*/351function main() {352const args = process.argv.slice(2);353const jsonOutput = args.includes("--json");354const assetPath = args.find((a) => !a.startsWith("--"));355356if (!assetPath) {357console.error("Usage: node validate-asset.cjs <asset-path> [--json]");358console.error("\nExamples:");359console.error(360" node validate-asset.cjs assets/banners/social-media/banner_launch_hero_20251209.png"361);362console.error(363" node validate-asset.cjs assets/logos/icon-only/logo-icon.svg --json"364);365process.exit(1);366}367368// Resolve path369const resolvedPath = path.isAbsolute(assetPath)370? assetPath371: path.join(process.cwd(), assetPath);372373// Validate374const results = validateAsset(resolvedPath);375376// Output377if (jsonOutput) {378console.log(JSON.stringify(results, null, 2));379} else {380console.log(formatOutput(results));381}382383// Exit with appropriate code384process.exit(results.valid ? 0 : 1);385}386387main();388