import { spawn, spawnSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const appRoot = path.resolve(__dirname, '..'); const brandName = 'TrueGrowth'; const bundleIdentifier = 'ai.truegrowth.desktop.dev'; const cacheRoot = path.join(appRoot, '.truegrowth-desktop'); const brandedAppPath = path.join(cacheRoot, `${brandName}.app`); const mainEntry = path.join(appRoot, 'apps/electron/main.mjs'); const cacheFormatVersion = '2'; const appVersion = JSON.parse(fs.readFileSync(path.join(appRoot, 'package.json'), 'utf8')) .version || '0.0.0'; const isMac = process.platform === 'darwin'; const prepareOnly = process.argv.includes('--prepare-only'); function runPlistBuddy(args) { const result = spawnSync('/usr/libexec/PlistBuddy', args, { encoding: 'utf8', }); if (result.status !== 0) { const details = result.stderr || result.stdout || args.join(' '); throw new Error(`Failed to update Electron app metadata: ${details}`); } } function setPlistValue(plistPath, key, value) { const escapedValue = String(value).replace(/"/g, '\\"'); const setCommand = `Set :${key} "${escapedValue}"`; const addCommand = `Add :${key} string "${escapedValue}"`; const setResult = spawnSync( '/usr/libexec/PlistBuddy', ['-c', setCommand, plistPath], { encoding: 'utf8', } ); if (setResult.status === 0) { return; } runPlistBuddy(['-c', addCommand, plistPath]); } function signMacApp(appPath) { const verifyResult = spawnSync( '/usr/bin/codesign', ['--verify', '--deep', '--strict', appPath], { encoding: 'utf8', } ); if (verifyResult.status === 0) { return; } const signResult = spawnSync( '/usr/bin/codesign', ['--force', '--deep', '--sign', '-', appPath], { encoding: 'utf8', } ); if (signResult.status !== 0) { const details = signResult.stderr || signResult.stdout || appPath; throw new Error(`Failed to sign branded Electron app: ${details}`); } } function getElectronExecutable() { const result = spawnSync(process.execPath, ['-p', "require('electron')"], { cwd: appRoot, encoding: 'utf8', }); if (result.status !== 0) { throw new Error(result.stderr || 'Unable to resolve Electron executable.'); } return result.stdout.trim(); } function ensureBrandedMacApp(electronExecutable) { const sourceAppPath = electronExecutable.slice( 0, electronExecutable.indexOf('.app') + '.app'.length ); const sourceInfoPath = path.join(sourceAppPath, 'Contents/Info.plist'); const sourceVersion = `${fs.statSync(sourceInfoPath).mtimeMs.toFixed( 0 )}:${cacheFormatVersion}`; const stampPath = path.join(cacheRoot, 'electron-source-version.txt'); const cachedVersion = fs.existsSync(stampPath) ? fs.readFileSync(stampPath, 'utf8') : ''; if (!fs.existsSync(brandedAppPath) || cachedVersion !== sourceVersion) { fs.rmSync(brandedAppPath, { recursive: true, force: true }); fs.mkdirSync(cacheRoot, { recursive: true }); fs.cpSync(sourceAppPath, brandedAppPath, { recursive: true, verbatimSymlinks: true, }); fs.writeFileSync(stampPath, sourceVersion); } const infoPath = path.join(brandedAppPath, 'Contents/Info.plist'); setPlistValue(infoPath, 'CFBundleName', brandName); setPlistValue(infoPath, 'CFBundleDisplayName', brandName); setPlistValue(infoPath, 'CFBundleIdentifier', bundleIdentifier); setPlistValue(infoPath, 'CFBundleShortVersionString', appVersion); setPlistValue(infoPath, 'CFBundleVersion', appVersion); setPlistValue( infoPath, 'LSApplicationCategoryType', 'public.app-category.productivity' ); signMacApp(brandedAppPath); return path.join(brandedAppPath, 'Contents/MacOS/Electron'); } function launch(executablePath) { const child = spawn(executablePath, [mainEntry], { cwd: appRoot, env: { ...process.env, TRUEGROWTH_BRANDED_ELECTRON_DEV: '1', }, stdio: 'inherit', }); child.on('exit', (code, signal) => { if (typeof code === 'number') { process.exit(code); } if (signal) { process.kill(process.pid, signal); } }); } const electronExecutable = getElectronExecutable(); const executablePath = isMac ? ensureBrandedMacApp(electronExecutable) : electronExecutable; if (prepareOnly) { console.log(executablePath); } else { launch(executablePath); }