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.
194 lines
5.9 KiB
JavaScript
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, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"');
|
|
}
|