@barnaby.build/barnaby 0.0.171 → 0.0.234

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,27 @@
1
+ const fs = require('node:fs')
2
+ const path = require('node:path')
3
+
4
+ /**
5
+ * Patch the packed app executable icon before artifacts (portable/nsis) are produced.
6
+ * This avoids mutating the final portable SFX, which rcedit corrupts.
7
+ */
8
+ exports.default = async function afterPack(context) {
9
+ if (context.electronPlatformName !== 'win32') return
10
+
11
+ const { rcedit } = await import('rcedit')
12
+ const exeName = `${context.packager.appInfo.productFilename}.exe`
13
+ const exePath = path.join(context.appOutDir, exeName)
14
+ const iconPath = path.join(context.packager.projectDir, 'build', 'icon.ico')
15
+
16
+ if (!fs.existsSync(iconPath)) {
17
+ console.warn('[afterPack] icon missing, skipping:', iconPath)
18
+ return
19
+ }
20
+ if (!fs.existsSync(exePath)) {
21
+ console.warn('[afterPack] app exe missing, skipping:', exePath)
22
+ return
23
+ }
24
+
25
+ await rcedit(exePath, { icon: iconPath })
26
+ console.log('[afterPack] Applied icon to', exePath)
27
+ }
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Generate a license key for a customer.
4
+ *
5
+ * Usage:
6
+ * node scripts/generate-license-key.mjs --email customer@example.com
7
+ * node scripts/generate-license-key.mjs --email customer@example.com --tier pro
8
+ * node scripts/generate-license-key.mjs --email customer@example.com --exp 2027-01-01
9
+ *
10
+ * Options:
11
+ * --email Customer email (required)
12
+ * --tier License tier: pro, enterprise (default: pro)
13
+ * --exp Expiry date in YYYY-MM-DD format (optional, omit for perpetual)
14
+ *
15
+ * Reads the private key from: ~/.barnaby/license-private-key.pem
16
+ * Run license-keypair-init.mjs first if you haven't generated a keypair.
17
+ */
18
+
19
+ import crypto from 'node:crypto'
20
+ import fs from 'node:fs'
21
+ import path from 'node:path'
22
+ import os from 'node:os'
23
+
24
+ const PRIVATE_KEY_PATH = path.join(os.homedir(), '.barnaby', 'license-private-key.pem')
25
+
26
+ // Parse args
27
+ const args = process.argv.slice(2)
28
+ function getArg(name) {
29
+ const idx = args.indexOf(`--${name}`)
30
+ if (idx === -1 || idx + 1 >= args.length) return undefined
31
+ return args[idx + 1]
32
+ }
33
+
34
+ const email = getArg('email')
35
+ const tier = getArg('tier') || 'pro'
36
+ const expStr = getArg('exp')
37
+
38
+ if (!email) {
39
+ console.error('❌ Missing --email argument')
40
+ console.error('Usage: node scripts/generate-license-key.mjs --email customer@example.com')
41
+ process.exit(1)
42
+ }
43
+
44
+ // Read private key
45
+ if (!fs.existsSync(PRIVATE_KEY_PATH)) {
46
+ console.error('❌ Private key not found at:', PRIVATE_KEY_PATH)
47
+ console.error(' Run: node scripts/license-keypair-init.mjs')
48
+ process.exit(1)
49
+ }
50
+
51
+ const privateKeyPem = fs.readFileSync(PRIVATE_KEY_PATH, 'utf8')
52
+ const privateKey = crypto.createPrivateKey(privateKeyPem)
53
+
54
+ // Build payload
55
+ const payload = {
56
+ email: email.toLowerCase().trim(),
57
+ product: 'orchestrator',
58
+ tier,
59
+ iat: Date.now(),
60
+ }
61
+
62
+ if (expStr) {
63
+ const expDate = new Date(expStr)
64
+ if (isNaN(expDate.getTime())) {
65
+ console.error('❌ Invalid --exp date format. Use YYYY-MM-DD.')
66
+ process.exit(1)
67
+ }
68
+ payload.exp = expDate.getTime()
69
+ }
70
+
71
+ // Encode payload
72
+ const payloadBuf = Buffer.from(JSON.stringify(payload), 'utf8')
73
+
74
+ // Sign
75
+ const signature = crypto.sign(null, payloadBuf, privateKey)
76
+
77
+ // Base64url encode (no padding)
78
+ function base64urlEncode(buf) {
79
+ return buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
80
+ }
81
+
82
+ const key = `ORCH-${base64urlEncode(payloadBuf)}.${base64urlEncode(signature)}`
83
+
84
+ console.log()
85
+ console.log('✅ License key generated for:', email)
86
+ console.log()
87
+ console.log('Details:')
88
+ console.log(' Email:', payload.email)
89
+ console.log(' Tier:', payload.tier)
90
+ console.log(' Issued:', new Date(payload.iat).toISOString())
91
+ console.log(' Expires:', payload.exp ? new Date(payload.exp).toISOString() : 'Never (perpetual)')
92
+ console.log()
93
+ console.log('License Key:')
94
+ console.log('─'.repeat(60))
95
+ console.log(key)
96
+ console.log('─'.repeat(60))
97
+ console.log()
98
+ console.log('Send this key to the customer. They paste it into')
99
+ console.log('Settings → Orchestrator → License Key.')
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * One-time script: Generate an ed25519 keypair for license key signing.
4
+ *
5
+ * Run: node scripts/license-keypair-init.mjs
6
+ *
7
+ * Output:
8
+ * - Prints the PUBLIC key PEM (embed in licenseKeys.ts)
9
+ * - Saves the PRIVATE key to ~/.barnaby/license-private-key.pem
10
+ *
11
+ * IMPORTANT: The private key file must NEVER be committed to version control.
12
+ * Back it up securely — if you lose it, you can't generate new keys.
13
+ */
14
+
15
+ import crypto from 'node:crypto'
16
+ import fs from 'node:fs'
17
+ import path from 'node:path'
18
+ import os from 'node:os'
19
+
20
+ const PRIVATE_KEY_PATH = path.join(os.homedir(), '.barnaby', 'license-private-key.pem')
21
+
22
+ // Check if keypair already exists
23
+ if (fs.existsSync(PRIVATE_KEY_PATH)) {
24
+ console.log('⚠️ Private key already exists at:', PRIVATE_KEY_PATH)
25
+ console.log(' Delete it first if you want to generate a new keypair.')
26
+ console.log()
27
+
28
+ // Show the existing public key for reference
29
+ const existingPrivate = fs.readFileSync(PRIVATE_KEY_PATH, 'utf8')
30
+ const privateKey = crypto.createPrivateKey(existingPrivate)
31
+ const publicKeyPem = crypto.createPublicKey(privateKey).export({ type: 'spki', format: 'pem' })
32
+ console.log('Existing PUBLIC key (already in licenseKeys.ts):')
33
+ console.log(publicKeyPem)
34
+ process.exit(0)
35
+ }
36
+
37
+ // Generate new ed25519 keypair
38
+ const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519')
39
+
40
+ const publicKeyPem = publicKey.export({ type: 'spki', format: 'pem' })
41
+ const privateKeyPem = privateKey.export({ type: 'pkcs8', format: 'pem' })
42
+
43
+ // Save private key
44
+ const dir = path.dirname(PRIVATE_KEY_PATH)
45
+ fs.mkdirSync(dir, { recursive: true })
46
+ fs.writeFileSync(PRIVATE_KEY_PATH, privateKeyPem, { mode: 0o600 })
47
+
48
+ console.log('✅ Ed25519 keypair generated successfully!')
49
+ console.log()
50
+ console.log('PRIVATE key saved to:', PRIVATE_KEY_PATH)
51
+ console.log('⚠️ NEVER commit this file. Back it up securely.')
52
+ console.log()
53
+ console.log('PUBLIC key — copy this into electron/main/licenseKeys.ts:')
54
+ console.log('─'.repeat(60))
55
+ console.log(publicKeyPem)
56
+ console.log('─'.repeat(60))
@@ -1,14 +1,14 @@
1
- import fs from 'fs'
2
-
3
- const p = 'C:/Users/stuar/AppData/Roaming/Barnaby/.storage/app-state.json'
4
- const d = JSON.parse(fs.readFileSync(p, 'utf8'))
5
- const s = d.state
6
- if (!s.themeOverrides) s.themeOverrides = {}
7
- if (!s.themeOverrides['nord-light']) s.themeOverrides['nord-light'] = {}
8
- Object.assign(s.themeOverrides['nord-light'], {
9
- accentSoft: '#f0f6ff',
10
- assistantBubbleBgLight: '#f0f0f0',
11
- thinkingProgress: '#082cdd',
12
- })
13
- fs.writeFileSync(p, JSON.stringify(d, null, 2))
14
- console.log('nord-light overrides patched')
1
+ import fs from 'fs'
2
+
3
+ const p = 'C:/Users/stuar/AppData/Roaming/Barnaby/.storage/app-state.json'
4
+ const d = JSON.parse(fs.readFileSync(p, 'utf8'))
5
+ const s = d.state
6
+ if (!s.themeOverrides) s.themeOverrides = {}
7
+ if (!s.themeOverrides['nord-light']) s.themeOverrides['nord-light'] = {}
8
+ Object.assign(s.themeOverrides['nord-light'], {
9
+ accentSoft: '#f0f6ff',
10
+ assistantBubbleBgLight: '#f0f0f0',
11
+ thinkingProgress: '#082cdd',
12
+ })
13
+ fs.writeFileSync(p, JSON.stringify(d, null, 2))
14
+ console.log('nord-light overrides patched')
@@ -1,47 +1,47 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Best-effort rebuild of native Node modules (node-pty) for Electron.
4
- * Runs as part of postinstall. Fails silently if build tools are missing.
5
- *
6
- * Requirements for a successful rebuild:
7
- * - Python 3.x on PATH
8
- * - C++ build tools (Visual Studio on Windows, Xcode CLT on macOS, gcc on Linux)
9
- *
10
- * If these are missing, node-pty's N-API prebuilds may still work at runtime.
11
- */
12
- import { execSync } from 'node:child_process'
13
- import { existsSync } from 'node:fs'
14
- import { join, dirname } from 'node:path'
15
- import { fileURLToPath } from 'node:url'
16
-
17
- const __dirname = dirname(fileURLToPath(import.meta.url))
18
- const root = join(__dirname, '..')
19
-
20
- const nodePtyDir = join(root, 'node_modules', 'node-pty')
21
- if (!existsSync(nodePtyDir)) {
22
- // node-pty not installed (optional dep) — nothing to do
23
- process.exit(0)
24
- }
25
-
26
- const electronRebuild = join(root, 'node_modules', '.bin', 'electron-rebuild')
27
- if (!existsSync(electronRebuild) && !existsSync(electronRebuild + '.cmd')) {
28
- process.exit(0)
29
- }
30
-
31
- try {
32
- console.log('[postinstall] Rebuilding native modules for Electron...')
33
- execSync(`"${electronRebuild}" -m "${nodePtyDir}"`, {
34
- cwd: root,
35
- stdio: 'inherit',
36
- timeout: 120_000,
37
- })
38
- console.log('[postinstall] Native modules rebuilt successfully.')
39
- } catch {
40
- console.log(
41
- '[postinstall] Native module rebuild skipped (missing Python or C++ build tools).\n' +
42
- ' The built-in terminal may not work. To fix:\n' +
43
- ' 1. Install Python 3: https://www.python.org/downloads/\n' +
44
- ' 2. Install C++ build tools (Visual Studio or `npm install -g windows-build-tools`)\n' +
45
- ' 3. Run: npx electron-rebuild'
46
- )
47
- }
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Best-effort rebuild of native Node modules (node-pty) for Electron.
4
+ * Runs as part of postinstall. Fails silently if build tools are missing.
5
+ *
6
+ * Requirements for a successful rebuild:
7
+ * - Python 3.x on PATH
8
+ * - C++ build tools (Visual Studio on Windows, Xcode CLT on macOS, gcc on Linux)
9
+ *
10
+ * If these are missing, node-pty's N-API prebuilds may still work at runtime.
11
+ */
12
+ import { execSync } from 'node:child_process'
13
+ import { existsSync } from 'node:fs'
14
+ import { join, dirname } from 'node:path'
15
+ import { fileURLToPath } from 'node:url'
16
+
17
+ const __dirname = dirname(fileURLToPath(import.meta.url))
18
+ const root = join(__dirname, '..')
19
+
20
+ const nodePtyDir = join(root, 'node_modules', 'node-pty')
21
+ if (!existsSync(nodePtyDir)) {
22
+ // node-pty not installed (optional dep) — nothing to do
23
+ process.exit(0)
24
+ }
25
+
26
+ const electronRebuild = join(root, 'node_modules', '.bin', 'electron-rebuild')
27
+ if (!existsSync(electronRebuild) && !existsSync(electronRebuild + '.cmd')) {
28
+ process.exit(0)
29
+ }
30
+
31
+ try {
32
+ console.log('[postinstall] Rebuilding native modules for Electron...')
33
+ execSync(`"${electronRebuild}" -m "${nodePtyDir}"`, {
34
+ cwd: root,
35
+ stdio: 'inherit',
36
+ timeout: 120_000,
37
+ })
38
+ console.log('[postinstall] Native modules rebuilt successfully.')
39
+ } catch {
40
+ console.log(
41
+ '[postinstall] Native module rebuild skipped (missing Python or C++ build tools).\n' +
42
+ ' The built-in terminal may not work. To fix:\n' +
43
+ ' 1. Install Python 3: https://www.python.org/downloads/\n' +
44
+ ' 2. Install C++ build tools (Visual Studio or `npm install -g windows-build-tools`)\n' +
45
+ ' 3. Run: npx electron-rebuild'
46
+ )
47
+ }
@@ -0,0 +1,80 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { spawn } from 'node:child_process'
4
+
5
+ const command = process.argv.slice(2).join(' ').trim()
6
+ if (!command) {
7
+ console.error('Usage: node scripts/run-with-version-bump.mjs "<command>"')
8
+ process.exit(1)
9
+ }
10
+
11
+ const root = process.cwd()
12
+ const packageJsonPath = path.join(root, 'package.json')
13
+ const packageLockPath = path.join(root, 'package-lock.json')
14
+
15
+ const originalPackageJsonText = fs.readFileSync(packageJsonPath, 'utf8')
16
+ const hadPackageLock = fs.existsSync(packageLockPath)
17
+ const originalPackageLockText = hadPackageLock ? fs.readFileSync(packageLockPath, 'utf8') : null
18
+
19
+ function bumpPatch(version) {
20
+ const parts = String(version).trim().split('.')
21
+ if (parts.length !== 3) {
22
+ throw new Error(`Expected semantic version x.y.z, got "${version}"`)
23
+ }
24
+ const [major, minor, patch] = parts.map((p) => Number(p))
25
+ if ([major, minor, patch].some((n) => Number.isNaN(n))) {
26
+ throw new Error(`Expected numeric semantic version, got "${version}"`)
27
+ }
28
+ return `${major}.${minor}.${patch + 1}`
29
+ }
30
+
31
+ function restoreVersionFiles() {
32
+ fs.writeFileSync(packageJsonPath, originalPackageJsonText, 'utf8')
33
+ if (originalPackageLockText === null) {
34
+ if (fs.existsSync(packageLockPath)) fs.rmSync(packageLockPath)
35
+ return
36
+ }
37
+ fs.writeFileSync(packageLockPath, originalPackageLockText, 'utf8')
38
+ }
39
+
40
+ const packageJson = JSON.parse(originalPackageJsonText)
41
+ const currentVersion = packageJson.version
42
+ const nextVersion = bumpPatch(currentVersion)
43
+ packageJson.version = nextVersion
44
+ fs.writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`, 'utf8')
45
+
46
+ if (hadPackageLock) {
47
+ const packageLock = JSON.parse(originalPackageLockText)
48
+ if (typeof packageLock.version === 'string') packageLock.version = nextVersion
49
+ if (packageLock.packages && packageLock.packages[''] && typeof packageLock.packages[''].version === 'string') {
50
+ packageLock.packages[''].version = nextVersion
51
+ }
52
+ fs.writeFileSync(packageLockPath, `${JSON.stringify(packageLock, null, 2)}\n`, 'utf8')
53
+ }
54
+
55
+ console.log(`Version bumped: ${currentVersion} -> ${nextVersion}`)
56
+
57
+ const child = spawn(command, {
58
+ cwd: root,
59
+ env: process.env,
60
+ shell: true,
61
+ stdio: 'inherit'
62
+ })
63
+
64
+ child.on('error', (error) => {
65
+ console.error(`Failed to start command: ${error.message}`)
66
+ restoreVersionFiles()
67
+ process.exit(1)
68
+ })
69
+
70
+ child.on('exit', (code, signal) => {
71
+ if (code === 0) process.exit(0)
72
+
73
+ restoreVersionFiles()
74
+ if (signal) {
75
+ console.error(`Command terminated by signal: ${signal}`)
76
+ } else {
77
+ console.error(`Command failed with exit code ${code}`)
78
+ }
79
+ process.exit(code ?? 1)
80
+ })
@@ -1,57 +1,57 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Creates a Start menu shortcut for Barnaby on Windows (global install only).
4
- * Fixes: (1) App not findable in Windows search (2) Taskbar pinning shows Electron.
5
- * Tip: Pin the "Barnaby" shortcut from Start menu to taskbar for correct icon.
6
- */
7
- import { platform } from 'node:os'
8
- import { existsSync, writeFileSync } from 'node:fs'
9
- import { dirname, join } from 'node:path'
10
- import { fileURLToPath } from 'node:url'
11
- import { execSync } from 'node:child_process'
12
-
13
- function main() {
14
- if (platform() !== 'win32') return
15
- const force = process.argv.includes('--force')
16
- if (!force && process.env.npm_config_global !== 'true') return
17
-
18
- const __dirname = dirname(fileURLToPath(import.meta.url))
19
- const pkgRoot = join(__dirname, '..')
20
-
21
- // Use node + our script directly (barnaby.cmd may not exist yet when postinstall runs)
22
- const barnabyScript = join(pkgRoot, 'bin', 'barnaby.cjs')
23
- if (!existsSync(barnabyScript)) return
24
- const nodeExe = process.execPath
25
-
26
- // Prefer .ico for Windows shortcuts (PNG often fails)
27
- const iconPath = [
28
- join(pkgRoot, 'build', 'icon.ico'),
29
- join(pkgRoot, 'dist', 'favicon.ico'),
30
- join(pkgRoot, 'public', 'favicon.ico'),
31
- join(pkgRoot, 'dist', 'appicon.png'),
32
- join(pkgRoot, 'public', 'appicon.png')
33
- ].find(existsSync)
34
- const startMenuDir = join(process.env.APPDATA || '', 'Microsoft', 'Windows', 'Start Menu', 'Programs')
35
- const shortcutPath = join(startMenuDir, 'Barnaby.lnk')
36
-
37
- const ps = `
38
- $WshShell = New-Object -ComObject WScript.Shell
39
- $Shortcut = $WshShell.CreateShortcut('${shortcutPath.replace(/'/g, "''")}')
40
- $Shortcut.TargetPath = '${nodeExe.replace(/'/g, "''")}'
41
- $Shortcut.Arguments = '${barnabyScript.replace(/'/g, "''")}'
42
- $Shortcut.WorkingDirectory = '${(process.env.USERPROFILE || '').replace(/'/g, "''")}'
43
- $Shortcut.Description = 'Barnaby - AI agent IDE'
44
- ${iconPath ? `if (Test-Path '${iconPath.replace(/'/g, "''")}') { $Shortcut.IconLocation = '${iconPath.replace(/'/g, "''")},0' }` : ''}
45
- $Shortcut.Save()
46
- `
47
- try {
48
- const tmp = join(process.env.TEMP || '', 'barnaby-shortcut.ps1')
49
- writeFileSync(tmp, ps.trim(), 'utf8')
50
- execSync(`powershell -NoProfile -ExecutionPolicy Bypass -File "${tmp}"`, { stdio: 'pipe' })
51
- console.log('[Barnaby] Start menu shortcut created. Pin it to taskbar for the correct icon.')
52
- } catch {
53
- // Silent fail - shortcut is optional
54
- }
55
- }
56
-
57
- main()
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Creates a Start menu shortcut for Barnaby on Windows (global install only).
4
+ * Fixes: (1) App not findable in Windows search (2) Taskbar pinning shows Electron.
5
+ * Tip: Pin the "Barnaby" shortcut from Start menu to taskbar for correct icon.
6
+ */
7
+ import { platform } from 'node:os'
8
+ import { existsSync, writeFileSync } from 'node:fs'
9
+ import { dirname, join } from 'node:path'
10
+ import { fileURLToPath } from 'node:url'
11
+ import { execSync } from 'node:child_process'
12
+
13
+ function main() {
14
+ if (platform() !== 'win32') return
15
+ const force = process.argv.includes('--force')
16
+ if (!force && process.env.npm_config_global !== 'true') return
17
+
18
+ const __dirname = dirname(fileURLToPath(import.meta.url))
19
+ const pkgRoot = join(__dirname, '..')
20
+
21
+ // Use node + our script directly (barnaby.cmd may not exist yet when postinstall runs)
22
+ const barnabyScript = join(pkgRoot, 'bin', 'barnaby.cjs')
23
+ if (!existsSync(barnabyScript)) return
24
+ const nodeExe = process.execPath
25
+
26
+ // Prefer .ico for Windows shortcuts (PNG often fails)
27
+ const iconPath = [
28
+ join(pkgRoot, 'build', 'icon.ico'),
29
+ join(pkgRoot, 'dist', 'favicon.ico'),
30
+ join(pkgRoot, 'public', 'favicon.ico'),
31
+ join(pkgRoot, 'dist', 'appicon.png'),
32
+ join(pkgRoot, 'public', 'appicon.png')
33
+ ].find(existsSync)
34
+ const startMenuDir = join(process.env.APPDATA || '', 'Microsoft', 'Windows', 'Start Menu', 'Programs')
35
+ const shortcutPath = join(startMenuDir, 'Barnaby.lnk')
36
+
37
+ const ps = `
38
+ $WshShell = New-Object -ComObject WScript.Shell
39
+ $Shortcut = $WshShell.CreateShortcut('${shortcutPath.replace(/'/g, "''")}')
40
+ $Shortcut.TargetPath = '${nodeExe.replace(/'/g, "''")}'
41
+ $Shortcut.Arguments = '${barnabyScript.replace(/'/g, "''")}'
42
+ $Shortcut.WorkingDirectory = '${(process.env.USERPROFILE || '').replace(/'/g, "''")}'
43
+ $Shortcut.Description = 'Barnaby - AI agent IDE'
44
+ ${iconPath ? `if (Test-Path '${iconPath.replace(/'/g, "''")}') { $Shortcut.IconLocation = '${iconPath.replace(/'/g, "''")},0' }` : ''}
45
+ $Shortcut.Save()
46
+ `
47
+ try {
48
+ const tmp = join(process.env.TEMP || '', 'barnaby-shortcut.ps1')
49
+ writeFileSync(tmp, ps.trim(), 'utf8')
50
+ execSync(`powershell -NoProfile -ExecutionPolicy Bypass -File "${tmp}"`, { stdio: 'pipe' })
51
+ console.log('[Barnaby] Start menu shortcut created. Pin it to taskbar for the correct icon.')
52
+ } catch {
53
+ // Silent fail - shortcut is optional
54
+ }
55
+ }
56
+
57
+ main()
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Sync build/icon.ico to public/favicon.ico so the BrowserWindow and packaged
4
+ * app use the same icon. electron-builder reads build/icon.ico for the .exe;
5
+ * the runtime uses public/favicon.ico (copied to dist by Vite).
6
+ */
7
+ import fs from 'node:fs'
8
+ import path from 'node:path'
9
+
10
+ const root = process.cwd()
11
+ const src = path.join(root, 'build', 'icon.ico')
12
+ const dest = path.join(root, 'public', 'favicon.ico')
13
+
14
+ if (!fs.existsSync(src)) {
15
+ console.warn('[sync-icon] build/icon.ico not found, skipping sync')
16
+ process.exit(0)
17
+ }
18
+
19
+ fs.copyFileSync(src, dest)
20
+ console.log('[sync-icon] Copied build/icon.ico → public/favicon.ico')