nport 2.1.0 → 2.1.2
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/dist/bin-manager.js +1 -0
- package/dist/index.js +69 -0
- package/package.json +7 -7
- package/CHANGELOG.md +0 -432
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var C=Object.defineProperty;var o=(r,n)=>C(r,"name",{value:n,configurable:!0});import s from"fs";import u from"path";import S from"https";import w from"os";import{execSync as _}from"child_process";import{fileURLToPath as E}from"url";var $=E(import.meta.url),v=u.dirname(u.dirname($)),m=u.join(v,"bin"),g="cloudflared",A=".tgz",R="cloudflared.tgz",f=w.platform(),p=w.arch(),h=f==="win32",N=h?`${g}.exe`:g,i=u.join(m,N),P="https://github.com/cloudflare/cloudflared/releases/latest/download",T=[301,302],D=200,M="755",U={darwin:{x64:"cloudflared-darwin-amd64.tgz",arm64:"cloudflared-darwin-arm64.tgz"},win32:{x64:"cloudflared-windows-amd64.exe",ia32:"cloudflared-windows-386.exe"},linux:{x64:"cloudflared-linux-amd64",arm64:"cloudflared-linux-arm64",arm:"cloudflared-linux-arm"}};function b(r){return{x64:"x64",amd64:"amd64",arm64:"arm64",ia32:"ia32",arm:"arm"}[r]||r}o(b,"normalizeArch");function B(){let r=b(p),n=U[f];if(!n)throw new Error(`Unsupported platform: ${f}. Supported platforms: darwin, win32, linux`);let t=n[r];if(!t)throw new Error(`Unsupported architecture: ${p} for platform ${f}. Supported architectures: ${Object.keys(n).join(", ")}`);return`${P}/${t}`}o(B,"getDownloadUrl");function L(r){return r.endsWith(A)}o(L,"isCompressedArchive");function O(r){s.existsSync(r)||s.mkdirSync(r,{recursive:!0})}o(O,"ensureDirectory");function c(r){try{s.existsSync(r)&&s.unlinkSync(r)}catch{}}o(c,"safeUnlink");function x(r,n=M){h||s.chmodSync(r,n)}o(x,"setExecutablePermissions");function z(r,n){if(!s.existsSync(r))throw new Error(n||`File not found: ${r}`)}o(z,"validateFileExists");async function y(r,n){return new Promise((t,d)=>{let e=s.createWriteStream(n);S.get(r,a=>{if(T.includes(a.statusCode)){e.close(),c(n),y(a.headers.location,n).then(t).catch(d);return}if(a.statusCode!==D){e.close(),c(n),d(new Error(`Download failed with status code ${a.statusCode} from ${r}`));return}a.pipe(e),e.on("finish",()=>{e.close(()=>t(n))}),e.on("error",I=>{e.close(),c(n),d(I)})}).on("error",a=>{e.close(),c(n),d(new Error(`Network error: ${a.message}`))})})}o(y,"downloadFile");function F(r,n){try{_(`tar -xzf "${r}" -C "${n}"`,{stdio:"pipe"})}catch(t){throw new Error(`Extraction failed: ${t.message}`)}}o(F,"extractTarGz");var l={info:o(r=>console.log(`\u2139\uFE0F ${r}`),"info"),success:o(r=>console.log(`\u2705 ${r}`),"success"),warn:o(r=>console.warn(`\u26A0\uFE0F ${r}`),"warn"),error:o(r=>console.error(`\u274C ${r}`),"error"),progress:o(r=>console.log(`\u{1F6A7} ${r}`),"progress"),extract:o(r=>console.log(`\u{1F4E6} ${r}`),"extract")};async function k(){l.progress("Cloudflared binary not found. Downloading... (This happens only once)");let r=B(),n=L(r),t=n?u.join(m,R):i;try{return await y(r,t),n&&(l.extract("Extracting binary..."),F(t,m),c(t),z(i,"Extraction failed: Binary not found after extraction")),x(i),l.success("Download complete."),i}catch(d){throw c(t),c(i),d}}o(k,"installBinary");async function G(){if(O(m),s.existsSync(i))return x(i),i;try{return await k()}catch(r){l.error(`Installation failed: ${r.message}`),process.exit(1)}}o(G,"ensureCloudflared");function H(){return!!(process.env.CI||process.env.GITHUB_ACTIONS||process.env.GITLAB_CI||process.env.CIRCLECI||process.env.TRAVIS||process.env.JENKINS_URL||process.env.BUILDKITE)}o(H,"isCI");async function W(){if(H()){l.info("Running in CI environment - skipping binary download");return}try{let r=await G();l.success(`Cloudflared binary is ready at: ${r}`)}catch(r){l.error(r.message),process.exit(1)}}o(W,"main");var j=E(import.meta.url);(process.argv[1]===j||process.argv[1]?.endsWith("bin-manager.js"))&&W();export{G as ensureCloudflared};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var he=Object.defineProperty;var i=(o,e)=>he(o,"name",{value:e,configurable:!0});import on from"ora";import b from"chalk";import P from"path";import{fileURLToPath as Ie}from"url";import{createRequire as we}from"module";var Q="https://api.nport.link";var X="user-";var $={THRESHOLD:5,COOLDOWN:3e4},Z={SUCCESS:["Registered tunnel connection"],ERROR:["ERR","error"],NETWORK_WARNING:["failed to accept QUIC stream","failed to dial to edge with quic","failed to accept incoming stream requests","Failed to dial a quic connection","timeout: no recent network activity","failed to dial to edge","quic:"],IGNORE:["Cannot determine default origin certificate path","No file cert.pem","origincert option","TUNNEL_ORIGIN_CERT","context canceled","failed to run the datagram handler","failed to serve tunnel connection","Connection terminated","no more connections active and exiting","Serve tunnel error","accept stream listener encountered a failure","Retrying connection","icmp router terminated","use of closed network connection","Application error 0x0"]},ee=["en","vi"],ne="https://github.com/tuanngocptn/nport",B="https://nport.link",te="https://buymeacoffee.com/tuanngocptn";var Ee=Ie(import.meta.url),oe=P.dirname(P.dirname(Ee)),be=we(import.meta.url),re=be("../package.json");function Ce(){return process.env.NPORT_BACKEND_URL?process.env.NPORT_BACKEND_URL:Q}i(Ce,"getBackendUrl");var u={PACKAGE_NAME:re.name,CURRENT_VERSION:re.version,BACKEND_URL:Ce(),DEFAULT_PORT:8080,SUBDOMAIN_PREFIX:X,TUNNEL_TIMEOUT_HOURS:4,UPDATE_CHECK_TIMEOUT:3e3},ke={IS_WINDOWS:process.platform==="win32",BIN_NAME:process.platform==="win32"?"cloudflared.exe":"cloudflared"},x={BIN_DIR:P.join(oe,"bin"),BIN_PATH:P.join(oe,"bin",ke.BIN_NAME)},U=Z,W={WARNING_THRESHOLD:$.THRESHOLD,WARNING_COOLDOWN:$.COOLDOWN},se=u.TUNNEL_TIMEOUT_HOURS*60*60*1e3;var O=class{static{i(this,"ArgumentParser")}static parse(e){let n=this.parsePort(e),t=this.parseSubdomain(e),r=this.parseLanguage(e),c=this.parseBackendUrl(e),g=this.parseSetBackend(e);return{port:n,subdomain:t,language:r,backendUrl:c,setBackend:g}}static parsePort(e){return parseInt(e[0],10)||u.DEFAULT_PORT}static parseSubdomain(e){let n=[()=>this.findFlagWithEquals(e,"--subdomain="),()=>this.findFlagWithEquals(e,"-s="),()=>this.findFlagWithValue(e,"--subdomain"),()=>this.findFlagWithValue(e,"-s")];for(let t of n){let r=t();if(r)return r}return this.generateRandomSubdomain()}static parseLanguage(e){if(e.includes("--language")||e.includes("--lang")||e.includes("-l")){let t=e.indexOf("--language")!==-1?e.indexOf("--language"):e.indexOf("--lang")!==-1?e.indexOf("--lang"):e.indexOf("-l"),r=e[t+1];if(!r||r.startsWith("-"))return"prompt"}let n=[()=>this.findFlagWithEquals(e,"--language="),()=>this.findFlagWithEquals(e,"--lang="),()=>this.findFlagWithEquals(e,"-l="),()=>this.findFlagWithValue(e,"--language"),()=>this.findFlagWithValue(e,"--lang"),()=>this.findFlagWithValue(e,"-l")];for(let t of n){let r=t();if(r)return r}return null}static parseBackendUrl(e){let n=[()=>this.findFlagWithEquals(e,"--backend="),()=>this.findFlagWithEquals(e,"-b="),()=>this.findFlagWithValue(e,"--backend"),()=>this.findFlagWithValue(e,"-b")];for(let t of n){let r=t();if(r)return r}return null}static parseSetBackend(e){let n=[()=>this.findFlagWithEquals(e,"--set-backend="),()=>this.findFlagWithValue(e,"--set-backend")];for(let t of n){let r=t();if(r)return r}return e.includes("--set-backend")?"clear":null}static findFlagWithEquals(e,n){let t=e.find(r=>r.startsWith(n));return t?t.split("=")[1]:null}static findFlagWithValue(e,n){let t=e.indexOf(n);return t!==-1&&e[t+1]?e[t+1]:null}static generateRandomSubdomain(){return`${u.SUBDOMAIN_PREFIX}${Math.floor(Math.random()*1e4)}`}};import nn from"ora";import J from"chalk";import tn from"fs";var M=class{static{i(this,"TunnelState")}tunnelId=null;subdomain=null;port=null;backendUrl=null;tunnelProcess=null;timeoutId=null;connectionCount=0;startTime=null;updateInfo=null;networkIssueCount=0;lastNetworkWarningTime=0;networkWarningShown=!1;setTunnel(e,n,t,r=null){this.tunnelId=e,this.subdomain=n,this.port=t,this.backendUrl=r,this.startTime||(this.startTime=Date.now())}setUpdateInfo(e){this.updateInfo=e}setProcess(e){this.tunnelProcess=e}setTimeout(e){this.timeoutId=e}clearTimeout(){this.timeoutId&&(clearTimeout(this.timeoutId),this.timeoutId=null)}incrementConnection(){return this.connectionCount++,this.connectionCount}hasTunnel(){return this.tunnelId!==null}hasProcess(){return this.tunnelProcess!==null&&!this.tunnelProcess.killed}getDurationSeconds(){return this.startTime?(Date.now()-this.startTime)/1e3:0}incrementNetworkIssue(){return this.networkIssueCount++,this.networkIssueCount}resetNetworkIssues(){this.networkIssueCount=0}shouldShowNetworkWarning(e,n){let t=Date.now();return this.networkIssueCount>=e&&t-this.lastNetworkWarningTime>n?(this.lastNetworkWarningTime=t,!0):!1}setNetworkWarningShown(e){this.networkWarningShown=e}reset(){this.clearTimeout(),this.tunnelId=null,this.subdomain=null,this.port=null,this.backendUrl=null,this.tunnelProcess=null,this.connectionCount=0,this.startTime=null,this.updateInfo=null,this.networkIssueCount=0,this.lastNetworkWarningTime=0,this.networkWarningShown=!1}},l=new M;import{spawn as Se}from"child_process";import p from"chalk";import Ue from"fs";import a from"chalk";import Ne from"readline";import T from"fs";import G from"path";import ve from"os";var V=class{static{i(this,"ConfigManager")}configDir;configFile;oldLangFile;config;constructor(){this.configDir=G.join(ve.homedir(),".nport"),this.configFile=G.join(this.configDir,"config.json"),this.oldLangFile=G.join(this.configDir,"lang"),this.config=this.loadConfig(),this.migrateOldConfig()}loadConfig(){try{if(T.existsSync(this.configFile)){let e=T.readFileSync(this.configFile,"utf8");return JSON.parse(e)}}catch{console.warn("Warning: Could not load config file, using defaults")}return{}}migrateOldConfig(){try{if(!this.config.language&&T.existsSync(this.oldLangFile)){let e=T.readFileSync(this.oldLangFile,"utf8").trim();if(e&&["en","vi"].includes(e)){this.config.language=e,this.saveConfig();try{T.unlinkSync(this.oldLangFile)}catch{}}}}catch{}}saveConfig(){try{return T.existsSync(this.configDir)||T.mkdirSync(this.configDir,{recursive:!0}),T.writeFileSync(this.configFile,JSON.stringify(this.config,null,2),"utf8"),!0}catch{return console.warn("Warning: Could not save configuration"),!1}}getBackendUrl(){return this.config.backendUrl??null}setBackendUrl(e){return e?this.config.backendUrl=e:delete this.config.backendUrl,this.saveConfig()}getLanguage(){return this.config.language??null}setLanguage(e){return e?this.config.language=e:delete this.config.language,this.saveConfig()}getAll(){return{...this.config}}clear(){return this.config={},this.saveConfig()}},I=new V;var H={en:{header:"N P O R T \u26A1\uFE0F Free & Open Source from Vietnam \u2764\uFE0F",creatingTunnel:"Creating tunnel for port {port}...",checkingUpdates:"Checking for updates...",tunnelLive:"\u{1F680} WE LIVE BABY!",connection1:" \u2714 [1/2] Connection established...",connection2:" \u2714 [2/2] Compression enabled...",timeRemaining:"\u23F1\uFE0F Time: {hours}h remaining",footerTitle:"\u{1F525} KEEP THE VIBE ALIVE?",footerSubtitle:"(Made with \u2764\uFE0F in Vietnam)",dropStar:"\u2B50\uFE0F Drop a Star: ",sendCoffee:"\u2615\uFE0F Buy Coffee: ",newVersion:"\u{1F6A8} NEW VERSION (v{version}) detected!",updateCommand:"> npm install -g nport@latest",tunnelShutdown:"\u{1F6D1} TUNNEL SHUTDOWN.",cleaningUp:"Cleaning up... ",cleanupDone:"Done.",cleanupFailed:"Failed.",subdomainReleased:"Subdomain... Released. \u{1F5D1}\uFE0F",serverBusy:"(Server might be down or busy)",goodbyeTitle:"\u{1F44B} BEFORE YOU GO...",goodbyeMessage:"Thanks for using NPort!",website:"\u{1F310} Website: ",author:"\u{1F464} Author: ",changeLanguage:"\u{1F30D} Language: ",changeLanguageHint:"nport --language",versionTitle:"NPort v{version}",versionSubtitle:"Free & open source ngrok alternative",versionLatest:"\u2714 You're running the latest version!",versionAvailable:"\u{1F6A8} New version available: v{version}",versionUpdate:"Update now: ",learnMore:"Learn more: ",languagePrompt:`
|
|
3
|
+
\u{1F30D} Language Selection / Ch\u1ECDn ng\xF4n ng\u1EEF
|
|
4
|
+
`,languageQuestion:"Choose your language (1-2): ",languageEnglish:"1. English",languageVietnamese:"2. Ti\u1EBFng Vi\u1EC7t (Vietnamese)",languageInvalid:"Invalid choice. Using English by default.",languageSaved:"\u2714 Language preference saved!",networkIssueTitle:`
|
|
5
|
+
\u26A0\uFE0F NETWORK CONNECTIVITY ISSUE DETECTED`,networkIssueDesc:" Cloudflared is having trouble maintaining a stable connection to Cloudflare's edge servers.",networkIssueTunnel:" \u{1F4E1} Your tunnel is still working, but connection quality may be affected.",networkIssueReasons:`
|
|
6
|
+
\u{1F4A1} Possible reasons:`,networkIssueReason1:" \u2022 Unstable internet connection or high packet loss",networkIssueReason2:" \u2022 Firewall/Router blocking UDP traffic (QUIC protocol)",networkIssueReason3:" \u2022 ISP throttling or network congestion",networkIssueFix:`
|
|
7
|
+
\u{1F527} What to try:`,networkIssueFix1:" \u2022 Check your internet connection stability",networkIssueFix2:" \u2022 Try connecting from a different network",networkIssueFix3:" \u2022 Disable VPN/Proxy if you're using one",networkIssueFix4:" \u2022 The tunnel will automatically fallback to HTTP/2 if QUIC fails",networkIssueIgnore:`
|
|
8
|
+
\u2139\uFE0F This is usually not critical - your tunnel should continue working normally.
|
|
9
|
+
`},vi:{header:"N P O R T \u26A1\uFE0F Vi\u1EC7t Nam M\xE3i \u0110\u1EC9nh \u2764\uFE0F",creatingTunnel:"\u{1F6E0}\uFE0F \u0110ang kh\u1EDFi \u0111\u1ED9ng c\u1ED5ng {port}... Chu\u1EA9n b\u1ECB bay n\xE0o!",checkingUpdates:"\u{1F50D} \u0110ang d\xF2 la b\u1EA3n c\u1EADp nh\u1EADt m\u1EDBi... \u0110\u1EE3i t\xED s\u1EAFp c\xF3 qu\xE0!",tunnelLive:"\u{1F680} B\u1EACT MODE T\u1ED0C H\xC0NH! \u0110ANG BAY R\u1ED2I N\xC8!",connection1:" \u2714 [1/2] \u0110ang c\u1EAFm d\xE2y m\u1EA1ng v\u0169 tr\u1EE5...",connection2:" \u2714 [2/2] \u0110ang b\u01A1m si\xEAu n\xE9n kh\xED t\u1ED1c \u0111\u1ED9 \xE1nh s\xE1ng...",timeRemaining:"\u23F1\uFE0F T\u0103ng t\u1ED1c th\u1EA7n s\u1EA7u: C\xF2n {hours}h \u0111\u1EC3 qu\u1EA9y!",footerTitle:"\u{1F525} L\u01AFU DANH S\u1EEC S\xC1CH! \u0110\u1EEANG QU\xCAN STAR \u2B50\uFE0F",footerSubtitle:"(Made in Vi\u1EC7t Nam, chu\u1EA9n kh\xF4ng c\u1EA7n ch\u1EC9nh! \u2764\uFE0F)",dropStar:"\u2B50\uFE0F Th\u1EA3 Star: ",sendCoffee:"\u2615\uFE0F T\u1EB7ng Coffee: ",newVersion:"\u{1F680} B\u1EA2N M\u1EDAI (v{version}) v\u1EEBa h\u1EA1 c\xE1nh!",updateCommand:"\u{1F4A1} G\xF5 li\u1EC1n: npm install -g nport@latest",tunnelShutdown:"\u{1F6D1} \u0110\xE3 t\u1EDBi gi\u1EDD 'ch\u1ED1t' deal r\u1ED3i c\u1EA3 nh\xE0 \u01A1i...",cleaningUp:"\u0110ang d\u1ECDn d\u1EB9p chi\u1EBFn tr\u01B0\u1EDDng... \u{1F9F9}",cleanupDone:"X\u1ECBn x\xF2! \u0110\xE3 d\u1ECDn xong r\u1ED3i n\xE8.",cleanupFailed:"O\u1EB1n tr\u1EDDi, d\u1ECDn kh\xF4ng n\u1ED5i!",subdomainReleased:"Subdomain... X\xED xo\xE1! T\u1EA1m bi\u1EC7t nh\xE9 \u{1F5D1}\uFE0F\u2728",serverBusy:"(C\xF3 th\u1EC3 server \u0111ang b\u1EADn order tr\xE0 s\u1EEFa)",goodbyeTitle:"\u{1F44B} G\u1EB6P L\u1EA0I B\u1EA0N \u1EDE \u0110\u01AF\u1EDCNG B\u0102NG KH\xC1C...",goodbyeMessage:"C\u1EA3m \u01A1n \u0111\xE3 qu\u1EA9y NPort! L\u1EA7n sau ch\u01A1i ti\u1EBFp nha \u{1F618}",website:"\u{1F310} S\xE2n ch\u01A1i ch\xEDnh: ",author:"\u{1F464} Nh\xE0 t\xE0i tr\u1EE3: ",changeLanguage:"\u{1F30D} \u0110\u1ED5i ng\xF4n ng\u1EEF: ",changeLanguageHint:"nport --language",versionTitle:"NPort v{version}",versionSubtitle:"H\u01A1n c\u1EA3 Ngrok - Ma-de in Vi\u1EC7t Nam",versionLatest:"\u{1F389} Ch\xFAc m\u1EEBng! \u0110ang c\xF9ng server v\u1EDBi b\u1EA3n m\u1EDBi nh\u1EA5t!",versionAvailable:"\u{1F31F} V\xE8o v\xE8o: C\xF3 b\u1EA3n m\u1EDBi v{version} v\u1EEBa c\u1EADp b\u1EBFn!",versionUpdate:"Update kh\u1EA9n tr\u01B0\u01A1ng l\u1EB9 l\xE0ng: ",learnMore:"Kh\xE1m ph\xE1 th\xEAm cho n\xF3ng: ",languagePrompt:`
|
|
10
|
+
\u{1F30D} Ch\u1ECDn l\u1EF1a ng\xF4n ng\u1EEF ngay b\xEAn d\u01B0\u1EDBi n\xE0o!
|
|
11
|
+
`,languageQuestion:"Ch\u1EDBp l\u1EA5y m\u1ED9t l\u1EF1a ch\u1ECDn nha (1-2): ",languageEnglish:"1. English (Chu\u1EA9n qu\u1ED1c t\u1EBF!)",languageVietnamese:"2. Ti\u1EBFng Vi\u1EC7t (\u0110\u1EC9nh c\u1EE7a ch\xF3p)",languageInvalid:"\u01A0 h\u01A1, ch\u1ECDn sai r\u1ED3i! M\u1EB7c \u0111\u1ECBnh Ti\u1EBFng Vi\u1EC7t lu\xF4n cho n\xF3ng.",languageSaved:"\u{1F3AF} Xong r\u1ED3i! L\u01B0u ng\xF4n ng\u1EEF th\xE0nh c\xF4ng!",networkIssueTitle:`
|
|
12
|
+
\u26A0\uFE0F PH\xC1T HI\u1EC6N V\u1EA4N \u0110\u1EC0 M\u1EA0NG`,networkIssueDesc:" Cloudflared \u0111ang g\u1EB7p kh\xF3 kh\u0103n khi gi\u1EEF k\u1EBFt n\u1ED1i \u1ED5n \u0111\u1ECBnh t\u1EDBi Cloudflare edge servers.",networkIssueTunnel:" \u{1F4E1} Tunnel c\u1EE7a b\u1EA1n v\u1EABn ho\u1EA1t \u0111\u1ED9ng, nh\u01B0ng ch\u1EA5t l\u01B0\u1EE3ng k\u1EBFt n\u1ED1i c\xF3 th\u1EC3 b\u1ECB \u1EA3nh h\u01B0\u1EDFng.",networkIssueReasons:`
|
|
13
|
+
\u{1F4A1} C\xF3 th\u1EC3 do:`,networkIssueReason1:" \u2022 M\u1EA1ng internet kh\xF4ng \u1ED5n \u0111\u1ECBnh ho\u1EB7c m\u1EA5t g\xF3i tin",networkIssueReason2:" \u2022 Firewall/Router ch\u1EB7n UDP traffic (giao th\u1EE9c QUIC)",networkIssueReason3:" \u2022 Nh\xE0 m\u1EA1ng throttle ho\u1EB7c t\u1EAFc ngh\u1EBDn m\u1EA1ng",networkIssueFix:`
|
|
14
|
+
\u{1F527} Th\u1EED c\xE1c c\xE1ch sau:`,networkIssueFix1:" \u2022 Ki\u1EC3m tra k\u1EBFt n\u1ED1i internet c\u1EE7a b\u1EA1n",networkIssueFix2:" \u2022 Th\u1EED \u0111\u1ED5i sang m\u1EA1ng kh\xE1c (v\xED d\u1EE5: 4G/5G)",networkIssueFix3:" \u2022 T\u1EAFt VPN/Proxy n\u1EBFu \u0111ang b\u1EADt",networkIssueFix4:" \u2022 Tunnel s\u1EBD t\u1EF1 \u0111\u1ED9ng chuy\u1EC3n sang HTTP/2 n\u1EBFu QUIC fail",networkIssueIgnore:`
|
|
15
|
+
\u2139\uFE0F L\u1ED7i n\xE0y th\u01B0\u1EDDng kh\xF4ng nghi\xEAm tr\u1ECDng - tunnel v\u1EABn ho\u1EA1t \u0111\u1ED9ng b\xECnh th\u01B0\u1EDDng.
|
|
16
|
+
`}},j=class{static{i(this,"LanguageManager")}currentLanguage="en";availableLanguages=ee;t(e,n={}){let r=(H[this.currentLanguage]||H.en)[e]||H.en[e]||e;return Object.keys(n).forEach(c=>{let g=n[c];g!==void 0&&(r=r.replace(`{${c}}`,String(g)))}),r}loadLanguagePreference(){let e=I.getLanguage();return e&&this.availableLanguages.includes(e)?e:null}saveLanguagePreference(e){I.setLanguage(e)}setLanguage(e){return this.availableLanguages.includes(e)?(this.currentLanguage=e,!0):!1}getLanguage(){return this.currentLanguage}async promptLanguageSelection(){return new Promise(e=>{let n=Ne.createInterface({input:process.stdin,output:process.stdout});console.log(this.t("languagePrompt")),console.log(` ${this.t("languageEnglish")}`),console.log(` ${this.t("languageVietnamese")}
|
|
17
|
+
`),n.question(`${this.t("languageQuestion")}`,t=>{n.close();let r=t.trim(),c="en";r==="1"?c="en":r==="2"?c="vi":console.log(`
|
|
18
|
+
${this.t("languageInvalid")}
|
|
19
|
+
`),this.setLanguage(c),this.saveLanguagePreference(c),console.log(`${this.t("languageSaved")}
|
|
20
|
+
`),e(c)})})}async initialize(e=null){if(e&&e!=="prompt"&&this.setLanguage(e))return this.saveLanguagePreference(e),e;if(e==="prompt")return await this.promptLanguageSelection();let n=this.loadLanguagePreference();return n?(this.setLanguage(n),n):await this.promptLanguageSelection()}},s=new j;var m=class{static{i(this,"UI")}static displayProjectInfo(){let e="\u2500".repeat(56),n=s.t("header"),r=" ".repeat(Math.max(0,59-n.length-4));console.log(a.gray(`
|
|
21
|
+
\u256D${e}\u256E`)),console.log(a.cyan.bold(` \u2502 ${n}`)+r+a.gray("\u2502")),console.log(a.gray(` \u2570${e}\u256F
|
|
22
|
+
`))}static displayStartupBanner(e){this.displayProjectInfo()}static displayTunnelSuccess(e,n,t){console.log(),console.log(a.cyan.bold(` \u{1F449} ${e} \u{1F448}
|
|
23
|
+
`)),console.log(a.gray(" "+"\u2500".repeat(54)+`
|
|
24
|
+
`)),console.log(a.gray(` ${s.t("timeRemaining",{hours:u.TUNNEL_TIMEOUT_HOURS})}
|
|
25
|
+
`))}static displayFooter(e){console.log(a.gray(" "+"\u2500".repeat(54)+`
|
|
26
|
+
`)),console.log(a.yellow.bold(` ${s.t("footerTitle")}
|
|
27
|
+
`)),console.log(a.gray(` ${s.t("footerSubtitle")}
|
|
28
|
+
`)),console.log(a.cyan(` ${s.t("dropStar")}`)+a.white(ne)),console.log(a.yellow(` ${s.t("sendCoffee")}`)+a.white(te)),e?.shouldUpdate&&(console.log(a.red.bold(`
|
|
29
|
+
${s.t("newVersion",{version:e.latest})}`)),console.log(a.gray(" ")+a.cyan(s.t("updateCommand")))),console.log()}static displayTimeoutWarning(){console.log(a.yellow(`
|
|
30
|
+
\u23F0 Tunnel has been running for ${u.TUNNEL_TIMEOUT_HOURS} hours.`)),console.log(a.yellow(" Automatically shutting down..."))}static displayError(e,n=null){n&&n.fail("Failed to connect to server."),console.error(a.red(e.message))}static displayCleanupStart(){console.log(a.red.bold(`
|
|
31
|
+
|
|
32
|
+
${s.t("tunnelShutdown")}
|
|
33
|
+
`)),process.stdout.write(a.gray(` ${s.t("cleaningUp")}`))}static displayCleanupSuccess(){console.log(a.green(s.t("cleanupDone"))),console.log(a.gray(` ${s.t("subdomainReleased")}
|
|
34
|
+
`)),this.displayGoodbye()}static displayCleanupError(){console.log(a.red(s.t("cleanupFailed"))),console.log(a.gray(` ${s.t("serverBusy")}
|
|
35
|
+
`)),this.displayGoodbye()}static displayGoodbye(){console.log(a.gray(" "+"\u2500".repeat(54)+`
|
|
36
|
+
`)),console.log(a.cyan.bold(` ${s.t("goodbyeTitle")}
|
|
37
|
+
`)),console.log(a.gray(` ${s.t("goodbyeMessage")}
|
|
38
|
+
`)),console.log(a.cyan(` ${s.t("website")}`)+a.white(B)),console.log(a.cyan(` ${s.t("author")}`)+a.white("Nick Pham (https://github.com/tuanngocptn)")),console.log(a.cyan(` ${s.t("changeLanguage")}`)+a.yellow(s.t("changeLanguageHint"))),console.log()}static displayVersion(e,n){console.log(a.cyan.bold(`
|
|
39
|
+
${s.t("versionTitle",{version:e})}`)),console.log(a.gray(`${s.t("versionSubtitle")}
|
|
40
|
+
`)),n?.shouldUpdate?(console.log(a.yellow(s.t("versionAvailable",{version:n.latest}))),console.log(a.cyan(s.t("versionUpdate"))+a.white(`npm install -g nport@latest
|
|
41
|
+
`))):console.log(a.green(`${s.t("versionLatest")}
|
|
42
|
+
`)),console.log(a.gray(s.t("learnMore"))+a.cyan(`${B}
|
|
43
|
+
`))}};var v=class{static{i(this,"BinaryManager")}static validate(e){return Ue.existsSync(e)?!0:(console.error(p.red(`
|
|
44
|
+
\u274C Error: Cloudflared binary not found at: ${e}`)),console.error(p.yellow(`\u{1F449} Please run 'npm install' again to download the binary.
|
|
45
|
+
`)),!1)}static spawn(e,n,t){return Se(e,["tunnel","run","--token",n,"--url",`http://localhost:${t}`])}static attachHandlers(e,n=null){e.stderr?.on("data",t=>this.handleStderr(t)),e.on("error",t=>this.handleError(t,n)),e.on("close",t=>this.handleClose(t))}static handleStderr(e){let n=e.toString();if(!U.IGNORE.some(t=>n.includes(t))){if(U.NETWORK_WARNING.some(t=>n.includes(t))){this.handleNetworkWarning();return}if(U.SUCCESS.some(t=>n.includes(t))){let t=l.incrementConnection();t===1?(l.resetNetworkIssues(),console.log(p.green(s.t("connection1")))):t===4&&(console.log(p.green(s.t("connection2"))),m.displayFooter(l.updateInfo));return}U.ERROR.some(t=>n.includes(t))&&console.error(p.red(`[Cloudflared] ${n.trim()}`))}}static handleNetworkWarning(){l.incrementNetworkIssue(),l.shouldShowNetworkWarning(W.WARNING_THRESHOLD,W.WARNING_COOLDOWN)&&this.displayNetworkWarning()}static displayNetworkWarning(){console.log(p.yellow(s.t("networkIssueTitle"))),console.log(p.gray(s.t("networkIssueDesc"))),console.log(p.cyan(s.t("networkIssueTunnel"))),console.log(p.yellow(s.t("networkIssueReasons"))),console.log(p.gray(s.t("networkIssueReason1"))),console.log(p.gray(s.t("networkIssueReason2"))),console.log(p.gray(s.t("networkIssueReason3"))),console.log(p.yellow(s.t("networkIssueFix"))),console.log(p.gray(s.t("networkIssueFix1"))),console.log(p.gray(s.t("networkIssueFix2"))),console.log(p.gray(s.t("networkIssueFix3"))),console.log(p.gray(s.t("networkIssueFix4"))),console.log(p.blue(s.t("networkIssueIgnore")))}static handleError(e,n){n&&n.fail("Failed to spawn cloudflared process."),console.error(p.red(`Process Error: ${e.message}`))}static handleClose(e){e!==0&&e!==null&&console.log(p.red(`Tunnel process exited with code ${e}`))}};import E from"fs";import R from"path";import Re from"https";import le from"os";import{execSync as _e}from"child_process";import{fileURLToPath as ce}from"url";var Le=ce(import.meta.url),Ae=R.dirname(R.dirname(Le)),D=R.join(Ae,"bin"),ie="cloudflared",Pe=".tgz",xe="cloudflared.tgz",F=le.platform(),ae=le.arch(),ue=F==="win32",Oe=ue?`${ie}.exe`:ie,w=R.join(D,Oe),Fe="https://github.com/cloudflare/cloudflared/releases/latest/download",De=[301,302],$e=200,Be="755",We={darwin:{x64:"cloudflared-darwin-amd64.tgz",arm64:"cloudflared-darwin-arm64.tgz"},win32:{x64:"cloudflared-windows-amd64.exe",ia32:"cloudflared-windows-386.exe"},linux:{x64:"cloudflared-linux-amd64",arm64:"cloudflared-linux-arm64",arm:"cloudflared-linux-arm"}};function Me(o){return{x64:"x64",amd64:"amd64",arm64:"arm64",ia32:"ia32",arm:"arm"}[o]||o}i(Me,"normalizeArch");function Ge(){let o=Me(ae),e=We[F];if(!e)throw new Error(`Unsupported platform: ${F}. Supported platforms: darwin, win32, linux`);let n=e[o];if(!n)throw new Error(`Unsupported architecture: ${ae} for platform ${F}. Supported architectures: ${Object.keys(e).join(", ")}`);return`${Fe}/${n}`}i(Ge,"getDownloadUrl");function Ve(o){return o.endsWith(Pe)}i(Ve,"isCompressedArchive");function He(o){E.existsSync(o)||E.mkdirSync(o,{recursive:!0})}i(He,"ensureDirectory");function C(o){try{E.existsSync(o)&&E.unlinkSync(o)}catch{}}i(C,"safeUnlink");function ge(o,e=Be){ue||E.chmodSync(o,e)}i(ge,"setExecutablePermissions");function je(o,e){if(!E.existsSync(o))throw new Error(e||`File not found: ${o}`)}i(je,"validateFileExists");async function de(o,e){return new Promise((n,t)=>{let r=E.createWriteStream(e);Re.get(o,c=>{if(De.includes(c.statusCode)){r.close(),C(e),de(c.headers.location,e).then(n).catch(t);return}if(c.statusCode!==$e){r.close(),C(e),t(new Error(`Download failed with status code ${c.statusCode} from ${o}`));return}c.pipe(r),r.on("finish",()=>{r.close(()=>n(e))}),r.on("error",g=>{r.close(),C(e),t(g)})}).on("error",c=>{r.close(),C(e),t(new Error(`Network error: ${c.message}`))})})}i(de,"downloadFile");function Ke(o,e){try{_e(`tar -xzf "${o}" -C "${e}"`,{stdio:"pipe"})}catch(n){throw new Error(`Extraction failed: ${n.message}`)}}i(Ke,"extractTarGz");var k={info:i(o=>console.log(`\u2139\uFE0F ${o}`),"info"),success:i(o=>console.log(`\u2705 ${o}`),"success"),warn:i(o=>console.warn(`\u26A0\uFE0F ${o}`),"warn"),error:i(o=>console.error(`\u274C ${o}`),"error"),progress:i(o=>console.log(`\u{1F6A7} ${o}`),"progress"),extract:i(o=>console.log(`\u{1F4E6} ${o}`),"extract")};async function qe(){k.progress("Cloudflared binary not found. Downloading... (This happens only once)");let o=Ge(),e=Ve(o),n=e?R.join(D,xe):w;try{return await de(o,n),e&&(k.extract("Extracting binary..."),Ke(n,D),C(n),je(w,"Extraction failed: Binary not found after extraction")),ge(w),k.success("Download complete."),w}catch(t){throw C(n),C(w),t}}i(qe,"installBinary");async function K(){if(He(D),E.existsSync(w))return ge(w),w;try{return await qe()}catch(o){k.error(`Installation failed: ${o.message}`),process.exit(1)}}i(K,"ensureCloudflared");function ze(){return!!(process.env.CI||process.env.GITHUB_ACTIONS||process.env.GITLAB_CI||process.env.CIRCLECI||process.env.TRAVIS||process.env.JENKINS_URL||process.env.BUILDKITE)}i(ze,"isCI");async function Je(){if(ze()){k.info("Running in CI environment - skipping binary download");return}try{let o=await K();k.success(`Cloudflared binary is ready at: ${o}`)}catch(o){k.error(o.message),process.exit(1)}}i(Je,"main");var Ye=ce(import.meta.url);(process.argv[1]===Ye||process.argv[1]?.endsWith("bin-manager.js"))&&Je();import pe from"axios";import d from"chalk";var _=class{static{i(this,"APIClient")}static async createTunnel(e,n=null){let t=n||u.BACKEND_URL;try{let{data:r}=await pe.post(t,{subdomain:e});if(!r.success)throw new Error(r.error||"Unknown error from backend");return{tunnelId:r.tunnelId,tunnelToken:r.tunnelToken,url:r.url}}catch(r){throw this.handleError(r,e)}}static async deleteTunnel(e,n,t=null){let r=t||u.BACKEND_URL;await pe.delete(r,{data:{subdomain:e,tunnelId:n}})}static handleError(e,n){let t=e.response?.data?.error;return t?t.includes("SUBDOMAIN_PROTECTED:")?new Error(`Subdomain "${n}" is already taken or in use.
|
|
46
|
+
|
|
47
|
+
`+d.yellow(`\u{1F4A1} Try one of these options:
|
|
48
|
+
`)+d.gray(" 1. Choose a different subdomain: ")+d.cyan(`nport ${l.port||u.DEFAULT_PORT} -s ${n}-v2
|
|
49
|
+
`)+d.gray(" 2. Use a random subdomain: ")+d.cyan(`nport ${l.port||u.DEFAULT_PORT}
|
|
50
|
+
`)+d.gray(" 3. Wait a few minutes and retry if you just stopped a tunnel with this name")):t.includes("SUBDOMAIN_IN_USE:")||t.includes("currently in use")||t.includes("already exists and is currently active")?new Error(d.red(`\u2717 Subdomain "${n}" is already in use!
|
|
51
|
+
|
|
52
|
+
`)+d.yellow(`\u{1F4A1} This subdomain is currently being used by another active tunnel.
|
|
53
|
+
|
|
54
|
+
`)+d.white(`Choose a different subdomain:
|
|
55
|
+
`)+d.gray(" 1. Add a suffix: ")+d.cyan(`nport ${l.port||u.DEFAULT_PORT} -s ${n}-2
|
|
56
|
+
`)+d.gray(" 2. Try a variation: ")+d.cyan(`nport ${l.port||u.DEFAULT_PORT} -s my-${n}
|
|
57
|
+
`)+d.gray(" 3. Use random name: ")+d.cyan(`nport ${l.port||u.DEFAULT_PORT}
|
|
58
|
+
`)):t.includes("already have a tunnel")||t.includes("[1013]")?new Error(`Subdomain "${n}" is already taken or in use.
|
|
59
|
+
|
|
60
|
+
`+d.yellow(`\u{1F4A1} Try one of these options:
|
|
61
|
+
`)+d.gray(" 1. Choose a different subdomain: ")+d.cyan(`nport ${l.port||u.DEFAULT_PORT} -s ${n}-v2
|
|
62
|
+
`)+d.gray(" 2. Use a random subdomain: ")+d.cyan(`nport ${l.port||u.DEFAULT_PORT}
|
|
63
|
+
`)+d.gray(" 3. Wait a few minutes and retry if you just stopped a tunnel with this name")):new Error(`Backend Error: ${t}`):e.response?new Error(`Backend Error: ${JSON.stringify(e.response.data,null,2)}`):e}};import en from"axios";import Xe from"axios";import{createHash as Ze}from"crypto";import y from"os";import L from"fs";import me from"path";var q={measurementId:"G-JJHG4DP1K9",apiSecret:"NjNID8jtRJe9s8uSBz2jfw"},h={enabled:!0,debug:process.env.NPORT_DEBUG==="true",timeout:2e3,userIdFile:me.join(y.homedir(),".nport","analytics-id")},z=class{static{i(this,"AnalyticsManager")}userId=null;sessionId=null;sessionStartTime=null;disabled=!1;constructor(){process.env.NPORT_ANALYTICS==="false"&&(this.disabled=!0)}async initialize(){if(!this.disabled){if(!q.apiSecret){h.debug&&console.warn("[Analytics] API secret not configured. Analytics disabled."),this.disabled=!0;return}try{this.userId=await this.getUserId(),this.sessionId=this.generateSessionId(),this.sessionStartTime=Date.now(),h.debug&&(console.log("[Analytics] Initialized successfully"),console.log(`[Analytics] User ID: ${this.userId}`),console.log(`[Analytics] Session ID: ${this.sessionId}`))}catch(e){h.debug&&console.warn("[Analytics] Failed to initialize:",e),this.disabled=!0}}}async getUserId(){try{let e=me.join(y.homedir(),".nport");if(L.existsSync(e)||L.mkdirSync(e,{recursive:!0}),L.existsSync(h.userIdFile)){let t=L.readFileSync(h.userIdFile,"utf8").trim();if(t)return t}let n=this.generateAnonymousId();return L.writeFileSync(h.userIdFile,n,"utf8"),n}catch{return this.generateAnonymousId()}}generateAnonymousId(){let e=[y.hostname(),y.platform(),y.arch(),y.homedir()].join("-");return Ze("sha256").update(e).digest("hex").substring(0,32)}generateSessionId(){return Math.floor(Date.now()/1e3)}async trackEvent(e,n={}){if(!(this.disabled||!h.enabled||!this.userId))try{let t=this.buildPayload(e,n),c=`https://www.google-analytics.com/mp/collect?measurement_id=${q.measurementId}&api_secret=${q.apiSecret}`;h.debug&&(console.log(`[Analytics] Sending event: ${e}`),console.log("[Analytics] Payload:",JSON.stringify(t,null,2))),Xe.post(c,t,{timeout:h.timeout,headers:{"Content-Type":"application/json"}}).then(g=>{h.debug&&(console.log(`[Analytics] Response status: ${g.status}`),g.data&&console.log("[Analytics] Response:",JSON.stringify(g.data,null,2)))}).catch(g=>{h.debug&&console.warn("[Analytics] Request failed:",g.message)})}catch(t){h.debug&&console.warn("[Analytics] Error tracking event:",t)}}getEngagementTime(){return this.sessionStartTime?Math.max(100,Date.now()-this.sessionStartTime):100}buildPayload(e,n){return{client_id:this.userId,timestamp_micros:Date.now()*1e3,events:[{name:e,params:{session_id:String(this.sessionId),engagement_time_msec:this.getEngagementTime(),...this.getSystemInfo(),...n}}]}}getSystemInfo(){return{os_platform:y.platform(),os_version:y.release(),os_arch:y.arch(),node_version:process.version}}trackCliStart(e,n,t){this.trackEvent("cli_start",{port:String(e),has_custom_subdomain:n&&!n.startsWith("user-"),cli_version:t})}trackTunnelCreated(e,n){this.trackEvent("tunnel_created",{subdomain_type:e.startsWith("user-")?"random":"custom",port:String(n)})}trackTunnelError(e,n){this.trackEvent("tunnel_error",{error_type:e,error_message:n.substring(0,100)})}trackTunnelShutdown(e,n){this.trackEvent("tunnel_shutdown",{shutdown_reason:e,duration_seconds:String(Math.floor(n))})}trackUpdateAvailable(e,n){this.trackEvent("update_available",{current_version:e,latest_version:n})}},f=new z;var N=class{static{i(this,"VersionManager")}static async checkForUpdates(){try{let n=(await en.get(`https://registry.npmjs.org/${u.PACKAGE_NAME}/latest`,{timeout:u.UPDATE_CHECK_TIMEOUT})).data.version,t=this.compareVersions(n,u.CURRENT_VERSION)>0;return t&&f.trackUpdateAvailable(u.CURRENT_VERSION,n),{current:u.CURRENT_VERSION,latest:n,shouldUpdate:t}}catch{return null}}static compareVersions(e,n){let t=e.split(".").map(Number),r=n.split(".").map(Number),c=Math.max(t.length,r.length);for(let g=0;g<c;g++){let A=t[g]||0,Y=r[g]||0;if(A>Y)return 1;if(A<Y)return-1}return 0}};var S=class{static{i(this,"TunnelOrchestrator")}static async start(e){l.setTunnel(null,e.subdomain,e.port,e.backendUrl),await f.initialize(),f.trackCliStart(e.port,e.subdomain,u.CURRENT_VERSION),m.displayStartupBanner(e.port);let n=await N.checkForUpdates();if(l.setUpdateInfo(n),!tn.existsSync(x.BIN_PATH)){console.log(J.yellow(`
|
|
64
|
+
\u{1F4E6} Cloudflared binary not found. Downloading...
|
|
65
|
+
`));try{await K()}catch(r){f.trackTunnelError("binary_download_failed",r.message),console.error(J.red(`
|
|
66
|
+
\u274C Failed to download cloudflared: ${r.message}`)),process.exit(1)}}v.validate(x.BIN_PATH)||(f.trackTunnelError("binary_missing","Cloudflared binary not found"),await new Promise(r=>setTimeout(r,100)),process.exit(1));let t=nn(s.t("creatingTunnel",{port:e.port})).start();try{let r=await _.createTunnel(e.subdomain,e.backendUrl);l.setTunnel(r.tunnelId,e.subdomain,e.port,e.backendUrl),f.trackTunnelCreated(e.subdomain,e.port),t.stop(),console.log(J.green(` ${s.t("tunnelLive")}`)),m.displayTunnelSuccess(r.url,e.port,n);let c=v.spawn(x.BIN_PATH,r.tunnelToken,e.port);l.setProcess(c),v.attachHandlers(c,t);let g=setTimeout(()=>{m.displayTimeoutWarning(),this.cleanup("timeout")},se);l.setTimeout(g)}catch(r){let c=r,g=c.message.includes("already taken")?"subdomain_taken":"tunnel_creation_failed";f.trackTunnelError(g,c.message),m.displayError(c,t),await new Promise(A=>setTimeout(A,100)),process.exit(1)}}static async cleanup(e="manual"){l.clearTimeout(),l.hasTunnel()||process.exit(0),m.displayCleanupStart();let n=l.getDurationSeconds();f.trackTunnelShutdown(e,n);try{l.hasProcess()&&l.tunnelProcess&&l.tunnelProcess.kill(),l.subdomain&&l.tunnelId&&await _.deleteTunnel(l.subdomain,l.tunnelId,l.backendUrl),m.displayCleanupSuccess()}catch{m.displayCleanupError()}await new Promise(t=>setTimeout(t,100)),process.exit(0)}};async function rn(){let o=on(s.t("checkingUpdates")).start(),e=await N.checkForUpdates();o.stop(),m.displayVersion(u.CURRENT_VERSION,e)}i(rn,"displayVersion");function sn(o){o==="clear"?(I.setBackendUrl(null),console.log(b.green("\u2714 Backend URL cleared. Using default backend.")),console.log(b.gray(` Default: https://api.nport.link
|
|
67
|
+
`))):(I.setBackendUrl(o),console.log(b.green("\u2714 Backend URL saved successfully!")),console.log(b.cyan(` Backend: ${o}`)),console.log(b.gray(`
|
|
68
|
+
This backend will be used for all future sessions.`)),console.log(b.gray(` To clear: nport --set-backend
|
|
69
|
+
`)));let e=I.getBackendUrl();e&&(console.log(b.white("Current configuration:")),console.log(b.cyan(` Saved backend: ${e}`)))}i(sn,"handleSetBackend");async function an(){try{let o=process.argv.slice(2),e=O.parse(o);await s.initialize(e.language),(o.includes("-v")||o.includes("--version"))&&(await rn(),process.exit(0)),e.setBackend&&(sn(e.setBackend),process.exit(0)),e.language==="prompt"&&(o.includes("--language")||o.includes("--lang")||o.includes("-l"))&&process.exit(0);let n=e.backendUrl;if(!n){let r=I.getBackendUrl();r&&(n=r)}let t={port:e.port,subdomain:e.subdomain,backendUrl:n,language:e.language};await S.start(t)}catch(o){console.error(`Fatal Error: ${o.message}`),process.exit(1)}}i(an,"main");process.on("SIGINT",()=>S.cleanup());process.on("SIGTERM",()=>S.cleanup());an();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nport",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.2",
|
|
4
4
|
"description": "Free & open source ngrok alternative - Tunnel HTTP/HTTPS connections via Cloudflare Edge with custom subdomains",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -63,13 +63,14 @@
|
|
|
63
63
|
}
|
|
64
64
|
],
|
|
65
65
|
"scripts": {
|
|
66
|
-
"build": "
|
|
67
|
-
"dev": "
|
|
66
|
+
"build": "node scripts/build.js",
|
|
67
|
+
"dev": "node scripts/build.js --watch",
|
|
68
68
|
"start": "node dist/index.js",
|
|
69
69
|
"postinstall": "node scripts/postinstall.js",
|
|
70
70
|
"test": "vitest run",
|
|
71
71
|
"test:watch": "vitest",
|
|
72
|
-
"lint": "tsc --noEmit"
|
|
72
|
+
"lint": "tsc --noEmit",
|
|
73
|
+
"typecheck": "tsc --noEmit"
|
|
73
74
|
},
|
|
74
75
|
"dependencies": {
|
|
75
76
|
"axios": "^1.13.2",
|
|
@@ -78,15 +79,14 @@
|
|
|
78
79
|
},
|
|
79
80
|
"devDependencies": {
|
|
80
81
|
"@types/node": "^22.0.0",
|
|
82
|
+
"esbuild": "^0.24.0",
|
|
81
83
|
"typescript": "^5.7.0",
|
|
82
84
|
"vitest": "~3.2.0"
|
|
83
85
|
},
|
|
84
86
|
"files": [
|
|
85
87
|
"dist/",
|
|
86
|
-
"
|
|
87
|
-
"scripts/",
|
|
88
|
+
"scripts/postinstall.js",
|
|
88
89
|
"README.md",
|
|
89
|
-
"CHANGELOG.md",
|
|
90
90
|
"LICENSE"
|
|
91
91
|
],
|
|
92
92
|
"os": [
|
package/CHANGELOG.md
DELETED
|
@@ -1,432 +0,0 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
All notable changes to NPort will be documented in this file.
|
|
4
|
-
|
|
5
|
-
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
-
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
-
|
|
8
|
-
## [2.1.0] - 2026-01-27
|
|
9
|
-
|
|
10
|
-
### Added
|
|
11
|
-
- 🔷 **Full TypeScript Migration**: Complete rewrite of CLI and Server in TypeScript
|
|
12
|
-
- Strict type checking enabled across the entire codebase
|
|
13
|
-
- All modules converted from JavaScript to TypeScript
|
|
14
|
-
- Type-safe interfaces for configuration, tunnels, analytics, and i18n
|
|
15
|
-
- Better IDE support with IntelliSense and autocompletion
|
|
16
|
-
- 📚 **Comprehensive Documentation**: New docs folder with detailed guides
|
|
17
|
-
- `docs/ARCHITECTURE.md`: System overview, component diagrams, and data flow
|
|
18
|
-
- `docs/API.md`: Complete API reference with endpoints and examples
|
|
19
|
-
- `docs/CONTRIBUTING.md`: Contribution guidelines and development setup
|
|
20
|
-
- 🤖 **AI Context Support**: Added `.ai/context.md` for AI-assisted development
|
|
21
|
-
- Project structure and key patterns documented
|
|
22
|
-
- Coding conventions and architecture decisions
|
|
23
|
-
- Makes AI pair programming more effective
|
|
24
|
-
- 🧪 **Unit Testing with Vitest**: Comprehensive test suite for CLI
|
|
25
|
-
- Tests for argument parsing (`tests/args.test.ts`)
|
|
26
|
-
- Tests for version comparison (`tests/version.test.ts`)
|
|
27
|
-
- Tests for state management (`tests/state.test.ts`)
|
|
28
|
-
- Easy to run with `npm test`
|
|
29
|
-
- 📋 **Code Ownership**: Added `.github/CODEOWNERS` file
|
|
30
|
-
- Clear ownership for code review assignments
|
|
31
|
-
- Faster PR reviews with automatic reviewer assignment
|
|
32
|
-
- 📝 **Cursor Rules**: Added `.cursorrules` for consistent AI assistance
|
|
33
|
-
- Project-specific coding conventions
|
|
34
|
-
- TypeScript and naming guidelines
|
|
35
|
-
- Common patterns and anti-patterns
|
|
36
|
-
- 🔄 **Auto-download Cloudflared**: Binary downloads automatically on first run
|
|
37
|
-
- No need to run separate install commands
|
|
38
|
-
- Seamless first-time user experience
|
|
39
|
-
- Progress indicator during download
|
|
40
|
-
- 🔒 **Protected Subdomain Support**: Enhanced error handling for reserved subdomains
|
|
41
|
-
- User-friendly error message when trying to create protected subdomains (like `api`)
|
|
42
|
-
- Formatted error output matching existing error style
|
|
43
|
-
- Helpful suggestions to use alternative subdomain names
|
|
44
|
-
- Prevents accidental use of backend service subdomains
|
|
45
|
-
- 📋 **TODO Management**: Added `TODO.md` for tracking planned features
|
|
46
|
-
- Move time limit enforcement to backend
|
|
47
|
-
- Update README powered-by section
|
|
48
|
-
- Track terminal link clicks
|
|
49
|
-
|
|
50
|
-
### Changed
|
|
51
|
-
- 🏗️ **Project Structure**: Reorganized for better maintainability
|
|
52
|
-
- All source code in `src/` directory
|
|
53
|
-
- Type definitions in `src/types/`
|
|
54
|
-
- Shared constants in `src/constants.ts`
|
|
55
|
-
- Tests in `tests/` directory
|
|
56
|
-
- 📦 **Build System**: Updated to TypeScript compilation
|
|
57
|
-
- Uses `tsc` for compilation
|
|
58
|
-
- Output to `dist/` directory
|
|
59
|
-
- Source maps for debugging
|
|
60
|
-
- 🔧 **Development Workflow**: Improved developer experience
|
|
61
|
-
- `npm run dev` for watch mode
|
|
62
|
-
- `npm run build` for production
|
|
63
|
-
- `npm test` for running tests
|
|
64
|
-
- `npm run lint` for type checking
|
|
65
|
-
- ⚙️ **System Requirements**: Updated to Node.js 20+
|
|
66
|
-
- Minimum Node.js version: 20.0.0
|
|
67
|
-
- Minimum npm version: 10.0.0
|
|
68
|
-
- Better security and performance with latest LTS
|
|
69
|
-
|
|
70
|
-
### Improved
|
|
71
|
-
- ✨ **Better Error Messages**: Enhanced user feedback for protected subdomains
|
|
72
|
-
- Catches `SUBDOMAIN_PROTECTED` errors from backend
|
|
73
|
-
- Formats error messages consistently with other error types
|
|
74
|
-
- Shows actionable options when subdomain is reserved
|
|
75
|
-
|
|
76
|
-
### Fixed
|
|
77
|
-
- 🐛 **Intel Mac Binary Download**: Fixed cloudflared binary download on Intel Macs
|
|
78
|
-
- Node.js reports architecture as `x64`, not `amd64` - now correctly mapped
|
|
79
|
-
- Fixed ARM64 Macs to download the correct `cloudflared-darwin-arm64.tgz` binary
|
|
80
|
-
- Previously ARM64 Macs were incorrectly downloading the AMD64 binary
|
|
81
|
-
|
|
82
|
-
### Server (v1.1.0)
|
|
83
|
-
- 🔷 **TypeScript Migration**: Server fully converted to TypeScript
|
|
84
|
-
- Type-safe Cloudflare Worker handlers
|
|
85
|
-
- Properly typed API responses
|
|
86
|
-
- Full type coverage for Cloudflare API interactions
|
|
87
|
-
- 🧪 **Server Tests**: Updated test suite for TypeScript
|
|
88
|
-
- Vitest with Cloudflare Workers pool
|
|
89
|
-
- Tests for all API endpoints
|
|
90
|
-
- Scheduled task testing
|
|
91
|
-
- 🔒 **Protected Subdomains**: Infrastructure to protect critical subdomains from deletion or overwriting
|
|
92
|
-
- New `PROTECTED_SUBDOMAINS` constant array for easy management of reserved subdomains
|
|
93
|
-
- Default protected subdomain: `api` (reserved for NPort backend service)
|
|
94
|
-
- Easy to add more protected subdomains by updating the array
|
|
95
|
-
- Protected subdomains are blocked at both creation and cleanup stages
|
|
96
|
-
- 🛡️ **Backend Service Protection**: Prevents accidental deletion or overwriting of production services
|
|
97
|
-
- `api` subdomain is now protected from user creation attempts
|
|
98
|
-
- Scheduled cleanup job skips protected subdomains
|
|
99
|
-
- Returns clear error message when users try to use protected names
|
|
100
|
-
- 🔧 **Manual Cleanup Endpoint**: Added `GET /scheduled` endpoint to manually trigger cleanup
|
|
101
|
-
- Useful for testing and on-demand cleanup
|
|
102
|
-
- Respects protected subdomains configuration
|
|
103
|
-
- Returns JSON response with cleanup results
|
|
104
|
-
|
|
105
|
-
### Technical Details
|
|
106
|
-
- **Type System**:
|
|
107
|
-
- `TunnelConfig`, `TunnelState`, `ConnectionInfo` interfaces
|
|
108
|
-
- `AnalyticsParams`, `VersionInfo`, `I18nStrings` types
|
|
109
|
-
- `LogPatterns` with readonly arrays for constants
|
|
110
|
-
- **ESM Compliance**: All imports use `.js` extensions as required
|
|
111
|
-
- **Constants**: Centralized in `src/constants.ts` with `as const` assertions
|
|
112
|
-
- **Error Handling**: Type-safe error boundaries throughout
|
|
113
|
-
|
|
114
|
-
### Migration
|
|
115
|
-
- Automatic migration - no manual steps required
|
|
116
|
-
- Existing configuration in `~/.nport/config.json` remains compatible
|
|
117
|
-
- All CLI flags and options unchanged
|
|
118
|
-
|
|
119
|
-
### Breaking Changes
|
|
120
|
-
- None - fully backward compatible!
|
|
121
|
-
|
|
122
|
-
## [2.0.7] - 2026-01-14
|
|
123
|
-
|
|
124
|
-
### Added
|
|
125
|
-
- 🌐 **Smart Network Warning System**: Intelligent handling of QUIC/network connectivity issues
|
|
126
|
-
- Automatic detection and filtering of QUIC protocol errors
|
|
127
|
-
- User-friendly warning messages instead of scary red error spam
|
|
128
|
-
- Bilingual support (English & Vietnamese)
|
|
129
|
-
- Smart throttling: Shows warning only after 5 errors, max once per 30 seconds
|
|
130
|
-
- Clear explanations of what's happening and how to fix it
|
|
131
|
-
- Automatic reset when connection is restored
|
|
132
|
-
- 🔒 **Protected Subdomain Support**: Enhanced error handling for reserved subdomains
|
|
133
|
-
- User-friendly error message when trying to create protected subdomains (like `api`)
|
|
134
|
-
- Formatted error output matching existing error style
|
|
135
|
-
- Helpful suggestions to use alternative subdomain names
|
|
136
|
-
- Prevents accidental use of backend service subdomains
|
|
137
|
-
|
|
138
|
-
### Improved
|
|
139
|
-
- 🔇 **Cleaner Terminal Output**: No more error spam from cloudflared
|
|
140
|
-
- QUIC timeout errors are now silently tracked instead of displayed
|
|
141
|
-
- Network warnings filtered: "failed to accept QUIC stream", "timeout: no recent network activity", etc.
|
|
142
|
-
- Only shows meaningful warnings when there's an actual persistent issue
|
|
143
|
-
- Terminal stays clean and readable during normal operation
|
|
144
|
-
- 📡 **Better User Communication**: Context-aware network issue reporting
|
|
145
|
-
- Explains that QUIC failures are usually not critical
|
|
146
|
-
- Tunnel continues working via HTTP/2 fallback
|
|
147
|
-
- Provides actionable troubleshooting steps
|
|
148
|
-
- Reassures users that the tunnel is still functional
|
|
149
|
-
- ✨ **Better Error Messages**: Enhanced user feedback for protected subdomains
|
|
150
|
-
- Catches `SUBDOMAIN_PROTECTED` errors from backend
|
|
151
|
-
- Formats error messages consistently with other error types
|
|
152
|
-
- Shows actionable options when subdomain is reserved
|
|
153
|
-
|
|
154
|
-
### Technical Details
|
|
155
|
-
- **Network Error Patterns**: Added detection for 7 common QUIC/network error patterns
|
|
156
|
-
- **State Management**: New network issue tracking in application state
|
|
157
|
-
- `networkIssueCount`: Counter for network errors
|
|
158
|
-
- `lastNetworkWarningTime`: Timestamp tracking for cooldown
|
|
159
|
-
- `shouldShowNetworkWarning()`: Smart decision logic
|
|
160
|
-
- **Configuration**: New `NETWORK_CONFIG` with threshold and cooldown settings
|
|
161
|
-
- **Bilingual Messages**: Complete translations for all network warning messages
|
|
162
|
-
- **Protected Subdomain Handling**: Enhanced error handling in `src/api.js`
|
|
163
|
-
- Added check for `SUBDOMAIN_PROTECTED` error type
|
|
164
|
-
- Consistent formatting with existing error messages
|
|
165
|
-
- Clear user guidance for alternative subdomain choices
|
|
166
|
-
|
|
167
|
-
### User Experience
|
|
168
|
-
**Before:**
|
|
169
|
-
```
|
|
170
|
-
[Cloudflared] 2026-01-14T04:33:02Z ERR failed to accept QUIC stream...
|
|
171
|
-
[Cloudflared] 2026-01-14T04:33:03Z ERR failed to accept QUIC stream...
|
|
172
|
-
[Cloudflared] 2026-01-14T04:33:04Z ERR failed to accept QUIC stream...
|
|
173
|
-
[Cloudflared] 2026-01-14T04:33:05Z ERR failed to accept QUIC stream...
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
**After:**
|
|
177
|
-
```
|
|
178
|
-
✔ [1/2] Connection established...
|
|
179
|
-
✔ [2/2] Compression enabled...
|
|
180
|
-
|
|
181
|
-
⚠️ NETWORK CONNECTIVITY ISSUE DETECTED
|
|
182
|
-
Cloudflared is having trouble maintaining a stable connection...
|
|
183
|
-
📡 Your tunnel is still working, but connection quality may be affected.
|
|
184
|
-
|
|
185
|
-
💡 Possible reasons:
|
|
186
|
-
• Unstable internet connection or high packet loss
|
|
187
|
-
• Firewall/Router blocking UDP traffic (QUIC protocol)
|
|
188
|
-
• ISP throttling or network congestion
|
|
189
|
-
|
|
190
|
-
🔧 What to try:
|
|
191
|
-
• Check your internet connection stability
|
|
192
|
-
• Try connecting from a different network
|
|
193
|
-
• Disable VPN/Proxy if you're using one
|
|
194
|
-
|
|
195
|
-
ℹ️ This is usually not critical - your tunnel should continue working normally.
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
## [2.0.6] - 2026-01-13
|
|
199
|
-
|
|
200
|
-
### Added
|
|
201
|
-
- 🔧 **Backend URL Configuration**: Full control over backend server
|
|
202
|
-
- `--backend` / `-b` flag for temporary backend override
|
|
203
|
-
- `--set-backend` command to save backend URL permanently
|
|
204
|
-
- `NPORT_BACKEND_URL` environment variable support
|
|
205
|
-
- Saved backend configuration persists across sessions
|
|
206
|
-
- Priority system: CLI flag > Saved config > Env var > Default
|
|
207
|
-
- 🗂️ **Unified Configuration System**: All settings in one place
|
|
208
|
-
- New centralized `config-manager.js` module
|
|
209
|
-
- All preferences stored in `~/.nport/config.json`
|
|
210
|
-
- Automatic migration from old format (v2.0.5)
|
|
211
|
-
- Easy to read and manually edit JSON format
|
|
212
|
-
- 🌐 **New Default Backend**: Updated to `api.nport.link`
|
|
213
|
-
- Professional domain structure
|
|
214
|
-
- Better branding alignment
|
|
215
|
-
- Shorter and easier to remember
|
|
216
|
-
|
|
217
|
-
### Changed
|
|
218
|
-
- 📝 **Language Configuration**: Now uses unified config system
|
|
219
|
-
- Language setting moved from `~/.nport/lang` to `~/.nport/config.json`
|
|
220
|
-
- Automatic migration from old file format
|
|
221
|
-
- Consistent configuration approach across all settings
|
|
222
|
-
- 📚 **Documentation Updates**: Complete overhaul
|
|
223
|
-
- Updated `README.md` with backend configuration options
|
|
224
|
-
- New `CLIENT_SETUP.md` focused on npm installation and backend setup
|
|
225
|
-
- Comprehensive backend URL documentation
|
|
226
|
-
- Clear priority order explanation
|
|
227
|
-
|
|
228
|
-
### Improved
|
|
229
|
-
- 🎯 **Consistency**: Unified approach to all configuration
|
|
230
|
-
- Backend URL and language now use same storage system
|
|
231
|
-
- Single config file for all preferences
|
|
232
|
-
- Cleaner architecture and code organization
|
|
233
|
-
- 💾 **Configuration File Structure**:
|
|
234
|
-
```json
|
|
235
|
-
{
|
|
236
|
-
"backendUrl": "https://api.nport.link",
|
|
237
|
-
"language": "en"
|
|
238
|
-
}
|
|
239
|
-
```
|
|
240
|
-
|
|
241
|
-
### Migration
|
|
242
|
-
- Automatic migration from v2.0.5 configuration files
|
|
243
|
-
- Old `~/.nport/lang` file automatically migrated to `config.json`
|
|
244
|
-
- No manual steps required
|
|
245
|
-
- Old files removed after successful migration
|
|
246
|
-
|
|
247
|
-
## [2.0.5] - 2026-01-13
|
|
248
|
-
|
|
249
|
-
### Added
|
|
250
|
-
- 🌍 **Multilingual Support**: Full internationalization with English and Vietnamese
|
|
251
|
-
- Interactive language selection on first run
|
|
252
|
-
- `--language` / `-l` flag to change language anytime
|
|
253
|
-
- Language preference saved to `~/.nport/lang`
|
|
254
|
-
- 🎨 **Enhanced UI**: Complete redesign with better visual hierarchy
|
|
255
|
-
- New header design with Vietnamese pride
|
|
256
|
-
- Improved connection status messages
|
|
257
|
-
- Better formatted output with consistent spacing
|
|
258
|
-
- Language change hint in goodbye message
|
|
259
|
-
- 📂 **Modular Architecture**: Refactored codebase into organized modules
|
|
260
|
-
- Split code into `/src` directory with clear separation of concerns
|
|
261
|
-
- Better maintainability and testability
|
|
262
|
-
- Modules: config, state, args, binary, api, version, ui, tunnel, lang, analytics
|
|
263
|
-
- ✅ **Version Command**: Added `-v` / `--version` flag
|
|
264
|
-
- Shows current version
|
|
265
|
-
- Checks for updates automatically
|
|
266
|
-
- Displays update instructions if available
|
|
267
|
-
- 📝 **Configuration Directory**: Organized config files under `~/.nport/`
|
|
268
|
-
- Language preference: `~/.nport/lang`
|
|
269
|
-
- Analytics ID: `~/.nport/analytics-id`
|
|
270
|
-
|
|
271
|
-
### Changed
|
|
272
|
-
- 🎯 **Improved Console Output**: Better formatting and spacing throughout
|
|
273
|
-
- Connection messages now properly indented
|
|
274
|
-
- Time remaining display updated
|
|
275
|
-
- Footer redesigned with clearer calls-to-action
|
|
276
|
-
- 🔧 **Better Argument Parsing**: Enhanced CLI argument handling
|
|
277
|
-
- Support for multiple language flag formats
|
|
278
|
-
- Interactive prompt when using `--language` without value
|
|
279
|
-
- 📚 **Updated Documentation**: Complete README overhaul
|
|
280
|
-
- Added multilingual feature documentation
|
|
281
|
-
- New CLI options table
|
|
282
|
-
- Project structure documentation
|
|
283
|
-
- Cleaner examples and better organization
|
|
284
|
-
|
|
285
|
-
### Fixed
|
|
286
|
-
- 🐛 **Terminal Compatibility**: Removed problematic emoji flags
|
|
287
|
-
- Vietnamese flag emoji replaced with text
|
|
288
|
-
- Better compatibility across different terminals
|
|
289
|
-
|
|
290
|
-
## [2.0.4] - Previous Release
|
|
291
|
-
|
|
292
|
-
### Features
|
|
293
|
-
- Basic tunnel functionality
|
|
294
|
-
- Custom subdomain support
|
|
295
|
-
- Cloudflare integration
|
|
296
|
-
- Auto-cleanup on exit
|
|
297
|
-
- 4-hour tunnel timeout
|
|
298
|
-
|
|
299
|
-
---
|
|
300
|
-
|
|
301
|
-
## Version Upgrade Guide
|
|
302
|
-
|
|
303
|
-
### From 2.0.7 to 2.1.0
|
|
304
|
-
|
|
305
|
-
```bash
|
|
306
|
-
npm install -g nport@latest
|
|
307
|
-
```
|
|
308
|
-
|
|
309
|
-
**What's New:**
|
|
310
|
-
|
|
311
|
-
1. **Full TypeScript Rewrite**
|
|
312
|
-
- Both CLI and Server now fully typed
|
|
313
|
-
- Better IDE support and autocompletion
|
|
314
|
-
- Catches errors at compile time
|
|
315
|
-
|
|
316
|
-
2. **Comprehensive Documentation**
|
|
317
|
-
- Architecture guide in `docs/ARCHITECTURE.md`
|
|
318
|
-
- API reference in `docs/API.md`
|
|
319
|
-
- Contributing guide in `docs/CONTRIBUTING.md`
|
|
320
|
-
|
|
321
|
-
3. **Unit Testing**
|
|
322
|
-
- Run tests with `npm test`
|
|
323
|
-
- Covers argument parsing, version checks, state management
|
|
324
|
-
|
|
325
|
-
4. **Auto-download Cloudflared**
|
|
326
|
-
- Binary downloads automatically on first `nport` run
|
|
327
|
-
- No separate install step needed
|
|
328
|
-
|
|
329
|
-
5. **AI-Friendly Codebase**
|
|
330
|
-
- `.ai/context.md` for AI assistants
|
|
331
|
-
- `.cursorrules` for consistent AI suggestions
|
|
332
|
-
- JSDoc comments throughout
|
|
333
|
-
|
|
334
|
-
**For Contributors:**
|
|
335
|
-
```bash
|
|
336
|
-
git clone https://github.com/tuanngocptn/nport
|
|
337
|
-
cd nport
|
|
338
|
-
npm install
|
|
339
|
-
npm run build
|
|
340
|
-
npm run dev # Watch mode
|
|
341
|
-
```
|
|
342
|
-
|
|
343
|
-
**Breaking Changes:** None - fully backward compatible!
|
|
344
|
-
|
|
345
|
-
### From 2.0.6 to 2.0.7
|
|
346
|
-
|
|
347
|
-
```bash
|
|
348
|
-
npm install -g nport@latest
|
|
349
|
-
```
|
|
350
|
-
|
|
351
|
-
**What's New:**
|
|
352
|
-
|
|
353
|
-
1. **Cleaner Terminal Experience**
|
|
354
|
-
- No more scary red QUIC error spam
|
|
355
|
-
- Smart network warnings when needed
|
|
356
|
-
- Automatic fallback to HTTP/2 when QUIC fails
|
|
357
|
-
|
|
358
|
-
2. **Better Error Communication**
|
|
359
|
-
- Understand what's happening with your connection
|
|
360
|
-
- Clear explanations in your language (EN/VI)
|
|
361
|
-
- Actionable troubleshooting steps
|
|
362
|
-
|
|
363
|
-
3. **When You'll See Warnings**
|
|
364
|
-
- Only after multiple network issues (not just one)
|
|
365
|
-
- Maximum once every 30 seconds (no spam)
|
|
366
|
-
- Automatically disappears when connection improves
|
|
367
|
-
|
|
368
|
-
**Breaking Changes:** None - fully backward compatible!
|
|
369
|
-
|
|
370
|
-
### From 2.0.5 to 2.0.6
|
|
371
|
-
|
|
372
|
-
```bash
|
|
373
|
-
npm install -g nport@latest
|
|
374
|
-
```
|
|
375
|
-
|
|
376
|
-
**New Features to Try:**
|
|
377
|
-
|
|
378
|
-
1. **Set Your Backend Permanently**
|
|
379
|
-
```bash
|
|
380
|
-
nport --set-backend https://api.nport.link
|
|
381
|
-
```
|
|
382
|
-
|
|
383
|
-
2. **Use Custom Backend Temporarily**
|
|
384
|
-
```bash
|
|
385
|
-
nport 3000 -b https://your-backend.com
|
|
386
|
-
```
|
|
387
|
-
|
|
388
|
-
3. **Check Current Configuration**
|
|
389
|
-
```bash
|
|
390
|
-
cat ~/.nport/config.json
|
|
391
|
-
```
|
|
392
|
-
|
|
393
|
-
4. **Use Environment Variable**
|
|
394
|
-
```bash
|
|
395
|
-
export NPORT_BACKEND_URL=https://your-backend.com
|
|
396
|
-
nport 3000
|
|
397
|
-
```
|
|
398
|
-
|
|
399
|
-
**Breaking Changes:** None - fully backward compatible!
|
|
400
|
-
|
|
401
|
-
**Migration:** Your language preference from v2.0.5 will be automatically migrated to the new unified config format.
|
|
402
|
-
|
|
403
|
-
### From 2.0.4 to 2.0.5
|
|
404
|
-
|
|
405
|
-
```bash
|
|
406
|
-
npm install -g nport@latest
|
|
407
|
-
```
|
|
408
|
-
|
|
409
|
-
**New Features to Try:**
|
|
410
|
-
|
|
411
|
-
1. **Change Language**
|
|
412
|
-
```bash
|
|
413
|
-
nport --language
|
|
414
|
-
```
|
|
415
|
-
|
|
416
|
-
2. **Check Version**
|
|
417
|
-
```bash
|
|
418
|
-
nport -v
|
|
419
|
-
```
|
|
420
|
-
|
|
421
|
-
3. **Direct Language Selection**
|
|
422
|
-
```bash
|
|
423
|
-
nport 3000 -l vi # Vietnamese
|
|
424
|
-
nport 3000 -l en # English
|
|
425
|
-
```
|
|
426
|
-
|
|
427
|
-
**Breaking Changes:** None - fully backward compatible!
|
|
428
|
-
|
|
429
|
-
---
|
|
430
|
-
|
|
431
|
-
Made with ❤️ in Vietnam by [Nick Pham](https://github.com/tuanngocptn)
|
|
432
|
-
|