#!/usr/bin/env bun import { promises as fs } from "fs"; import * as os from "os"; import * as path from "path"; const CONFIG_FILE = path.join(os.homedir(), ".frpdev.conf"); interface FrpConfig { FRP_HOST: string; FRP_PORT: string; FRP_USER: string; FRP_SECRET: string; FRP_PROTO: string; } interface ProxyConf { type?: string; remotePort?: number; subdomain?: string; customDomains?: string[]; } interface Proxy { name?: string; status?: string; conf?: ProxyConf; } interface ProxyResponse { proxies?: Proxy[]; } /** * Pastikan config file ada. Jika tidak ada -> stop proses. */ async function ensureConfigFile(): Promise { try { await fs.access(CONFIG_FILE); } catch { const template = `FRP_HOST="" FRP_PORT="443" FRP_USER="" FRP_SECRET="" FRP_PROTO="https" `; console.error(`❌ Config not found. Template created at: ${CONFIG_FILE}`); process.exit(1); } } /** * Load config dari file .frpdev.conf */ async function loadConfig(): Promise { await ensureConfigFile(); const raw = await fs.readFile(CONFIG_FILE, "utf8"); const lines = raw.split("\n").map((l) => l.trim()).filter(Boolean); const conf: Record = {}; for (const line of lines) { const [key, ...rest] = line.split("="); if (!key) continue; let value = rest.join("=").trim(); if (value.startsWith('"') && value.endsWith('"')) { value = value.slice(1, -1); } conf[key] = value; } return { FRP_HOST: conf["FRP_HOST"] || "", FRP_PORT: conf["FRP_PORT"] || "443", FRP_USER: conf["FRP_USER"] || "", FRP_SECRET: conf["FRP_SECRET"] || "", FRP_PROTO: conf["FRP_PROTO"] || "https", }; } async function fetchFrp(config: FrpConfig, url: string): Promise { const fullUrl = `${config.FRP_PROTO}://${config.FRP_HOST}:${config.FRP_PORT}${url}`; try { const resp = await fetch(fullUrl, { headers: { Authorization: "Basic " + Buffer.from(`${config.FRP_USER}:${config.FRP_SECRET}`).toString("base64"), }, }); if (!resp.ok) { return { proxies: [] }; } const data: ProxyResponse = await resp.json(); return data; } catch { return { proxies: [] }; } } /** * Format array of objects jadi table string */ function formatTable(headers: string[], rows: string[][]): string { const allRows = [headers, ...rows]; const colWidths = headers.map((_, i) => Math.max(...allRows.map((row) => (row[i] || "").length)) ); return allRows .map((row) => row.map((cell, i) => (cell || "").padEnd(colWidths[i] ?? 0)).join(" ").trimEnd() ) .join("\n"); } async function main(): Promise { const config = await loadConfig(); const API_TCP = "/api/proxy/tcp"; const API_HTTP = "/api/proxy/http"; const tcpResp = await fetchFrp(config, API_TCP); const httpResp = await fetchFrp(config, API_HTTP); // ==================== TCP ==================== console.log("========== TCP PROXIES =========="); const tcpHeaders = ["NAME", "STATUS", "TYPE", "PORT"]; const tcpRows: string[][] = (tcpResp.proxies || []).map((p) => [ p.name ?? "-", p.status ?? "-", p.conf?.type === "unknown" ? "" : p.conf?.type ?? "", p.conf?.remotePort?.toString() ?? "-", ]); console.log(formatTable(tcpHeaders, tcpRows)); console.log(); // ==================== HTTP ==================== console.log("========== HTTP PROXIES =========="); const httpHeaders = ["NAME", "STATUS", "TYPE", "SUBDOMAIN", "CUSTOM_DOMAIN"]; const httpRows: string[][] = (httpResp.proxies || []).map((p) => [ p.name ?? "-", p.status ?? "-", p.conf?.type === "unknown" ? "" : p.conf?.type ?? "", p.conf?.subdomain ?? "", Array.isArray(p.conf?.customDomains) ? p.conf.customDomains.join(",") : "", ]); console.log(formatTable(httpHeaders, httpRows)); } main().catch((err) => { console.error("❌ Error:", err); process.exit(1); });