From 6086aa31c210036f454c68cdf97cff6636278a7d Mon Sep 17 00:00:00 2001 From: bipproduction Date: Thu, 25 Sep 2025 12:08:35 +0800 Subject: [PATCH] tambahan --- bin/g3n.ts | 172 +++++++++++------- bin/src/compose.ts | 1 + bin/src/frp.ts | 165 ++++++++++++++++++ bin/src/port.ts | 84 ++++++--- x.ts | 422 ++++++++++++++------------------------------- 5 files changed, 469 insertions(+), 375 deletions(-) create mode 100644 bin/src/frp.ts diff --git a/bin/g3n.ts b/bin/g3n.ts index a609698..ba74f1a 100755 --- a/bin/g3n.ts +++ b/bin/g3n.ts @@ -7,22 +7,33 @@ import checkPort from "./src/port"; import route from "./src/route"; import compose from "./src/compose"; import generateDockerfile from "./src/docker-file"; +import frp from "./src/frp"; interface CheckPortResult { port: number; open: boolean; } -const args = minimist(process.argv.slice(2)); +// Default constants (12-Factor App) +const DEFAULTS = { + ENV_FILE: ".env", + ENV_OUT: "types/env.d.ts", + PORT_START: 3000, + PORT_END: 4000, + HOST: "127.0.0.1", +}; -const help = ` +// CLI Help +const HELP_TEXT = ` g3n [command] [options] Commands: - env Generate env.d.ts from .env file + env Generate env.d.ts from .env file scan-port Scan port range (default 3000-4000) - route Generate routes.ts from AppRoutes.tsx - compose Generate compose.yml from name + route Generate routes.ts from AppRoutes.tsx + compose Generate compose.yml from name + docker-file Generate Dockerfile + Options: --env Path ke file .env (default: .env) --out Path file output (default: types/env.d.ts) @@ -34,64 +45,105 @@ Examples: g3n env --env .env.local --out src/types/env.d.ts g3n scan-port --start 7700 --end 7800 --host 127.0.0.1 g3n route - g3n compose `; + g3n compose + g3n docker-file +`; -(async () => { - const cmd = args._[0]; +// Parse CLI arguments +const args = minimist(process.argv.slice(2)); - if (cmd === "env") { - generateEnvTypes({ - envFilePath: args.env, - outputDir: args.out ? path.dirname(args.out) : undefined, - outputFileName: args.out ? path.basename(args.out) : undefined, - }); +/** + * Main CLI handler + */ +async function main(): Promise { + const [command, name] = args._; + + switch (command) { + case "env": + handleEnv(); + break; + + case "scan-port": + await handleScanPort(); + break; + + case "route": + route(); + break; + + case "compose": + handleCompose(name); + break; + + case "docker-file": + generateDockerfile(); + break; + case "frp": + frp().catch((err) => { + console.error("❌ Error:", err); + process.exit(1); + }); + break; + + default: + console.error(HELP_TEXT); + break; + } +} + +/** + * Handle "env" command + */ +function handleEnv(): void { + const envFile = args.env || DEFAULTS.ENV_FILE; + const output = args.out || DEFAULTS.ENV_OUT; + + generateEnvTypes({ + envFilePath: envFile, + outputDir: path.dirname(output), + outputFileName: path.basename(output), + }); + + console.log(`✅ Env types generated at ${output}`); +} + +/** + * Handle "scan-port" command + */ +async function handleScanPort(): Promise { + const start = Number(args.start) || DEFAULTS.PORT_START; + const end = Number(args.end) || DEFAULTS.PORT_END; + const host = args.host || DEFAULTS.HOST; + + console.log(`🔍 Scanning ports ${start}-${end} on host ${host}...`); + + const ports = Array.from({ length: end - start + 1 }, (_, i) => start + i); + + const results: CheckPortResult[] = await Promise.all( + ports.map((port) => checkPort(port, host)) + ); + + const openPorts = results.filter((r) => r.open); + openPorts.forEach((r) => console.log(`✅ Port ${r.port} is open`)); + + console.log("✅ Scan completed"); +} + +/** + * Handle "compose" command + */ +function handleCompose(name?: string): void { + if (!name) { + console.error("❌ Compose name is required"); return; } - if (cmd === "scan-port") { - const start: number = args.start ? parseInt(args.start, 10) : 3000; - const end: number = args.end ? parseInt(args.end, 10) : 4000; - const host: string = args.host || "localhost"; - - console.log(`🔍 Scan port ${start}-${end} di host ${host} ...`); - - const ports: number[] = Array.from( - { length: end - start + 1 }, - (_, i) => start + i - ); - - const results: CheckPortResult[] = await Promise.all( - ports.map((p) => checkPort(p, host)) - ); - - results.filter((r) => r.open).forEach((r) => { - console.log(`✅ Port ${r.port} sedang digunakan`); - }); - - console.log("✅ Selesai"); - return; - } - - if (cmd === "route") { - route(); - return; - } - - if (cmd === "compose") { - if (!args._[1]) { - console.error("❌ Name is required"); - return; - } - compose(args._[1] as string); - return; - } - - if (cmd === "docker-file") { - generateDockerfile(); - return; - } - - console.error(help); -})(); - + compose(name); + console.log(`✅ Compose file generated for ${name}`); +} +// Execute CLI +main().catch((err) => { + console.error("❌ Unexpected error:", err); + process.exit(1); +}); diff --git a/bin/src/compose.ts b/bin/src/compose.ts index d1ed187..d52725c 100644 --- a/bin/src/compose.ts +++ b/bin/src/compose.ts @@ -128,6 +128,7 @@ async function compose(name: string) { const composeFile = text(name); await fs.writeFile(`./compose.yml`, composeFile); Bun.spawnSync(["bash", "-c", generate(name)]); + console.log("✅ Compose file generated"); } export default compose diff --git a/bin/src/frp.ts b/bin/src/frp.ts new file mode 100644 index 0000000..ebaa45a --- /dev/null +++ b/bin/src/frp.ts @@ -0,0 +1,165 @@ +#!/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[]; +} + +async function ensureConfigFile(): Promise { + try { + await fs.access(CONFIG_FILE); + } catch { + const template = ` +FRP_HOST="" +FRP_USER="" +FRP_SECRET="" +`; + console.error(`❌ Config not found. Template created at: ${CONFIG_FILE}`); + console.log(template); + 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; + } + + return { + FRP_HOST: conf.FRP_HOST || "", + FRP_PORT: "443", + FRP_USER: conf.FRP_USER || "", + FRP_SECRET: conf.FRP_SECRET || "", + 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: [] }; + + return (await resp.json()) as ProxyResponse; + } catch { + return { proxies: [] }; + } +} + +function sortProxies(proxies: Proxy[]): Proxy[] { + return [...proxies].sort((a, b) => { + const order = (status?: string) => + status?.toLowerCase() === "online" || status?.toLowerCase() === "running" + ? 0 + : 1; + return order(a.status) - order(b.status); + }); +} + +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; + diff --git a/bin/src/port.ts b/bin/src/port.ts index 1a439b1..399eea5 100644 --- a/bin/src/port.ts +++ b/bin/src/port.ts @@ -2,30 +2,66 @@ import net from "net"; interface CheckPortResult { - port: number; - open: boolean; + port: number; + open: boolean; } +/** + * Memeriksa apakah port tertentu pada host terbuka. + * @param port Nomor port yang akan diperiksa + * @param host Host target + * @returns Promise yang resolve dengan status port + */ export default function checkPort(port: number, host: string): Promise { - return new Promise((resolve) => { - const socket = new net.Socket(); - socket.setTimeout(200); - - socket.once("connect", () => { - socket.destroy(); - resolve({ port, open: true }); - }); - - socket.once("timeout", () => { - socket.destroy(); - resolve({ port, open: false }); - }); - - socket.once("error", () => { - socket.destroy(); - resolve({ port, open: false }); - }); - - socket.connect(port, host); - }); - } \ No newline at end of file + return new Promise((resolve) => { + const socket = new net.Socket(); + + // Timeout untuk koneksi + socket.setTimeout(200); + + const finalize = (isOpen: boolean) => { + socket.destroy(); + resolve({ port, open: isOpen }); + }; + + socket.once("connect", () => finalize(true)); + socket.once("timeout", () => finalize(false)); + socket.once("error", () => finalize(false)); + + socket.connect(port, host); + }); +} + +/** +## 🔍 Analisis + +* Masalah yang ditemukan: + - Duplikasi kode pada `socket.destroy()` dan `resolve`. + - Tidak ada dokumentasi fungsi. +* Code smells terdeteksi: + - Callback anonymous yang sama diulang tiga kali. +* Peluang perbaikan: + - Ekstraksi logika finalize untuk DRY. + - Tambahkan komentar/documentation agar self-explanatory. + +## ✨ Kode yang Direfaktor + +Sudah diterapkan di atas. + +## 📋 Perubahan yang Dilakukan + +* Prinsip yang diterapkan: DRY, KISS, Clean Code +* Pattern yang digunakan: Callback extraction untuk pengurangan duplikasi +* Pertimbangan performa: Tidak ada perubahan signifikan, tetap ringan dan async. + +## 🎯 Manfaat yang Dicapai + +* Kemudahan pemeliharaan: Logika finalize berada di satu tempat +* Keterbacaan: Fungsi lebih jelas dengan dokumentasi dan nama `finalize` +* Kemudahan pengembangan: Bisa dengan mudah menambahkan logging atau metrik + +## ⚡ Langkah Selanjutnya (Opsional) + +* Tambahkan parameter timeout fleksibel +* Gunakan TypeScript enums untuk status port jika diperlukan +*/ diff --git a/x.ts b/x.ts index 91d6d75..aaf762a 100644 --- a/x.ts +++ b/x.ts @@ -1,327 +1,167 @@ #!/usr/bin/env bun -import * as fs from 'fs'; -import * as path from 'path'; -import * as os from 'os'; -import { execSync } from 'child_process'; +import { promises as fs } from "fs"; +import * as os from "os"; +import * as path from "path"; -const CONFIG_FILE = path.join(os.homedir(), '.note.conf'); +const CONFIG_FILE = path.join(os.homedir(), ".frpdev.conf"); -interface Config { - TOKEN?: string; - REPO?: string; - URL?: string; +interface FrpConfig { + FRP_HOST: string; + FRP_PORT: string; + FRP_USER: string; + FRP_SECRET: string; + FRP_PROTO: string; } -// --- Config management --- -function createDefaultConfig(): void { - const defaultConfig = `TOKEN= -REPO= -URL=https://cld-dkr-makuro-seafile.wibudev.com/api2 +interface ProxyConf { + type?: string; + remotePort?: number; + subdomain?: string; + customDomains?: string[]; +} + +interface Proxy { + name?: string; + status?: string; + conf?: ProxyConf; +} + +interface ProxyResponse { + proxies?: Proxy[]; +} + +async function ensureConfigFile(): Promise { + try { + await fs.access(CONFIG_FILE); + } catch { + const template = ` +FRP_HOST="" +FRP_USER="" +FRP_SECRET="" `; - fs.writeFileSync(CONFIG_FILE, defaultConfig); -} - -function editConfig(): void { - if (!fs.existsSync(CONFIG_FILE)) { - createDefaultConfig(); - } - - const editor = process.env.EDITOR || 'vim'; - try { - execSync(`${editor} "${CONFIG_FILE}"`, { stdio: 'inherit' }); - } catch (error) { - console.error('❌ Failed to open editor'); + console.error(`❌ Config not found. Template created at: ${CONFIG_FILE}`); + console.log(template); process.exit(1); } } -function loadConfig(): Config { - if (!fs.existsSync(CONFIG_FILE)) { - console.error(`⚠️ Config file not found at ${CONFIG_FILE}`); - console.error('Run: bun note.ts config to create/edit it.'); - process.exit(1); - } +async function loadConfig(): Promise { + await ensureConfigFile(); - const configContent = fs.readFileSync(CONFIG_FILE, 'utf8'); - const config: Config = {}; - - for (const line of configContent.split('\n')) { - const trimmed = line.trim(); - if (trimmed && !trimmed.startsWith('#')) { - const [key, ...valueParts] = trimmed.split('='); - if (key && valueParts.length > 0) { - let value = valueParts.join('='); - // Remove surrounding quotes if present - if ((value.startsWith('"') && value.endsWith('"')) || - (value.startsWith("'") && value.endsWith("'"))) { - value = value.slice(1, -1); - } - config[key as keyof Config] = value; - } + 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 (!config.TOKEN || !config.REPO) { - console.error(`❌ Config invalid. Please set TOKEN=... and REPO=... inside ${CONFIG_FILE}`); - process.exit(1); - } - - if (!config.URL) { - console.error(`❌ Config invalid. Please set URL=... inside ${CONFIG_FILE}`); - process.exit(1); - } - - return config; -} - -// --- HTTP helpers --- -async function fetchWithAuth(config: Config, url: string, options: RequestInit = {}): Promise { - const headers = { - 'Authorization': `Token ${config.TOKEN}`, - ...options.headers + return { + FRP_HOST: conf.FRP_HOST || "", + FRP_PORT: "443", + FRP_USER: conf.FRP_USER || "", + FRP_SECRET: conf.FRP_SECRET || "", + FRP_PROTO: "https", }; - - try { - const response = await fetch(url, { ...options, headers }); - if (!response.ok) { - console.error(`❌ Request failed: ${response.status} ${response.statusText}`); - console.error(`🔍 URL: ${url}`); - console.error(`🔍 Headers:`, headers); - - // Try to get response body for more details - try { - const errorText = await response.text(); - console.error(`🔍 Response body: ${errorText}`); - } catch (e) { - console.error('🔍 Could not read response body'); - } - - throw new Error(`HTTP ${response.status}: ${response.statusText}`); - } - return response; - } catch (error) { - if (error instanceof Error && error.message.includes('HTTP')) { - // Already handled above - process.exit(1); - } - console.error('❌ Network request failed:', error); - process.exit(1); - } } -// --- Commands --- -async function testConnection(config: Config): Promise { - console.log('🔍 Testing connection...'); - try { - // Test basic API endpoint - const response = await fetchWithAuth(config, `${config.URL}/ping/`); - const result = await response.text(); - console.log(`✅ API connection successful: ${result}`); - } catch (error) { - console.log('⚠️ API ping failed, trying repo access...'); - try { - // Try accessing the repo directly - const response = await fetchWithAuth(config, `${config.URL}/${config.REPO}/`); - const result = await response.text(); - console.log(`✅ Repo access successful`); - } catch (error) { - console.error('❌ Both API ping and repo access failed'); - } - } -} - -async function listFiles(config: Config): Promise { - const url = `${config.URL}/${config.REPO}/dir/?p=/`; - const response = await fetchWithAuth(config, url); - - try { - const files = await response.json(); - for (const file of files as { name: string }[]) { - console.log(file.name); - } - } catch (error) { - console.error('❌ Failed to parse response'); - process.exit(1); - } -} - -async function catFile(config: Config, fileName: string): Promise { - const downloadUrlResponse = await fetchWithAuth(config, `${config.URL}/${config.REPO}/file/?p=/${fileName}`); - const downloadUrl = (await downloadUrlResponse.text()).replace(/"/g, ''); - - const contentResponse = await fetchWithAuth(config, downloadUrl); - const content = await contentResponse.text(); - console.log(content); -} - -async function uploadFile(config: Config, localFile: string, remoteFile?: string): Promise { - if (!fs.existsSync(localFile)) { - console.error(`❌ File not found: ${localFile}`); - process.exit(1); - } - - const remoteName = remoteFile || path.basename(localFile); - - // Get upload URL - const uploadUrlResponse = await fetchWithAuth(config, `${config.URL}/${config.REPO}/upload-link/?p=/`); - const uploadUrl = (await uploadUrlResponse.text()).replace(/"/g, ''); - - // Create FormData - const formData = new FormData(); - const fileContent = fs.readFileSync(localFile); - const blob = new Blob([fileContent]); - formData.append('file', blob, remoteName); - formData.append('filename', remoteName); - formData.append('parent_dir', '/'); +async function fetchFrp(config: FrpConfig, url: string): Promise { + const fullUrl = `${config.FRP_PROTO}://${config.FRP_HOST}:${config.FRP_PORT}${url}`; try { - await fetchWithAuth(config, uploadUrl, { - method: 'POST', - body: formData + const resp = await fetch(fullUrl, { + headers: { + Authorization: + "Basic " + + Buffer.from(`${config.FRP_USER}:${config.FRP_SECRET}`).toString("base64"), + }, }); - console.log(`✅ Uploaded ${localFile} → ${remoteName}`); - } catch (error) { - console.error('❌ Upload failed'); - process.exit(1); + + if (!resp.ok) return { proxies: [] }; + + return (await resp.json()) as ProxyResponse; + } catch { + return { proxies: [] }; } } -async function removeFile(config: Config, fileName: string): Promise { - const url = `${config.URL}/${config.REPO}/file/?p=/${fileName}`; - await fetchWithAuth(config, url, { method: 'DELETE' }); - console.log(`🗑️ Removed ${fileName}`); +function sortProxies(proxies: Proxy[]): Proxy[] { + return [...proxies].sort((a, b) => { + const order = (status?: string) => + status?.toLowerCase() === "online" || status?.toLowerCase() === "running" + ? 0 + : 1; + return order(a.status) - order(b.status); + }); } -async function moveFile(config: Config, oldName: string, newName: string): Promise { - const url = `${config.URL}/${config.REPO}/file/?p=/${oldName}`; - const formData = new FormData(); - formData.append('operation', 'rename'); - formData.append('newname', newName); +function formatTable(headers: string[], rows: string[][]): string { + const allRows = [headers, ...rows]; + const colWidths = headers.map((_, i) => + Math.max(...allRows.map((row) => (row[i] || "").length)), + ); - try { - await fetchWithAuth(config, url, { - method: 'POST', - body: formData - }); - console.log(`✏️ Renamed ${oldName} → ${newName}`); - } catch (error) { - console.error('❌ Rename failed'); - process.exit(1); - } + return allRows + .map((row) => + row.map((cell, i) => (cell || "").padEnd(colWidths[i] ?? 0)).join(" ").trimEnd(), + ) + .join("\n"); } -async function downloadFile(config: Config, remoteFile: string, localFile?: string): Promise { - const localName = localFile || remoteFile; - - // Get download URL - const downloadUrlResponse = await fetchWithAuth(config, `${config.URL}/${config.REPO}/file/?p=/${remoteFile}`); - const downloadUrl = (await downloadUrlResponse.text()).replace(/"/g, ''); +async function printTable( + title: string, + headers: string[], + rows: string[][], +): Promise { + console.log(`========== ${title} ==========`); - try { - const fileResponse = await fetchWithAuth(config, downloadUrl); - const arrayBuffer = await fileResponse.arrayBuffer(); - const buffer = Buffer.from(arrayBuffer); - - fs.writeFileSync(localName, buffer); - console.log(`⬇️ Downloaded ${remoteFile} → ${localName}`); - } catch (error) { - console.error('❌ Download failed'); - process.exit(1); - } -} - -function showHelp(): void { - console.log(`note - simple CLI for Seafile notes -Usage: - bun note.ts ls List files - bun note.ts cat Show file content - bun note.ts cp [remote] Upload file - bun note.ts rm Remove file - bun note.ts mv Rename/move file - bun note.ts get [local] Download file - bun note.ts test Test API connection - bun note.ts config Edit config (~/.note.conf) - -Config (~/.note.conf): - TOKEN=your_seafile_token - REPO=repos/ - URL=your_seafile_url/api2`); -} - -// --- Main --- -async function not3(): Promise { - const args = process.argv.slice(2); - const cmd = args[0] || 'help'; - - // Handle config command - if (cmd === 'config') { - editConfig(); + if (rows.length === 0) { + console.log("No proxies found.\n"); return; } - // Load config for other commands - const config = loadConfig(); - - switch (cmd) { - case 'test': - await testConnection(config); - break; - - case 'ls': - await listFiles(config); - break; - - case 'cat': - if (!args[1]) { - console.error('Usage: bun note.ts cat '); - process.exit(1); - } - await catFile(config, args[1]); - break; - - case 'cp': - if (!args[1]) { - console.error('Usage: bun note.ts cp [remote_file]'); - process.exit(1); - } - await uploadFile(config, args[1], args[2]); - break; - - case 'rm': - if (!args[1]) { - console.error('Usage: bun note.ts rm '); - process.exit(1); - } - await removeFile(config, args[1]); - break; - - case 'mv': - if (!args[1] || !args[2]) { - console.error('Usage: bun note.ts mv '); - process.exit(1); - } - await moveFile(config, args[1], args[2]); - break; - - case 'get': - if (!args[1]) { - console.error('Usage: bun note.ts get [local_file]'); - process.exit(1); - } - await downloadFile(config, args[1], args[2]); - break; - - case 'help': - default: - showHelp(); - break; - } + console.log(formatTable(headers, rows)); + console.log(); } -// Run the main function -not3().catch((error) => { - console.error('❌ Error:', error); +async function main(): 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, + ); +} + +main().catch((err) => { + console.error("❌ Error:", err); process.exit(1); }); - -export default not3; \ No newline at end of file