145 lines
4.6 KiB
JavaScript
145 lines
4.6 KiB
JavaScript
import { readFileSync, writeFileSync } from 'node:fs';
|
|
|
|
/**
|
|
* Regenerates src/ui/provider-icons.ts from the upstream brand sets.
|
|
*
|
|
* Stores each mark as its viewBox plus the individual path data, so the component renders with
|
|
* <Svg>/<Path> instead of handing an XML string to SvgXml (which needs a full <svg> document and
|
|
* silently renders nothing otherwise).
|
|
*/
|
|
|
|
const https = await import('node:https');
|
|
|
|
const get = (url) =>
|
|
new Promise((resolve) => {
|
|
https
|
|
.get(url, { headers: { 'user-agent': 'node', accept: '*/*' } }, (res) => {
|
|
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
res.resume();
|
|
return get(new URL(res.headers.location, url).href).then(resolve);
|
|
}
|
|
let body = '';
|
|
res.on('data', (c) => (body += c));
|
|
res.on('end', () => resolve({ status: res.statusCode, body }));
|
|
})
|
|
.on('error', (e) => resolve({ status: 0, body: String(e.message) }));
|
|
});
|
|
|
|
/** providerId -> upstream icon */
|
|
const SOURCES = {
|
|
codex: 'lobehub:openai',
|
|
copilot: 'simple:github',
|
|
'kimi-code': 'lobehub:kimi',
|
|
'kimi-balance': 'lobehub:kimi',
|
|
openrouter: 'lobehub:openrouter',
|
|
minimax: 'lobehub:minimax',
|
|
zai: 'lobehub:zai',
|
|
runway: 'lobehub:runway',
|
|
poe: 'lobehub:poe',
|
|
deepseek: 'lobehub:deepseek',
|
|
'command-goat': 'lobehub:commandcode',
|
|
cline: 'lobehub:cline',
|
|
'openai-platform': 'lobehub:openai',
|
|
'anthropic-platform': 'lobehub:anthropic',
|
|
'opencode-go': 'lobehub:opencode',
|
|
opencode: 'lobehub:opencode',
|
|
claude: 'lobehub:claude',
|
|
cursor: 'lobehub:cursor',
|
|
};
|
|
|
|
/**
|
|
* Marks drawn for this app, for entries that are not a brand. `relay` stands for any API relay
|
|
* (New API, Sub2API, One API...): two opposing arrows, i.e. requests forwarded both ways.
|
|
*/
|
|
const LOCAL = {
|
|
relay: {
|
|
viewBox: '0 0 24 24',
|
|
paths: [{ d: 'M4 7h12V4l5 4-5 4V9H4zM20 15H8v-3l-5 4 5 4v-3h12z' }],
|
|
},
|
|
};
|
|
|
|
const urlFor = (ref) => {
|
|
const [kind, slug] = ref.split(':');
|
|
return kind === 'lobehub'
|
|
? `https://unpkg.com/@lobehub/icons-static-svg/icons/${slug}.svg`
|
|
: `https://raw.githubusercontent.com/simple-icons/simple-icons/develop/icons/${slug}.svg`;
|
|
};
|
|
|
|
const parse = (svg) => {
|
|
const root = svg.match(/<svg[^>]*>/)?.[0] ?? '';
|
|
const viewBox = root.match(/viewBox="([^"]+)"/)?.[1] ?? '0 0 24 24';
|
|
const paths = [];
|
|
for (const [, attrs] of svg.matchAll(/<path\b([^>]*)\/?>/g)) {
|
|
const d = attrs.match(/\bd="([^"]+)"/)?.[1];
|
|
if (!d) continue;
|
|
const rule = attrs.match(/fill-rule="([^"]+)"/)?.[1];
|
|
const extra = attrs
|
|
.replace(/\bd="[^"]*"/g, '')
|
|
.replace(/fill-rule="[^"]*"/g, '')
|
|
.replace(/clip-rule="[^"]*"/g, '')
|
|
.replace(/fill="[^"]*"/g, '')
|
|
.trim();
|
|
paths.push({ d, evenOdd: rule === 'evenodd', extra });
|
|
}
|
|
return { viewBox, paths };
|
|
};
|
|
|
|
const entries = [];
|
|
let warnings = 0;
|
|
|
|
for (const [id, ref] of Object.entries(SOURCES)) {
|
|
const res = await get(urlFor(ref));
|
|
if (res.status !== 200) {
|
|
console.log(`FAIL ${id} (${ref}) status ${res.status}`);
|
|
process.exitCode = 1;
|
|
continue;
|
|
}
|
|
const { viewBox, paths } = parse(res.body);
|
|
const noisy = paths.filter((p) => p.extra.length > 0);
|
|
if (noisy.length) {
|
|
warnings++;
|
|
console.log(`note ${id}: kept extra attributes on ${noisy.length} path(s)`);
|
|
}
|
|
entries.push({ id, viewBox, paths });
|
|
console.log(`ok ${id.padEnd(14)} viewBox=${viewBox} paths=${paths.length}`);
|
|
}
|
|
|
|
for (const [id, mark] of Object.entries(LOCAL)) {
|
|
entries.push({ id, ...mark });
|
|
console.log(`ok ${id.padEnd(14)} (drawn locally)`);
|
|
}
|
|
|
|
const data = entries
|
|
.map(
|
|
({ id, viewBox, paths }) =>
|
|
` ${JSON.stringify(id)}: {\n viewBox: ${JSON.stringify(viewBox)},\n paths: [\n` +
|
|
paths
|
|
.map((p) => ` { d: ${JSON.stringify(p.d)}${p.evenOdd ? ', evenOdd: true' : ''} },`)
|
|
.join('\n') +
|
|
`\n ],\n },`,
|
|
)
|
|
.join('\n');
|
|
|
|
const file = `/**
|
|
* Official provider marks, stored as structured path data so the component can render them with
|
|
* <Svg>/<Path> and tint them with the theme text colour (see ui/provider-icon.tsx).
|
|
*
|
|
* Sources: @lobehub/icons-static-svg (AI brand set) and simple-icons (GitHub); the API relay mark
|
|
* is drawn for this app. Brands remain the property of their owners; used here only to identify
|
|
* the provider.
|
|
*
|
|
* Regenerate with scripts/fetch-provider-icons.mjs when a brand updates its mark.
|
|
*/
|
|
export interface ProviderMark {
|
|
viewBox: string;
|
|
paths: { d: string; evenOdd?: boolean }[];
|
|
}
|
|
|
|
export const providerIcons: Record<string, ProviderMark> = {
|
|
${data}
|
|
};
|
|
`;
|
|
|
|
writeFileSync('src/ui/provider-icons.ts', file);
|
|
console.log(`\nwrote ${entries.length} marks, ${warnings} with extra attributes`);
|