Files

386 lines
12 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 sidecarOverride = process.env.OPENWORK_SIDECAR_DIR?.trim() || readArg("--outdir");
const sidecarDir = sidecarOverride ? resolve(sidecarOverride) : join(__dirname, "..", "resources", "sidecars");
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;
// 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 isWindowsTarget = process.platform === "win32" || resolvedTargetTriple?.includes("windows") === true;
const opencodeBaseName = isWindowsTarget ? "opencode.exe" : "opencode";
const opencodePath = join(sidecarDir, opencodeBaseName);
const opencodeTargetName = resolvedTargetTriple
? `opencode-${resolvedTargetTriple}${isWindowsTarget ? ".exe" : ""}`
: null;
const opencodeTargetPath = opencodeTargetName ? join(sidecarDir, opencodeTargetName) : null;
const opencodeCandidatePath = opencodeTargetPath ?? opencodePath;
let existingOpencodeVersion = null;
// openwork-server paths
const openworkServerDir = resolve(__dirname, "..", "..", "server");
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.exe") || file.endsWith("\\opencode.exe")) ??
candidates.find((file) => file.endsWith("/opencode") || file.endsWith("\\opencode")) ??
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 adHocSignDarwin = (filePath) => {
if (process.platform !== "darwin" || !filePath || !existsSync(filePath)) return;
const remove = spawnSync("codesign", ["--remove-signature", filePath], {
encoding: "utf8",
});
if (remove.error && remove.error.code === "ENOENT") {
throw new Error("codesign is required to prepare runnable macOS sidecars");
}
const sign = spawnSync("codesign", ["--force", "--sign", "-", filePath], {
encoding: "utf8",
});
if (sign.error) {
if (sign.error.code === "ENOENT") {
throw new Error("codesign is required to prepare runnable macOS sidecars");
}
throw sign.error;
}
if (sign.status !== 0) {
const stderr = sign.stderr?.trim();
throw new Error(`Failed to codesign ${filePath}${stderr ? `: ${stderr}` : ""}`);
}
};
const adHocSignDarwinSidecars = (paths) => {
if (process.platform !== "darwin") return;
for (const filePath of [...new Set(paths.filter(Boolean))]) {
adHocSignDarwin(filePath);
}
};
// openwork-server is no longer compiled as a sidecar binary — it runs
// in-process inside Electron via a direct import of the server library.
// Server binary copy/sign skipped — runs in-process.
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}.`);
}
adHocSignDarwinSidecars([
opencodePath,
opencodeTargetPath,
// openwork-server runs in-process — no binary to sign.
]);
const openworkServerVersion = (() => {
try {
const raw = readFileSync(resolve(openworkServerDir, "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: "in-process",
},
};
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 = isWindowsTarget ? ".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);
}