opentunnel-cli 1.0.31 → 1.0.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -238,7 +238,7 @@ program
238
238
  .name("opentunnel")
239
239
  .alias("ot")
240
240
  .description("Expose local ports to the internet via custom domains, ngrok, or Cloudflare Tunnel")
241
- .version("1.0.31");
241
+ .version("1.0.33");
242
242
  // Helper function to build WebSocket URL from domain
243
243
  // User only provides base domain (e.g., fjrg2007.com), system handles the rest
244
244
  // Note: --insecure flag only affects certificate verification, not the protocol
@@ -651,9 +651,9 @@ program
651
651
  .option("--streaming", "Enable streaming mode (longer timeouts for video/large files)")
652
652
  .option("--timeout <ms>", "Custom request timeout in milliseconds")
653
653
  .option("--ngrok", "Use ngrok instead of OpenTunnel server")
654
- .option("--region <region>", "Ngrok region (us, eu, ap, au, sa, jp, in)", "us")
654
+ .option("--region <region>", "Region (ngrok: us, eu, ap, au, sa, jp, in)", "us")
655
655
  .option("--cloudflare, --cf", "Use Cloudflare Tunnel instead of OpenTunnel server")
656
- .option("--cf-hostname <hostname>", "Custom hostname for Cloudflare Tunnel")
656
+ .option("--hostname <hostname>", "Custom hostname for tunnel (cloudflare named tunnels)")
657
657
  .option("--provider <provider>", "Tunnel provider (opentunnel, ngrok, cloudflare)")
658
658
  .action(async (port, options) => {
659
659
  // Determine provider
@@ -666,8 +666,8 @@ program
666
666
  protocol: options.https ? "https" : "http",
667
667
  localHost: options.host,
668
668
  localPort: parseInt(port),
669
- hostname: options.cfHostname,
670
- tunnelName: options.subdomain, // -n works as tunnel name for CF
669
+ hostname: options.hostname,
670
+ tunnelName: options.subdomain, // -n works as tunnel name
671
671
  noTlsVerify: options.insecure,
672
672
  });
673
673
  return;
@@ -721,9 +721,9 @@ program
721
721
  .option("-h, --host <host>", "Local host", "localhost")
722
722
  .option("--insecure", "Skip SSL verification (for self-signed certs)")
723
723
  .option("--ngrok", "Use ngrok instead of OpenTunnel server")
724
- .option("--region <region>", "Ngrok region (us, eu, ap, au, sa, jp, in)", "us")
724
+ .option("--region <region>", "Region (ngrok: us, eu, ap, au, sa, jp, in)", "us")
725
725
  .option("--cloudflare, --cf", "Use Cloudflare Tunnel (TCP requires named tunnel)")
726
- .option("--cf-hostname <hostname>", "Custom hostname for Cloudflare Tunnel")
726
+ .option("--hostname <hostname>", "Custom hostname for tunnel")
727
727
  .option("--provider <provider>", "Tunnel provider (opentunnel, ngrok, cloudflare)")
728
728
  .action(async (port, options) => {
729
729
  // Determine provider
@@ -741,7 +741,7 @@ program
741
741
  protocol: "tcp",
742
742
  localHost: options.host,
743
743
  localPort: parseInt(port),
744
- hostname: options.cfHostname,
744
+ hostname: options.hostname,
745
745
  tunnelName: options.subdomain,
746
746
  noTlsVerify: options.insecure,
747
747
  });
@@ -2661,8 +2661,8 @@ async function startTunnelsFromConfig(tunnels, serverUrl, token, insecure, globa
2661
2661
  const ngrokTunnels = tunnelsByProvider.get("ngrok") || [];
2662
2662
  for (const tunnel of ngrokTunnels) {
2663
2663
  const spinner = (0, ora_1.default)(`Creating ngrok tunnel: ${tunnel.name}...`).start();
2664
- const ngrokToken = tunnel.ngrokToken || globalConfig?.ngrok?.token;
2665
- const ngrokRegion = tunnel.ngrokRegion || globalConfig?.ngrok?.region || "us";
2664
+ const ngrokToken = tunnel.token || globalConfig?.token;
2665
+ const ngrokRegion = tunnel.region || globalConfig?.region || "us";
2666
2666
  // Merge IP access config: tunnel-specific > global security > none
2667
2667
  const { IpFilter } = await Promise.resolve().then(() => __importStar(require("../shared/ip-filter")));
2668
2668
  const ipAccess = IpFilter.mergeConfigs(globalConfig?.security?.ipAccess, tunnel.ipAccess);
@@ -2693,31 +2693,31 @@ async function startTunnelsFromConfig(tunnels, serverUrl, token, insecure, globa
2693
2693
  const cloudflareTunnels = tunnelsByProvider.get("cloudflare") || [];
2694
2694
  for (const tunnel of cloudflareTunnels) {
2695
2695
  const spinner = (0, ora_1.default)(`Creating Cloudflare tunnel: ${tunnel.name}...`).start();
2696
- const cfHostname = tunnel.cfHostname || globalConfig?.cloudflare?.hostname;
2697
- // For Cloudflare: subdomain = tunnel name (for named tunnels)
2698
- const cfTunnelName = tunnel.subdomain || globalConfig?.cloudflare?.tunnelName;
2699
- // Validate: named tunnels require cfHostname for public access
2700
- if (cfTunnelName && !cfHostname) {
2701
- spinner.fail(`${tunnel.name}: Named tunnel '${cfTunnelName}' requires cfHostname`);
2696
+ const tunnelHostname = tunnel.hostname || globalConfig?.hostname;
2697
+ // tunnelName takes priority, subdomain as fallback for compatibility
2698
+ const namedTunnel = tunnel.tunnelName || tunnel.subdomain || globalConfig?.tunnelName;
2699
+ // Validate: named tunnels require hostname for public access
2700
+ if (namedTunnel && !tunnelHostname) {
2701
+ spinner.fail(`${tunnel.name}: Named tunnel '${namedTunnel}' requires hostname`);
2702
2702
  console.log(chalk_1.default.gray(` Add to your config:`));
2703
- console.log(chalk_1.default.white(` cfHostname: ${cfTunnelName}.yourdomain.com`));
2704
- console.log(chalk_1.default.gray(` Or remove 'subdomain' for a quick tunnel with random URL`));
2703
+ console.log(chalk_1.default.white(` hostname: ${namedTunnel}.yourdomain.com`));
2704
+ console.log(chalk_1.default.gray(` Or remove 'tunnelName' for a quick tunnel with random URL`));
2705
2705
  continue;
2706
2706
  }
2707
2707
  // Merge IP access config: tunnel-specific > global security > none
2708
2708
  const { IpFilter } = await Promise.resolve().then(() => __importStar(require("../shared/ip-filter")));
2709
2709
  const ipAccess = IpFilter.mergeConfigs(globalConfig?.security?.ipAccess, tunnel.ipAccess);
2710
2710
  const client = new CloudflareTunnelClient_1.CloudflareTunnelClient({
2711
- hostname: cfHostname,
2712
- tunnelName: cfTunnelName,
2711
+ hostname: tunnelHostname,
2712
+ tunnelName: namedTunnel,
2713
2713
  protocol: tunnel.protocol === "https" ? "https" : "http",
2714
2714
  ipAccess,
2715
2715
  });
2716
2716
  try {
2717
2717
  await client.connect();
2718
2718
  // Auto-route DNS if using named tunnel with hostname
2719
- if (cfTunnelName && cfHostname) {
2720
- const routeResult = await CloudflareTunnelClient_1.CloudflareTunnelClient.routeDns(cfTunnelName, cfHostname);
2719
+ if (namedTunnel && tunnelHostname) {
2720
+ const routeResult = await CloudflareTunnelClient_1.CloudflareTunnelClient.routeDns(namedTunnel, tunnelHostname);
2721
2721
  if (!routeResult.success && !routeResult.error?.includes("already exists")) {
2722
2722
  spinner.warn(`${tunnel.name}: DNS routing failed: ${routeResult.error}`);
2723
2723
  }
@@ -2729,7 +2729,7 @@ async function startTunnelsFromConfig(tunnels, serverUrl, token, insecure, globa
2729
2729
  });
2730
2730
  activeTunnels.push({ name: tunnel.name, tunnelId, publicUrl, provider: "cloudflare", client });
2731
2731
  clients.push({ provider: "cloudflare", client });
2732
- const tunnelMode = cfTunnelName ? ` [${cfTunnelName}]` : "";
2732
+ const tunnelMode = namedTunnel ? ` [${namedTunnel}]` : "";
2733
2733
  spinner.succeed(`${tunnel.name}: ${publicUrl}${tunnelMode} (cloudflare)`);
2734
2734
  }
2735
2735
  catch (err) {
@@ -3426,5 +3426,174 @@ configCommand
3426
3426
  console.log(chalk_1.default.gray(" " + "─".repeat(40)));
3427
3427
  console.log(chalk_1.default.gray(`\n Config file: ${credentials_1.CredentialsManager.getCredentialsFile()}\n`));
3428
3428
  });
3429
+ // =============================================================================
3430
+ // Diagnose command - Generate troubleshooting report
3431
+ // =============================================================================
3432
+ program
3433
+ .command("diagnose")
3434
+ .description("Generate a diagnostic report for troubleshooting")
3435
+ .option("-o, --output <file>", "Output file name", "opentunnel-diagnostic.txt")
3436
+ .action(async (options) => {
3437
+ const os = await Promise.resolve().then(() => __importStar(require("os")));
3438
+ const { execSync } = await Promise.resolve().then(() => __importStar(require("child_process")));
3439
+ const lines = [];
3440
+ const addSection = (title) => {
3441
+ lines.push("");
3442
+ lines.push("=".repeat(60));
3443
+ lines.push(title);
3444
+ lines.push("=".repeat(60));
3445
+ };
3446
+ const addLine = (label, value) => {
3447
+ lines.push(`${label.padEnd(25)}: ${value}`);
3448
+ };
3449
+ const runCommand = (cmd) => {
3450
+ try {
3451
+ return execSync(cmd, { encoding: "utf-8", timeout: 5000 }).trim();
3452
+ }
3453
+ catch {
3454
+ return "(not available)";
3455
+ }
3456
+ };
3457
+ lines.push("OpenTunnel Diagnostic Report");
3458
+ lines.push(`Generated: ${new Date().toISOString()}`);
3459
+ // System Information
3460
+ addSection("SYSTEM INFORMATION");
3461
+ addLine("OpenTunnel Version", program.version() || "unknown");
3462
+ addLine("Node.js Version", process.version);
3463
+ addLine("Platform", `${os.platform()} (${os.arch()})`);
3464
+ addLine("OS Release", os.release());
3465
+ addLine("OS Type", os.type());
3466
+ addLine("Hostname", os.hostname());
3467
+ addLine("Working Directory", process.cwd());
3468
+ addLine("Home Directory", os.homedir());
3469
+ addLine("Total Memory", `${Math.round(os.totalmem() / 1024 / 1024 / 1024)} GB`);
3470
+ addLine("Free Memory", `${Math.round(os.freemem() / 1024 / 1024 / 1024)} GB`);
3471
+ // Provider Status
3472
+ addSection("PROVIDER STATUS");
3473
+ // Check cloudflared
3474
+ const cloudflaredVersion = runCommand("cloudflared --version");
3475
+ addLine("cloudflared", cloudflaredVersion.includes("cloudflared") ? cloudflaredVersion.split("\n")[0] : "(not installed)");
3476
+ // Check ngrok
3477
+ const ngrokVersion = runCommand("ngrok version");
3478
+ addLine("ngrok", ngrokVersion.includes("ngrok") ? ngrokVersion : "(not installed)");
3479
+ // Configuration
3480
+ addSection("CONFIGURATION");
3481
+ const configPath = path.join(process.cwd(), CONFIG_FILE);
3482
+ if (fs.existsSync(configPath)) {
3483
+ addLine("Config File", configPath);
3484
+ try {
3485
+ const config = loadConfig(configPath);
3486
+ if (config) {
3487
+ addLine("Provider", config.provider || "opentunnel");
3488
+ addLine("Mode", config.mode || "auto");
3489
+ addLine("Tunnel Name", config.tunnelName || "(not set)");
3490
+ addLine("Hostname", config.hostname || "(not set)");
3491
+ addLine("Region", config.region || "(not set)");
3492
+ addLine("Tunnels Count", config.tunnels?.length?.toString() || "0");
3493
+ if (config.tunnels) {
3494
+ lines.push("");
3495
+ lines.push("Configured Tunnels:");
3496
+ for (const t of config.tunnels) {
3497
+ lines.push(` - ${t.name}: ${t.protocol}://localhost:${t.port} (provider: ${t.provider || config.provider || "opentunnel"})`);
3498
+ }
3499
+ }
3500
+ }
3501
+ }
3502
+ catch (err) {
3503
+ addLine("Config Parse Error", err.message);
3504
+ }
3505
+ }
3506
+ else {
3507
+ addLine("Config File", "(not found)");
3508
+ }
3509
+ // Credentials
3510
+ addSection("CREDENTIALS STATUS");
3511
+ const credsFile = credentials_1.CredentialsManager.getCredentialsFile();
3512
+ addLine("Credentials File", fs.existsSync(credsFile) ? "exists" : "not found");
3513
+ try {
3514
+ const credsMgr = new credentials_1.CredentialsManager();
3515
+ const creds = credsMgr.getAll();
3516
+ addLine("ngrok Token", creds.ngrok?.token ? "configured" : "not set");
3517
+ addLine("Cloudflare Auth", creds.cloudflare ? "configured" : "not set");
3518
+ }
3519
+ catch {
3520
+ addLine("Credentials", "(error reading)");
3521
+ }
3522
+ // Running Instances
3523
+ addSection("RUNNING INSTANCES");
3524
+ const runDir = path.join(os.homedir(), ".opentunnel", "run");
3525
+ if (fs.existsSync(runDir)) {
3526
+ const pidFiles = fs.readdirSync(runDir).filter(f => f.endsWith(".pid"));
3527
+ if (pidFiles.length > 0) {
3528
+ for (const pidFile of pidFiles) {
3529
+ const name = pidFile.replace(".pid", "");
3530
+ const pidPath = path.join(runDir, pidFile);
3531
+ try {
3532
+ const pid = fs.readFileSync(pidPath, "utf-8").trim();
3533
+ addLine(`Instance: ${name}`, `PID ${pid}`);
3534
+ }
3535
+ catch {
3536
+ addLine(`Instance: ${name}`, "(error reading PID)");
3537
+ }
3538
+ }
3539
+ }
3540
+ else {
3541
+ lines.push("No running instances found");
3542
+ }
3543
+ }
3544
+ else {
3545
+ lines.push("No running instances found");
3546
+ }
3547
+ // Recent Logs
3548
+ addSection("RECENT LOGS (last 50 lines)");
3549
+ const logsDir = path.join(os.homedir(), ".opentunnel", "logs");
3550
+ if (fs.existsSync(logsDir)) {
3551
+ const logFiles = fs.readdirSync(logsDir)
3552
+ .filter(f => f.endsWith(".log"))
3553
+ .map(f => ({ name: f, mtime: fs.statSync(path.join(logsDir, f)).mtime }))
3554
+ .sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
3555
+ if (logFiles.length > 0) {
3556
+ const latestLog = logFiles[0];
3557
+ lines.push(`Log file: ${latestLog.name}`);
3558
+ lines.push("-".repeat(40));
3559
+ try {
3560
+ const logContent = fs.readFileSync(path.join(logsDir, latestLog.name), "utf-8");
3561
+ const logLines = logContent.split("\n").slice(-50);
3562
+ lines.push(...logLines);
3563
+ }
3564
+ catch {
3565
+ lines.push("(error reading log file)");
3566
+ }
3567
+ }
3568
+ else {
3569
+ lines.push("No log files found");
3570
+ }
3571
+ }
3572
+ else {
3573
+ lines.push("No logs directory found");
3574
+ }
3575
+ // Environment Variables (relevant ones only)
3576
+ addSection("ENVIRONMENT");
3577
+ const relevantEnvVars = [
3578
+ "OPENTUNNEL_TOKEN",
3579
+ "NGROK_AUTHTOKEN",
3580
+ "CLOUDFLARE_TUNNEL_TOKEN",
3581
+ "TUNNEL_TOKEN",
3582
+ "HTTP_PROXY",
3583
+ "HTTPS_PROXY",
3584
+ "NO_PROXY"
3585
+ ];
3586
+ for (const envVar of relevantEnvVars) {
3587
+ const value = process.env[envVar];
3588
+ addLine(envVar, value ? "(set)" : "(not set)");
3589
+ }
3590
+ // Write report
3591
+ const reportContent = lines.join("\n");
3592
+ const outputFile = options.output;
3593
+ fs.writeFileSync(outputFile, reportContent, "utf-8");
3594
+ console.log(chalk_1.default.green(`\n✓ Diagnostic report generated: ${outputFile}`));
3595
+ console.log(chalk_1.default.gray(`\nInclude this file when reporting issues at:`));
3596
+ console.log(chalk_1.default.cyan(" https://github.com/anthropics/opentunnel/issues\n"));
3597
+ });
3429
3598
  program.parse();
3430
3599
  //# sourceMappingURL=index.js.map