Initial TrueGrowth source import
This commit is contained in:
295
scripts/prepare-desktop-release.mjs
Normal file
295
scripts/prepare-desktop-release.mjs
Normal file
@@ -0,0 +1,295 @@
|
||||
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', '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();
|
||||
Reference in New Issue
Block a user