#!/usr/bin/env bun import minimist from "minimist"; import os from "os"; import path from "path"; import compose from "./src/compose"; import generateDockerfile from "./src/docker-file"; import frp from "./src/frp"; import { generateEnvTypes } from "./src/generate/env.generate"; import checkPort from "./src/port"; import route from "./src/route"; import { version } from '../package.json' assert { type: 'json' }; import appCreate from "./src/app-create"; interface CheckPortResult { port: number; open: boolean; } // 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", }; // CLI Help const HELP_TEXT = ` g3n [command] [options] Commands: 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 docker-file Generate Dockerfile frp Show frp proxy list app-create Generate app with Elysia and React Options: --env Path ke file .env (default: .env) --out Path file output (default: types/env.d.ts) --start Port awal scan (default: 3000) --end Port akhir scan (default: 4000) --host Host/IP target (default: 127.0.0.1) 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 docker-file g3n frp g3n app-create Version: ${version} `; const g3nConf = path.join(os.homedir(), ".g3n.conf"); if (!(await Bun.file(g3nConf).exists())) { const conf = ` # CODE CODE_TOKEN= # FRP FRP_HOST= FRP_USER= FRP_SECRET= ` Bun.write(g3nConf, conf); console.log(`✅ G3N config created at ${g3nConf}`); } // Parse CLI arguments const args = minimist(process.argv.slice(2)); /** * 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; case "app-create": if (!name) { console.error("❌ App name is required"); return; } appCreate({ appName: name }); 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 (!args.env) { console.error("❌ Compose env (staging/prod) is required"); return; } if (args.env !== "staging" && args.env !== "prod") { console.error("❌ Compose env (staging/prod) is required"); return; } if (!args.port) { console.error("❌ Compose port is required"); return; } if(args.port.length !== 2) { console.error("❌ Compose port must be 2 digits"); return; } compose(name, args.env, Number(args.port)); console.log(`✅ Compose file generated for ${name}`); } // Execute CLI main().catch((err) => { console.error("❌ Unexpected error:", err); process.exit(1); });