9remote 0.1.62 → 0.1.63
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/README.md +119 -0
- package/dist/cli.cjs +2 -2
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# 9remote
|
|
4
|
+
|
|
5
|
+
**Your Mac/Linux terminal in your pocket — anywhere, instantly**
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/9remote)
|
|
8
|
+
[](LICENSE)
|
|
9
|
+
[](https://www.npmjs.com/package/9remote)
|
|
10
|
+
|
|
11
|
+
[🌐 Web App](https://9remote.cc) • [📖 Docs](https://docs.9remote.cc) • [🚀 Get Started](#-quick-start)
|
|
12
|
+
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Why 9remote?
|
|
18
|
+
|
|
19
|
+
- ❌ **SSH is a hassle** — need to open firewall, configure port forwarding, manage keys
|
|
20
|
+
- ❌ **VPN is overkill** — too complex to set up just to check a terminal
|
|
21
|
+
- ❌ **ngrok / tunnels expire** — lose connection, have to restart everything
|
|
22
|
+
|
|
23
|
+
**9remote solves it:**
|
|
24
|
+
- ✅ **One command** — `npx 9remote`, scan QR, done
|
|
25
|
+
- ✅ **Auto tunnel** — Cloudflare tunnel starts automatically, no port forwarding
|
|
26
|
+
- ✅ **Works on phone** — full terminal from your browser, under 50ms latency
|
|
27
|
+
- ✅ **Remote Desktop** — view and control your screen, mouse & keyboard
|
|
28
|
+
- ✅ **Persistent sessions** — PTY daemon survives server restarts
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## ⚡ Quick Start
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npx 9remote
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Scan the QR code on your phone → you're in.
|
|
39
|
+
|
|
40
|
+
Or install globally:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npm install -g 9remote
|
|
44
|
+
9remote
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
> **Ready in 30 seconds. No config needed.**
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## ✨ Features
|
|
52
|
+
|
|
53
|
+
- 🖥️ **Remote Terminal** — Full PTY shell, always on, persistent across reconnects
|
|
54
|
+
- 🖱️ **Remote Desktop** — Live screen streaming via WebRTC + mouse/keyboard control
|
|
55
|
+
- 📁 **File Explorer** — Browse, upload, download files from your browser
|
|
56
|
+
- 📱 **QR Login** — One-time key (30 min) for quick mobile access
|
|
57
|
+
- 🔒 **E2E Secure** — All traffic through Cloudflare tunnel, no open ports
|
|
58
|
+
- 🔄 **Auto Restart** — Server + tunnel auto-recover on crash
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## 📖 CLI Commands
|
|
63
|
+
|
|
64
|
+
| Command | Description |
|
|
65
|
+
|---|---|
|
|
66
|
+
| `9remote` | TUI mode — interactive menu with QR |
|
|
67
|
+
| `9remote ui` | Web UI mode — open browser dashboard |
|
|
68
|
+
| `9remote start` | Auto start server + tunnel (headless) |
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
<details>
|
|
73
|
+
<summary><b>🔧 How it works</b></summary>
|
|
74
|
+
|
|
75
|
+
1. **Start** — `9remote` spawns a local server on port `2208`
|
|
76
|
+
2. **Tunnel** — A Cloudflare Quick Tunnel is created automatically (no account needed)
|
|
77
|
+
3. **QR** — A one-time login link is generated and shown as QR code
|
|
78
|
+
4. **Connect** — Scan from your phone → authenticated session via [9remote.cc](https://9remote.cc)
|
|
79
|
+
5. **Transport** — Terminal uses WebSocket; Remote Desktop uses WebRTC DataChannel for low latency
|
|
80
|
+
|
|
81
|
+
</details>
|
|
82
|
+
|
|
83
|
+
<details>
|
|
84
|
+
<summary><b>📡 Remote Desktop</b></summary>
|
|
85
|
+
|
|
86
|
+
- Screen streaming via WebRTC (adaptive: 60ms active / 400ms idle)
|
|
87
|
+
- Tile-based diff rendering — only changed regions are sent
|
|
88
|
+
- Mouse & keyboard control via `robotjs`
|
|
89
|
+
- Requires macOS permissions: **Screen Recording** + **Accessibility**
|
|
90
|
+
|
|
91
|
+
Enable from TUI menu: `Remote Desktop → Toggle ON`
|
|
92
|
+
|
|
93
|
+
</details>
|
|
94
|
+
|
|
95
|
+
<details>
|
|
96
|
+
<summary><b>🔑 Keys & Security</b></summary>
|
|
97
|
+
|
|
98
|
+
- **Permanent Key** — stored locally, tied to your machine ID
|
|
99
|
+
- **One-Time Key** — 30-minute temporary key for quick phone access
|
|
100
|
+
- Keys are never stored on our servers after session ends
|
|
101
|
+
- Regenerate your key anytime from the TUI menu
|
|
102
|
+
|
|
103
|
+
</details>
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## 🛠️ Built With
|
|
108
|
+
|
|
109
|
+
- [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) — Zero-config secure tunnel
|
|
110
|
+
- [node-datachannel](https://github.com/murat-dogan/node-datachannel) — WebRTC for low-latency desktop streaming
|
|
111
|
+
- [node-pty](https://github.com/microsoft/node-pty) — Persistent PTY terminal sessions
|
|
112
|
+
- [Socket.IO](https://socket.io/) — Real-time terminal + signaling
|
|
113
|
+
- [Preact](https://preactjs.com/) — Lightweight Web UI
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## 📝 License
|
|
118
|
+
|
|
119
|
+
MIT © [9remote](https://github.com/decolua/9remote)
|
package/dist/cli.cjs
CHANGED
|
@@ -9,7 +9,7 @@ var Zn=Object.create;var Dt=Object.defineProperty;var er=Object.getOwnPropertyDe
|
|
|
9
9
|
`:`
|
|
10
10
|
`)+t,e=o+1,o=r.indexOf(`
|
|
11
11
|
`,e)}while(o!==-1);return i+=r.slice(e),i}var{stdout:Yt,stderr:Vt}=Ht,ft=Symbol("GENERATOR"),Te=Symbol("STYLER"),Ie=Symbol("IS_EMPTY"),Jt=["ansi","ansi","ansi256","ansi16m"],Ee=Object.create(null),hr=(r,n={})=>{if(n.level&&!(Number.isInteger(n.level)&&n.level>=0&&n.level<=3))throw new Error("The `level` option should be an integer from 0 to 3");let t=Yt?Yt.level:0;r.level=n.level===void 0?t:n.level};var dr=r=>{let n=(...t)=>t.join(" ");return hr(n,r),Object.setPrototypeOf(n,Me.prototype),n};function Me(r){return dr(r)}Object.setPrototypeOf(Me.prototype,Function.prototype);for(let[r,n]of Object.entries(Z))Ee[r]={get(){let t=Qe(this,ht(n.open,n.close,this[Te]),this[Ie]);return Object.defineProperty(this,r,{value:t}),t}};Ee.visible={get(){let r=Qe(this,this[Te],!0);return Object.defineProperty(this,"visible",{value:r}),r}};var pt=(r,n,t,...o)=>r==="rgb"?n==="ansi16m"?Z[t].ansi16m(...o):n==="ansi256"?Z[t].ansi256(Z.rgbToAnsi256(...o)):Z[t].ansi(Z.rgbToAnsi(...o)):r==="hex"?pt("rgb",n,t,...Z.hexToRgb(...o)):Z[t][r](...o),mr=["rgb","hex","ansi256"];for(let r of mr){Ee[r]={get(){let{level:t}=this;return function(...o){let e=ht(pt(r,Jt[t],"color",...o),Z.color.close,this[Te]);return Qe(this,e,this[Ie])}}};let n="bg"+r[0].toUpperCase()+r.slice(1);Ee[n]={get(){let{level:t}=this;return function(...o){let e=ht(pt(r,Jt[t],"bgColor",...o),Z.bgColor.close,this[Te]);return Qe(this,e,this[Ie])}}}}var gr=Object.defineProperties(()=>{},{...Ee,level:{enumerable:!0,get(){return this[ft].level},set(r){this[ft].level=r}}}),ht=(r,n,t)=>{let o,e;return t===void 0?(o=r,e=n):(o=t.openAll+r,e=n+t.closeAll),{open:r,close:n,openAll:o,closeAll:e,parent:t}},Qe=(r,n,t)=>{let o=(...e)=>yr(o,e.length===1?""+e[0]:e.join(" "));return Object.setPrototypeOf(o,gr),o[ft]=r,o[Te]=n,o[Ie]=t,o},yr=(r,n)=>{if(r.level<=0||!n)return r[Ie]?"":n;let t=r[Te];if(t===void 0)return n;let{openAll:o,closeAll:e}=t;if(n.includes("\x1B"))for(;t!==void 0;)n=Wt(n,t.close,t.open),t=t.parent;let i=n.indexOf(`
|
|
12
|
-
`);return i!==-1&&(n=Qt(n,e,o,i)),o+n+e};Object.defineProperties(Me.prototype,Ee);var vr=Me(),yo=Me({level:Vt?Vt.level:0});var v=vr;var Mt=M(Tn(),1),ke=require("child_process"),ge=M(require("path"),1),Vn=require("url"),st=M(require("fs"),1),Jn=M(require("os"),1);var bn=M(En(),1),Tt=M(require("crypto"),1),{machineIdSync:Rr}=bn.default;async function be(r=null){let n=r||process.env.MACHINE_ID_SALT||"9remote-salt";try{let t=Rr();return Tt.default.createHash("sha256").update(t+n).digest("hex").substring(0,16)}catch(t){return console.error("Error getting machine ID:",t),Tt.default.randomUUID().replace(/-/g,"").substring(0,16)}}var _n=M(require("crypto"),1),Ar=process.env.API_KEY_SECRET||"9remote-api-key-secret";function Or(){let r="abcdefghijklmnopqrstuvwxyz0123456789",n="";for(let t=0;t<4;t++)n+=r.charAt(Math.floor(Math.random()*r.length));return n}function Pr(r,n){return _n.default.createHmac("sha256",Ar).update(r+n).digest("hex").slice(0,6)}function _e(r){let n=r.slice(0,8),t=Or(),o=Pr(n,t);return{key:`sk-${n}-${t}-${o}`,keyId:t}}var Q=M(require("fs"),1),Ne=M(require("path"),1),xn=M(require("os"),1),$e=Ne.default.join(xn.default.homedir(),".9remote"),Et=Ne.default.join($e,"state.json"),bt=Ne.default.join($e,"keys.json"),qe=Ne.default.join($e,"cmd.json");function Xe(){Q.default.existsSync($e)||Q.default.mkdirSync($e,{recursive:!0})}function _t(r){try{Xe(),Q.default.writeFileSync(Et,JSON.stringify(r,null,2))}catch(n){console.error("Error saving state:",n)}}function Sn(){try{Q.default.existsSync(Et)&&Q.default.unlinkSync(Et)}catch{}}function Be(){try{return Xe(),Q.default.existsSync(bt)?JSON.parse(Q.default.readFileSync(bt,"utf8")):{machineId:null,key:null,name:"Default",createdAt:null}}catch{return{machineId:null,key:null,name:"Default",createdAt:null}}}function kn(r){try{Xe(),Q.default.writeFileSync(qe,JSON.stringify({cmd:r,ts:Date.now()}))}catch{}}function Rn(){try{if(!Q.default.existsSync(qe))return null;let r=JSON.parse(Q.default.readFileSync(qe,"utf8"));return Q.default.unlinkSync(qe),r.cmd}catch{return null}}function xe(r,n,t="Default"){try{Xe();let o={machineId:r,key:n,name:t,createdAt:new Date().toISOString()};return Q.default.writeFileSync(bt,JSON.stringify(o,null,2)),o}catch(o){return console.error("Error saving key:",o),null}}async function De(r,n){try{let t=await fetch(`${n}/api/temp-key/create`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({apiKey:r,expiryMinutes:30})});if(!t.ok){let o=await t.json();throw new Error(o.error||"Failed to create temp key")}return await t.json()}catch(t){return console.error("Error creating temp key:",t),null}}var he=require("fs"),An=require("url"),On=require("child_process"),je=M(require("path"),1),xt=M(require("os"),1),Cr=je.default.dirname((0,An.fileURLToPath)(__importMetaUrl)),Y="9remote",Pn=`https://registry.npmjs.org/${Y}/latest`,Cn=3e3,Ir=8e3;function In(){return"0.1.
|
|
12
|
+
`);return i!==-1&&(n=Qt(n,e,o,i)),o+n+e};Object.defineProperties(Me.prototype,Ee);var vr=Me(),yo=Me({level:Vt?Vt.level:0});var v=vr;var Mt=M(Tn(),1),ke=require("child_process"),ge=M(require("path"),1),Vn=require("url"),st=M(require("fs"),1),Jn=M(require("os"),1);var bn=M(En(),1),Tt=M(require("crypto"),1),{machineIdSync:Rr}=bn.default;async function be(r=null){let n=r||process.env.MACHINE_ID_SALT||"9remote-salt";try{let t=Rr();return Tt.default.createHash("sha256").update(t+n).digest("hex").substring(0,16)}catch(t){return console.error("Error getting machine ID:",t),Tt.default.randomUUID().replace(/-/g,"").substring(0,16)}}var _n=M(require("crypto"),1),Ar=process.env.API_KEY_SECRET||"9remote-api-key-secret";function Or(){let r="abcdefghijklmnopqrstuvwxyz0123456789",n="";for(let t=0;t<4;t++)n+=r.charAt(Math.floor(Math.random()*r.length));return n}function Pr(r,n){return _n.default.createHmac("sha256",Ar).update(r+n).digest("hex").slice(0,6)}function _e(r){let n=r.slice(0,8),t=Or(),o=Pr(n,t);return{key:`sk-${n}-${t}-${o}`,keyId:t}}var Q=M(require("fs"),1),Ne=M(require("path"),1),xn=M(require("os"),1),$e=Ne.default.join(xn.default.homedir(),".9remote"),Et=Ne.default.join($e,"state.json"),bt=Ne.default.join($e,"keys.json"),qe=Ne.default.join($e,"cmd.json");function Xe(){Q.default.existsSync($e)||Q.default.mkdirSync($e,{recursive:!0})}function _t(r){try{Xe(),Q.default.writeFileSync(Et,JSON.stringify(r,null,2))}catch(n){console.error("Error saving state:",n)}}function Sn(){try{Q.default.existsSync(Et)&&Q.default.unlinkSync(Et)}catch{}}function Be(){try{return Xe(),Q.default.existsSync(bt)?JSON.parse(Q.default.readFileSync(bt,"utf8")):{machineId:null,key:null,name:"Default",createdAt:null}}catch{return{machineId:null,key:null,name:"Default",createdAt:null}}}function kn(r){try{Xe(),Q.default.writeFileSync(qe,JSON.stringify({cmd:r,ts:Date.now()}))}catch{}}function Rn(){try{if(!Q.default.existsSync(qe))return null;let r=JSON.parse(Q.default.readFileSync(qe,"utf8"));return Q.default.unlinkSync(qe),r.cmd}catch{return null}}function xe(r,n,t="Default"){try{Xe();let o={machineId:r,key:n,name:t,createdAt:new Date().toISOString()};return Q.default.writeFileSync(bt,JSON.stringify(o,null,2)),o}catch(o){return console.error("Error saving key:",o),null}}async function De(r,n){try{let t=await fetch(`${n}/api/temp-key/create`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({apiKey:r,expiryMinutes:30})});if(!t.ok){let o=await t.json();throw new Error(o.error||"Failed to create temp key")}return await t.json()}catch(t){return console.error("Error creating temp key:",t),null}}var he=require("fs"),An=require("url"),On=require("child_process"),je=M(require("path"),1),xt=M(require("os"),1),Cr=je.default.dirname((0,An.fileURLToPath)(__importMetaUrl)),Y="9remote",Pn=`https://registry.npmjs.org/${Y}/latest`,Cn=3e3,Ir=8e3;function In(){return"0.1.63"}function Mn(r,n){let t=r.split(".").map(Number),o=n.split(".").map(Number);for(let e=0;e<3;e++){if(o[e]>t[e])return!0;if(o[e]<t[e])return!1}return!1}function Mr(){return process.env.CODESPACES==="true"||process.env.GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN?"GitHub Codespaces":(0,he.existsSync)("/.dockerenv")?"Docker":null}async function Ln(){try{let r=In();if(!r)return null;let n=await fetch(Pn,{signal:AbortSignal.timeout(Cn)});if(!n.ok)return null;let{version:t}=await n.json();return t&&Mn(r,t)?{current:r,latest:t}:null}catch{return null}}async function $n(r=!1){if(r)return!1;let n=In();if(!n)return!1;let t=["\u280B","\u2819","\u2839","\u2838","\u283C","\u2834","\u2826","\u2827","\u2807","\u280F"],o=0,e=null,i=a=>{process.stdout.isTTY&&(process.stdout.write(`\r${t[0]} ${a}`),e=setInterval(()=>{process.stdout.write(`\r${t[o++%t.length]} ${a}`)},80))},s=()=>{e&&(clearInterval(e),e=null),process.stdout.isTTY&&process.stdout.write("\r\x1B[K")};return new Promise(a=>{let c=!1,l=f=>{c||(c=!0,s(),a(f))},u=setTimeout(()=>l(!1),Ir);i("Checking for updates..."),fetch(Pn,{signal:AbortSignal.timeout(Cn)}).then(f=>f.json()).then(f=>{if(c)return;clearTimeout(u);let h=f.version;if(!h||!Mn(n,h)){l(!1);return}s(),console.log(v.green(`\u2705 New version available: ${n} \u2192 ${h}`));let d=Mr();if(d){console.log(v.yellow(` \u26A0\uFE0F ${d} detected - manual update required`)),console.log(v.gray(` Run: npm install -g ${Y}@latest
|
|
13
13
|
`)),l(!1);return}console.log(v.yellow(`\u{1F504} Auto-updating...
|
|
14
14
|
`));let g=process.argv.slice(2).filter(b=>b!=="--skip-update").join(" "),T=process.platform,k,_;if(T==="win32"){let b=`@echo off
|
|
15
15
|
echo \u{1F4E5} Downloading update...
|
|
@@ -60,7 +60,7 @@ fi
|
|
|
60
60
|
`),process.stdout.write(`${S.dim}${r}${S.reset}
|
|
61
61
|
|
|
62
62
|
`),n.forEach((p,g)=>{let T=g===a?l?">":"\u2605":l?" ":"\u2606";console.log(g===a?` \x1B[7m${S.bold}${T} ${p.label}${S.reset}`:` ${T} ${p.label}`)})},f=()=>{if(c){if(c=!1,process.stdin.isTTY)try{process.stdin.setRawMode(!1)}catch{}process.stdin.removeListener("keypress",h),process.stdin.pause()}},h=(d,p)=>{!c||!p||(p.name==="up"?(a=(a-1+n.length)%n.length,u()):p.name==="down"?(a=(a+1)%n.length,u()):p.name==="return"?(f(),s(a)):p.name==="escape"?(f(),s(-1)):p.ctrl&&p.name==="c"&&(f(),i&&i(),process.exit(0)))};if(process.stdin.removeAllListeners("keypress"),Pt.default.emitKeypressEvents(process.stdin),process.stdin.isTTY)try{process.stdin.setRawMode(!0)}catch{s(-1);return}process.stdin.on("keypress",h),process.stdin.resume(),u(),e&&e(u)})}function Gn(r){return new Promise(n=>{if(process.stdin.isTTY)try{process.stdin.setRawMode(!1)}catch{}process.stdin.removeAllListeners("keypress");let t=Pt.default.createInterface({input:process.stdin,output:process.stdout});t.question(`${r} (y/N): `,o=>{t.close(),n(o.trim().toLowerCase()==="y")})})}function Hn(r,n){let t=null,o=!1,e=()=>{o||(t=Fn.default.get(`http://localhost:${r}/api/ui/events`,i=>{let s="";i.on("data",a=>{s+=a.toString();let c=s.split(`
|
|
63
|
-
`);s=c.pop();let l="";for(let u of c)if(u.startsWith("data: "))l=u.slice(6);else if(u===""&&l){try{let f=JSON.parse(l);n(f.type,f)}catch{}l=""}}),i.on("end",()=>{o||setTimeout(e,2e3)})}),t.on("error",()=>{o||setTimeout(e,2e3)}))};return e(),()=>{o=!0,t?.destroy()}}var Yr=process.argv.includes("--skip-update"),ct=ge.default.dirname((0,Vn.fileURLToPath)(__importMetaUrl)),Xo=ge.default.resolve(ct,"../.."),Vr=ge.default.resolve(ct,"../dist/server.cjs"),Wn=ge.default.resolve(ct,"../index.js"),U="https://9remote.cc",B=2208,It=10,Qn=6e4,te=v.rgb(230,138,110),qn=v.rgb(200,120,95);function Lt(){return"0.1.
|
|
63
|
+
`);s=c.pop();let l="";for(let u of c)if(u.startsWith("data: "))l=u.slice(6);else if(u===""&&l){try{let f=JSON.parse(l);n(f.type,f)}catch{}l=""}}),i.on("end",()=>{o||setTimeout(e,2e3)})}),t.on("error",()=>{o||setTimeout(e,2e3)}))};return e(),()=>{o=!0,t?.destroy()}}var Yr=process.argv.includes("--skip-update"),ct=ge.default.dirname((0,Vn.fileURLToPath)(__importMetaUrl)),Xo=ge.default.resolve(ct,"../.."),Vr=ge.default.resolve(ct,"../dist/server.cjs"),Wn=ge.default.resolve(ct,"../index.js"),U="https://9remote.cc",B=2208,It=10,Qn=6e4,te=v.rgb(230,138,110),qn=v.rgb(200,120,95);function Lt(){return"0.1.63"}function Jr(r,n="\u{1F4F1} Scan QR to connect:"){console.log(te(`
|
|
64
64
|
${n}`)),Mt.default.generate(r,{small:!0,type:"terminal",margin:0},t=>{console.log(t.trim())})}function qr(r){return new Promise(n=>{Mt.default.generate(r,{small:!0,type:"terminal",margin:0},t=>{n(qn("\u{1F4F1} Scan QR to connect:")+`
|
|
65
65
|
`+t.trim())})})}async function Xn(r,n){let t=await De(r,U);if(!t){console.log(v.red("\u274C Failed to create temp key"));return}let o=`${U}/login?k=${t.tempKey}`,e=Math.min(44,process.stdout.columns||55);D({step:4,tunnelUrl:n,oneTimeKey:t.tempKey,oneTimeKeyExpiresAt:t.expiresAt,permanentKey:r,qrUrl:o,workerUrl:U}),Jr(o),console.log(v.gray(`
|
|
66
66
|
QR will expire in 30 minutes (one-time use)
|