Files
Netcatty/scripts/sync-docker-icons.mjs
陈大猫 36267717ac feat(terminal): add system manager side panel for processes, tmux, and Docker
Introduce workspace-aware System side panel with remote process/tmux/Docker management, terminal popup for interactive attach, capability warmup, review-hardened IPC, performance optimizations, toast action errors, and SSH channel recovery on reconnect.
2026-06-11 04:19:21 +08:00

194 lines
5.9 KiB
JavaScript

/**
* Manual one-off sync — NOT wired into build or pack.
* node scripts/sync-docker-icons.mjs
*
* Generates colored SVGs (Simple Icons, CC0) plus a few official brand assets
* into public/docker-icons/, then writes domain/systemManager/dockerIconBundled.ts.
*/
import { mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import * as simpleIcons from 'simple-icons';
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = join(__dirname, '..');
const SRC_FILE = join(ROOT, 'domain/systemManager/dockerImageIcons.ts');
const OUT_DIR = join(ROOT, 'public/docker-icons');
const MANIFEST_FILE = join(ROOT, 'domain/systemManager/dockerIconBundled.ts');
/**
* Official assets not covered by Simple Icons — see public/docker-icons/NOTICE.md
* Prefer SVG when the upstream project publishes one; PNG only when no SVG exists.
*/
const OFFICIAL_ICONS = [
{
iconId: 'nacos',
file: 'nacos.svg',
url: 'https://raw.githubusercontent.com/nacos-group/nacos-logo/master/Nacos%20logo%20%E7%99%BD%E8%93%9D%E8%B5%84%E6%BA%90%205.svg',
},
{
iconId: 'polaris',
file: 'polaris.svg',
url: 'https://raw.githubusercontent.com/polarismesh/polaris/main/logo.svg',
},
{
iconId: 'memcached',
file: 'memcached.png',
url: 'https://memcached.org/images/memcached_link_125.png',
},
];
const slugById = parseStringRecord(SRC_FILE, 'SIMPLE_ICONS_SLUG');
const styleById = parseTileStyles(SRC_FILE);
const iconsBySlug = new Map();
for (const icon of Object.values(simpleIcons)) {
if (icon && typeof icon === 'object' && 'slug' in icon && 'path' in icon) {
iconsBySlug.set(icon.slug, icon);
}
}
mkdirSync(OUT_DIR, { recursive: true });
const bundled = [];
const iconFiles = {};
const skipped = [];
for (const [iconId, slug] of Object.entries(slugById)) {
const brand = iconsBySlug.get(slug);
if (!brand) {
skipped.push({ iconId, slug, reason: 'slug not in simple-icons' });
continue;
}
const iconColor = styleById[iconId]?.iconColor ?? 'ffffff';
const fill = iconColor.startsWith('#') ? iconColor : `#${iconColor}`;
const file = `${iconId}.svg`;
const svg = [
`<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">`,
` <title>${escapeXml(brand.title)}</title>`,
` <path d="${brand.path}" fill="${fill}"/>`,
`</svg>`,
'',
].join('\n');
writeFileSync(join(OUT_DIR, file), svg, 'utf8');
bundled.push(iconId);
iconFiles[iconId] = file;
}
for (const entry of OFFICIAL_ICONS) {
const response = await fetch(entry.url);
if (!response.ok) {
throw new Error(`Failed to download ${entry.iconId} from ${entry.url}: ${response.status}`);
}
const buffer = Buffer.from(await response.arrayBuffer());
writeFileSync(join(OUT_DIR, entry.file), buffer);
bundled.push(entry.iconId);
iconFiles[entry.iconId] = entry.file;
console.log(`Downloaded official ${entry.iconId} -> ${entry.file}`);
}
const bundledSet = new Set(bundled);
for (const file of readdirSync(OUT_DIR)) {
if (file === 'NOTICE.md') continue;
const iconId = file.replace(/\.(svg|png|webp)$/i, '');
const owned =
bundledSet.has(iconId) && iconFiles[iconId] === file;
if (!owned) {
unlinkSync(join(OUT_DIR, file));
console.log(`Removed stale ${file}`);
}
}
bundled.sort();
const fileEntries = bundled
.filter((id) => iconFiles[id] !== `${id}.svg`)
.map((id) => ` '${id}': '${iconFiles[id]}',`);
writeFileSync(
MANIFEST_FILE,
[
'// Auto-generated by scripts/sync-docker-icons.mjs — do not edit.',
'export const BUNDLED_DOCKER_ICON_IDS = new Set<string>([',
...bundled.map((id) => ` '${id}',`),
']);',
'',
'/** Non-default filenames (e.g. official PNG assets). Default is `${id}.svg`. */',
'export const DOCKER_ICON_FILES: Record<string, string> = {',
...fileEntries,
'};',
'',
].join('\n'),
'utf8',
);
console.log(`Wrote ${bundled.length} icons to public/docker-icons/`);
console.log(`Updated ${MANIFEST_FILE}`);
if (skipped.length > 0) {
console.log(`Skipped ${skipped.length} (no official Simple Icons slug):`);
for (const item of skipped) {
console.log(` ${item.iconId} -> ${item.slug}`);
}
}
function parseStringRecord(filePath, constName) {
const src = readFileSync(filePath, 'utf8');
const start = src.indexOf(`const ${constName}`);
if (start < 0) throw new Error(`Could not find ${constName} in ${filePath}`);
const braceStart = src.indexOf('{', start);
const braceEnd = findMatchingBrace(src, braceStart);
const block = src.slice(braceStart + 1, braceEnd);
const out = {};
for (const line of block.split('\n')) {
const match = line.match(/^\s*([a-zA-Z0-9_]+):\s*'([^']*)',?\s*$/);
if (match) out[match[1]] = match[2];
}
return out;
}
function parseTileStyles(filePath) {
const src = readFileSync(filePath, 'utf8');
const start = src.indexOf('const ICON_TILE_STYLE');
if (start < 0) throw new Error(`Could not find ICON_TILE_STYLE in ${filePath}`);
const braceStart = src.indexOf('{', start);
const braceEnd = findMatchingBrace(src, braceStart);
const block = src.slice(braceStart + 1, braceEnd);
const out = {};
const re =
/^\s*([a-zA-Z0-9_]+):\s*\{\s*background:\s*'([^']*)',\s*iconColor:\s*'([^']*)'\s*\},?\s*$/;
for (const line of block.split('\n')) {
const match = line.match(re);
if (match) {
out[match[1]] = { background: match[2], iconColor: match[3] };
}
}
return out;
}
function findMatchingBrace(src, openIndex) {
let depth = 0;
for (let i = openIndex; i < src.length; i += 1) {
if (src[i] === '{') depth += 1;
else if (src[i] === '}') {
depth -= 1;
if (depth === 0) return i;
}
}
throw new Error('Unbalanced braces while parsing dockerImageIcons.ts');
}
function escapeXml(value) {
return value
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}