#!/usr/bin/env node import { exec as execCb, spawnSync } from "child_process"; import { createHash, randomUUID } from "crypto"; import { chmodSync, copyFileSync, createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, } from "fs"; import { arch, homedir, platform } from "os"; import { resolve, join } from "path"; import { Readable } from "stream"; import { finished } from "stream/promises"; import extractZip from "extract-zip"; const __dirname = import.meta.dirname; const cwd = resolve(__dirname, ".."); const PLAYWRIGHT_SKIP = process.env.PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD === "1" || process.env.PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD === "true"; const RTK_SKIP = process.env.SF_SKIP_RTK_INSTALL === "1" || process.env.SF_SKIP_RTK_INSTALL === "true" || process.env.SF_RTK_DISABLED === "1" || process.env.SF_RTK_DISABLED === "true"; const RTK_VERSION = "0.37.0"; const RTK_REPO = "rtk-ai/rtk"; const RTK_ENV = { ...process.env, RTK_TELEMETRY_DISABLED: "1" }; const managedBinDir = join( process.env.SF_HOME || join(homedir(), ".sf"), "agent", "bin", ); const managedBinaryPath = join( managedBinDir, platform() === "win32" ? "rtk.exe" : "rtk", ); function run(cmd) { return new Promise((resolvePromise) => { execCb(cmd, { cwd }, (error, stdout, stderr) => { resolvePromise({ ok: !error, stdout, stderr }); }); }); } function logWarn(message) { process.stderr.write(`[forge] postinstall: ${message}\n`); } function resolveAssetName() { const currentPlatform = platform(); const currentArch = arch(); if (currentPlatform === "darwin" && currentArch === "arm64") return "rtk-aarch64-apple-darwin.tar.gz"; if (currentPlatform === "darwin" && currentArch === "x64") return "rtk-x86_64-apple-darwin.tar.gz"; if (currentPlatform === "linux" && currentArch === "arm64") return "rtk-aarch64-unknown-linux-gnu.tar.gz"; if (currentPlatform === "linux" && currentArch === "x64") return "rtk-x86_64-unknown-linux-musl.tar.gz"; if (currentPlatform === "win32" && currentArch === "x64") return "rtk-x86_64-pc-windows-msvc.zip"; return null; } function parseChecksums(text) { const checksums = new Map(); for (const rawLine of text.split(/\r?\n/)) { const line = rawLine.trim(); if (!line) continue; const match = line.match(/^([a-f0-9]{64})\s+(.+)$/i); if (!match) continue; checksums.set(match[2], match[1].toLowerCase()); } return checksums; } function sha256File(path) { const hash = createHash("sha256"); hash.update(readFileSync(path)); return hash.digest("hex"); } async function downloadToFile(url, destination) { const response = await fetch(url, { headers: { "User-Agent": "sf-pi-postinstall" }, }); if (!response.ok) { throw new Error(`download failed (${response.status}) for ${url}`); } if (!response.body) { throw new Error(`download returned no body for ${url}`); } const output = createWriteStream(destination); await finished(Readable.fromWeb(response.body).pipe(output)); } function findBinaryRecursively(rootDir, binaryName) { const stack = [rootDir]; while (stack.length > 0) { const current = stack.pop(); if (!current) continue; const entries = readdirSync(current, { withFileTypes: true }); for (const entry of entries) { const fullPath = join(current, entry.name); if (entry.isFile() && entry.name === binaryName) return fullPath; if (entry.isDirectory()) stack.push(fullPath); } } return null; } function validateRtkBinary(binaryPath) { const result = spawnSync(binaryPath, ["rewrite", "git status"], { encoding: "utf-8", env: RTK_ENV, stdio: ["ignore", "pipe", "ignore"], timeout: 5000, }); return ( !result.error && result.status === 0 && (result.stdout || "").trim() === "rtk git status" ); } async function ensureRtkInstalled() { if (RTK_SKIP) return; const assetName = resolveAssetName(); if (!assetName) return; if (existsSync(managedBinaryPath) && validateRtkBinary(managedBinaryPath)) return; const tempRoot = join( managedBinDir, `.rtk-postinstall-${randomUUID().slice(0, 8)}`, ); const archivePath = join(tempRoot, assetName); const extractDir = join(tempRoot, "extract"); const releaseBase = `https://github.com/${RTK_REPO}/releases/download/v${RTK_VERSION}`; mkdirSync(tempRoot, { recursive: true }); mkdirSync(managedBinDir, { recursive: true }); try { const checksumsResponse = await fetch(`${releaseBase}/checksums.txt`, { headers: { "User-Agent": "sf-pi-postinstall" }, }); if (!checksumsResponse.ok) { throw new Error( `failed to fetch RTK checksums (${checksumsResponse.status})`, ); } const checksums = parseChecksums(await checksumsResponse.text()); const expectedSha = checksums.get(assetName); if (!expectedSha) { throw new Error(`missing checksum for ${assetName}`); } await downloadToFile(`${releaseBase}/${assetName}`, archivePath); const actualSha = sha256File(archivePath); if (actualSha !== expectedSha) { throw new Error(`checksum mismatch for ${assetName}`); } mkdirSync(extractDir, { recursive: true }); if (assetName.endsWith(".zip")) { await extractZip(archivePath, { dir: extractDir }); } else { const extractResult = spawnSync( "tar", ["xzf", archivePath, "-C", extractDir], { encoding: "utf-8", timeout: 30000, }, ); if (extractResult.error || extractResult.status !== 0) { throw new Error( extractResult.error?.message || extractResult.stderr?.trim() || `failed to extract ${assetName}`, ); } } const extractedBinary = findBinaryRecursively( extractDir, platform() === "win32" ? "rtk.exe" : "rtk", ); if (!extractedBinary) { throw new Error(`RTK binary not found in ${assetName}`); } copyFileSync(extractedBinary, managedBinaryPath); if (platform() !== "win32") { chmodSync(managedBinaryPath, 0o755); } if (!validateRtkBinary(managedBinaryPath)) { rmSync(managedBinaryPath, { force: true }); throw new Error("downloaded RTK binary failed validation"); } } catch (error) { logWarn(`RTK install skipped: ${describeFetchError(error)}`); } finally { rmSync(tempRoot, { recursive: true, force: true }); } } function describeFetchError(err) { const base = err?.message || String(err); const cause = err?.cause; if (!cause) return base; const code = cause.code || cause.errno; const causeMsg = cause.message || ""; const detail = code ? `${code}${causeMsg && causeMsg !== code ? ` — ${causeMsg}` : ""}` : causeMsg; return detail ? `${base} (${detail})` : base; } if (!PLAYWRIGHT_SKIP) { await run("npx playwright install chromium"); } await ensureRtkInstalled();