182 lines
6.6 KiB
JavaScript
182 lines
6.6 KiB
JavaScript
const fs = require("node:fs");
|
|
const path = require("node:path");
|
|
const { spawnSync } = require("node:child_process");
|
|
const asar = require("@electron/asar");
|
|
|
|
const computerUseHelperAppName = "OpenWork Computer Use.app";
|
|
|
|
const sidecarBases = [
|
|
"opencode",
|
|
];
|
|
|
|
function targetTriple(platformName, arch) {
|
|
if (platformName === "darwin") {
|
|
if (arch === "arm64") return "aarch64-apple-darwin";
|
|
if (arch === "x64") return "x86_64-apple-darwin";
|
|
}
|
|
if (platformName === "linux") {
|
|
if (arch === "arm64") return "aarch64-unknown-linux-gnu";
|
|
if (arch === "x64") return "x86_64-unknown-linux-gnu";
|
|
}
|
|
if (platformName === "win32") {
|
|
if (arch === "arm64") return "aarch64-pc-windows-msvc";
|
|
if (arch === "x64") return "x86_64-pc-windows-msvc";
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function resolveSidecarsDir(context) {
|
|
if (context.electronPlatformName === "darwin") {
|
|
const entries = fs.existsSync(context.appOutDir) ? fs.readdirSync(context.appOutDir) : [];
|
|
const appName = entries.find((entry) => entry.endsWith(".app"));
|
|
return appName ? path.join(context.appOutDir, appName, "Contents", "Resources", "sidecars") : null;
|
|
}
|
|
return path.join(context.appOutDir, "resources", "sidecars");
|
|
}
|
|
|
|
function resolveMacAppPath(context) {
|
|
if (context.electronPlatformName !== "darwin") return null;
|
|
const appName = `${context.packager.appInfo.productFilename}.app`;
|
|
const direct = path.join(context.appOutDir, appName);
|
|
if (fs.existsSync(direct)) return direct;
|
|
|
|
const entries = fs.existsSync(context.appOutDir) ? fs.readdirSync(context.appOutDir) : [];
|
|
const fallback = entries.find((entry) => entry.endsWith(".app"));
|
|
return fallback ? path.join(context.appOutDir, fallback) : null;
|
|
}
|
|
|
|
function resolveAppAsarPath(context) {
|
|
if (context.electronPlatformName === "darwin") {
|
|
const appPath = resolveMacAppPath(context);
|
|
return appPath ? path.join(appPath, "Contents", "Resources", "app.asar") : null;
|
|
}
|
|
return path.join(context.appOutDir, "resources", "app.asar");
|
|
}
|
|
|
|
function normalizeAsarEntryPath(entry, separator) {
|
|
return entry.split(separator).join("/");
|
|
}
|
|
|
|
function verifyRuntimeDependencies(context) {
|
|
const appAsarPath = resolveAppAsarPath(context);
|
|
if (!appAsarPath || !fs.existsSync(appAsarPath)) {
|
|
throw new Error(`Missing packaged app.asar at ${appAsarPath || context.appOutDir}`);
|
|
}
|
|
const packagedFiles = new Set(
|
|
asar
|
|
.listPackage(appAsarPath, { isPack: false })
|
|
.map((entry) => normalizeAsarEntryPath(entry, path.sep)),
|
|
);
|
|
const stagedNodeModules = path.resolve(__dirname, "..", ".electron-runtime", "node_modules");
|
|
const packageJsonPaths = [];
|
|
function visitNodeModules(nodeModulesPath, archivePrefix) {
|
|
for (const entry of fs.readdirSync(nodeModulesPath, { withFileTypes: true })) {
|
|
if (!entry.isDirectory()) continue;
|
|
if (entry.name.startsWith("@")) {
|
|
visitNodeModules(path.join(nodeModulesPath, entry.name), `${archivePrefix}/${entry.name}`);
|
|
continue;
|
|
}
|
|
const packagePath = path.join(nodeModulesPath, entry.name);
|
|
if (!fs.existsSync(path.join(packagePath, "package.json"))) continue;
|
|
const packageJsonPath = `${archivePrefix}/${entry.name}/package.json`;
|
|
packageJsonPaths.push(packageJsonPath);
|
|
const nestedNodeModules = path.join(packagePath, "node_modules");
|
|
if (fs.existsSync(nestedNodeModules)) {
|
|
visitNodeModules(nestedNodeModules, `${archivePrefix}/${entry.name}/node_modules`);
|
|
}
|
|
}
|
|
}
|
|
if (!fs.existsSync(stagedNodeModules)) {
|
|
throw new Error(`Missing staged MCP runtime at ${stagedNodeModules}`);
|
|
}
|
|
visitNodeModules(stagedNodeModules, "/node_modules");
|
|
if (packageJsonPaths.length === 0) throw new Error("The staged MCP runtime is empty.");
|
|
for (const packageJsonPath of packageJsonPaths) {
|
|
if (!packagedFiles.has(packageJsonPath)) {
|
|
throw new Error(`Missing staged MCP runtime package from app.asar: ${packageJsonPath}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
function signComputerUseHelper(context) {
|
|
const appPath = resolveMacAppPath(context);
|
|
if (!appPath) return;
|
|
|
|
const helperPath = path.join(appPath, "Contents", "Resources", "helpers", computerUseHelperAppName);
|
|
if (!fs.existsSync(helperPath)) {
|
|
throw new Error(`Missing Computer Use helper app at ${helperPath}`);
|
|
}
|
|
|
|
const identity = process.env.OPENWORK_COMPUTER_USE_CODESIGN_IDENTITY
|
|
|| process.env.CSC_NAME
|
|
|| process.env.APPLE_CODESIGN_IDENTITY
|
|
|| "-";
|
|
const args = ["--force", "--deep", "--options", "runtime", "--sign", identity];
|
|
if (identity !== "-") args.push("--timestamp");
|
|
args.push(helperPath);
|
|
|
|
const result = spawnSync("codesign", args, { stdio: "inherit" });
|
|
if (result.error) throw result.error;
|
|
if (result.status !== 0) {
|
|
throw new Error(`codesign failed for Computer Use helper app with status ${result.status}`);
|
|
}
|
|
}
|
|
|
|
function copyExecutableTargetToAlias(sidecarsDir, targetName, aliasName) {
|
|
const targetPath = path.join(sidecarsDir, targetName);
|
|
if (!fs.existsSync(targetPath)) {
|
|
throw new Error(`Missing packaged sidecar for target: ${targetName}`);
|
|
}
|
|
|
|
const aliasPath = path.join(sidecarsDir, aliasName);
|
|
fs.copyFileSync(targetPath, aliasPath);
|
|
try {
|
|
fs.chmodSync(aliasPath, 0o755);
|
|
} catch {
|
|
// Windows and some filesystems may ignore chmod.
|
|
}
|
|
}
|
|
|
|
async function afterPack(context) {
|
|
verifyRuntimeDependencies(context);
|
|
const triple = targetTriple(context.electronPlatformName, context.arch);
|
|
if (!triple) return;
|
|
|
|
const sidecarsDir = resolveSidecarsDir(context);
|
|
if (!sidecarsDir || !fs.existsSync(sidecarsDir)) return;
|
|
|
|
const isWindows = context.electronPlatformName === "win32";
|
|
const executableSuffix = isWindows ? ".exe" : "";
|
|
const keep = new Set();
|
|
|
|
for (const base of sidecarBases) {
|
|
const aliasName = `${base}${executableSuffix}`;
|
|
const targetName = `${base}-${triple}${executableSuffix}`;
|
|
copyExecutableTargetToAlias(sidecarsDir, targetName, aliasName);
|
|
keep.add(aliasName);
|
|
keep.add(targetName);
|
|
}
|
|
|
|
const versionsAlias = "versions.json";
|
|
const versionsTarget = `versions.json-${triple}${executableSuffix}`;
|
|
const versionsTargetPath = path.join(sidecarsDir, versionsTarget);
|
|
if (!fs.existsSync(versionsTargetPath)) {
|
|
throw new Error(`Missing packaged sidecar metadata for target: ${versionsTarget}`);
|
|
}
|
|
fs.copyFileSync(versionsTargetPath, path.join(sidecarsDir, versionsAlias));
|
|
keep.add(versionsAlias);
|
|
keep.add(versionsTarget);
|
|
|
|
for (const entry of fs.readdirSync(sidecarsDir)) {
|
|
if (!keep.has(entry)) {
|
|
fs.rmSync(path.join(sidecarsDir, entry), { force: true, recursive: true });
|
|
}
|
|
}
|
|
|
|
signComputerUseHelper(context);
|
|
}
|
|
|
|
module.exports = afterPack;
|
|
module.exports.default = afterPack;
|
|
module.exports.normalizeAsarEntryPath = normalizeAsarEntryPath;
|