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/src/lang.js
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import readline from "readline";
|
|
5
|
+
import { configManager } from "./config-manager.js";
|
|
6
|
+
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// Language Translations
|
|
9
|
+
// ============================================================================
|
|
10
|
+
|
|
11
|
+
const TRANSLATIONS = {
|
|
12
|
+
en: {
|
|
13
|
+
// Header
|
|
14
|
+
header: "N P O R T ⚡️ Free & Open Source from Vietnam ❤️",
|
|
15
|
+
|
|
16
|
+
// Spinners
|
|
17
|
+
creatingTunnel: "Creating tunnel for port {port}...",
|
|
18
|
+
checkingUpdates: "Checking for updates...",
|
|
19
|
+
|
|
20
|
+
// Success messages
|
|
21
|
+
tunnelLive: "🚀 WE LIVE BABY!",
|
|
22
|
+
connection1: " ✔ [1/2] Connection established...",
|
|
23
|
+
connection2: " ✔ [2/2] Compression enabled...",
|
|
24
|
+
timeRemaining: "⏱️ Time: {hours}h remaining",
|
|
25
|
+
|
|
26
|
+
// Footer
|
|
27
|
+
footerTitle: "🔥 KEEP THE VIBE ALIVE?",
|
|
28
|
+
footerSubtitle: "(Made with ❤️ in Vietnam)",
|
|
29
|
+
dropStar: "⭐️ Drop a Star: ",
|
|
30
|
+
sendCoffee: "☕️ Buy Coffee: ",
|
|
31
|
+
newVersion: "🚨 NEW VERSION (v{version}) detected!",
|
|
32
|
+
updateCommand: "> npm install -g nport@latest",
|
|
33
|
+
|
|
34
|
+
// Cleanup
|
|
35
|
+
tunnelShutdown: "🛑 TUNNEL SHUTDOWN.",
|
|
36
|
+
cleaningUp: "Cleaning up... ",
|
|
37
|
+
cleanupDone: "Done.",
|
|
38
|
+
cleanupFailed: "Failed.",
|
|
39
|
+
subdomainReleased: "Subdomain... Released. 🗑️",
|
|
40
|
+
serverBusy: "(Server might be down or busy)",
|
|
41
|
+
|
|
42
|
+
// Goodbye
|
|
43
|
+
goodbyeTitle: "👋 BEFORE YOU GO...",
|
|
44
|
+
goodbyeMessage: "Thanks for using NPort!",
|
|
45
|
+
website: "🌐 Website: ",
|
|
46
|
+
author: "👤 Author: ",
|
|
47
|
+
changeLanguage: "🌍 Language: ",
|
|
48
|
+
changeLanguageHint: "nport --language",
|
|
49
|
+
|
|
50
|
+
// Version
|
|
51
|
+
versionTitle: "NPort v{version}",
|
|
52
|
+
versionSubtitle: "Free & open source ngrok alternative",
|
|
53
|
+
versionLatest: "✔ You're running the latest version!",
|
|
54
|
+
versionAvailable: "🚨 New version available: v{version}",
|
|
55
|
+
versionUpdate: "Update now: ",
|
|
56
|
+
learnMore: "Learn more: ",
|
|
57
|
+
|
|
58
|
+
// Language selection
|
|
59
|
+
languagePrompt: "\n🌍 Language Selection / Chọn ngôn ngữ\n",
|
|
60
|
+
languageQuestion: "Choose your language (1-2): ",
|
|
61
|
+
languageEnglish: "1. English",
|
|
62
|
+
languageVietnamese: "2. Tiếng Việt (Vietnamese)",
|
|
63
|
+
languageInvalid: "Invalid choice. Using English by default.",
|
|
64
|
+
languageSaved: "✔ Language preference saved!",
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
vi: {
|
|
68
|
+
// Header
|
|
69
|
+
header: "N P O R T ⚡️ Việt Nam Mãi Đỉnh ❤️",
|
|
70
|
+
|
|
71
|
+
// Spinners
|
|
72
|
+
creatingTunnel: "🛠️ Đang khởi động cổng {port}... Chuẩn bị bay nào!",
|
|
73
|
+
checkingUpdates: "🔍 Đang dò la bản cập nhật mới... Đợi tí sắp có quà!",
|
|
74
|
+
|
|
75
|
+
// Success messages
|
|
76
|
+
tunnelLive: "🚀 BẬT MODE TỐC HÀNH! ĐANG BAY RỒI NÈ!",
|
|
77
|
+
connection1: " ✔ [1/2] Đang cắm dây mạng vũ trụ...",
|
|
78
|
+
connection2: " ✔ [2/2] Đang bơm siêu nén khí tốc độ ánh sáng...",
|
|
79
|
+
timeRemaining: "⏱️ Tăng tốc thần sầu: Còn {hours}h để quẩy!",
|
|
80
|
+
|
|
81
|
+
// Footer
|
|
82
|
+
footerTitle: "🔥 LƯU DANH SỬ SÁCH! ĐỪNG QUÊN STAR ⭐️",
|
|
83
|
+
footerSubtitle: "(Made in Việt Nam, chuẩn không cần chỉnh! ❤️)",
|
|
84
|
+
dropStar: "⭐️ Thả Star: ",
|
|
85
|
+
sendCoffee: "☕️ Tặng Coffee: ",
|
|
86
|
+
newVersion: "🚀 BẢN MỚI (v{version}) vừa hạ cánh!",
|
|
87
|
+
updateCommand: "💡 Gõ liền: npm install -g nport@latest",
|
|
88
|
+
|
|
89
|
+
// Cleanup
|
|
90
|
+
tunnelShutdown: "🛑 Đã tới giờ 'chốt' deal rồi cả nhà ơi...",
|
|
91
|
+
cleaningUp: "Đang dọn dẹp chiến trường... 🧹",
|
|
92
|
+
cleanupDone: "Xịn xò! Đã dọn xong rồi nè.",
|
|
93
|
+
cleanupFailed: "Oằn trời, dọn không nổi!",
|
|
94
|
+
subdomainReleased: "Subdomain... Xí xoá! Tạm biệt nhé 🗑️✨",
|
|
95
|
+
serverBusy: "(Có thể server đang bận order trà sữa)",
|
|
96
|
+
|
|
97
|
+
// Goodbye
|
|
98
|
+
goodbyeTitle: "👋 GẶP LẠI BẠN Ở ĐƯỜNG BĂNG KHÁC...",
|
|
99
|
+
goodbyeMessage: "Cảm ơn đã quẩy NPort! Lần sau chơi tiếp nha 😘",
|
|
100
|
+
website: "🌐 Sân chơi chính: ",
|
|
101
|
+
author: "👤 Nhà tài trợ: ",
|
|
102
|
+
changeLanguage: "🌍 Đổi ngôn ngữ: ",
|
|
103
|
+
changeLanguageHint: "nport --language",
|
|
104
|
+
|
|
105
|
+
// Version
|
|
106
|
+
versionTitle: "NPort v{version}",
|
|
107
|
+
versionSubtitle: "Hơn cả Ngrok - Ma-de in Ziệt Nam",
|
|
108
|
+
versionLatest: "🎉 Chúc mừng! Đang cùng server với bản mới nhất!",
|
|
109
|
+
versionAvailable: "🌟 Vèo vèo: Có bản mới v{version} vừa cập bến!",
|
|
110
|
+
versionUpdate: "Update khẩn trương lẹ làng: ",
|
|
111
|
+
learnMore: "Khám phá thêm cho nóng: ",
|
|
112
|
+
|
|
113
|
+
// Language selection
|
|
114
|
+
languagePrompt: "\n🌍 Chọn lựa ngôn ngữ ngay bên dưới nào!\n",
|
|
115
|
+
languageQuestion: "Chớp lấy một lựa chọn nha (1-2): ",
|
|
116
|
+
languageEnglish: "1. English (Chuẩn quốc tế!)",
|
|
117
|
+
languageVietnamese: "2. Tiếng Việt (Đỉnh của chóp)",
|
|
118
|
+
languageInvalid: "Ơ hơ, chọn sai rồi! Mặc định Tiếng Việt luôn cho nóng.",
|
|
119
|
+
languageSaved: "🎯 Xong rồi! Lưu ngôn ngữ thành công!",
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// ============================================================================
|
|
124
|
+
// Language Manager
|
|
125
|
+
// ============================================================================
|
|
126
|
+
|
|
127
|
+
class LanguageManager {
|
|
128
|
+
constructor() {
|
|
129
|
+
this.currentLanguage = "en";
|
|
130
|
+
this.availableLanguages = ["en", "vi"];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Get translation string with variable substitution
|
|
135
|
+
* @param {string} key - Translation key
|
|
136
|
+
* @param {object} vars - Variables to substitute
|
|
137
|
+
* @returns {string} Translated string
|
|
138
|
+
*/
|
|
139
|
+
t(key, vars = {}) {
|
|
140
|
+
const translations = TRANSLATIONS[this.currentLanguage] || TRANSLATIONS.en;
|
|
141
|
+
let text = translations[key] || TRANSLATIONS.en[key] || key;
|
|
142
|
+
|
|
143
|
+
// Replace variables like {port}, {version}, etc.
|
|
144
|
+
Object.keys(vars).forEach(varKey => {
|
|
145
|
+
text = text.replace(`{${varKey}}`, vars[varKey]);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
return text;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Load saved language preference
|
|
153
|
+
* @returns {string|null} Saved language code or null
|
|
154
|
+
*/
|
|
155
|
+
loadLanguagePreference() {
|
|
156
|
+
const lang = configManager.getLanguage();
|
|
157
|
+
if (lang && this.availableLanguages.includes(lang)) {
|
|
158
|
+
return lang;
|
|
159
|
+
}
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Save language preference
|
|
165
|
+
* @param {string} lang - Language code to save
|
|
166
|
+
*/
|
|
167
|
+
saveLanguagePreference(lang) {
|
|
168
|
+
configManager.setLanguage(lang);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Set current language
|
|
173
|
+
* @param {string} lang - Language code
|
|
174
|
+
*/
|
|
175
|
+
setLanguage(lang) {
|
|
176
|
+
if (this.availableLanguages.includes(lang)) {
|
|
177
|
+
this.currentLanguage = lang;
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Get current language
|
|
185
|
+
* @returns {string} Current language code
|
|
186
|
+
*/
|
|
187
|
+
getLanguage() {
|
|
188
|
+
return this.currentLanguage;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Prompt user to select language
|
|
193
|
+
* @returns {Promise<string>} Selected language code
|
|
194
|
+
*/
|
|
195
|
+
async promptLanguageSelection() {
|
|
196
|
+
return new Promise((resolve) => {
|
|
197
|
+
const rl = readline.createInterface({
|
|
198
|
+
input: process.stdin,
|
|
199
|
+
output: process.stdout
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
console.log(this.t("languagePrompt"));
|
|
203
|
+
console.log(` ${this.t("languageEnglish")}`);
|
|
204
|
+
console.log(` ${this.t("languageVietnamese")}\n`);
|
|
205
|
+
|
|
206
|
+
rl.question(`${this.t("languageQuestion")}`, (answer) => {
|
|
207
|
+
rl.close();
|
|
208
|
+
|
|
209
|
+
const choice = answer.trim();
|
|
210
|
+
let selectedLang = "en";
|
|
211
|
+
|
|
212
|
+
if (choice === "1") {
|
|
213
|
+
selectedLang = "en";
|
|
214
|
+
} else if (choice === "2") {
|
|
215
|
+
selectedLang = "vi";
|
|
216
|
+
} else {
|
|
217
|
+
console.log(`\n${this.t("languageInvalid")}\n`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
this.setLanguage(selectedLang);
|
|
221
|
+
this.saveLanguagePreference(selectedLang);
|
|
222
|
+
console.log(`${this.t("languageSaved")}\n`);
|
|
223
|
+
|
|
224
|
+
resolve(selectedLang);
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Initialize language - load from config or prompt user
|
|
231
|
+
* @param {string|null} cliLanguage - Language from CLI argument (or 'prompt' to force prompt)
|
|
232
|
+
* @returns {Promise<string>} Selected language code
|
|
233
|
+
*/
|
|
234
|
+
async initialize(cliLanguage = null) {
|
|
235
|
+
// Priority 1: CLI argument with value (e.g., --language en)
|
|
236
|
+
if (cliLanguage && cliLanguage !== 'prompt' && this.setLanguage(cliLanguage)) {
|
|
237
|
+
this.saveLanguagePreference(cliLanguage);
|
|
238
|
+
return cliLanguage;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Priority 2: Force prompt if --language flag without value
|
|
242
|
+
if (cliLanguage === 'prompt') {
|
|
243
|
+
return await this.promptLanguageSelection();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Priority 3: Saved preference
|
|
247
|
+
const savedLang = this.loadLanguagePreference();
|
|
248
|
+
if (savedLang) {
|
|
249
|
+
this.setLanguage(savedLang);
|
|
250
|
+
return savedLang;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Priority 4: Prompt user on first run
|
|
254
|
+
return await this.promptLanguageSelection();
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ============================================================================
|
|
259
|
+
// Export singleton instance
|
|
260
|
+
// ============================================================================
|
|
261
|
+
|
|
262
|
+
export const lang = new LanguageManager();
|
|
263
|
+
|
package/src/state.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Application State Manager
|
|
3
|
+
* Manages tunnel state including connection info, process, and timers
|
|
4
|
+
*/
|
|
5
|
+
class TunnelState {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.tunnelId = null;
|
|
8
|
+
this.subdomain = null;
|
|
9
|
+
this.port = null;
|
|
10
|
+
this.backendUrl = null;
|
|
11
|
+
this.tunnelProcess = null;
|
|
12
|
+
this.timeoutId = null;
|
|
13
|
+
this.connectionCount = 0;
|
|
14
|
+
this.startTime = null;
|
|
15
|
+
this.updateInfo = null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
setTunnel(tunnelId, subdomain, port, backendUrl = null) {
|
|
19
|
+
this.tunnelId = tunnelId;
|
|
20
|
+
this.subdomain = subdomain;
|
|
21
|
+
this.port = port;
|
|
22
|
+
this.backendUrl = backendUrl;
|
|
23
|
+
if (!this.startTime) {
|
|
24
|
+
this.startTime = Date.now();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
setUpdateInfo(updateInfo) {
|
|
29
|
+
this.updateInfo = updateInfo;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
setProcess(process) {
|
|
33
|
+
this.tunnelProcess = process;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
setTimeout(timeoutId) {
|
|
37
|
+
this.timeoutId = timeoutId;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
clearTimeout() {
|
|
41
|
+
if (this.timeoutId) {
|
|
42
|
+
clearTimeout(this.timeoutId);
|
|
43
|
+
this.timeoutId = null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
incrementConnection() {
|
|
48
|
+
this.connectionCount++;
|
|
49
|
+
return this.connectionCount;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
hasTunnel() {
|
|
53
|
+
return this.tunnelId !== null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
hasProcess() {
|
|
57
|
+
return this.tunnelProcess && !this.tunnelProcess.killed;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
getDurationSeconds() {
|
|
61
|
+
if (!this.startTime) return 0;
|
|
62
|
+
return (Date.now() - this.startTime) / 1000;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
reset() {
|
|
66
|
+
this.clearTimeout();
|
|
67
|
+
this.tunnelId = null;
|
|
68
|
+
this.subdomain = null;
|
|
69
|
+
this.port = null;
|
|
70
|
+
this.backendUrl = null;
|
|
71
|
+
this.tunnelProcess = null;
|
|
72
|
+
this.connectionCount = 0;
|
|
73
|
+
this.startTime = null;
|
|
74
|
+
this.updateInfo = null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const state = new TunnelState();
|
|
79
|
+
|
package/src/tunnel.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import ora from "ora";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { CONFIG, PATHS, TUNNEL_TIMEOUT_MS } from "./config.js";
|
|
4
|
+
import { state } from "./state.js";
|
|
5
|
+
import { BinaryManager } from "./binary.js";
|
|
6
|
+
import { APIClient } from "./api.js";
|
|
7
|
+
import { VersionManager } from "./version.js";
|
|
8
|
+
import { UI } from "./ui.js";
|
|
9
|
+
import { analytics } from "./analytics.js";
|
|
10
|
+
import { lang } from "./lang.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Tunnel Orchestrator
|
|
14
|
+
* Main controller for tunnel lifecycle management
|
|
15
|
+
*/
|
|
16
|
+
export class TunnelOrchestrator {
|
|
17
|
+
static async start(config) {
|
|
18
|
+
state.setTunnel(null, config.subdomain, config.port, config.backendUrl);
|
|
19
|
+
|
|
20
|
+
// Initialize analytics
|
|
21
|
+
await analytics.initialize();
|
|
22
|
+
|
|
23
|
+
// Track CLI start
|
|
24
|
+
analytics.trackCliStart(config.port, config.subdomain, CONFIG.CURRENT_VERSION);
|
|
25
|
+
|
|
26
|
+
// Display UI
|
|
27
|
+
UI.displayStartupBanner(config.port);
|
|
28
|
+
|
|
29
|
+
// Check for updates
|
|
30
|
+
const updateInfo = await VersionManager.checkForUpdates();
|
|
31
|
+
state.setUpdateInfo(updateInfo);
|
|
32
|
+
|
|
33
|
+
// Validate binary
|
|
34
|
+
if (!BinaryManager.validate(PATHS.BIN_PATH)) {
|
|
35
|
+
analytics.trackTunnelError("binary_missing", "Cloudflared binary not found");
|
|
36
|
+
// Give analytics a moment to send before exiting
|
|
37
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const spinner = ora(lang.t("creatingTunnel", { port: config.port })).start();
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
// Create tunnel
|
|
45
|
+
const tunnel = await APIClient.createTunnel(config.subdomain, config.backendUrl);
|
|
46
|
+
state.setTunnel(tunnel.tunnelId, config.subdomain, config.port, config.backendUrl);
|
|
47
|
+
|
|
48
|
+
// Track successful tunnel creation
|
|
49
|
+
analytics.trackTunnelCreated(config.subdomain, config.port);
|
|
50
|
+
|
|
51
|
+
spinner.stop();
|
|
52
|
+
console.log(chalk.green(` ${lang.t("tunnelLive")}`));
|
|
53
|
+
UI.displayTunnelSuccess(tunnel.url, config.port, updateInfo);
|
|
54
|
+
|
|
55
|
+
// Spawn cloudflared
|
|
56
|
+
const process = BinaryManager.spawn(
|
|
57
|
+
PATHS.BIN_PATH,
|
|
58
|
+
tunnel.tunnelToken,
|
|
59
|
+
config.port
|
|
60
|
+
);
|
|
61
|
+
state.setProcess(process);
|
|
62
|
+
BinaryManager.attachHandlers(process, spinner);
|
|
63
|
+
|
|
64
|
+
// Set timeout
|
|
65
|
+
const timeoutId = setTimeout(() => {
|
|
66
|
+
UI.displayTimeoutWarning();
|
|
67
|
+
this.cleanup("timeout");
|
|
68
|
+
}, TUNNEL_TIMEOUT_MS);
|
|
69
|
+
state.setTimeout(timeoutId);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
// Track tunnel creation error
|
|
72
|
+
const errorType = error.message.includes("already taken")
|
|
73
|
+
? "subdomain_taken"
|
|
74
|
+
: "tunnel_creation_failed";
|
|
75
|
+
analytics.trackTunnelError(errorType, error.message);
|
|
76
|
+
|
|
77
|
+
UI.displayError(error, spinner);
|
|
78
|
+
// Give analytics a moment to send before exiting
|
|
79
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
static async cleanup(reason = "manual") {
|
|
85
|
+
state.clearTimeout();
|
|
86
|
+
|
|
87
|
+
if (!state.hasTunnel()) {
|
|
88
|
+
process.exit(0);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
UI.displayCleanupStart();
|
|
92
|
+
|
|
93
|
+
// Track tunnel shutdown with duration
|
|
94
|
+
const duration = state.getDurationSeconds();
|
|
95
|
+
analytics.trackTunnelShutdown(reason, duration);
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
// Kill process
|
|
99
|
+
if (state.hasProcess()) {
|
|
100
|
+
state.tunnelProcess.kill();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Delete tunnel
|
|
104
|
+
await APIClient.deleteTunnel(state.subdomain, state.tunnelId, state.backendUrl);
|
|
105
|
+
UI.displayCleanupSuccess();
|
|
106
|
+
} catch (err) {
|
|
107
|
+
UI.displayCleanupError();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Give analytics a moment to send (non-blocking)
|
|
111
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
112
|
+
|
|
113
|
+
process.exit(0);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
package/src/ui.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { CONFIG } from "./config.js";
|
|
3
|
+
import { lang } from "./lang.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* UI Display Manager
|
|
7
|
+
* Handles all console output and user interface with multilingual support
|
|
8
|
+
*/
|
|
9
|
+
export class UI {
|
|
10
|
+
static displayProjectInfo() {
|
|
11
|
+
const line = "─".repeat(56);
|
|
12
|
+
const headerText = lang.t("header");
|
|
13
|
+
// Calculate proper padding (accounting for emojis which take visual space)
|
|
14
|
+
const visualLength = 59; // Target visual width
|
|
15
|
+
const padding = " ".repeat(Math.max(0, visualLength - headerText.length - 4));
|
|
16
|
+
|
|
17
|
+
console.log(chalk.gray(`\n ╭${line}╮`));
|
|
18
|
+
console.log(chalk.cyan.bold(` │ ${headerText}`) + padding + chalk.gray("│"));
|
|
19
|
+
console.log(chalk.gray(` ╰${line}╯\n`));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
static displayStartupBanner(port) {
|
|
23
|
+
this.displayProjectInfo();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
static displayTunnelSuccess(url, port, updateInfo) {
|
|
27
|
+
console.log(); // Extra spacing
|
|
28
|
+
console.log(chalk.cyan.bold(` 👉 ${url} 👈\n`));
|
|
29
|
+
console.log(chalk.gray(" " + "─".repeat(54) + "\n"));
|
|
30
|
+
console.log(chalk.gray(` ${lang.t("timeRemaining", { hours: CONFIG.TUNNEL_TIMEOUT_HOURS })}\n`));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
static displayFooter(updateInfo) {
|
|
34
|
+
console.log(chalk.gray(" " + "─".repeat(54) + "\n"));
|
|
35
|
+
console.log(chalk.yellow.bold(` ${lang.t("footerTitle")}\n`));
|
|
36
|
+
console.log(chalk.gray(` ${lang.t("footerSubtitle")}\n`));
|
|
37
|
+
console.log(chalk.cyan(` ${lang.t("dropStar")}`) + chalk.white("https://github.com/tuanngocptn/nport"));
|
|
38
|
+
console.log(chalk.yellow(` ${lang.t("sendCoffee")}`) + chalk.white("https://buymeacoffee.com/tuanngocptn"));
|
|
39
|
+
|
|
40
|
+
if (updateInfo && updateInfo.shouldUpdate) {
|
|
41
|
+
console.log(chalk.red.bold(`\n ${lang.t("newVersion", { version: updateInfo.latest })}`));
|
|
42
|
+
console.log(chalk.gray(" ") + chalk.cyan(lang.t("updateCommand")));
|
|
43
|
+
}
|
|
44
|
+
console.log();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
static displayTimeoutWarning() {
|
|
48
|
+
console.log(
|
|
49
|
+
chalk.yellow(
|
|
50
|
+
`\n⏰ Tunnel has been running for ${CONFIG.TUNNEL_TIMEOUT_HOURS} hours.`
|
|
51
|
+
)
|
|
52
|
+
);
|
|
53
|
+
console.log(chalk.yellow(" Automatically shutting down..."));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
static displayError(error, spinner = null) {
|
|
57
|
+
if (spinner) {
|
|
58
|
+
spinner.fail("Failed to connect to server.");
|
|
59
|
+
}
|
|
60
|
+
console.error(chalk.red(error.message));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
static displayCleanupStart() {
|
|
64
|
+
console.log(chalk.red.bold(`\n\n ${lang.t("tunnelShutdown")}\n`));
|
|
65
|
+
process.stdout.write(chalk.gray(` ${lang.t("cleaningUp")}`));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
static displayCleanupSuccess() {
|
|
69
|
+
console.log(chalk.green(lang.t("cleanupDone")));
|
|
70
|
+
console.log(chalk.gray(` ${lang.t("subdomainReleased")}\n`));
|
|
71
|
+
this.displayGoodbye();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
static displayCleanupError() {
|
|
75
|
+
console.log(chalk.red(lang.t("cleanupFailed")));
|
|
76
|
+
console.log(chalk.gray(` ${lang.t("serverBusy")}\n`));
|
|
77
|
+
this.displayGoodbye();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
static displayGoodbye() {
|
|
81
|
+
console.log(chalk.gray(" " + "─".repeat(54) + "\n"));
|
|
82
|
+
console.log(chalk.cyan.bold(` ${lang.t("goodbyeTitle")}\n`));
|
|
83
|
+
console.log(chalk.gray(` ${lang.t("goodbyeMessage")}\n`));
|
|
84
|
+
console.log(chalk.cyan(` ${lang.t("website")}`) + chalk.white("https://nport.link"));
|
|
85
|
+
console.log(chalk.cyan(` ${lang.t("author")}`) + chalk.white("Nick Pham (https://github.com/tuanngocptn)"));
|
|
86
|
+
console.log(chalk.cyan(` ${lang.t("changeLanguage")}`) + chalk.yellow(lang.t("changeLanguageHint")));
|
|
87
|
+
console.log();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
static displayVersion(current, updateInfo) {
|
|
91
|
+
console.log(chalk.cyan.bold(`\n${lang.t("versionTitle", { version: current })}`));
|
|
92
|
+
console.log(chalk.gray(`${lang.t("versionSubtitle")}\n`));
|
|
93
|
+
|
|
94
|
+
if (updateInfo && updateInfo.shouldUpdate) {
|
|
95
|
+
console.log(chalk.yellow(lang.t("versionAvailable", { version: updateInfo.latest })));
|
|
96
|
+
console.log(chalk.cyan(lang.t("versionUpdate")) + chalk.white(`npm install -g nport@latest\n`));
|
|
97
|
+
} else {
|
|
98
|
+
console.log(chalk.green(`${lang.t("versionLatest")}\n`));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
console.log(chalk.gray(lang.t("learnMore")) + chalk.cyan("https://nport.link\n"));
|
|
102
|
+
}
|
|
103
|
+
}
|
package/src/version.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
import { CONFIG } from "./config.js";
|
|
3
|
+
import { analytics } from "./analytics.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Version Manager
|
|
7
|
+
* Handles version checking and update notifications
|
|
8
|
+
*/
|
|
9
|
+
export class VersionManager {
|
|
10
|
+
static async checkForUpdates() {
|
|
11
|
+
try {
|
|
12
|
+
const response = await axios.get(
|
|
13
|
+
`https://registry.npmjs.org/${CONFIG.PACKAGE_NAME}/latest`,
|
|
14
|
+
{ timeout: CONFIG.UPDATE_CHECK_TIMEOUT }
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
const latestVersion = response.data.version;
|
|
18
|
+
const shouldUpdate =
|
|
19
|
+
this.compareVersions(latestVersion, CONFIG.CURRENT_VERSION) > 0;
|
|
20
|
+
|
|
21
|
+
// Track update notification if available
|
|
22
|
+
if (shouldUpdate) {
|
|
23
|
+
analytics.trackUpdateAvailable(CONFIG.CURRENT_VERSION, latestVersion);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
current: CONFIG.CURRENT_VERSION,
|
|
28
|
+
latest: latestVersion,
|
|
29
|
+
shouldUpdate,
|
|
30
|
+
};
|
|
31
|
+
} catch (error) {
|
|
32
|
+
// Silently fail if can't check for updates
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
static compareVersions(v1, v2) {
|
|
38
|
+
const parts1 = v1.split(".").map(Number);
|
|
39
|
+
const parts2 = v2.split(".").map(Number);
|
|
40
|
+
|
|
41
|
+
// Compare up to the maximum length of both version arrays
|
|
42
|
+
const maxLength = Math.max(parts1.length, parts2.length);
|
|
43
|
+
|
|
44
|
+
for (let i = 0; i < maxLength; i++) {
|
|
45
|
+
// Treat missing parts as 0 (e.g., "1.0" is "1.0.0")
|
|
46
|
+
const part1 = parts1[i] || 0;
|
|
47
|
+
const part2 = parts2[i] || 0;
|
|
48
|
+
|
|
49
|
+
if (part1 > part2) return 1;
|
|
50
|
+
if (part1 < part2) return -1;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|