@wanosoft/wanolink 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +62 -0
- package/analytics.js +227 -0
- package/bin-manager.js +426 -0
- package/index.js +991 -0
- package/package.json +65 -0
package/index.js
ADDED
|
@@ -0,0 +1,991 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import axios from "axios";
|
|
4
|
+
import { spawn } from "child_process";
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
import ora from "ora";
|
|
7
|
+
import fs from "fs";
|
|
8
|
+
import path from "path";
|
|
9
|
+
import { fileURLToPath } from "url";
|
|
10
|
+
import { createRequire } from "module";
|
|
11
|
+
import { createInterface } from "readline";
|
|
12
|
+
import os from "os";
|
|
13
|
+
import { analytics } from "./analytics.js";
|
|
14
|
+
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// Module Setup & Constants
|
|
17
|
+
// ============================================================================
|
|
18
|
+
|
|
19
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
20
|
+
const __dirname = path.dirname(__filename);
|
|
21
|
+
const require = createRequire(import.meta.url);
|
|
22
|
+
const packageJson = require("./package.json");
|
|
23
|
+
|
|
24
|
+
// Application constants
|
|
25
|
+
const CONFIG = {
|
|
26
|
+
PACKAGE_NAME: packageJson.name,
|
|
27
|
+
CURRENT_VERSION: packageJson.version,
|
|
28
|
+
BACKEND_URL: "https://wanolink.wanolink.workers.dev",
|
|
29
|
+
DEFAULT_PORT: 8080,
|
|
30
|
+
SUBDOMAIN_PREFIX: "user-",
|
|
31
|
+
TUNNEL_TIMEOUT_HOURS: 4,
|
|
32
|
+
UPDATE_CHECK_TIMEOUT: 3000,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Platform-specific configuration
|
|
36
|
+
const PLATFORM = {
|
|
37
|
+
IS_WINDOWS: process.platform === "win32",
|
|
38
|
+
BIN_NAME: process.platform === "win32" ? "cloudflared.exe" : "cloudflared",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// Paths
|
|
42
|
+
const PATHS = {
|
|
43
|
+
BIN_DIR: path.join(__dirname, "bin"),
|
|
44
|
+
BIN_PATH: path.join(__dirname, "bin", PLATFORM.BIN_NAME),
|
|
45
|
+
CONFIG_DIR: path.join(os.homedir(), ".wanolink"),
|
|
46
|
+
CONFIG_FILE: path.join(os.homedir(), ".wanolink", "config.json"),
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Log patterns for filtering cloudflared output
|
|
50
|
+
const LOG_PATTERNS = {
|
|
51
|
+
SUCCESS: ["Registered tunnel connection"],
|
|
52
|
+
ERROR: ["ERR", "error"],
|
|
53
|
+
IGNORE: [
|
|
54
|
+
"Cannot determine default origin certificate path",
|
|
55
|
+
"No file cert.pem",
|
|
56
|
+
"origincert option",
|
|
57
|
+
"TUNNEL_ORIGIN_CERT",
|
|
58
|
+
"context canceled",
|
|
59
|
+
"failed to run the datagram handler",
|
|
60
|
+
"failed to serve tunnel connection",
|
|
61
|
+
"Connection terminated",
|
|
62
|
+
"no more connections active and exiting",
|
|
63
|
+
"Serve tunnel error",
|
|
64
|
+
"accept stream listener encountered a failure",
|
|
65
|
+
"Retrying connection",
|
|
66
|
+
"icmp router terminated",
|
|
67
|
+
"use of closed network connection",
|
|
68
|
+
"Application error 0x0",
|
|
69
|
+
"Failed to fetch features",
|
|
70
|
+
"Failed to initialize DNS",
|
|
71
|
+
"i/o timeout",
|
|
72
|
+
"argotunnel.com",
|
|
73
|
+
],
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// Helper function to compute timeout MS from hours (0 = unlimited)
|
|
77
|
+
function computeTimeoutMs(hours) {
|
|
78
|
+
if (hours === 0 || hours === null) return null; // Unlimited
|
|
79
|
+
return hours * 60 * 60 * 1000;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ============================================================================
|
|
83
|
+
// Token Manager
|
|
84
|
+
// ============================================================================
|
|
85
|
+
|
|
86
|
+
class TokenManager {
|
|
87
|
+
static loadConfig() {
|
|
88
|
+
try {
|
|
89
|
+
if (fs.existsSync(PATHS.CONFIG_FILE)) {
|
|
90
|
+
const content = fs.readFileSync(PATHS.CONFIG_FILE, "utf-8");
|
|
91
|
+
return JSON.parse(content);
|
|
92
|
+
}
|
|
93
|
+
} catch (error) {
|
|
94
|
+
// Config file corrupt or unreadable
|
|
95
|
+
}
|
|
96
|
+
return {};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
static saveConfig(config) {
|
|
100
|
+
try {
|
|
101
|
+
// Create config directory if it doesn't exist
|
|
102
|
+
if (!fs.existsSync(PATHS.CONFIG_DIR)) {
|
|
103
|
+
fs.mkdirSync(PATHS.CONFIG_DIR, { recursive: true });
|
|
104
|
+
}
|
|
105
|
+
fs.writeFileSync(PATHS.CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
106
|
+
return true;
|
|
107
|
+
} catch (error) {
|
|
108
|
+
console.error(chalk.red(`Failed to save config: ${error.message}`));
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
static getToken() {
|
|
114
|
+
const config = this.loadConfig();
|
|
115
|
+
return config.token || null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
static setToken(token) {
|
|
119
|
+
const config = this.loadConfig();
|
|
120
|
+
config.token = token;
|
|
121
|
+
return this.saveConfig(config);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
static clearToken() {
|
|
125
|
+
const config = this.loadConfig();
|
|
126
|
+
delete config.token;
|
|
127
|
+
return this.saveConfig(config);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
static async promptToken() {
|
|
131
|
+
const rl = createInterface({
|
|
132
|
+
input: process.stdin,
|
|
133
|
+
output: process.stdout,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
return new Promise((resolve) => {
|
|
137
|
+
rl.question(chalk.cyan("🔐 Enter token: "), (answer) => {
|
|
138
|
+
rl.close();
|
|
139
|
+
resolve(answer.trim());
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
static async handleAuth() {
|
|
145
|
+
console.log(chalk.bold("\n🔑 Wanolink Authentication\n"));
|
|
146
|
+
|
|
147
|
+
const existingToken = this.getToken();
|
|
148
|
+
if (existingToken) {
|
|
149
|
+
console.log(chalk.gray(`Current token: ${existingToken.substring(0, 8)}...`));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const token = await this.promptToken();
|
|
153
|
+
|
|
154
|
+
if (!token) {
|
|
155
|
+
console.log(chalk.red("❌ No token provided."));
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (this.setToken(token)) {
|
|
160
|
+
console.log(chalk.green("\n✔ Token saved successfully!"));
|
|
161
|
+
console.log(chalk.gray(` Config: ${PATHS.CONFIG_FILE}`));
|
|
162
|
+
} else {
|
|
163
|
+
console.log(chalk.red("\n❌ Failed to save token."));
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
static handleLogout() {
|
|
169
|
+
const existingToken = this.getToken();
|
|
170
|
+
|
|
171
|
+
if (!existingToken) {
|
|
172
|
+
console.log(chalk.yellow("No token found. Already logged out."));
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (this.clearToken()) {
|
|
177
|
+
console.log(chalk.green("✔ Logged out successfully. Token removed."));
|
|
178
|
+
} else {
|
|
179
|
+
console.log(chalk.red("❌ Failed to remove token."));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ============================================================================
|
|
185
|
+
// Connection Progress Tracker
|
|
186
|
+
// ============================================================================
|
|
187
|
+
|
|
188
|
+
class ConnectionProgressTracker {
|
|
189
|
+
constructor() {
|
|
190
|
+
this.startTime = null;
|
|
191
|
+
this.intervalId = null;
|
|
192
|
+
this.isActive = false;
|
|
193
|
+
this.spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
194
|
+
this.currentFrame = 0;
|
|
195
|
+
this.bouncePosition = 0;
|
|
196
|
+
this.bounceDirection = 1;
|
|
197
|
+
this.barWidth = 20;
|
|
198
|
+
this.blockWidth = 4;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
start() {
|
|
202
|
+
if (this.isActive) return;
|
|
203
|
+
|
|
204
|
+
this.isActive = true;
|
|
205
|
+
this.startTime = Date.now();
|
|
206
|
+
this.currentFrame = 0;
|
|
207
|
+
this.bouncePosition = 0;
|
|
208
|
+
this.bounceDirection = 1;
|
|
209
|
+
|
|
210
|
+
// Clear line and show initial state
|
|
211
|
+
this.render();
|
|
212
|
+
|
|
213
|
+
// Update every 80ms for smooth animation
|
|
214
|
+
this.intervalId = setInterval(() => {
|
|
215
|
+
this.currentFrame = (this.currentFrame + 1) % this.spinnerFrames.length;
|
|
216
|
+
|
|
217
|
+
// Bounce animation - move block back and forth
|
|
218
|
+
this.bouncePosition += this.bounceDirection;
|
|
219
|
+
if (this.bouncePosition >= this.barWidth - this.blockWidth) {
|
|
220
|
+
this.bounceDirection = -1;
|
|
221
|
+
} else if (this.bouncePosition <= 0) {
|
|
222
|
+
this.bounceDirection = 1;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
this.render();
|
|
226
|
+
}, 80);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
formatElapsedTime(ms) {
|
|
230
|
+
const totalSeconds = Math.floor(ms / 1000);
|
|
231
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
232
|
+
const seconds = totalSeconds % 60;
|
|
233
|
+
const tenths = Math.floor((ms % 1000) / 100);
|
|
234
|
+
|
|
235
|
+
if (minutes > 0) {
|
|
236
|
+
return `${minutes}:${seconds.toString().padStart(2, "0")}.${tenths}`;
|
|
237
|
+
}
|
|
238
|
+
return `${seconds}.${tenths}s`;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
getIndeterminateBar() {
|
|
242
|
+
// Create bouncing block animation
|
|
243
|
+
const emptyChar = "░";
|
|
244
|
+
const blockChar = "█";
|
|
245
|
+
|
|
246
|
+
const before = emptyChar.repeat(this.bouncePosition);
|
|
247
|
+
const block = blockChar.repeat(this.blockWidth);
|
|
248
|
+
const after = emptyChar.repeat(this.barWidth - this.bouncePosition - this.blockWidth);
|
|
249
|
+
|
|
250
|
+
return before + block + after;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
render() {
|
|
254
|
+
if (!this.isActive) return;
|
|
255
|
+
|
|
256
|
+
const elapsed = Date.now() - this.startTime;
|
|
257
|
+
const spinner = this.spinnerFrames[this.currentFrame];
|
|
258
|
+
const elapsedFormatted = this.formatElapsedTime(elapsed);
|
|
259
|
+
const bar = this.getIndeterminateBar();
|
|
260
|
+
|
|
261
|
+
// Clear line and move cursor to beginning
|
|
262
|
+
process.stdout.write("\r\x1b[K");
|
|
263
|
+
|
|
264
|
+
// Render professional progress line with indeterminate animation
|
|
265
|
+
const line = chalk.cyan(spinner) +
|
|
266
|
+
chalk.gray(" Connecting to global network") +
|
|
267
|
+
chalk.gray(" │ ") +
|
|
268
|
+
chalk.blue(bar) +
|
|
269
|
+
chalk.gray(" │ ") +
|
|
270
|
+
chalk.gray(`⏱ ${elapsedFormatted}`);
|
|
271
|
+
|
|
272
|
+
process.stdout.write(line);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
stop(success = true) {
|
|
276
|
+
if (!this.isActive) return;
|
|
277
|
+
|
|
278
|
+
this.isActive = false;
|
|
279
|
+
|
|
280
|
+
if (this.intervalId) {
|
|
281
|
+
clearInterval(this.intervalId);
|
|
282
|
+
this.intervalId = null;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const elapsed = Date.now() - this.startTime;
|
|
286
|
+
const elapsedFormatted = this.formatElapsedTime(elapsed);
|
|
287
|
+
|
|
288
|
+
// Clear the progress line
|
|
289
|
+
process.stdout.write("\r\x1b[K");
|
|
290
|
+
|
|
291
|
+
// Show final status
|
|
292
|
+
if (success) {
|
|
293
|
+
console.log(
|
|
294
|
+
chalk.green("✔") +
|
|
295
|
+
chalk.green(" Connected to global network") +
|
|
296
|
+
chalk.gray(" │ ") +
|
|
297
|
+
chalk.gray(`Completed in ${elapsedFormatted}`)
|
|
298
|
+
);
|
|
299
|
+
} else {
|
|
300
|
+
console.log(
|
|
301
|
+
chalk.red("✖") +
|
|
302
|
+
chalk.red(" Connection failed") +
|
|
303
|
+
chalk.gray(" │ ") +
|
|
304
|
+
chalk.gray(`Duration: ${elapsedFormatted}`)
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const progressTracker = new ConnectionProgressTracker();
|
|
311
|
+
|
|
312
|
+
// ============================================================================
|
|
313
|
+
// Application State
|
|
314
|
+
// ============================================================================
|
|
315
|
+
|
|
316
|
+
class TunnelState {
|
|
317
|
+
constructor() {
|
|
318
|
+
this.tunnelId = null;
|
|
319
|
+
this.subdomain = null;
|
|
320
|
+
this.port = null;
|
|
321
|
+
this.tunnelProcess = null;
|
|
322
|
+
this.timeoutId = null;
|
|
323
|
+
this.connectionCount = 0;
|
|
324
|
+
this.startTime = null;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
setTunnel(tunnelId, subdomain, port) {
|
|
328
|
+
this.tunnelId = tunnelId;
|
|
329
|
+
this.subdomain = subdomain;
|
|
330
|
+
this.port = port;
|
|
331
|
+
if (!this.startTime) {
|
|
332
|
+
this.startTime = Date.now();
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
setProcess(process) {
|
|
337
|
+
this.tunnelProcess = process;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
setTimeout(timeoutId) {
|
|
341
|
+
this.timeoutId = timeoutId;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
clearTimeout() {
|
|
345
|
+
if (this.timeoutId) {
|
|
346
|
+
clearTimeout(this.timeoutId);
|
|
347
|
+
this.timeoutId = null;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
incrementConnection() {
|
|
352
|
+
this.connectionCount++;
|
|
353
|
+
return this.connectionCount;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
hasTunnel() {
|
|
357
|
+
return this.tunnelId !== null;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
hasProcess() {
|
|
361
|
+
return this.tunnelProcess && !this.tunnelProcess.killed;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
getDurationSeconds() {
|
|
365
|
+
if (!this.startTime) return 0;
|
|
366
|
+
return (Date.now() - this.startTime) / 1000;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
reset() {
|
|
370
|
+
this.clearTimeout();
|
|
371
|
+
this.tunnelId = null;
|
|
372
|
+
this.subdomain = null;
|
|
373
|
+
this.port = null;
|
|
374
|
+
this.tunnelProcess = null;
|
|
375
|
+
this.connectionCount = 0;
|
|
376
|
+
this.startTime = null;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const state = new TunnelState();
|
|
381
|
+
|
|
382
|
+
// ============================================================================
|
|
383
|
+
// Argument Parsing
|
|
384
|
+
// ============================================================================
|
|
385
|
+
|
|
386
|
+
class ArgumentParser {
|
|
387
|
+
static parse(argv) {
|
|
388
|
+
// Check for help flag
|
|
389
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
390
|
+
this.displayHelp();
|
|
391
|
+
process.exit(0);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const port = this.parsePort(argv);
|
|
395
|
+
const subdomain = this.parseSubdomain(argv);
|
|
396
|
+
const timeout = this.parseTimeout(argv);
|
|
397
|
+
return { port, subdomain, timeout };
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
static displayHelp() {
|
|
401
|
+
console.log(`
|
|
402
|
+
${chalk.bold("Wanolink")} - Tunnel localhost to the internet via Cloudflare Edge
|
|
403
|
+
|
|
404
|
+
${chalk.yellow("Usage:")}
|
|
405
|
+
wanolink <port> [options]
|
|
406
|
+
wanolink <command>
|
|
407
|
+
|
|
408
|
+
${chalk.yellow("Commands:")}
|
|
409
|
+
${chalk.cyan("auth")} Authenticate with a token
|
|
410
|
+
${chalk.cyan("logout")} Remove saved token
|
|
411
|
+
|
|
412
|
+
${chalk.yellow("Options:")}
|
|
413
|
+
${chalk.cyan("-s, --subdomain <name>")} Custom subdomain (default: random)
|
|
414
|
+
${chalk.cyan("-t, --timeout <hours>")} Timeout in hours, 0 for unlimited (default: ${CONFIG.TUNNEL_TIMEOUT_HOURS})
|
|
415
|
+
${chalk.cyan("-h, --help")} Show this help message
|
|
416
|
+
|
|
417
|
+
${chalk.yellow("Examples:")}
|
|
418
|
+
${chalk.gray("# First time: authenticate")}
|
|
419
|
+
wanolink auth
|
|
420
|
+
|
|
421
|
+
${chalk.gray("# Basic usage with default timeout (4 hours)")}
|
|
422
|
+
wanolink 3000
|
|
423
|
+
|
|
424
|
+
${chalk.gray("# Custom subdomain")}
|
|
425
|
+
wanolink 3000 -s myapp
|
|
426
|
+
|
|
427
|
+
${chalk.gray("# Custom timeout (2 hours)")}
|
|
428
|
+
wanolink 3000 --timeout 2
|
|
429
|
+
|
|
430
|
+
${chalk.gray("# Unlimited timeout (no auto-shutdown)")}
|
|
431
|
+
wanolink 3000 -t 0
|
|
432
|
+
|
|
433
|
+
${chalk.gray("# Combine all options")}
|
|
434
|
+
wanolink 3000 -s myapp -t 0
|
|
435
|
+
`);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
static parsePort(argv) {
|
|
439
|
+
const portArg = parseInt(argv[0]);
|
|
440
|
+
return portArg || CONFIG.DEFAULT_PORT;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
static parseSubdomain(argv) {
|
|
444
|
+
// Try all subdomain formats
|
|
445
|
+
const formats = [
|
|
446
|
+
() => this.findFlagWithEquals(argv, "--subdomain="),
|
|
447
|
+
() => this.findFlagWithEquals(argv, "-s="),
|
|
448
|
+
() => this.findFlagWithValue(argv, "--subdomain"),
|
|
449
|
+
() => this.findFlagWithValue(argv, "-s"),
|
|
450
|
+
];
|
|
451
|
+
|
|
452
|
+
for (const format of formats) {
|
|
453
|
+
const subdomain = format();
|
|
454
|
+
if (subdomain) return subdomain;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return this.generateRandomSubdomain();
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
static findFlagWithEquals(argv, flag) {
|
|
461
|
+
const arg = argv.find((a) => a.startsWith(flag));
|
|
462
|
+
return arg ? arg.split("=")[1] : null;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
static findFlagWithValue(argv, flag) {
|
|
466
|
+
const index = argv.indexOf(flag);
|
|
467
|
+
return index !== -1 && argv[index + 1] ? argv[index + 1] : null;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
static generateRandomSubdomain() {
|
|
471
|
+
return `${CONFIG.SUBDOMAIN_PREFIX}${Math.floor(Math.random() * 10000)}`;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
static parseTimeout(argv) {
|
|
475
|
+
// Try all timeout formats
|
|
476
|
+
const formats = [
|
|
477
|
+
() => this.findFlagWithEquals(argv, "--timeout="),
|
|
478
|
+
() => this.findFlagWithEquals(argv, "-t="),
|
|
479
|
+
() => this.findFlagWithValue(argv, "--timeout"),
|
|
480
|
+
() => this.findFlagWithValue(argv, "-t"),
|
|
481
|
+
];
|
|
482
|
+
|
|
483
|
+
for (const format of formats) {
|
|
484
|
+
const value = format();
|
|
485
|
+
if (value !== null) {
|
|
486
|
+
const parsed = parseFloat(value);
|
|
487
|
+
if (!isNaN(parsed) && parsed >= 0) {
|
|
488
|
+
return parsed;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
return CONFIG.TUNNEL_TIMEOUT_HOURS; // Default
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// ============================================================================
|
|
498
|
+
// Binary Management
|
|
499
|
+
// ============================================================================
|
|
500
|
+
|
|
501
|
+
class BinaryManager {
|
|
502
|
+
static validate(binaryPath) {
|
|
503
|
+
if (fs.existsSync(binaryPath)) {
|
|
504
|
+
return true;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
console.error(
|
|
508
|
+
chalk.red(`\n❌ Error: Cloudflared binary not found at: ${binaryPath}`)
|
|
509
|
+
);
|
|
510
|
+
console.error(
|
|
511
|
+
chalk.yellow(
|
|
512
|
+
"👉 Please run 'npm install' again to download the binary.\n"
|
|
513
|
+
)
|
|
514
|
+
);
|
|
515
|
+
return false;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
static spawn(binaryPath, token, port) {
|
|
519
|
+
return spawn(binaryPath, [
|
|
520
|
+
"tunnel",
|
|
521
|
+
"run",
|
|
522
|
+
"--token",
|
|
523
|
+
token,
|
|
524
|
+
"--url",
|
|
525
|
+
`http://localhost:${port}`,
|
|
526
|
+
]);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
static attachHandlers(process, spinner = null) {
|
|
530
|
+
process.stderr.on("data", (chunk) => this.handleStderr(chunk));
|
|
531
|
+
process.on("error", (err) => this.handleError(err, spinner));
|
|
532
|
+
process.on("close", (code) => this.handleClose(code));
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
static handleStderr(chunk) {
|
|
536
|
+
const msg = chunk.toString();
|
|
537
|
+
|
|
538
|
+
// Skip harmless warnings
|
|
539
|
+
if (LOG_PATTERNS.IGNORE.some((pattern) => msg.includes(pattern))) {
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Show success messages with connection count
|
|
544
|
+
if (LOG_PATTERNS.SUCCESS.some((pattern) => msg.includes(pattern))) {
|
|
545
|
+
const count = state.incrementConnection();
|
|
546
|
+
|
|
547
|
+
// Stop progress tracker on first connection
|
|
548
|
+
if (count === 1) {
|
|
549
|
+
progressTracker.stop(true);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const messages = [
|
|
553
|
+
"✔ Connection established [1/4] - Establishing redundancy...",
|
|
554
|
+
"✔ Connection established [2/4] - Building tunnel network...",
|
|
555
|
+
"✔ Connection established [3/4] - Almost there...",
|
|
556
|
+
"✔ Connection established [4/4] - Tunnel is fully active! 🚀",
|
|
557
|
+
];
|
|
558
|
+
|
|
559
|
+
if (count <= 4) {
|
|
560
|
+
console.log(chalk.blueBright(messages[count - 1]));
|
|
561
|
+
}
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Show critical errors only
|
|
566
|
+
if (LOG_PATTERNS.ERROR.some((pattern) => msg.includes(pattern))) {
|
|
567
|
+
// Stop progress tracker if still running (connection failed)
|
|
568
|
+
if (progressTracker.isActive) {
|
|
569
|
+
progressTracker.stop(false);
|
|
570
|
+
}
|
|
571
|
+
console.error(chalk.red(`[Cloudflared] ${msg.trim()}`));
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
static handleError(err, spinner) {
|
|
576
|
+
// Stop progress tracker if still running
|
|
577
|
+
if (progressTracker.isActive) {
|
|
578
|
+
progressTracker.stop(false);
|
|
579
|
+
}
|
|
580
|
+
if (spinner) {
|
|
581
|
+
spinner.fail("Failed to spawn cloudflared process.");
|
|
582
|
+
}
|
|
583
|
+
console.error(chalk.red(`Process Error: ${err.message}`));
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
static handleClose(code) {
|
|
587
|
+
// Stop progress tracker if still running (unexpected exit)
|
|
588
|
+
if (progressTracker.isActive) {
|
|
589
|
+
progressTracker.stop(false);
|
|
590
|
+
}
|
|
591
|
+
if (code !== 0 && code !== null) {
|
|
592
|
+
console.log(chalk.red(`Tunnel process exited with code ${code}`));
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// ============================================================================
|
|
598
|
+
// API Client
|
|
599
|
+
// ============================================================================
|
|
600
|
+
|
|
601
|
+
class APIClient {
|
|
602
|
+
static async createTunnel(subdomain) {
|
|
603
|
+
const token = TokenManager.getToken();
|
|
604
|
+
|
|
605
|
+
if (!token) {
|
|
606
|
+
throw new Error(
|
|
607
|
+
chalk.red("❌ Authentication required.\n\n") +
|
|
608
|
+
chalk.yellow("Please run: ") + chalk.cyan("wanolink auth") + chalk.yellow(" to authenticate.")
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
try {
|
|
613
|
+
const { data } = await axios.post(CONFIG.BACKEND_URL, { subdomain, token });
|
|
614
|
+
|
|
615
|
+
if (!data.success) {
|
|
616
|
+
throw new Error(data.error || "Unknown error from backend");
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
return {
|
|
620
|
+
tunnelId: data.tunnelId,
|
|
621
|
+
tunnelToken: data.tunnelToken,
|
|
622
|
+
url: data.url,
|
|
623
|
+
};
|
|
624
|
+
} catch (error) {
|
|
625
|
+
throw this.handleError(error, subdomain);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
static async deleteTunnel(subdomain, tunnelId) {
|
|
630
|
+
const token = TokenManager.getToken();
|
|
631
|
+
await axios.delete(CONFIG.BACKEND_URL, {
|
|
632
|
+
data: { subdomain, tunnelId, token },
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
static handleError(error, subdomain) {
|
|
637
|
+
if (error.response?.data?.error) {
|
|
638
|
+
const errorMsg = error.response.data.error;
|
|
639
|
+
|
|
640
|
+
// Check for auth errors
|
|
641
|
+
if (errorMsg.includes("AUTH_REQUIRED") || errorMsg.includes("AUTH_INVALID")) {
|
|
642
|
+
return new Error(
|
|
643
|
+
chalk.red("❌ Authentication failed.\n\n") +
|
|
644
|
+
chalk.yellow("Your token is invalid or has been revoked.\n") +
|
|
645
|
+
chalk.white("Please run: ") + chalk.cyan("wanolink auth") + chalk.white(" to re-authenticate.\n")
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// Check for subdomain in use (active tunnel)
|
|
650
|
+
if (
|
|
651
|
+
errorMsg.includes("SUBDOMAIN_IN_USE:") ||
|
|
652
|
+
errorMsg.includes("currently in use") ||
|
|
653
|
+
errorMsg.includes("already exists and is currently active")
|
|
654
|
+
) {
|
|
655
|
+
return new Error(
|
|
656
|
+
chalk.red(`✗ Subdomain "${subdomain}" is already in use!\n\n`) +
|
|
657
|
+
chalk.yellow(`💡 This subdomain is currently being used by another active tunnel.\n\n`) +
|
|
658
|
+
chalk.white(`Choose a different subdomain:\n`) +
|
|
659
|
+
chalk.gray(` 1. Add a suffix: `) +
|
|
660
|
+
chalk.cyan(`wanolink ${state.port || CONFIG.DEFAULT_PORT} -s ${subdomain}-2\n`) +
|
|
661
|
+
chalk.gray(` 2. Try a variation: `) +
|
|
662
|
+
chalk.cyan(`wanolink ${state.port || CONFIG.DEFAULT_PORT} -s my-${subdomain}\n`) +
|
|
663
|
+
chalk.gray(` 3. Use random name: `) +
|
|
664
|
+
chalk.cyan(`wanolink ${state.port || CONFIG.DEFAULT_PORT}\n`)
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Check for duplicate tunnel error (other Cloudflare errors)
|
|
669
|
+
if (
|
|
670
|
+
errorMsg.includes("already have a tunnel") ||
|
|
671
|
+
errorMsg.includes("[1013]")
|
|
672
|
+
) {
|
|
673
|
+
return new Error(
|
|
674
|
+
`Subdomain "${subdomain}" is already taken or in use.\n\n` +
|
|
675
|
+
chalk.yellow(`💡 Try one of these options:\n`) +
|
|
676
|
+
chalk.gray(` 1. Choose a different subdomain: `) +
|
|
677
|
+
chalk.cyan(`wanolink ${state.port || CONFIG.DEFAULT_PORT} -s ${subdomain}-v2\n`) +
|
|
678
|
+
chalk.gray(` 2. Use a random subdomain: `) +
|
|
679
|
+
chalk.cyan(`wanolink ${state.port || CONFIG.DEFAULT_PORT}\n`) +
|
|
680
|
+
chalk.gray(
|
|
681
|
+
` 3. Wait a few minutes and retry if you just stopped a tunnel with this name`
|
|
682
|
+
)
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
return new Error(`Backend Error: ${errorMsg}`);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
if (error.response) {
|
|
690
|
+
const errorMsg = JSON.stringify(error.response.data, null, 2);
|
|
691
|
+
return new Error(`Backend Error: ${errorMsg}`);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
return error;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// ============================================================================
|
|
699
|
+
// Version Management
|
|
700
|
+
// ============================================================================
|
|
701
|
+
|
|
702
|
+
class VersionManager {
|
|
703
|
+
static async checkForUpdates() {
|
|
704
|
+
try {
|
|
705
|
+
const response = await axios.get(
|
|
706
|
+
`https://registry.npmjs.org/${CONFIG.PACKAGE_NAME}/latest`,
|
|
707
|
+
{ timeout: CONFIG.UPDATE_CHECK_TIMEOUT }
|
|
708
|
+
);
|
|
709
|
+
|
|
710
|
+
const latestVersion = response.data.version;
|
|
711
|
+
const shouldUpdate =
|
|
712
|
+
this.compareVersions(latestVersion, CONFIG.CURRENT_VERSION) > 0;
|
|
713
|
+
|
|
714
|
+
// Track update notification if available
|
|
715
|
+
if (shouldUpdate) {
|
|
716
|
+
analytics.trackUpdateAvailable(CONFIG.CURRENT_VERSION, latestVersion);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
return {
|
|
720
|
+
current: CONFIG.CURRENT_VERSION,
|
|
721
|
+
latest: latestVersion,
|
|
722
|
+
shouldUpdate,
|
|
723
|
+
};
|
|
724
|
+
} catch (error) {
|
|
725
|
+
// Silently fail if can't check for updates
|
|
726
|
+
return null;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
static compareVersions(v1, v2) {
|
|
731
|
+
const parts1 = v1.split(".").map(Number);
|
|
732
|
+
const parts2 = v2.split(".").map(Number);
|
|
733
|
+
|
|
734
|
+
// Compare up to the maximum length of both version arrays
|
|
735
|
+
const maxLength = Math.max(parts1.length, parts2.length);
|
|
736
|
+
|
|
737
|
+
for (let i = 0; i < maxLength; i++) {
|
|
738
|
+
// Treat missing parts as 0 (e.g., "1.0" is "1.0.0")
|
|
739
|
+
const part1 = parts1[i] || 0;
|
|
740
|
+
const part2 = parts2[i] || 0;
|
|
741
|
+
|
|
742
|
+
if (part1 > part2) return 1;
|
|
743
|
+
if (part1 < part2) return -1;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
return 0;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// ============================================================================
|
|
751
|
+
// UI Display
|
|
752
|
+
// ============================================================================
|
|
753
|
+
|
|
754
|
+
class UI {
|
|
755
|
+
static displayUpdateNotification(updateInfo) {
|
|
756
|
+
if (!updateInfo || !updateInfo.shouldUpdate) return;
|
|
757
|
+
|
|
758
|
+
const border = "═".repeat(59);
|
|
759
|
+
const boxWidth = 59;
|
|
760
|
+
|
|
761
|
+
// Calculate padding dynamically
|
|
762
|
+
const currentVersionText = ` Current version: v${updateInfo.current}`;
|
|
763
|
+
const latestVersionText = ` Latest version: v${updateInfo.latest}`;
|
|
764
|
+
const runCommandText = ` Run: npm install -g ${CONFIG.PACKAGE_NAME}@latest`;
|
|
765
|
+
|
|
766
|
+
console.log(chalk.yellow(`\n╔${border}╗`));
|
|
767
|
+
console.log(
|
|
768
|
+
chalk.yellow("║") +
|
|
769
|
+
chalk.bold.yellow(" 📦 Update Available!") +
|
|
770
|
+
" ".repeat(37) +
|
|
771
|
+
chalk.yellow("║")
|
|
772
|
+
);
|
|
773
|
+
console.log(chalk.yellow(`╠${border}╣`));
|
|
774
|
+
console.log(
|
|
775
|
+
chalk.yellow("║") +
|
|
776
|
+
chalk.gray(` Current version: `) +
|
|
777
|
+
chalk.red(`v${updateInfo.current}`) +
|
|
778
|
+
" ".repeat(boxWidth - currentVersionText.length) +
|
|
779
|
+
chalk.yellow("║")
|
|
780
|
+
);
|
|
781
|
+
console.log(
|
|
782
|
+
chalk.yellow("║") +
|
|
783
|
+
chalk.gray(` Latest version: `) +
|
|
784
|
+
chalk.green(`v${updateInfo.latest}`) +
|
|
785
|
+
" ".repeat(boxWidth - latestVersionText.length) +
|
|
786
|
+
chalk.yellow("║")
|
|
787
|
+
);
|
|
788
|
+
console.log(chalk.yellow(`╠${border}╣`));
|
|
789
|
+
console.log(
|
|
790
|
+
chalk.yellow("║") +
|
|
791
|
+
chalk.cyan(` Run: `) +
|
|
792
|
+
chalk.bold(`npm install -g ${CONFIG.PACKAGE_NAME}@latest`) +
|
|
793
|
+
" ".repeat(boxWidth - runCommandText.length) +
|
|
794
|
+
chalk.yellow("║")
|
|
795
|
+
);
|
|
796
|
+
console.log(chalk.yellow(`╚${border}╝\n`));
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
static displayStartupBanner(port) {
|
|
800
|
+
console.log(`🚀 Starting Tunnel for port ${port}...`);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
static displayTunnelSuccess(url, timeout) {
|
|
804
|
+
console.log(chalk.bold(`🌍 Public URL: ${url}`));
|
|
805
|
+
if (timeout === 0) {
|
|
806
|
+
console.log(chalk.gray(` No auto-cleanup (unlimited duration)`));
|
|
807
|
+
} else {
|
|
808
|
+
console.log(
|
|
809
|
+
chalk.gray(` Auto-cleanup in ${timeout} hour${timeout !== 1 ? 's' : ''}`)
|
|
810
|
+
);
|
|
811
|
+
}
|
|
812
|
+
console.log(""); // Empty line before progress
|
|
813
|
+
// Start the progress tracker instead of static message
|
|
814
|
+
progressTracker.start();
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
static displayTimeoutWarning(timeout) {
|
|
818
|
+
console.log(
|
|
819
|
+
chalk.yellow(
|
|
820
|
+
`\n⏰ Tunnel has been running for ${timeout} hour${timeout !== 1 ? 's' : ''}.`
|
|
821
|
+
)
|
|
822
|
+
);
|
|
823
|
+
console.log(chalk.yellow(" Automatically shutting down..."));
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
static displayError(error, spinner = null) {
|
|
827
|
+
if (spinner) {
|
|
828
|
+
spinner.fail("Failed to connect to server.");
|
|
829
|
+
}
|
|
830
|
+
console.error(chalk.red(error.message));
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
static displayCleanupStart() {
|
|
834
|
+
console.log(
|
|
835
|
+
chalk.yellow("\n\n🛑 Shutting down... Cleaning up resources...")
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
static displayCleanupSuccess() {
|
|
840
|
+
console.log(chalk.green("✔ Cleanup successful. Subdomain released."));
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
static displayCleanupError() {
|
|
844
|
+
console.error(
|
|
845
|
+
chalk.red("✖ Cleanup failed (Server might be down or busy).")
|
|
846
|
+
);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// ============================================================================
|
|
851
|
+
// Tunnel Orchestrator
|
|
852
|
+
// ============================================================================
|
|
853
|
+
|
|
854
|
+
class TunnelOrchestrator {
|
|
855
|
+
static async start(config) {
|
|
856
|
+
state.setTunnel(null, config.subdomain, config.port);
|
|
857
|
+
|
|
858
|
+
// Initialize analytics
|
|
859
|
+
await analytics.initialize();
|
|
860
|
+
|
|
861
|
+
// Track CLI start
|
|
862
|
+
analytics.trackCliStart(config.port, config.subdomain, CONFIG.CURRENT_VERSION);
|
|
863
|
+
|
|
864
|
+
// Display UI
|
|
865
|
+
UI.displayStartupBanner(config.port);
|
|
866
|
+
|
|
867
|
+
// Check for updates
|
|
868
|
+
const updateInfo = await VersionManager.checkForUpdates();
|
|
869
|
+
UI.displayUpdateNotification(updateInfo);
|
|
870
|
+
|
|
871
|
+
// Validate binary
|
|
872
|
+
if (!BinaryManager.validate(PATHS.BIN_PATH)) {
|
|
873
|
+
analytics.trackTunnelError("binary_missing", "Cloudflared binary not found");
|
|
874
|
+
// Give analytics a moment to send before exiting
|
|
875
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
876
|
+
process.exit(1);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
const spinner = ora("Requesting access...").start();
|
|
880
|
+
|
|
881
|
+
try {
|
|
882
|
+
// Create tunnel
|
|
883
|
+
const tunnel = await APIClient.createTunnel(config.subdomain);
|
|
884
|
+
state.setTunnel(tunnel.tunnelId, config.subdomain, config.port);
|
|
885
|
+
|
|
886
|
+
// Track successful tunnel creation
|
|
887
|
+
analytics.trackTunnelCreated(config.subdomain, config.port);
|
|
888
|
+
|
|
889
|
+
spinner.succeed(chalk.green("Tunnel created!"));
|
|
890
|
+
UI.displayTunnelSuccess(tunnel.url, config.timeout);
|
|
891
|
+
|
|
892
|
+
// Spawn cloudflared
|
|
893
|
+
const process = BinaryManager.spawn(
|
|
894
|
+
PATHS.BIN_PATH,
|
|
895
|
+
tunnel.tunnelToken,
|
|
896
|
+
config.port
|
|
897
|
+
);
|
|
898
|
+
state.setProcess(process);
|
|
899
|
+
BinaryManager.attachHandlers(process, spinner);
|
|
900
|
+
|
|
901
|
+
// Set timeout only if not unlimited (timeout > 0)
|
|
902
|
+
const timeoutMs = computeTimeoutMs(config.timeout);
|
|
903
|
+
if (timeoutMs !== null) {
|
|
904
|
+
const timeoutId = setTimeout(() => {
|
|
905
|
+
UI.displayTimeoutWarning(config.timeout);
|
|
906
|
+
this.cleanup("timeout");
|
|
907
|
+
}, timeoutMs);
|
|
908
|
+
state.setTimeout(timeoutId);
|
|
909
|
+
}
|
|
910
|
+
} catch (error) {
|
|
911
|
+
// Track tunnel creation error
|
|
912
|
+
const errorType = error.message.includes("already taken")
|
|
913
|
+
? "subdomain_taken"
|
|
914
|
+
: "tunnel_creation_failed";
|
|
915
|
+
analytics.trackTunnelError(errorType, error.message);
|
|
916
|
+
|
|
917
|
+
UI.displayError(error, spinner);
|
|
918
|
+
// Give analytics a moment to send before exiting
|
|
919
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
920
|
+
process.exit(1);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
static async cleanup(reason = "manual") {
|
|
925
|
+
state.clearTimeout();
|
|
926
|
+
|
|
927
|
+
if (!state.hasTunnel()) {
|
|
928
|
+
process.exit(0);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
UI.displayCleanupStart();
|
|
932
|
+
|
|
933
|
+
// Track tunnel shutdown with duration
|
|
934
|
+
const duration = state.getDurationSeconds();
|
|
935
|
+
analytics.trackTunnelShutdown(reason, duration);
|
|
936
|
+
|
|
937
|
+
try {
|
|
938
|
+
// Kill process
|
|
939
|
+
if (state.hasProcess()) {
|
|
940
|
+
state.tunnelProcess.kill();
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// Delete tunnel
|
|
944
|
+
await APIClient.deleteTunnel(state.subdomain, state.tunnelId);
|
|
945
|
+
UI.displayCleanupSuccess();
|
|
946
|
+
} catch (err) {
|
|
947
|
+
UI.displayCleanupError();
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// Give analytics a moment to send (non-blocking)
|
|
951
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
952
|
+
|
|
953
|
+
process.exit(0);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// ============================================================================
|
|
958
|
+
// Application Entry Point
|
|
959
|
+
// ============================================================================
|
|
960
|
+
|
|
961
|
+
async function main() {
|
|
962
|
+
const args = process.argv.slice(2);
|
|
963
|
+
const command = args[0];
|
|
964
|
+
|
|
965
|
+
// Handle special commands
|
|
966
|
+
if (command === "auth") {
|
|
967
|
+
await TokenManager.handleAuth();
|
|
968
|
+
process.exit(0);
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
if (command === "logout") {
|
|
972
|
+
TokenManager.handleLogout();
|
|
973
|
+
process.exit(0);
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// Normal tunnel flow
|
|
977
|
+
try {
|
|
978
|
+
const config = ArgumentParser.parse(args);
|
|
979
|
+
await TunnelOrchestrator.start(config);
|
|
980
|
+
} catch (error) {
|
|
981
|
+
console.error(chalk.red(`Fatal Error: ${error.message}`));
|
|
982
|
+
process.exit(1);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// Register cleanup handlers
|
|
987
|
+
process.on("SIGINT", () => TunnelOrchestrator.cleanup());
|
|
988
|
+
process.on("SIGTERM", () => TunnelOrchestrator.cleanup());
|
|
989
|
+
|
|
990
|
+
// Start application
|
|
991
|
+
main();
|