fix(coding-agent): harden fd/rg bootstrap on windows

fixes #1348
This commit is contained in:
Mario Zechner 2026-02-26 00:19:43 +01:00
parent ef8c950357
commit 3db5715de4
4 changed files with 133 additions and 23 deletions

View file

@ -1,6 +1,7 @@
import chalk from "chalk";
import { spawnSync } from "child_process";
import { chmodSync, createWriteStream, existsSync, mkdirSync, renameSync, rmSync } from "fs";
import extractZip from "extract-zip";
import { chmodSync, createWriteStream, existsSync, mkdirSync, readdirSync, renameSync, rmSync } from "fs";
import { arch, platform } from "os";
import { join } from "path";
import { Readable } from "stream";
@ -130,6 +131,28 @@ async function downloadFile(url: string, dest: string): Promise<void> {
await finished(Readable.fromWeb(response.body as any).pipe(fileStream));
}
function findBinaryRecursively(rootDir: string, binaryFileName: string): string | null {
const stack: string[] = [rootDir];
while (stack.length > 0) {
const currentDir = stack.pop();
if (!currentDir) continue;
const entries = readdirSync(currentDir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = join(currentDir, entry.name);
if (entry.isFile() && entry.name === binaryFileName) {
return fullPath;
}
if (entry.isDirectory()) {
stack.push(fullPath);
}
}
}
return null;
}
// Download and install a tool
async function downloadTool(tool: "fd" | "rg"): Promise<string> {
const config = TOOLS[tool];
@ -158,33 +181,42 @@ async function downloadTool(tool: "fd" | "rg"): Promise<string> {
// Download
await downloadFile(downloadUrl, archivePath);
// Extract
const extractDir = join(TOOLS_DIR, "extract_tmp");
// Extract into a unique temp directory. fd and rg downloads can run concurrently
// during startup, so sharing a fixed directory causes races.
const extractDir = join(
TOOLS_DIR,
`extract_tmp_${config.binaryName}_${process.pid}_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`,
);
mkdirSync(extractDir, { recursive: true });
try {
// Use tar for both .tar.gz and .zip extraction. Windows 10+ ships bsdtar
// which handles both formats, avoiding the need for `unzip` (not available
// on Windows by default).
const extractResult = assetName.endsWith(".tar.gz")
? spawnSync("tar", ["xzf", archivePath, "-C", extractDir], { stdio: "pipe" })
: assetName.endsWith(".zip")
? spawnSync("tar", ["xf", archivePath, "-C", extractDir], { stdio: "pipe" })
: null;
if (!extractResult || extractResult.error || extractResult.status !== 0) {
const errMsg = extractResult?.error?.message ?? extractResult?.stderr?.toString().trim() ?? "unknown error";
throw new Error(`Failed to extract ${assetName}: ${errMsg}`);
if (assetName.endsWith(".tar.gz")) {
const extractResult = spawnSync("tar", ["xzf", archivePath, "-C", extractDir], { stdio: "pipe" });
if (extractResult.error || extractResult.status !== 0) {
const errMsg = extractResult.error?.message ?? extractResult.stderr?.toString().trim() ?? "unknown error";
throw new Error(`Failed to extract ${assetName}: ${errMsg}`);
}
} else if (assetName.endsWith(".zip")) {
await extractZip(archivePath, { dir: extractDir });
} else {
throw new Error(`Unsupported archive format: ${assetName}`);
}
// Find the binary in extracted files
// Find the binary in extracted files. Some archives contain files directly
// at root, others nest under a versioned subdirectory.
const binaryFileName = config.binaryName + binaryExt;
const extractedDir = join(extractDir, assetName.replace(/\.(tar\.gz|zip)$/, ""));
const extractedBinary = join(extractedDir, config.binaryName + binaryExt);
const extractedBinaryCandidates = [join(extractedDir, binaryFileName), join(extractDir, binaryFileName)];
let extractedBinary = extractedBinaryCandidates.find((candidate) => existsSync(candidate));
if (existsSync(extractedBinary)) {
if (!extractedBinary) {
extractedBinary = findBinaryRecursively(extractDir, binaryFileName) ?? undefined;
}
if (extractedBinary) {
renameSync(extractedBinary, binaryPath);
} else {
throw new Error(`Binary not found in archive: ${extractedBinary}`);
throw new Error(`Binary not found in archive: expected ${binaryFileName} under ${extractDir}`);
}
// Make executable (Unix only)