Files
TrueGrowth/scripts/prepare-desktop-release.mjs
jiam 5119ac0ef8
Some checks failed
CI / main (push) Has been cancelled
CI / release-e2e (push) Has been cancelled
Sync latest TrueGrowth updates
2026-07-08 02:03:18 +08:00

296 lines
8.9 KiB
JavaScript

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();