opentunnel-cli 1.0.22 → 1.0.25

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.
Files changed (50) hide show
  1. package/README.md +69 -864
  2. package/dist/cli/index.js +712 -84
  3. package/dist/cli/index.js.map +1 -1
  4. package/dist/client/CloudflareTunnelClient.d.ts +110 -0
  5. package/dist/client/CloudflareTunnelClient.d.ts.map +1 -0
  6. package/dist/client/CloudflareTunnelClient.js +531 -0
  7. package/dist/client/CloudflareTunnelClient.js.map +1 -0
  8. package/dist/client/NgrokClient.d.ts +18 -1
  9. package/dist/client/NgrokClient.d.ts.map +1 -1
  10. package/dist/client/NgrokClient.js +130 -4
  11. package/dist/client/NgrokClient.js.map +1 -1
  12. package/dist/client/TunnelClient.d.ts +0 -1
  13. package/dist/client/TunnelClient.d.ts.map +1 -1
  14. package/dist/client/TunnelClient.js +2 -96
  15. package/dist/client/TunnelClient.js.map +1 -1
  16. package/dist/client/index.d.ts +1 -0
  17. package/dist/client/index.d.ts.map +1 -1
  18. package/dist/client/index.js +3 -1
  19. package/dist/client/index.js.map +1 -1
  20. package/dist/index.d.ts +1 -0
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +3 -1
  23. package/dist/index.js.map +1 -1
  24. package/dist/lib/index.d.ts +2 -0
  25. package/dist/lib/index.d.ts.map +1 -0
  26. package/dist/lib/index.js +18 -0
  27. package/dist/lib/index.js.map +1 -0
  28. package/dist/lib/pages/index.d.ts +2 -0
  29. package/dist/lib/pages/index.d.ts.map +1 -0
  30. package/dist/lib/pages/index.js +6 -0
  31. package/dist/lib/pages/index.js.map +1 -0
  32. package/dist/lib/pages/not-running.d.ts +10 -0
  33. package/dist/lib/pages/not-running.d.ts.map +1 -0
  34. package/dist/lib/pages/not-running.js +117 -0
  35. package/dist/lib/pages/not-running.js.map +1 -0
  36. package/dist/server/TunnelServer.d.ts +7 -3
  37. package/dist/server/TunnelServer.d.ts.map +1 -1
  38. package/dist/server/TunnelServer.js +164 -51
  39. package/dist/server/TunnelServer.js.map +1 -1
  40. package/dist/shared/credentials.d.ts +101 -0
  41. package/dist/shared/credentials.d.ts.map +1 -0
  42. package/dist/shared/credentials.js +302 -0
  43. package/dist/shared/credentials.js.map +1 -0
  44. package/dist/shared/ip-filter.d.ts +75 -0
  45. package/dist/shared/ip-filter.d.ts.map +1 -0
  46. package/dist/shared/ip-filter.js +203 -0
  47. package/dist/shared/ip-filter.js.map +1 -0
  48. package/dist/shared/types.d.ts +46 -0
  49. package/dist/shared/types.d.ts.map +1 -1
  50. package/package.json +7 -3
package/dist/cli/index.js CHANGED
@@ -42,6 +42,8 @@ const chalk_1 = __importDefault(require("chalk"));
42
42
  const ora_1 = __importDefault(require("ora"));
43
43
  const TunnelClient_1 = require("../client/TunnelClient");
44
44
  const NgrokClient_1 = require("../client/NgrokClient");
45
+ const CloudflareTunnelClient_1 = require("../client/CloudflareTunnelClient");
46
+ const credentials_1 = require("../shared/credentials");
45
47
  const utils_1 = require("../shared/utils");
46
48
  const yaml_1 = require("yaml");
47
49
  const fs = __importStar(require("fs"));
@@ -220,8 +222,8 @@ const program = new commander_1.Command();
220
222
  program
221
223
  .name("opentunnel")
222
224
  .alias("ot")
223
- .description("Expose local ports to the internet via custom domains or ngrok")
224
- .version("1.0.22");
225
+ .description("Expose local ports to the internet via custom domains, ngrok, or Cloudflare Tunnel")
226
+ .version("1.0.25");
225
227
  // Helper function to build WebSocket URL from domain
226
228
  // User only provides base domain (e.g., fjrg2007.com), system handles the rest
227
229
  // Note: --insecure flag only affects certificate verification, not the protocol
@@ -618,8 +620,28 @@ program
618
620
  .option("--insecure", "Skip SSL verification (for self-signed certs)")
619
621
  .option("--ngrok", "Use ngrok instead of OpenTunnel server")
620
622
  .option("--region <region>", "Ngrok region (us, eu, ap, au, sa, jp, in)", "us")
623
+ .option("--cloudflare, --cf", "Use Cloudflare Tunnel instead of OpenTunnel server")
624
+ .option("--cf-hostname <hostname>", "Custom hostname for Cloudflare Tunnel")
625
+ .option("--provider <provider>", "Tunnel provider (opentunnel, ngrok, cloudflare)")
621
626
  .action(async (port, options) => {
622
- if (options.ngrok || options.domain === "ngrok") {
627
+ // Determine provider
628
+ const provider = options.provider?.toLowerCase() ||
629
+ (options.cloudflare || options.cf ? "cloudflare" : null) ||
630
+ (options.ngrok ? "ngrok" : null);
631
+ // Cloudflare Tunnel
632
+ if (provider === "cloudflare" || provider === "cf") {
633
+ await createCloudflareTunnel({
634
+ protocol: options.https ? "https" : "http",
635
+ localHost: options.host,
636
+ localPort: parseInt(port),
637
+ hostname: options.cfHostname,
638
+ tunnelName: options.subdomain, // -n works as tunnel name for CF
639
+ noTlsVerify: options.insecure,
640
+ });
641
+ return;
642
+ }
643
+ // ngrok
644
+ if (provider === "ngrok") {
623
645
  await createNgrokTunnel({
624
646
  protocol: options.https ? "https" : "http",
625
647
  localHost: options.host,
@@ -630,8 +652,8 @@ program
630
652
  });
631
653
  return;
632
654
  }
633
- // If remote server domain provided, just connect to it
634
- if (options.domain) {
655
+ // If remote server domain provided, connect to OpenTunnel server
656
+ if (options.domain && options.domain !== "cloudflare" && options.domain !== "ngrok") {
635
657
  const { url: serverUrl } = buildServerUrl(options.domain, options.basePath);
636
658
  await createTunnel({
637
659
  protocol: options.https ? "https" : "http",
@@ -649,6 +671,8 @@ program
649
671
  console.log(chalk_1.default.gray("\nExamples:"));
650
672
  console.log(chalk_1.default.cyan(" opentunnel http 3000 -s example.com"));
651
673
  console.log(chalk_1.default.cyan(" opentunnel http 3000 --domain example.com -n myapp"));
674
+ console.log(chalk_1.default.cyan(" opentunnel http 3000 --cloudflare"));
675
+ console.log(chalk_1.default.cyan(" opentunnel http 3000 --ngrok"));
652
676
  process.exit(1);
653
677
  });
654
678
  // TCP tunnel command
@@ -664,8 +688,33 @@ program
664
688
  .option("--insecure", "Skip SSL verification (for self-signed certs)")
665
689
  .option("--ngrok", "Use ngrok instead of OpenTunnel server")
666
690
  .option("--region <region>", "Ngrok region (us, eu, ap, au, sa, jp, in)", "us")
691
+ .option("--cloudflare, --cf", "Use Cloudflare Tunnel (TCP requires named tunnel)")
692
+ .option("--cf-hostname <hostname>", "Custom hostname for Cloudflare Tunnel")
693
+ .option("--provider <provider>", "Tunnel provider (opentunnel, ngrok, cloudflare)")
667
694
  .action(async (port, options) => {
668
- if (options.ngrok || options.domain === "ngrok") {
695
+ // Determine provider
696
+ const provider = options.provider?.toLowerCase() ||
697
+ (options.cloudflare || options.cf ? "cloudflare" : null) ||
698
+ (options.ngrok ? "ngrok" : null);
699
+ // Cloudflare Tunnel - Note: TCP requires named tunnel
700
+ if (provider === "cloudflare" || provider === "cf") {
701
+ if (!options.subdomain) {
702
+ console.log(chalk_1.default.yellow("Note: Cloudflare TCP tunnels require a named tunnel."));
703
+ console.log(chalk_1.default.gray("Use -n <tunnel-name> to specify a named tunnel."));
704
+ console.log(chalk_1.default.gray("\nExample: opentunnel tcp 5432 --cf -n my-tunnel"));
705
+ }
706
+ await createCloudflareTunnel({
707
+ protocol: "tcp",
708
+ localHost: options.host,
709
+ localPort: parseInt(port),
710
+ hostname: options.cfHostname,
711
+ tunnelName: options.subdomain,
712
+ noTlsVerify: options.insecure,
713
+ });
714
+ return;
715
+ }
716
+ // ngrok
717
+ if (provider === "ngrok") {
669
718
  await createNgrokTunnel({
670
719
  protocol: "tcp",
671
720
  localHost: options.host,
@@ -676,8 +725,8 @@ program
676
725
  });
677
726
  return;
678
727
  }
679
- // If remote server domain provided, just connect to it
680
- if (options.domain) {
728
+ // If remote server domain provided, connect to OpenTunnel server
729
+ if (options.domain && options.domain !== "cloudflare" && options.domain !== "ngrok") {
681
730
  const { url: serverUrl } = buildServerUrl(options.domain, options.basePath);
682
731
  await createTunnel({
683
732
  protocol: "tcp",
@@ -695,6 +744,7 @@ program
695
744
  console.log(chalk_1.default.gray("\nExamples:"));
696
745
  console.log(chalk_1.default.cyan(" opentunnel tcp 5432 -s example.com"));
697
746
  console.log(chalk_1.default.cyan(" opentunnel tcp 5432 --domain example.com -r 15432"));
747
+ console.log(chalk_1.default.cyan(" opentunnel tcp 5432 --ngrok"));
698
748
  process.exit(1);
699
749
  });
700
750
  // Quick expose command
@@ -709,6 +759,7 @@ program
709
759
  .option("--domain <domain>", "Server domain (e.g., domain.com)")
710
760
  .option("--insecure", "Skip SSL certificate verification (for self-signed certs)")
711
761
  .option("--ngrok", "Use ngrok instead of OpenTunnel server")
762
+ .option("--cloudflare, --cf", "Use Cloudflare Tunnel instead of OpenTunnel server")
712
763
  .action(async (port, options) => {
713
764
  const serverUrl = options.server || (options.domain
714
765
  ? `wss://${options.domain}/_tunnel`
@@ -717,26 +768,37 @@ program
717
768
  await runTunnelInBackground("expose", port, { ...options, server: serverUrl });
718
769
  return;
719
770
  }
720
- if (options.ngrok || options.server === "ngrok") {
721
- await createNgrokTunnel({
771
+ // Cloudflare Tunnel
772
+ if (options.cloudflare || options.cf || options.server === "cloudflare") {
773
+ await createCloudflareTunnel({
722
774
  protocol: options.protocol,
723
775
  localHost: "localhost",
724
776
  localPort: parseInt(port),
725
- subdomain: options.subdomain,
726
- authtoken: options.token
777
+ noTlsVerify: options.insecure,
727
778
  });
779
+ return;
728
780
  }
729
- else {
730
- await createTunnel({
781
+ // ngrok
782
+ if (options.ngrok || options.server === "ngrok") {
783
+ await createNgrokTunnel({
731
784
  protocol: options.protocol,
732
785
  localHost: "localhost",
733
786
  localPort: parseInt(port),
734
787
  subdomain: options.subdomain,
735
- serverUrl,
736
- token: options.token,
737
- insecure: options.insecure
788
+ authtoken: options.token
738
789
  });
790
+ return;
739
791
  }
792
+ // OpenTunnel server
793
+ await createTunnel({
794
+ protocol: options.protocol,
795
+ localHost: "localhost",
796
+ localPort: parseInt(port),
797
+ subdomain: options.subdomain,
798
+ serverUrl,
799
+ token: options.token,
800
+ insecure: options.insecure
801
+ });
740
802
  });
741
803
  // Server command
742
804
  program
@@ -761,6 +823,12 @@ program
761
823
  .option("--ip-mode <mode>", "IP access mode: all, allowlist, denylist (default: all)")
762
824
  .option("--ip-allow <ips>", "Comma-separated IPs/CIDRs to allow (e.g., 192.168.1.0/24,10.0.0.1)")
763
825
  .option("--ip-deny <ips>", "Comma-separated IPs/CIDRs to deny")
826
+ .option("--dymo-api-key <key>", "Dymo API key for fraud detection (optional)")
827
+ .option("--no-dymo-block-bots", "Allow bot user agents (blocked by default when Dymo enabled)")
828
+ .option("--dymo-block-proxies", "Block proxy/VPN IPs")
829
+ .option("--dymo-block-hosting", "Block hosting/datacenter IPs")
830
+ .option("--no-dymo-cache", "Disable Dymo verification caching (sends API request for every HTTP request)")
831
+ .option("--dymo-cache-ttl <seconds>", "Dymo cache TTL in seconds (default: 300)")
764
832
  .option("-d, --detach", "Run server in background (detached mode)")
765
833
  .action(async (options) => {
766
834
  // Load config from opentunnel.yml if exists (with env variable substitution)
@@ -795,6 +863,12 @@ program
795
863
  ipMode: options.ipMode || fileConfig.ipAccess?.mode || "all",
796
864
  ipAllow: options.ipAllow || fileConfig.ipAccess?.allowList?.join(","),
797
865
  ipDeny: options.ipDeny || fileConfig.ipAccess?.denyList?.join(","),
866
+ dymoApiKey: options.dymoApiKey || fileConfig.dymo?.apiKey,
867
+ dymoBlockBots: options.dymoBlockBots ?? fileConfig.dymo?.blockBots ?? true,
868
+ dymoBlockProxies: options.dymoBlockProxies ?? fileConfig.dymo?.blockProxies ?? false,
869
+ dymoBlockHosting: options.dymoBlockHosting ?? fileConfig.dymo?.blockHosting ?? false,
870
+ dymoCache: options.dymoCache ?? fileConfig.dymo?.cacheResults ?? true,
871
+ dymoCacheTtl: options.dymoCacheTtl ? parseInt(options.dymoCacheTtl) : (fileConfig.dymo?.cacheTTL ?? 300),
798
872
  detach: options.detach,
799
873
  };
800
874
  // Detached mode - run in background
@@ -845,6 +919,14 @@ program
845
919
  args.push("--ip-allow", mergedOptions.ipAllow);
846
920
  if (mergedOptions.ipDeny)
847
921
  args.push("--ip-deny", mergedOptions.ipDeny);
922
+ if (mergedOptions.dymoApiKey)
923
+ args.push("--dymo-api-key", mergedOptions.dymoApiKey);
924
+ if (mergedOptions.dymoBlockBots === false)
925
+ args.push("--no-dymo-block-bots");
926
+ if (mergedOptions.dymoBlockProxies)
927
+ args.push("--dymo-block-proxies");
928
+ if (mergedOptions.dymoBlockHosting)
929
+ args.push("--dymo-block-hosting");
848
930
  const out = fsAsync.openSync(logFile, "a");
849
931
  const err = fsAsync.openSync(logFile, "a");
850
932
  const child = spawn(process.execPath, [process.argv[1], ...args], {
@@ -900,6 +982,15 @@ program
900
982
  allowList: mergedOptions.ipAllow ? mergedOptions.ipAllow.split(",").map((ip) => ip.trim()) : undefined,
901
983
  denyList: mergedOptions.ipDeny ? mergedOptions.ipDeny.split(",").map((ip) => ip.trim()) : undefined,
902
984
  } : undefined;
985
+ // Build Dymo API config (optional fraud detection)
986
+ const dymoConfig = mergedOptions.dymoApiKey ? {
987
+ apiKey: mergedOptions.dymoApiKey,
988
+ blockBots: mergedOptions.dymoBlockBots ?? true,
989
+ blockProxies: mergedOptions.dymoBlockProxies ?? false,
990
+ blockHosting: mergedOptions.dymoBlockHosting ?? false,
991
+ cacheResults: mergedOptions.dymoCache ?? true,
992
+ cacheTTL: mergedOptions.dymoCacheTtl ?? 300,
993
+ } : undefined;
903
994
  const server = new TunnelServer({
904
995
  port: parseInt(mergedOptions.port),
905
996
  publicPort: mergedOptions.publicPort ? parseInt(mergedOptions.publicPort) : undefined,
@@ -914,6 +1005,7 @@ program
914
1005
  ? { required: true, tokens: mergedOptions.authTokens.split(",") }
915
1006
  : undefined,
916
1007
  ipAccess: ipAccessConfig,
1008
+ dymo: dymoConfig,
917
1009
  https: httpsConfig,
918
1010
  selfSignedHttps: selfSignedHttpsConfig,
919
1011
  autoHttps: autoHttpsConfig,
@@ -1583,7 +1675,7 @@ program
1583
1675
  console.log(chalk_1.default.cyan(`Connecting to ${remote}...\n`));
1584
1676
  console.log(chalk_1.default.cyan(`Starting ${tunnelsToStart.length} tunnel(s)...\n`));
1585
1677
  try {
1586
- await startTunnelsFromConfig(tunnelsToStart, serverUrl, config.server?.token, true);
1678
+ await startTunnelsFromConfig(tunnelsToStart, serverUrl, config.server?.token, true, config);
1587
1679
  console.log(chalk_1.default.gray("\nPress Ctrl+C to stop"));
1588
1680
  // Keep running
1589
1681
  await new Promise(() => { });
@@ -1611,6 +1703,10 @@ program
1611
1703
  max: tcpMax,
1612
1704
  },
1613
1705
  selfSignedHttps: useHttps ? { enabled: true } : undefined,
1706
+ // In hybrid mode, auth is not needed for localhost. For remote clients, still require token.
1707
+ auth: config.server?.token && !isHybridMode ? { required: true, tokens: [config.server.token] } : undefined,
1708
+ dymo: config.server?.dymo,
1709
+ ipAccess: config.server?.ipAccess,
1614
1710
  });
1615
1711
  try {
1616
1712
  await server.start();
@@ -1631,7 +1727,7 @@ program
1631
1727
  const serverUrl = `${wsProtocol}://localhost:${port}/_tunnel`;
1632
1728
  // Small delay to ensure server is fully ready
1633
1729
  await new Promise(resolve => setTimeout(resolve, 500));
1634
- await startTunnelsFromConfig(tunnelsToStart, serverUrl, config.server?.token, true);
1730
+ await startTunnelsFromConfig(tunnelsToStart, serverUrl, config.server?.token, true, config);
1635
1731
  }
1636
1732
  else if (isServerMode) {
1637
1733
  console.log(chalk_1.default.gray("\nServer ready. Waiting for connections..."));
@@ -2267,79 +2363,175 @@ async function runTunnelInBackgroundFromConfig(name, protocol, port, options) {
2267
2363
  console.log(chalk_1.default.green(` ✓ ${name}: started (PID: ${child.pid})`));
2268
2364
  }
2269
2365
  ;
2270
- async function startTunnelsFromConfig(tunnels, serverUrl, token, insecure) {
2271
- const spinner = (0, ora_1.default)("Connecting to server...").start();
2272
- const client = new TunnelClient_1.TunnelClient({
2273
- serverUrl,
2274
- token,
2275
- reconnect: true,
2276
- silent: true,
2277
- rejectUnauthorized: !insecure,
2278
- });
2279
- try {
2280
- await client.connect();
2281
- spinner.succeed("Connected to server");
2282
- const activeTunnels = [];
2283
- for (const tunnel of tunnels) {
2284
- const tunnelSpinner = (0, ora_1.default)(`Creating tunnel: ${tunnel.name}...`).start();
2285
- try {
2286
- const { tunnelId, publicUrl } = await client.createTunnel({
2287
- protocol: tunnel.protocol,
2288
- localHost: tunnel.host || "localhost",
2289
- localPort: tunnel.port,
2290
- subdomain: tunnel.subdomain,
2291
- remotePort: tunnel.remotePort,
2292
- });
2293
- activeTunnels.push({ name: tunnel.name, tunnelId, publicUrl });
2294
- tunnelSpinner.succeed(`${tunnel.name}: ${publicUrl}`);
2295
- }
2296
- catch (err) {
2297
- tunnelSpinner.fail(`${tunnel.name}: ${err.message}`);
2298
- }
2366
+ async function startTunnelsFromConfig(tunnels, serverUrl, token, insecure, globalConfig) {
2367
+ // Determine global provider
2368
+ const globalProvider = globalConfig?.provider || "opentunnel";
2369
+ // Group tunnels by provider
2370
+ const tunnelsByProvider = new Map();
2371
+ for (const tunnel of tunnels) {
2372
+ const provider = tunnel.provider || globalProvider;
2373
+ if (!tunnelsByProvider.has(provider)) {
2374
+ tunnelsByProvider.set(provider, []);
2299
2375
  }
2300
- if (activeTunnels.length === 0) {
2301
- console.log(chalk_1.default.red("\nNo tunnels created"));
2302
- process.exit(1);
2376
+ tunnelsByProvider.get(provider).push(tunnel);
2377
+ }
2378
+ const activeTunnels = [];
2379
+ const clients = [];
2380
+ // Process OpenTunnel tunnels
2381
+ const opentunnelTunnels = tunnelsByProvider.get("opentunnel") || [];
2382
+ if (opentunnelTunnels.length > 0) {
2383
+ const spinner = (0, ora_1.default)("Connecting to OpenTunnel server...").start();
2384
+ const client = new TunnelClient_1.TunnelClient({
2385
+ serverUrl,
2386
+ token,
2387
+ reconnect: true,
2388
+ silent: true,
2389
+ rejectUnauthorized: !insecure,
2390
+ });
2391
+ try {
2392
+ await client.connect();
2393
+ spinner.succeed("Connected to OpenTunnel server");
2394
+ clients.push({ provider: "opentunnel", client });
2395
+ for (const tunnel of opentunnelTunnels) {
2396
+ const tunnelSpinner = (0, ora_1.default)(`Creating tunnel: ${tunnel.name}...`).start();
2397
+ try {
2398
+ const { tunnelId, publicUrl } = await client.createTunnel({
2399
+ protocol: tunnel.protocol,
2400
+ localHost: tunnel.host || "localhost",
2401
+ localPort: tunnel.port,
2402
+ subdomain: tunnel.subdomain,
2403
+ remotePort: tunnel.remotePort,
2404
+ });
2405
+ activeTunnels.push({ name: tunnel.name, tunnelId, publicUrl, provider: "opentunnel", client });
2406
+ tunnelSpinner.succeed(`${tunnel.name}: ${publicUrl}`);
2407
+ }
2408
+ catch (err) {
2409
+ tunnelSpinner.fail(`${tunnel.name}: ${err.message}`);
2410
+ }
2411
+ }
2412
+ // Handle reconnection events
2413
+ client.on("disconnected", () => {
2414
+ console.log(chalk_1.default.yellow("\n OpenTunnel disconnected, reconnecting..."));
2415
+ });
2416
+ client.on("connected", () => {
2417
+ console.log(chalk_1.default.green(" OpenTunnel reconnected!"));
2418
+ });
2303
2419
  }
2304
- console.log(chalk_1.default.cyan("\n─────────────────────────────────────────"));
2305
- console.log(chalk_1.default.green(` ${activeTunnels.length} tunnel(s) active`));
2306
- console.log(chalk_1.default.cyan("─────────────────────────────────────────\n"));
2307
- for (const t of activeTunnels) {
2308
- console.log(` ${chalk_1.default.white(t.name.padEnd(15))} ${chalk_1.default.green(t.publicUrl)}`);
2420
+ catch (err) {
2421
+ spinner.fail(`OpenTunnel connection failed: ${err.message}`);
2309
2422
  }
2310
- console.log(chalk_1.default.gray("\n Press Ctrl+C to stop all tunnels\n"));
2311
- // Keep alive with uptime counter
2312
- const startTime = Date.now();
2313
- const statsInterval = setInterval(() => {
2314
- const uptime = (0, utils_1.formatDuration)(Date.now() - startTime);
2315
- process.stdout.write(`\r ${chalk_1.default.gray(`Uptime: ${uptime}`)}`);
2316
- }, 1000);
2317
- // Handle exit
2318
- const cleanup = async () => {
2319
- clearInterval(statsInterval);
2320
- console.log("\n");
2321
- const closeSpinner = (0, ora_1.default)("Closing tunnels...").start();
2322
- for (const t of activeTunnels) {
2323
- await client.closeTunnel(t.tunnelId);
2324
- }
2325
- await client.disconnect();
2326
- closeSpinner.succeed("All tunnels closed");
2327
- process.exit(0);
2328
- };
2329
- process.on("SIGINT", cleanup);
2330
- process.on("SIGTERM", cleanup);
2331
- // Handle reconnection
2332
- client.on("disconnected", () => {
2333
- console.log(chalk_1.default.yellow("\n Disconnected, reconnecting..."));
2423
+ }
2424
+ // Process ngrok tunnels (one at a time, ngrok limitation)
2425
+ const ngrokTunnels = tunnelsByProvider.get("ngrok") || [];
2426
+ for (const tunnel of ngrokTunnels) {
2427
+ const spinner = (0, ora_1.default)(`Creating ngrok tunnel: ${tunnel.name}...`).start();
2428
+ const ngrokToken = tunnel.ngrokToken || globalConfig?.ngrok?.token;
2429
+ const ngrokRegion = tunnel.ngrokRegion || globalConfig?.ngrok?.region || "us";
2430
+ // Merge IP access config: tunnel-specific > global security > none
2431
+ const { IpFilter } = await Promise.resolve().then(() => __importStar(require("../shared/ip-filter")));
2432
+ const ipAccess = IpFilter.mergeConfigs(globalConfig?.security?.ipAccess, tunnel.ipAccess);
2433
+ const client = new NgrokClient_1.NgrokClient({
2434
+ authtoken: ngrokToken,
2435
+ region: ngrokRegion,
2436
+ ipAccess,
2334
2437
  });
2335
- client.on("connected", () => {
2336
- console.log(chalk_1.default.green(" Reconnected!"));
2438
+ try {
2439
+ await client.connect();
2440
+ const { tunnelId, publicUrl } = await client.createTunnel({
2441
+ protocol: tunnel.protocol,
2442
+ localHost: tunnel.host || "localhost",
2443
+ localPort: tunnel.port,
2444
+ subdomain: tunnel.subdomain,
2445
+ remotePort: tunnel.remotePort,
2446
+ });
2447
+ activeTunnels.push({ name: tunnel.name, tunnelId, publicUrl, provider: "ngrok", client });
2448
+ clients.push({ provider: "ngrok", client });
2449
+ spinner.succeed(`${tunnel.name}: ${publicUrl} (ngrok)`);
2450
+ }
2451
+ catch (err) {
2452
+ spinner.fail(`${tunnel.name}: ${err.message}`);
2453
+ console.log(chalk_1.default.yellow(" Make sure ngrok is installed: https://ngrok.com/download"));
2454
+ }
2455
+ }
2456
+ // Process Cloudflare tunnels (one at a time)
2457
+ const cloudflareTunnels = tunnelsByProvider.get("cloudflare") || [];
2458
+ for (const tunnel of cloudflareTunnels) {
2459
+ const spinner = (0, ora_1.default)(`Creating Cloudflare tunnel: ${tunnel.name}...`).start();
2460
+ const cfHostname = tunnel.cfHostname || globalConfig?.cloudflare?.hostname;
2461
+ // For Cloudflare: subdomain = tunnel name
2462
+ const cfTunnelName = tunnel.subdomain || globalConfig?.cloudflare?.tunnelName;
2463
+ // Merge IP access config: tunnel-specific > global security > none
2464
+ const { IpFilter } = await Promise.resolve().then(() => __importStar(require("../shared/ip-filter")));
2465
+ const ipAccess = IpFilter.mergeConfigs(globalConfig?.security?.ipAccess, tunnel.ipAccess);
2466
+ const client = new CloudflareTunnelClient_1.CloudflareTunnelClient({
2467
+ hostname: cfHostname,
2468
+ tunnelName: cfTunnelName,
2469
+ protocol: tunnel.protocol === "https" ? "https" : "http",
2470
+ ipAccess,
2337
2471
  });
2472
+ try {
2473
+ await client.connect();
2474
+ const { tunnelId, publicUrl } = await client.createTunnel({
2475
+ protocol: tunnel.protocol,
2476
+ localHost: tunnel.host || "localhost",
2477
+ localPort: tunnel.port,
2478
+ });
2479
+ activeTunnels.push({ name: tunnel.name, tunnelId, publicUrl, provider: "cloudflare", client });
2480
+ clients.push({ provider: "cloudflare", client });
2481
+ const tunnelMode = cfTunnelName ? ` [${cfTunnelName}]` : "";
2482
+ spinner.succeed(`${tunnel.name}: ${publicUrl}${tunnelMode} (cloudflare)`);
2483
+ }
2484
+ catch (err) {
2485
+ spinner.fail(`${tunnel.name}: ${err.message}`);
2486
+ console.log(chalk_1.default.yellow(" cloudflared installs automatically. Check internet connection or try: npm rebuild cloudflared"));
2487
+ }
2338
2488
  }
2339
- catch (err) {
2340
- spinner.fail(`Failed: ${err.message}`);
2489
+ if (activeTunnels.length === 0) {
2490
+ console.log(chalk_1.default.red("\nNo tunnels created"));
2341
2491
  process.exit(1);
2342
2492
  }
2493
+ console.log(chalk_1.default.cyan("\n─────────────────────────────────────────"));
2494
+ console.log(chalk_1.default.green(` ${activeTunnels.length} tunnel(s) active`));
2495
+ console.log(chalk_1.default.cyan("─────────────────────────────────────────\n"));
2496
+ for (const t of activeTunnels) {
2497
+ const providerTag = t.provider !== "opentunnel" ? chalk_1.default.gray(` (${t.provider})`) : "";
2498
+ console.log(` ${chalk_1.default.white(t.name.padEnd(15))} ${chalk_1.default.green(t.publicUrl)}${providerTag}`);
2499
+ }
2500
+ console.log(chalk_1.default.gray("\n Press Ctrl+C to stop all tunnels\n"));
2501
+ // Keep alive with uptime counter
2502
+ const startTime = Date.now();
2503
+ const statsInterval = setInterval(() => {
2504
+ const uptime = (0, utils_1.formatDuration)(Date.now() - startTime);
2505
+ process.stdout.write(`\r ${chalk_1.default.gray(`Uptime: ${uptime}`)}`);
2506
+ }, 1000);
2507
+ // Handle exit
2508
+ const cleanup = async () => {
2509
+ clearInterval(statsInterval);
2510
+ console.log("\n");
2511
+ const closeSpinner = (0, ora_1.default)("Closing tunnels...").start();
2512
+ for (const t of activeTunnels) {
2513
+ try {
2514
+ await t.client.closeTunnel(t.tunnelId);
2515
+ }
2516
+ catch {
2517
+ // Ignore errors during cleanup
2518
+ }
2519
+ }
2520
+ for (const { client } of clients) {
2521
+ try {
2522
+ await client.disconnect();
2523
+ }
2524
+ catch {
2525
+ // Ignore errors during cleanup
2526
+ }
2527
+ }
2528
+ closeSpinner.succeed("All tunnels closed");
2529
+ process.exit(0);
2530
+ };
2531
+ process.on("SIGINT", cleanup);
2532
+ process.on("SIGTERM", cleanup);
2533
+ // Keep process alive
2534
+ await new Promise(() => { });
2343
2535
  }
2344
2536
  ;
2345
2537
  async function runTunnelInBackground(command, port, options) {
@@ -2531,6 +2723,61 @@ async function createNgrokTunnel(options) {
2531
2723
  }
2532
2724
  }
2533
2725
  ;
2726
+ async function createCloudflareTunnel(options) {
2727
+ const tunnelType = options.tunnelName ? `named tunnel '${options.tunnelName}'` : "quick tunnel";
2728
+ const spinner = (0, ora_1.default)(`Starting Cloudflare ${tunnelType}...`).start();
2729
+ const client = new CloudflareTunnelClient_1.CloudflareTunnelClient({
2730
+ hostname: options.hostname,
2731
+ tunnelName: options.tunnelName,
2732
+ noTlsVerify: options.noTlsVerify,
2733
+ protocol: options.protocol === "https" ? "https" : "http",
2734
+ });
2735
+ try {
2736
+ await client.connect();
2737
+ spinner.text = "Creating tunnel...";
2738
+ const { tunnelId, publicUrl } = await client.createTunnel({
2739
+ protocol: options.protocol,
2740
+ localHost: options.localHost,
2741
+ localPort: options.localPort,
2742
+ });
2743
+ spinner.succeed("Tunnel established!");
2744
+ const providerInfo = options.tunnelName
2745
+ ? `Cloudflare [${options.tunnelName}]`
2746
+ : "Cloudflare";
2747
+ printTunnelInfo({
2748
+ status: "Online",
2749
+ protocol: options.protocol,
2750
+ localHost: options.localHost,
2751
+ localPort: options.localPort,
2752
+ publicUrl,
2753
+ provider: providerInfo,
2754
+ });
2755
+ // Keep alive
2756
+ const startTime = Date.now();
2757
+ const statsInterval = setInterval(() => {
2758
+ const uptime = (0, utils_1.formatDuration)(Date.now() - startTime);
2759
+ process.stdout.write(`\r ${chalk_1.default.gray(`Uptime: ${uptime}`)}`);
2760
+ }, 1000);
2761
+ // Handle exit
2762
+ const cleanup = async () => {
2763
+ clearInterval(statsInterval);
2764
+ console.log("\n");
2765
+ spinner.start("Closing tunnel...");
2766
+ await client.closeTunnel(tunnelId);
2767
+ await client.disconnect();
2768
+ spinner.succeed("Tunnel closed");
2769
+ process.exit(0);
2770
+ };
2771
+ process.on("SIGINT", cleanup);
2772
+ process.on("SIGTERM", cleanup);
2773
+ }
2774
+ catch (err) {
2775
+ spinner.fail(`Failed: ${err.message}`);
2776
+ console.log(chalk_1.default.yellow("\ncloudflared installs automatically. Check your internet connection or try: npm rebuild cloudflared"));
2777
+ process.exit(1);
2778
+ }
2779
+ }
2780
+ ;
2534
2781
  function printTunnelInfo(info) {
2535
2782
  console.log("");
2536
2783
  console.log(chalk_1.default.cyan(` OpenTunnel ${chalk_1.default.gray(`(via ${info.provider})`)}`));
@@ -2545,5 +2792,386 @@ function printTunnelInfo(info) {
2545
2792
  console.log("");
2546
2793
  }
2547
2794
  ;
2795
+ // ============================================================================
2796
+ // Unified Provider Commands
2797
+ // ============================================================================
2798
+ // Login command - unified for all providers
2799
+ program
2800
+ .command("login <provider>")
2801
+ .description("Authenticate with a provider (cloudflare, ngrok)")
2802
+ .option("-t, --token <token>", "Authentication token (required for ngrok)")
2803
+ .action(async (provider, options) => {
2804
+ const credManager = new credentials_1.CredentialsManager();
2805
+ const normalizedProvider = provider.toLowerCase();
2806
+ if (normalizedProvider === "cloudflare" || normalizedProvider === "cf") {
2807
+ const spinner = (0, ora_1.default)("Running cloudflared login...").start();
2808
+ try {
2809
+ const binPath = await CloudflareTunnelClient_1.CloudflareTunnelClient.ensureInstalled();
2810
+ const { spawn } = await Promise.resolve().then(() => __importStar(require("child_process")));
2811
+ spinner.stop();
2812
+ console.log(chalk_1.default.cyan("\n Opening browser for Cloudflare authentication...\n"));
2813
+ const proc = spawn(binPath, ["login"], {
2814
+ stdio: "inherit",
2815
+ shell: process.platform === "win32",
2816
+ });
2817
+ await new Promise((resolve, reject) => {
2818
+ proc.on("close", (code) => {
2819
+ if (code === 0) {
2820
+ const os = require("os");
2821
+ const defaultCertPath = path.join(os.homedir(), ".cloudflared", "cert.pem");
2822
+ credManager.setCloudflare({ certPath: defaultCertPath });
2823
+ console.log(chalk_1.default.green("\n Cloudflare credentials saved"));
2824
+ console.log(chalk_1.default.gray(` Cert path: ${defaultCertPath}`));
2825
+ resolve();
2826
+ }
2827
+ else {
2828
+ reject(new Error(`cloudflared login exited with code ${code}`));
2829
+ }
2830
+ });
2831
+ proc.on("error", reject);
2832
+ });
2833
+ }
2834
+ catch (err) {
2835
+ spinner.fail(`Login failed: ${err.message}`);
2836
+ process.exit(1);
2837
+ }
2838
+ }
2839
+ else if (normalizedProvider === "ngrok") {
2840
+ if (!options.token) {
2841
+ console.log(chalk_1.default.red("Error: ngrok requires --token <token>"));
2842
+ console.log(chalk_1.default.gray("\nGet your token from: https://dashboard.ngrok.com/get-started/your-authtoken"));
2843
+ console.log(chalk_1.default.cyan("\nUsage: opentunnel login ngrok --token <your-token>"));
2844
+ process.exit(1);
2845
+ }
2846
+ credManager.setNgrok({ token: options.token });
2847
+ console.log(chalk_1.default.green("\n ngrok credentials saved"));
2848
+ console.log(chalk_1.default.gray(` Token: ${options.token.slice(0, 8)}...`));
2849
+ console.log(chalk_1.default.gray(` Stored at: ${credentials_1.CredentialsManager.getCredentialsFile()}\n`));
2850
+ }
2851
+ else {
2852
+ console.log(chalk_1.default.red(`Unknown provider: ${provider}`));
2853
+ console.log(chalk_1.default.gray("\nSupported providers: cloudflare (cf), ngrok"));
2854
+ process.exit(1);
2855
+ }
2856
+ });
2857
+ // Logout command - unified for all providers
2858
+ program
2859
+ .command("logout <provider>")
2860
+ .description("Remove stored credentials for a provider")
2861
+ .action(async (provider) => {
2862
+ const credManager = new credentials_1.CredentialsManager();
2863
+ const normalizedProvider = provider.toLowerCase();
2864
+ if (normalizedProvider === "cloudflare" || normalizedProvider === "cf") {
2865
+ const removed = credManager.removeProvider("cloudflare");
2866
+ if (removed) {
2867
+ console.log(chalk_1.default.green("\n Cloudflare credentials removed\n"));
2868
+ }
2869
+ else {
2870
+ console.log(chalk_1.default.yellow("\n No Cloudflare credentials found\n"));
2871
+ }
2872
+ }
2873
+ else if (normalizedProvider === "ngrok") {
2874
+ const removed = credManager.removeProvider("ngrok");
2875
+ if (removed) {
2876
+ console.log(chalk_1.default.green("\n ngrok credentials removed\n"));
2877
+ }
2878
+ else {
2879
+ console.log(chalk_1.default.yellow("\n No ngrok credentials found\n"));
2880
+ }
2881
+ }
2882
+ else {
2883
+ console.log(chalk_1.default.red(`Unknown provider: ${provider}`));
2884
+ console.log(chalk_1.default.gray("\nSupported providers: cloudflare (cf), ngrok"));
2885
+ process.exit(1);
2886
+ }
2887
+ });
2888
+ // Create tunnel command (for named tunnels)
2889
+ program
2890
+ .command("create <name>")
2891
+ .description("Create a named tunnel")
2892
+ .option("--cf, --cloudflare", "Create Cloudflare named tunnel")
2893
+ .option("--provider <provider>", "Provider (cloudflare)")
2894
+ .action(async (name, options) => {
2895
+ const provider = options.provider?.toLowerCase() ||
2896
+ (options.cloudflare || options.cf ? "cloudflare" : null);
2897
+ if (!provider) {
2898
+ console.log(chalk_1.default.red("Error: Provider required"));
2899
+ console.log(chalk_1.default.gray("\nUsage: opentunnel create <name> --cf"));
2900
+ console.log(chalk_1.default.gray(" opentunnel create <name> --provider cloudflare"));
2901
+ process.exit(1);
2902
+ }
2903
+ if (provider === "cloudflare" || provider === "cf") {
2904
+ const spinner = (0, ora_1.default)(`Creating Cloudflare tunnel '${name}'...`).start();
2905
+ try {
2906
+ const binPath = await CloudflareTunnelClient_1.CloudflareTunnelClient.ensureInstalled();
2907
+ const { spawn } = await Promise.resolve().then(() => __importStar(require("child_process")));
2908
+ const proc = spawn(binPath, ["tunnel", "create", name], {
2909
+ stdio: ["ignore", "pipe", "pipe"],
2910
+ shell: process.platform === "win32",
2911
+ });
2912
+ let output = "";
2913
+ let errorOutput = "";
2914
+ proc.stdout?.on("data", (data) => { output += data.toString(); });
2915
+ proc.stderr?.on("data", (data) => { errorOutput += data.toString(); });
2916
+ await new Promise((resolve, reject) => {
2917
+ proc.on("close", (code) => {
2918
+ if (code === 0) {
2919
+ spinner.succeed(`Tunnel '${name}' created`);
2920
+ if (output)
2921
+ console.log(chalk_1.default.gray(output.trim()));
2922
+ resolve();
2923
+ }
2924
+ else {
2925
+ reject(new Error(errorOutput || `Exit code ${code}`));
2926
+ }
2927
+ });
2928
+ proc.on("error", reject);
2929
+ });
2930
+ }
2931
+ catch (err) {
2932
+ spinner.fail(`Failed to create tunnel: ${err.message}`);
2933
+ if (err.message.includes("login")) {
2934
+ console.log(chalk_1.default.yellow("\nRun 'opentunnel login cloudflare' first"));
2935
+ }
2936
+ process.exit(1);
2937
+ }
2938
+ }
2939
+ else {
2940
+ console.log(chalk_1.default.red(`Provider '${provider}' doesn't support named tunnels`));
2941
+ process.exit(1);
2942
+ }
2943
+ });
2944
+ // Delete tunnel command
2945
+ program
2946
+ .command("delete <name>")
2947
+ .description("Delete a named tunnel")
2948
+ .option("--cf, --cloudflare", "Delete Cloudflare named tunnel")
2949
+ .option("--provider <provider>", "Provider (cloudflare)")
2950
+ .option("-f, --force", "Force deletion without confirmation")
2951
+ .action(async (name, options) => {
2952
+ const provider = options.provider?.toLowerCase() ||
2953
+ (options.cloudflare || options.cf ? "cloudflare" : null);
2954
+ if (!provider) {
2955
+ console.log(chalk_1.default.red("Error: Provider required"));
2956
+ console.log(chalk_1.default.gray("\nUsage: opentunnel delete <name> --cf"));
2957
+ process.exit(1);
2958
+ }
2959
+ if (provider === "cloudflare" || provider === "cf") {
2960
+ if (!options.force) {
2961
+ console.log(chalk_1.default.yellow(`\n This will delete tunnel '${name}' permanently.`));
2962
+ console.log(chalk_1.default.gray(" Use --force to skip this confirmation.\n"));
2963
+ const readline = await Promise.resolve().then(() => __importStar(require("readline")));
2964
+ const rl = readline.createInterface({
2965
+ input: process.stdin,
2966
+ output: process.stdout,
2967
+ });
2968
+ const answer = await new Promise((resolve) => {
2969
+ rl.question(" Type the tunnel name to confirm: ", resolve);
2970
+ });
2971
+ rl.close();
2972
+ if (answer !== name) {
2973
+ console.log(chalk_1.default.red("\n Cancelled: name didn't match\n"));
2974
+ return;
2975
+ }
2976
+ }
2977
+ const spinner = (0, ora_1.default)(`Deleting tunnel '${name}'...`).start();
2978
+ try {
2979
+ const binPath = await CloudflareTunnelClient_1.CloudflareTunnelClient.ensureInstalled();
2980
+ const { spawn } = await Promise.resolve().then(() => __importStar(require("child_process")));
2981
+ const proc = spawn(binPath, ["tunnel", "delete", name], {
2982
+ stdio: ["ignore", "pipe", "pipe"],
2983
+ shell: process.platform === "win32",
2984
+ });
2985
+ let errorOutput = "";
2986
+ proc.stderr?.on("data", (data) => { errorOutput += data.toString(); });
2987
+ await new Promise((resolve, reject) => {
2988
+ proc.on("close", (code) => {
2989
+ if (code === 0) {
2990
+ spinner.succeed(`Tunnel '${name}' deleted`);
2991
+ resolve();
2992
+ }
2993
+ else {
2994
+ reject(new Error(errorOutput || `Exit code ${code}`));
2995
+ }
2996
+ });
2997
+ proc.on("error", reject);
2998
+ });
2999
+ }
3000
+ catch (err) {
3001
+ spinner.fail(`Failed to delete tunnel: ${err.message}`);
3002
+ process.exit(1);
3003
+ }
3004
+ }
3005
+ });
3006
+ // Route DNS command (Cloudflare specific)
3007
+ program
3008
+ .command("route <name> <hostname>")
3009
+ .description("Route a hostname to a tunnel (Cloudflare DNS)")
3010
+ .option("--cf, --cloudflare", "Route via Cloudflare")
3011
+ .option("--provider <provider>", "Provider (cloudflare)")
3012
+ .action(async (name, hostname, options) => {
3013
+ const provider = options.provider?.toLowerCase() ||
3014
+ (options.cloudflare || options.cf ? "cloudflare" : "cloudflare"); // Default to cloudflare
3015
+ if (provider === "cloudflare" || provider === "cf") {
3016
+ const spinner = (0, ora_1.default)(`Routing ${hostname} to tunnel '${name}'...`).start();
3017
+ try {
3018
+ const binPath = await CloudflareTunnelClient_1.CloudflareTunnelClient.ensureInstalled();
3019
+ const { spawn } = await Promise.resolve().then(() => __importStar(require("child_process")));
3020
+ const proc = spawn(binPath, ["tunnel", "route", "dns", name, hostname], {
3021
+ stdio: ["ignore", "pipe", "pipe"],
3022
+ shell: process.platform === "win32",
3023
+ });
3024
+ let output = "";
3025
+ let errorOutput = "";
3026
+ proc.stdout?.on("data", (data) => { output += data.toString(); });
3027
+ proc.stderr?.on("data", (data) => { errorOutput += data.toString(); });
3028
+ await new Promise((resolve, reject) => {
3029
+ proc.on("close", (code) => {
3030
+ if (code === 0) {
3031
+ spinner.succeed(`DNS route created: ${hostname} -> ${name}`);
3032
+ if (output)
3033
+ console.log(chalk_1.default.gray(output.trim()));
3034
+ resolve();
3035
+ }
3036
+ else {
3037
+ reject(new Error(errorOutput || `Exit code ${code}`));
3038
+ }
3039
+ });
3040
+ proc.on("error", reject);
3041
+ });
3042
+ }
3043
+ catch (err) {
3044
+ spinner.fail(`Failed to create route: ${err.message}`);
3045
+ process.exit(1);
3046
+ }
3047
+ }
3048
+ });
3049
+ // List tunnels command - extend existing list command
3050
+ program
3051
+ .command("tunnels")
3052
+ .description("List named tunnels")
3053
+ .option("--cf, --cloudflare", "List Cloudflare tunnels")
3054
+ .option("--provider <provider>", "Provider (cloudflare)")
3055
+ .action(async (options) => {
3056
+ const provider = options.provider?.toLowerCase() ||
3057
+ (options.cloudflare || options.cf ? "cloudflare" : null);
3058
+ if (!provider) {
3059
+ console.log(chalk_1.default.red("Error: Provider required"));
3060
+ console.log(chalk_1.default.gray("\nUsage: opentunnel tunnels --cf"));
3061
+ process.exit(1);
3062
+ }
3063
+ if (provider === "cloudflare" || provider === "cf") {
3064
+ const spinner = (0, ora_1.default)("Listing Cloudflare tunnels...").start();
3065
+ try {
3066
+ const binPath = await CloudflareTunnelClient_1.CloudflareTunnelClient.ensureInstalled();
3067
+ const { spawn } = await Promise.resolve().then(() => __importStar(require("child_process")));
3068
+ const proc = spawn(binPath, ["tunnel", "list"], {
3069
+ stdio: ["ignore", "pipe", "pipe"],
3070
+ shell: process.platform === "win32",
3071
+ });
3072
+ let output = "";
3073
+ let errorOutput = "";
3074
+ proc.stdout?.on("data", (data) => { output += data.toString(); });
3075
+ proc.stderr?.on("data", (data) => { errorOutput += data.toString(); });
3076
+ await new Promise((resolve, reject) => {
3077
+ proc.on("close", (code) => {
3078
+ if (code === 0) {
3079
+ spinner.stop();
3080
+ console.log(chalk_1.default.cyan("\n Cloudflare Tunnels"));
3081
+ console.log(chalk_1.default.gray(" " + "─".repeat(60)));
3082
+ if (output.trim()) {
3083
+ console.log(output);
3084
+ }
3085
+ else {
3086
+ console.log(chalk_1.default.yellow(" No tunnels found"));
3087
+ console.log(chalk_1.default.gray("\n Create one with: opentunnel create <name> --cf"));
3088
+ }
3089
+ resolve();
3090
+ }
3091
+ else {
3092
+ reject(new Error(errorOutput || `Exit code ${code}`));
3093
+ }
3094
+ });
3095
+ proc.on("error", reject);
3096
+ });
3097
+ }
3098
+ catch (err) {
3099
+ spinner.fail(`Failed to list tunnels: ${err.message}`);
3100
+ if (err.message.includes("login")) {
3101
+ console.log(chalk_1.default.yellow("\nRun 'opentunnel login cloudflare' first"));
3102
+ }
3103
+ process.exit(1);
3104
+ }
3105
+ }
3106
+ });
3107
+ // ============================================================================
3108
+ // Config Commands
3109
+ // ============================================================================
3110
+ const configCommand = program
3111
+ .command("config")
3112
+ .description("Manage OpenTunnel configuration");
3113
+ configCommand
3114
+ .command("set <key> <value>")
3115
+ .description("Set a configuration value (e.g., ngrok.token, cloudflare.accountId)")
3116
+ .action(async (key, value) => {
3117
+ const credManager = new credentials_1.CredentialsManager();
3118
+ const validKeys = [
3119
+ "ngrok.token",
3120
+ "cloudflare.accountId",
3121
+ "cloudflare.tunnelToken",
3122
+ "cloudflare.certPath",
3123
+ ];
3124
+ if (!validKeys.includes(key)) {
3125
+ console.log(chalk_1.default.red(`Invalid key: ${key}`));
3126
+ console.log(chalk_1.default.gray("\nValid keys:"));
3127
+ for (const k of validKeys) {
3128
+ console.log(chalk_1.default.cyan(` ${k}`));
3129
+ }
3130
+ process.exit(1);
3131
+ }
3132
+ credManager.set(key, value);
3133
+ console.log(chalk_1.default.green(`\n ${key} = ${value.slice(0, 20)}${value.length > 20 ? "..." : ""}\n`));
3134
+ });
3135
+ configCommand
3136
+ .command("get <key>")
3137
+ .description("Get a configuration value")
3138
+ .action(async (key) => {
3139
+ const credManager = new credentials_1.CredentialsManager();
3140
+ const value = credManager.get(key);
3141
+ if (value) {
3142
+ // Mask sensitive values
3143
+ const masked = key.includes("token") || key.includes("Token")
3144
+ ? value.slice(0, 8) + "..." + value.slice(-4)
3145
+ : value;
3146
+ console.log(chalk_1.default.cyan(`\n ${key} = ${masked}\n`));
3147
+ }
3148
+ else {
3149
+ console.log(chalk_1.default.yellow(`\n ${key} is not set\n`));
3150
+ }
3151
+ });
3152
+ configCommand
3153
+ .command("list")
3154
+ .description("List all stored configuration")
3155
+ .action(async () => {
3156
+ const credManager = new credentials_1.CredentialsManager();
3157
+ const keys = credManager.listKeys();
3158
+ console.log(chalk_1.default.cyan("\n Stored Configuration"));
3159
+ console.log(chalk_1.default.gray(" " + "─".repeat(40)));
3160
+ if (keys.length === 0) {
3161
+ console.log(chalk_1.default.yellow(" No configuration stored"));
3162
+ console.log(chalk_1.default.gray("\n Use 'opentunnel login' or 'opentunnel config set' to add credentials"));
3163
+ }
3164
+ else {
3165
+ for (const key of keys) {
3166
+ const value = credManager.get(key);
3167
+ const masked = (key.includes("token") || key.includes("Token")) && value
3168
+ ? value.slice(0, 8) + "..." + value.slice(-4)
3169
+ : value;
3170
+ console.log(` ${chalk_1.default.white(key.padEnd(25))} ${chalk_1.default.gray(masked || "")}`);
3171
+ }
3172
+ }
3173
+ console.log(chalk_1.default.gray(" " + "─".repeat(40)));
3174
+ console.log(chalk_1.default.gray(`\n Config file: ${credentials_1.CredentialsManager.getCredentialsFile()}\n`));
3175
+ });
2548
3176
  program.parse();
2549
3177
  //# sourceMappingURL=index.js.map