Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 81 additions & 7 deletions scripts/build-from-upstream.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
* build-from-upstream.js — Patch upstream Codex and repackage
*
* For macOS and Windows: no forge needed.
* Takes the upstream app, patches ASAR in-place, replaces codex CLI, outputs distributable.
* Takes the upstream app, patches ASAR in-place, and outputs a distributable.
* Windows keeps the upstream codex.exe so the app-server protocol stays aligned
* with the bundled cua_node/node_repl binaries from the same MSIX.
*
* Usage:
* node scripts/build-from-upstream.js --platform mac-arm64
Expand All @@ -12,7 +14,7 @@
*/
const fs = require("fs");
const path = require("path");
const { execSync } = require("child_process");
const { execSync, execFileSync } = require("child_process");

const PROJECT_ROOT = path.resolve(__dirname, "..");
const SRC_DIR = path.join(PROJECT_ROOT, "src");
Expand Down Expand Up @@ -49,6 +51,23 @@ function copyRecursive(src, dest) {
return count;
}

function clearExistingAsarUnpacked(asarPath) {
const unpackedPath = `${asarPath}.unpacked`;
if (fs.existsSync(unpackedPath)) {
fs.rmSync(unpackedPath, { recursive: true, force: true });
}
}

function asarCliPath() {
return path.join(PROJECT_ROOT, "node_modules", "@electron", "asar", "bin", "asar.mjs");
}

function packAsar(asarDir, asarPath, extraArgs = []) {
execFileSync(process.execPath, [asarCliPath(), "pack", asarDir, asarPath, ...extraArgs], {
stdio: "pipe",
});
}

function resolveCodexVendor(platform) {
const triple = TARGET_TRIPLE_MAP[platform];
if (!triple) return null;
Expand Down Expand Up @@ -148,7 +167,11 @@ function buildMac(platform) {
// 3. Repack patched ASAR
const asarPath = path.join(resourcesDir, "app.asar");
console.log(" [asar pack] _asar/ -> app.asar");
execSync(`npx asar pack "${asarDir}" "${asarPath}"`);
clearExistingAsarUnpacked(asarPath);
packAsar(asarDir, asarPath, [
"--unpack-dir", "{node_modules/better-sqlite3,node_modules/node-pty}",
"--unpack", "{**/*.node,**/node-pty/build/Release/*.exe}",
]);

// 4. Update ASAR integrity hash in Info.plist
const infoPlist = path.join(outApp, "Contents", "Info.plist");
Expand Down Expand Up @@ -220,7 +243,10 @@ function buildWin(platform) {

// Repack patched ASAR
console.log(" [asar pack] _asar/ -> app.asar");
execSync(`npx asar pack "${asarDir}" "${asarPath}"`);
clearExistingAsarUnpacked(asarPath);
packAsar(asarDir, asarPath, [
"--unpack-dir", "{node_modules/better-sqlite3,node_modules/node-pty,node_modules/@worklouder}",
]);

// Compute new hash and patch exe
const newHash = computeAsarHeaderHash(asarPath);
Expand All @@ -236,15 +262,18 @@ function buildWin(platform) {
}
}

// Replace codex CLI
replaceCodex(platform, resourcesDir, "codex.exe");
// Keep the upstream Windows codex.exe. The Desktop app-server and
// cua_node/node_repl exchange Codex-specific MCP metadata; replacing only the
// CLI with @cometix/codex can make that protocol drift and break browser
// tools (for example: sandboxCwd must use the file URI scheme).
keepUpstreamCodex(platform, resourcesDir, "codex.exe");

// Create ZIP
const version = getVersion(asarDir);
const zipName = `Codex-win-x64-${version}.zip`;
const zipPath = path.join(OUT_DIR, zipName);
console.log(` [zip] ${zipName}`);
execSync(`7zz a -tzip -mx=5 "${zipPath}" .`, { cwd: outApp });
createZip(zipPath, outApp);

const sizeMB = (fs.statSync(zipPath).size / 1048576).toFixed(1);
console.log(` [ok] ${zipPath} (${sizeMB} MB)`);
Expand Down Expand Up @@ -301,6 +330,51 @@ function replaceCodex(platform, resourcesDir, binName) {
}
}

function keepUpstreamCodex(platform, resourcesDir, binName) {
const codexPath = path.join(resourcesDir, binName);
if (!fs.existsSync(codexPath)) {
console.log(` [!] upstream ${binName} not found for ${platform}`);
return;
}
console.log(` [codex] keeping upstream ${binName}`);
}

function createZip(zipPath, cwd) {
let sevenZipError = null;
try {
execFileSync("7zz", ["a", "-tzip", "-mx=5", zipPath, "."], { cwd, stdio: "pipe" });
return;
} catch (e) {
sevenZipError = e;
}

if (process.platform === "win32") {
try {
execFileSync("powershell", [
"-NoProfile",
"-ExecutionPolicy", "Bypass",
"-Command",
"& { param($src, $zip) " +
"Add-Type -AssemblyName System.IO.Compression.FileSystem; " +
"if (Test-Path -LiteralPath $zip) { Remove-Item -LiteralPath $zip -Force }; " +
"[System.IO.Compression.ZipFile]::CreateFromDirectory($src, $zip, " +
"[System.IO.Compression.CompressionLevel]::Optimal, $false) }",
cwd,
zipPath,
], { stdio: "pipe" });
return;
} catch (e) {
throw new Error(`Failed to create ZIP with 7zz or PowerShell: 7zz: ${sevenZipError.message}; PowerShell: ${e.message}`);
}
}

try {
execFileSync("zip", ["-r", zipPath, "."], { cwd, stdio: "pipe" });
} catch (e) {
throw new Error(`Failed to create ZIP with 7zz or zip: 7zz: ${sevenZipError.message}; zip: ${e.message}`);
}
}

function getVersion(asarDir) {
try {
const pkg = JSON.parse(fs.readFileSync(path.join(asarDir, "package.json"), "utf-8"));
Expand Down
40 changes: 33 additions & 7 deletions scripts/prepare-src.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
#!/usr/bin/env node
/**
* Pre-build: Repack patched ASAR, replace codex CLI, assemble for forge.
* Pre-build: Repack patched ASAR, align platform resources, assemble for forge.
*
* Flow:
* 1. Repack _asar/ -> app.asar (with patches applied)
* 2. Replace codex binary with @cometix/codex version
* 2. Replace codex binary with @cometix/codex version where needed
* 3. Copy everything to src/ for forge (app.asar + unpacked + resources)
*
* For Windows: keep upstream codex.exe so app-server and cua_node/node_repl
* MCP metadata stay protocol-compatible.
* For Linux: strip macOS-only resources, add Linux codex from @cometix/codex
*
* Usage:
Expand All @@ -15,7 +17,7 @@
*/
const fs = require("fs");
const path = require("path");
const { execSync } = require("child_process");
const { execSync, execFileSync } = require("child_process");

const SRC = path.join(__dirname, "..", "src");
const PROJECT_ROOT = path.join(__dirname, "..");
Expand All @@ -36,6 +38,16 @@ const MACOS_STRIP = new Set([
]);
const MACOS_STRIP_DIRS = new Set(["native"]);

function asarCliPath() {
return path.join(PROJECT_ROOT, "node_modules", "@electron", "asar", "bin", "asar.mjs");
}

function packAsar(asarDir, asarPath, extraArgs = []) {
execFileSync(process.execPath, [asarCliPath(), "pack", asarDir, asarPath, ...extraArgs], {
stdio: "pipe",
});
}

function copyRecursive(src, dest, skipFiles, skipDirs) {
fs.mkdirSync(dest, { recursive: true });
let count = 0;
Expand Down Expand Up @@ -163,15 +175,29 @@ function main() {
// 1. Repack _asar/ -> app.asar
const repackedAsar = path.join(sourceDir, "app.asar");
console.log(" [repack] _asar/ -> app.asar");
execSync(`npx asar pack "${asarContentDir}" "${repackedAsar}"`);
const asarPackArgs = isLinux
? []
: platform === "win"
? ["--unpack-dir", "{node_modules/better-sqlite3,node_modules/node-pty,node_modules/@worklouder}"]
: [
"--unpack-dir", "{node_modules/better-sqlite3,node_modules/node-pty}",
"--unpack", "{**/*.node,**/node-pty/build/Release/*.exe}",
];
packAsar(asarContentDir, repackedAsar, asarPackArgs);
const asarSize = (fs.statSync(repackedAsar).size / 1048576).toFixed(1);
console.log(` [ok] app.asar: ${asarSize} MB`);

// 2. Replace codex binary with @cometix/codex
// 2. Replace codex binary with @cometix/codex where needed.
// Windows must keep the upstream codex.exe from the MSIX. The Desktop
// app-server and bundled cua_node/node_repl exchange Codex-specific MCP
// metadata; mixing a different @cometix/codex build can break browser tools
// with errors such as "sandboxCwd must use the file URI scheme".
const isWin = platform === "win";
const codexBinName = isWin ? "codex.exe" : "codex";
const vendorCodex = resolveCodexVendor(platform);
if (vendorCodex) {
const vendorCodex = isWin ? null : resolveCodexVendor(platform);
if (isWin) {
console.log(` [codex] keeping upstream ${codexBinName}`);
} else if (vendorCodex) {
// For Linux: put codex in sourceDir (mac-x64/) so it can be found,
// but also mark for later copy to forge output.
const dest = path.join(sourceDir, codexBinName);
Expand Down
Loading