import { join } from 'node:path'; import { promises as fs } from 'node:fs'; import { createWriteStream } from 'node:fs'; import { getAgiBinDir } from '../core/src/tools/bin-manager.ts'; const BINARY_NAME = 'tunnel'; const CLOUDFLARED_VERSION = '2024.12.2'; interface PlatformInfo { os: 'darwin' | 'linux' | 'windows'; arch: 'amd64' | 'arm64'; ext: string; } function getPlatformInfo(): PlatformInfo { const platform = process.platform; const arch = process.arch; const os = platform === 'darwin' ? 'darwin' : platform === 'win32' ? 'windows' : 'linux'; const cpu = arch === 'arm64' ? 'arm64' : 'amd64'; const ext = platform === 'win32' ? '.exe' : ''; return { os, arch: cpu, ext }; } function getDownloadUrl(version: string, info: PlatformInfo): string { const base = `https://github.com/cloudflare/cloudflared/releases/download/${version}`; if (info.os === 'darwin') { return `${base}/cloudflared-darwin-${info.arch}.tgz`; } if (info.os === 'windows') { return `${base}/cloudflared-windows-${info.arch}.exe`; } return `${base}/cloudflared-linux-${info.arch}`; } async function fileExists(p: string): Promise { try { await fs.access(p); return true; } catch { return false; } } async function ensureDir(dir: string): Promise { await fs.mkdir(dir, { recursive: true }); } async function makeExecutable(p: string): Promise { if (process.platform === 'win32') return; try { await fs.chmod(p, 0o755); } catch {} } async function extractTarGz(tgzPath: string, destDir: string): Promise { const { spawn } = await import('node:child_process'); return new Promise((resolve, reject) => { const proc = spawn('tar', ['-xzf', tgzPath, '-C', destDir], { stdio: ['ignore', 'pipe', 'pipe'], }); proc.on('close', (code) => { if (code === 0) { resolve(join(destDir, 'cloudflared')); } else { reject(new Error(`tar extraction failed with code ${code}`)); } }); proc.on('error', reject); }); } function formatBytes(bytes: number): string { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; } async function downloadFile( url: string, dest: string, onProgress?: (message: string) => void, ): Promise { const response = await fetch(url, { redirect: 'follow' }); if (!response.ok) { throw new Error( `Download failed: ${response.status} ${response.statusText}`, ); } if (!response.body) { throw new Error('No response body'); } await ensureDir(join(dest, '..')); const contentLength = response.headers.get('content-length'); const totalBytes = contentLength ? parseInt(contentLength, 10) : 0; const fileStream = createWriteStream(dest); const reader = response.body.getReader(); let downloadedBytes = 0; let lastProgressUpdate = 0; try { while (true) { const { done, value } = await reader.read(); if (done) break; fileStream.write(value); downloadedBytes += value.length; if (onProgress && totalBytes > 0) { const now = Date.now(); if (now - lastProgressUpdate > 200) { const percent = Math.round((downloadedBytes / totalBytes) * 100); onProgress( `Downloading... ${formatBytes(downloadedBytes)} / ${formatBytes(totalBytes)} (${percent}%)`, ); lastProgressUpdate = now; } } } fileStream.end(); await new Promise((resolve, reject) => { fileStream.on('finish', resolve); fileStream.on('error', reject); }); } finally { reader.releaseLock(); } } export function getTunnelBinaryPath(): string { const binDir = getAgiBinDir(); const ext = process.platform === 'win32' ? '.exe' : ''; return join(binDir, `${BINARY_NAME}${ext}`); } export async function isTunnelBinaryInstalled(): Promise { const binPath = getTunnelBinaryPath(); return fileExists(binPath); } export async function downloadTunnelBinary( onProgress?: (message: string) => void, ): Promise { const binPath = getTunnelBinaryPath(); if (await fileExists(binPath)) { return binPath; } const info = getPlatformInfo(); const url = getDownloadUrl(CLOUDFLARED_VERSION, info); const binDir = getAgiBinDir(); await ensureDir(binDir); onProgress?.('Downloading tunnel binary (one-time setup)...'); if (info.os === 'darwin') { const tgzPath = join(binDir, 'cloudflared.tgz'); await downloadFile(url, tgzPath, onProgress); onProgress?.('Extracting...'); const extractedPath = await extractTarGz(tgzPath, binDir); await fs.rename(extractedPath, binPath); await fs.unlink(tgzPath); } else { await downloadFile(url, binPath, onProgress); } await makeExecutable(binPath); onProgress?.('Tunnel binary ready.'); return binPath; } export async function ensureTunnelBinary( onProgress?: (message: string) => void, ): Promise { const binPath = getTunnelBinaryPath(); if (await fileExists(binPath)) { return binPath; } return downloadTunnelBinary(onProgress); } export async function removeTunnelBinary(): Promise { const binPath = getTunnelBinaryPath(); if (await fileExists(binPath)) { await fs.unlink(binPath); } }