feat: introduce archify skill for generating architecture diagrams
- Added a new Archify skill, enabling users to create polished architecture, workflow, sequence, data-flow, and lifecycle diagrams. - Implemented comprehensive functionality including rendering, validation, and delivery of diagrams in various formats. - Integrated a user-friendly command-line interface for generating and previewing diagrams. - Developed supporting files including package.json, LICENSE, and SKILL.md for documentation and licensing. - Added unit tests to ensure reliability and functionality of the new skill. These changes enhance the application by providing a structured approach to visualizing system architecture and workflows, improving user experience and data representation.
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import * as simpleIcons from 'simple-icons';
|
||||
|
||||
const here = path.dirname(fileURLToPath(import.meta.url));
|
||||
const root = path.resolve(here, '..');
|
||||
const catalogPath = path.join(root, 'brand-marks', 'catalog.json');
|
||||
const outputPath = path.join(root, 'renderers', 'shared', 'generated-brand-marks.mjs');
|
||||
const catalog = JSON.parse(fs.readFileSync(catalogPath, 'utf8'));
|
||||
const simpleIconsVersion = JSON.parse(fs.readFileSync(
|
||||
path.join(root, 'node_modules', 'simple-icons', 'package.json'),
|
||||
'utf8',
|
||||
)).version;
|
||||
const simpleBySlug = new Map(Object.values(simpleIcons)
|
||||
.filter((icon) => icon && typeof icon === 'object' && icon.slug && icon.path)
|
||||
.map((icon) => [icon.slug, icon]));
|
||||
|
||||
function normalizedList(value) {
|
||||
return [...new Set((Array.isArray(value) ? value : [])
|
||||
.map((item) => String(item).trim())
|
||||
.filter(Boolean))];
|
||||
}
|
||||
|
||||
function lookupForms(value) {
|
||||
const raw = String(value ?? '').trim().toLocaleLowerCase('en-US');
|
||||
if (!raw) return [];
|
||||
return [...new Set([
|
||||
raw,
|
||||
raw.replace(/[\s_]+/g, '-'),
|
||||
raw.replace(/[\s_.-]+/g, ''),
|
||||
])];
|
||||
}
|
||||
|
||||
function fail(message) {
|
||||
console.error(`brand catalog: ${message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (catalog.schemaVersion !== 1 || !Array.isArray(catalog.marks) || catalog.marks.length === 0) {
|
||||
fail('catalog.json must contain a non-empty schemaVersion 1 marks array');
|
||||
}
|
||||
|
||||
const ids = new Set();
|
||||
const lookupKeys = new Map();
|
||||
const domains = new Map();
|
||||
const generated = catalog.marks.map((entry, index) => {
|
||||
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(entry.id || '')) fail(`marks[${index}] has an invalid id`);
|
||||
if (ids.has(entry.id)) fail(`duplicate id ${entry.id}`);
|
||||
ids.add(entry.id);
|
||||
|
||||
const aliases = normalizedList(entry.aliases);
|
||||
const entryDomains = normalizedList(entry.domains).map((domain) => domain.toLowerCase());
|
||||
for (const key of [entry.id, ...aliases]) {
|
||||
for (const form of lookupForms(key)) {
|
||||
if (lookupKeys.has(form) && lookupKeys.get(form) !== entry.id) {
|
||||
fail(`lookup key ${JSON.stringify(key)} is shared by ${lookupKeys.get(form)} and ${entry.id}`);
|
||||
}
|
||||
lookupKeys.set(form, entry.id);
|
||||
}
|
||||
}
|
||||
for (const domain of entryDomains) {
|
||||
if (domains.has(domain) && domains.get(domain) !== entry.id) {
|
||||
fail(`domain ${domain} is shared by ${domains.get(domain)} and ${entry.id}`);
|
||||
}
|
||||
domains.set(domain, entry.id);
|
||||
}
|
||||
|
||||
let mark;
|
||||
if (entry.simpleIcon) {
|
||||
const icon = simpleBySlug.get(entry.simpleIcon);
|
||||
if (!icon) fail(`${entry.id} references missing Simple Icons slug ${entry.simpleIcon}`);
|
||||
mark = {
|
||||
id: entry.id,
|
||||
title: entry.title || icon.title,
|
||||
category: entry.category,
|
||||
aliases,
|
||||
domains: entryDomains,
|
||||
viewBox: 24,
|
||||
hex: icon.hex,
|
||||
path: icon.path,
|
||||
provenance: {
|
||||
provider: 'Simple Icons',
|
||||
providerVersion: simpleIconsVersion,
|
||||
source: icon.source,
|
||||
...(icon.guidelines ? { guidelines: icon.guidelines } : {}),
|
||||
...(icon.license ? { license: icon.license } : {}),
|
||||
},
|
||||
};
|
||||
} else if (entry.custom) {
|
||||
const custom = entry.custom;
|
||||
if (!entry.title || !custom.path || !custom.source || !/^[0-9A-F]{6}$/i.test(custom.hex || '')) {
|
||||
fail(`${entry.id} custom mark requires title, path, source, and six-digit hex`);
|
||||
}
|
||||
mark = {
|
||||
id: entry.id,
|
||||
title: entry.title,
|
||||
category: entry.category,
|
||||
aliases,
|
||||
domains: entryDomains,
|
||||
viewBox: custom.viewBox || 24,
|
||||
hex: custom.hex.toUpperCase(),
|
||||
path: custom.path,
|
||||
provenance: {
|
||||
provider: 'Official brand asset',
|
||||
source: custom.source,
|
||||
...(custom.guidelines ? { guidelines: custom.guidelines } : {}),
|
||||
},
|
||||
};
|
||||
} else {
|
||||
fail(`${entry.id} must provide simpleIcon or custom`);
|
||||
}
|
||||
if (!mark.category || !mark.title) fail(`${entry.id} is missing category or title`);
|
||||
for (const form of lookupForms(mark.title)) {
|
||||
if (lookupKeys.has(form) && lookupKeys.get(form) !== entry.id) {
|
||||
fail(`title ${JSON.stringify(mark.title)} is shared by ${lookupKeys.get(form)} and ${entry.id}`);
|
||||
}
|
||||
lookupKeys.set(form, entry.id);
|
||||
}
|
||||
return mark;
|
||||
}).sort((left, right) => left.id.localeCompare(right.id));
|
||||
|
||||
const banner = `// Generated by scripts/generate-brand-marks.mjs from brand-marks/catalog.json.\n// Simple Icons ${simpleIconsVersion}. Do not edit by hand.\n`;
|
||||
const source = `${banner}export const BRAND_MARKS = Object.freeze(${JSON.stringify(generated, null, 2)});\n`;
|
||||
|
||||
if (process.argv.includes('--check')) {
|
||||
const current = fs.existsSync(outputPath)
|
||||
? fs.readFileSync(outputPath, 'utf8').replace(/\r\n?/g, '\n')
|
||||
: '';
|
||||
if (current !== source) {
|
||||
console.error('generated brand marks are stale — run npm run generate:brand-marks');
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
const temporary = `${outputPath}.${process.pid}.tmp`;
|
||||
fs.writeFileSync(temporary, source);
|
||||
fs.renameSync(temporary, outputPath);
|
||||
console.log(`generated ${path.relative(root, outputPath)} (${generated.length} marks)`);
|
||||
}
|
||||
Reference in New Issue
Block a user