/ scripts / gen-icons.mjs
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.')