gen-icons.mjs
1 #!/usr/bin/env node 2 3 import { spawnSync } from 'node:child_process' 4 import fs from 'node:fs' 5 import os from 'node:os' 6 import path from 'node:path' 7 import { fileURLToPath } from 'node:url' 8 9 const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') 10 const sourcePng = path.join(repoRoot, 'public', 'branding', 'swarmclaw-org-avatar.png') 11 const outDir = path.join(repoRoot, 'resources') 12 const outIcns = path.join(outDir, 'icon.icns') 13 const outIco = path.join(outDir, 'icon.ico') 14 const outPng = path.join(outDir, 'icon.png') 15 16 if (!fs.existsSync(sourcePng)) { 17 console.error(`[gen-icons] missing source ${sourcePng}`) 18 process.exit(1) 19 } 20 21 fs.mkdirSync(outDir, { recursive: true }) 22 23 function requireCmd(cmd) { 24 const probe = spawnSync('which', [cmd]) 25 if (probe.status !== 0) { 26 console.error(`[gen-icons] ${cmd} not found on PATH. Run this script on macOS with Xcode command line tools installed.`) 27 process.exit(1) 28 } 29 } 30 31 function run(cmd, args) { 32 const r = spawnSync(cmd, args, { stdio: 'inherit' }) 33 if (r.status !== 0) { 34 console.error(`[gen-icons] ${cmd} ${args.join(' ')} failed`) 35 process.exit(r.status ?? 1) 36 } 37 } 38 39 if (process.platform !== 'darwin') { 40 console.error('[gen-icons] this script requires macOS (uses sips + iconutil). Run it on a Mac and commit the generated files.') 41 process.exit(1) 42 } 43 44 requireCmd('sips') 45 requireCmd('iconutil') 46 47 console.log(`[gen-icons] source: ${sourcePng}`) 48 49 fs.copyFileSync(sourcePng, outPng) 50 console.log(`[gen-icons] wrote ${outPng}`) 51 52 const scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-iconset-')) 53 const iconset = path.join(scratch, 'icon.iconset') 54 fs.mkdirSync(iconset) 55 56 const sizes = [ 57 { name: 'icon_16x16.png', size: 16 }, 58 { name: 'icon_16x16@2x.png', size: 32 }, 59 { name: 'icon_32x32.png', size: 32 }, 60 { name: 'icon_32x32@2x.png', size: 64 }, 61 { name: 'icon_128x128.png', size: 128 }, 62 { name: 'icon_128x128@2x.png', size: 256 }, 63 { name: 'icon_256x256.png', size: 256 }, 64 { name: 'icon_256x256@2x.png', size: 512 }, 65 { name: 'icon_512x512.png', size: 512 }, 66 { name: 'icon_512x512@2x.png', size: 1024 }, 67 ] 68 for (const { name, size } of sizes) { 69 const dest = path.join(iconset, name) 70 run('sips', ['-z', String(size), String(size), sourcePng, '--out', dest]) 71 } 72 run('iconutil', ['-c', 'icns', iconset, '-o', outIcns]) 73 fs.rmSync(scratch, { recursive: true, force: true }) 74 console.log(`[gen-icons] wrote ${outIcns}`) 75 76 const pngToIco = await import('png-to-ico').then((m) => m.default ?? m).catch(() => null) 77 if (!pngToIco) { 78 console.error('[gen-icons] png-to-ico not installed. Run `npm i -D png-to-ico` and rerun.') 79 process.exit(1) 80 } 81 82 const icoScratch = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-ico-')) 83 const icoSizes = [16, 24, 32, 48, 64, 128, 256] 84 const icoInputs = [] 85 for (const size of icoSizes) { 86 const dest = path.join(icoScratch, `icon-${size}.png`) 87 run('sips', ['-z', String(size), String(size), sourcePng, '--out', dest]) 88 icoInputs.push(dest) 89 } 90 const icoBuf = await pngToIco(icoInputs) 91 fs.writeFileSync(outIco, icoBuf) 92 fs.rmSync(icoScratch, { recursive: true, force: true }) 93 console.log(`[gen-icons] wrote ${outIco}`) 94 95 console.log('[gen-icons] done.')