import fs from 'node:fs'; import path from 'node:path'; import { spawnSync } from 'node:child_process'; import { createRequire } from 'node:module'; import { fileURLToPath } from 'node:url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const require = createRequire(import.meta.url); const workspaceRoot = path.resolve(__dirname, '..'); const stagedAppDir = path.join(workspaceRoot, 'dist/desktop/app'); const buildResourcesDir = path.join(workspaceRoot, 'apps/electron/build'); const sourceIcon = path.join( workspaceRoot, 'apps/electron/assets/truegrowth-dock-icon.png' ); const requiredWebFiles = [ path.join(workspaceRoot, 'dist/apps/web/index.html'), ]; const runtimeDependencies = ['croner', 'electron-updater', 'p-queue', 'xstate']; function copyIfExists(from, to, options = {}) { if (!fs.existsSync(from)) { if (options.required) { throw new Error(`Required desktop package input is missing: ${from}`); } return false; } fs.rmSync(to, { recursive: true, force: true }); fs.mkdirSync(path.dirname(to), { recursive: true }); fs.cpSync(from, to, { recursive: true, verbatimSymlinks: true, filter: (source) => { const basename = path.basename(source); if ( basename === '.DS_Store' || basename === '.git' || basename === '.venv' || basename === 'node_modules' || basename === '__pycache__' ) { return false; } const normalized = source.split(path.sep).join('/'); return ![ '/vendor/ComfyUI/models/', '/vendor/ComfyUI/output/', '/vendor/ComfyUI/input/', '/vendor/ComfyUI/temp/', '/vendor/aigcpanel/data/model/', '/vendor/social-auto-upload/.venv/', '/vendor/social-auto-upload/media/', '/vendor/social-auto-upload/videos/', '/vendor/social-auto-upload/logs/', '/vendor/social-auto-upload/cookies/', ].some((fragment) => normalized.includes(fragment)); }, }); return true; } function writeJson(filePath, value) { fs.mkdirSync(path.dirname(filePath), { recursive: true }); fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`); } function findPackageRoot(packageName, startPath) { let current = path.dirname(startPath); while (current !== path.dirname(current)) { const candidate = path.join(current, 'package.json'); if (fs.existsSync(candidate)) { const pkg = JSON.parse(fs.readFileSync(candidate, 'utf8')); if (pkg.name === packageName) { return current; } } current = path.dirname(current); } throw new Error(`Unable to locate package root for ${packageName}`); } function resolvePackageRoot(packageName, fromDir = workspaceRoot) { const resolver = createRequire(path.join(fromDir, 'package.json')); try { const packageJsonPath = resolver.resolve(`${packageName}/package.json`); return findPackageRoot(packageName, packageJsonPath); } catch (error) { if (error?.code !== 'ERR_PACKAGE_PATH_NOT_EXPORTED') { throw error; } return findPackageRoot(packageName, resolver.resolve(packageName)); } } function stageRuntimeDependency(packageName, seen = new Set(), fromDir = workspaceRoot) { if (seen.has(packageName)) { return; } seen.add(packageName); const packageRoot = resolvePackageRoot(packageName, fromDir); const packageJson = JSON.parse( fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8') ); const targetRoot = path.join(stagedAppDir, 'node_modules', packageName); copyIfExists(packageRoot, targetRoot, { required: true }); const dependencies = Object.keys(packageJson.dependencies || {}); for (const dependency of dependencies) { stageRuntimeDependency(dependency, seen, packageRoot); } } function stageRuntimeDependencies() { const seen = new Set(); fs.rmSync(path.join(stagedAppDir, 'node_modules'), { recursive: true, force: true, }); for (const packageName of runtimeDependencies) { stageRuntimeDependency(packageName, seen); } } function run(command, args, options = {}) { const result = spawnSync(command, args, { cwd: workspaceRoot, encoding: 'utf8', stdio: options.stdio || 'pipe', }); if (result.status !== 0) { const details = [result.stdout, result.stderr].filter(Boolean).join('\n'); throw new Error( `Command failed: ${command} ${args.join(' ')}\n${details}`.trim() ); } return result; } function ensureWebBuild() { for (const filePath of requiredWebFiles) { if (!fs.existsSync(filePath)) { throw new Error( `Missing web build file: ${filePath}\nRun pnpm build:web before desktop packaging.` ); } } } function createMacIcon() { if (process.platform !== 'darwin') { return; } const iconsetDir = path.join(buildResourcesDir, 'icon.iconset'); const iconOutput = path.join(buildResourcesDir, 'icon.icns'); fs.rmSync(iconsetDir, { recursive: true, force: true }); fs.mkdirSync(iconsetDir, { recursive: true }); const sizes = [ [16, 'icon_16x16.png'], [32, 'icon_16x16@2x.png'], [32, 'icon_32x32.png'], [64, 'icon_32x32@2x.png'], [128, 'icon_128x128.png'], [256, 'icon_128x128@2x.png'], [256, 'icon_256x256.png'], [512, 'icon_256x256@2x.png'], [512, 'icon_512x512.png'], [1024, 'icon_512x512@2x.png'], ]; for (const [size, name] of sizes) { run('/usr/bin/sips', [ '-z', String(size), String(size), sourceIcon, '--out', path.join(iconsetDir, name), ]); } try { run('/usr/bin/iconutil', ['-c', 'icns', iconsetDir, '-o', iconOutput]); } catch (error) { console.warn( `[desktop:prepare] Could not generate icon.icns; Electron Builder will use icon.png. ${error.message}` ); } fs.rmSync(iconsetDir, { recursive: true, force: true }); } function createWindowsIcon() { const iconOutput = path.join(buildResourcesDir, 'icon.ico'); if (fs.existsSync(iconOutput)) { return; } const magick = spawnSync('which', ['magick'], { encoding: 'utf8' }); const convert = spawnSync('which', ['convert'], { encoding: 'utf8' }); const command = magick.status === 0 ? magick.stdout.trim() : convert.status === 0 ? convert.stdout.trim() : ''; if (!command) { console.warn( '[desktop:prepare] ImageMagick is unavailable; Windows icon.ico was not generated.' ); return; } run(command, [ sourceIcon, '-define', 'icon:auto-resize=256,128,64,48,32,16', iconOutput, ]); } function stageDesktopApp() { const rootPackage = JSON.parse( fs.readFileSync(path.join(workspaceRoot, 'package.json'), 'utf8') ); fs.rmSync(stagedAppDir, { recursive: true, force: true }); fs.mkdirSync(stagedAppDir, { recursive: true }); copyIfExists( path.join(workspaceRoot, 'dist/apps/web'), path.join(stagedAppDir, 'dist/apps/web'), { required: true } ); copyIfExists( path.join(workspaceRoot, 'apps/electron/main.mjs'), path.join(stagedAppDir, 'apps/electron/main.mjs'), { required: true } ); copyIfExists( path.join(workspaceRoot, 'apps/electron/preload.cjs'), path.join(stagedAppDir, 'apps/electron/preload.cjs'), { required: true } ); copyIfExists( path.join(workspaceRoot, 'apps/electron/assets'), path.join(stagedAppDir, 'apps/electron/assets'), { required: true } ); copyIfExists( path.join(workspaceRoot, 'scripts/truegrowth-local-api.mjs'), path.join(stagedAppDir, 'scripts/truegrowth-local-api.mjs'), { required: true } ); copyIfExists( path.join(workspaceRoot, 'scripts/business-workflow-utility-worker.mjs'), path.join(stagedAppDir, 'scripts/business-workflow-utility-worker.mjs'), { required: true } ); copyIfExists( path.join(workspaceRoot, 'vendor/aigcpanel/data'), path.join(stagedAppDir, 'vendor/aigcpanel/data') ); copyIfExists( path.join(workspaceRoot, 'vendor/Agent-Reach'), path.join(stagedAppDir, 'vendor/Agent-Reach') ); copyIfExists( path.join(workspaceRoot, 'vendor/social-auto-upload'), path.join(stagedAppDir, 'vendor/social-auto-upload') ); stageRuntimeDependencies(); writeJson(path.join(stagedAppDir, 'package.json'), { name: 'truegrowth-desktop', productName: rootPackage.productName || 'TrueGrowth', version: rootPackage.version || '0.0.0', description: rootPackage.description || 'TrueGrowth desktop workbench', author: rootPackage.author || 'TrueGrowth', license: rootPackage.license || 'UNLICENSED', main: 'apps/electron/main.mjs', dependencies: Object.fromEntries( runtimeDependencies.map((name) => [name, rootPackage.dependencies?.[name]]) ), }); } function main() { ensureWebBuild(); fs.mkdirSync(buildResourcesDir, { recursive: true }); copyIfExists(sourceIcon, path.join(buildResourcesDir, 'icon.png'), { required: true, }); createMacIcon(); createWindowsIcon(); stageDesktopApp(); console.log(`[desktop:prepare] Staged app at ${stagedAppDir}`); } main();