nport 2.0.4 → 2.0.5

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/src/state.js ADDED
@@ -0,0 +1,76 @@
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.tunnelProcess = null;
11
+ this.timeoutId = null;
12
+ this.connectionCount = 0;
13
+ this.startTime = null;
14
+ this.updateInfo = null;
15
+ }
16
+
17
+ setTunnel(tunnelId, subdomain, port) {
18
+ this.tunnelId = tunnelId;
19
+ this.subdomain = subdomain;
20
+ this.port = port;
21
+ if (!this.startTime) {
22
+ this.startTime = Date.now();
23
+ }
24
+ }
25
+
26
+ setUpdateInfo(updateInfo) {
27
+ this.updateInfo = updateInfo;
28
+ }
29
+
30
+ setProcess(process) {
31
+ this.tunnelProcess = process;
32
+ }
33
+
34
+ setTimeout(timeoutId) {
35
+ this.timeoutId = timeoutId;
36
+ }
37
+
38
+ clearTimeout() {
39
+ if (this.timeoutId) {
40
+ clearTimeout(this.timeoutId);
41
+ this.timeoutId = null;
42
+ }
43
+ }
44
+
45
+ incrementConnection() {
46
+ this.connectionCount++;
47
+ return this.connectionCount;
48
+ }
49
+
50
+ hasTunnel() {
51
+ return this.tunnelId !== null;
52
+ }
53
+
54
+ hasProcess() {
55
+ return this.tunnelProcess && !this.tunnelProcess.killed;
56
+ }
57
+
58
+ getDurationSeconds() {
59
+ if (!this.startTime) return 0;
60
+ return (Date.now() - this.startTime) / 1000;
61
+ }
62
+
63
+ reset() {
64
+ this.clearTimeout();
65
+ this.tunnelId = null;
66
+ this.subdomain = null;
67
+ this.port = null;
68
+ this.tunnelProcess = null;
69
+ this.connectionCount = 0;
70
+ this.startTime = null;
71
+ this.updateInfo = null;
72
+ }
73
+ }
74
+
75
+ export const state = new TunnelState();
76
+
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);
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);
46
+ state.setTunnel(tunnel.tunnelId, config.subdomain, config.port);
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);
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,98 @@
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
+ console.log(chalk.gray(`\n ╭${line}╮`));
13
+ console.log(chalk.cyan.bold(` │ ${lang.t("header")}`) + " ".repeat(56 - lang.t("header").length - 4) + chalk.gray("│"));
14
+ console.log(chalk.gray(` ╰${line}╯\n`));
15
+ }
16
+
17
+ static displayStartupBanner(port) {
18
+ this.displayProjectInfo();
19
+ }
20
+
21
+ static displayTunnelSuccess(url, port, updateInfo) {
22
+ console.log(); // Extra spacing
23
+ console.log(chalk.cyan.bold(` 👉 ${url} 👈\n`));
24
+ console.log(chalk.gray(" " + "─".repeat(54) + "\n"));
25
+ console.log(chalk.gray(` ${lang.t("timeRemaining", { hours: CONFIG.TUNNEL_TIMEOUT_HOURS })}\n`));
26
+ }
27
+
28
+ static displayFooter(updateInfo) {
29
+ console.log(chalk.gray(" " + "─".repeat(54) + "\n"));
30
+ console.log(chalk.yellow.bold(` ${lang.t("footerTitle")}\n`));
31
+ console.log(chalk.gray(` ${lang.t("footerSubtitle")}\n`));
32
+ console.log(chalk.cyan(` ${lang.t("dropStar")}`) + chalk.white("https://github.com/tuanngocptn/nport"));
33
+ console.log(chalk.yellow(` ${lang.t("sendCoffee")}`) + chalk.white("https://buymeacoffee.com/tuanngocptn"));
34
+
35
+ if (updateInfo && updateInfo.shouldUpdate) {
36
+ console.log(chalk.red.bold(`\n ${lang.t("newVersion", { version: updateInfo.latest })}`));
37
+ console.log(chalk.gray(" ") + chalk.cyan(lang.t("updateCommand")));
38
+ }
39
+ console.log();
40
+ }
41
+
42
+ static displayTimeoutWarning() {
43
+ console.log(
44
+ chalk.yellow(
45
+ `\n⏰ Tunnel has been running for ${CONFIG.TUNNEL_TIMEOUT_HOURS} hours.`
46
+ )
47
+ );
48
+ console.log(chalk.yellow(" Automatically shutting down..."));
49
+ }
50
+
51
+ static displayError(error, spinner = null) {
52
+ if (spinner) {
53
+ spinner.fail("Failed to connect to server.");
54
+ }
55
+ console.error(chalk.red(error.message));
56
+ }
57
+
58
+ static displayCleanupStart() {
59
+ console.log(chalk.red.bold(`\n\n ${lang.t("tunnelShutdown")}\n`));
60
+ process.stdout.write(chalk.gray(` ${lang.t("cleaningUp")}`));
61
+ }
62
+
63
+ static displayCleanupSuccess() {
64
+ console.log(chalk.green(lang.t("cleanupDone")));
65
+ console.log(chalk.gray(` ${lang.t("subdomainReleased")}\n`));
66
+ this.displayGoodbye();
67
+ }
68
+
69
+ static displayCleanupError() {
70
+ console.log(chalk.red(lang.t("cleanupFailed")));
71
+ console.log(chalk.gray(` ${lang.t("serverBusy")}\n`));
72
+ this.displayGoodbye();
73
+ }
74
+
75
+ static displayGoodbye() {
76
+ console.log(chalk.gray(" " + "─".repeat(54) + "\n"));
77
+ console.log(chalk.cyan.bold(` ${lang.t("goodbyeTitle")}\n`));
78
+ console.log(chalk.gray(` ${lang.t("goodbyeMessage")}\n`));
79
+ console.log(chalk.cyan(` ${lang.t("website")}`) + chalk.white("https://nport.link"));
80
+ console.log(chalk.cyan(` ${lang.t("author")}`) + chalk.white("Nick Pham (https://github.com/tuanngocptn)"));
81
+ console.log(chalk.cyan(` ${lang.t("changeLanguage")}`) + chalk.yellow(lang.t("changeLanguageHint")));
82
+ console.log();
83
+ }
84
+
85
+ static displayVersion(current, updateInfo) {
86
+ console.log(chalk.cyan.bold(`\n${lang.t("versionTitle", { version: current })}`));
87
+ console.log(chalk.gray(`${lang.t("versionSubtitle")}\n`));
88
+
89
+ if (updateInfo && updateInfo.shouldUpdate) {
90
+ console.log(chalk.yellow(lang.t("versionAvailable", { version: updateInfo.latest })));
91
+ console.log(chalk.cyan(lang.t("versionUpdate")) + chalk.white(`npm install -g nport@latest\n`));
92
+ } else {
93
+ console.log(chalk.green(`${lang.t("versionLatest")}\n`));
94
+ }
95
+
96
+ console.log(chalk.gray(lang.t("learnMore")) + chalk.cyan("https://nport.link\n"));
97
+ }
98
+ }
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
+