mirror of
https://github.com/different-ai/openwork
synced 2026-04-25 17:15:34 +02:00
Keep OpenCode version selection predictable by reading a single repo-wide constant and packaging that pin into orchestrator builds. Remove env and latest-release fallbacks so desktop, workers, snapshots, and CI stay aligned. Co-authored-by: Omar McAdam <omar@OpenWork-Studio.localdomain>
773 lines
26 KiB
JavaScript
773 lines
26 KiB
JavaScript
import { spawnSync } from "child_process";
|
|
import { createHash } from "crypto";
|
|
import {
|
|
chmodSync,
|
|
closeSync,
|
|
copyFileSync,
|
|
existsSync,
|
|
mkdirSync,
|
|
openSync,
|
|
readFileSync,
|
|
readSync,
|
|
readdirSync,
|
|
statSync,
|
|
unlinkSync,
|
|
writeFileSync,
|
|
} from "fs";
|
|
import { dirname, join, resolve } from "path";
|
|
import { tmpdir } from "os";
|
|
import { fileURLToPath } from "url";
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const readArg = (name) => {
|
|
const raw = process.argv.slice(2);
|
|
const direct = raw.find((arg) => arg.startsWith(`${name}=`));
|
|
if (direct) return direct.split("=")[1];
|
|
const index = raw.indexOf(name);
|
|
if (index >= 0 && raw[index + 1]) return raw[index + 1];
|
|
return null;
|
|
};
|
|
|
|
const hasFlag = (name) => process.argv.slice(2).includes(name);
|
|
const forceBuild = hasFlag("--force") || process.env.OPENWORK_SIDECAR_FORCE_BUILD === "1";
|
|
const sidecarOverride = process.env.OPENWORK_SIDECAR_DIR?.trim() || readArg("--outdir");
|
|
const sidecarDir = sidecarOverride ? resolve(sidecarOverride) : join(__dirname, "..", "src-tauri", "sidecars");
|
|
const packageJsonPath = resolve(__dirname, "..", "package.json");
|
|
const constantsPath = resolve(__dirname, "..", "..", "..", "constants.json");
|
|
|
|
const opencodeGithubRepo = (() => {
|
|
const raw =
|
|
process.env.OPENCODE_GITHUB_REPO?.trim() ||
|
|
process.env.OPENWORK_OPENCODE_GITHUB_REPO?.trim() ||
|
|
"anomalyco/opencode";
|
|
const normalized = raw
|
|
.replace(/^https:\/\/github\.com\//i, "")
|
|
.replace(/\.git$/i, "")
|
|
.trim();
|
|
if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(normalized)) {
|
|
return "anomalyco/opencode";
|
|
}
|
|
return normalized;
|
|
})();
|
|
const opencodeVersion = (() => {
|
|
try {
|
|
const raw = readFileSync(constantsPath, "utf8");
|
|
const parsed = JSON.parse(raw);
|
|
return typeof parsed.opencodeVersion === "string" ? parsed.opencodeVersion.trim() || null : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
})();
|
|
|
|
const normalizeVersion = (value) => {
|
|
const raw = String(value ?? "").trim();
|
|
if (!raw) return null;
|
|
if (raw.toLowerCase() === "latest") return null;
|
|
return raw.startsWith("v") ? raw.slice(1) : raw;
|
|
};
|
|
|
|
const opencodeAssetOverride = process.env.OPENCODE_ASSET?.trim() || null;
|
|
const opencodeRouterVersion = (() => {
|
|
if (process.env.OPENCODE_ROUTER_VERSION?.trim()) return process.env.OPENCODE_ROUTER_VERSION.trim();
|
|
try {
|
|
const raw = readFileSync(packageJsonPath, "utf8");
|
|
const pkg = JSON.parse(raw);
|
|
if (pkg.opencodeRouterVersion) return String(pkg.opencodeRouterVersion).trim();
|
|
} catch {
|
|
// ignore
|
|
}
|
|
return null;
|
|
})();
|
|
const chromeDevtoolsMcpVersion =
|
|
process.env.CHROME_DEVTOOLS_MCP_VERSION?.trim() ||
|
|
process.env.OPENWORK_CHROME_DEVTOOLS_MCP_VERSION?.trim() ||
|
|
"0.17.0";
|
|
|
|
// Target triple for native platform binaries
|
|
const resolvedTargetTriple = (() => {
|
|
const envTarget =
|
|
process.env.TAURI_ENV_TARGET_TRIPLE ??
|
|
process.env.CARGO_CFG_TARGET_TRIPLE ??
|
|
process.env.TARGET;
|
|
if (envTarget) return envTarget;
|
|
if (process.platform === "darwin") {
|
|
return process.arch === "arm64" ? "aarch64-apple-darwin" : "x86_64-apple-darwin";
|
|
}
|
|
if (process.platform === "linux") {
|
|
return process.arch === "arm64" ? "aarch64-unknown-linux-gnu" : "x86_64-unknown-linux-gnu";
|
|
}
|
|
if (process.platform === "win32") {
|
|
return process.arch === "arm64" ? "aarch64-pc-windows-msvc" : "x86_64-pc-windows-msvc";
|
|
}
|
|
return null;
|
|
})();
|
|
|
|
const bunTarget = (() => {
|
|
switch (resolvedTargetTriple) {
|
|
case "aarch64-apple-darwin":
|
|
return "bun-darwin-arm64";
|
|
case "x86_64-apple-darwin":
|
|
return "bun-darwin-x64-baseline";
|
|
case "aarch64-unknown-linux-gnu":
|
|
return "bun-linux-arm64";
|
|
case "x86_64-unknown-linux-gnu":
|
|
return "bun-linux-x64-baseline";
|
|
// Windows baseline artifacts intermittently fail to extract in CI
|
|
// with Bun 1.3.6. Use the stable x64 target here for now.
|
|
case "x86_64-pc-windows-msvc":
|
|
return "bun-windows-x64";
|
|
default:
|
|
return null;
|
|
}
|
|
})();
|
|
|
|
const opencodeBaseName = process.platform === "win32" ? "opencode.exe" : "opencode";
|
|
const opencodePath = join(sidecarDir, opencodeBaseName);
|
|
const opencodeTargetName = resolvedTargetTriple
|
|
? `opencode-${resolvedTargetTriple}${process.platform === "win32" ? ".exe" : ""}`
|
|
: null;
|
|
const opencodeTargetPath = opencodeTargetName ? join(sidecarDir, opencodeTargetName) : null;
|
|
|
|
const opencodeCandidatePath = opencodeTargetPath ?? opencodePath;
|
|
let existingOpencodeVersion = null;
|
|
|
|
// openwork-server paths
|
|
const openworkServerBaseName = "openwork-server";
|
|
const openworkServerName = process.platform === "win32" ? `${openworkServerBaseName}.exe` : openworkServerBaseName;
|
|
const openworkServerPath = join(sidecarDir, openworkServerName);
|
|
const openworkServerBuildName = bunTarget
|
|
? `${openworkServerBaseName}-${bunTarget}${bunTarget.includes("windows") ? ".exe" : ""}`
|
|
: openworkServerName;
|
|
const openworkServerBuildPath = join(sidecarDir, openworkServerBuildName);
|
|
const openworkServerTargetTriple = resolvedTargetTriple;
|
|
const openworkServerTargetName = openworkServerTargetTriple
|
|
? `${openworkServerBaseName}-${openworkServerTargetTriple}${openworkServerTargetTriple.includes("windows") ? ".exe" : ""}`
|
|
: null;
|
|
const openworkServerTargetPath = openworkServerTargetName ? join(sidecarDir, openworkServerTargetName) : null;
|
|
|
|
const openworkServerDir = resolve(__dirname, "..", "..", "server");
|
|
|
|
const resolveBuildScript = (dir) => {
|
|
const scriptPath = resolve(dir, "script", "build.ts");
|
|
if (existsSync(scriptPath)) return scriptPath;
|
|
const scriptsPath = resolve(dir, "scripts", "build.ts");
|
|
if (existsSync(scriptsPath)) return scriptsPath;
|
|
return scriptPath;
|
|
};
|
|
|
|
// opencode-router paths
|
|
const opencodeRouterBaseName = "opencode-router";
|
|
const opencodeRouterName = process.platform === "win32" ? `${opencodeRouterBaseName}.exe` : opencodeRouterBaseName;
|
|
const opencodeRouterPath = join(sidecarDir, opencodeRouterName);
|
|
const opencodeRouterBuildName = bunTarget
|
|
? `${opencodeRouterBaseName}-${bunTarget}${bunTarget.includes("windows") ? ".exe" : ""}`
|
|
: opencodeRouterName;
|
|
const opencodeRouterBuildPath = join(sidecarDir, opencodeRouterBuildName);
|
|
const opencodeRouterTargetTriple = resolvedTargetTriple;
|
|
const opencodeRouterTargetName = opencodeRouterTargetTriple
|
|
? `${opencodeRouterBaseName}-${opencodeRouterTargetTriple}${opencodeRouterTargetTriple.includes("windows") ? ".exe" : ""}`
|
|
: null;
|
|
const opencodeRouterTargetPath = opencodeRouterTargetName ? join(sidecarDir, opencodeRouterTargetName) : null;
|
|
const opencodeRouterDir = resolve(__dirname, "..", "..", "opencode-router");
|
|
|
|
// orchestrator paths
|
|
const orchestratorBaseName = "openwork-orchestrator";
|
|
const orchestratorName =
|
|
process.platform === "win32" ? `${orchestratorBaseName}.exe` : orchestratorBaseName;
|
|
const orchestratorPath = join(sidecarDir, orchestratorName);
|
|
const orchestratorBuildName = bunTarget
|
|
? `${orchestratorBaseName}-${bunTarget}${bunTarget.includes("windows") ? ".exe" : ""}`
|
|
: orchestratorName;
|
|
const orchestratorBuildPath = join(sidecarDir, orchestratorBuildName);
|
|
const orchestratorTargetTriple = resolvedTargetTriple;
|
|
const orchestratorTargetName = orchestratorTargetTriple
|
|
? `${orchestratorBaseName}-${orchestratorTargetTriple}${orchestratorTargetTriple.includes("windows") ? ".exe" : ""}`
|
|
: null;
|
|
const orchestratorTargetPath = orchestratorTargetName ? join(sidecarDir, orchestratorTargetName) : null;
|
|
const orchestratorDir = resolve(__dirname, "..", "..", "orchestrator");
|
|
|
|
// chrome-devtools-mcp shim sidecar
|
|
const chromeDevtoolsBaseName = "chrome-devtools-mcp";
|
|
const chromeDevtoolsName = process.platform === "win32" ? `${chromeDevtoolsBaseName}.exe` : chromeDevtoolsBaseName;
|
|
const chromeDevtoolsPath = join(sidecarDir, chromeDevtoolsName);
|
|
const chromeDevtoolsBuildName = bunTarget
|
|
? `${chromeDevtoolsBaseName}-${bunTarget}${bunTarget.includes("windows") ? ".exe" : ""}`
|
|
: chromeDevtoolsName;
|
|
const chromeDevtoolsBuildPath = join(sidecarDir, chromeDevtoolsBuildName);
|
|
const chromeDevtoolsTargetTriple = resolvedTargetTriple;
|
|
const chromeDevtoolsTargetName = chromeDevtoolsTargetTriple
|
|
? `${chromeDevtoolsBaseName}-${chromeDevtoolsTargetTriple}${chromeDevtoolsTargetTriple.includes("windows") ? ".exe" : ""}`
|
|
: null;
|
|
const chromeDevtoolsTargetPath = chromeDevtoolsTargetName ? join(sidecarDir, chromeDevtoolsTargetName) : null;
|
|
const chromeDevtoolsShimPath = resolve(__dirname, "chrome-devtools-mcp-shim.ts");
|
|
|
|
const readHeader = (filePath, length = 256) => {
|
|
const fd = openSync(filePath, "r");
|
|
try {
|
|
const buffer = Buffer.alloc(length);
|
|
const bytesRead = readSync(fd, buffer, 0, length, 0);
|
|
return buffer.subarray(0, bytesRead).toString("utf8");
|
|
} finally {
|
|
closeSync(fd);
|
|
}
|
|
};
|
|
|
|
const isStubBinary = (filePath) => {
|
|
try {
|
|
const stat = statSync(filePath);
|
|
if (!stat.isFile()) return true;
|
|
if (stat.size < 1024) return true;
|
|
const header = readHeader(filePath);
|
|
if (header.startsWith("#!")) return true;
|
|
if (header.includes("Sidecar missing") || header.includes("Bun is required")) return true;
|
|
} catch {
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
const readDirectory = (dir) => {
|
|
let entries = [];
|
|
try {
|
|
entries = readdirSync(dir, { withFileTypes: true });
|
|
} catch {
|
|
return [];
|
|
}
|
|
|
|
return entries.flatMap((entry) => {
|
|
const next = join(dir, entry.name);
|
|
if (entry.isDirectory()) {
|
|
return readDirectory(next);
|
|
}
|
|
if (entry.isFile()) {
|
|
return [next];
|
|
}
|
|
return [];
|
|
});
|
|
};
|
|
|
|
const findOpencodeBinary = (dir) => {
|
|
const candidates = readDirectory(dir);
|
|
return (
|
|
candidates.find((file) => file.endsWith(`/${opencodeBaseName}`) || file.endsWith(`\\${opencodeBaseName}`)) ??
|
|
candidates.find((file) => file.endsWith("/opencode") || file.endsWith("\\opencode")) ??
|
|
null
|
|
);
|
|
};
|
|
|
|
const findOpenCodeRouterBinary = (dir) => {
|
|
const candidates = readDirectory(dir);
|
|
return (
|
|
candidates.find((file) => file.endsWith(`/${opencodeRouterName}`) || file.endsWith(`\\${opencodeRouterName}`)) ??
|
|
candidates.find((file) => file.endsWith("/opencode-router") || file.endsWith("\\opencodeRouter")) ??
|
|
null
|
|
);
|
|
};
|
|
|
|
const readBinaryVersion = (filePath) => {
|
|
try {
|
|
const result = spawnSync(filePath, ["--version"], { encoding: "utf8" });
|
|
if (result.status === 0 && result.stdout) return result.stdout.trim();
|
|
} catch {
|
|
// ignore
|
|
}
|
|
return null;
|
|
};
|
|
|
|
const sha256File = (filePath) => {
|
|
const hash = createHash("sha256");
|
|
hash.update(readFileSync(filePath));
|
|
return hash.digest("hex");
|
|
};
|
|
|
|
const parseChecksum = (content, assetName) => {
|
|
const lines = content.split(/\r?\n/);
|
|
for (const line of lines) {
|
|
const trimmed = line.trim();
|
|
if (!trimmed) continue;
|
|
const [hash, name] = trimmed.split(/\s+/);
|
|
if (name === assetName) return hash.toLowerCase();
|
|
if (trimmed.endsWith(` ${assetName}`)) {
|
|
return trimmed.split(/\s+/)[0]?.toLowerCase() ?? null;
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
|
|
let didBuildOpenworkServer = false;
|
|
const shouldBuildOpenworkServer =
|
|
forceBuild || !existsSync(openworkServerBuildPath) || isStubBinary(openworkServerBuildPath);
|
|
|
|
if (shouldBuildOpenworkServer) {
|
|
mkdirSync(sidecarDir, { recursive: true });
|
|
if (existsSync(openworkServerBuildPath)) {
|
|
try {
|
|
unlinkSync(openworkServerBuildPath);
|
|
} catch {
|
|
// ignore
|
|
}
|
|
}
|
|
const openworkServerScript = resolveBuildScript(openworkServerDir);
|
|
if (!existsSync(openworkServerScript)) {
|
|
console.error(`OpenWork server build script not found at ${openworkServerScript}`);
|
|
process.exit(1);
|
|
}
|
|
const openworkServerArgs = [openworkServerScript, "--outdir", sidecarDir, "--filename", "openwork-server"];
|
|
if (bunTarget) {
|
|
openworkServerArgs.push("--target", bunTarget);
|
|
}
|
|
const buildResult = spawnSync("bun", openworkServerArgs, {
|
|
cwd: openworkServerDir,
|
|
stdio: "inherit",
|
|
shell: true,
|
|
});
|
|
|
|
if (buildResult.status !== 0) {
|
|
process.exit(buildResult.status ?? 1);
|
|
}
|
|
|
|
didBuildOpenworkServer = true;
|
|
}
|
|
|
|
if (existsSync(openworkServerBuildPath)) {
|
|
const shouldCopyCanonical = didBuildOpenworkServer || !existsSync(openworkServerPath) || isStubBinary(openworkServerPath);
|
|
if (shouldCopyCanonical && openworkServerBuildPath !== openworkServerPath) {
|
|
try {
|
|
if (existsSync(openworkServerPath)) {
|
|
unlinkSync(openworkServerPath);
|
|
}
|
|
} catch {
|
|
// ignore
|
|
}
|
|
copyFileSync(openworkServerBuildPath, openworkServerPath);
|
|
}
|
|
|
|
if (openworkServerTargetPath) {
|
|
const shouldCopyTarget =
|
|
didBuildOpenworkServer || !existsSync(openworkServerTargetPath) || isStubBinary(openworkServerTargetPath);
|
|
if (shouldCopyTarget && openworkServerBuildPath !== openworkServerTargetPath) {
|
|
try {
|
|
if (existsSync(openworkServerTargetPath)) {
|
|
unlinkSync(openworkServerTargetPath);
|
|
}
|
|
} catch {
|
|
// ignore
|
|
}
|
|
copyFileSync(openworkServerBuildPath, openworkServerTargetPath);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!existingOpencodeVersion && opencodeCandidatePath) {
|
|
existingOpencodeVersion =
|
|
existsSync(opencodeCandidatePath) && !isStubBinary(opencodeCandidatePath)
|
|
? readBinaryVersion(opencodeCandidatePath)
|
|
: null;
|
|
}
|
|
|
|
const normalizedOpencodeVersion = normalizeVersion(opencodeVersion);
|
|
|
|
if (!normalizedOpencodeVersion) {
|
|
console.error(
|
|
`OpenCode version could not be resolved from ${constantsPath}.`
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
const opencodeAssetByTarget = {
|
|
"aarch64-apple-darwin": "opencode-darwin-arm64.zip",
|
|
"x86_64-apple-darwin": "opencode-darwin-x64-baseline.zip",
|
|
"x86_64-unknown-linux-gnu": "opencode-linux-x64-baseline.tar.gz",
|
|
"aarch64-unknown-linux-gnu": "opencode-linux-arm64.tar.gz",
|
|
"x86_64-pc-windows-msvc": "opencode-windows-x64-baseline.zip",
|
|
"aarch64-pc-windows-msvc": "opencode-windows-arm64.zip",
|
|
};
|
|
|
|
const opencodeAsset =
|
|
opencodeAssetOverride ?? (resolvedTargetTriple ? opencodeAssetByTarget[resolvedTargetTriple] : null);
|
|
|
|
const opencodeUrl = opencodeAsset
|
|
? `https://github.com/${opencodeGithubRepo}/releases/download/v${normalizedOpencodeVersion}/${opencodeAsset}`
|
|
: null;
|
|
|
|
const shouldDownloadOpencode =
|
|
!opencodeCandidatePath ||
|
|
!existsSync(opencodeCandidatePath) ||
|
|
isStubBinary(opencodeCandidatePath) ||
|
|
!existingOpencodeVersion ||
|
|
existingOpencodeVersion !== normalizedOpencodeVersion;
|
|
|
|
if (!shouldDownloadOpencode) {
|
|
console.log(`OpenCode sidecar already present (${existingOpencodeVersion}).`);
|
|
}
|
|
|
|
if (shouldDownloadOpencode) {
|
|
if (!opencodeAsset || !opencodeUrl) {
|
|
console.error(
|
|
`No OpenCode asset configured for target ${resolvedTargetTriple ?? "unknown"}. Set OPENCODE_ASSET to override.`
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
mkdirSync(sidecarDir, { recursive: true });
|
|
|
|
const stamp = Date.now();
|
|
const archivePath = join(tmpdir(), `opencode-${stamp}-${opencodeAsset}`);
|
|
const extractDir = join(tmpdir(), `opencode-${stamp}`);
|
|
|
|
mkdirSync(extractDir, { recursive: true });
|
|
|
|
if (process.platform === "win32") {
|
|
const psQuote = (value) => `'${value.replace(/'/g, "''")}'`;
|
|
const psScript = [
|
|
"$ErrorActionPreference = 'Stop'",
|
|
`Invoke-WebRequest -Uri ${psQuote(opencodeUrl)} -OutFile ${psQuote(archivePath)}`,
|
|
`Expand-Archive -Path ${psQuote(archivePath)} -DestinationPath ${psQuote(extractDir)} -Force`,
|
|
].join("; ");
|
|
|
|
const result = spawnSync("powershell", ["-NoProfile", "-Command", psScript], {
|
|
stdio: "inherit",
|
|
});
|
|
|
|
if (result.status !== 0) {
|
|
process.exit(result.status ?? 1);
|
|
}
|
|
} else {
|
|
const downloadResult = spawnSync("curl", ["-fsSL", "-o", archivePath, opencodeUrl], {
|
|
stdio: "inherit",
|
|
});
|
|
if (downloadResult.status !== 0) {
|
|
process.exit(downloadResult.status ?? 1);
|
|
}
|
|
|
|
mkdirSync(extractDir, { recursive: true });
|
|
|
|
if (opencodeAsset.endsWith(".zip")) {
|
|
const unzipResult = spawnSync("unzip", ["-q", archivePath, "-d", extractDir], {
|
|
stdio: "inherit",
|
|
});
|
|
if (unzipResult.status !== 0) {
|
|
process.exit(unzipResult.status ?? 1);
|
|
}
|
|
} else if (opencodeAsset.endsWith(".tar.gz")) {
|
|
const tarResult = spawnSync("tar", ["-xzf", archivePath, "-C", extractDir], {
|
|
stdio: "inherit",
|
|
});
|
|
if (tarResult.status !== 0) {
|
|
process.exit(tarResult.status ?? 1);
|
|
}
|
|
} else {
|
|
console.error(`Unknown OpenCode archive type: ${opencodeAsset}`);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
const extractedBinary = findOpencodeBinary(extractDir);
|
|
if (!extractedBinary) {
|
|
console.error("OpenCode binary not found after extraction.");
|
|
process.exit(1);
|
|
}
|
|
|
|
const opencodeTargets = [opencodeTargetPath, opencodePath].filter(Boolean);
|
|
for (const target of opencodeTargets) {
|
|
try {
|
|
if (existsSync(target)) {
|
|
unlinkSync(target);
|
|
}
|
|
} catch {
|
|
// ignore
|
|
}
|
|
copyFileSync(extractedBinary, target);
|
|
try {
|
|
chmodSync(target, 0o755);
|
|
} catch {
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
console.log(`OpenCode sidecar updated to ${normalizedOpencodeVersion}.`);
|
|
}
|
|
|
|
const opencodeRouterPkgRaw = readFileSync(resolve(opencodeRouterDir, "package.json"), "utf8");
|
|
const opencodeRouterPkg = JSON.parse(opencodeRouterPkgRaw);
|
|
const opencodeRouterPkgVersion = String(opencodeRouterPkg.version ?? "").trim();
|
|
const normalizedOpenCodeRouterVersion = opencodeRouterVersion?.startsWith("v")
|
|
? opencodeRouterVersion.slice(1)
|
|
: opencodeRouterVersion;
|
|
const expectedOpenCodeRouterVersion = normalizedOpenCodeRouterVersion || opencodeRouterPkgVersion;
|
|
|
|
if (!expectedOpenCodeRouterVersion) {
|
|
console.error("OpenCodeRouter version missing. Set opencodeRouterVersion or ensure package.json has version.");
|
|
process.exit(1);
|
|
}
|
|
|
|
if (normalizedOpenCodeRouterVersion && opencodeRouterPkgVersion && normalizedOpenCodeRouterVersion !== opencodeRouterPkgVersion) {
|
|
console.error(`OpenCodeRouter version mismatch: desktop=${normalizedOpenCodeRouterVersion}, package=${opencodeRouterPkgVersion}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
let didBuildOpenCodeRouter = false;
|
|
const shouldBuildOpenCodeRouter = forceBuild || !existsSync(opencodeRouterBuildPath) || isStubBinary(opencodeRouterBuildPath);
|
|
if (shouldBuildOpenCodeRouter) {
|
|
mkdirSync(sidecarDir, { recursive: true });
|
|
if (existsSync(opencodeRouterBuildPath)) {
|
|
try {
|
|
unlinkSync(opencodeRouterBuildPath);
|
|
} catch {
|
|
// ignore
|
|
}
|
|
}
|
|
const opencodeRouterScript = resolveBuildScript(opencodeRouterDir);
|
|
if (!existsSync(opencodeRouterScript)) {
|
|
console.error(`OpenCodeRouter build script not found at ${opencodeRouterScript}`);
|
|
process.exit(1);
|
|
}
|
|
const opencodeRouterArgs = [opencodeRouterScript, "--outdir", sidecarDir, "--filename", "opencode-router"];
|
|
if (bunTarget) {
|
|
opencodeRouterArgs.push("--target", bunTarget);
|
|
}
|
|
const result = spawnSync("bun", opencodeRouterArgs, { cwd: opencodeRouterDir, stdio: "inherit", shell: true });
|
|
if (result.status !== 0) {
|
|
process.exit(result.status ?? 1);
|
|
}
|
|
|
|
didBuildOpenCodeRouter = true;
|
|
}
|
|
|
|
if (existsSync(opencodeRouterBuildPath)) {
|
|
const shouldCopyCanonical = didBuildOpenCodeRouter || !existsSync(opencodeRouterPath) || isStubBinary(opencodeRouterPath);
|
|
if (shouldCopyCanonical && opencodeRouterBuildPath !== opencodeRouterPath) {
|
|
try {
|
|
if (existsSync(opencodeRouterPath)) unlinkSync(opencodeRouterPath);
|
|
} catch {
|
|
// ignore
|
|
}
|
|
copyFileSync(opencodeRouterBuildPath, opencodeRouterPath);
|
|
}
|
|
|
|
if (opencodeRouterTargetPath) {
|
|
const shouldCopyTarget = didBuildOpenCodeRouter || !existsSync(opencodeRouterTargetPath) || isStubBinary(opencodeRouterTargetPath);
|
|
if (shouldCopyTarget && opencodeRouterBuildPath !== opencodeRouterTargetPath) {
|
|
try {
|
|
if (existsSync(opencodeRouterTargetPath)) unlinkSync(opencodeRouterTargetPath);
|
|
} catch {
|
|
// ignore
|
|
}
|
|
copyFileSync(opencodeRouterBuildPath, opencodeRouterTargetPath);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Build orchestrator sidecar
|
|
let didBuildOrchestrator = false;
|
|
const shouldBuildOrchestrator =
|
|
forceBuild || !existsSync(orchestratorBuildPath) || isStubBinary(orchestratorBuildPath);
|
|
if (shouldBuildOrchestrator) {
|
|
mkdirSync(sidecarDir, { recursive: true });
|
|
if (existsSync(orchestratorBuildPath)) {
|
|
try {
|
|
unlinkSync(orchestratorBuildPath);
|
|
} catch {
|
|
// ignore
|
|
}
|
|
}
|
|
const orchestratorBuildScript = resolveBuildScript(orchestratorDir);
|
|
if (!existsSync(orchestratorBuildScript)) {
|
|
console.error(`Orchestrator build script not found at ${orchestratorBuildScript}`);
|
|
process.exit(1);
|
|
}
|
|
const orchestratorArgs = [
|
|
orchestratorBuildScript,
|
|
"--outdir",
|
|
sidecarDir,
|
|
"--filename",
|
|
orchestratorBaseName,
|
|
];
|
|
if (bunTarget) {
|
|
orchestratorArgs.push("--target", bunTarget);
|
|
}
|
|
const result = spawnSync("bun", orchestratorArgs, {
|
|
cwd: orchestratorDir,
|
|
stdio: "inherit",
|
|
shell: true,
|
|
env: {
|
|
...process.env,
|
|
NODE_ENV: "production",
|
|
BUN_ENV: "production",
|
|
},
|
|
});
|
|
if (result.status !== 0) {
|
|
process.exit(result.status ?? 1);
|
|
}
|
|
|
|
didBuildOrchestrator = true;
|
|
}
|
|
|
|
if (existsSync(orchestratorBuildPath)) {
|
|
const shouldCopyCanonical =
|
|
didBuildOrchestrator || !existsSync(orchestratorPath) || isStubBinary(orchestratorPath);
|
|
if (shouldCopyCanonical && orchestratorBuildPath !== orchestratorPath) {
|
|
try {
|
|
if (existsSync(orchestratorPath)) unlinkSync(orchestratorPath);
|
|
} catch {
|
|
// ignore
|
|
}
|
|
copyFileSync(orchestratorBuildPath, orchestratorPath);
|
|
}
|
|
|
|
if (orchestratorTargetPath) {
|
|
const shouldCopyTarget =
|
|
didBuildOrchestrator ||
|
|
!existsSync(orchestratorTargetPath) ||
|
|
isStubBinary(orchestratorTargetPath);
|
|
if (shouldCopyTarget && orchestratorBuildPath !== orchestratorTargetPath) {
|
|
try {
|
|
if (existsSync(orchestratorTargetPath)) unlinkSync(orchestratorTargetPath);
|
|
} catch {
|
|
// ignore
|
|
}
|
|
copyFileSync(orchestratorBuildPath, orchestratorTargetPath);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Build chrome-devtools-mcp shim sidecar
|
|
let didBuildChromeDevtools = false;
|
|
const shouldBuildChromeDevtools =
|
|
forceBuild || !existsSync(chromeDevtoolsBuildPath) || isStubBinary(chromeDevtoolsBuildPath);
|
|
if (shouldBuildChromeDevtools) {
|
|
mkdirSync(sidecarDir, { recursive: true });
|
|
if (existsSync(chromeDevtoolsBuildPath)) {
|
|
try {
|
|
unlinkSync(chromeDevtoolsBuildPath);
|
|
} catch {
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
if (!existsSync(chromeDevtoolsShimPath)) {
|
|
console.error(`Chrome DevTools MCP shim source not found at ${chromeDevtoolsShimPath}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
const chromeDevtoolsArgs = [
|
|
"build",
|
|
"--compile",
|
|
chromeDevtoolsShimPath,
|
|
"--outfile",
|
|
chromeDevtoolsBuildPath,
|
|
];
|
|
if (bunTarget) {
|
|
chromeDevtoolsArgs.push("--target", bunTarget);
|
|
}
|
|
|
|
const result = spawnSync("bun", chromeDevtoolsArgs, {
|
|
cwd: __dirname,
|
|
stdio: "inherit",
|
|
shell: true,
|
|
env: {
|
|
...process.env,
|
|
NODE_ENV: "production",
|
|
BUN_ENV: "production",
|
|
},
|
|
});
|
|
if (result.status !== 0) {
|
|
process.exit(result.status ?? 1);
|
|
}
|
|
|
|
didBuildChromeDevtools = true;
|
|
}
|
|
|
|
if (existsSync(chromeDevtoolsBuildPath)) {
|
|
const shouldCopyCanonical =
|
|
didBuildChromeDevtools || !existsSync(chromeDevtoolsPath) || isStubBinary(chromeDevtoolsPath);
|
|
if (shouldCopyCanonical && chromeDevtoolsBuildPath !== chromeDevtoolsPath) {
|
|
try {
|
|
if (existsSync(chromeDevtoolsPath)) unlinkSync(chromeDevtoolsPath);
|
|
} catch {
|
|
// ignore
|
|
}
|
|
copyFileSync(chromeDevtoolsBuildPath, chromeDevtoolsPath);
|
|
}
|
|
|
|
if (chromeDevtoolsTargetPath) {
|
|
const shouldCopyTarget =
|
|
didBuildChromeDevtools ||
|
|
!existsSync(chromeDevtoolsTargetPath) ||
|
|
isStubBinary(chromeDevtoolsTargetPath);
|
|
if (shouldCopyTarget && chromeDevtoolsBuildPath !== chromeDevtoolsTargetPath) {
|
|
try {
|
|
if (existsSync(chromeDevtoolsTargetPath)) unlinkSync(chromeDevtoolsTargetPath);
|
|
} catch {
|
|
// ignore
|
|
}
|
|
copyFileSync(chromeDevtoolsBuildPath, chromeDevtoolsTargetPath);
|
|
}
|
|
}
|
|
}
|
|
|
|
const openworkServerVersion = (() => {
|
|
try {
|
|
const raw = readFileSync(resolve(openworkServerDir, "package.json"), "utf8");
|
|
return String(JSON.parse(raw).version ?? "").trim();
|
|
} catch {
|
|
return null;
|
|
}
|
|
})();
|
|
|
|
const orchestratorVersion = (() => {
|
|
try {
|
|
const raw = readFileSync(resolve(orchestratorDir, "package.json"), "utf8");
|
|
return String(JSON.parse(raw).version ?? "").trim();
|
|
} catch {
|
|
return null;
|
|
}
|
|
})();
|
|
|
|
const versions = {
|
|
opencode: {
|
|
version: normalizedOpencodeVersion,
|
|
sha256: opencodeCandidatePath && existsSync(opencodeCandidatePath) ? sha256File(opencodeCandidatePath) : null,
|
|
},
|
|
"openwork-server": {
|
|
version: openworkServerVersion,
|
|
sha256: existsSync(openworkServerPath) ? sha256File(openworkServerPath) : null,
|
|
},
|
|
opencodeRouter: {
|
|
version: expectedOpenCodeRouterVersion,
|
|
sha256: existsSync(opencodeRouterPath) ? sha256File(opencodeRouterPath) : null,
|
|
},
|
|
"openwork-orchestrator": {
|
|
version: orchestratorVersion,
|
|
sha256: existsSync(orchestratorPath) ? sha256File(orchestratorPath) : null,
|
|
},
|
|
"chrome-devtools-mcp": {
|
|
version: chromeDevtoolsMcpVersion,
|
|
sha256: existsSync(chromeDevtoolsPath) ? sha256File(chromeDevtoolsPath) : null,
|
|
},
|
|
};
|
|
|
|
const missing = Object.entries(versions)
|
|
.filter(([, info]) => !info.version || !info.sha256)
|
|
.map(([name]) => name);
|
|
|
|
if (missing.length) {
|
|
console.error(`Sidecar version metadata incomplete for: ${missing.join(", ")}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
const versionsPath = join(sidecarDir, "versions.json");
|
|
try {
|
|
mkdirSync(sidecarDir, { recursive: true });
|
|
const content = JSON.stringify(versions, null, 2) + "\n";
|
|
writeFileSync(versionsPath, content, "utf8");
|
|
if (resolvedTargetTriple) {
|
|
const targetSuffix = process.platform === "win32" ? ".exe" : "";
|
|
const targetVersionsPath = join(sidecarDir, `versions.json-${resolvedTargetTriple}${targetSuffix}`);
|
|
writeFileSync(targetVersionsPath, content, "utf8");
|
|
}
|
|
} catch (error) {
|
|
console.error(`Failed to write versions.json: ${error}`);
|
|
process.exit(1);
|
|
}
|