fvttt
Version:
FVTTT is a CLI tool for tunneling Foundry VTT application through Cloudflare.
162 lines (133 loc) • 4.56 kB
text/typescript
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);
});