UNPKG

fvttt

Version:

FVTTT is a CLI tool for tunneling Foundry VTT application through Cloudflare.

162 lines (133 loc) 4.56 kB
#!/usr/bin/env bun import * as p from "@clack/prompts"; import { bin, install, Tunnel } from "cloudflared"; import fs from "node:fs"; import { execSync } from "node:child_process"; const APP_VERSION = "5.1.0"; const DEFAULT_PORT = "30000"; const CLOUDFLARED_VERSION = "2025.7.0"; const GITHUB_CLOUDFLARED_URL = "https://api.github.com/repos/cloudflare/cloudflared/releases/latest"; async function getLatestVersion() { try { const response = await fetch(GITHUB_CLOUDFLARED_URL); if (!response.ok) { throw new Error(`GitHub responded with status: ${response.status}`); } const data = await response.json(); return data.tag_name; } catch (error) { p.log.error("Error fetching latest Cloudflared version:", error.message); return null; } } function compareVersions(v1: string, v2: string) { const parts1 = v1.split(".").map(Number); const parts2 = v2.split(".").map(Number); for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) { const p1 = parts1[i] || 0; const p2 = parts2[i] || 0; if (p1 > p2) return 1; if (p1 < p2) return -1; } return 0; } function parseCloudflaredVersion(versionOutput) { const versionMatch = versionOutput.match(/version (\d+\.\d+\.\d+)/); return versionMatch ? versionMatch[1] : null; } async function main() { console.log(); p.intro(`[ FVTTT ${APP_VERSION} ]`); const targetVersion = (await getLatestVersion()) || CLOUDFLARED_VERSION; let needsInstall = false; if (!fs.existsSync(bin)) { p.log.info("Cloudflared not found. Installing..."); needsInstall = true; } else { try { const cloudflaredVersionOutput = execSync(`${bin} --version`) .toString() .trim(); p.log.info(`Current ${cloudflaredVersionOutput}`); const currentVersion = parseCloudflaredVersion(cloudflaredVersionOutput); if (!currentVersion) { p.log.info("Unable to parse current version. Will attempt to update."); needsInstall = true; } else if (compareVersions(targetVersion, currentVersion) > 0) { p.log.info( `Cloudflared version ${currentVersion} is older than ${targetVersion}. Updating...` ); needsInstall = true; } else { p.log.info(`Using Cloudflared version ${currentVersion}`); } } catch (error) { p.log.error("Error checking Cloudflared version:", error.message); needsInstall = true; } } if (needsInstall) { const s = p.spinner(); s.start(`Installing cloudflared ${targetVersion}...`); try { await install(bin, targetVersion); s.stop(`Cloudflared ${targetVersion} installed successfully.`); } catch (error) { s.stop(`Failed to install Cloudflared: ${error.message}`); p.log.error("Error installing Cloudflared:", error.message); process.exit(1); } } const port = await p.text({ message: "FoundryVTT Port", placeholder: DEFAULT_PORT, initialValue: DEFAULT_PORT, validate: (value) => { const portNum = parseInt(value || DEFAULT_PORT, 10); if (isNaN(portNum) || portNum < 1 || portNum > 65535) { return "Please enter a valid port number (1-65535)"; } }, }); if (p.isCancel(port)) { p.cancel("Operation cancelled"); return process.exit(0); } const s = p.spinner(); s.start("Waiting for tunnel URL..."); const host = `http://localhost:${port || DEFAULT_PORT}`; const tunnel = Tunnel.quick(host); tunnel.on("error", (error) => { s.stop(`Tunnel error: ${error.message}`); p.log.error(`Tunnel error: ${error.message}`); process.exit(1); }); tunnel.on("exit", (code) => { p.log.info(`Cloudflare tunnel exited with code ${code}`); process.exit(code); }); try { const url = await new Promise((resolve, reject) => { tunnel.once("url", resolve); tunnel.once("error", reject); setTimeout( () => reject(new Error("Timeout waiting for tunnel URL")), 30000 ); }); tunnel.on("exit", () => { p.log.info("Tunnel closed"); process.exit(0); }); s.stop(`Tunnel URL: ${url}`); p.outro(`Select URL and press Ctrl+Shift+C to copy it`); } catch (error) { s.stop(`Failed to establish tunnel: ${error.message}`); process.exit(1); } } main().catch((error) => { p.log.error("Error:", error); process.exit(1); });