ui-pro-forge-cli 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +99 -0
- package/assets/data/app-interface.csv +31 -0
- package/assets/data/charts.csv +26 -0
- package/assets/data/colors.csv +194 -0
- package/assets/data/google-fonts.csv +1924 -0
- package/assets/data/icons.csv +106 -0
- package/assets/data/landing.csv +35 -0
- package/assets/data/motion.csv +17 -0
- package/assets/data/products.csv +193 -0
- package/assets/data/react-performance.csv +45 -0
- package/assets/data/stacks/angular.csv +51 -0
- package/assets/data/stacks/astro.csv +54 -0
- package/assets/data/stacks/avalonia.csv +57 -0
- package/assets/data/stacks/flutter.csv +53 -0
- package/assets/data/stacks/html-tailwind.csv +56 -0
- package/assets/data/stacks/javafx.csv +76 -0
- package/assets/data/stacks/jetpack-compose.csv +53 -0
- package/assets/data/stacks/laravel.csv +51 -0
- package/assets/data/stacks/nextjs.csv +53 -0
- package/assets/data/stacks/nuxt-ui.csv +71 -0
- package/assets/data/stacks/nuxtjs.csv +59 -0
- package/assets/data/stacks/react-native.csv +52 -0
- package/assets/data/stacks/react.csv +54 -0
- package/assets/data/stacks/shadcn.csv +61 -0
- package/assets/data/stacks/solidjs.csv +21 -0
- package/assets/data/stacks/svelte.csv +54 -0
- package/assets/data/stacks/swiftui.csv +51 -0
- package/assets/data/stacks/threejs.csv +54 -0
- package/assets/data/stacks/uno.csv +60 -0
- package/assets/data/stacks/vue.csv +50 -0
- package/assets/data/stacks/winui.csv +60 -0
- package/assets/data/styles.csv +86 -0
- package/assets/data/typography.csv +75 -0
- package/assets/data/ui-reasoning.csv +162 -0
- package/assets/data/ux-guidelines.csv +100 -0
- package/assets/scripts/core.py +463 -0
- package/assets/scripts/design_system.py +1371 -0
- package/assets/scripts/search.py +162 -0
- package/assets/scripts/tests/test_core.py +134 -0
- package/assets/scripts/validate_data.py +114 -0
- package/assets/skills/banner-design/SKILL.md +196 -0
- package/assets/skills/banner-design/references/banner-sizes-and-styles.md +118 -0
- package/assets/skills/brand/SKILL.md +97 -0
- package/assets/skills/brand/references/approval-checklist.md +169 -0
- package/assets/skills/brand/references/asset-organization.md +157 -0
- package/assets/skills/brand/references/brand-guideline-template.md +140 -0
- package/assets/skills/brand/references/color-palette-management.md +186 -0
- package/assets/skills/brand/references/consistency-checklist.md +94 -0
- package/assets/skills/brand/references/logo-usage-rules.md +185 -0
- package/assets/skills/brand/references/messaging-framework.md +85 -0
- package/assets/skills/brand/references/typography-specifications.md +214 -0
- package/assets/skills/brand/references/update.md +118 -0
- package/assets/skills/brand/references/visual-identity.md +96 -0
- package/assets/skills/brand/references/voice-framework.md +88 -0
- package/assets/skills/brand/scripts/extract-colors.cjs +341 -0
- package/assets/skills/brand/scripts/inject-brand-context.cjs +349 -0
- package/assets/skills/brand/scripts/sync-brand-to-tokens.cjs +248 -0
- package/assets/skills/brand/scripts/tests/test_sync_brand_to_tokens.py +52 -0
- package/assets/skills/brand/scripts/validate-asset.cjs +387 -0
- package/assets/skills/brand/templates/brand-guidelines-starter.md +275 -0
- package/assets/skills/design/SKILL.md +313 -0
- package/assets/skills/design/data/cip/deliverables.csv +51 -0
- package/assets/skills/design/data/cip/industries.csv +21 -0
- package/assets/skills/design/data/cip/mockup-contexts.csv +21 -0
- package/assets/skills/design/data/cip/styles.csv +21 -0
- package/assets/skills/design/data/icon/styles.csv +16 -0
- package/assets/skills/design/data/logo/colors.csv +56 -0
- package/assets/skills/design/data/logo/industries.csv +56 -0
- package/assets/skills/design/data/logo/styles.csv +56 -0
- package/assets/skills/design/references/banner-sizes-and-styles.md +118 -0
- package/assets/skills/design/references/cip-deliverable-guide.md +95 -0
- package/assets/skills/design/references/cip-design.md +121 -0
- package/assets/skills/design/references/cip-prompt-engineering.md +84 -0
- package/assets/skills/design/references/cip-style-guide.md +68 -0
- package/assets/skills/design/references/design-routing.md +207 -0
- package/assets/skills/design/references/icon-design.md +122 -0
- package/assets/skills/design/references/logo-color-psychology.md +101 -0
- package/assets/skills/design/references/logo-design.md +92 -0
- package/assets/skills/design/references/logo-prompt-engineering.md +158 -0
- package/assets/skills/design/references/logo-style-guide.md +109 -0
- package/assets/skills/design/references/slides-copywriting-formulas.md +84 -0
- package/assets/skills/design/references/slides-create.md +4 -0
- package/assets/skills/design/references/slides-html-template.md +295 -0
- package/assets/skills/design/references/slides-layout-patterns.md +137 -0
- package/assets/skills/design/references/slides-strategies.md +94 -0
- package/assets/skills/design/references/slides.md +42 -0
- package/assets/skills/design/references/social-photos-design.md +329 -0
- package/assets/skills/design/scripts/cip/core.py +215 -0
- package/assets/skills/design/scripts/cip/generate.py +484 -0
- package/assets/skills/design/scripts/cip/render-html.py +424 -0
- package/assets/skills/design/scripts/cip/search.py +127 -0
- package/assets/skills/design/scripts/icon/generate.py +487 -0
- package/assets/skills/design/scripts/logo/core.py +175 -0
- package/assets/skills/design/scripts/logo/generate.py +362 -0
- package/assets/skills/design/scripts/logo/search.py +114 -0
- package/assets/skills/design-system/SKILL.md +244 -0
- package/assets/skills/design-system/data/slide-backgrounds.csv +11 -0
- package/assets/skills/design-system/data/slide-charts.csv +26 -0
- package/assets/skills/design-system/data/slide-color-logic.csv +14 -0
- package/assets/skills/design-system/data/slide-copy.csv +26 -0
- package/assets/skills/design-system/data/slide-layout-logic.csv +16 -0
- package/assets/skills/design-system/data/slide-layouts.csv +26 -0
- package/assets/skills/design-system/data/slide-strategies.csv +16 -0
- package/assets/skills/design-system/data/slide-typography.csv +15 -0
- package/assets/skills/design-system/references/component-specs.md +236 -0
- package/assets/skills/design-system/references/component-tokens.md +214 -0
- package/assets/skills/design-system/references/primitive-tokens.md +203 -0
- package/assets/skills/design-system/references/semantic-tokens.md +215 -0
- package/assets/skills/design-system/references/states-and-variants.md +241 -0
- package/assets/skills/design-system/references/tailwind-integration.md +251 -0
- package/assets/skills/design-system/references/token-architecture.md +224 -0
- package/assets/skills/design-system/scripts/embed-tokens.cjs +99 -0
- package/assets/skills/design-system/scripts/fetch-background.py +317 -0
- package/assets/skills/design-system/scripts/generate-slide.py +770 -0
- package/assets/skills/design-system/scripts/generate-tokens.cjs +205 -0
- package/assets/skills/design-system/scripts/html-token-validator.py +327 -0
- package/assets/skills/design-system/scripts/search-slides.py +218 -0
- package/assets/skills/design-system/scripts/slide-token-validator.py +35 -0
- package/assets/skills/design-system/scripts/slide_search_core.py +453 -0
- package/assets/skills/design-system/scripts/tests/test_validate_tokens.py +48 -0
- package/assets/skills/design-system/scripts/validate-tokens.cjs +246 -0
- package/assets/skills/design-system/templates/design-tokens-starter.json +143 -0
- package/assets/skills/slides/SKILL.md +40 -0
- package/assets/skills/slides/references/copywriting-formulas.md +84 -0
- package/assets/skills/slides/references/create.md +4 -0
- package/assets/skills/slides/references/html-template.md +295 -0
- package/assets/skills/slides/references/layout-patterns.md +137 -0
- package/assets/skills/slides/references/slide-strategies.md +94 -0
- package/assets/skills/ui-styling/LICENSE.txt +202 -0
- package/assets/skills/ui-styling/SKILL.md +324 -0
- package/assets/skills/ui-styling/references/canvas-design-system.md +320 -0
- package/assets/skills/ui-styling/references/shadcn-accessibility.md +471 -0
- package/assets/skills/ui-styling/references/shadcn-components.md +424 -0
- package/assets/skills/ui-styling/references/shadcn-theming.md +373 -0
- package/assets/skills/ui-styling/references/tailwind-customization.md +483 -0
- package/assets/skills/ui-styling/references/tailwind-responsive.md +382 -0
- package/assets/skills/ui-styling/references/tailwind-utilities.md +455 -0
- package/assets/skills/ui-styling/scripts/requirements.txt +17 -0
- package/assets/skills/ui-styling/scripts/shadcn_add.py +308 -0
- package/assets/skills/ui-styling/scripts/tailwind_config_gen.py +473 -0
- package/assets/skills/ui-styling/scripts/tests/coverage-ui.json +1 -0
- package/assets/skills/ui-styling/scripts/tests/requirements.txt +3 -0
- package/assets/skills/ui-styling/scripts/tests/test_shadcn_add.py +266 -0
- package/assets/skills/ui-styling/scripts/tests/test_tailwind_config_gen.py +394 -0
- package/assets/templates/base/quick-reference.md +297 -0
- package/assets/templates/base/skill-content.md +384 -0
- package/assets/templates/platforms/agent.json +21 -0
- package/assets/templates/platforms/augment.json +18 -0
- package/assets/templates/platforms/claude.json +21 -0
- package/assets/templates/platforms/codebuddy.json +21 -0
- package/assets/templates/platforms/codewhale.json +21 -0
- package/assets/templates/platforms/codex.json +21 -0
- package/assets/templates/platforms/continue.json +21 -0
- package/assets/templates/platforms/copilot.json +21 -0
- package/assets/templates/platforms/cursor.json +21 -0
- package/assets/templates/platforms/droid.json +21 -0
- package/assets/templates/platforms/gemini.json +21 -0
- package/assets/templates/platforms/kilocode.json +21 -0
- package/assets/templates/platforms/kiro.json +21 -0
- package/assets/templates/platforms/opencode.json +21 -0
- package/assets/templates/platforms/qoder.json +21 -0
- package/assets/templates/platforms/roocode.json +21 -0
- package/assets/templates/platforms/trae.json +21 -0
- package/assets/templates/platforms/warp.json +18 -0
- package/assets/templates/platforms/windsurf.json +21 -0
- package/dist/commands/init.d.ts +11 -0
- package/dist/commands/init.js +165 -0
- package/dist/commands/uninstall.d.ts +7 -0
- package/dist/commands/uninstall.js +131 -0
- package/dist/commands/update.d.ts +7 -0
- package/dist/commands/update.js +73 -0
- package/dist/commands/versions.d.ts +5 -0
- package/dist/commands/versions.js +36 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +78 -0
- package/dist/types/index.d.ts +40 -0
- package/dist/types/index.js +25 -0
- package/dist/utils/detect.d.ts +8 -0
- package/dist/utils/detect.js +115 -0
- package/dist/utils/extract.d.ts +15 -0
- package/dist/utils/extract.js +125 -0
- package/dist/utils/github.d.ts +12 -0
- package/dist/utils/github.js +97 -0
- package/dist/utils/logger.d.ts +8 -0
- package/dist/utils/logger.js +9 -0
- package/dist/utils/template.d.ts +50 -0
- package/dist/utils/template.js +249 -0
- package/package.json +65 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* sync-brand-to-tokens.cjs
|
|
4
|
+
*
|
|
5
|
+
* Syncs brand-guidelines.md colors → design-tokens.json → design-tokens.css
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* node sync-brand-to-tokens.cjs
|
|
9
|
+
* node sync-brand-to-tokens.cjs --dry-run
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const { execFileSync } = require('child_process');
|
|
15
|
+
|
|
16
|
+
// Paths
|
|
17
|
+
const BRAND_GUIDELINES = 'docs/brand-guidelines.md';
|
|
18
|
+
const DESIGN_TOKENS_JSON = 'assets/design-tokens.json';
|
|
19
|
+
const DESIGN_TOKENS_CSS = 'assets/design-tokens.css';
|
|
20
|
+
const GENERATE_TOKENS_SCRIPT = '.claude/skills/design-system/scripts/generate-tokens.cjs';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Extract color info from brand guidelines markdown
|
|
24
|
+
*/
|
|
25
|
+
function extractColorsFromMarkdown(content) {
|
|
26
|
+
const colors = {
|
|
27
|
+
primary: { name: 'primary', shades: {} },
|
|
28
|
+
secondary: { name: 'secondary', shades: {} },
|
|
29
|
+
accent: { name: 'accent', shades: {} }
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Match a "| Label | #hex |" markdown table row. Bold around the label
|
|
33
|
+
// (**Label**) is optional, so this handles both the bundled starter template
|
|
34
|
+
// ("| Primary Blue | #2563EB |") and bolded variants.
|
|
35
|
+
const rowRe = /\|\s*\*{0,2}([^*|]+?)\*{0,2}\s*\|\s*#([A-Fa-f0-9]{6})\b/g;
|
|
36
|
+
|
|
37
|
+
// 1) Quick Reference table — hex only, no parenthesized name required.
|
|
38
|
+
const quickRef = {
|
|
39
|
+
primary: /Primary Color\s*\|\s*#([A-Fa-f0-9]{6})/i,
|
|
40
|
+
secondary: /Secondary Color\s*\|\s*#([A-Fa-f0-9]{6})/i,
|
|
41
|
+
accent: /Accent Color\s*\|\s*#([A-Fa-f0-9]{6})/i
|
|
42
|
+
};
|
|
43
|
+
for (const key of Object.keys(quickRef)) {
|
|
44
|
+
const m = content.match(quickRef[key]);
|
|
45
|
+
if (m) colors[key].base = `#${m[1]}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 2) Dedicated "### <Role> Colors" tables — assign base/dark/light by the
|
|
49
|
+
// row label keyword.
|
|
50
|
+
const assignFromSection = (heading, target) => {
|
|
51
|
+
const section = content.match(new RegExp(`### ${heading}[\\s\\S]*?(?=\\n###|$)`, 'i'));
|
|
52
|
+
if (!section) return;
|
|
53
|
+
for (const m of section[0].matchAll(rowRe)) {
|
|
54
|
+
const label = m[1].trim().toLowerCase();
|
|
55
|
+
const hex = `#${m[2]}`;
|
|
56
|
+
if (label.includes('dark')) target.dark = hex;
|
|
57
|
+
else if (label.includes('light')) target.light = hex;
|
|
58
|
+
else if (!target.base) target.base = hex;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
assignFromSection('Primary Colors', colors.primary);
|
|
62
|
+
assignFromSection('Secondary Colors', colors.secondary);
|
|
63
|
+
assignFromSection('Accent Colors', colors.accent);
|
|
64
|
+
|
|
65
|
+
// 3) Fallback: an accent swatch may live in another table (the starter
|
|
66
|
+
// lists "Accent Green" under Secondary Colors).
|
|
67
|
+
if (!colors.accent.base) {
|
|
68
|
+
for (const m of content.matchAll(rowRe)) {
|
|
69
|
+
if (m[1].trim().toLowerCase().includes('accent')) {
|
|
70
|
+
colors.accent.base = `#${m[2]}`;
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return colors;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Generate color scale from base color (simple approach)
|
|
81
|
+
*/
|
|
82
|
+
function generateColorScale(baseHex, darkHex, lightHex) {
|
|
83
|
+
// Use provided shades or generate approximations
|
|
84
|
+
return {
|
|
85
|
+
"50": { "$value": lightHex || adjustBrightness(baseHex, 0.9), "$type": "color" },
|
|
86
|
+
"100": { "$value": lightHex || adjustBrightness(baseHex, 0.8), "$type": "color" },
|
|
87
|
+
"200": { "$value": adjustBrightness(baseHex, 0.6), "$type": "color" },
|
|
88
|
+
"300": { "$value": adjustBrightness(baseHex, 0.4), "$type": "color" },
|
|
89
|
+
"400": { "$value": adjustBrightness(baseHex, 0.2), "$type": "color" },
|
|
90
|
+
"500": { "$value": baseHex, "$type": "color" },
|
|
91
|
+
"600": { "$value": darkHex || adjustBrightness(baseHex, -0.15), "$type": "color" },
|
|
92
|
+
"700": { "$value": adjustBrightness(baseHex, -0.3), "$type": "color" },
|
|
93
|
+
"800": { "$value": adjustBrightness(baseHex, -0.45), "$type": "color" },
|
|
94
|
+
"900": { "$value": adjustBrightness(baseHex, -0.6), "$type": "color" }
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Adjust hex color brightness
|
|
100
|
+
*/
|
|
101
|
+
function adjustBrightness(hex, percent) {
|
|
102
|
+
if (typeof hex !== 'string') return '#000000';
|
|
103
|
+
const num = parseInt(hex.replace('#', ''), 16);
|
|
104
|
+
const r = Math.min(255, Math.max(0, (num >> 16) + Math.round(255 * percent)));
|
|
105
|
+
const g = Math.min(255, Math.max(0, ((num >> 8) & 0x00FF) + Math.round(255 * percent)));
|
|
106
|
+
const b = Math.min(255, Math.max(0, (num & 0x0000FF) + Math.round(255 * percent)));
|
|
107
|
+
return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, '0').toUpperCase()}`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Update design tokens JSON
|
|
112
|
+
*/
|
|
113
|
+
function updateDesignTokens(tokens, colors) {
|
|
114
|
+
// Update brand name
|
|
115
|
+
const brandName = `Acme Marketing - ${colors.primary.name.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ')}`;
|
|
116
|
+
tokens.brand = brandName;
|
|
117
|
+
|
|
118
|
+
// Update primitive colors with new names
|
|
119
|
+
tokens.primitive = tokens.primitive || {};
|
|
120
|
+
const primitiveColors = tokens.primitive.color || {};
|
|
121
|
+
|
|
122
|
+
// Remove old color keys, add new ones
|
|
123
|
+
delete primitiveColors.coral;
|
|
124
|
+
delete primitiveColors.purple;
|
|
125
|
+
delete primitiveColors.mint;
|
|
126
|
+
|
|
127
|
+
// Add new named colors. Skip any role with no base hex rather than crashing
|
|
128
|
+
// on an unexpected guidelines format.
|
|
129
|
+
for (const role of ['primary', 'secondary', 'accent']) {
|
|
130
|
+
const c = colors[role];
|
|
131
|
+
if (!c.base) {
|
|
132
|
+
console.warn(`⚠️ No base hex found for ${role} color — skipping its token scale.`);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
primitiveColors[c.name] = generateColorScale(c.base, c.dark, c.light);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
tokens.primitive.color = primitiveColors;
|
|
139
|
+
|
|
140
|
+
// Update ALL semantic color references
|
|
141
|
+
if (tokens.semantic?.color) {
|
|
142
|
+
const sem = tokens.semantic.color;
|
|
143
|
+
const p = colors.primary.name;
|
|
144
|
+
const s = colors.secondary.name;
|
|
145
|
+
const a = colors.accent.name;
|
|
146
|
+
|
|
147
|
+
// Primary variants
|
|
148
|
+
sem.primary = { "$value": `{primitive.color.${p}.500}`, "$type": "color" };
|
|
149
|
+
sem['primary-hover'] = { "$value": `{primitive.color.${p}.600}`, "$type": "color" };
|
|
150
|
+
sem['primary-active'] = { "$value": `{primitive.color.${p}.700}`, "$type": "color" };
|
|
151
|
+
sem['primary-light'] = { "$value": `{primitive.color.${p}.400}`, "$type": "color" };
|
|
152
|
+
sem['primary-lighter'] = { "$value": `{primitive.color.${p}.100}`, "$type": "color" };
|
|
153
|
+
sem['primary-dark'] = { "$value": `{primitive.color.${p}.600}`, "$type": "color" };
|
|
154
|
+
|
|
155
|
+
// Secondary variants
|
|
156
|
+
sem.secondary = { "$value": `{primitive.color.${s}.500}`, "$type": "color" };
|
|
157
|
+
sem['secondary-hover'] = { "$value": `{primitive.color.${s}.600}`, "$type": "color" };
|
|
158
|
+
sem['secondary-light'] = { "$value": `{primitive.color.${s}.300}`, "$type": "color" };
|
|
159
|
+
sem['secondary-dark'] = { "$value": `{primitive.color.${s}.600}`, "$type": "color" };
|
|
160
|
+
|
|
161
|
+
// Accent variants
|
|
162
|
+
sem.accent = { "$value": `{primitive.color.${a}.500}`, "$type": "color" };
|
|
163
|
+
sem['accent-hover'] = { "$value": `{primitive.color.${a}.600}`, "$type": "color" };
|
|
164
|
+
sem['accent-light'] = { "$value": `{primitive.color.${a}.300}`, "$type": "color" };
|
|
165
|
+
|
|
166
|
+
// Status colors (use accent for success, primary for error/info)
|
|
167
|
+
sem.success = { "$value": `{primitive.color.${a}.500}`, "$type": "color" };
|
|
168
|
+
sem['success-light'] = { "$value": `{primitive.color.${a}.300}`, "$type": "color" };
|
|
169
|
+
sem.error = { "$value": `{primitive.color.${p}.500}`, "$type": "color" };
|
|
170
|
+
sem['error-light'] = { "$value": `{primitive.color.${p}.300}`, "$type": "color" };
|
|
171
|
+
sem.info = { "$value": `{primitive.color.${s}.500}`, "$type": "color" };
|
|
172
|
+
sem['info-light'] = { "$value": `{primitive.color.${s}.300}`, "$type": "color" };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Update component references (button uses primary color with opacity)
|
|
176
|
+
if (tokens.component?.button?.secondary && colors.primary.base) {
|
|
177
|
+
const primaryBase = colors.primary.base;
|
|
178
|
+
tokens.component.button.secondary['bg-hover'] = {
|
|
179
|
+
"$value": `${primaryBase}1A`,
|
|
180
|
+
"$type": "color"
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return tokens;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Main
|
|
189
|
+
*/
|
|
190
|
+
function main() {
|
|
191
|
+
const dryRun = process.argv.includes('--dry-run');
|
|
192
|
+
|
|
193
|
+
console.log('🔄 Syncing brand guidelines → design tokens\n');
|
|
194
|
+
|
|
195
|
+
// Read brand guidelines
|
|
196
|
+
const guidelinesPath = path.resolve(process.cwd(), BRAND_GUIDELINES);
|
|
197
|
+
if (!fs.existsSync(guidelinesPath)) {
|
|
198
|
+
console.error(`❌ Brand guidelines not found: ${guidelinesPath}`);
|
|
199
|
+
process.exit(1);
|
|
200
|
+
}
|
|
201
|
+
const guidelinesContent = fs.readFileSync(guidelinesPath, 'utf-8');
|
|
202
|
+
|
|
203
|
+
// Extract colors
|
|
204
|
+
const colors = extractColorsFromMarkdown(guidelinesContent);
|
|
205
|
+
console.log('📊 Extracted colors:');
|
|
206
|
+
console.log(` Primary: ${colors.primary.name} (${colors.primary.base})`);
|
|
207
|
+
console.log(` Secondary: ${colors.secondary.name} (${colors.secondary.base})`);
|
|
208
|
+
console.log(` Accent: ${colors.accent.name} (${colors.accent.base})\n`);
|
|
209
|
+
|
|
210
|
+
// Read existing tokens
|
|
211
|
+
const tokensPath = path.resolve(process.cwd(), DESIGN_TOKENS_JSON);
|
|
212
|
+
let tokens = {};
|
|
213
|
+
if (fs.existsSync(tokensPath)) {
|
|
214
|
+
tokens = JSON.parse(fs.readFileSync(tokensPath, 'utf-8'));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Update tokens
|
|
218
|
+
tokens = updateDesignTokens(tokens, colors);
|
|
219
|
+
|
|
220
|
+
if (dryRun) {
|
|
221
|
+
console.log('📋 Would update design-tokens.json:');
|
|
222
|
+
console.log(JSON.stringify(tokens.primitive.color, null, 2).slice(0, 500) + '...');
|
|
223
|
+
console.log('\n⏭️ Dry run - no files changed');
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Write updated tokens
|
|
228
|
+
fs.writeFileSync(tokensPath, JSON.stringify(tokens, null, 2));
|
|
229
|
+
console.log(`✅ Updated: ${DESIGN_TOKENS_JSON}`);
|
|
230
|
+
|
|
231
|
+
// Regenerate CSS
|
|
232
|
+
const generateScript = path.resolve(process.cwd(), GENERATE_TOKENS_SCRIPT);
|
|
233
|
+
if (fs.existsSync(generateScript)) {
|
|
234
|
+
try {
|
|
235
|
+
execFileSync('node', [generateScript, '--config', DESIGN_TOKENS_JSON, '-o', DESIGN_TOKENS_CSS], {
|
|
236
|
+
cwd: process.cwd(),
|
|
237
|
+
stdio: 'inherit'
|
|
238
|
+
});
|
|
239
|
+
console.log(`✅ Regenerated: ${DESIGN_TOKENS_CSS}`);
|
|
240
|
+
} catch (e) {
|
|
241
|
+
console.error('⚠️ Failed to regenerate CSS:', e.message);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
console.log('\n✨ Brand sync complete!');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
main();
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Regression test for sync-brand-to-tokens.cjs.
|
|
2
|
+
|
|
3
|
+
The color parser required a parenthesized name in the Quick Reference row
|
|
4
|
+
(`#2563EB (name)`) and a bolded label in the color tables (`**Primary Blue**`),
|
|
5
|
+
neither of which the bundled starter template uses. As a result the base hex
|
|
6
|
+
came back `undefined` and `adjustBrightness(undefined)` threw a TypeError —
|
|
7
|
+
i.e. the script crashed on its own documented happy path. This test runs the
|
|
8
|
+
sync against the bundled starter template and asserts it completes and writes
|
|
9
|
+
the expected base colors. It is pytest-based so the existing pytest CI runs it.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import shutil
|
|
14
|
+
import subprocess
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
import pytest
|
|
18
|
+
|
|
19
|
+
SCRIPTS = Path(__file__).resolve().parent.parent
|
|
20
|
+
SCRIPT = SCRIPTS / "sync-brand-to-tokens.cjs"
|
|
21
|
+
BRAND_STARTER = SCRIPTS.parent / "templates" / "brand-guidelines-starter.md"
|
|
22
|
+
TOKENS_STARTER = (
|
|
23
|
+
SCRIPTS.parent.parent / "design-system" / "templates" / "design-tokens-starter.json"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_sync_parses_bundled_starter_template(tmp_path):
|
|
28
|
+
node = shutil.which("node")
|
|
29
|
+
if not node:
|
|
30
|
+
pytest.skip("node not available")
|
|
31
|
+
|
|
32
|
+
(tmp_path / "docs").mkdir()
|
|
33
|
+
(tmp_path / "assets").mkdir()
|
|
34
|
+
shutil.copy(BRAND_STARTER, tmp_path / "docs" / "brand-guidelines.md")
|
|
35
|
+
shutil.copy(TOKENS_STARTER, tmp_path / "assets" / "design-tokens.json")
|
|
36
|
+
|
|
37
|
+
result = subprocess.run(
|
|
38
|
+
[node, str(SCRIPT)],
|
|
39
|
+
cwd=tmp_path,
|
|
40
|
+
capture_output=True,
|
|
41
|
+
text=True,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# Must not crash (the bug raised an unhandled TypeError).
|
|
45
|
+
assert "TypeError" not in result.stderr, result.stderr
|
|
46
|
+
assert result.returncode == 0, result.stderr + result.stdout
|
|
47
|
+
|
|
48
|
+
tokens = json.loads((tmp_path / "assets" / "design-tokens.json").read_text())
|
|
49
|
+
primitive = tokens["primitive"]["color"]
|
|
50
|
+
assert primitive["primary"]["500"]["$value"] == "#2563EB"
|
|
51
|
+
assert primitive["secondary"]["500"]["$value"] == "#8B5CF6"
|
|
52
|
+
assert primitive["accent"]["500"]["$value"] == "#10B981"
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* validate-asset.cjs
|
|
4
|
+
*
|
|
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> --json
|
|
11
|
+
* node validate-asset.cjs <asset-path> --fix
|
|
12
|
+
*
|
|
13
|
+
* For color validation of images, use with extract-colors.cjs
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const fs = require("fs");
|
|
17
|
+
const path = require("path");
|
|
18
|
+
|
|
19
|
+
// Validation rules
|
|
20
|
+
const RULES = {
|
|
21
|
+
naming: {
|
|
22
|
+
pattern: /^[a-z]+_[a-z0-9-]+_[a-z0-9-]+_\d{8}(_[a-z0-9-]+)?\.[a-z]+$/,
|
|
23
|
+
description:
|
|
24
|
+
"{type}_{campaign}_{description}_{timestamp}_{variant}.{ext}",
|
|
25
|
+
examples: [
|
|
26
|
+
"banner_claude-launch_hero-image_20251209.png",
|
|
27
|
+
"logo_brand-refresh_horizontal_20251209_dark.svg",
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
dimensions: {
|
|
31
|
+
banner: { minWidth: 600, minHeight: 300 },
|
|
32
|
+
logo: { minWidth: 100, minHeight: 100 },
|
|
33
|
+
design: { minWidth: 800, minHeight: 600 },
|
|
34
|
+
video: { minWidth: 640, minHeight: 480 },
|
|
35
|
+
default: { minWidth: 100, minHeight: 100 },
|
|
36
|
+
},
|
|
37
|
+
fileSize: {
|
|
38
|
+
image: { max: 5 * 1024 * 1024, recommended: 1 * 1024 * 1024 },
|
|
39
|
+
video: { max: 100 * 1024 * 1024, recommended: 50 * 1024 * 1024 },
|
|
40
|
+
svg: { max: 500 * 1024, recommended: 100 * 1024 },
|
|
41
|
+
},
|
|
42
|
+
formats: {
|
|
43
|
+
image: ["png", "jpg", "jpeg", "webp", "gif"],
|
|
44
|
+
vector: ["svg"],
|
|
45
|
+
video: ["mp4", "mov", "webm"],
|
|
46
|
+
document: ["pdf", "psd", "ai", "fig"],
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Parse asset filename
|
|
52
|
+
*/
|
|
53
|
+
function parseFilename(filename) {
|
|
54
|
+
const parts = filename.replace(/\.[^.]+$/, "").split("_");
|
|
55
|
+
|
|
56
|
+
if (parts.length < 4) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
type: parts[0],
|
|
62
|
+
campaign: parts[1],
|
|
63
|
+
description: parts[2],
|
|
64
|
+
timestamp: parts[3],
|
|
65
|
+
variant: parts.length > 4 ? parts[4] : null,
|
|
66
|
+
extension: path.extname(filename).slice(1).toLowerCase(),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Validate filename convention
|
|
72
|
+
*/
|
|
73
|
+
function validateFilename(filename) {
|
|
74
|
+
const issues = [];
|
|
75
|
+
const suggestions = [];
|
|
76
|
+
|
|
77
|
+
// Check pattern match
|
|
78
|
+
if (!RULES.naming.pattern.test(filename)) {
|
|
79
|
+
issues.push("Filename does not match naming convention");
|
|
80
|
+
suggestions.push(`Expected format: ${RULES.naming.description}`);
|
|
81
|
+
suggestions.push(`Examples: ${RULES.naming.examples.join(", ")}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Parse and check components
|
|
85
|
+
const parsed = parseFilename(filename);
|
|
86
|
+
if (parsed) {
|
|
87
|
+
// Check timestamp format
|
|
88
|
+
if (!/^\d{8}$/.test(parsed.timestamp)) {
|
|
89
|
+
issues.push("Timestamp should be YYYYMMDD format");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Check kebab-case for campaign and description
|
|
93
|
+
if (parsed.campaign && !/^[a-z0-9-]+$/.test(parsed.campaign)) {
|
|
94
|
+
issues.push("Campaign name should be kebab-case");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (parsed.description && !/^[a-z0-9-]+$/.test(parsed.description)) {
|
|
98
|
+
issues.push("Description should be kebab-case");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Check valid type
|
|
102
|
+
const validTypes = [
|
|
103
|
+
"banner",
|
|
104
|
+
"logo",
|
|
105
|
+
"design",
|
|
106
|
+
"video",
|
|
107
|
+
"infographic",
|
|
108
|
+
"icon",
|
|
109
|
+
"photo",
|
|
110
|
+
];
|
|
111
|
+
if (!validTypes.includes(parsed.type)) {
|
|
112
|
+
suggestions.push(`Consider using type: ${validTypes.join(", ")}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return { valid: issues.length === 0, issues, suggestions, parsed };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Validate file size
|
|
121
|
+
*/
|
|
122
|
+
function validateFileSize(filepath, extension) {
|
|
123
|
+
const issues = [];
|
|
124
|
+
const warnings = [];
|
|
125
|
+
|
|
126
|
+
const stats = fs.statSync(filepath);
|
|
127
|
+
const size = stats.size;
|
|
128
|
+
|
|
129
|
+
let limits;
|
|
130
|
+
if (RULES.formats.video.includes(extension)) {
|
|
131
|
+
limits = RULES.fileSize.video;
|
|
132
|
+
} else if (extension === "svg") {
|
|
133
|
+
limits = RULES.fileSize.svg;
|
|
134
|
+
} else {
|
|
135
|
+
limits = RULES.fileSize.image;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (size > limits.max) {
|
|
139
|
+
issues.push(
|
|
140
|
+
`File size (${formatBytes(size)}) exceeds maximum (${formatBytes(
|
|
141
|
+
limits.max
|
|
142
|
+
)})`
|
|
143
|
+
);
|
|
144
|
+
} else if (size > limits.recommended) {
|
|
145
|
+
warnings.push(
|
|
146
|
+
`File size (${formatBytes(size)}) exceeds recommended (${formatBytes(
|
|
147
|
+
limits.recommended
|
|
148
|
+
)})`
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return { valid: issues.length === 0, issues, warnings, size };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Validate file format
|
|
157
|
+
*/
|
|
158
|
+
function validateFormat(extension) {
|
|
159
|
+
const issues = [];
|
|
160
|
+
const info = { category: null };
|
|
161
|
+
|
|
162
|
+
const allFormats = [
|
|
163
|
+
...RULES.formats.image,
|
|
164
|
+
...RULES.formats.vector,
|
|
165
|
+
...RULES.formats.video,
|
|
166
|
+
...RULES.formats.document,
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
if (!allFormats.includes(extension)) {
|
|
170
|
+
issues.push(`Unsupported file format: .${extension}`);
|
|
171
|
+
return { valid: false, issues, info };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Determine category
|
|
175
|
+
if (RULES.formats.image.includes(extension)) info.category = "image";
|
|
176
|
+
else if (RULES.formats.vector.includes(extension)) info.category = "vector";
|
|
177
|
+
else if (RULES.formats.video.includes(extension)) info.category = "video";
|
|
178
|
+
else if (RULES.formats.document.includes(extension))
|
|
179
|
+
info.category = "document";
|
|
180
|
+
|
|
181
|
+
return { valid: true, issues, info };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Check if asset exists in manifest
|
|
186
|
+
*/
|
|
187
|
+
function checkManifest(filepath) {
|
|
188
|
+
const manifestPath = path.join(process.cwd(), ".assets", "manifest.json");
|
|
189
|
+
|
|
190
|
+
if (!fs.existsSync(manifestPath)) {
|
|
191
|
+
return { registered: false, message: "Manifest not found" };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
|
|
196
|
+
const relativePath = path.relative(process.cwd(), filepath);
|
|
197
|
+
const found = manifest.assets?.find(
|
|
198
|
+
(a) => a.path === relativePath || a.path === filepath
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
registered: !!found,
|
|
203
|
+
message: found ? "Asset registered in manifest" : "Asset not in manifest",
|
|
204
|
+
asset: found,
|
|
205
|
+
};
|
|
206
|
+
} catch {
|
|
207
|
+
return { registered: false, message: "Error reading manifest" };
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Generate suggested filename
|
|
213
|
+
*/
|
|
214
|
+
function suggestFilename(original, parsed) {
|
|
215
|
+
if (!parsed) return null;
|
|
216
|
+
|
|
217
|
+
const today = new Date().toISOString().slice(0, 10).replace(/-/g, "");
|
|
218
|
+
const type = parsed.type || "asset";
|
|
219
|
+
const campaign = parsed.campaign || "general";
|
|
220
|
+
const description = parsed.description || "untitled";
|
|
221
|
+
const ext = parsed.extension || "png";
|
|
222
|
+
|
|
223
|
+
return `${type}_${campaign}_${description}_${today}.${ext}`;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Format bytes to human readable
|
|
228
|
+
*/
|
|
229
|
+
function formatBytes(bytes) {
|
|
230
|
+
if (bytes === 0) return "0 Bytes";
|
|
231
|
+
const k = 1024;
|
|
232
|
+
const sizes = ["Bytes", "KB", "MB", "GB"];
|
|
233
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
234
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Main validation function
|
|
239
|
+
*/
|
|
240
|
+
function validateAsset(assetPath) {
|
|
241
|
+
const results = {
|
|
242
|
+
path: assetPath,
|
|
243
|
+
filename: path.basename(assetPath),
|
|
244
|
+
valid: true,
|
|
245
|
+
issues: [],
|
|
246
|
+
warnings: [],
|
|
247
|
+
suggestions: [],
|
|
248
|
+
checks: {},
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
// Check file exists
|
|
252
|
+
if (!fs.existsSync(assetPath)) {
|
|
253
|
+
results.valid = false;
|
|
254
|
+
results.issues.push(`File not found: ${assetPath}`);
|
|
255
|
+
return results;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const filename = path.basename(assetPath);
|
|
259
|
+
const extension = path.extname(filename).slice(1).toLowerCase();
|
|
260
|
+
|
|
261
|
+
// 1. Validate filename
|
|
262
|
+
const filenameResult = validateFilename(filename);
|
|
263
|
+
results.checks.filename = filenameResult;
|
|
264
|
+
if (!filenameResult.valid) {
|
|
265
|
+
results.issues.push(...filenameResult.issues);
|
|
266
|
+
results.suggestions.push(...filenameResult.suggestions);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// 2. Validate format
|
|
270
|
+
const formatResult = validateFormat(extension);
|
|
271
|
+
results.checks.format = formatResult;
|
|
272
|
+
if (!formatResult.valid) {
|
|
273
|
+
results.issues.push(...formatResult.issues);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// 3. Validate file size
|
|
277
|
+
const sizeResult = validateFileSize(assetPath, extension);
|
|
278
|
+
results.checks.fileSize = sizeResult;
|
|
279
|
+
if (!sizeResult.valid) {
|
|
280
|
+
results.issues.push(...sizeResult.issues);
|
|
281
|
+
}
|
|
282
|
+
results.warnings.push(...sizeResult.warnings);
|
|
283
|
+
|
|
284
|
+
// 4. Check manifest registration
|
|
285
|
+
const manifestResult = checkManifest(assetPath);
|
|
286
|
+
results.checks.manifest = manifestResult;
|
|
287
|
+
if (!manifestResult.registered) {
|
|
288
|
+
results.warnings.push("Asset not registered in manifest.json");
|
|
289
|
+
results.suggestions.push(
|
|
290
|
+
"Register asset in .assets/manifest.json for tracking"
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// 5. Suggest corrected filename if needed
|
|
295
|
+
if (!filenameResult.valid && filenameResult.parsed) {
|
|
296
|
+
const suggested = suggestFilename(filename, filenameResult.parsed);
|
|
297
|
+
if (suggested) {
|
|
298
|
+
results.suggestions.push(`Suggested filename: ${suggested}`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Overall validity
|
|
303
|
+
results.valid = results.issues.length === 0;
|
|
304
|
+
|
|
305
|
+
return results;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Format output for console
|
|
310
|
+
*/
|
|
311
|
+
function formatOutput(results) {
|
|
312
|
+
const lines = [];
|
|
313
|
+
|
|
314
|
+
lines.push("\n" + "=".repeat(60));
|
|
315
|
+
lines.push(`ASSET VALIDATION: ${results.filename}`);
|
|
316
|
+
lines.push("=".repeat(60));
|
|
317
|
+
|
|
318
|
+
lines.push(`\nStatus: ${results.valid ? "PASS" : "FAIL"}`);
|
|
319
|
+
lines.push(`Path: ${results.path}`);
|
|
320
|
+
|
|
321
|
+
if (results.issues.length > 0) {
|
|
322
|
+
lines.push("\nISSUES:");
|
|
323
|
+
results.issues.forEach((issue) => lines.push(` - ${issue}`));
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (results.warnings.length > 0) {
|
|
327
|
+
lines.push("\nWARNINGS:");
|
|
328
|
+
results.warnings.forEach((warning) => lines.push(` - ${warning}`));
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (results.suggestions.length > 0) {
|
|
332
|
+
lines.push("\nSUGGESTIONS:");
|
|
333
|
+
results.suggestions.forEach((suggestion) =>
|
|
334
|
+
lines.push(` - ${suggestion}`)
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// File size info
|
|
339
|
+
if (results.checks.fileSize?.size) {
|
|
340
|
+
lines.push(`\nFile Size: ${formatBytes(results.checks.fileSize.size)}`);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
lines.push("\n" + "=".repeat(60));
|
|
344
|
+
|
|
345
|
+
return lines.join("\n");
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Main
|
|
350
|
+
*/
|
|
351
|
+
function main() {
|
|
352
|
+
const args = process.argv.slice(2);
|
|
353
|
+
const jsonOutput = args.includes("--json");
|
|
354
|
+
const assetPath = args.find((a) => !a.startsWith("--"));
|
|
355
|
+
|
|
356
|
+
if (!assetPath) {
|
|
357
|
+
console.error("Usage: node validate-asset.cjs <asset-path> [--json]");
|
|
358
|
+
console.error("\nExamples:");
|
|
359
|
+
console.error(
|
|
360
|
+
" node validate-asset.cjs assets/banners/social-media/banner_launch_hero_20251209.png"
|
|
361
|
+
);
|
|
362
|
+
console.error(
|
|
363
|
+
" node validate-asset.cjs assets/logos/icon-only/logo-icon.svg --json"
|
|
364
|
+
);
|
|
365
|
+
process.exit(1);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Resolve path
|
|
369
|
+
const resolvedPath = path.isAbsolute(assetPath)
|
|
370
|
+
? assetPath
|
|
371
|
+
: path.join(process.cwd(), assetPath);
|
|
372
|
+
|
|
373
|
+
// Validate
|
|
374
|
+
const results = validateAsset(resolvedPath);
|
|
375
|
+
|
|
376
|
+
// Output
|
|
377
|
+
if (jsonOutput) {
|
|
378
|
+
console.log(JSON.stringify(results, null, 2));
|
|
379
|
+
} else {
|
|
380
|
+
console.log(formatOutput(results));
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Exit with appropriate code
|
|
384
|
+
process.exit(results.valid ? 0 : 1);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
main();
|