源码快照 @ 2026-09-02 (source-only, 剔除非源码素材)

This commit is contained in:
agent-radar
2026-09-02 01:05:41 +08:00
commit 04bac1f6e2
2200 changed files with 489415 additions and 0 deletions
@@ -0,0 +1,44 @@
import { readFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const desktopRoot = resolve(__dirname, "..");
const repoRoot = resolve(desktopRoot, "../..");
const desktopBridgePath = resolve(repoRoot, "apps/app/src/app/lib/desktop.ts");
const electronMainPath = resolve(desktopRoot, "electron/main.mjs");
const desktopBridgeSource = readFileSync(desktopBridgePath, "utf8");
const electronMainSource = readFileSync(electronMainPath, "utf8");
const destructure = desktopBridgeSource.match(/const\s*\{([\s\S]*?)\}\s*=\s*desktopBridge;/);
if (!destructure?.[1]) {
throw new Error(`Could not find desktopBridge export destructure in ${desktopBridgePath}`);
}
const clientOnlyBridgeMethods = new Set([
// Pure helper implemented in apps/app/src/app/lib/desktop.ts and
// intentionally satisfied inside the renderer proxy, not over Electron IPC.
"resolveWorkspaceListSelectedId",
]);
const bridgeMethods = destructure[1]
.split(/\r?\n/)
.map((line) => line.replace(/\/\/.*$/, "").trim().replace(/,$/, ""))
.filter(Boolean)
.filter((name) => !clientOnlyBridgeMethods.has(name));
const electronHandlers = new Set(
// Registry entries look like `"workspaceCreate": async (event, ...args) =>`.
Array.from(electronMainSource.matchAll(/^ "([^"]+)": async \(event/gm)).map((match) => match[1]),
);
const missing = bridgeMethods.filter((name) => !electronHandlers.has(name));
if (missing.length > 0) {
console.error("Electron desktop bridge is missing IPC handlers:");
for (const name of missing) console.error(`- ${name}`);
process.exit(1);
}
console.log(`Electron desktop bridge covers ${bridgeMethods.length} renderer methods.`);
@@ -0,0 +1,181 @@
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;
@@ -0,0 +1,103 @@
const { spawnSync } = require("node:child_process");
const { existsSync, mkdtempSync, rmSync } = require("node:fs");
const { tmpdir } = require("node:os");
const path = require("node:path");
const computerUseHelperAppName = "OpenWork Computer Use.app";
function run(command, args) {
const result = spawnSync(command, args, { stdio: "inherit" });
if (result.status !== 0) {
throw new Error(`${command} ${args.join(" ")} failed with status ${result.status}`);
}
}
async function runWithRetry(command, args, attempts, baseDelayMs = 30_000) {
for (let attempt = 1; ; attempt++) {
const result = spawnSync(command, args, { stdio: "inherit" });
if (result.status === 0) return;
if (attempt >= attempts) {
throw new Error(`${command} ${args.join(" ")} failed with status ${result.status} after ${attempts} attempts`);
}
const delayMs = baseDelayMs * attempt;
console.warn(
`[electron-after-sign] ${command} ${args.join(" ")} failed with status ${result.status}; retrying in ${delayMs / 1000}s (attempt ${attempt}/${attempts}).`,
);
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
}
function requireEnv(name) {
const value = process.env[name];
if (!value) {
throw new Error(`${name} is required to notarize the Electron macOS app`);
}
return value;
}
function computerUseHelperPath(appPath) {
return path.join(appPath, "Contents", "Resources", "helpers", computerUseHelperAppName);
}
function verifyComputerUseHelper(appPath, requireDistributionSignature) {
const helperPath = computerUseHelperPath(appPath);
if (!existsSync(helperPath)) {
throw new Error(`Computer Use helper app is missing from packaged app: ${helperPath}`);
}
run("codesign", ["--verify", "--deep", "--strict", "--verbose=2", helperPath]);
if (!requireDistributionSignature) return;
const result = spawnSync("codesign", ["--display", "--verbose=4", helperPath], { encoding: "utf8" });
if (result.error) throw result.error;
if (result.status !== 0) {
throw new Error(`codesign --display failed for Computer Use helper with status ${result.status}`);
}
if (result.stderr.includes("Signature=adhoc")) {
throw new Error("Computer Use helper app is ad-hoc signed; notarized builds require a Developer ID signature.");
}
}
async function afterSign(context) {
if (context.electronPlatformName !== "darwin") return;
if (process.env.MACOS_NOTARIZE !== "true") {
console.warn("[electron-after-sign] MACOS_NOTARIZE is not true; skipping notarization.");
return;
}
const appName = `${context.packager.appInfo.productFilename}.app`;
const appPath = path.join(context.appOutDir, appName);
verifyComputerUseHelper(appPath, process.env.MACOS_NOTARIZE === "true");
const notaryTempDir = mkdtempSync(path.join(tmpdir(), "openwork-electron-notary-"));
const notaryZipPath = path.join(notaryTempDir, `${context.packager.appInfo.productFilename}-notary.zip`);
const keyPath = requireEnv("APPLE_API_KEY_PATH");
const keyId = requireEnv("APPLE_API_KEY");
const issuer = requireEnv("APPLE_API_ISSUER");
try {
run("ditto", ["-c", "-k", "--keepParent", appPath, notaryZipPath]);
run("xcrun", [
"notarytool",
"submit",
notaryZipPath,
"--key",
keyPath,
"--key-id",
keyId,
"--issuer",
issuer,
"--wait",
]);
// Notarization tickets can take minutes to propagate to Apple's CDN after acceptance; stapler can transiently fail with status 65 ("CloudKit query failed").
await runWithRetry("xcrun", ["stapler", "staple", appPath], 5);
run("xcrun", ["stapler", "validate", appPath]);
} finally {
rmSync(notaryTempDir, { recursive: true, force: true });
}
}
module.exports = afterSign;
module.exports.default = afterSign;
module.exports.runWithRetry = runWithRetry;
+97
View File
@@ -0,0 +1,97 @@
import { spawnSync } from "node:child_process";
import { copyFileSync, cpSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const desktopRoot = resolve(__dirname, "..");
const repoRoot = resolve(desktopRoot, "../..");
const electronSidecarDir = resolve(desktopRoot, "resources", "sidecars");
const electronHelperDir = resolve(desktopRoot, "resources", "helpers");
const electronRoot = resolve(desktopRoot, "electron");
const packagedServerRoot = resolve(desktopRoot, "server");
const packagedRuntimeRoot = resolve(desktopRoot, ".electron-runtime", "node_modules");
const sentryBuildConfigPath = resolve(desktopRoot, ".electron-runtime", "openwork-sentry.json");
const pnpmCmd = process.platform === "win32" ? "pnpm.cmd" : "pnpm";
const nodeCmd = process.execPath;
function needsShell(command) {
return process.platform === "win32" && /\.(cmd|bat)$/i.test(command);
}
function run(command, args, cwd, env) {
const result = spawnSync(command, args, {
cwd,
stdio: "inherit",
shell: needsShell(command),
env: env ? { ...process.env, ...env } : process.env,
});
if (result.status !== 0) {
process.exit(result.status ?? 1);
}
}
function writeSentryBuildConfig() {
const dsn = process.env.OPENWORK_DESKTOP_SENTRY_DSN?.trim() ?? "";
const tracesSampleRateRaw = process.env.OPENWORK_DESKTOP_SENTRY_TRACES_SAMPLE_RATE?.trim() ?? "";
const tracesSampleRate = tracesSampleRateRaw ? Number(tracesSampleRateRaw) : 0.01;
const config = {
dsn: dsn || null,
tracesSampleRate: Number.isFinite(tracesSampleRate) && tracesSampleRate >= 0 && tracesSampleRate <= 1
? tracesSampleRate
: 0.01,
};
writeFileSync(sentryBuildConfigPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
}
run(nodeCmd, [resolve(__dirname, "prepare-sidecar.mjs"), "--force", "--outdir", electronSidecarDir], desktopRoot);
run(nodeCmd, [resolve(__dirname, "prepare-computer-use-helper.mjs"), "--force", "--outdir", electronHelperDir], desktopRoot);
run(nodeCmd, [resolve(__dirname, "prepare-runtime-node-modules.mjs"), "--outdir", packagedRuntimeRoot], desktopRoot);
writeSentryBuildConfig();
// Build the server TS → JS so Electron can import it in-process
run(pnpmCmd, ["--filter", "openwork-server", "build"], repoRoot);
// automation-runner.mjs imports @openwork/headless-threads through its
// published "default" export (dist/index.js); build it so plain-node
// consumers resolve it in packaged layouts.
run(pnpmCmd, ["--filter", "@openwork/headless-threads", "build"], repoRoot);
// OPENWORK_ELECTRON_BUILD tells Vite to emit relative asset paths so
// index.html resolves /assets/* correctly when loaded via file:// from
// inside the packaged .app bundle.
run(pnpmCmd, ["--filter", "@openwork/app", "build"], repoRoot, {
OPENWORK_ELECTRON_BUILD: "1",
});
// Copy constants.json next to server dist so the packaged asar can resolve it.
// Also patch the compiled import path so it works from both dev and packaged layouts.
const serverDistDir = resolve(repoRoot, "apps", "server", "dist");
const constantsSrc = resolve(repoRoot, "constants.json");
copyFileSync(constantsSrc, resolve(serverDistDir, "constants.json"));
const serverJsPath = resolve(serverDistDir, "server.js");
const serverJsSrc = readFileSync(serverJsPath, "utf8");
const patched = serverJsSrc.replace(
/from\s+["']\.\.\/\.\.\/\.\.\/constants\.json["']/,
'from "./constants.json"',
);
if (patched !== serverJsSrc) {
writeFileSync(serverJsPath, patched, "utf8");
}
rmSync(packagedServerRoot, { recursive: true, force: true });
cpSync(serverDistDir, resolve(packagedServerRoot, "dist"), { recursive: true });
copyFileSync(resolve(repoRoot, "apps", "server", "package.json"), resolve(packagedServerRoot, "package.json"));
for (const fileName of readdirSync(electronRoot).filter((name) => name.endsWith(".mjs")).sort()) {
run(nodeCmd, ["--check", resolve(electronRoot, fileName)], repoRoot);
}
run(nodeCmd, [resolve(__dirname, "check-electron-bridge.mjs")], repoRoot);
process.stdout.write(
`${JSON.stringify(
{
ok: true,
renderer: "apps/app/dist",
electronMain: "apps/desktop/electron/main.mjs",
electronPreload: "apps/desktop/electron/preload.mjs",
},
null,
2,
)}\n`,
);
+299
View File
@@ -0,0 +1,299 @@
import { spawn, spawnSync } from "node:child_process";
import net from "node:net";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const desktopRoot = resolve(__dirname, "..");
const repoRoot = resolve(desktopRoot, "../..");
const electronSidecarDir = resolve(desktopRoot, "resources", "sidecars");
const electronHelperDir = resolve(desktopRoot, "resources", "helpers");
const defaultDevDataDir = resolve(
process.env.HOME ?? process.env.USERPROFILE ?? repoRoot,
".openwork",
"openwork-server-dev",
);
const pnpmCmd = process.platform === "win32" ? "pnpm.cmd" : "pnpm";
const nodeCmd = process.execPath;
async function findFreeTcpPort() {
return new Promise((resolvePort, rejectPort) => {
const server = net.createServer();
server.once("error", rejectPort);
server.listen({ port: 0, host: "127.0.0.1" }, () => {
const address = server.address();
server.close(() => {
if (address && typeof address === "object") {
resolvePort(address.port);
return;
}
rejectPort(new Error("Unable to resolve a free Vite dev port"));
});
});
});
}
const rawPort = process.env.PORT?.trim() ?? "";
const portValue = Number.parseInt(rawPort, 10);
const devPort = rawPort === "0"
? await findFreeTcpPort()
: Number.isFinite(portValue) && portValue > 0 ? portValue : 5173;
if (rawPort === "0") {
console.log(`[electron-dev] Vite dev server will use free port ${devPort}`);
}
const explicitStartUrl = process.env.OPENWORK_ELECTRON_START_URL?.trim() || "";
const startUrl = explicitStartUrl || `http://localhost:${devPort}`;
const viteProbeUrls = explicitStartUrl
? [explicitStartUrl]
: [
`http://127.0.0.1:${devPort}`,
`http://[::1]:${devPort}`,
`http://localhost:${devPort}`,
];
function needsShell(command) {
return process.platform === "win32" && /\.(cmd|bat)$/i.test(command);
}
function run(command, args, options = {}) {
return spawn(command, args, {
stdio: ["ignore", "inherit", "inherit"],
shell: needsShell(command),
...options,
});
}
function runSync(command, args, options = {}) {
const result = spawnSync(command, args, {
stdio: "inherit",
shell: needsShell(command),
...options,
});
if (result.status !== 0) {
process.exit(result.status ?? 1);
}
}
async function fetchWithTimeout(url, timeoutMs = 4000) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
return await fetch(url, { signal: controller.signal });
} finally {
clearTimeout(timer);
}
}
async function probeHost(host, port) {
return new Promise((resolveCheck) => {
const socket = net.createConnection({ host, port });
const onDone = (ready) => {
socket.removeAllListeners();
socket.destroy();
resolveCheck(ready);
};
socket.setTimeout(1200);
socket.once("connect", () => onDone(true));
socket.once("timeout", () => onDone(false));
socket.once("error", () => onDone(false));
});
}
async function looksLikeVite(url) {
try {
const response = await fetchWithTimeout(`${url}/@vite/client`);
if (!response.ok) return false;
const body = await response.text();
return body.includes("@vite/client") || body.includes("import.meta.hot");
} catch {
return false;
}
}
async function portIsOpenForVite(url) {
try {
const parsed = new URL(url);
const host = parsed.hostname.replace(/^\[|\]$/g, "");
const port = Number.parseInt(parsed.port || (parsed.protocol === "https:" ? "443" : "80"), 10);
if (!Number.isFinite(port)) return false;
return probeHost(host, port);
} catch {
return false;
}
}
async function waitForVite(url, timeoutMs = 60_000) {
const startedAt = Date.now();
while (Date.now() - startedAt < timeoutMs) {
for (const candidate of [url, ...viteProbeUrls].filter(Boolean)) {
if (await looksLikeVite(candidate)) {
return candidate;
}
}
for (const candidate of [url, ...viteProbeUrls].filter(Boolean)) {
if (await portIsOpenForVite(candidate)) {
return candidate;
}
}
await new Promise((resolveDelay) => setTimeout(resolveDelay, 500));
}
throw new Error(`Timed out waiting for Vite dev server at ${viteProbeUrls.join(", ")}`);
}
function signalTree(child, signal) {
if (!child?.pid) return;
if (process.platform === "win32") {
try {
spawnSync("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore" });
} catch {
// ignore
}
return;
}
try {
process.kill(-child.pid, signal);
} catch {
try {
child.kill(signal);
} catch {
// ignore
}
}
}
function restoreTerminal() {
try {
if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
process.stdin.setRawMode(false);
}
} catch {
// ignore
}
}
function waitForExit(child, timeoutMs) {
if (!child || child.exitCode !== null || child.signalCode !== null) return Promise.resolve(true);
return new Promise((resolveWait) => {
let settled = false;
const finish = (clean) => {
if (settled) return;
settled = true;
clearTimeout(timer);
child.off("exit", onExit);
resolveWait(clean);
};
const onExit = () => finish(true);
const timer = setTimeout(() => finish(false), timeoutMs);
child.once("exit", onExit);
});
}
async function waitForChildren(children, timeoutMs) {
const results = await Promise.all(children.map((child) => waitForExit(child, timeoutMs)));
return results.every(Boolean);
}
let uiChild = null;
let electronChild = null;
let stopping = false;
async function stopAll(exitCode = 0) {
if (stopping) return;
stopping = true;
restoreTerminal();
const children = [electronChild, uiChild].filter(Boolean);
for (const child of children) signalTree(child, "SIGINT");
const stoppedCleanly = await waitForChildren(children, 2_000);
if (!stoppedCleanly) {
for (const child of children) signalTree(child, "SIGTERM");
await waitForChildren(children, 1_000);
}
restoreTerminal();
process.exit(exitCode);
}
process.once("SIGINT", () => void stopAll(130));
process.once("SIGTERM", () => void stopAll(143));
if (process.env.OPENWORK_ELECTRON_SKIP_SHARED_PREPARE !== "1") {
runSync(nodeCmd, [resolve(__dirname, "prepare-sidecar.mjs"), "--force", "--outdir", electronSidecarDir], { cwd: desktopRoot });
runSync(nodeCmd, [resolve(__dirname, "prepare-computer-use-helper.mjs"), "--force", "--outdir", electronHelperDir], { cwd: desktopRoot });
}
// Build workspace packages that Electron imports from their dist output.
console.log("[electron-dev] Building Electron workspace dependencies...");
runSync(pnpmCmd, ["--filter", "@openwork/headless-threads", "build"], { cwd: repoRoot });
runSync(pnpmCmd, ["--filter", "openwork-server", "build"], { cwd: repoRoot });
const initialProbeUrls = [startUrl, ...viteProbeUrls].filter(Boolean);
let viteReady = false;
for (const candidate of initialProbeUrls) {
if (await looksLikeVite(candidate)) {
viteReady = true;
break;
}
}
if (!viteReady) {
for (const candidate of initialProbeUrls) {
if (await portIsOpenForVite(candidate)) {
viteReady = true;
break;
}
}
}
if (!viteReady) {
uiChild = run(pnpmCmd, ["-w", "dev:ui"], {
cwd: repoRoot,
env: {
...process.env,
PORT: String(devPort),
OPENWORK_DEV_MODE: process.env.OPENWORK_DEV_MODE ?? "1",
OPENWORK_DATA_DIR: process.env.OPENWORK_DATA_DIR ?? defaultDevDataDir,
},
});
}
const resolvedStartUrl = await waitForVite(startUrl);
// Native dependencies installed for the host Node ABI must be rebuilt before
// Electron loads the embedded server and terminal runtime.
if (process.env.OPENWORK_ELECTRON_SKIP_NATIVE_REBUILD === "1") {
console.log("[electron-dev] Using prebuilt Electron native dependencies.");
} else {
console.log("[electron-dev] Rebuilding native dependencies for Electron...");
runSync(pnpmCmd, ["--filter", "@openwork/desktop", "run", "rebuild:electron-native"], { cwd: repoRoot });
}
// Optional Electron CDP for external debugging / raw CDP clients.
// NOT required for the built-in browser (uses native webContents APIs).
// Set OPENWORK_ELECTRON_REMOTE_DEBUG_PORT=9823 to enable.
const cdpPortRaw = process.env.OPENWORK_ELECTRON_REMOTE_DEBUG_PORT?.trim() ?? "";
const cdpPort = cdpPortRaw === "" || cdpPortRaw === "0" ? "" : cdpPortRaw;
const blankSlateArgs = process.argv.includes("--blank-slate") ? ["--blank-slate"] : [];
electronChild = run(pnpmCmd, ["exec", "electron", "./electron/main.mjs", ...blankSlateArgs], {
cwd: desktopRoot,
env: {
...process.env,
OPENWORK_DEV_MODE: process.env.OPENWORK_DEV_MODE ?? "1",
OPENWORK_DATA_DIR: process.env.OPENWORK_DATA_DIR ?? defaultDevDataDir,
OPENWORK_ELECTRON_START_URL: resolvedStartUrl,
...(cdpPort ? { OPENWORK_ELECTRON_REMOTE_DEBUG_PORT: cdpPort } : {}),
},
});
if (cdpPort) {
console.log(`[openwork] Electron CDP exposed at http://127.0.0.1:${cdpPort}`);
}
electronChild.on("exit", (code) => {
if (stopping) return;
void stopAll(code ?? 0);
});
@@ -0,0 +1,146 @@
import { spawnSync } from "node:child_process";
import { chmodSync, copyFileSync, existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const desktopRoot = resolve(__dirname, "..");
const repoRoot = resolve(desktopRoot, "../..");
const packagePath = resolve(repoRoot, "packages", "handsfree", "native", "HandsFree");
const iconPath = resolve(desktopRoot, "resources", "icons", "icon.icns");
const productName = "HandsFreeComputerUse";
const helperExecutableName = "ComputerUse";
const helperAppName = "OpenWork Computer Use.app";
const bundleIdentifier = "com.differentai.openwork.computer-use";
const readArg = (name) => {
const raw = process.argv.slice(2);
const direct = raw.find((arg) => arg.startsWith(`${name}=`));
if (direct) return direct.slice(name.length + 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 outDir = resolve(readArg("--outdir") ?? join(desktopRoot, "resources", "helpers"));
const force = hasFlag("--force") || process.env.OPENWORK_COMPUTER_USE_FORCE_BUILD === "1";
const appPath = join(outDir, helperAppName);
function run(command, args, options = {}) {
const result = spawnSync(command, args, {
encoding: "utf8",
stdio: options.stdio ?? "pipe",
...options,
});
if (result.error) throw result.error;
if (result.status !== 0) {
const stderr = result.stderr?.trim();
throw new Error(`${command} ${args.join(" ")} failed${stderr ? `: ${stderr}` : ""}`);
}
return result;
}
function signingIdentity() {
const fromEnv = process.env.OPENWORK_COMPUTER_USE_SIGN_IDENTITY;
if (fromEnv) return fromEnv;
const result = spawnSync("security", ["find-identity", "-v", "-p", "codesigning"], { encoding: "utf8" });
if (result.status !== 0) return "-";
const match = result.stdout.match(/"(Developer ID Application: [^"]+)"/);
// Prefer a stable Developer ID signature so macOS TCC permission grants
// (Accessibility, Screen Recording) survive rebuilds. Ad-hoc signatures are
// content hashes, so every rebuild would invalidate existing grants.
return match ? match[1] : "-";
}
function signHelperApp() {
if (process.platform !== "darwin") return;
const identity = signingIdentity();
const args = ["--force", "--deep", "--sign", identity];
if (identity !== "-") args.push("--options", "runtime");
const result = spawnSync("codesign", [...args, appPath], {
encoding: "utf8",
stdio: "pipe",
});
if (identity !== "-" && (result.status !== 0 || result.error)) {
// Keychain may refuse non-interactive signing; fall back to ad-hoc.
const fallback = spawnSync("codesign", ["--force", "--deep", "--sign", "-", appPath], {
encoding: "utf8",
stdio: "pipe",
});
if (fallback.status === 0) return;
}
if (result.error) {
if (result.error.code === "ENOENT") {
throw new Error("codesign is required to prepare the Computer Use helper app");
}
throw result.error;
}
if (result.status !== 0) {
throw new Error(`Failed to codesign ${appPath}: ${result.stderr?.trim() ?? "unknown error"}`);
}
}
function infoPlist() {
return `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>OpenWork Computer Use</string>
<key>CFBundleExecutable</key>
<string>${helperExecutableName}</string>
<key>CFBundleIdentifier</key>
<string>${bundleIdentifier}</string>
<key>CFBundleIconFile</key>
<string>AppIcon</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>OpenWork Computer Use</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSMinimumSystemVersion</key>
<string>14.0</string>
</dict>
</plist>
`;
}
if (process.platform !== "darwin") {
process.stdout.write(JSON.stringify({ ok: true, skipped: true, reason: "computer-use-helper-is-macos-only" }, null, 2) + "\n");
process.exit(0);
}
if (!force && existsSync(join(appPath, "Contents", "MacOS", helperExecutableName))) {
process.stdout.write(JSON.stringify({ ok: true, skipped: true, appPath }, null, 2) + "\n");
process.exit(0);
}
run("swift", ["build", "--package-path", packagePath, "-c", "release", "--product", productName], { stdio: "inherit" });
const binPathResult = run("swift", ["build", "--package-path", packagePath, "-c", "release", "--show-bin-path"]);
const binDir = binPathResult.stdout.trim();
const builtExecutable = join(binDir, productName);
if (!existsSync(builtExecutable)) {
throw new Error(`Swift build succeeded, but ${builtExecutable} was not found`);
}
rmSync(appPath, { recursive: true, force: true });
mkdirSync(join(appPath, "Contents", "MacOS"), { recursive: true });
mkdirSync(join(appPath, "Contents", "Resources"), { recursive: true });
writeFileSync(join(appPath, "Contents", "Info.plist"), infoPlist(), "utf8");
writeFileSync(join(appPath, "Contents", "PkgInfo"), "APPL????", "utf8");
copyFileSync(builtExecutable, join(appPath, "Contents", "MacOS", helperExecutableName));
if (existsSync(iconPath)) {
copyFileSync(iconPath, join(appPath, "Contents", "Resources", "AppIcon.icns"));
}
chmodSync(join(appPath, "Contents", "MacOS", helperExecutableName), 0o755);
signHelperApp();
process.stdout.write(JSON.stringify({ ok: true, appPath, executable: join(appPath, "Contents", "MacOS", helperExecutableName) }, null, 2) + "\n");
@@ -0,0 +1,90 @@
import { cpSync, mkdirSync, readFileSync, rmSync } from "node:fs";
import { createRequire } from "node:module";
import { dirname, join, relative, resolve, sep } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
const dirnameHere = dirname(fileURLToPath(import.meta.url));
const desktopRoot = resolve(dirnameHere, "..");
const requiredRoots = [{
name: "@modelcontextprotocol/sdk",
resolveTarget: "@modelcontextprotocol/sdk/validation/ajv",
}];
function packageRoot(name, fromPackageJson, resolveTarget = name) {
const requireFromPackage = createRequire(pathToFileURL(fromPackageJson));
let current;
try {
current = dirname(requireFromPackage.resolve(resolveTarget));
} catch (error) {
const linkedPackageRoot = resolve(dirname(dirname(fromPackageJson)), name);
try {
const linkedPackageJson = JSON.parse(readFileSync(join(linkedPackageRoot, "package.json"), "utf8"));
if (linkedPackageJson.name === name) return { root: linkedPackageRoot, packageJson: linkedPackageJson };
} catch {
// Preserve the original module-resolution error below.
}
throw error;
}
while (current !== dirname(current)) {
try {
const packageJson = JSON.parse(readFileSync(join(current, "package.json"), "utf8"));
if (packageJson.name === name) return { root: current, packageJson };
} catch {
// Keep walking from the resolved entry to its package root.
}
current = dirname(current);
}
throw new Error(`Could not resolve runtime package root for ${name}.`);
}
export function stageRuntimeNodeModules(outdir) {
const output = resolve(outdir);
const desktopPackageJson = resolve(desktopRoot, "package.json");
const staged = [];
const rootPackages = new Map();
const placements = new Set();
rmSync(output, { recursive: true, force: true });
mkdirSync(output, { recursive: true });
function stagePackage({ name, fromPackageJson, resolveTarget }, parentDestination, forceRoot = false) {
const resolvedPackage = packageRoot(name, fromPackageJson, resolveTarget);
const rootPackage = rootPackages.get(name);
if (!rootPackage) rootPackages.set(name, resolvedPackage.root);
if (!forceRoot && rootPackage === resolvedPackage.root) return;
const nodeModulesRoot = forceRoot || !rootPackage
? output
: resolve(parentDestination, "node_modules");
const destination = resolve(nodeModulesRoot, name);
const placementKey = `${destination}\0${resolvedPackage.root}`;
if (placements.has(placementKey)) return;
placements.add(placementKey);
mkdirSync(dirname(destination), { recursive: true });
cpSync(resolvedPackage.root, destination, {
recursive: true,
dereference: true,
filter: (source) => source === resolvedPackage.root
|| !relative(resolvedPackage.root, source).split(sep).includes("node_modules"),
});
staged.push(destination.slice(output.length + 1));
const dependencies = resolvedPackage.packageJson.dependencies ?? {};
for (const dependencyName of Object.keys(dependencies)) {
stagePackage(
{ name: dependencyName, fromPackageJson: resolve(resolvedPackage.root, "package.json") },
destination,
);
}
}
for (const item of requiredRoots) {
stagePackage({ ...item, fromPackageJson: desktopPackageJson }, output, true);
}
return staged.sort();
}
const outdirIndex = process.argv.indexOf("--outdir");
if (outdirIndex !== -1) {
const outdir = process.argv[outdirIndex + 1];
if (!outdir) throw new Error("--outdir requires a path.");
process.stdout.write(`${JSON.stringify({ staged: stageRuntimeNodeModules(outdir) })}\n`);
}
+385
View File
@@ -0,0 +1,385 @@
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);
}