import { chmodSync, createWriteStream, existsSync, mkdirSync, renameSync, rmSync, unlinkSync } from "node:fs"; import { join } from "node:path"; import { Readable } from "node:stream"; import { pipeline } from "node:stream/promises"; import AdmZip from "adm-zip"; import { extract as extractTar } from "tar"; import { verifyHash } from "./hash"; import { BUNDLE_SPECS } from "./manifest"; import { getBundleDir, getBundledBinaryPath } from "./paths"; import { getRuntimeTarget } from "./platform"; import { checkSystemCommand } from "./process"; import { verifyBundledBinary } from "./integrity"; import type { TargetKey, ToolBundleSpec, ToolName } from "./types"; export function getAssetFileName(tool: ToolName, spec: ToolBundleSpec): string { if (tool === "rg") { return `ripgrep-${spec.version}-${spec.assetTriple}.${spec.archiveExt}`; } return `fd-v${spec.version}-${spec.assetTriple}.${spec.archiveExt}`; } export function getAssetUrl(tool: ToolName, spec: ToolBundleSpec): string { if (spec.sourceUrl) return spec.sourceUrl; if (tool === "rg") { return `https://github.com/BurntSushi/ripgrep/releases/download/${spec.version}/${getAssetFileName(tool, spec)}`; } return `https://github.com/sharkdp/fd/releases/download/v${spec.version}/${getAssetFileName(tool, spec)}`; } function makeStageDir(parentDir: string, name: string): string { const stageDir = join(parentDir, `.stage-${name}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`); mkdirSync(stageDir, { recursive: true }); return stageDir; } async function downloadFile(url: string, destPath: string): Promise { const response = await fetch(url); if (!response.ok || !response.body) { throw new Error(`Download failed: ${response.status} ${response.statusText} for ${url}`); } const stream = createWriteStream(destPath); await pipeline(Readable.fromWeb(response.body as any), stream); } async function extractBinary(opts: { archivePath: string; archiveExt: "tar.gz" | "zip"; outputDir: string; binaryName: string; }): Promise { const { archivePath, archiveExt, outputDir, binaryName } = opts; mkdirSync(outputDir, { recursive: true }); if (archiveExt === "tar.gz") { await extractTar({ file: archivePath, cwd: outputDir, strip: 1 }); const outputPath = join(outputDir, binaryName); if (!existsSync(outputPath)) { throw new Error(`Expected binary not found after tar extraction: ${outputPath}`); } if (process.platform !== "win32") chmodSync(outputPath, 0o755); return outputPath; } const zip = new AdmZip(archivePath); const entry = zip.getEntries().find((candidate) => { return !candidate.isDirectory && (candidate.entryName.endsWith(`/${binaryName}`) || candidate.entryName === binaryName); }); if (!entry) { throw new Error(`Binary ${binaryName} not found in zip archive`); } zip.extractEntryTo(entry, outputDir, false, true, binaryName); return join(outputDir, binaryName); } export async function ensureBundledBinary(tool: ToolName, targetKey = getRuntimeTarget().key): Promise { const spec = BUNDLE_SPECS[tool][targetKey]; const outputDir = getBundleDir(targetKey); const finalPath = getBundledBinaryPath(tool, targetKey, spec.binaryName); const existing = verifyBundledBinary(tool, targetKey); if (existing.ok) return finalPath; mkdirSync(outputDir, { recursive: true }); const stageDir = makeStageDir(outputDir, tool); const archivePath = join(stageDir, getAssetFileName(tool, spec)); try { await downloadFile(getAssetUrl(tool, spec), archivePath); const archiveCheck = verifyHash(archivePath, spec.archiveSha256); if (!archiveCheck.ok) { throw new Error(`Archive hash mismatch for ${tool}. expected=${spec.archiveSha256} actual=${archiveCheck.actual}`); } const extractedPath = await extractBinary({ archivePath, archiveExt: spec.archiveExt, outputDir: stageDir, binaryName: spec.binaryName, }); const binaryCheck = verifyHash(extractedPath, spec.sha256); if (!binaryCheck.ok) { throw new Error(`Binary hash mismatch for ${tool}. expected=${spec.sha256} actual=${binaryCheck.actual}`); } if (existsSync(finalPath)) { try { unlinkSync(finalPath); } catch {} } renameSync(extractedPath, finalPath); if (process.platform !== "win32") chmodSync(finalPath, 0o755); const verified = verifyBundledBinary(tool, targetKey); if (!verified.ok) { throw new Error(`Installed ${tool} failed integrity check (${verified.reason}). expected=${verified.expected} actual=${verified.actual || "n/a"}`); } return finalPath; } finally { try { rmSync(stageDir, { recursive: true, force: true }); } catch {} } } export async function resolvePreferredBinary(tool: ToolName): Promise { const verification = verifyBundledBinary(tool); if (verification.ok) return verification.path; if (await checkSystemCommand(tool)) return tool; return null; }