Add sub-skills from ui-ux-pro-max-skill repo
Added: - banner-design (new) - brand (new) - design-system (new) - slides (new) - ui-ux-pro-max data/scripts (from GitHub clone) - ui-styling data/scripts (from GitHub clone)
This commit is contained in:
387
skills/website-creator/brand/scripts/validate-asset.cjs
Executable file
387
skills/website-creator/brand/scripts/validate-asset.cjs
Executable file
@@ -0,0 +1,387 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* validate-asset.cjs
|
||||
*
|
||||
* Validates marketing assets against brand guidelines.
|
||||
* Checks: file naming, dimensions, file size, metadata.
|
||||
*
|
||||
* Usage:
|
||||
* node validate-asset.cjs <asset-path>
|
||||
* node validate-asset.cjs <asset-path> --json
|
||||
* node validate-asset.cjs <asset-path> --fix
|
||||
*
|
||||
* For color validation of images, use with extract-colors.cjs
|
||||
*/
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
// Validation rules
|
||||
const RULES = {
|
||||
naming: {
|
||||
pattern: /^[a-z]+_[a-z0-9-]+_[a-z0-9-]+_\d{8}(_[a-z0-9-]+)?\.[a-z]+$/,
|
||||
description:
|
||||
"{type}_{campaign}_{description}_{timestamp}_{variant}.{ext}",
|
||||
examples: [
|
||||
"banner_claude-launch_hero-image_20251209.png",
|
||||
"logo_brand-refresh_horizontal_20251209_dark.svg",
|
||||
],
|
||||
},
|
||||
dimensions: {
|
||||
banner: { minWidth: 600, minHeight: 300 },
|
||||
logo: { minWidth: 100, minHeight: 100 },
|
||||
design: { minWidth: 800, minHeight: 600 },
|
||||
video: { minWidth: 640, minHeight: 480 },
|
||||
default: { minWidth: 100, minHeight: 100 },
|
||||
},
|
||||
fileSize: {
|
||||
image: { max: 5 * 1024 * 1024, recommended: 1 * 1024 * 1024 },
|
||||
video: { max: 100 * 1024 * 1024, recommended: 50 * 1024 * 1024 },
|
||||
svg: { max: 500 * 1024, recommended: 100 * 1024 },
|
||||
},
|
||||
formats: {
|
||||
image: ["png", "jpg", "jpeg", "webp", "gif"],
|
||||
vector: ["svg"],
|
||||
video: ["mp4", "mov", "webm"],
|
||||
document: ["pdf", "psd", "ai", "fig"],
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse asset filename
|
||||
*/
|
||||
function parseFilename(filename) {
|
||||
const parts = filename.replace(/\.[^.]+$/, "").split("_");
|
||||
|
||||
if (parts.length < 4) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
type: parts[0],
|
||||
campaign: parts[1],
|
||||
description: parts[2],
|
||||
timestamp: parts[3],
|
||||
variant: parts.length > 4 ? parts[4] : null,
|
||||
extension: path.extname(filename).slice(1).toLowerCase(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate filename convention
|
||||
*/
|
||||
function validateFilename(filename) {
|
||||
const issues = [];
|
||||
const suggestions = [];
|
||||
|
||||
// Check pattern match
|
||||
if (!RULES.naming.pattern.test(filename)) {
|
||||
issues.push("Filename does not match naming convention");
|
||||
suggestions.push(`Expected format: ${RULES.naming.description}`);
|
||||
suggestions.push(`Examples: ${RULES.naming.examples.join(", ")}`);
|
||||
}
|
||||
|
||||
// Parse and check components
|
||||
const parsed = parseFilename(filename);
|
||||
if (parsed) {
|
||||
// Check timestamp format
|
||||
if (!/^\d{8}$/.test(parsed.timestamp)) {
|
||||
issues.push("Timestamp should be YYYYMMDD format");
|
||||
}
|
||||
|
||||
// Check kebab-case for campaign and description
|
||||
if (parsed.campaign && !/^[a-z0-9-]+$/.test(parsed.campaign)) {
|
||||
issues.push("Campaign name should be kebab-case");
|
||||
}
|
||||
|
||||
if (parsed.description && !/^[a-z0-9-]+$/.test(parsed.description)) {
|
||||
issues.push("Description should be kebab-case");
|
||||
}
|
||||
|
||||
// Check valid type
|
||||
const validTypes = [
|
||||
"banner",
|
||||
"logo",
|
||||
"design",
|
||||
"video",
|
||||
"infographic",
|
||||
"icon",
|
||||
"photo",
|
||||
];
|
||||
if (!validTypes.includes(parsed.type)) {
|
||||
suggestions.push(`Consider using type: ${validTypes.join(", ")}`);
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: issues.length === 0, issues, suggestions, parsed };
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate file size
|
||||
*/
|
||||
function validateFileSize(filepath, extension) {
|
||||
const issues = [];
|
||||
const warnings = [];
|
||||
|
||||
const stats = fs.statSync(filepath);
|
||||
const size = stats.size;
|
||||
|
||||
let limits;
|
||||
if (RULES.formats.video.includes(extension)) {
|
||||
limits = RULES.fileSize.video;
|
||||
} else if (extension === "svg") {
|
||||
limits = RULES.fileSize.svg;
|
||||
} else {
|
||||
limits = RULES.fileSize.image;
|
||||
}
|
||||
|
||||
if (size > limits.max) {
|
||||
issues.push(
|
||||
`File size (${formatBytes(size)}) exceeds maximum (${formatBytes(
|
||||
limits.max
|
||||
)})`
|
||||
);
|
||||
} else if (size > limits.recommended) {
|
||||
warnings.push(
|
||||
`File size (${formatBytes(size)}) exceeds recommended (${formatBytes(
|
||||
limits.recommended
|
||||
)})`
|
||||
);
|
||||
}
|
||||
|
||||
return { valid: issues.length === 0, issues, warnings, size };
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate file format
|
||||
*/
|
||||
function validateFormat(extension) {
|
||||
const issues = [];
|
||||
const info = { category: null };
|
||||
|
||||
const allFormats = [
|
||||
...RULES.formats.image,
|
||||
...RULES.formats.vector,
|
||||
...RULES.formats.video,
|
||||
...RULES.formats.document,
|
||||
];
|
||||
|
||||
if (!allFormats.includes(extension)) {
|
||||
issues.push(`Unsupported file format: .${extension}`);
|
||||
return { valid: false, issues, info };
|
||||
}
|
||||
|
||||
// Determine category
|
||||
if (RULES.formats.image.includes(extension)) info.category = "image";
|
||||
else if (RULES.formats.vector.includes(extension)) info.category = "vector";
|
||||
else if (RULES.formats.video.includes(extension)) info.category = "video";
|
||||
else if (RULES.formats.document.includes(extension))
|
||||
info.category = "document";
|
||||
|
||||
return { valid: true, issues, info };
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if asset exists in manifest
|
||||
*/
|
||||
function checkManifest(filepath) {
|
||||
const manifestPath = path.join(process.cwd(), ".assets", "manifest.json");
|
||||
|
||||
if (!fs.existsSync(manifestPath)) {
|
||||
return { registered: false, message: "Manifest not found" };
|
||||
}
|
||||
|
||||
try {
|
||||
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
|
||||
const relativePath = path.relative(process.cwd(), filepath);
|
||||
const found = manifest.assets?.find(
|
||||
(a) => a.path === relativePath || a.path === filepath
|
||||
);
|
||||
|
||||
return {
|
||||
registered: !!found,
|
||||
message: found ? "Asset registered in manifest" : "Asset not in manifest",
|
||||
asset: found,
|
||||
};
|
||||
} catch {
|
||||
return { registered: false, message: "Error reading manifest" };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate suggested filename
|
||||
*/
|
||||
function suggestFilename(original, parsed) {
|
||||
if (!parsed) return null;
|
||||
|
||||
const today = new Date().toISOString().slice(0, 10).replace(/-/g, "");
|
||||
const type = parsed.type || "asset";
|
||||
const campaign = parsed.campaign || "general";
|
||||
const description = parsed.description || "untitled";
|
||||
const ext = parsed.extension || "png";
|
||||
|
||||
return `${type}_${campaign}_${description}_${today}.${ext}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format bytes to human readable
|
||||
*/
|
||||
function formatBytes(bytes) {
|
||||
if (bytes === 0) return "0 Bytes";
|
||||
const k = 1024;
|
||||
const sizes = ["Bytes", "KB", "MB", "GB"];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
||||
}
|
||||
|
||||
/**
|
||||
* Main validation function
|
||||
*/
|
||||
function validateAsset(assetPath) {
|
||||
const results = {
|
||||
path: assetPath,
|
||||
filename: path.basename(assetPath),
|
||||
valid: true,
|
||||
issues: [],
|
||||
warnings: [],
|
||||
suggestions: [],
|
||||
checks: {},
|
||||
};
|
||||
|
||||
// Check file exists
|
||||
if (!fs.existsSync(assetPath)) {
|
||||
results.valid = false;
|
||||
results.issues.push(`File not found: ${assetPath}`);
|
||||
return results;
|
||||
}
|
||||
|
||||
const filename = path.basename(assetPath);
|
||||
const extension = path.extname(filename).slice(1).toLowerCase();
|
||||
|
||||
// 1. Validate filename
|
||||
const filenameResult = validateFilename(filename);
|
||||
results.checks.filename = filenameResult;
|
||||
if (!filenameResult.valid) {
|
||||
results.issues.push(...filenameResult.issues);
|
||||
results.suggestions.push(...filenameResult.suggestions);
|
||||
}
|
||||
|
||||
// 2. Validate format
|
||||
const formatResult = validateFormat(extension);
|
||||
results.checks.format = formatResult;
|
||||
if (!formatResult.valid) {
|
||||
results.issues.push(...formatResult.issues);
|
||||
}
|
||||
|
||||
// 3. Validate file size
|
||||
const sizeResult = validateFileSize(assetPath, extension);
|
||||
results.checks.fileSize = sizeResult;
|
||||
if (!sizeResult.valid) {
|
||||
results.issues.push(...sizeResult.issues);
|
||||
}
|
||||
results.warnings.push(...sizeResult.warnings);
|
||||
|
||||
// 4. Check manifest registration
|
||||
const manifestResult = checkManifest(assetPath);
|
||||
results.checks.manifest = manifestResult;
|
||||
if (!manifestResult.registered) {
|
||||
results.warnings.push("Asset not registered in manifest.json");
|
||||
results.suggestions.push(
|
||||
"Register asset in .assets/manifest.json for tracking"
|
||||
);
|
||||
}
|
||||
|
||||
// 5. Suggest corrected filename if needed
|
||||
if (!filenameResult.valid && filenameResult.parsed) {
|
||||
const suggested = suggestFilename(filename, filenameResult.parsed);
|
||||
if (suggested) {
|
||||
results.suggestions.push(`Suggested filename: ${suggested}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Overall validity
|
||||
results.valid = results.issues.length === 0;
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format output for console
|
||||
*/
|
||||
function formatOutput(results) {
|
||||
const lines = [];
|
||||
|
||||
lines.push("\n" + "=".repeat(60));
|
||||
lines.push(`ASSET VALIDATION: ${results.filename}`);
|
||||
lines.push("=".repeat(60));
|
||||
|
||||
lines.push(`\nStatus: ${results.valid ? "PASS" : "FAIL"}`);
|
||||
lines.push(`Path: ${results.path}`);
|
||||
|
||||
if (results.issues.length > 0) {
|
||||
lines.push("\nISSUES:");
|
||||
results.issues.forEach((issue) => lines.push(` - ${issue}`));
|
||||
}
|
||||
|
||||
if (results.warnings.length > 0) {
|
||||
lines.push("\nWARNINGS:");
|
||||
results.warnings.forEach((warning) => lines.push(` - ${warning}`));
|
||||
}
|
||||
|
||||
if (results.suggestions.length > 0) {
|
||||
lines.push("\nSUGGESTIONS:");
|
||||
results.suggestions.forEach((suggestion) =>
|
||||
lines.push(` - ${suggestion}`)
|
||||
);
|
||||
}
|
||||
|
||||
// File size info
|
||||
if (results.checks.fileSize?.size) {
|
||||
lines.push(`\nFile Size: ${formatBytes(results.checks.fileSize.size)}`);
|
||||
}
|
||||
|
||||
lines.push("\n" + "=".repeat(60));
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Main
|
||||
*/
|
||||
function main() {
|
||||
const args = process.argv.slice(2);
|
||||
const jsonOutput = args.includes("--json");
|
||||
const assetPath = args.find((a) => !a.startsWith("--"));
|
||||
|
||||
if (!assetPath) {
|
||||
console.error("Usage: node validate-asset.cjs <asset-path> [--json]");
|
||||
console.error("\nExamples:");
|
||||
console.error(
|
||||
" node validate-asset.cjs assets/banners/social-media/banner_launch_hero_20251209.png"
|
||||
);
|
||||
console.error(
|
||||
" node validate-asset.cjs assets/logos/icon-only/logo-icon.svg --json"
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Resolve path
|
||||
const resolvedPath = path.isAbsolute(assetPath)
|
||||
? assetPath
|
||||
: path.join(process.cwd(), assetPath);
|
||||
|
||||
// Validate
|
||||
const results = validateAsset(resolvedPath);
|
||||
|
||||
// Output
|
||||
if (jsonOutput) {
|
||||
console.log(JSON.stringify(results, null, 2));
|
||||
} else {
|
||||
console.log(formatOutput(results));
|
||||
}
|
||||
|
||||
// Exit with appropriate code
|
||||
process.exit(results.valid ? 0 : 1);
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user