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/CHANGELOG.md +93 -0
- package/README.md +117 -16
- package/index.js +48 -645
- package/package.json +4 -4
- package/{analytics.js → src/analytics.js} +8 -2
- package/src/api.js +87 -0
- package/src/args.js +83 -0
- package/{bin-manager.js → src/bin-manager.js} +2 -1
- package/src/binary.js +88 -0
- package/src/config.js +58 -0
- package/src/lang.js +279 -0
- package/src/state.js +76 -0
- package/src/tunnel.js +116 -0
- package/src/ui.js +98 -0
- package/src/version.js +56 -0
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
|
+
|