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>
197 lines
6.1 KiB
JavaScript
197 lines
6.1 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import { spawnSync } from "node:child_process";
|
|
import { chmodSync, copyFileSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { basename, join, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const runtimeRoot = resolve(fileURLToPath(new URL("..", import.meta.url)));
|
|
const repoRoot = resolve(runtimeRoot, "..", "..");
|
|
const outputDir = resolve(runtimeRoot, "bin");
|
|
const outputName = process.platform === "win32" ? "opencode.exe" : "opencode";
|
|
const outputPath = join(outputDir, outputName);
|
|
const versionStampPath = join(outputDir, "opencode.version");
|
|
|
|
function readJson(filePath) {
|
|
return JSON.parse(readFileSync(filePath, "utf8"));
|
|
}
|
|
|
|
function run(command, args, cwd) {
|
|
const result = spawnSync(command, args, {
|
|
cwd,
|
|
stdio: "inherit",
|
|
encoding: "utf8",
|
|
});
|
|
if (result.status !== 0) {
|
|
throw new Error(`Command failed: ${command} ${args.join(" ")}`);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function resolveOrchestratorPackageJson() {
|
|
const globalRoot = spawnSync("npm", ["root", "-g"], {
|
|
cwd: runtimeRoot,
|
|
encoding: "utf8",
|
|
});
|
|
|
|
if (globalRoot.status === 0) {
|
|
const candidate = resolve(globalRoot.stdout.trim(), "openwork-orchestrator", "package.json");
|
|
if (existsSync(candidate)) {
|
|
return candidate;
|
|
}
|
|
}
|
|
|
|
const repoCandidate = resolve(repoRoot, "packages", "orchestrator", "package.json");
|
|
if (existsSync(repoCandidate)) {
|
|
return repoCandidate;
|
|
}
|
|
|
|
throw new Error("Unable to locate openwork-orchestrator package.json");
|
|
}
|
|
|
|
function resolveOpencodeVersion() {
|
|
const orchestratorPackageJson = resolveOrchestratorPackageJson();
|
|
const orchestratorRoot = resolve(orchestratorPackageJson, "..");
|
|
const versionPath = resolve(orchestratorRoot, "constants.json");
|
|
if (!existsSync(versionPath)) {
|
|
throw new Error(`Missing pinned constants file at ${versionPath}`);
|
|
}
|
|
const parsed = JSON.parse(readFileSync(versionPath, "utf8"));
|
|
const version = String(parsed.opencodeVersion ?? "").trim().replace(/^v/, "");
|
|
if (!version) {
|
|
throw new Error(`Pinned OpenCode version is missing from ${versionPath}`);
|
|
}
|
|
return version;
|
|
}
|
|
|
|
function resolveAssetName() {
|
|
const target = `${process.platform}-${process.arch}`;
|
|
const assets = {
|
|
"darwin-arm64": "opencode-darwin-arm64.zip",
|
|
"darwin-x64": "opencode-darwin-x64-baseline.zip",
|
|
"linux-arm64": "opencode-linux-arm64.tar.gz",
|
|
"linux-x64": "opencode-linux-x64-baseline.tar.gz",
|
|
"win32-arm64": "opencode-windows-arm64.zip",
|
|
"win32-x64": "opencode-windows-x64-baseline.zip",
|
|
};
|
|
|
|
const asset = assets[target];
|
|
if (!asset) {
|
|
throw new Error(`Unsupported platform for opencode bundle: ${target}`);
|
|
}
|
|
return asset;
|
|
}
|
|
|
|
async function downloadWithRetries(url, destination) {
|
|
let lastError = null;
|
|
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
|
try {
|
|
const response = await fetch(url);
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP ${response.status}`);
|
|
}
|
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
writeFileSync(destination, buffer);
|
|
return;
|
|
} catch (error) {
|
|
lastError = error;
|
|
if (attempt < 3) {
|
|
await new Promise((resolvePromise) => setTimeout(resolvePromise, attempt * 1000));
|
|
}
|
|
}
|
|
}
|
|
|
|
throw new Error(`Failed to download ${url}: ${String(lastError)}`);
|
|
}
|
|
|
|
function extractArchive(archivePath, outputDirectory) {
|
|
if (archivePath.endsWith(".tar.gz")) {
|
|
run("tar", ["-xzf", archivePath, "-C", outputDirectory], runtimeRoot);
|
|
return;
|
|
}
|
|
|
|
if (archivePath.endsWith(".zip")) {
|
|
if (process.platform === "win32") {
|
|
run(
|
|
"powershell",
|
|
[
|
|
"-NoProfile",
|
|
"-Command",
|
|
`Expand-Archive -Path '${archivePath.replace(/'/g, "''")}' -DestinationPath '${outputDirectory.replace(/'/g, "''")}' -Force`,
|
|
],
|
|
runtimeRoot,
|
|
);
|
|
return;
|
|
}
|
|
|
|
run("unzip", ["-q", archivePath, "-d", outputDirectory], runtimeRoot);
|
|
return;
|
|
}
|
|
|
|
throw new Error(`Unsupported archive format: ${basename(archivePath)}`);
|
|
}
|
|
|
|
function findBinary(searchRoot) {
|
|
const stack = [searchRoot];
|
|
while (stack.length > 0) {
|
|
const current = stack.pop();
|
|
if (!current) continue;
|
|
|
|
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
const entryPath = join(current, entry.name);
|
|
if (entry.isDirectory()) {
|
|
stack.push(entryPath);
|
|
continue;
|
|
}
|
|
if (entry.isFile() && entry.name === outputName) {
|
|
return entryPath;
|
|
}
|
|
}
|
|
}
|
|
|
|
throw new Error(`Unable to find ${outputName} inside extracted archive`);
|
|
}
|
|
|
|
const version = resolveOpencodeVersion();
|
|
const versionLabel = version;
|
|
|
|
if (
|
|
existsSync(outputPath) &&
|
|
existsSync(versionStampPath) &&
|
|
readFileSync(versionStampPath, "utf8").trim() === versionLabel
|
|
) {
|
|
console.log(`[den-worker-runtime] opencode ${versionLabel} already bundled at ${outputPath}`);
|
|
process.exit(0);
|
|
}
|
|
|
|
const assetName = resolveAssetName();
|
|
const downloadUrl = process.env.OPENWORK_OPENCODE_DOWNLOAD_URL?.trim()
|
|
|| (version
|
|
? `https://github.com/anomalyco/opencode/releases/download/v${version}/${assetName}`
|
|
: null);
|
|
if (!downloadUrl) {
|
|
throw new Error("Pinned OpenCode version is required to bundle the worker runtime");
|
|
}
|
|
const tempDir = mkdtempSync(join(tmpdir(), "den-worker-opencode-"));
|
|
const archivePath = join(tempDir, assetName);
|
|
const extractDir = join(tempDir, "extract");
|
|
|
|
mkdirSync(extractDir, { recursive: true });
|
|
mkdirSync(outputDir, { recursive: true });
|
|
|
|
try {
|
|
console.log(`[den-worker-runtime] downloading opencode ${versionLabel} from ${downloadUrl}`);
|
|
await downloadWithRetries(downloadUrl, archivePath);
|
|
extractArchive(archivePath, extractDir);
|
|
const extractedBinary = findBinary(extractDir);
|
|
copyFileSync(extractedBinary, outputPath);
|
|
if (process.platform !== "win32") {
|
|
chmodSync(outputPath, 0o755);
|
|
}
|
|
writeFileSync(versionStampPath, `${versionLabel}\n`, "utf8");
|
|
console.log(`[den-worker-runtime] bundled opencode ${versionLabel} at ${outputPath}`);
|
|
} finally {
|
|
rmSync(tempDir, { recursive: true, force: true });
|
|
}
|