#!/usr/bin/env node import { Command } from "commander"; import { startServer } from "./server.js"; import { startTunnel } from "./tunnels/index.js"; import { getConfig, getConfigValue, setConfigValue, deleteConfigValue, listConfig, isValidKey, isValidProvider, getValidKeys, } from "./config.js"; import type { Provider } from "./config.js"; import chalk from "chalk"; import { resolve } from "path"; import { existsSync, statSync, readFileSync } from "fs"; import { fileURLToPath } from "url"; import { dirname, join } from "path"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const pkg = JSON.parse( readFileSync(join(__dirname, "..", "package.json"), "utf-8") ); const program = new Command(); program .name("folderex") .description( "Share any local folder via a public HTTPS URL" ) .version(pkg.version); // ── Main command: serve ────────────────────────────────────────────── program .argument("[folder]", "Folder to share", ".") .option("-u, --user ", "Username for basic auth") .option("-p, --pass ", "Password for basic auth") .option("-t, --tunnel ", "Tunnel provider: cloudflare | loophole | zrok | expose | packetriot | srvus") .option("-s, --subdomain ", "Custom subdomain or GitHub username") .option("-k, --sshkey ", "SSH private key (name or full path)") .option("-P, --port ", "Local port (default: random available)") .option("-L, --local", "Disable tunnel, local server only") .action(async (folder: string, options: Record) => { const config = getConfig(); // Resolve with config fallback const user = (options.user as string) || config.user || undefined; const pass = (options.pass as string) || config.pass || undefined; const tunnelRaw = (options.tunnel as string) || config.tunnel || "cloudflare"; const subdomain = (options.subdomain as string) || config.subdomain; const sshkey = (options.sshkey as string) || config.sshkey; const portRaw = (options.port as string) || config.port; const port = portRaw ? parseInt(portRaw, 10) : 0; const localOnly = options.local === true; // Validate provider if (!isValidProvider(tunnelRaw)) { console.error( chalk.red( `\n Error: invalid tunnel provider "${tunnelRaw}".\n` + ` Valid providers: cloudflare, loophole, zrok, expose, packetriot, srvus\n` ) ); process.exit(1); } const provider = tunnelRaw as Provider; const root = resolve(folder); if (!existsSync(root)) { console.error(chalk.red(`\n Error: folder "${root}" does not exist.\n`)); process.exit(1); } if (!statSync(root).isDirectory()) { console.error( chalk.red(`\n Error: "${root}" is not a directory.\n`) ); process.exit(1); } // Determine if auth is enabled const useAuth = !!(user && pass); try { const { url: localUrl, port: actualPort } = await startServer({ root, user: user || "", pass: pass || "", port, }); // ── Startup summary ────────────────────────────────────────── console.log(""); console.log(chalk.bold(" folderex")); console.log(""); console.log(chalk.green(" * ") + `Local: ${chalk.underline(localUrl)}`); if (!localOnly) { console.log(chalk.dim(" - Starting tunnel...")); const publicUrl = await startTunnel({ port: actualPort, provider, subdomain, sshkey, }); // Clear the "Starting tunnel..." line process.stdout.write("\x1B[1A\x1B[2K"); console.log( chalk.green(" * ") + `Public: ${chalk.bold.underline(publicUrl)}` ); } console.log(""); console.log(chalk.dim(" ─────────────────────────────────────")); console.log(chalk.dim(` Folder: ${root}`)); console.log(chalk.dim(` Port: ${actualPort}`)); console.log(chalk.dim(` Tunnel: ${localOnly ? "disabled" : provider}`)); if (subdomain) { console.log(chalk.dim(` Subdomain: ${subdomain}`)); } if (useAuth) { console.log(chalk.dim(` Auth: ${user} / ${"*".repeat(pass!.length)}`)); } else { console.log(chalk.dim(` Auth: ${chalk.yellow("disabled (no credentials)")}`)); } console.log(chalk.dim(" ─────────────────────────────────────")); console.log(""); console.log(chalk.dim(" Press Ctrl+C to stop")); console.log(""); } catch (err: unknown) { const message = err instanceof Error ? err.message : String(err); console.error(chalk.red(`\n Error: ${message}\n`)); process.exit(1); } }); // ── Subcommand: config ─────────────────────────────────────────────── const configCmd = program .command("config") .description("Manage folderex configuration"); // config set configCmd .command("set") .description("Set a configuration value") .argument("", "Configuration key") .argument("", "Configuration value") .action((key: string, value: string) => { if (!isValidKey(key)) { console.error( chalk.red( `\n Error: invalid key "${key}". Valid keys: ${getValidKeys().join(", ")}\n` ) ); process.exit(1); } if (key === "tunnel" && !isValidProvider(value)) { console.error( chalk.red( `\n Error: invalid tunnel provider "${value}".\n` + ` Valid providers: cloudflare, loophole, zrok, expose, packetriot, srvus\n` ) ); process.exit(1); } setConfigValue(key, value); console.log(chalk.green(`\n ${key} = ${key === "pass" ? "********" : value}\n`)); }); // config get configCmd .command("get") .description("Get a configuration value") .argument("", "Configuration key") .action((key: string) => { const value = getConfigValue(key); if (value === undefined) { console.log(chalk.dim(`\n ${key} is not set\n`)); } else { console.log(`\n ${key} = ${key === "pass" ? "********" : value}\n`); } }); // config list configCmd .command("list") .description("List all configuration values") .action(() => { const entries = listConfig(); if (entries.length === 0) { console.log(chalk.dim("\n No configuration set.\n")); console.log( chalk.dim(" Use: folderex config set ") ); console.log( chalk.dim(` Valid keys: ${getValidKeys().join(", ")}\n`) ); return; } console.log(""); const maxKeyLen = Math.max(...entries.map((e) => e.key.length)); for (const { key, value } of entries) { const display = key === "pass" ? "********" : value; console.log(` ${key.padEnd(maxKeyLen)} ${display}`); } console.log(""); }); // config delete configCmd .command("delete") .description("Delete a configuration value") .argument("", "Configuration key") .action((key: string) => { if (deleteConfigValue(key)) { console.log(chalk.green(`\n Deleted: ${key}\n`)); } else { console.log(chalk.dim(`\n ${key} was not set\n`)); } }); // ── Graceful shutdown ──────────────────────────────────────────────── process.on("SIGINT", () => { console.log(chalk.dim("\n Shutting down...")); process.exit(0); }); process.on("SIGTERM", () => { process.exit(0); }); program.parse();