nport 2.0.4 → 2.0.6
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/CHANGELOG.md +175 -0
- package/README.md +174 -16
- package/index.js +84 -639
- package/package.json +4 -4
- package/{analytics.js → src/analytics.js} +8 -2
- package/src/api.js +89 -0
- package/src/args.js +122 -0
- package/{bin-manager.js → src/bin-manager.js} +2 -1
- package/src/binary.js +88 -0
- package/src/config-manager.js +139 -0
- package/src/config.js +70 -0
- package/src/lang.js +263 -0
- package/src/state.js +79 -0
- package/src/tunnel.js +116 -0
- package/src/ui.js +103 -0
- package/src/version.js +56 -0
package/index.js
CHANGED
|
@@ -1,665 +1,110 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import axios from "axios";
|
|
4
|
-
import { spawn } from "child_process";
|
|
5
|
-
import chalk from "chalk";
|
|
6
3
|
import ora from "ora";
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
// Platform-specific configuration
|
|
34
|
-
const PLATFORM = {
|
|
35
|
-
IS_WINDOWS: process.platform === "win32",
|
|
36
|
-
BIN_NAME: process.platform === "win32" ? "cloudflared.exe" : "cloudflared",
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
// Paths
|
|
40
|
-
const PATHS = {
|
|
41
|
-
BIN_DIR: path.join(__dirname, "bin"),
|
|
42
|
-
BIN_PATH: path.join(__dirname, "bin", PLATFORM.BIN_NAME),
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
// Log patterns for filtering cloudflared output
|
|
46
|
-
const LOG_PATTERNS = {
|
|
47
|
-
SUCCESS: ["Registered tunnel connection"],
|
|
48
|
-
ERROR: ["ERR", "error"],
|
|
49
|
-
IGNORE: [
|
|
50
|
-
"Cannot determine default origin certificate path",
|
|
51
|
-
"No file cert.pem",
|
|
52
|
-
"origincert option",
|
|
53
|
-
"TUNNEL_ORIGIN_CERT",
|
|
54
|
-
"context canceled",
|
|
55
|
-
"failed to run the datagram handler",
|
|
56
|
-
"failed to serve tunnel connection",
|
|
57
|
-
"Connection terminated",
|
|
58
|
-
"no more connections active and exiting",
|
|
59
|
-
"Serve tunnel error",
|
|
60
|
-
"accept stream listener encountered a failure",
|
|
61
|
-
"Retrying connection",
|
|
62
|
-
"icmp router terminated",
|
|
63
|
-
"use of closed network connection",
|
|
64
|
-
"Application error 0x0",
|
|
65
|
-
],
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
// Computed constants
|
|
69
|
-
const TUNNEL_TIMEOUT_MS = CONFIG.TUNNEL_TIMEOUT_HOURS * 60 * 60 * 1000;
|
|
70
|
-
|
|
71
|
-
// ============================================================================
|
|
72
|
-
// Application State
|
|
73
|
-
// ============================================================================
|
|
74
|
-
|
|
75
|
-
class TunnelState {
|
|
76
|
-
constructor() {
|
|
77
|
-
this.tunnelId = null;
|
|
78
|
-
this.subdomain = null;
|
|
79
|
-
this.port = null;
|
|
80
|
-
this.tunnelProcess = null;
|
|
81
|
-
this.timeoutId = null;
|
|
82
|
-
this.connectionCount = 0;
|
|
83
|
-
this.startTime = null;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
setTunnel(tunnelId, subdomain, port) {
|
|
87
|
-
this.tunnelId = tunnelId;
|
|
88
|
-
this.subdomain = subdomain;
|
|
89
|
-
this.port = port;
|
|
90
|
-
if (!this.startTime) {
|
|
91
|
-
this.startTime = Date.now();
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
setProcess(process) {
|
|
96
|
-
this.tunnelProcess = process;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
setTimeout(timeoutId) {
|
|
100
|
-
this.timeoutId = timeoutId;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
clearTimeout() {
|
|
104
|
-
if (this.timeoutId) {
|
|
105
|
-
clearTimeout(this.timeoutId);
|
|
106
|
-
this.timeoutId = null;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
incrementConnection() {
|
|
111
|
-
this.connectionCount++;
|
|
112
|
-
return this.connectionCount;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
hasTunnel() {
|
|
116
|
-
return this.tunnelId !== null;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
hasProcess() {
|
|
120
|
-
return this.tunnelProcess && !this.tunnelProcess.killed;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
getDurationSeconds() {
|
|
124
|
-
if (!this.startTime) return 0;
|
|
125
|
-
return (Date.now() - this.startTime) / 1000;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
reset() {
|
|
129
|
-
this.clearTimeout();
|
|
130
|
-
this.tunnelId = null;
|
|
131
|
-
this.subdomain = null;
|
|
132
|
-
this.port = null;
|
|
133
|
-
this.tunnelProcess = null;
|
|
134
|
-
this.connectionCount = 0;
|
|
135
|
-
this.startTime = null;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const state = new TunnelState();
|
|
140
|
-
|
|
141
|
-
// ============================================================================
|
|
142
|
-
// Argument Parsing
|
|
143
|
-
// ============================================================================
|
|
144
|
-
|
|
145
|
-
class ArgumentParser {
|
|
146
|
-
static parse(argv) {
|
|
147
|
-
const port = this.parsePort(argv);
|
|
148
|
-
const subdomain = this.parseSubdomain(argv);
|
|
149
|
-
return { port, subdomain };
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
static parsePort(argv) {
|
|
153
|
-
const portArg = parseInt(argv[0]);
|
|
154
|
-
return portArg || CONFIG.DEFAULT_PORT;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
static parseSubdomain(argv) {
|
|
158
|
-
// Try all subdomain formats
|
|
159
|
-
const formats = [
|
|
160
|
-
() => this.findFlagWithEquals(argv, "--subdomain="),
|
|
161
|
-
() => this.findFlagWithEquals(argv, "-s="),
|
|
162
|
-
() => this.findFlagWithValue(argv, "--subdomain"),
|
|
163
|
-
() => this.findFlagWithValue(argv, "-s"),
|
|
164
|
-
];
|
|
165
|
-
|
|
166
|
-
for (const format of formats) {
|
|
167
|
-
const subdomain = format();
|
|
168
|
-
if (subdomain) return subdomain;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
return this.generateRandomSubdomain();
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
static findFlagWithEquals(argv, flag) {
|
|
175
|
-
const arg = argv.find((a) => a.startsWith(flag));
|
|
176
|
-
return arg ? arg.split("=")[1] : null;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
static findFlagWithValue(argv, flag) {
|
|
180
|
-
const index = argv.indexOf(flag);
|
|
181
|
-
return index !== -1 && argv[index + 1] ? argv[index + 1] : null;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
static generateRandomSubdomain() {
|
|
185
|
-
return `${CONFIG.SUBDOMAIN_PREFIX}${Math.floor(Math.random() * 10000)}`;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// ============================================================================
|
|
190
|
-
// Binary Management
|
|
191
|
-
// ============================================================================
|
|
192
|
-
|
|
193
|
-
class BinaryManager {
|
|
194
|
-
static validate(binaryPath) {
|
|
195
|
-
if (fs.existsSync(binaryPath)) {
|
|
196
|
-
return true;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
console.error(
|
|
200
|
-
chalk.red(`\n❌ Error: Cloudflared binary not found at: ${binaryPath}`)
|
|
201
|
-
);
|
|
202
|
-
console.error(
|
|
203
|
-
chalk.yellow(
|
|
204
|
-
"👉 Please run 'npm install' again to download the binary.\n"
|
|
205
|
-
)
|
|
206
|
-
);
|
|
207
|
-
return false;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
static spawn(binaryPath, token, port) {
|
|
211
|
-
return spawn(binaryPath, [
|
|
212
|
-
"tunnel",
|
|
213
|
-
"run",
|
|
214
|
-
"--token",
|
|
215
|
-
token,
|
|
216
|
-
"--url",
|
|
217
|
-
`http://localhost:${port}`,
|
|
218
|
-
]);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
static attachHandlers(process, spinner = null) {
|
|
222
|
-
process.stderr.on("data", (chunk) => this.handleStderr(chunk));
|
|
223
|
-
process.on("error", (err) => this.handleError(err, spinner));
|
|
224
|
-
process.on("close", (code) => this.handleClose(code));
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
static handleStderr(chunk) {
|
|
228
|
-
const msg = chunk.toString();
|
|
229
|
-
|
|
230
|
-
// Skip harmless warnings
|
|
231
|
-
if (LOG_PATTERNS.IGNORE.some((pattern) => msg.includes(pattern))) {
|
|
232
|
-
return;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// Show success messages with connection count
|
|
236
|
-
if (LOG_PATTERNS.SUCCESS.some((pattern) => msg.includes(pattern))) {
|
|
237
|
-
const count = state.incrementConnection();
|
|
238
|
-
|
|
239
|
-
const messages = [
|
|
240
|
-
"✔ Connection established [1/4] - Establishing redundancy...",
|
|
241
|
-
"✔ Connection established [2/4] - Building tunnel network...",
|
|
242
|
-
"✔ Connection established [3/4] - Almost there...",
|
|
243
|
-
"✔ Connection established [4/4] - Tunnel is fully active! 🚀",
|
|
244
|
-
];
|
|
245
|
-
|
|
246
|
-
if (count <= 4) {
|
|
247
|
-
console.log(chalk.blueBright(messages[count - 1]));
|
|
248
|
-
}
|
|
249
|
-
return;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// Show critical errors only
|
|
253
|
-
if (LOG_PATTERNS.ERROR.some((pattern) => msg.includes(pattern))) {
|
|
254
|
-
console.error(chalk.red(`[Cloudflared] ${msg.trim()}`));
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
static handleError(err, spinner) {
|
|
259
|
-
if (spinner) {
|
|
260
|
-
spinner.fail("Failed to spawn cloudflared process.");
|
|
261
|
-
}
|
|
262
|
-
console.error(chalk.red(`Process Error: ${err.message}`));
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
static handleClose(code) {
|
|
266
|
-
if (code !== 0 && code !== null) {
|
|
267
|
-
console.log(chalk.red(`Tunnel process exited with code ${code}`));
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// ============================================================================
|
|
273
|
-
// API Client
|
|
274
|
-
// ============================================================================
|
|
275
|
-
|
|
276
|
-
class APIClient {
|
|
277
|
-
static async createTunnel(subdomain) {
|
|
278
|
-
try {
|
|
279
|
-
const { data } = await axios.post(CONFIG.BACKEND_URL, { subdomain });
|
|
280
|
-
|
|
281
|
-
if (!data.success) {
|
|
282
|
-
throw new Error(data.error || "Unknown error from backend");
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
return {
|
|
286
|
-
tunnelId: data.tunnelId,
|
|
287
|
-
tunnelToken: data.tunnelToken,
|
|
288
|
-
url: data.url,
|
|
289
|
-
};
|
|
290
|
-
} catch (error) {
|
|
291
|
-
throw this.handleError(error, subdomain);
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
static async deleteTunnel(subdomain, tunnelId) {
|
|
296
|
-
await axios.delete(CONFIG.BACKEND_URL, {
|
|
297
|
-
data: { subdomain, tunnelId },
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
static handleError(error, subdomain) {
|
|
302
|
-
if (error.response?.data?.error) {
|
|
303
|
-
const errorMsg = error.response.data.error;
|
|
304
|
-
|
|
305
|
-
// Check for subdomain in use (active tunnel)
|
|
306
|
-
if (
|
|
307
|
-
errorMsg.includes("SUBDOMAIN_IN_USE:") ||
|
|
308
|
-
errorMsg.includes("currently in use") ||
|
|
309
|
-
errorMsg.includes("already exists and is currently active")
|
|
310
|
-
) {
|
|
311
|
-
return new Error(
|
|
312
|
-
chalk.red(`✗ Subdomain "${subdomain}" is already in use!\n\n`) +
|
|
313
|
-
chalk.yellow(`💡 This subdomain is currently being used by another active tunnel.\n\n`) +
|
|
314
|
-
chalk.white(`Choose a different subdomain:\n`) +
|
|
315
|
-
chalk.gray(` 1. Add a suffix: `) +
|
|
316
|
-
chalk.cyan(`nport ${state.port || CONFIG.DEFAULT_PORT} -s ${subdomain}-2\n`) +
|
|
317
|
-
chalk.gray(` 2. Try a variation: `) +
|
|
318
|
-
chalk.cyan(`nport ${state.port || CONFIG.DEFAULT_PORT} -s my-${subdomain}\n`) +
|
|
319
|
-
chalk.gray(` 3. Use random name: `) +
|
|
320
|
-
chalk.cyan(`nport ${state.port || CONFIG.DEFAULT_PORT}\n`)
|
|
321
|
-
);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// Check for duplicate tunnel error (other Cloudflare errors)
|
|
325
|
-
if (
|
|
326
|
-
errorMsg.includes("already have a tunnel") ||
|
|
327
|
-
errorMsg.includes("[1013]")
|
|
328
|
-
) {
|
|
329
|
-
return new Error(
|
|
330
|
-
`Subdomain "${subdomain}" is already taken or in use.\n\n` +
|
|
331
|
-
chalk.yellow(`💡 Try one of these options:\n`) +
|
|
332
|
-
chalk.gray(` 1. Choose a different subdomain: `) +
|
|
333
|
-
chalk.cyan(`nport ${state.port || CONFIG.DEFAULT_PORT} -s ${subdomain}-v2\n`) +
|
|
334
|
-
chalk.gray(` 2. Use a random subdomain: `) +
|
|
335
|
-
chalk.cyan(`nport ${state.port || CONFIG.DEFAULT_PORT}\n`) +
|
|
336
|
-
chalk.gray(
|
|
337
|
-
` 3. Wait a few minutes and retry if you just stopped a tunnel with this name`
|
|
338
|
-
)
|
|
339
|
-
);
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
return new Error(`Backend Error: ${errorMsg}`);
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
if (error.response) {
|
|
346
|
-
const errorMsg = JSON.stringify(error.response.data, null, 2);
|
|
347
|
-
return new Error(`Backend Error: ${errorMsg}`);
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
return error;
|
|
351
|
-
}
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import { ArgumentParser } from "./src/args.js";
|
|
6
|
+
import { TunnelOrchestrator } from "./src/tunnel.js";
|
|
7
|
+
import { VersionManager } from "./src/version.js";
|
|
8
|
+
import { UI } from "./src/ui.js";
|
|
9
|
+
import { CONFIG } from "./src/config.js";
|
|
10
|
+
import { lang } from "./src/lang.js";
|
|
11
|
+
import { configManager } from "./src/config-manager.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* NPort - Free & Open Source ngrok Alternative
|
|
15
|
+
*
|
|
16
|
+
* Main entry point for the NPort CLI application.
|
|
17
|
+
* Handles command-line arguments and orchestrates tunnel creation.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Display version information with update check
|
|
22
|
+
*/
|
|
23
|
+
async function displayVersion() {
|
|
24
|
+
const spinner = ora(lang.t("checkingUpdates")).start();
|
|
25
|
+
const updateInfo = await VersionManager.checkForUpdates();
|
|
26
|
+
spinner.stop();
|
|
27
|
+
|
|
28
|
+
UI.displayVersion(CONFIG.CURRENT_VERSION, updateInfo);
|
|
352
29
|
}
|
|
353
30
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
latest: latestVersion,
|
|
378
|
-
shouldUpdate,
|
|
379
|
-
};
|
|
380
|
-
} catch (error) {
|
|
381
|
-
// Silently fail if can't check for updates
|
|
382
|
-
return null;
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
static compareVersions(v1, v2) {
|
|
387
|
-
const parts1 = v1.split(".").map(Number);
|
|
388
|
-
const parts2 = v2.split(".").map(Number);
|
|
389
|
-
|
|
390
|
-
// Compare up to the maximum length of both version arrays
|
|
391
|
-
const maxLength = Math.max(parts1.length, parts2.length);
|
|
392
|
-
|
|
393
|
-
for (let i = 0; i < maxLength; i++) {
|
|
394
|
-
// Treat missing parts as 0 (e.g., "1.0" is "1.0.0")
|
|
395
|
-
const part1 = parts1[i] || 0;
|
|
396
|
-
const part2 = parts2[i] || 0;
|
|
397
|
-
|
|
398
|
-
if (part1 > part2) return 1;
|
|
399
|
-
if (part1 < part2) return -1;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
return 0;
|
|
31
|
+
/**
|
|
32
|
+
* Handle --set-backend command
|
|
33
|
+
*/
|
|
34
|
+
function handleSetBackend(value) {
|
|
35
|
+
if (value === 'clear') {
|
|
36
|
+
// Clear saved backend URL
|
|
37
|
+
configManager.setBackendUrl(null);
|
|
38
|
+
console.log(chalk.green('✔ Backend URL cleared. Using default backend.'));
|
|
39
|
+
console.log(chalk.gray(' Default: https://api.nport.link\n'));
|
|
40
|
+
} else {
|
|
41
|
+
// Save new backend URL
|
|
42
|
+
configManager.setBackendUrl(value);
|
|
43
|
+
console.log(chalk.green('✔ Backend URL saved successfully!'));
|
|
44
|
+
console.log(chalk.cyan(` Backend: ${value}`));
|
|
45
|
+
console.log(chalk.gray('\n This backend will be used for all future sessions.'));
|
|
46
|
+
console.log(chalk.gray(' To clear: nport --set-backend\n'));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Show current configuration
|
|
50
|
+
const savedUrl = configManager.getBackendUrl();
|
|
51
|
+
if (savedUrl) {
|
|
52
|
+
console.log(chalk.white('Current configuration:'));
|
|
53
|
+
console.log(chalk.cyan(` Saved backend: ${savedUrl}`));
|
|
403
54
|
}
|
|
404
55
|
}
|
|
405
56
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
if (!updateInfo || !updateInfo.shouldUpdate) return;
|
|
413
|
-
|
|
414
|
-
const border = "═".repeat(59);
|
|
415
|
-
const boxWidth = 59;
|
|
57
|
+
/**
|
|
58
|
+
* Main application entry point
|
|
59
|
+
*/
|
|
60
|
+
async function main() {
|
|
61
|
+
try {
|
|
62
|
+
const args = process.argv.slice(2);
|
|
416
63
|
|
|
417
|
-
//
|
|
418
|
-
const
|
|
419
|
-
const latestVersionText = ` Latest version: v${updateInfo.latest}`;
|
|
420
|
-
const runCommandText = ` Run: npm install -g ${CONFIG.PACKAGE_NAME}@latest`;
|
|
64
|
+
// Parse arguments
|
|
65
|
+
const config = ArgumentParser.parse(args);
|
|
421
66
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
console.log(chalk.yellow(`╠${border}╣`));
|
|
430
|
-
console.log(
|
|
431
|
-
chalk.yellow("║") +
|
|
432
|
-
chalk.gray(` Current version: `) +
|
|
433
|
-
chalk.red(`v${updateInfo.current}`) +
|
|
434
|
-
" ".repeat(boxWidth - currentVersionText.length) +
|
|
435
|
-
chalk.yellow("║")
|
|
436
|
-
);
|
|
437
|
-
console.log(
|
|
438
|
-
chalk.yellow("║") +
|
|
439
|
-
chalk.gray(` Latest version: `) +
|
|
440
|
-
chalk.green(`v${updateInfo.latest}`) +
|
|
441
|
-
" ".repeat(boxWidth - latestVersionText.length) +
|
|
442
|
-
chalk.yellow("║")
|
|
443
|
-
);
|
|
444
|
-
console.log(chalk.yellow(`╠${border}╣`));
|
|
445
|
-
console.log(
|
|
446
|
-
chalk.yellow("║") +
|
|
447
|
-
chalk.cyan(` Run: `) +
|
|
448
|
-
chalk.bold(`npm install -g ${CONFIG.PACKAGE_NAME}@latest`) +
|
|
449
|
-
" ".repeat(boxWidth - runCommandText.length) +
|
|
450
|
-
chalk.yellow("║")
|
|
451
|
-
);
|
|
452
|
-
console.log(chalk.yellow(`╚${border}╝\n`));
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
static displayProjectInfo() {
|
|
456
|
-
const line = "─".repeat(65);
|
|
457
|
-
console.log(chalk.gray(`\n${line}`));
|
|
458
|
-
console.log(
|
|
459
|
-
chalk.cyan.bold("NPort") +
|
|
460
|
-
chalk.gray(" - ngrok who? (Free & Open Source from Vietnam)")
|
|
461
|
-
);
|
|
462
|
-
console.log(chalk.gray(line));
|
|
463
|
-
console.log(
|
|
464
|
-
chalk.magenta("⚡ Built different: ") +
|
|
465
|
-
chalk.white("No cap, actually free forever")
|
|
466
|
-
);
|
|
467
|
-
console.log(
|
|
468
|
-
chalk.gray("🌐 Website: ") +
|
|
469
|
-
chalk.blue("https://nport.link")
|
|
470
|
-
);
|
|
471
|
-
console.log(
|
|
472
|
-
chalk.gray("📦 NPM: ") +
|
|
473
|
-
chalk.blue("npm i -g nport")
|
|
474
|
-
);
|
|
475
|
-
console.log(
|
|
476
|
-
chalk.gray("💻 GitHub: ") +
|
|
477
|
-
chalk.blue("https://github.com/tuanngocptn/nport")
|
|
478
|
-
);
|
|
479
|
-
console.log(
|
|
480
|
-
chalk.gray("👤 Made by: ") +
|
|
481
|
-
chalk.cyan("@tuanngocptn") +
|
|
482
|
-
chalk.gray(" (") +
|
|
483
|
-
chalk.blue("https://github.com/tuanngocptn") +
|
|
484
|
-
chalk.gray(")")
|
|
485
|
-
);
|
|
486
|
-
console.log(
|
|
487
|
-
chalk.gray("☕ Buy me coffee: ") +
|
|
488
|
-
chalk.yellow("https://buymeacoffee.com/tuanngocptn")
|
|
489
|
-
);
|
|
490
|
-
console.log(chalk.gray(line));
|
|
491
|
-
console.log(chalk.dim("💭 No paywalls. No BS. Just vibes. ✨"));
|
|
492
|
-
console.log(chalk.gray(`${line}\n`));
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
static displayStartupBanner(port) {
|
|
496
|
-
this.displayProjectInfo();
|
|
497
|
-
console.log(chalk.green(`🚀 Starting Tunnel for port ${port}...`));
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
static displayTunnelSuccess(url) {
|
|
501
|
-
console.log(chalk.yellow(`🌍 Public URL: ${chalk.bold(url)}`));
|
|
502
|
-
console.log(chalk.gray(` (Using bundled binary)`));
|
|
503
|
-
console.log(
|
|
504
|
-
chalk.gray(` Auto-cleanup in ${CONFIG.TUNNEL_TIMEOUT_HOURS} hours`)
|
|
505
|
-
);
|
|
506
|
-
console.log(chalk.gray("Connecting to global network..."));
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
static displayTimeoutWarning() {
|
|
510
|
-
console.log(
|
|
511
|
-
chalk.yellow(
|
|
512
|
-
`\n⏰ Tunnel has been running for ${CONFIG.TUNNEL_TIMEOUT_HOURS} hours.`
|
|
513
|
-
)
|
|
514
|
-
);
|
|
515
|
-
console.log(chalk.yellow(" Automatically shutting down..."));
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
static displayError(error, spinner = null) {
|
|
519
|
-
if (spinner) {
|
|
520
|
-
spinner.fail("Failed to connect to server.");
|
|
521
|
-
}
|
|
522
|
-
console.error(chalk.red(error.message));
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
static displayCleanupStart() {
|
|
526
|
-
console.log(
|
|
527
|
-
chalk.yellow("\n\n🛑 Shutting down... Cleaning up resources...")
|
|
528
|
-
);
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
static displayCleanupSuccess() {
|
|
532
|
-
console.log(chalk.green("✔ Cleanup successful. Subdomain released."));
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
static displayCleanupError() {
|
|
536
|
-
console.error(
|
|
537
|
-
chalk.red("✖ Cleanup failed (Server might be down or busy).")
|
|
538
|
-
);
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
// ============================================================================
|
|
543
|
-
// Tunnel Orchestrator
|
|
544
|
-
// ============================================================================
|
|
545
|
-
|
|
546
|
-
class TunnelOrchestrator {
|
|
547
|
-
static async start(config) {
|
|
548
|
-
state.setTunnel(null, config.subdomain, config.port);
|
|
549
|
-
|
|
550
|
-
// Initialize analytics
|
|
551
|
-
await analytics.initialize();
|
|
552
|
-
|
|
553
|
-
// Track CLI start
|
|
554
|
-
analytics.trackCliStart(config.port, config.subdomain, CONFIG.CURRENT_VERSION);
|
|
555
|
-
|
|
556
|
-
// Display UI
|
|
557
|
-
UI.displayStartupBanner(config.port);
|
|
558
|
-
|
|
559
|
-
// Check for updates
|
|
560
|
-
const updateInfo = await VersionManager.checkForUpdates();
|
|
561
|
-
UI.displayUpdateNotification(updateInfo);
|
|
562
|
-
|
|
563
|
-
// Validate binary
|
|
564
|
-
if (!BinaryManager.validate(PATHS.BIN_PATH)) {
|
|
565
|
-
analytics.trackTunnelError("binary_missing", "Cloudflared binary not found");
|
|
566
|
-
// Give analytics a moment to send before exiting
|
|
567
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
568
|
-
process.exit(1);
|
|
67
|
+
// Initialize language first (may prompt user)
|
|
68
|
+
await lang.initialize(config.language);
|
|
69
|
+
|
|
70
|
+
// Check for version flag (after language is set)
|
|
71
|
+
if (args.includes('-v') || args.includes('--version')) {
|
|
72
|
+
await displayVersion();
|
|
73
|
+
process.exit(0);
|
|
569
74
|
}
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
const tunnel = await APIClient.createTunnel(config.subdomain);
|
|
576
|
-
state.setTunnel(tunnel.tunnelId, config.subdomain, config.port);
|
|
577
|
-
|
|
578
|
-
// Track successful tunnel creation
|
|
579
|
-
analytics.trackTunnelCreated(config.subdomain, config.port);
|
|
580
|
-
|
|
581
|
-
spinner.succeed(chalk.green("Tunnel created!"));
|
|
582
|
-
UI.displayTunnelSuccess(tunnel.url);
|
|
583
|
-
|
|
584
|
-
// Spawn cloudflared
|
|
585
|
-
const process = BinaryManager.spawn(
|
|
586
|
-
PATHS.BIN_PATH,
|
|
587
|
-
tunnel.tunnelToken,
|
|
588
|
-
config.port
|
|
589
|
-
);
|
|
590
|
-
state.setProcess(process);
|
|
591
|
-
BinaryManager.attachHandlers(process, spinner);
|
|
592
|
-
|
|
593
|
-
// Set timeout
|
|
594
|
-
const timeoutId = setTimeout(() => {
|
|
595
|
-
UI.displayTimeoutWarning();
|
|
596
|
-
this.cleanup("timeout");
|
|
597
|
-
}, TUNNEL_TIMEOUT_MS);
|
|
598
|
-
state.setTimeout(timeoutId);
|
|
599
|
-
} catch (error) {
|
|
600
|
-
// Track tunnel creation error
|
|
601
|
-
const errorType = error.message.includes("already taken")
|
|
602
|
-
? "subdomain_taken"
|
|
603
|
-
: "tunnel_creation_failed";
|
|
604
|
-
analytics.trackTunnelError(errorType, error.message);
|
|
605
|
-
|
|
606
|
-
UI.displayError(error, spinner);
|
|
607
|
-
// Give analytics a moment to send before exiting
|
|
608
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
609
|
-
process.exit(1);
|
|
75
|
+
|
|
76
|
+
// Handle --set-backend command
|
|
77
|
+
if (config.setBackend) {
|
|
78
|
+
handleSetBackend(config.setBackend);
|
|
79
|
+
process.exit(0);
|
|
610
80
|
}
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
if (!state.hasTunnel()) {
|
|
81
|
+
|
|
82
|
+
// If only --language flag was used, show success message and exit
|
|
83
|
+
if (config.language === 'prompt' &&
|
|
84
|
+
(args.includes('--language') || args.includes('--lang') || args.includes('-l'))) {
|
|
85
|
+
// Language was already selected in initialize(), just exit
|
|
617
86
|
process.exit(0);
|
|
618
87
|
}
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
try {
|
|
627
|
-
// Kill process
|
|
628
|
-
if (state.hasProcess()) {
|
|
629
|
-
state.tunnelProcess.kill();
|
|
88
|
+
|
|
89
|
+
// Load saved backend URL if no CLI backend specified
|
|
90
|
+
if (!config.backendUrl) {
|
|
91
|
+
const savedBackend = configManager.getBackendUrl();
|
|
92
|
+
if (savedBackend) {
|
|
93
|
+
config.backendUrl = savedBackend;
|
|
630
94
|
}
|
|
631
|
-
|
|
632
|
-
// Delete tunnel
|
|
633
|
-
await APIClient.deleteTunnel(state.subdomain, state.tunnelId);
|
|
634
|
-
UI.displayCleanupSuccess();
|
|
635
|
-
} catch (err) {
|
|
636
|
-
UI.displayCleanupError();
|
|
637
95
|
}
|
|
638
|
-
|
|
639
|
-
//
|
|
640
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
641
|
-
|
|
642
|
-
process.exit(0);
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
// ============================================================================
|
|
647
|
-
// Application Entry Point
|
|
648
|
-
// ============================================================================
|
|
649
|
-
|
|
650
|
-
async function main() {
|
|
651
|
-
try {
|
|
652
|
-
const config = ArgumentParser.parse(process.argv.slice(2));
|
|
96
|
+
|
|
97
|
+
// Start tunnel
|
|
653
98
|
await TunnelOrchestrator.start(config);
|
|
654
99
|
} catch (error) {
|
|
655
|
-
console.error(
|
|
100
|
+
console.error(`Fatal Error: ${error.message}`);
|
|
656
101
|
process.exit(1);
|
|
657
102
|
}
|
|
658
103
|
}
|
|
659
104
|
|
|
660
|
-
// Register cleanup handlers
|
|
105
|
+
// Register cleanup handlers for graceful shutdown
|
|
661
106
|
process.on("SIGINT", () => TunnelOrchestrator.cleanup());
|
|
662
107
|
process.on("SIGTERM", () => TunnelOrchestrator.cleanup());
|
|
663
108
|
|
|
664
109
|
// Start application
|
|
665
|
-
main();
|
|
110
|
+
main();
|