opentunnel-cli 1.0.23 → 1.0.26

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 +43 -2
  2. package/dist/cli/index.js +722 -85
  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 +1 -3
  37. package/dist/server/TunnelServer.d.ts.map +1 -1
  38. package/dist/server/TunnelServer.js +7 -58
  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 +34 -0
  49. package/dist/shared/types.d.ts.map +1 -1
  50. package/package.json +6 -3
package/dist/cli/index.js CHANGED
@@ -42,10 +42,13 @@ 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"));
48
50
  const path = __importStar(require("path"));
51
+ const net = __importStar(require("net"));
49
52
  const CONFIG_FILE = "opentunnel.yml";
50
53
  function getRegistryPath() {
51
54
  const os = require("os");
@@ -53,6 +56,20 @@ function getRegistryPath() {
53
56
  const registryDir = path.join(os.homedir(), ".opentunnel");
54
57
  return path.join(registryDir, "registry.json");
55
58
  }
59
+ function checkPortInUse(port) {
60
+ return new Promise((resolve) => {
61
+ const server = net.createServer();
62
+ server.listen(port, () => {
63
+ server.once('close', () => {
64
+ resolve(false);
65
+ });
66
+ server.close();
67
+ });
68
+ server.on('error', () => {
69
+ resolve(true);
70
+ });
71
+ });
72
+ }
56
73
  function getLogsDir() {
57
74
  const os = require("os");
58
75
  const path = require("path");
@@ -220,8 +237,8 @@ const program = new commander_1.Command();
220
237
  program
221
238
  .name("opentunnel")
222
239
  .alias("ot")
223
- .description("Expose local ports to the internet via custom domains or ngrok")
224
- .version("1.0.23");
240
+ .description("Expose local ports to the internet via custom domains, ngrok, or Cloudflare Tunnel")
241
+ .version("1.0.26");
225
242
  // Helper function to build WebSocket URL from domain
226
243
  // User only provides base domain (e.g., fjrg2007.com), system handles the rest
227
244
  // Note: --insecure flag only affects certificate verification, not the protocol
@@ -233,6 +250,13 @@ function buildServerUrl(server, basePath) {
233
250
  hostname = hostname.replace(/\/_tunnel.*$/, "");
234
251
  // Remove trailing slash
235
252
  hostname = hostname.replace(/\/$/, "");
253
+ // Extract port if present (support both hostname:port and format)
254
+ let port = "";
255
+ const portMatch = hostname.match(/:(\d+)$/);
256
+ if (portMatch) {
257
+ port = `:${portMatch[1]}`;
258
+ hostname = hostname.slice(0, -portMatch[1].length - 1);
259
+ }
236
260
  // Build the full hostname with basePath if provided and not empty
237
261
  // If basePath is "op" (default), connect to op.domain.com
238
262
  // If basePath is empty or not provided, connect directly to domain.com
@@ -240,7 +264,7 @@ function buildServerUrl(server, basePath) {
240
264
  const fullHostname = effectiveBasePath ? `${effectiveBasePath}.${hostname}` : hostname;
241
265
  // Always use wss:// for remote servers (--insecure only skips cert verification)
242
266
  return {
243
- url: `wss://${fullHostname}/_tunnel`,
267
+ url: `wss://${fullHostname}${port}/_tunnel`,
244
268
  displayName: hostname,
245
269
  };
246
270
  }
@@ -618,8 +642,28 @@ program
618
642
  .option("--insecure", "Skip SSL verification (for self-signed certs)")
619
643
  .option("--ngrok", "Use ngrok instead of OpenTunnel server")
620
644
  .option("--region <region>", "Ngrok region (us, eu, ap, au, sa, jp, in)", "us")
645
+ .option("--cloudflare, --cf", "Use Cloudflare Tunnel instead of OpenTunnel server")
646
+ .option("--cf-hostname <hostname>", "Custom hostname for Cloudflare Tunnel")
647
+ .option("--provider <provider>", "Tunnel provider (opentunnel, ngrok, cloudflare)")
621
648
  .action(async (port, options) => {
622
- if (options.ngrok || options.domain === "ngrok") {
649
+ // Determine provider
650
+ const provider = options.provider?.toLowerCase() ||
651
+ (options.cloudflare || options.cf ? "cloudflare" : null) ||
652
+ (options.ngrok ? "ngrok" : null);
653
+ // Cloudflare Tunnel
654
+ if (provider === "cloudflare" || provider === "cf") {
655
+ await createCloudflareTunnel({
656
+ protocol: options.https ? "https" : "http",
657
+ localHost: options.host,
658
+ localPort: parseInt(port),
659
+ hostname: options.cfHostname,
660
+ tunnelName: options.subdomain, // -n works as tunnel name for CF
661
+ noTlsVerify: options.insecure,
662
+ });
663
+ return;
664
+ }
665
+ // ngrok
666
+ if (provider === "ngrok") {
623
667
  await createNgrokTunnel({
624
668
  protocol: options.https ? "https" : "http",
625
669
  localHost: options.host,
@@ -630,8 +674,8 @@ program
630
674
  });
631
675
  return;
632
676
  }
633
- // If remote server domain provided, just connect to it
634
- if (options.domain) {
677
+ // If remote server domain provided, connect to OpenTunnel server
678
+ if (options.domain && options.domain !== "cloudflare" && options.domain !== "ngrok") {
635
679
  const { url: serverUrl } = buildServerUrl(options.domain, options.basePath);
636
680
  await createTunnel({
637
681
  protocol: options.https ? "https" : "http",
@@ -649,6 +693,8 @@ program
649
693
  console.log(chalk_1.default.gray("\nExamples:"));
650
694
  console.log(chalk_1.default.cyan(" opentunnel http 3000 -s example.com"));
651
695
  console.log(chalk_1.default.cyan(" opentunnel http 3000 --domain example.com -n myapp"));
696
+ console.log(chalk_1.default.cyan(" opentunnel http 3000 --cloudflare"));
697
+ console.log(chalk_1.default.cyan(" opentunnel http 3000 --ngrok"));
652
698
  process.exit(1);
653
699
  });
654
700
  // TCP tunnel command
@@ -664,8 +710,33 @@ program
664
710
  .option("--insecure", "Skip SSL verification (for self-signed certs)")
665
711
  .option("--ngrok", "Use ngrok instead of OpenTunnel server")
666
712
  .option("--region <region>", "Ngrok region (us, eu, ap, au, sa, jp, in)", "us")
713
+ .option("--cloudflare, --cf", "Use Cloudflare Tunnel (TCP requires named tunnel)")
714
+ .option("--cf-hostname <hostname>", "Custom hostname for Cloudflare Tunnel")
715
+ .option("--provider <provider>", "Tunnel provider (opentunnel, ngrok, cloudflare)")
667
716
  .action(async (port, options) => {
668
- if (options.ngrok || options.domain === "ngrok") {
717
+ // Determine provider
718
+ const provider = options.provider?.toLowerCase() ||
719
+ (options.cloudflare || options.cf ? "cloudflare" : null) ||
720
+ (options.ngrok ? "ngrok" : null);
721
+ // Cloudflare Tunnel - Note: TCP requires named tunnel
722
+ if (provider === "cloudflare" || provider === "cf") {
723
+ if (!options.subdomain) {
724
+ console.log(chalk_1.default.yellow("Note: Cloudflare TCP tunnels require a named tunnel."));
725
+ console.log(chalk_1.default.gray("Use -n <tunnel-name> to specify a named tunnel."));
726
+ console.log(chalk_1.default.gray("\nExample: opentunnel tcp 5432 --cf -n my-tunnel"));
727
+ }
728
+ await createCloudflareTunnel({
729
+ protocol: "tcp",
730
+ localHost: options.host,
731
+ localPort: parseInt(port),
732
+ hostname: options.cfHostname,
733
+ tunnelName: options.subdomain,
734
+ noTlsVerify: options.insecure,
735
+ });
736
+ return;
737
+ }
738
+ // ngrok
739
+ if (provider === "ngrok") {
669
740
  await createNgrokTunnel({
670
741
  protocol: "tcp",
671
742
  localHost: options.host,
@@ -676,8 +747,8 @@ program
676
747
  });
677
748
  return;
678
749
  }
679
- // If remote server domain provided, just connect to it
680
- if (options.domain) {
750
+ // If remote server domain provided, connect to OpenTunnel server
751
+ if (options.domain && options.domain !== "cloudflare" && options.domain !== "ngrok") {
681
752
  const { url: serverUrl } = buildServerUrl(options.domain, options.basePath);
682
753
  await createTunnel({
683
754
  protocol: "tcp",
@@ -695,6 +766,7 @@ program
695
766
  console.log(chalk_1.default.gray("\nExamples:"));
696
767
  console.log(chalk_1.default.cyan(" opentunnel tcp 5432 -s example.com"));
697
768
  console.log(chalk_1.default.cyan(" opentunnel tcp 5432 --domain example.com -r 15432"));
769
+ console.log(chalk_1.default.cyan(" opentunnel tcp 5432 --ngrok"));
698
770
  process.exit(1);
699
771
  });
700
772
  // Quick expose command
@@ -709,6 +781,7 @@ program
709
781
  .option("--domain <domain>", "Server domain (e.g., domain.com)")
710
782
  .option("--insecure", "Skip SSL certificate verification (for self-signed certs)")
711
783
  .option("--ngrok", "Use ngrok instead of OpenTunnel server")
784
+ .option("--cloudflare, --cf", "Use Cloudflare Tunnel instead of OpenTunnel server")
712
785
  .action(async (port, options) => {
713
786
  const serverUrl = options.server || (options.domain
714
787
  ? `wss://${options.domain}/_tunnel`
@@ -717,26 +790,37 @@ program
717
790
  await runTunnelInBackground("expose", port, { ...options, server: serverUrl });
718
791
  return;
719
792
  }
720
- if (options.ngrok || options.server === "ngrok") {
721
- await createNgrokTunnel({
793
+ // Cloudflare Tunnel
794
+ if (options.cloudflare || options.cf || options.server === "cloudflare") {
795
+ await createCloudflareTunnel({
722
796
  protocol: options.protocol,
723
797
  localHost: "localhost",
724
798
  localPort: parseInt(port),
725
- subdomain: options.subdomain,
726
- authtoken: options.token
799
+ noTlsVerify: options.insecure,
727
800
  });
801
+ return;
728
802
  }
729
- else {
730
- await createTunnel({
803
+ // ngrok
804
+ if (options.ngrok || options.server === "ngrok") {
805
+ await createNgrokTunnel({
731
806
  protocol: options.protocol,
732
807
  localHost: "localhost",
733
808
  localPort: parseInt(port),
734
809
  subdomain: options.subdomain,
735
- serverUrl,
736
- token: options.token,
737
- insecure: options.insecure
810
+ authtoken: options.token
738
811
  });
812
+ return;
739
813
  }
814
+ // OpenTunnel server
815
+ await createTunnel({
816
+ protocol: options.protocol,
817
+ localHost: "localhost",
818
+ localPort: parseInt(port),
819
+ subdomain: options.subdomain,
820
+ serverUrl,
821
+ token: options.token,
822
+ insecure: options.insecure
823
+ });
740
824
  });
741
825
  // Server command
742
826
  program
@@ -1613,7 +1697,7 @@ program
1613
1697
  console.log(chalk_1.default.cyan(`Connecting to ${remote}...\n`));
1614
1698
  console.log(chalk_1.default.cyan(`Starting ${tunnelsToStart.length} tunnel(s)...\n`));
1615
1699
  try {
1616
- await startTunnelsFromConfig(tunnelsToStart, serverUrl, config.server?.token, true);
1700
+ await startTunnelsFromConfig(tunnelsToStart, serverUrl, config.server?.token, true, config);
1617
1701
  console.log(chalk_1.default.gray("\nPress Ctrl+C to stop"));
1618
1702
  // Keep running
1619
1703
  await new Promise(() => { });
@@ -1629,6 +1713,27 @@ program
1629
1713
  const { TunnelServer } = await Promise.resolve().then(() => __importStar(require("../server/TunnelServer")));
1630
1714
  const tcpMin = config.server?.tcpPortMin || 10000;
1631
1715
  const tcpMax = config.server?.tcpPortMax || 20000;
1716
+ // Check if port is in use before starting server
1717
+ const isPortInUse = await checkPortInUse(port);
1718
+ if (isPortInUse) {
1719
+ console.log(chalk_1.default.red(`\n❌ Port ${port} is already in use!`));
1720
+ console.log(chalk_1.default.yellow(`Common conflicts:`));
1721
+ if (port === 8080) {
1722
+ console.log(chalk_1.default.gray(` • Pi-hole often uses port 8080`));
1723
+ console.log(chalk_1.default.gray(` • Try: --port 8081 or --port 8443`));
1724
+ }
1725
+ if (port === 443) {
1726
+ console.log(chalk_1.default.gray(` • Web servers (Apache/Nginx) often use port 443`));
1727
+ console.log(chalk_1.default.gray(` • Try: --port 8443 or --port 4430`));
1728
+ }
1729
+ console.log(chalk_1.default.cyan(`\nTo check what's using the port:`));
1730
+ console.log(chalk_1.default.white(` • Linux/macOS: sudo lsof -i :${port}`));
1731
+ console.log(chalk_1.default.white(` • Windows: netstat -ano | findstr :${port}`));
1732
+ console.log(chalk_1.default.cyan(`\nTo use a different port:`));
1733
+ console.log(chalk_1.default.white(` • CLI: opentunnel server --port 8081`));
1734
+ console.log(chalk_1.default.white(` • Config: server.port: 8081`));
1735
+ process.exit(1);
1736
+ }
1632
1737
  const spinner = (0, ora_1.default)("Starting server...").start();
1633
1738
  const server = new TunnelServer({
1634
1739
  port,
@@ -1665,7 +1770,7 @@ program
1665
1770
  const serverUrl = `${wsProtocol}://localhost:${port}/_tunnel`;
1666
1771
  // Small delay to ensure server is fully ready
1667
1772
  await new Promise(resolve => setTimeout(resolve, 500));
1668
- await startTunnelsFromConfig(tunnelsToStart, serverUrl, config.server?.token, true);
1773
+ await startTunnelsFromConfig(tunnelsToStart, serverUrl, config.server?.token, true, config);
1669
1774
  }
1670
1775
  else if (isServerMode) {
1671
1776
  console.log(chalk_1.default.gray("\nServer ready. Waiting for connections..."));
@@ -2301,79 +2406,175 @@ async function runTunnelInBackgroundFromConfig(name, protocol, port, options) {
2301
2406
  console.log(chalk_1.default.green(` ✓ ${name}: started (PID: ${child.pid})`));
2302
2407
  }
2303
2408
  ;
2304
- async function startTunnelsFromConfig(tunnels, serverUrl, token, insecure) {
2305
- const spinner = (0, ora_1.default)("Connecting to server...").start();
2306
- const client = new TunnelClient_1.TunnelClient({
2307
- serverUrl,
2308
- token,
2309
- reconnect: true,
2310
- silent: true,
2311
- rejectUnauthorized: !insecure,
2312
- });
2313
- try {
2314
- await client.connect();
2315
- spinner.succeed("Connected to server");
2316
- const activeTunnels = [];
2317
- for (const tunnel of tunnels) {
2318
- const tunnelSpinner = (0, ora_1.default)(`Creating tunnel: ${tunnel.name}...`).start();
2319
- try {
2320
- const { tunnelId, publicUrl } = await client.createTunnel({
2321
- protocol: tunnel.protocol,
2322
- localHost: tunnel.host || "localhost",
2323
- localPort: tunnel.port,
2324
- subdomain: tunnel.subdomain,
2325
- remotePort: tunnel.remotePort,
2326
- });
2327
- activeTunnels.push({ name: tunnel.name, tunnelId, publicUrl });
2328
- tunnelSpinner.succeed(`${tunnel.name}: ${publicUrl}`);
2329
- }
2330
- catch (err) {
2331
- tunnelSpinner.fail(`${tunnel.name}: ${err.message}`);
2332
- }
2409
+ async function startTunnelsFromConfig(tunnels, serverUrl, token, insecure, globalConfig) {
2410
+ // Determine global provider
2411
+ const globalProvider = globalConfig?.provider || "opentunnel";
2412
+ // Group tunnels by provider
2413
+ const tunnelsByProvider = new Map();
2414
+ for (const tunnel of tunnels) {
2415
+ const provider = tunnel.provider || globalProvider;
2416
+ if (!tunnelsByProvider.has(provider)) {
2417
+ tunnelsByProvider.set(provider, []);
2333
2418
  }
2334
- if (activeTunnels.length === 0) {
2335
- console.log(chalk_1.default.red("\nNo tunnels created"));
2336
- process.exit(1);
2419
+ tunnelsByProvider.get(provider).push(tunnel);
2420
+ }
2421
+ const activeTunnels = [];
2422
+ const clients = [];
2423
+ // Process OpenTunnel tunnels
2424
+ const opentunnelTunnels = tunnelsByProvider.get("opentunnel") || [];
2425
+ if (opentunnelTunnels.length > 0) {
2426
+ const spinner = (0, ora_1.default)("Connecting to OpenTunnel server...").start();
2427
+ const client = new TunnelClient_1.TunnelClient({
2428
+ serverUrl,
2429
+ token,
2430
+ reconnect: true,
2431
+ silent: true,
2432
+ rejectUnauthorized: !insecure,
2433
+ });
2434
+ try {
2435
+ await client.connect();
2436
+ spinner.succeed("Connected to OpenTunnel server");
2437
+ clients.push({ provider: "opentunnel", client });
2438
+ for (const tunnel of opentunnelTunnels) {
2439
+ const tunnelSpinner = (0, ora_1.default)(`Creating tunnel: ${tunnel.name}...`).start();
2440
+ try {
2441
+ const { tunnelId, publicUrl } = await client.createTunnel({
2442
+ protocol: tunnel.protocol,
2443
+ localHost: tunnel.host || "localhost",
2444
+ localPort: tunnel.port,
2445
+ subdomain: tunnel.subdomain,
2446
+ remotePort: tunnel.remotePort,
2447
+ });
2448
+ activeTunnels.push({ name: tunnel.name, tunnelId, publicUrl, provider: "opentunnel", client });
2449
+ tunnelSpinner.succeed(`${tunnel.name}: ${publicUrl}`);
2450
+ }
2451
+ catch (err) {
2452
+ tunnelSpinner.fail(`${tunnel.name}: ${err.message}`);
2453
+ }
2454
+ }
2455
+ // Handle reconnection events
2456
+ client.on("disconnected", () => {
2457
+ console.log(chalk_1.default.yellow("\n OpenTunnel disconnected, reconnecting..."));
2458
+ });
2459
+ client.on("connected", () => {
2460
+ console.log(chalk_1.default.green(" OpenTunnel reconnected!"));
2461
+ });
2337
2462
  }
2338
- console.log(chalk_1.default.cyan("\n─────────────────────────────────────────"));
2339
- console.log(chalk_1.default.green(` ${activeTunnels.length} tunnel(s) active`));
2340
- console.log(chalk_1.default.cyan("─────────────────────────────────────────\n"));
2341
- for (const t of activeTunnels) {
2342
- console.log(` ${chalk_1.default.white(t.name.padEnd(15))} ${chalk_1.default.green(t.publicUrl)}`);
2463
+ catch (err) {
2464
+ spinner.fail(`OpenTunnel connection failed: ${err.message}`);
2343
2465
  }
2344
- console.log(chalk_1.default.gray("\n Press Ctrl+C to stop all tunnels\n"));
2345
- // Keep alive with uptime counter
2346
- const startTime = Date.now();
2347
- const statsInterval = setInterval(() => {
2348
- const uptime = (0, utils_1.formatDuration)(Date.now() - startTime);
2349
- process.stdout.write(`\r ${chalk_1.default.gray(`Uptime: ${uptime}`)}`);
2350
- }, 1000);
2351
- // Handle exit
2352
- const cleanup = async () => {
2353
- clearInterval(statsInterval);
2354
- console.log("\n");
2355
- const closeSpinner = (0, ora_1.default)("Closing tunnels...").start();
2356
- for (const t of activeTunnels) {
2357
- await client.closeTunnel(t.tunnelId);
2358
- }
2359
- await client.disconnect();
2360
- closeSpinner.succeed("All tunnels closed");
2361
- process.exit(0);
2362
- };
2363
- process.on("SIGINT", cleanup);
2364
- process.on("SIGTERM", cleanup);
2365
- // Handle reconnection
2366
- client.on("disconnected", () => {
2367
- console.log(chalk_1.default.yellow("\n Disconnected, reconnecting..."));
2466
+ }
2467
+ // Process ngrok tunnels (one at a time, ngrok limitation)
2468
+ const ngrokTunnels = tunnelsByProvider.get("ngrok") || [];
2469
+ for (const tunnel of ngrokTunnels) {
2470
+ const spinner = (0, ora_1.default)(`Creating ngrok tunnel: ${tunnel.name}...`).start();
2471
+ const ngrokToken = tunnel.ngrokToken || globalConfig?.ngrok?.token;
2472
+ const ngrokRegion = tunnel.ngrokRegion || globalConfig?.ngrok?.region || "us";
2473
+ // Merge IP access config: tunnel-specific > global security > none
2474
+ const { IpFilter } = await Promise.resolve().then(() => __importStar(require("../shared/ip-filter")));
2475
+ const ipAccess = IpFilter.mergeConfigs(globalConfig?.security?.ipAccess, tunnel.ipAccess);
2476
+ const client = new NgrokClient_1.NgrokClient({
2477
+ authtoken: ngrokToken,
2478
+ region: ngrokRegion,
2479
+ ipAccess,
2368
2480
  });
2369
- client.on("connected", () => {
2370
- console.log(chalk_1.default.green(" Reconnected!"));
2481
+ try {
2482
+ await client.connect();
2483
+ const { tunnelId, publicUrl } = await client.createTunnel({
2484
+ protocol: tunnel.protocol,
2485
+ localHost: tunnel.host || "localhost",
2486
+ localPort: tunnel.port,
2487
+ subdomain: tunnel.subdomain,
2488
+ remotePort: tunnel.remotePort,
2489
+ });
2490
+ activeTunnels.push({ name: tunnel.name, tunnelId, publicUrl, provider: "ngrok", client });
2491
+ clients.push({ provider: "ngrok", client });
2492
+ spinner.succeed(`${tunnel.name}: ${publicUrl} (ngrok)`);
2493
+ }
2494
+ catch (err) {
2495
+ spinner.fail(`${tunnel.name}: ${err.message}`);
2496
+ console.log(chalk_1.default.yellow(" Make sure ngrok is installed: https://ngrok.com/download"));
2497
+ }
2498
+ }
2499
+ // Process Cloudflare tunnels (one at a time)
2500
+ const cloudflareTunnels = tunnelsByProvider.get("cloudflare") || [];
2501
+ for (const tunnel of cloudflareTunnels) {
2502
+ const spinner = (0, ora_1.default)(`Creating Cloudflare tunnel: ${tunnel.name}...`).start();
2503
+ const cfHostname = tunnel.cfHostname || globalConfig?.cloudflare?.hostname;
2504
+ // For Cloudflare: subdomain = tunnel name
2505
+ const cfTunnelName = tunnel.subdomain || globalConfig?.cloudflare?.tunnelName;
2506
+ // Merge IP access config: tunnel-specific > global security > none
2507
+ const { IpFilter } = await Promise.resolve().then(() => __importStar(require("../shared/ip-filter")));
2508
+ const ipAccess = IpFilter.mergeConfigs(globalConfig?.security?.ipAccess, tunnel.ipAccess);
2509
+ const client = new CloudflareTunnelClient_1.CloudflareTunnelClient({
2510
+ hostname: cfHostname,
2511
+ tunnelName: cfTunnelName,
2512
+ protocol: tunnel.protocol === "https" ? "https" : "http",
2513
+ ipAccess,
2371
2514
  });
2515
+ try {
2516
+ await client.connect();
2517
+ const { tunnelId, publicUrl } = await client.createTunnel({
2518
+ protocol: tunnel.protocol,
2519
+ localHost: tunnel.host || "localhost",
2520
+ localPort: tunnel.port,
2521
+ });
2522
+ activeTunnels.push({ name: tunnel.name, tunnelId, publicUrl, provider: "cloudflare", client });
2523
+ clients.push({ provider: "cloudflare", client });
2524
+ const tunnelMode = cfTunnelName ? ` [${cfTunnelName}]` : "";
2525
+ spinner.succeed(`${tunnel.name}: ${publicUrl}${tunnelMode} (cloudflare)`);
2526
+ }
2527
+ catch (err) {
2528
+ spinner.fail(`${tunnel.name}: ${err.message}`);
2529
+ console.log(chalk_1.default.yellow(" cloudflared installs automatically. Check internet connection or try: npm rebuild cloudflared"));
2530
+ }
2372
2531
  }
2373
- catch (err) {
2374
- spinner.fail(`Failed: ${err.message}`);
2532
+ if (activeTunnels.length === 0) {
2533
+ console.log(chalk_1.default.red("\nNo tunnels created"));
2375
2534
  process.exit(1);
2376
2535
  }
2536
+ console.log(chalk_1.default.cyan("\n─────────────────────────────────────────"));
2537
+ console.log(chalk_1.default.green(` ${activeTunnels.length} tunnel(s) active`));
2538
+ console.log(chalk_1.default.cyan("─────────────────────────────────────────\n"));
2539
+ for (const t of activeTunnels) {
2540
+ const providerTag = t.provider !== "opentunnel" ? chalk_1.default.gray(` (${t.provider})`) : "";
2541
+ console.log(` ${chalk_1.default.white(t.name.padEnd(15))} ${chalk_1.default.green(t.publicUrl)}${providerTag}`);
2542
+ }
2543
+ console.log(chalk_1.default.gray("\n Press Ctrl+C to stop all tunnels\n"));
2544
+ // Keep alive with uptime counter
2545
+ const startTime = Date.now();
2546
+ const statsInterval = setInterval(() => {
2547
+ const uptime = (0, utils_1.formatDuration)(Date.now() - startTime);
2548
+ process.stdout.write(`\r ${chalk_1.default.gray(`Uptime: ${uptime}`)}`);
2549
+ }, 1000);
2550
+ // Handle exit
2551
+ const cleanup = async () => {
2552
+ clearInterval(statsInterval);
2553
+ console.log("\n");
2554
+ const closeSpinner = (0, ora_1.default)("Closing tunnels...").start();
2555
+ for (const t of activeTunnels) {
2556
+ try {
2557
+ await t.client.closeTunnel(t.tunnelId);
2558
+ }
2559
+ catch {
2560
+ // Ignore errors during cleanup
2561
+ }
2562
+ }
2563
+ for (const { client } of clients) {
2564
+ try {
2565
+ await client.disconnect();
2566
+ }
2567
+ catch {
2568
+ // Ignore errors during cleanup
2569
+ }
2570
+ }
2571
+ closeSpinner.succeed("All tunnels closed");
2572
+ process.exit(0);
2573
+ };
2574
+ process.on("SIGINT", cleanup);
2575
+ process.on("SIGTERM", cleanup);
2576
+ // Keep process alive
2577
+ await new Promise(() => { });
2377
2578
  }
2378
2579
  ;
2379
2580
  async function runTunnelInBackground(command, port, options) {
@@ -2565,6 +2766,61 @@ async function createNgrokTunnel(options) {
2565
2766
  }
2566
2767
  }
2567
2768
  ;
2769
+ async function createCloudflareTunnel(options) {
2770
+ const tunnelType = options.tunnelName ? `named tunnel '${options.tunnelName}'` : "quick tunnel";
2771
+ const spinner = (0, ora_1.default)(`Starting Cloudflare ${tunnelType}...`).start();
2772
+ const client = new CloudflareTunnelClient_1.CloudflareTunnelClient({
2773
+ hostname: options.hostname,
2774
+ tunnelName: options.tunnelName,
2775
+ noTlsVerify: options.noTlsVerify,
2776
+ protocol: options.protocol === "https" ? "https" : "http",
2777
+ });
2778
+ try {
2779
+ await client.connect();
2780
+ spinner.text = "Creating tunnel...";
2781
+ const { tunnelId, publicUrl } = await client.createTunnel({
2782
+ protocol: options.protocol,
2783
+ localHost: options.localHost,
2784
+ localPort: options.localPort,
2785
+ });
2786
+ spinner.succeed("Tunnel established!");
2787
+ const providerInfo = options.tunnelName
2788
+ ? `Cloudflare [${options.tunnelName}]`
2789
+ : "Cloudflare";
2790
+ printTunnelInfo({
2791
+ status: "Online",
2792
+ protocol: options.protocol,
2793
+ localHost: options.localHost,
2794
+ localPort: options.localPort,
2795
+ publicUrl,
2796
+ provider: providerInfo,
2797
+ });
2798
+ // Keep alive
2799
+ const startTime = Date.now();
2800
+ const statsInterval = setInterval(() => {
2801
+ const uptime = (0, utils_1.formatDuration)(Date.now() - startTime);
2802
+ process.stdout.write(`\r ${chalk_1.default.gray(`Uptime: ${uptime}`)}`);
2803
+ }, 1000);
2804
+ // Handle exit
2805
+ const cleanup = async () => {
2806
+ clearInterval(statsInterval);
2807
+ console.log("\n");
2808
+ spinner.start("Closing tunnel...");
2809
+ await client.closeTunnel(tunnelId);
2810
+ await client.disconnect();
2811
+ spinner.succeed("Tunnel closed");
2812
+ process.exit(0);
2813
+ };
2814
+ process.on("SIGINT", cleanup);
2815
+ process.on("SIGTERM", cleanup);
2816
+ }
2817
+ catch (err) {
2818
+ spinner.fail(`Failed: ${err.message}`);
2819
+ console.log(chalk_1.default.yellow("\ncloudflared installs automatically. Check your internet connection or try: npm rebuild cloudflared"));
2820
+ process.exit(1);
2821
+ }
2822
+ }
2823
+ ;
2568
2824
  function printTunnelInfo(info) {
2569
2825
  console.log("");
2570
2826
  console.log(chalk_1.default.cyan(` OpenTunnel ${chalk_1.default.gray(`(via ${info.provider})`)}`));
@@ -2579,5 +2835,386 @@ function printTunnelInfo(info) {
2579
2835
  console.log("");
2580
2836
  }
2581
2837
  ;
2838
+ // ============================================================================
2839
+ // Unified Provider Commands
2840
+ // ============================================================================
2841
+ // Login command - unified for all providers
2842
+ program
2843
+ .command("login <provider>")
2844
+ .description("Authenticate with a provider (cloudflare, ngrok)")
2845
+ .option("-t, --token <token>", "Authentication token (required for ngrok)")
2846
+ .action(async (provider, options) => {
2847
+ const credManager = new credentials_1.CredentialsManager();
2848
+ const normalizedProvider = provider.toLowerCase();
2849
+ if (normalizedProvider === "cloudflare" || normalizedProvider === "cf") {
2850
+ const spinner = (0, ora_1.default)("Running cloudflared login...").start();
2851
+ try {
2852
+ const binPath = await CloudflareTunnelClient_1.CloudflareTunnelClient.ensureInstalled();
2853
+ const { spawn } = await Promise.resolve().then(() => __importStar(require("child_process")));
2854
+ spinner.stop();
2855
+ console.log(chalk_1.default.cyan("\n Opening browser for Cloudflare authentication...\n"));
2856
+ const proc = spawn(binPath, ["login"], {
2857
+ stdio: "inherit",
2858
+ shell: process.platform === "win32",
2859
+ });
2860
+ await new Promise((resolve, reject) => {
2861
+ proc.on("close", (code) => {
2862
+ if (code === 0) {
2863
+ const os = require("os");
2864
+ const defaultCertPath = path.join(os.homedir(), ".cloudflared", "cert.pem");
2865
+ credManager.setCloudflare({ certPath: defaultCertPath });
2866
+ console.log(chalk_1.default.green("\n Cloudflare credentials saved"));
2867
+ console.log(chalk_1.default.gray(` Cert path: ${defaultCertPath}`));
2868
+ resolve();
2869
+ }
2870
+ else {
2871
+ reject(new Error(`cloudflared login exited with code ${code}`));
2872
+ }
2873
+ });
2874
+ proc.on("error", reject);
2875
+ });
2876
+ }
2877
+ catch (err) {
2878
+ spinner.fail(`Login failed: ${err.message}`);
2879
+ process.exit(1);
2880
+ }
2881
+ }
2882
+ else if (normalizedProvider === "ngrok") {
2883
+ if (!options.token) {
2884
+ console.log(chalk_1.default.red("Error: ngrok requires --token <token>"));
2885
+ console.log(chalk_1.default.gray("\nGet your token from: https://dashboard.ngrok.com/get-started/your-authtoken"));
2886
+ console.log(chalk_1.default.cyan("\nUsage: opentunnel login ngrok --token <your-token>"));
2887
+ process.exit(1);
2888
+ }
2889
+ credManager.setNgrok({ token: options.token });
2890
+ console.log(chalk_1.default.green("\n ngrok credentials saved"));
2891
+ console.log(chalk_1.default.gray(` Token: ${options.token.slice(0, 8)}...`));
2892
+ console.log(chalk_1.default.gray(` Stored at: ${credentials_1.CredentialsManager.getCredentialsFile()}\n`));
2893
+ }
2894
+ else {
2895
+ console.log(chalk_1.default.red(`Unknown provider: ${provider}`));
2896
+ console.log(chalk_1.default.gray("\nSupported providers: cloudflare (cf), ngrok"));
2897
+ process.exit(1);
2898
+ }
2899
+ });
2900
+ // Logout command - unified for all providers
2901
+ program
2902
+ .command("logout <provider>")
2903
+ .description("Remove stored credentials for a provider")
2904
+ .action(async (provider) => {
2905
+ const credManager = new credentials_1.CredentialsManager();
2906
+ const normalizedProvider = provider.toLowerCase();
2907
+ if (normalizedProvider === "cloudflare" || normalizedProvider === "cf") {
2908
+ const removed = credManager.removeProvider("cloudflare");
2909
+ if (removed) {
2910
+ console.log(chalk_1.default.green("\n Cloudflare credentials removed\n"));
2911
+ }
2912
+ else {
2913
+ console.log(chalk_1.default.yellow("\n No Cloudflare credentials found\n"));
2914
+ }
2915
+ }
2916
+ else if (normalizedProvider === "ngrok") {
2917
+ const removed = credManager.removeProvider("ngrok");
2918
+ if (removed) {
2919
+ console.log(chalk_1.default.green("\n ngrok credentials removed\n"));
2920
+ }
2921
+ else {
2922
+ console.log(chalk_1.default.yellow("\n No ngrok credentials found\n"));
2923
+ }
2924
+ }
2925
+ else {
2926
+ console.log(chalk_1.default.red(`Unknown provider: ${provider}`));
2927
+ console.log(chalk_1.default.gray("\nSupported providers: cloudflare (cf), ngrok"));
2928
+ process.exit(1);
2929
+ }
2930
+ });
2931
+ // Create tunnel command (for named tunnels)
2932
+ program
2933
+ .command("create <name>")
2934
+ .description("Create a named tunnel")
2935
+ .option("--cf, --cloudflare", "Create Cloudflare named tunnel")
2936
+ .option("--provider <provider>", "Provider (cloudflare)")
2937
+ .action(async (name, options) => {
2938
+ const provider = options.provider?.toLowerCase() ||
2939
+ (options.cloudflare || options.cf ? "cloudflare" : null);
2940
+ if (!provider) {
2941
+ console.log(chalk_1.default.red("Error: Provider required"));
2942
+ console.log(chalk_1.default.gray("\nUsage: opentunnel create <name> --cf"));
2943
+ console.log(chalk_1.default.gray(" opentunnel create <name> --provider cloudflare"));
2944
+ process.exit(1);
2945
+ }
2946
+ if (provider === "cloudflare" || provider === "cf") {
2947
+ const spinner = (0, ora_1.default)(`Creating Cloudflare tunnel '${name}'...`).start();
2948
+ try {
2949
+ const binPath = await CloudflareTunnelClient_1.CloudflareTunnelClient.ensureInstalled();
2950
+ const { spawn } = await Promise.resolve().then(() => __importStar(require("child_process")));
2951
+ const proc = spawn(binPath, ["tunnel", "create", name], {
2952
+ stdio: ["ignore", "pipe", "pipe"],
2953
+ shell: process.platform === "win32",
2954
+ });
2955
+ let output = "";
2956
+ let errorOutput = "";
2957
+ proc.stdout?.on("data", (data) => { output += data.toString(); });
2958
+ proc.stderr?.on("data", (data) => { errorOutput += data.toString(); });
2959
+ await new Promise((resolve, reject) => {
2960
+ proc.on("close", (code) => {
2961
+ if (code === 0) {
2962
+ spinner.succeed(`Tunnel '${name}' created`);
2963
+ if (output)
2964
+ console.log(chalk_1.default.gray(output.trim()));
2965
+ resolve();
2966
+ }
2967
+ else {
2968
+ reject(new Error(errorOutput || `Exit code ${code}`));
2969
+ }
2970
+ });
2971
+ proc.on("error", reject);
2972
+ });
2973
+ }
2974
+ catch (err) {
2975
+ spinner.fail(`Failed to create tunnel: ${err.message}`);
2976
+ if (err.message.includes("login")) {
2977
+ console.log(chalk_1.default.yellow("\nRun 'opentunnel login cloudflare' first"));
2978
+ }
2979
+ process.exit(1);
2980
+ }
2981
+ }
2982
+ else {
2983
+ console.log(chalk_1.default.red(`Provider '${provider}' doesn't support named tunnels`));
2984
+ process.exit(1);
2985
+ }
2986
+ });
2987
+ // Delete tunnel command
2988
+ program
2989
+ .command("delete <name>")
2990
+ .description("Delete a named tunnel")
2991
+ .option("--cf, --cloudflare", "Delete Cloudflare named tunnel")
2992
+ .option("--provider <provider>", "Provider (cloudflare)")
2993
+ .option("-f, --force", "Force deletion without confirmation")
2994
+ .action(async (name, options) => {
2995
+ const provider = options.provider?.toLowerCase() ||
2996
+ (options.cloudflare || options.cf ? "cloudflare" : null);
2997
+ if (!provider) {
2998
+ console.log(chalk_1.default.red("Error: Provider required"));
2999
+ console.log(chalk_1.default.gray("\nUsage: opentunnel delete <name> --cf"));
3000
+ process.exit(1);
3001
+ }
3002
+ if (provider === "cloudflare" || provider === "cf") {
3003
+ if (!options.force) {
3004
+ console.log(chalk_1.default.yellow(`\n This will delete tunnel '${name}' permanently.`));
3005
+ console.log(chalk_1.default.gray(" Use --force to skip this confirmation.\n"));
3006
+ const readline = await Promise.resolve().then(() => __importStar(require("readline")));
3007
+ const rl = readline.createInterface({
3008
+ input: process.stdin,
3009
+ output: process.stdout,
3010
+ });
3011
+ const answer = await new Promise((resolve) => {
3012
+ rl.question(" Type the tunnel name to confirm: ", resolve);
3013
+ });
3014
+ rl.close();
3015
+ if (answer !== name) {
3016
+ console.log(chalk_1.default.red("\n Cancelled: name didn't match\n"));
3017
+ return;
3018
+ }
3019
+ }
3020
+ const spinner = (0, ora_1.default)(`Deleting tunnel '${name}'...`).start();
3021
+ try {
3022
+ const binPath = await CloudflareTunnelClient_1.CloudflareTunnelClient.ensureInstalled();
3023
+ const { spawn } = await Promise.resolve().then(() => __importStar(require("child_process")));
3024
+ const proc = spawn(binPath, ["tunnel", "delete", name], {
3025
+ stdio: ["ignore", "pipe", "pipe"],
3026
+ shell: process.platform === "win32",
3027
+ });
3028
+ let errorOutput = "";
3029
+ proc.stderr?.on("data", (data) => { errorOutput += data.toString(); });
3030
+ await new Promise((resolve, reject) => {
3031
+ proc.on("close", (code) => {
3032
+ if (code === 0) {
3033
+ spinner.succeed(`Tunnel '${name}' deleted`);
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 delete tunnel: ${err.message}`);
3045
+ process.exit(1);
3046
+ }
3047
+ }
3048
+ });
3049
+ // Route DNS command (Cloudflare specific)
3050
+ program
3051
+ .command("route <name> <hostname>")
3052
+ .description("Route a hostname to a tunnel (Cloudflare DNS)")
3053
+ .option("--cf, --cloudflare", "Route via Cloudflare")
3054
+ .option("--provider <provider>", "Provider (cloudflare)")
3055
+ .action(async (name, hostname, options) => {
3056
+ const provider = options.provider?.toLowerCase() ||
3057
+ (options.cloudflare || options.cf ? "cloudflare" : "cloudflare"); // Default to cloudflare
3058
+ if (provider === "cloudflare" || provider === "cf") {
3059
+ const spinner = (0, ora_1.default)(`Routing ${hostname} to tunnel '${name}'...`).start();
3060
+ try {
3061
+ const binPath = await CloudflareTunnelClient_1.CloudflareTunnelClient.ensureInstalled();
3062
+ const { spawn } = await Promise.resolve().then(() => __importStar(require("child_process")));
3063
+ const proc = spawn(binPath, ["tunnel", "route", "dns", name, hostname], {
3064
+ stdio: ["ignore", "pipe", "pipe"],
3065
+ shell: process.platform === "win32",
3066
+ });
3067
+ let output = "";
3068
+ let errorOutput = "";
3069
+ proc.stdout?.on("data", (data) => { output += data.toString(); });
3070
+ proc.stderr?.on("data", (data) => { errorOutput += data.toString(); });
3071
+ await new Promise((resolve, reject) => {
3072
+ proc.on("close", (code) => {
3073
+ if (code === 0) {
3074
+ spinner.succeed(`DNS route created: ${hostname} -> ${name}`);
3075
+ if (output)
3076
+ console.log(chalk_1.default.gray(output.trim()));
3077
+ resolve();
3078
+ }
3079
+ else {
3080
+ reject(new Error(errorOutput || `Exit code ${code}`));
3081
+ }
3082
+ });
3083
+ proc.on("error", reject);
3084
+ });
3085
+ }
3086
+ catch (err) {
3087
+ spinner.fail(`Failed to create route: ${err.message}`);
3088
+ process.exit(1);
3089
+ }
3090
+ }
3091
+ });
3092
+ // List tunnels command - extend existing list command
3093
+ program
3094
+ .command("tunnels")
3095
+ .description("List named tunnels")
3096
+ .option("--cf, --cloudflare", "List Cloudflare tunnels")
3097
+ .option("--provider <provider>", "Provider (cloudflare)")
3098
+ .action(async (options) => {
3099
+ const provider = options.provider?.toLowerCase() ||
3100
+ (options.cloudflare || options.cf ? "cloudflare" : null);
3101
+ if (!provider) {
3102
+ console.log(chalk_1.default.red("Error: Provider required"));
3103
+ console.log(chalk_1.default.gray("\nUsage: opentunnel tunnels --cf"));
3104
+ process.exit(1);
3105
+ }
3106
+ if (provider === "cloudflare" || provider === "cf") {
3107
+ const spinner = (0, ora_1.default)("Listing Cloudflare tunnels...").start();
3108
+ try {
3109
+ const binPath = await CloudflareTunnelClient_1.CloudflareTunnelClient.ensureInstalled();
3110
+ const { spawn } = await Promise.resolve().then(() => __importStar(require("child_process")));
3111
+ const proc = spawn(binPath, ["tunnel", "list"], {
3112
+ stdio: ["ignore", "pipe", "pipe"],
3113
+ shell: process.platform === "win32",
3114
+ });
3115
+ let output = "";
3116
+ let errorOutput = "";
3117
+ proc.stdout?.on("data", (data) => { output += data.toString(); });
3118
+ proc.stderr?.on("data", (data) => { errorOutput += data.toString(); });
3119
+ await new Promise((resolve, reject) => {
3120
+ proc.on("close", (code) => {
3121
+ if (code === 0) {
3122
+ spinner.stop();
3123
+ console.log(chalk_1.default.cyan("\n Cloudflare Tunnels"));
3124
+ console.log(chalk_1.default.gray(" " + "─".repeat(60)));
3125
+ if (output.trim()) {
3126
+ console.log(output);
3127
+ }
3128
+ else {
3129
+ console.log(chalk_1.default.yellow(" No tunnels found"));
3130
+ console.log(chalk_1.default.gray("\n Create one with: opentunnel create <name> --cf"));
3131
+ }
3132
+ resolve();
3133
+ }
3134
+ else {
3135
+ reject(new Error(errorOutput || `Exit code ${code}`));
3136
+ }
3137
+ });
3138
+ proc.on("error", reject);
3139
+ });
3140
+ }
3141
+ catch (err) {
3142
+ spinner.fail(`Failed to list tunnels: ${err.message}`);
3143
+ if (err.message.includes("login")) {
3144
+ console.log(chalk_1.default.yellow("\nRun 'opentunnel login cloudflare' first"));
3145
+ }
3146
+ process.exit(1);
3147
+ }
3148
+ }
3149
+ });
3150
+ // ============================================================================
3151
+ // Config Commands
3152
+ // ============================================================================
3153
+ const configCommand = program
3154
+ .command("config")
3155
+ .description("Manage OpenTunnel configuration");
3156
+ configCommand
3157
+ .command("set <key> <value>")
3158
+ .description("Set a configuration value (e.g., ngrok.token, cloudflare.accountId)")
3159
+ .action(async (key, value) => {
3160
+ const credManager = new credentials_1.CredentialsManager();
3161
+ const validKeys = [
3162
+ "ngrok.token",
3163
+ "cloudflare.accountId",
3164
+ "cloudflare.tunnelToken",
3165
+ "cloudflare.certPath",
3166
+ ];
3167
+ if (!validKeys.includes(key)) {
3168
+ console.log(chalk_1.default.red(`Invalid key: ${key}`));
3169
+ console.log(chalk_1.default.gray("\nValid keys:"));
3170
+ for (const k of validKeys) {
3171
+ console.log(chalk_1.default.cyan(` ${k}`));
3172
+ }
3173
+ process.exit(1);
3174
+ }
3175
+ credManager.set(key, value);
3176
+ console.log(chalk_1.default.green(`\n ${key} = ${value.slice(0, 20)}${value.length > 20 ? "..." : ""}\n`));
3177
+ });
3178
+ configCommand
3179
+ .command("get <key>")
3180
+ .description("Get a configuration value")
3181
+ .action(async (key) => {
3182
+ const credManager = new credentials_1.CredentialsManager();
3183
+ const value = credManager.get(key);
3184
+ if (value) {
3185
+ // Mask sensitive values
3186
+ const masked = key.includes("token") || key.includes("Token")
3187
+ ? value.slice(0, 8) + "..." + value.slice(-4)
3188
+ : value;
3189
+ console.log(chalk_1.default.cyan(`\n ${key} = ${masked}\n`));
3190
+ }
3191
+ else {
3192
+ console.log(chalk_1.default.yellow(`\n ${key} is not set\n`));
3193
+ }
3194
+ });
3195
+ configCommand
3196
+ .command("list")
3197
+ .description("List all stored configuration")
3198
+ .action(async () => {
3199
+ const credManager = new credentials_1.CredentialsManager();
3200
+ const keys = credManager.listKeys();
3201
+ console.log(chalk_1.default.cyan("\n Stored Configuration"));
3202
+ console.log(chalk_1.default.gray(" " + "─".repeat(40)));
3203
+ if (keys.length === 0) {
3204
+ console.log(chalk_1.default.yellow(" No configuration stored"));
3205
+ console.log(chalk_1.default.gray("\n Use 'opentunnel login' or 'opentunnel config set' to add credentials"));
3206
+ }
3207
+ else {
3208
+ for (const key of keys) {
3209
+ const value = credManager.get(key);
3210
+ const masked = (key.includes("token") || key.includes("Token")) && value
3211
+ ? value.slice(0, 8) + "..." + value.slice(-4)
3212
+ : value;
3213
+ console.log(` ${chalk_1.default.white(key.padEnd(25))} ${chalk_1.default.gray(masked || "")}`);
3214
+ }
3215
+ }
3216
+ console.log(chalk_1.default.gray(" " + "─".repeat(40)));
3217
+ console.log(chalk_1.default.gray(`\n Config file: ${credentials_1.CredentialsManager.getCredentialsFile()}\n`));
3218
+ });
2582
3219
  program.parse();
2583
3220
  //# sourceMappingURL=index.js.map