opentunnel-cli 1.0.0

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 (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +284 -0
  3. package/dist/cli/index.d.ts +3 -0
  4. package/dist/cli/index.d.ts.map +1 -0
  5. package/dist/cli/index.js +1357 -0
  6. package/dist/cli/index.js.map +1 -0
  7. package/dist/client/NgrokClient.d.ts +40 -0
  8. package/dist/client/NgrokClient.d.ts.map +1 -0
  9. package/dist/client/NgrokClient.js +155 -0
  10. package/dist/client/NgrokClient.js.map +1 -0
  11. package/dist/client/TunnelClient.d.ts +47 -0
  12. package/dist/client/TunnelClient.d.ts.map +1 -0
  13. package/dist/client/TunnelClient.js +435 -0
  14. package/dist/client/TunnelClient.js.map +1 -0
  15. package/dist/client/index.d.ts +3 -0
  16. package/dist/client/index.d.ts.map +1 -0
  17. package/dist/client/index.js +8 -0
  18. package/dist/client/index.js.map +1 -0
  19. package/dist/dns/CloudflareDNS.d.ts +45 -0
  20. package/dist/dns/CloudflareDNS.d.ts.map +1 -0
  21. package/dist/dns/CloudflareDNS.js +286 -0
  22. package/dist/dns/CloudflareDNS.js.map +1 -0
  23. package/dist/dns/DuckDNS.d.ts +20 -0
  24. package/dist/dns/DuckDNS.d.ts.map +1 -0
  25. package/dist/dns/DuckDNS.js +109 -0
  26. package/dist/dns/DuckDNS.js.map +1 -0
  27. package/dist/dns/index.d.ts +3 -0
  28. package/dist/dns/index.d.ts.map +1 -0
  29. package/dist/dns/index.js +9 -0
  30. package/dist/dns/index.js.map +1 -0
  31. package/dist/index.d.ts +7 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +35 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/server/CertManager.d.ts +54 -0
  36. package/dist/server/CertManager.d.ts.map +1 -0
  37. package/dist/server/CertManager.js +414 -0
  38. package/dist/server/CertManager.js.map +1 -0
  39. package/dist/server/TunnelServer.d.ts +42 -0
  40. package/dist/server/TunnelServer.d.ts.map +1 -0
  41. package/dist/server/TunnelServer.js +790 -0
  42. package/dist/server/TunnelServer.js.map +1 -0
  43. package/dist/server/index.d.ts +3 -0
  44. package/dist/server/index.d.ts.map +1 -0
  45. package/dist/server/index.js +48 -0
  46. package/dist/server/index.js.map +1 -0
  47. package/dist/shared/types.d.ts +147 -0
  48. package/dist/shared/types.d.ts.map +1 -0
  49. package/dist/shared/types.js +3 -0
  50. package/dist/shared/types.js.map +1 -0
  51. package/dist/shared/utils.d.ts +29 -0
  52. package/dist/shared/utils.d.ts.map +1 -0
  53. package/dist/shared/utils.js +135 -0
  54. package/dist/shared/utils.js.map +1 -0
  55. package/package.json +66 -0
@@ -0,0 +1,1357 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ var __importDefault = (this && this.__importDefault) || function (mod) {
37
+ return (mod && mod.__esModule) ? mod : { "default": mod };
38
+ };
39
+ Object.defineProperty(exports, "__esModule", { value: true });
40
+ const commander_1 = require("commander");
41
+ const chalk_1 = __importDefault(require("chalk"));
42
+ const ora_1 = __importDefault(require("ora"));
43
+ const TunnelClient_1 = require("../client/TunnelClient");
44
+ const NgrokClient_1 = require("../client/NgrokClient");
45
+ const utils_1 = require("../shared/utils");
46
+ const yaml_1 = require("yaml");
47
+ const CONFIG_FILE = "opentunnel.yml";
48
+ const program = new commander_1.Command();
49
+ program
50
+ .name("opentunnel")
51
+ .alias("ot")
52
+ .description("Expose local ports to the internet via custom domains or ngrok")
53
+ .version("1.0.0");
54
+ // HTTP tunnel command
55
+ program
56
+ .command("http <port>")
57
+ .description("Expose a local HTTP server")
58
+ .option("-s, --server <url>", "Server URL (use 'ngrok' for ngrok)")
59
+ .option("-t, --token <token>", "Authentication token (or ngrok authtoken)")
60
+ .option("-n, --subdomain <name>", "Custom subdomain (e.g., 'myapp' for myapp.op.domain.com)")
61
+ .option("-d, --detach", "Run tunnel in background")
62
+ .option("-h, --host <host>", "Local host", "localhost")
63
+ .option("--domain <domain>", "Server domain (e.g., domain.com)")
64
+ .option("--https", "Use HTTPS for local connection")
65
+ .option("--insecure", "Skip SSL certificate verification (for self-signed certs)")
66
+ .option("--ngrok", "Use ngrok instead of OpenTunnel server")
67
+ .option("--region <region>", "Ngrok region (us, eu, ap, au, sa, jp, in)", "us")
68
+ .action(async (port, options) => {
69
+ // Build server URL from domain if provided
70
+ const serverUrl = options.server || (options.domain
71
+ ? `wss://${options.domain}/_tunnel`
72
+ : "ws://localhost:8080/_tunnel");
73
+ if (options.detach) {
74
+ await runTunnelInBackground("http", port, { ...options, server: serverUrl });
75
+ return;
76
+ }
77
+ if (options.ngrok || options.server === "ngrok") {
78
+ await createNgrokTunnel({
79
+ protocol: options.https ? "https" : "http",
80
+ localHost: options.host,
81
+ localPort: parseInt(port),
82
+ subdomain: options.subdomain,
83
+ authtoken: options.token,
84
+ region: options.region,
85
+ });
86
+ }
87
+ else {
88
+ await createTunnel({
89
+ protocol: options.https ? "https" : "http",
90
+ localHost: options.host,
91
+ localPort: parseInt(port),
92
+ subdomain: options.subdomain,
93
+ serverUrl,
94
+ token: options.token,
95
+ insecure: options.insecure,
96
+ });
97
+ }
98
+ });
99
+ // TCP tunnel command
100
+ program
101
+ .command("tcp <port>")
102
+ .description("Expose a local TCP server")
103
+ .option("-s, --server <url>", "Server URL (use 'ngrok' for ngrok)")
104
+ .option("-t, --token <token>", "Authentication token (or ngrok authtoken)")
105
+ .option("-r, --remote-port <port>", "Remote port to use")
106
+ .option("-h, --host <host>", "Local host", "localhost")
107
+ .option("-d, --detach", "Run tunnel in background")
108
+ .option("--domain <domain>", "Server domain (e.g., domain.com)")
109
+ .option("--insecure", "Skip SSL certificate verification (for self-signed certs)")
110
+ .option("--ngrok", "Use ngrok instead of OpenTunnel server")
111
+ .option("--region <region>", "Ngrok region (us, eu, ap, au, sa, jp, in)", "us")
112
+ .action(async (port, options) => {
113
+ const serverUrl = options.server || (options.domain
114
+ ? `wss://${options.domain}/_tunnel`
115
+ : "ws://localhost:8080/_tunnel");
116
+ if (options.detach) {
117
+ await runTunnelInBackground("tcp", port, { ...options, server: serverUrl });
118
+ return;
119
+ }
120
+ if (options.ngrok || options.server === "ngrok") {
121
+ await createNgrokTunnel({
122
+ protocol: "tcp",
123
+ localHost: options.host,
124
+ localPort: parseInt(port),
125
+ remotePort: options.remotePort ? parseInt(options.remotePort) : undefined,
126
+ authtoken: options.token,
127
+ region: options.region,
128
+ });
129
+ }
130
+ else {
131
+ await createTunnel({
132
+ protocol: "tcp",
133
+ localHost: options.host,
134
+ localPort: parseInt(port),
135
+ remotePort: options.remotePort ? parseInt(options.remotePort) : undefined,
136
+ serverUrl,
137
+ token: options.token,
138
+ insecure: options.insecure,
139
+ });
140
+ }
141
+ });
142
+ // Quick expose command
143
+ program
144
+ .command("expose <port>")
145
+ .description("Quick expose a local port (auto-detects HTTP)")
146
+ .option("-s, --server <url>", "Server URL")
147
+ .option("-t, --token <token>", "Authentication token")
148
+ .option("-n, --subdomain <name>", "Custom subdomain")
149
+ .option("-d, --detach", "Run tunnel in background")
150
+ .option("-p, --protocol <proto>", "Protocol (http, https, tcp)", "http")
151
+ .option("--domain <domain>", "Server domain (e.g., domain.com)")
152
+ .option("--insecure", "Skip SSL certificate verification (for self-signed certs)")
153
+ .option("--ngrok", "Use ngrok instead of OpenTunnel server")
154
+ .action(async (port, options) => {
155
+ const serverUrl = options.server || (options.domain
156
+ ? `wss://${options.domain}/_tunnel`
157
+ : "ws://localhost:8080/_tunnel");
158
+ if (options.detach) {
159
+ await runTunnelInBackground("expose", port, { ...options, server: serverUrl });
160
+ return;
161
+ }
162
+ if (options.ngrok || options.server === "ngrok") {
163
+ await createNgrokTunnel({
164
+ protocol: options.protocol,
165
+ localHost: "localhost",
166
+ localPort: parseInt(port),
167
+ subdomain: options.subdomain,
168
+ authtoken: options.token,
169
+ });
170
+ }
171
+ else {
172
+ await createTunnel({
173
+ protocol: options.protocol,
174
+ localHost: "localhost",
175
+ localPort: parseInt(port),
176
+ subdomain: options.subdomain,
177
+ serverUrl,
178
+ token: options.token,
179
+ insecure: options.insecure,
180
+ });
181
+ }
182
+ });
183
+ // Server command
184
+ program
185
+ .command("server")
186
+ .description("Start the OpenTunnel server (standalone mode)")
187
+ .option("-p, --port <port>", "Server port", "443")
188
+ .option("--public-port <port>", "Public port shown in URLs (default: same as port)")
189
+ .option("--domain <domain>", "Base domain", "localhost")
190
+ .option("-b, --base-path <path>", "Subdomain base path (e.g., 'op' for *.op.domain.com)", "op")
191
+ .option("--host <host>", "Bind host", "0.0.0.0")
192
+ .option("--tcp-min <port>", "Minimum TCP port", "10000")
193
+ .option("--tcp-max <port>", "Maximum TCP port", "20000")
194
+ .option("--auth-tokens <tokens>", "Comma-separated auth tokens")
195
+ .option("--no-https", "Disable HTTPS (use plain HTTP)")
196
+ .option("--https-cert <path>", "Path to SSL certificate (for custom certs)")
197
+ .option("--https-key <path>", "Path to SSL private key (for custom certs)")
198
+ .option("--letsencrypt", "Use Let's Encrypt instead of self-signed (requires port 80)")
199
+ .option("--email <email>", "Email for Let's Encrypt notifications")
200
+ .option("--production", "Use Let's Encrypt production (default: staging)")
201
+ .option("--cloudflare-token <token>", "Cloudflare API token for DNS-01 challenge")
202
+ .option("--duckdns-token <token>", "DuckDNS token for dynamic DNS updates")
203
+ .option("-d, --detach", "Run server in background (detached mode)")
204
+ .action(async (options) => {
205
+ // Detached mode - run in background
206
+ if (options.detach) {
207
+ const { spawn } = await Promise.resolve().then(() => __importStar(require("child_process")));
208
+ const fs = await Promise.resolve().then(() => __importStar(require("fs")));
209
+ const path = await Promise.resolve().then(() => __importStar(require("path")));
210
+ const pidFile = path.join(process.cwd(), ".opentunnel.pid");
211
+ const logFile = path.join(process.cwd(), "opentunnel.log");
212
+ // Check if already running
213
+ if (fs.existsSync(pidFile)) {
214
+ const oldPid = fs.readFileSync(pidFile, "utf-8").trim();
215
+ try {
216
+ process.kill(parseInt(oldPid), 0);
217
+ console.log(chalk_1.default.yellow(`Server already running (PID: ${oldPid})`));
218
+ console.log(chalk_1.default.gray(`Stop it with: opentunnel stop`));
219
+ return;
220
+ }
221
+ catch {
222
+ fs.unlinkSync(pidFile);
223
+ }
224
+ }
225
+ // Build args without -d flag
226
+ const args = ["server"];
227
+ if (options.port)
228
+ args.push("-p", options.port);
229
+ if (options.publicPort)
230
+ args.push("--public-port", options.publicPort);
231
+ if (options.domain)
232
+ args.push("--domain", options.domain);
233
+ if (options.basePath)
234
+ args.push("-b", options.basePath);
235
+ if (options.host)
236
+ args.push("--host", options.host);
237
+ if (options.tcpMin)
238
+ args.push("--tcp-min", options.tcpMin);
239
+ if (options.tcpMax)
240
+ args.push("--tcp-max", options.tcpMax);
241
+ if (options.authTokens)
242
+ args.push("--auth-tokens", options.authTokens);
243
+ if (options.https)
244
+ args.push("--https");
245
+ if (options.email)
246
+ args.push("--email", options.email);
247
+ if (options.production)
248
+ args.push("--production");
249
+ if (options.cloudflareToken)
250
+ args.push("--cloudflare-token", options.cloudflareToken);
251
+ if (options.duckdnsToken)
252
+ args.push("--duckdns-token", options.duckdnsToken);
253
+ if (options.autoDns)
254
+ args.push("--auto-dns");
255
+ if (options.dnsCreateRecords)
256
+ args.push("--dns-create-records");
257
+ if (options.dnsDeleteOnClose)
258
+ args.push("--dns-delete-on-close");
259
+ const out = fs.openSync(logFile, "a");
260
+ const err = fs.openSync(logFile, "a");
261
+ const child = spawn(process.execPath, [process.argv[1], ...args], {
262
+ detached: true,
263
+ stdio: ["ignore", out, err],
264
+ cwd: process.cwd(),
265
+ });
266
+ child.unref();
267
+ fs.writeFileSync(pidFile, String(child.pid));
268
+ console.log(chalk_1.default.green(`OpenTunnel server started in background`));
269
+ console.log(chalk_1.default.gray(` PID: ${child.pid}`));
270
+ console.log(chalk_1.default.gray(` Port: ${options.port}`));
271
+ console.log(chalk_1.default.gray(` Domain: ${options.domain}`));
272
+ console.log(chalk_1.default.gray(` Log: ${logFile}`));
273
+ console.log(chalk_1.default.gray(` PID file: ${pidFile}`));
274
+ console.log("");
275
+ console.log(chalk_1.default.gray(`Stop with: node dist/cli/index.js stop`));
276
+ console.log(chalk_1.default.gray(`Logs: tail -f ${logFile}`));
277
+ return;
278
+ }
279
+ // Normal foreground mode
280
+ const { TunnelServer } = await Promise.resolve().then(() => __importStar(require("../server/TunnelServer")));
281
+ // Determine HTTPS configuration (self-signed enabled by default)
282
+ let httpsConfig = undefined;
283
+ let selfSignedHttpsConfig = undefined;
284
+ let autoHttpsConfig = undefined;
285
+ if (options.httpsCert && options.httpsKey) {
286
+ // Custom certificates provided
287
+ const fs = await Promise.resolve().then(() => __importStar(require("fs")));
288
+ httpsConfig = {
289
+ cert: fs.readFileSync(options.httpsCert, "utf-8"),
290
+ key: fs.readFileSync(options.httpsKey, "utf-8"),
291
+ };
292
+ }
293
+ else if (options.letsencrypt) {
294
+ // Let's Encrypt
295
+ autoHttpsConfig = {
296
+ enabled: true,
297
+ email: options.email || `admin@${options.domain}`,
298
+ production: options.production || false,
299
+ cloudflareToken: options.cloudflareToken,
300
+ };
301
+ }
302
+ else {
303
+ // Self-signed by default (use --no-https to disable)
304
+ selfSignedHttpsConfig = {
305
+ enabled: options.https !== false,
306
+ };
307
+ }
308
+ const server = new TunnelServer({
309
+ port: parseInt(options.port),
310
+ publicPort: options.publicPort ? parseInt(options.publicPort) : undefined,
311
+ host: options.host,
312
+ domain: options.domain,
313
+ basePath: options.basePath,
314
+ tunnelPortRange: {
315
+ min: parseInt(options.tcpMin),
316
+ max: parseInt(options.tcpMax),
317
+ },
318
+ auth: options.authTokens
319
+ ? { required: true, tokens: options.authTokens.split(",") }
320
+ : undefined,
321
+ https: httpsConfig,
322
+ selfSignedHttps: selfSignedHttpsConfig,
323
+ autoHttps: autoHttpsConfig,
324
+ autoDns: detectDnsConfig(options),
325
+ });
326
+ // Helper function to auto-detect DNS provider
327
+ function detectDnsConfig(opts) {
328
+ // Auto-detect provider based on tokens or domain
329
+ const domain = opts.domain || "localhost";
330
+ const isDuckDnsDomain = domain.endsWith(".duckdns.org");
331
+ // Priority: explicit token > domain detection
332
+ if (opts.cloudflareToken) {
333
+ return {
334
+ enabled: true,
335
+ provider: "cloudflare",
336
+ cloudflareToken: opts.cloudflareToken,
337
+ createRecords: opts.dnsCreateRecords !== false,
338
+ deleteOnClose: opts.dnsDeleteOnClose || false,
339
+ setupWildcard: true,
340
+ };
341
+ }
342
+ if (opts.duckdnsToken || isDuckDnsDomain) {
343
+ return {
344
+ enabled: true,
345
+ provider: "duckdns",
346
+ duckdnsToken: opts.duckdnsToken,
347
+ createRecords: false, // DuckDNS doesn't support subdomains
348
+ deleteOnClose: false,
349
+ setupWildcard: false,
350
+ };
351
+ }
352
+ // No auto DNS if no tokens provided
353
+ if (opts.autoDns) {
354
+ console.log(chalk_1.default.yellow("Warning: --auto-dns requires --cloudflare-token or --duckdns-token"));
355
+ }
356
+ return undefined;
357
+ }
358
+ console.log(chalk_1.default.cyan(`
359
+ ██████╗ ██████╗ ███████╗███╗ ██╗████████╗██╗ ██╗███╗ ██╗███╗ ██╗███████╗██╗
360
+ ██╔═══██╗██╔══██╗██╔════╝████╗ ██║╚══██╔══╝██║ ██║████╗ ██║████╗ ██║██╔════╝██║
361
+ ██║ ██║██████╔╝█████╗ ██╔██╗ ██║ ██║ ██║ ██║██╔██╗ ██║██╔██╗ ██║█████╗ ██║
362
+ ██║ ██║██╔═══╝ ██╔══╝ ██║╚██╗██║ ██║ ██║ ██║██║╚██╗██║██║╚██╗██║██╔══╝ ██║
363
+ ╚██████╔╝██║ ███████╗██║ ╚████║ ██║ ╚██████╔╝██║ ╚████║██║ ╚████║███████╗███████╗
364
+ ╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═══╝╚══════╝╚══════╝
365
+ `));
366
+ server.on("tunnel:created", ({ tunnelId, publicUrl }) => {
367
+ console.log(chalk_1.default.green(`[+] Tunnel created: ${publicUrl}`));
368
+ });
369
+ server.on("tunnel:closed", ({ tunnelId }) => {
370
+ console.log(chalk_1.default.yellow(`[-] Tunnel closed: ${tunnelId}`));
371
+ });
372
+ await server.start();
373
+ console.log(chalk_1.default.green(`\nServer running on ${options.host}:${options.port}`));
374
+ console.log(chalk_1.default.gray(`Domain: ${options.domain}`));
375
+ console.log(chalk_1.default.gray(`Subdomain pattern: *.${options.basePath}.${options.domain}`));
376
+ console.log(chalk_1.default.gray(`TCP port range: ${options.tcpMin}-${options.tcpMax}\n`));
377
+ });
378
+ // Stop command
379
+ program
380
+ .command("stop")
381
+ .description("Stop the OpenTunnel server running in background")
382
+ .action(async () => {
383
+ const fs = await Promise.resolve().then(() => __importStar(require("fs")));
384
+ const path = await Promise.resolve().then(() => __importStar(require("path")));
385
+ const pidFile = path.join(process.cwd(), ".opentunnel.pid");
386
+ if (!fs.existsSync(pidFile)) {
387
+ console.log(chalk_1.default.yellow("No server running (PID file not found)"));
388
+ return;
389
+ }
390
+ const pid = parseInt(fs.readFileSync(pidFile, "utf-8").trim());
391
+ try {
392
+ process.kill(pid, "SIGTERM");
393
+ fs.unlinkSync(pidFile);
394
+ console.log(chalk_1.default.green(`Server stopped (PID: ${pid})`));
395
+ }
396
+ catch (err) {
397
+ if (err.code === "ESRCH") {
398
+ fs.unlinkSync(pidFile);
399
+ console.log(chalk_1.default.yellow(`Server was not running (stale PID file removed)`));
400
+ }
401
+ else {
402
+ console.log(chalk_1.default.red(`Failed to stop server: ${err.message}`));
403
+ }
404
+ }
405
+ });
406
+ // Logs command
407
+ program
408
+ .command("logs")
409
+ .description("Show server logs")
410
+ .option("-f, --follow", "Follow log output")
411
+ .option("-n, --lines <n>", "Number of lines to show", "50")
412
+ .action(async (options) => {
413
+ const fs = await Promise.resolve().then(() => __importStar(require("fs")));
414
+ const path = await Promise.resolve().then(() => __importStar(require("path")));
415
+ const { spawn } = await Promise.resolve().then(() => __importStar(require("child_process")));
416
+ const logFile = path.join(process.cwd(), "opentunnel.log");
417
+ if (!fs.existsSync(logFile)) {
418
+ console.log(chalk_1.default.yellow("No log file found"));
419
+ return;
420
+ }
421
+ if (options.follow) {
422
+ // Use tail -f on Unix or PowerShell on Windows
423
+ const isWindows = process.platform === "win32";
424
+ if (isWindows) {
425
+ const child = spawn("powershell", ["-Command", `Get-Content -Path "${logFile}" -Tail ${options.lines} -Wait`], {
426
+ stdio: "inherit"
427
+ });
428
+ child.on("error", () => {
429
+ // Fallback: just read the file
430
+ console.log(fs.readFileSync(logFile, "utf-8"));
431
+ });
432
+ }
433
+ else {
434
+ spawn("tail", ["-f", "-n", options.lines, logFile], { stdio: "inherit" });
435
+ }
436
+ }
437
+ else {
438
+ const content = fs.readFileSync(logFile, "utf-8");
439
+ const lines = content.split("\n");
440
+ const lastLines = lines.slice(-parseInt(options.lines));
441
+ console.log(lastLines.join("\n"));
442
+ }
443
+ });
444
+ // Status command
445
+ program
446
+ .command("status")
447
+ .description("Check server status")
448
+ .option("-s, --server <url>", "Server URL", "http://localhost:8080")
449
+ .action(async (options) => {
450
+ const spinner = (0, ora_1.default)("Checking server status...").start();
451
+ try {
452
+ const http = await Promise.resolve().then(() => __importStar(require("http")));
453
+ const url = new URL("/api/stats", options.server);
454
+ const response = await new Promise((resolve, reject) => {
455
+ http.get(url.toString(), (res) => {
456
+ let data = "";
457
+ res.on("data", chunk => data += chunk);
458
+ res.on("end", () => resolve(JSON.parse(data)));
459
+ }).on("error", reject);
460
+ });
461
+ spinner.succeed("Server is running");
462
+ console.log(chalk_1.default.gray(` Clients: ${response.clients}`));
463
+ console.log(chalk_1.default.gray(` Tunnels: ${response.tunnels}`));
464
+ console.log(chalk_1.default.gray(` Uptime: ${(0, utils_1.formatDuration)(response.uptime * 1000)}`));
465
+ }
466
+ catch {
467
+ spinner.fail("Server is not reachable");
468
+ }
469
+ });
470
+ // List tunnels command
471
+ program
472
+ .command("list")
473
+ .description("List active tunnels")
474
+ .option("-s, --server <url>", "Server URL", "http://localhost:8080")
475
+ .action(async (options) => {
476
+ const spinner = (0, ora_1.default)("Fetching tunnels...").start();
477
+ try {
478
+ const http = await Promise.resolve().then(() => __importStar(require("http")));
479
+ const url = new URL("/api/tunnels", options.server);
480
+ const response = await new Promise((resolve, reject) => {
481
+ http.get(url.toString(), (res) => {
482
+ let data = "";
483
+ res.on("data", chunk => data += chunk);
484
+ res.on("end", () => resolve(JSON.parse(data)));
485
+ }).on("error", reject);
486
+ });
487
+ spinner.stop();
488
+ if (response.tunnels.length === 0) {
489
+ console.log(chalk_1.default.yellow("No active tunnels"));
490
+ return;
491
+ }
492
+ console.log(chalk_1.default.cyan("\nActive Tunnels:"));
493
+ console.log(chalk_1.default.gray("─".repeat(80)));
494
+ for (const tunnel of response.tunnels) {
495
+ console.log(` ${chalk_1.default.white(tunnel.id)}`);
496
+ console.log(` Protocol: ${chalk_1.default.yellow(tunnel.protocol.toUpperCase())}`);
497
+ console.log(` Local: ${chalk_1.default.gray(tunnel.localAddress)}`);
498
+ console.log(` Public: ${chalk_1.default.green(tunnel.publicUrl)}`);
499
+ console.log(` Traffic: ${chalk_1.default.blue(`↓${(0, utils_1.formatBytes)(tunnel.bytesIn)} ↑${(0, utils_1.formatBytes)(tunnel.bytesOut)}`)}`);
500
+ console.log(chalk_1.default.gray("─".repeat(80)));
501
+ }
502
+ }
503
+ catch (err) {
504
+ spinner.fail("Failed to fetch tunnels");
505
+ }
506
+ });
507
+ // Setup/help command - explains requirements
508
+ program
509
+ .command("setup")
510
+ .description("Show setup instructions for running OpenTunnel on a custom domain")
511
+ .option("--domain <domain>", "Show setup for specific domain")
512
+ .action(async (options) => {
513
+ const domain = options.domain || "yourdomain.com";
514
+ console.log(chalk_1.default.cyan(`
515
+ ╔══════════════════════════════════════════════════════════════════════════════╗
516
+ ║ OpenTunnel Setup Guide ║
517
+ ╚══════════════════════════════════════════════════════════════════════════════╝
518
+ `));
519
+ console.log(chalk_1.default.white.bold("1. SERVER REQUIREMENTS"));
520
+ console.log(chalk_1.default.gray("─".repeat(78)));
521
+ console.log(`
522
+ You need a server (VPS, cloud instance, etc.) with:
523
+ ${chalk_1.default.green("✓")} Public IP address
524
+ ${chalk_1.default.green("✓")} Ports 80 and 443 open (for HTTP/HTTPS)
525
+ ${chalk_1.default.green("✓")} Port 8080 open (for WebSocket tunnel, or use reverse proxy)
526
+ ${chalk_1.default.green("✓")} Node.js 18+ installed
527
+ `);
528
+ // Try to get public IP for the docs
529
+ let serverIP = "<YOUR_SERVER_IP>";
530
+ try {
531
+ const https = await Promise.resolve().then(() => __importStar(require("https")));
532
+ serverIP = await new Promise((resolve) => {
533
+ https.get("https://api.ipify.org", (res) => {
534
+ let data = "";
535
+ res.on("data", (chunk) => (data += chunk));
536
+ res.on("end", () => resolve(data.trim()));
537
+ }).on("error", () => resolve("<YOUR_SERVER_IP>"));
538
+ });
539
+ }
540
+ catch { }
541
+ console.log(chalk_1.default.white.bold("2. DNS CONFIGURATION"));
542
+ console.log(chalk_1.default.gray("─".repeat(78)));
543
+ console.log(`
544
+ ${chalk_1.default.yellow.bold("Your server's public IP:")} ${chalk_1.default.green(serverIP)}
545
+
546
+ ${chalk_1.default.white.bold("Required DNS Records (create in Cloudflare/your DNS provider):")}
547
+
548
+ ┌──────────┬─────────────────────────┬─────────────────────┬──────────────┐
549
+ │ ${chalk_1.default.cyan("Type")} │ ${chalk_1.default.cyan("Name")} │ ${chalk_1.default.cyan("Content")} │ ${chalk_1.default.cyan("Proxy")} │
550
+ ├──────────┼─────────────────────────┼─────────────────────┼──────────────┤
551
+ │ ${chalk_1.default.yellow("A")} │ op.${domain.padEnd(20)} │ ${serverIP.padEnd(19)} │ ${chalk_1.default.red("OFF (DNS only)")} │
552
+ │ ${chalk_1.default.yellow("A")} │ *.op.${domain.padEnd(18)} │ ${serverIP.padEnd(19)} │ ${chalk_1.default.red("OFF (DNS only)")} │
553
+ └──────────┴─────────────────────────┴─────────────────────┴──────────────┘
554
+
555
+ ${chalk_1.default.yellow("⚠ IMPORTANT: Disable Cloudflare Proxy (gray cloud, not orange)")}
556
+ ${chalk_1.default.gray(" - Proxy OFF = DNS only (gray cloud) ← Use this")}
557
+ ${chalk_1.default.gray(" - Proxy ON = Proxied (orange cloud) ← Don't use")}
558
+
559
+ ${chalk_1.default.gray("Why? WebSocket tunnels and TCP don't work well through Cloudflare's proxy.")}
560
+
561
+ ${chalk_1.default.gray("─".repeat(40))}
562
+
563
+ ${chalk_1.default.green("Option A: Automatic DNS with Cloudflare (Recommended)")}
564
+ ${chalk_1.default.gray("If you provide a Cloudflare token, OpenTunnel will create these records automatically!")}
565
+
566
+ ${chalk_1.default.cyan("Option B: Manual DNS setup")}
567
+ ${chalk_1.default.gray("Create the records above manually in your DNS provider.")}
568
+
569
+ ${chalk_1.default.gray("After DNS propagation, these URLs will work:")}
570
+ ${chalk_1.default.green(` https://op.${domain}`)} ${chalk_1.default.gray("← Server dashboard")}
571
+ ${chalk_1.default.green(` https://myapp.op.${domain}`)} ${chalk_1.default.gray("← Your tunnel")}
572
+ ${chalk_1.default.green(` https://api.op.${domain}`)} ${chalk_1.default.gray("← Another tunnel")}
573
+ ${chalk_1.default.green(` https://anything.op.${domain}`)} ${chalk_1.default.gray("← Any subdomain")}
574
+
575
+ ${chalk_1.default.yellow("Tip:")} You can change 'op' to any prefix you prefer (e.g., 'tunnel', 't', etc.)
576
+ `);
577
+ console.log(chalk_1.default.white.bold("3. SERVER SETUP (Automatic HTTPS + DNS)"));
578
+ console.log(chalk_1.default.gray("─".repeat(78)));
579
+ console.log(`
580
+ On your server, run:
581
+
582
+ ${chalk_1.default.cyan("# Clone and install")}
583
+ git clone https://github.com/your-repo/opentunnel.git
584
+ cd opentunnel
585
+ npm install && npm run build
586
+
587
+ ${chalk_1.default.cyan("# Option A: Full automatic setup with Cloudflare (Recommended)")}
588
+ ${chalk_1.default.gray("Get your Cloudflare API token from: https://dash.cloudflare.com/profile/api-tokens")}
589
+ ${chalk_1.default.gray("Required permissions: Zone:DNS:Edit")}
590
+
591
+ ${chalk_1.default.green(`sudo node dist/cli/index.js server \\
592
+ --domain ${domain} \\
593
+ --https \\
594
+ --email admin@${domain} \\
595
+ --cloudflare-token YOUR_CF_API_TOKEN \\
596
+ --production -d`)}
597
+
598
+ ${chalk_1.default.gray("This will automatically:")}
599
+ ${chalk_1.default.green("✓")} Create DNS records (*.op.${domain} and op.${domain})
600
+ ${chalk_1.default.green("✓")} Obtain wildcard SSL certificate
601
+ ${chalk_1.default.green("✓")} Use DNS-01 challenge (no port 80 needed during setup)
602
+ ${chalk_1.default.green("✓")} Listen on port 443 (HTTPS)
603
+ ${chalk_1.default.green("✓")} Redirect HTTP (80) to HTTPS
604
+ ${chalk_1.default.green("✓")} Auto-renew certificates
605
+ ${chalk_1.default.green("✓")} Create individual DNS records per tunnel
606
+
607
+ ${chalk_1.default.cyan("# Option B: DNS only (no HTTPS)")}
608
+ ${chalk_1.default.green(`node dist/cli/index.js server --domain ${domain} --cloudflare-token YOUR_CF_TOKEN -d`)}
609
+ ${chalk_1.default.gray("Creates DNS records automatically, HTTP only (port 8080)")}
610
+
611
+ ${chalk_1.default.cyan("# Option C: Manual DNS + HTTPS")}
612
+ ${chalk_1.default.yellow("Note: Requires manual DNS wildcard record setup")}
613
+ ${chalk_1.default.green(`sudo node dist/cli/index.js server --domain ${domain} --https --email admin@${domain} --production -d`)}
614
+
615
+ ${chalk_1.default.cyan("# Option D: Testing/local (no HTTPS, no auto DNS)")}
616
+ node dist/cli/index.js server --domain ${domain} -d
617
+
618
+ ${chalk_1.default.cyan("# With authentication")}
619
+ sudo node dist/cli/index.js server --domain ${domain} --https --cloudflare-token CF_TOKEN --auth-tokens "secret" -d
620
+
621
+ ${chalk_1.default.yellow("Note: Use 'sudo' for ports 80/443. Or run without --https on port 8080.")}
622
+ `);
623
+ console.log(chalk_1.default.white.bold("4. REVERSE PROXY (Optional - only if needed)"));
624
+ console.log(chalk_1.default.gray("─".repeat(78)));
625
+ console.log(`
626
+ ${chalk_1.default.green("OpenTunnel handles HTTPS automatically!")}
627
+ ${chalk_1.default.gray("Only use a reverse proxy if you need additional features.")}
628
+
629
+ ${chalk_1.default.cyan("# If you prefer using Caddy:")}
630
+ ${domain}, *.op.${domain} {
631
+ reverse_proxy localhost:8080
632
+ }
633
+
634
+ ${chalk_1.default.cyan("# If you prefer Nginx (requires manual cert setup):")}
635
+ server {
636
+ listen 443 ssl;
637
+ server_name ${domain} *.op.${domain};
638
+ ssl_certificate /etc/letsencrypt/live/${domain}/fullchain.pem;
639
+ ssl_certificate_key /etc/letsencrypt/live/${domain}/privkey.pem;
640
+ location / {
641
+ proxy_pass http://localhost:8080;
642
+ proxy_http_version 1.1;
643
+ proxy_set_header Upgrade $http_upgrade;
644
+ proxy_set_header Connection "upgrade";
645
+ proxy_set_header Host $host;
646
+ }
647
+ }
648
+ `);
649
+ console.log(chalk_1.default.white.bold("5. CLIENT USAGE"));
650
+ console.log(chalk_1.default.gray("─".repeat(78)));
651
+ console.log(`
652
+ From your local machine:
653
+
654
+ ${chalk_1.default.cyan("# Connect to your server")}
655
+ opentunnel http 3000 --subdomain myapp --domain ${domain}
656
+
657
+ ${chalk_1.default.cyan("# Your local port 3000 will be available at:")}
658
+ ${chalk_1.default.green(`https://myapp.op.${domain}`)}
659
+
660
+ ${chalk_1.default.cyan("# With authentication token")}
661
+ opentunnel http 3000 -n myapp --domain ${domain} -t "secret-token"
662
+ `);
663
+ console.log(chalk_1.default.white.bold("6. PORT FORWARDING (If behind NAT/Router)"));
664
+ console.log(chalk_1.default.gray("─".repeat(78)));
665
+ console.log(`
666
+ If your server is behind a router:
667
+
668
+ ${chalk_1.default.yellow("Router Settings → Port Forwarding:")}
669
+ ┌────────────────┬───────────────┬────────────────┐
670
+ │ External Port │ Internal Port │ Protocol │
671
+ ├────────────────┼───────────────┼────────────────┤
672
+ │ 80 │ 80 │ TCP │
673
+ │ 443 │ 443 │ TCP │
674
+ │ 8080 │ 8080 │ TCP │
675
+ │ 10000-20000 │ 10000-20000 │ TCP (for TCP) │
676
+ └────────────────┴───────────────┴────────────────┘
677
+ `);
678
+ console.log(chalk_1.default.white.bold("7. VERIFY SETUP"));
679
+ console.log(chalk_1.default.gray("─".repeat(78)));
680
+ console.log(`
681
+ ${chalk_1.default.cyan("# Check DNS propagation")}
682
+ nslookup ${domain}
683
+ nslookup test.op.${domain}
684
+
685
+ ${chalk_1.default.cyan("# Test server connection")}
686
+ curl -I https://${domain}
687
+
688
+ ${chalk_1.default.cyan("# Test WebSocket endpoint")}
689
+ curl -I https://${domain}/_tunnel
690
+ `);
691
+ console.log(chalk_1.default.gray("─".repeat(78)));
692
+ console.log(chalk_1.default.green("\nNeed help? https://github.com/your-repo/opentunnel/issues\n"));
693
+ });
694
+ // Init command - create example config
695
+ program
696
+ .command("init")
697
+ .description("Create an example opentunnel.yml configuration file")
698
+ .option("-f, --force", "Overwrite existing config file")
699
+ .action(async (options) => {
700
+ const fs = await Promise.resolve().then(() => __importStar(require("fs")));
701
+ const path = await Promise.resolve().then(() => __importStar(require("path")));
702
+ const configPath = path.join(process.cwd(), CONFIG_FILE);
703
+ if (fs.existsSync(configPath) && !options.force) {
704
+ console.log(chalk_1.default.yellow(`Config file already exists: ${configPath}`));
705
+ console.log(chalk_1.default.gray("Use --force to overwrite"));
706
+ return;
707
+ }
708
+ const exampleConfig = {
709
+ version: "1.0",
710
+ server: {
711
+ url: "ws://localhost:8080/_tunnel",
712
+ // token: "your-auth-token",
713
+ },
714
+ tunnels: [
715
+ {
716
+ name: "web",
717
+ protocol: "http",
718
+ port: 3000,
719
+ subdomain: "web",
720
+ autostart: true,
721
+ },
722
+ {
723
+ name: "api",
724
+ protocol: "http",
725
+ port: 4000,
726
+ subdomain: "api",
727
+ autostart: true,
728
+ },
729
+ {
730
+ name: "database",
731
+ protocol: "tcp",
732
+ port: 5432,
733
+ autostart: false,
734
+ },
735
+ ],
736
+ };
737
+ fs.writeFileSync(configPath, (0, yaml_1.stringify)(exampleConfig, { indent: 2 }));
738
+ console.log(chalk_1.default.green(`Created ${CONFIG_FILE}`));
739
+ console.log(chalk_1.default.gray(`\nEdit the file to configure your tunnels, then run:`));
740
+ console.log(chalk_1.default.cyan(` opentunnel up # Start all tunnels`));
741
+ console.log(chalk_1.default.cyan(` opentunnel up -d # Start in background`));
742
+ });
743
+ // Up command - start tunnels from config (like docker-compose up)
744
+ program
745
+ .command("up")
746
+ .description("Start server and tunnels from opentunnel.yml (like docker-compose up)")
747
+ .option("-d, --detach", "Run in background (detached mode)")
748
+ .option("-f, --file <path>", "Config file path", CONFIG_FILE)
749
+ .option("--no-autostart", "Ignore autostart setting, start all tunnels")
750
+ .action(async (options) => {
751
+ const fs = await Promise.resolve().then(() => __importStar(require("fs")));
752
+ const path = await Promise.resolve().then(() => __importStar(require("path")));
753
+ const { TunnelServer } = await Promise.resolve().then(() => __importStar(require("../server/TunnelServer")));
754
+ // Load config file
755
+ const configPath = path.join(process.cwd(), options.file);
756
+ let config = { version: "1.0", tunnels: [] };
757
+ if (fs.existsSync(configPath)) {
758
+ const configContent = fs.readFileSync(configPath, "utf-8");
759
+ config = (0, yaml_1.parse)(configContent);
760
+ }
761
+ else {
762
+ console.log(chalk_1.default.red(`Config file not found: ${configPath}`));
763
+ console.log(chalk_1.default.gray(`Run 'opentunnel init' to create one`));
764
+ return;
765
+ }
766
+ const tunnelsToStart = options.autostart === false
767
+ ? config.tunnels
768
+ : config.tunnels?.filter(t => t.autostart !== false) || [];
769
+ // Get server config from yml
770
+ const port = config.server?.port || 443;
771
+ const domain = config.server?.domain || "localhost";
772
+ const basePath = config.server?.basePath || "op";
773
+ const useHttps = config.server?.https !== false;
774
+ const tcpMin = config.server?.tcpPortMin || 10000;
775
+ const tcpMax = config.server?.tcpPortMax || 20000;
776
+ // Display banner
777
+ console.log(chalk_1.default.cyan(`
778
+ ██████╗ ██████╗ ███████╗███╗ ██╗████████╗██╗ ██╗███╗ ██╗███╗ ██╗███████╗██╗
779
+ ██╔═══██╗██╔══██╗██╔════╝████╗ ██║╚══██╔══╝██║ ██║████╗ ██║████╗ ██║██╔════╝██║
780
+ ██║ ██║██████╔╝█████╗ ██╔██╗ ██║ ██║ ██║ ██║██╔██╗ ██║██╔██╗ ██║█████╗ ██║
781
+ ██║ ██║██╔═══╝ ██╔══╝ ██║╚██╗██║ ██║ ██║ ██║██║╚██╗██║██║╚██╗██║██╔══╝ ██║
782
+ ╚██████╔╝██║ ███████╗██║ ╚████║ ██║ ╚██████╔╝██║ ╚████║██║ ╚████║███████╗███████╗
783
+ ╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═══╝╚══════╝╚══════╝
784
+ `));
785
+ // Start server
786
+ const spinner = (0, ora_1.default)("Starting server...").start();
787
+ const server = new TunnelServer({
788
+ port,
789
+ host: "0.0.0.0",
790
+ domain,
791
+ basePath,
792
+ tunnelPortRange: {
793
+ min: tcpMin,
794
+ max: tcpMax,
795
+ },
796
+ selfSignedHttps: useHttps ? { enabled: true } : undefined,
797
+ });
798
+ try {
799
+ await server.start();
800
+ spinner.succeed(`Server running on https://${basePath}.${domain}:${port}`);
801
+ // Start tunnels if defined
802
+ if (tunnelsToStart.length > 0) {
803
+ console.log(chalk_1.default.cyan(`\nStarting ${tunnelsToStart.length} tunnel(s)...\n`));
804
+ const protocol = useHttps ? "wss" : "ws";
805
+ const serverUrl = `${protocol}://localhost:${port}/_tunnel`;
806
+ // Small delay to ensure server is fully ready
807
+ await new Promise(resolve => setTimeout(resolve, 500));
808
+ await startTunnelsFromConfig(tunnelsToStart, serverUrl, config.server?.token, true);
809
+ }
810
+ else {
811
+ console.log(chalk_1.default.yellow("\nNo tunnels defined in config"));
812
+ }
813
+ console.log(chalk_1.default.gray("\nPress Ctrl+C to stop"));
814
+ // Keep running
815
+ await new Promise(() => { });
816
+ }
817
+ catch (error) {
818
+ spinner.fail(`Failed to start: ${error.message}`);
819
+ process.exit(1);
820
+ }
821
+ });
822
+ // Down command - stop all tunnels (like docker-compose down)
823
+ program
824
+ .command("down")
825
+ .description("Stop all running tunnels (like docker-compose down)")
826
+ .action(async () => {
827
+ const fs = await Promise.resolve().then(() => __importStar(require("fs")));
828
+ const path = await Promise.resolve().then(() => __importStar(require("path")));
829
+ // Find all tunnel PID files
830
+ const pidFiles = fs.readdirSync(process.cwd())
831
+ .filter(f => f.startsWith(".opentunnel-") && f.endsWith(".pid"));
832
+ // Also include the server PID
833
+ const serverPidFile = ".opentunnel.pid";
834
+ if (fs.existsSync(path.join(process.cwd(), serverPidFile))) {
835
+ pidFiles.push(serverPidFile);
836
+ }
837
+ if (pidFiles.length === 0) {
838
+ console.log(chalk_1.default.yellow("No tunnels running"));
839
+ return;
840
+ }
841
+ console.log(chalk_1.default.cyan(`Stopping ${pidFiles.length} process(es)...\n`));
842
+ for (const pidFile of pidFiles) {
843
+ const pidPath = path.join(process.cwd(), pidFile);
844
+ const pid = parseInt(fs.readFileSync(pidPath, "utf-8").trim());
845
+ const name = pidFile.replace(".opentunnel-", "").replace(".pid", "").replace(".opentunnel", "server");
846
+ try {
847
+ process.kill(pid, "SIGTERM");
848
+ fs.unlinkSync(pidPath);
849
+ console.log(chalk_1.default.green(` ✓ Stopped ${name} (PID: ${pid})`));
850
+ }
851
+ catch (err) {
852
+ if (err.code === "ESRCH") {
853
+ fs.unlinkSync(pidPath);
854
+ console.log(chalk_1.default.yellow(` - ${name} was not running (cleaned up)`));
855
+ }
856
+ else {
857
+ console.log(chalk_1.default.red(` ✗ Failed to stop ${name}: ${err.message}`));
858
+ }
859
+ }
860
+ }
861
+ // Clean up log files
862
+ const logFiles = fs.readdirSync(process.cwd())
863
+ .filter(f => f.startsWith("opentunnel") && f.endsWith(".log"));
864
+ if (logFiles.length > 0) {
865
+ console.log(chalk_1.default.gray(`\nLog files preserved: ${logFiles.join(", ")}`));
866
+ }
867
+ console.log(chalk_1.default.green("\nAll tunnels stopped"));
868
+ });
869
+ // PS command - list running tunnel processes (like docker ps)
870
+ program
871
+ .command("ps")
872
+ .description("List running tunnel processes (like docker ps)")
873
+ .action(async () => {
874
+ const fs = await Promise.resolve().then(() => __importStar(require("fs")));
875
+ const path = await Promise.resolve().then(() => __importStar(require("path")));
876
+ const pidFiles = fs.readdirSync(process.cwd())
877
+ .filter(f => f.startsWith(".opentunnel") && f.endsWith(".pid"));
878
+ if (pidFiles.length === 0) {
879
+ console.log(chalk_1.default.yellow("No tunnels running"));
880
+ console.log(chalk_1.default.gray("Start tunnels with: opentunnel up -d"));
881
+ return;
882
+ }
883
+ console.log(chalk_1.default.cyan("\nRunning Processes:"));
884
+ console.log(chalk_1.default.gray("─".repeat(60)));
885
+ console.log(chalk_1.default.gray(` ${"NAME".padEnd(20)} ${"PID".padEnd(10)} ${"STATUS".padEnd(10)}`));
886
+ console.log(chalk_1.default.gray("─".repeat(60)));
887
+ for (const pidFile of pidFiles) {
888
+ const pidPath = path.join(process.cwd(), pidFile);
889
+ const pid = parseInt(fs.readFileSync(pidPath, "utf-8").trim());
890
+ let name = pidFile.replace(".opentunnel-", "").replace(".pid", "").replace(".opentunnel", "");
891
+ if (name === "")
892
+ name = "server";
893
+ let status = "unknown";
894
+ let statusColor = chalk_1.default.gray;
895
+ try {
896
+ process.kill(pid, 0); // Check if process exists
897
+ status = "running";
898
+ statusColor = chalk_1.default.green;
899
+ }
900
+ catch {
901
+ status = "stopped";
902
+ statusColor = chalk_1.default.red;
903
+ }
904
+ console.log(` ${chalk_1.default.white(name.padEnd(20))} ${chalk_1.default.gray(String(pid).padEnd(10))} ${statusColor(status)}`);
905
+ }
906
+ console.log(chalk_1.default.gray("─".repeat(60)));
907
+ console.log(chalk_1.default.gray(`\nStop all: opentunnel down`));
908
+ });
909
+ // Test server command - simple HTTP server for testing tunnels
910
+ program
911
+ .command("test-server")
912
+ .description("Start a simple HTTP test server")
913
+ .option("-p, --port <port>", "Port to listen on", "3000")
914
+ .option("-d, --detach", "Run in background")
915
+ .action(async (options) => {
916
+ const http = await Promise.resolve().then(() => __importStar(require("http")));
917
+ const port = parseInt(options.port);
918
+ if (options.detach) {
919
+ const { spawn } = await Promise.resolve().then(() => __importStar(require("child_process")));
920
+ const fs = await Promise.resolve().then(() => __importStar(require("fs")));
921
+ const path = await Promise.resolve().then(() => __importStar(require("path")));
922
+ const pidFile = path.join(process.cwd(), `.test-server-${port}.pid`);
923
+ const logFile = path.join(process.cwd(), `test-server-${port}.log`);
924
+ if (fs.existsSync(pidFile)) {
925
+ const oldPid = fs.readFileSync(pidFile, "utf-8").trim();
926
+ try {
927
+ process.kill(parseInt(oldPid), 0);
928
+ console.log(chalk_1.default.yellow(`Test server already running on port ${port} (PID: ${oldPid})`));
929
+ return;
930
+ }
931
+ catch {
932
+ fs.unlinkSync(pidFile);
933
+ }
934
+ }
935
+ const out = fs.openSync(logFile, "a");
936
+ const err = fs.openSync(logFile, "a");
937
+ const child = spawn(process.execPath, [process.argv[1], "test-server", "-p", String(port)], {
938
+ detached: true,
939
+ stdio: ["ignore", out, err],
940
+ cwd: process.cwd(),
941
+ });
942
+ child.unref();
943
+ fs.writeFileSync(pidFile, String(child.pid));
944
+ console.log(chalk_1.default.green(`Test server started on port ${port}`));
945
+ console.log(chalk_1.default.gray(` PID: ${child.pid}`));
946
+ console.log(chalk_1.default.gray(` URL: http://localhost:${port}`));
947
+ console.log(chalk_1.default.gray(` Log: ${logFile}`));
948
+ return;
949
+ }
950
+ const server = http.createServer((req, res) => {
951
+ const timestamp = new Date().toISOString();
952
+ const method = req.method;
953
+ const url = req.url;
954
+ const headers = JSON.stringify(req.headers, null, 2);
955
+ console.log(chalk_1.default.cyan(`[${timestamp}] ${method} ${url}`));
956
+ // Collect body for POST/PUT
957
+ let body = "";
958
+ req.on("data", (chunk) => { body += chunk; });
959
+ req.on("end", () => {
960
+ const response = {
961
+ success: true,
962
+ message: "OpenTunnel Test Server",
963
+ request: {
964
+ method,
965
+ url,
966
+ headers: req.headers,
967
+ body: body || undefined,
968
+ },
969
+ server: {
970
+ port,
971
+ timestamp,
972
+ uptime: process.uptime(),
973
+ },
974
+ };
975
+ res.writeHead(200, {
976
+ "Content-Type": "application/json",
977
+ "X-Powered-By": "OpenTunnel Test Server",
978
+ });
979
+ res.end(JSON.stringify(response, null, 2));
980
+ });
981
+ });
982
+ server.listen(port, () => {
983
+ console.log(chalk_1.default.green(`\n OpenTunnel Test Server`));
984
+ console.log(chalk_1.default.gray(" ─────────────────────────────────────────"));
985
+ console.log(` ${chalk_1.default.white("Status:")} ${chalk_1.default.green("● Running")}`);
986
+ console.log(` ${chalk_1.default.white("Port:")} ${chalk_1.default.cyan(port)}`);
987
+ console.log(` ${chalk_1.default.white("URL:")} ${chalk_1.default.cyan(`http://localhost:${port}`)}`);
988
+ console.log(chalk_1.default.gray(" ─────────────────────────────────────────"));
989
+ console.log(chalk_1.default.gray("\n Endpoints:"));
990
+ console.log(chalk_1.default.gray(` GET / → Returns server info`));
991
+ console.log(chalk_1.default.gray(` GET /health → Health check`));
992
+ console.log(chalk_1.default.gray(` POST /echo → Echo request body`));
993
+ console.log(chalk_1.default.gray(` ANY /* → Returns request details`));
994
+ console.log(chalk_1.default.gray("\n Press Ctrl+C to stop\n"));
995
+ });
996
+ process.on("SIGINT", () => {
997
+ console.log(chalk_1.default.yellow("\n Shutting down..."));
998
+ server.close(() => {
999
+ console.log(chalk_1.default.green(" Test server stopped"));
1000
+ process.exit(0);
1001
+ });
1002
+ });
1003
+ });
1004
+ // Stop test servers command
1005
+ program
1006
+ .command("test-server-stop")
1007
+ .description("Stop all test servers")
1008
+ .option("-p, --port <port>", "Stop specific port")
1009
+ .action(async (options) => {
1010
+ const fs = await Promise.resolve().then(() => __importStar(require("fs")));
1011
+ const path = await Promise.resolve().then(() => __importStar(require("path")));
1012
+ let pidFiles;
1013
+ if (options.port) {
1014
+ const specific = `.test-server-${options.port}.pid`;
1015
+ pidFiles = fs.existsSync(path.join(process.cwd(), specific)) ? [specific] : [];
1016
+ }
1017
+ else {
1018
+ pidFiles = fs.readdirSync(process.cwd())
1019
+ .filter(f => f.startsWith(".test-server-") && f.endsWith(".pid"));
1020
+ }
1021
+ if (pidFiles.length === 0) {
1022
+ console.log(chalk_1.default.yellow("No test servers running"));
1023
+ return;
1024
+ }
1025
+ for (const pidFile of pidFiles) {
1026
+ const pidPath = path.join(process.cwd(), pidFile);
1027
+ const pid = parseInt(fs.readFileSync(pidPath, "utf-8").trim());
1028
+ const port = pidFile.replace(".test-server-", "").replace(".pid", "");
1029
+ try {
1030
+ process.kill(pid, "SIGTERM");
1031
+ fs.unlinkSync(pidPath);
1032
+ console.log(chalk_1.default.green(` ✓ Stopped test server on port ${port} (PID: ${pid})`));
1033
+ }
1034
+ catch (err) {
1035
+ if (err.code === "ESRCH") {
1036
+ fs.unlinkSync(pidPath);
1037
+ console.log(chalk_1.default.yellow(` - Test server on port ${port} was not running`));
1038
+ }
1039
+ }
1040
+ }
1041
+ });
1042
+ async function runTunnelInBackgroundFromConfig(name, protocol, port, options) {
1043
+ const { spawn } = await Promise.resolve().then(() => __importStar(require("child_process")));
1044
+ const fs = await Promise.resolve().then(() => __importStar(require("fs")));
1045
+ const path = await Promise.resolve().then(() => __importStar(require("path")));
1046
+ const pidFile = path.join(process.cwd(), `.opentunnel-${name}.pid`);
1047
+ const logFile = path.join(process.cwd(), `opentunnel-${name}.log`);
1048
+ // Check if already running
1049
+ if (fs.existsSync(pidFile)) {
1050
+ const oldPid = fs.readFileSync(pidFile, "utf-8").trim();
1051
+ try {
1052
+ process.kill(parseInt(oldPid), 0);
1053
+ console.log(chalk_1.default.yellow(` - ${name}: already running (PID: ${oldPid})`));
1054
+ return;
1055
+ }
1056
+ catch {
1057
+ fs.unlinkSync(pidFile);
1058
+ }
1059
+ }
1060
+ // Build args
1061
+ const args = [protocol, String(port)];
1062
+ if (options.subdomain)
1063
+ args.push("-n", options.subdomain);
1064
+ if (options.server)
1065
+ args.push("-s", options.server);
1066
+ if (options.token)
1067
+ args.push("-t", options.token);
1068
+ if (options.host)
1069
+ args.push("-h", options.host);
1070
+ if (options.remotePort)
1071
+ args.push("-r", String(options.remotePort));
1072
+ const out = fs.openSync(logFile, "a");
1073
+ const err = fs.openSync(logFile, "a");
1074
+ const child = spawn(process.execPath, [process.argv[1], ...args], {
1075
+ detached: true,
1076
+ stdio: ["ignore", out, err],
1077
+ cwd: process.cwd(),
1078
+ });
1079
+ child.unref();
1080
+ fs.writeFileSync(pidFile, String(child.pid));
1081
+ console.log(chalk_1.default.green(` ✓ ${name}: started (PID: ${child.pid})`));
1082
+ }
1083
+ async function startTunnelsFromConfig(tunnels, serverUrl, token, insecure) {
1084
+ const spinner = (0, ora_1.default)("Connecting to server...").start();
1085
+ const client = new TunnelClient_1.TunnelClient({
1086
+ serverUrl,
1087
+ token,
1088
+ reconnect: true,
1089
+ silent: true,
1090
+ rejectUnauthorized: !insecure,
1091
+ });
1092
+ try {
1093
+ await client.connect();
1094
+ spinner.succeed("Connected to server");
1095
+ const activeTunnels = [];
1096
+ for (const tunnel of tunnels) {
1097
+ const tunnelSpinner = (0, ora_1.default)(`Creating tunnel: ${tunnel.name}...`).start();
1098
+ try {
1099
+ const { tunnelId, publicUrl } = await client.createTunnel({
1100
+ protocol: tunnel.protocol,
1101
+ localHost: tunnel.host || "localhost",
1102
+ localPort: tunnel.port,
1103
+ subdomain: tunnel.subdomain,
1104
+ remotePort: tunnel.remotePort,
1105
+ });
1106
+ activeTunnels.push({ name: tunnel.name, tunnelId, publicUrl });
1107
+ tunnelSpinner.succeed(`${tunnel.name}: ${publicUrl}`);
1108
+ }
1109
+ catch (err) {
1110
+ tunnelSpinner.fail(`${tunnel.name}: ${err.message}`);
1111
+ }
1112
+ }
1113
+ if (activeTunnels.length === 0) {
1114
+ console.log(chalk_1.default.red("\nNo tunnels created"));
1115
+ process.exit(1);
1116
+ }
1117
+ console.log(chalk_1.default.cyan("\n─────────────────────────────────────────"));
1118
+ console.log(chalk_1.default.green(` ${activeTunnels.length} tunnel(s) active`));
1119
+ console.log(chalk_1.default.cyan("─────────────────────────────────────────\n"));
1120
+ for (const t of activeTunnels) {
1121
+ console.log(` ${chalk_1.default.white(t.name.padEnd(15))} ${chalk_1.default.green(t.publicUrl)}`);
1122
+ }
1123
+ console.log(chalk_1.default.gray("\n Press Ctrl+C to stop all tunnels\n"));
1124
+ // Keep alive with uptime counter
1125
+ const startTime = Date.now();
1126
+ const statsInterval = setInterval(() => {
1127
+ const uptime = (0, utils_1.formatDuration)(Date.now() - startTime);
1128
+ process.stdout.write(`\r ${chalk_1.default.gray(`Uptime: ${uptime}`)}`);
1129
+ }, 1000);
1130
+ // Handle exit
1131
+ const cleanup = async () => {
1132
+ clearInterval(statsInterval);
1133
+ console.log("\n");
1134
+ const closeSpinner = (0, ora_1.default)("Closing tunnels...").start();
1135
+ for (const t of activeTunnels) {
1136
+ await client.closeTunnel(t.tunnelId);
1137
+ }
1138
+ await client.disconnect();
1139
+ closeSpinner.succeed("All tunnels closed");
1140
+ process.exit(0);
1141
+ };
1142
+ process.on("SIGINT", cleanup);
1143
+ process.on("SIGTERM", cleanup);
1144
+ // Handle reconnection
1145
+ client.on("disconnected", () => {
1146
+ console.log(chalk_1.default.yellow("\n Disconnected, reconnecting..."));
1147
+ });
1148
+ client.on("connected", () => {
1149
+ console.log(chalk_1.default.green(" Reconnected!"));
1150
+ });
1151
+ }
1152
+ catch (err) {
1153
+ spinner.fail(`Failed: ${err.message}`);
1154
+ process.exit(1);
1155
+ }
1156
+ }
1157
+ async function runTunnelInBackground(command, port, options) {
1158
+ const { spawn } = await Promise.resolve().then(() => __importStar(require("child_process")));
1159
+ const fs = await Promise.resolve().then(() => __importStar(require("fs")));
1160
+ const path = await Promise.resolve().then(() => __importStar(require("path")));
1161
+ const tunnelId = `tunnel-${port}-${Date.now()}`;
1162
+ const pidFile = path.join(process.cwd(), `.opentunnel-${port}.pid`);
1163
+ const logFile = path.join(process.cwd(), `opentunnel-${port}.log`);
1164
+ // Check if already running
1165
+ if (fs.existsSync(pidFile)) {
1166
+ const oldPid = fs.readFileSync(pidFile, "utf-8").trim();
1167
+ try {
1168
+ process.kill(parseInt(oldPid), 0);
1169
+ console.log(chalk_1.default.yellow(`Tunnel already running on port ${port} (PID: ${oldPid})`));
1170
+ console.log(chalk_1.default.gray(`Stop it with: kill ${oldPid}`));
1171
+ return;
1172
+ }
1173
+ catch {
1174
+ fs.unlinkSync(pidFile);
1175
+ }
1176
+ }
1177
+ // Build args without -d flag
1178
+ const args = [command, port];
1179
+ if (options.subdomain)
1180
+ args.push("-n", options.subdomain);
1181
+ if (options.server)
1182
+ args.push("-s", options.server);
1183
+ if (options.token)
1184
+ args.push("-t", options.token);
1185
+ if (options.host)
1186
+ args.push("-h", options.host);
1187
+ if (options.https)
1188
+ args.push("--https");
1189
+ if (options.ngrok)
1190
+ args.push("--ngrok");
1191
+ if (options.region)
1192
+ args.push("--region", options.region);
1193
+ if (options.remotePort)
1194
+ args.push("-r", options.remotePort);
1195
+ if (options.protocol)
1196
+ args.push("-p", options.protocol);
1197
+ const out = fs.openSync(logFile, "a");
1198
+ const err = fs.openSync(logFile, "a");
1199
+ const child = spawn(process.execPath, [process.argv[1], ...args], {
1200
+ detached: true,
1201
+ stdio: ["ignore", out, err],
1202
+ cwd: process.cwd(),
1203
+ });
1204
+ child.unref();
1205
+ fs.writeFileSync(pidFile, String(child.pid));
1206
+ // Extract domain from server URL for display
1207
+ const serverUrl = options.server || "ws://localhost:8080/_tunnel";
1208
+ let displayDomain = "localhost:8080";
1209
+ let expectedUrl = `http://${options.subdomain || "random"}.op.localhost:8080`;
1210
+ try {
1211
+ const url = new URL(serverUrl.replace("wss://", "https://").replace("ws://", "http://"));
1212
+ displayDomain = url.host;
1213
+ const isSecure = serverUrl.startsWith("wss://");
1214
+ const protocol = isSecure ? "https" : "http";
1215
+ const subdomain = options.subdomain || "<random>";
1216
+ expectedUrl = `${protocol}://${subdomain}.op.${url.hostname}`;
1217
+ if ((isSecure && url.port && url.port !== "443") || (!isSecure && url.port && url.port !== "80")) {
1218
+ expectedUrl += `:${url.port}`;
1219
+ }
1220
+ }
1221
+ catch { }
1222
+ console.log(chalk_1.default.green(`Tunnel started in background`));
1223
+ console.log(chalk_1.default.gray(" ─────────────────────────────────────────"));
1224
+ console.log(` ${chalk_1.default.white("PID:")} ${chalk_1.default.cyan(child.pid)}`);
1225
+ console.log(` ${chalk_1.default.white("Local:")} ${chalk_1.default.gray(`localhost:${port}`)}`);
1226
+ console.log(` ${chalk_1.default.white("Server:")} ${chalk_1.default.gray(displayDomain)}`);
1227
+ console.log(` ${chalk_1.default.white("Public:")} ${chalk_1.default.green(expectedUrl)} ${chalk_1.default.yellow("(pending)")}`);
1228
+ console.log(chalk_1.default.gray(" ─────────────────────────────────────────"));
1229
+ console.log(chalk_1.default.gray(` Log: ${logFile}`));
1230
+ console.log("");
1231
+ console.log(chalk_1.default.gray(`Stop with: kill ${child.pid}`));
1232
+ console.log(chalk_1.default.gray(`Check: tail -f ${logFile}`));
1233
+ }
1234
+ async function createTunnel(options) {
1235
+ const spinner = (0, ora_1.default)("Connecting to server...").start();
1236
+ const client = new TunnelClient_1.TunnelClient({
1237
+ serverUrl: options.serverUrl,
1238
+ token: options.token,
1239
+ reconnect: true,
1240
+ silent: true,
1241
+ rejectUnauthorized: !options.insecure,
1242
+ });
1243
+ try {
1244
+ await client.connect();
1245
+ spinner.text = "Creating tunnel...";
1246
+ const { tunnelId, publicUrl } = await client.createTunnel({
1247
+ protocol: options.protocol,
1248
+ localHost: options.localHost,
1249
+ localPort: options.localPort,
1250
+ subdomain: options.subdomain,
1251
+ remotePort: options.remotePort,
1252
+ });
1253
+ spinner.succeed("Tunnel established!");
1254
+ printTunnelInfo({
1255
+ status: "Online",
1256
+ protocol: options.protocol,
1257
+ localHost: options.localHost,
1258
+ localPort: options.localPort,
1259
+ publicUrl,
1260
+ provider: "OpenTunnel",
1261
+ });
1262
+ // Keep alive
1263
+ const startTime = Date.now();
1264
+ const statsInterval = setInterval(() => {
1265
+ const uptime = (0, utils_1.formatDuration)(Date.now() - startTime);
1266
+ process.stdout.write(`\r ${chalk_1.default.gray(`Uptime: ${uptime}`)}`);
1267
+ }, 1000);
1268
+ // Handle exit
1269
+ const cleanup = async () => {
1270
+ clearInterval(statsInterval);
1271
+ console.log("\n");
1272
+ spinner.start("Closing tunnel...");
1273
+ await client.closeTunnel(tunnelId);
1274
+ await client.disconnect();
1275
+ spinner.succeed("Tunnel closed");
1276
+ process.exit(0);
1277
+ };
1278
+ process.on("SIGINT", cleanup);
1279
+ process.on("SIGTERM", cleanup);
1280
+ // Handle reconnection
1281
+ client.on("disconnected", () => {
1282
+ console.log(chalk_1.default.yellow("\n Disconnected, reconnecting..."));
1283
+ });
1284
+ client.on("connected", () => {
1285
+ console.log(chalk_1.default.green(" Reconnected!"));
1286
+ });
1287
+ }
1288
+ catch (err) {
1289
+ spinner.fail(`Failed: ${err.message}`);
1290
+ process.exit(1);
1291
+ }
1292
+ }
1293
+ async function createNgrokTunnel(options) {
1294
+ const spinner = (0, ora_1.default)("Starting ngrok...").start();
1295
+ const client = new NgrokClient_1.NgrokClient({
1296
+ authtoken: options.authtoken,
1297
+ region: options.region,
1298
+ });
1299
+ try {
1300
+ await client.connect();
1301
+ spinner.text = "Creating tunnel...";
1302
+ const { tunnelId, publicUrl } = await client.createTunnel({
1303
+ protocol: options.protocol,
1304
+ localHost: options.localHost,
1305
+ localPort: options.localPort,
1306
+ subdomain: options.subdomain,
1307
+ remotePort: options.remotePort,
1308
+ });
1309
+ spinner.succeed("Tunnel established!");
1310
+ printTunnelInfo({
1311
+ status: "Online",
1312
+ protocol: options.protocol,
1313
+ localHost: options.localHost,
1314
+ localPort: options.localPort,
1315
+ publicUrl,
1316
+ provider: "ngrok",
1317
+ });
1318
+ // Keep alive
1319
+ const startTime = Date.now();
1320
+ const statsInterval = setInterval(() => {
1321
+ const uptime = (0, utils_1.formatDuration)(Date.now() - startTime);
1322
+ process.stdout.write(`\r ${chalk_1.default.gray(`Uptime: ${uptime}`)}`);
1323
+ }, 1000);
1324
+ // Handle exit
1325
+ const cleanup = async () => {
1326
+ clearInterval(statsInterval);
1327
+ console.log("\n");
1328
+ spinner.start("Closing tunnel...");
1329
+ await client.closeTunnel(tunnelId);
1330
+ await client.disconnect();
1331
+ spinner.succeed("Tunnel closed");
1332
+ process.exit(0);
1333
+ };
1334
+ process.on("SIGINT", cleanup);
1335
+ process.on("SIGTERM", cleanup);
1336
+ }
1337
+ catch (err) {
1338
+ spinner.fail(`Failed: ${err.message}`);
1339
+ console.log(chalk_1.default.yellow("\nMake sure ngrok is installed: https://ngrok.com/download"));
1340
+ process.exit(1);
1341
+ }
1342
+ }
1343
+ function printTunnelInfo(info) {
1344
+ console.log("");
1345
+ console.log(chalk_1.default.cyan(` OpenTunnel ${chalk_1.default.gray(`(via ${info.provider})`)}`));
1346
+ console.log(chalk_1.default.gray(" ─────────────────────────────────────────"));
1347
+ console.log(` ${chalk_1.default.white("Status:")} ${chalk_1.default.green(`● ${info.status}`)}`);
1348
+ console.log(` ${chalk_1.default.white("Protocol:")} ${chalk_1.default.yellow(info.protocol.toUpperCase())}`);
1349
+ console.log(` ${chalk_1.default.white("Local:")} ${chalk_1.default.gray(`${info.localHost}:${info.localPort}`)}`);
1350
+ console.log(` ${chalk_1.default.white("Public:")} ${chalk_1.default.green(info.publicUrl)}`);
1351
+ console.log(chalk_1.default.gray(" ─────────────────────────────────────────"));
1352
+ console.log("");
1353
+ console.log(chalk_1.default.gray(" Press Ctrl+C to close the tunnel"));
1354
+ console.log("");
1355
+ }
1356
+ program.parse();
1357
+ //# sourceMappingURL=index.js.map