#!/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(), ".g3n.conf"); interface FrpConfig { FRP_HOST: string; FRP_PORT: string; FRP_USER: string; FRP_SECRET: string; FRP_PROTO: string; FRP_AUTH_TOKEN: string; } interface ProxyConf { type?: string; remotePort?: number; subdomain?: string; customDomains?: string[]; } interface Proxy { name?: string; status?: string; conf?: ProxyConf; } interface ProxyResponse { proxies?: Proxy[]; } const templateConfig = ` FRP_HOST="" FRP_USER="" FRP_SECRET="" FRP_AUTH_TOKEN=""`; async function ensureConfigFile(): Promise { try { await fs.access(CONFIG_FILE); } catch { console.error(`❌ Config not found. Template created at: ${CONFIG_FILE}`); console.log(templateConfig); process.exit(1); } } async function loadConfig(): Promise { await ensureConfigFile(); const raw = await fs.readFile(CONFIG_FILE, "utf8"); const lines = raw .split("\n") .map((line) => line.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; } if (!conf.FRP_HOST || !conf.FRP_USER || !conf.FRP_SECRET || !conf.FRP_AUTH_TOKEN) { console.error(`❌ Config not found. Template created at: ${CONFIG_FILE}`); console.log(raw); process.exit(1); } return { FRP_HOST: conf.FRP_HOST || "", FRP_PORT: "443", FRP_USER: conf.FRP_USER || "", FRP_SECRET: conf.FRP_SECRET || "", FRP_PROTO: "https", FRP_AUTH_TOKEN: conf.FRP_AUTH_TOKEN || "", }; } 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: [] }; return (await resp.json()) as ProxyResponse; } catch { return { proxies: [] }; } } function sortProxies(proxies: Proxy[]): Proxy[] { return [...proxies].sort((a, b) => { // Urutan utama: status (online/running duluan) const order = (status?: string) => status?.toLowerCase() === "online" || status?.toLowerCase() === "running" ? 0 : 1; const statusDiff = order(a.status) - order(b.status); if (statusDiff !== 0) return statusDiff; // Jika status sama, urutkan berdasarkan remotePort numerik (ascending) const portA = a.conf?.remotePort ?? Number.MAX_SAFE_INTEGER; const portB = b.conf?.remotePort ?? Number.MAX_SAFE_INTEGER; return portA - portB; }); } 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 printTable( title: string, headers: string[], rows: string[][], ): Promise { console.log(`========== ${title} ==========`); if (rows.length === 0) { console.log("No proxies found.\n"); return; } console.log(formatTable(headers, rows)); console.log(); } async function frp(): Promise { const config = await loadConfig(); const [tcpResp, httpResp] = await Promise.all([ fetchFrp(config, "/api/proxy/tcp"), fetchFrp(config, "/api/proxy/http"), ]); const tcpRows: string[][] = sortProxies(tcpResp.proxies || []).map((p) => [ p.name ?? "-", p.status ?? "-", p.conf?.remotePort?.toString() ?? "-", ]); await printTable("TCP PROXIES", ["NAME", "STATUS", "PORT"], tcpRows); const httpRows: string[][] = sortProxies(httpResp.proxies || []).map((p) => [ p.name ?? "-", p.status ?? "-", Array.isArray(p.conf?.customDomains) ? p.conf.customDomains.join(",") : "", ]); await printTable( "HTTP PROXIES", ["NAME", "STATUS", "CUSTOM_DOMAIN"], httpRows, ); } export default frp;