296 lines
8.9 KiB
JavaScript
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();
|