235 lines
6.5 KiB
JavaScript
235 lines
6.5 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import { exec as execCb, spawnSync } from "node:child_process";
|
|
import { createHash, randomUUID } from "node:crypto";
|
|
import {
|
|
chmodSync,
|
|
copyFileSync,
|
|
createWriteStream,
|
|
existsSync,
|
|
mkdirSync,
|
|
readdirSync,
|
|
readFileSync,
|
|
rmSync,
|
|
} from "node:fs";
|
|
import { arch, homedir, platform } from "node:os";
|
|
import { join, resolve } from "node:path";
|
|
import { Readable } from "node:stream";
|
|
import { finished } from "node: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();
|