localians 0.1.0 → 0.2.0
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 +44 -0
- package/README.md +24 -3
- package/dist/localians.js +30 -19
- package/package.json +2 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to the `localians` CLI. Versions follow [semantic versioning](https://semver.org/) (0.x: minor
|
|
4
|
+
versions add features, patch versions fix bugs).
|
|
5
|
+
|
|
6
|
+
## 0.2.0
|
|
7
|
+
|
|
8
|
+
Sign in once, share several apps at the same time, and always know which account and project you're using.
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `localians whoami` shows which account this computer is signed in to (username, email, workspace), checked with
|
|
13
|
+
the server.
|
|
14
|
+
- `localians share <port> --new [--name <name>]` creates a new project for the app you're sharing.
|
|
15
|
+
- Share several apps at once: run one `localians share <port>` per app, each in its own terminal. Every share has its
|
|
16
|
+
own project, link, timer and public or password setting, and stopping one leaves the others running. `status`
|
|
17
|
+
lists them all.
|
|
18
|
+
- The CLI remembers which project each app folder and port uses, separately for each account, so the next
|
|
19
|
+
`localians share` picks the same project without asking.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- `localians login` shows the one-time code and the link to approve it, and says when you're already signed in (and
|
|
24
|
+
how to switch accounts) instead of starting over. Success reads "Signed in as @username. You're ready to share."
|
|
25
|
+
- Logins renew themselves while you use them; unused ones still expire after 90 days, and revoked ones stay revoked.
|
|
26
|
+
- Project selection never guesses silently: the remembered project, else the only project using the port (you confirm
|
|
27
|
+
it the first time), else you choose when several match.
|
|
28
|
+
- A port that's already being shared from another terminal on this computer isn't shared twice: the CLI shows the
|
|
29
|
+
existing link instead. If that terminal crashed, sharing the port again takes over from it.
|
|
30
|
+
- Clearer errors that say what to do next, for example "Nothing is running on localhost:3000. Start your app and try
|
|
31
|
+
again.", "You're not signed in. Run: npx localians login", and "This project's preview period has ended. Upgrade or
|
|
32
|
+
renew your Preview Pass."
|
|
33
|
+
- A dashboard command (`--setup`) made by a different account says so and offers to switch accounts.
|
|
34
|
+
- Hints show `npx localians …` when the CLI runs through npx, and `localians …` when it's installed.
|
|
35
|
+
|
|
36
|
+
### Compatibility
|
|
37
|
+
|
|
38
|
+
- Works with existing logins from 0.1.0: no need to sign in again after upgrading.
|
|
39
|
+
- 0.1.0 keeps working against the current Localians server.
|
|
40
|
+
|
|
41
|
+
## 0.1.0
|
|
42
|
+
|
|
43
|
+
First public release: `localians share <port>` with browser login, `status`, `logout`, `--project`, `--setup`,
|
|
44
|
+
`--duration` and password-protected previews.
|
package/README.md
CHANGED
|
@@ -24,15 +24,32 @@ project and password already chosen.
|
|
|
24
24
|
| `localians share <port>` | Share `localhost:<port>` (signs in first if needed) |
|
|
25
25
|
| `localians share <port> --setup <code>` | Use the project and access chosen in the dashboard's Share App page |
|
|
26
26
|
| `localians share <port> --project <name>` | Share under a specific project |
|
|
27
|
-
| `localians
|
|
28
|
-
| `localians
|
|
27
|
+
| `localians share <port> --new [--name <name>]` | Share under a new project |
|
|
28
|
+
| `localians login` | Sign this computer in (a one-time code approved in your browser) |
|
|
29
|
+
| `localians whoami` | Show which account this computer is signed in to |
|
|
30
|
+
| `localians status` | Show your login and everything this computer is sharing |
|
|
29
31
|
| `localians logout` | Sign out and revoke this computer's access |
|
|
30
32
|
|
|
31
|
-
Options: `-
|
|
33
|
+
Options: `-d, --duration <time>` (an earlier auto-stop: `5m`, `15m`, `30m`, `1h`, `12h` or `24h`, plus `3d` or `7d` with a
|
|
34
|
+
7-Day Preview Pass; by default the link works until the project's free window or pass ends), `-y, --yes` (create a project without asking, for scripts), `--host-header preserve`, `-v, --verbose`,
|
|
32
35
|
`--no-browser`, `--api-url <url>` (only needed for a self-hosted server). Run `localians --help` for details.
|
|
33
36
|
|
|
37
|
+
Several apps at once: run one `localians share <port>` per app, each in its own terminal. Each gets its own project,
|
|
38
|
+
link, timer and access, and stopping one leaves the others running (up to 5 at once per account by default). The CLI
|
|
39
|
+
remembers which project each app folder and port uses, per account. Switch accounts with `localians logout`, then
|
|
40
|
+
`localians login`.
|
|
41
|
+
|
|
34
42
|
Exit codes: `0` ok, `1` other error, `2` usage, `3` login needed, `4` nothing running on that port, `5` Localians unreachable.
|
|
35
43
|
|
|
44
|
+
## Preview time
|
|
45
|
+
|
|
46
|
+
- **Free:** every project gets one 24-hour preview window, starting with its first share. Restarting or reconnecting
|
|
47
|
+
stays within that window. When it has been used, the CLI tells you and points you to the dashboard.
|
|
48
|
+
- **7-Day Preview Pass ($5, one-time):** 7 more days of sharing for one project, added from the dashboard when
|
|
49
|
+
available.
|
|
50
|
+
|
|
51
|
+
Your computer and the app must stay running while you share — Localians doesn't host a copy of your app.
|
|
52
|
+
|
|
36
53
|
## Security
|
|
37
54
|
|
|
38
55
|
- The CLI only forwards to your own computer (127.0.0.1 / ::1) on the port you choose.
|
|
@@ -47,3 +64,7 @@ Requires Node.js 20 or newer on Windows, macOS or Linux. Docs: https://github.co
|
|
|
47
64
|
|
|
48
65
|
MIT — see [LICENSE](LICENSE). This license covers the `localians` CLI package only; the Localians service is not
|
|
49
66
|
open source.
|
|
67
|
+
|
|
68
|
+
## Release notes
|
|
69
|
+
|
|
70
|
+
See [CHANGELOG.md](CHANGELOG.md).
|
package/dist/localians.js
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
"use strict";var
|
|
2
|
+
"use strict";var bt=Object.create;var Ie=Object.defineProperty;var $t=Object.getOwnPropertyDescriptor;var St=Object.getOwnPropertyNames;var vt=Object.getPrototypeOf,xt=Object.prototype.hasOwnProperty;var At=(e,t,n,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of St(t))!xt.call(e,o)&&o!==n&&Ie(e,o,{get:()=>t[o],enumerable:!(r=$t(t,o))||r.enumerable});return e};var I=(e,t,n)=>(n=e!=null?bt(vt(e)):{},At(t||!e||!e.__esModule?Ie(n,"default",{value:e,enumerable:!0}):n,e));var Ge=require("node:util");var ge=I(require("node:fs")),Ne=I(require("node:net")),Ue=I(require("node:os")),_=I(require("node:path")),fe=["127.0.0.1","::1"];function De(e){if(!e||!/^\d{1,5}$/.test(e.trim()))return null;let t=Number(e);return t>=1&&t<=65535?t:null}function kt(e,t,n=1500){return new Promise(r=>{let o=Ne.default.connect({host:e,port:t}),s=i=>{o.destroy(),r(i)};o.setTimeout(n,()=>s(!1)),o.once("connect",()=>s(!0)),o.once("error",()=>s(!1))})}async function Me(e){for(let t of fe)if(await kt(t,e))return t;return null}var B=e=>{try{return ge.default.readFileSync(e,"utf8")}catch{return null}};function me(e,t){let n=e.indexOf(`[${t}]`);return n<0?null:e.slice(n+t.length+2).split(/^\[/m)[0].match(/^\s*name\s*=\s*["']([^"']+)["']/m)?.[1]??null}function _t(e){let t=B(_.default.join(e,"package.json"));if(t!==null)try{let i=JSON.parse(t),l={...i.dependencies,...i.devDependencies},y=l.next?"NEXTJS":l["@angular/core"]?"ANGULAR":l.vue?"VUE":l.vite||l.react?"REACT_VITE":Object.keys(l).length?"NODEJS":null;return{name:i.name?.replace(/^@[^/]+\//,"")||null,framework:y}}catch{return{name:null,framework:null}}let n=B(_.default.join(e,"pyproject.toml"));if(n!==null)return{name:me(n,"project")??me(n,"tool.poetry"),framework:null};let r=B(_.default.join(e,"Cargo.toml"));if(r!==null)return{name:me(r,"package"),framework:null};let o=B(_.default.join(e,"composer.json"));if(o!==null)try{return{name:JSON.parse(o).name?.split("/").pop()||null,framework:null}}catch{return{name:null,framework:null}}let s=B(_.default.join(e,"go.mod"));if(s!==null)return{name:s.match(/^module\s+(\S+)/m)?.[1].split("/").pop()??null,framework:null};for(let i of["Gemfile","manage.py","requirements.txt","index.html"])if(ge.default.existsSync(_.default.join(e,i)))return{name:null,framework:null};return null}var Re=e=>e.trim().replace(/\s+/g," ").slice(0,80);function re(e,t=Ue.default.homedir()){let n=new Set([_.default.resolve(t),_.default.parse(_.default.resolve(e)).root]),r=_.default.resolve(e);for(let s=0;s<6&&!n.has(r);s++){let i=_t(r);if(i)return{name:Re(i.name??_.default.basename(r))||"My app",framework:i.framework,root:r};let l=_.default.dirname(r);if(l===r)break;r=l}let o=_.default.resolve(e);return{name:n.has(o)?"My app":Re(_.default.basename(o))||"My app",framework:null,root:o}}var jt=[{minutes:5,label:"5 minutes",short:"5m",plan:"free"},{minutes:15,label:"15 minutes",short:"15m",plan:"free"},{minutes:30,label:"30 minutes",short:"30m",plan:"free"},{minutes:60,label:"1 hour",short:"1h",plan:"free"},{minutes:720,label:"12 hours",short:"12h",plan:"free"},{minutes:1440,label:"24 hours",short:"24h",plan:"free"},{minutes:4320,label:"3 days",short:"3d",plan:"pass"},{minutes:10080,label:"7 days",short:"7d",plan:"pass"}];var Ct=e=>jt.some(t=>t.minutes===e);function He(e){let t=/^\s*(\d{1,5})\s*(m|min|mins|minutes?|h|hr|hrs|hours?|d|days?)?\s*$/i.exec(e);if(!t)return null;let n=Number(t[1]),r=(t[2]??"m").toLowerCase()[0],o=r==="d"?n*24*60:r==="h"?n*60:n;return Ct(o)?o:null}function Fe(e){let t=Math.max(0,Math.floor(e/1e3)),n=Math.floor(t/86400),r=Math.floor(t%86400/3600),o=Math.floor(t%3600/60),s=t%60,i=l=>String(l).padStart(2,"0");return n?`${n}d ${i(r)}h`:r?`${r}h ${i(o)}m`:o?`${o}m ${i(s)}s`:`${s}s`}var Ye=`Usage: localians <command> [options]
|
|
3
3
|
|
|
4
4
|
Commands:
|
|
5
5
|
share <port> Share the app running on localhost:<port> (signs you in first if needed)
|
|
6
|
-
login Sign in to Localians through your browser
|
|
7
|
-
|
|
6
|
+
login Sign in to Localians through your browser (a one-time code)
|
|
7
|
+
whoami Show which account this computer is signed in to
|
|
8
|
+
status Show your login and everything this computer is sharing
|
|
8
9
|
logout Sign out and revoke this computer's CLI access
|
|
9
10
|
|
|
10
11
|
Options for share:
|
|
11
|
-
--project <id|slug> Project to share under (default: the
|
|
12
|
-
|
|
13
|
-
--
|
|
12
|
+
--project <id|slug> Project to share under (default: the one remembered for this folder,
|
|
13
|
+
else the project that uses this port; you choose if several do)
|
|
14
|
+
--new Create a new project for this app (named after it, or --name)
|
|
15
|
+
--name <name> Name for the new project (with --new)
|
|
16
|
+
-y, --yes Don't ask: create a project for this app if none uses the port
|
|
17
|
+
--setup <code> Use the project, access and duration chosen in the dashboard's Share App page
|
|
18
|
+
-d, --duration <time> How long to share: 5m, 15m, 30m, 1h, 12h or 24h (default 24h;
|
|
19
|
+
3d or 7d with a Preview Pass). The link stops working after that
|
|
14
20
|
--host-header <mode> "rewrite" (default) sends Host: localhost:<port>;
|
|
15
21
|
"preserve" forwards the public preview host
|
|
16
22
|
|
|
@@ -22,21 +28,26 @@ Global options:
|
|
|
22
28
|
--version Print the CLI version
|
|
23
29
|
|
|
24
30
|
Examples:
|
|
31
|
+
npx localians login Sign in once on this computer
|
|
25
32
|
npx localians share 3000 Share a Next.js / React app on port 3000
|
|
26
|
-
npx localians share 5173 Share a Vite app
|
|
33
|
+
npx localians share 5173 Share a Vite app (in another terminal: both stay live)
|
|
34
|
+
npx localians share 3000 --duration 1h Share for one hour
|
|
27
35
|
npx localians share 3000 --project shop Share under an existing project
|
|
36
|
+
npx localians share 8000 --new --name API Share under a new project called "API"
|
|
37
|
+
npx localians whoami Check which account you're signed in to
|
|
28
38
|
npx localians status See what this computer is sharing
|
|
29
39
|
|
|
40
|
+
Share several apps at once: run one "localians share" per app, each in its own terminal.
|
|
30
41
|
Stop sharing: press Ctrl+C in the window where "localians share" is running.
|
|
31
|
-
`;function ke(e){let t;try{t=(0,ve.parseArgs)({args:e,allowPositionals:!0,strict:!0,options:{"api-url":{type:"string"},project:{type:"string"},setup:{type:"string"},"host-header":{type:"string"},"no-browser":{type:"boolean"},verbose:{type:"boolean",short:"v"},yes:{type:"boolean",short:"y"},help:{type:"boolean",short:"h"},version:{type:"boolean"}}})}catch(m){return{kind:"error",message:m.message.replace(/^TypeError \[[A-Z_]+\]: /,"")}}let{values:r,positionals:s}=t;if(r.version)return{kind:"version"};if(r.help||s.length===0||s[0]==="help")return{kind:"help"};let[n,...i]=s;if(n!=="login"&&n!=="share"&&n!=="status"&&n!=="logout")return{kind:"error",message:`Unknown command "${n}".`};let o=r["host-header"]??"rewrite";if(o!=="rewrite"&&o!=="preserve")return{kind:"error",message:'--host-header must be "rewrite" or "preserve".'};if(r["api-url"])try{let m=new URL(r["api-url"]);if(m.protocol!=="https:"&&m.protocol!=="http:")throw new Error}catch{return{kind:"error",message:"--api-url must be an http(s) URL."}}let u=null;if(n==="share"){if(i.length!==1)return{kind:"error",message:"Usage: localians share <port> (for example: localians share 3000)"};if(u=be(i[0]),u===null)return{kind:"error",message:`"${i[0]}" isn't a valid port. Use a number from 1 to 65535.`}}else if(i.length)return{kind:"error",message:`"${n}" doesn't take arguments.`};return{kind:"command",command:n,port:u,options:{apiUrl:r["api-url"],project:r.project,setup:r.setup,hostHeader:o,browser:!r["no-browser"],verbose:!!r.verbose,yes:!!r.yes}}}var De=require("node:child_process"),Me=T(require("node:os")),Ge=require("node:readline/promises");var Z=T(require("node:http")),le=T(require("node:http2")),Pe=T(require("node:https")),Le=require("node:net");function xe(e,{base:t=1e3,cap:r=3e4,min:s=250,random:n=Math.random}={}){let i=Math.min(r,t*2**Math.max(0,e));return Math.max(s,Math.round(n()*i))}var nt=new Set(["connection","keep-alive","proxy-connection","proxy-authenticate","proxy-authorization","te","trailer","transfer-encoding","upgrade","http2-settings","host"]);var ot="x-localians-";function st(e){let t=e.connection,r=Array.isArray(t)?t.join(","):String(t??"");return new Set(r.split(",").map(s=>s.trim().toLowerCase()).filter(Boolean))}function ce(e){let t=st(e),r={};for(let[s,n]of Object.entries(e)){let i=s.toLowerCase();n===void 0||i.startsWith(":")||nt.has(i)||t.has(i)||i.startsWith(ot)||(r[i]=Array.isArray(n)?n.map(String):String(n))}return r}var Ce="localians-tunnel/1",$e="/v1/connect",W={originalHost:"x-localians-original-host",upgrade:"x-localians-upgrade",upstreamStatus:"x-localians-upstream-status",error:"x-localians-error"};var{NGHTTP2_CANCEL:_e}=le.default.constants,R=class extends Error{constructor(r,s,n){super(r);this.status=s;this.code=n}},it=new Set([401,403,404,410]);function Ee(e){let t=e.localPort,r=e.localAddress,s=e.hostHeader??"rewrite",n=new Z.default.Agent({keepAlive:!0,maxSockets:64}),i="connecting",o=null,u=!1,m=null,f=null,L,U=new Promise(a=>L=a),O=(a,w={})=>{i=a,e.onState?.(a,w)},H=le.default.createServer({settings:{enablePush:!1,maxConcurrentStreams:256,initialWindowSize:1024*1024}});H.on("stream",(a,w)=>V(a,w)),H.on("session",a=>{a.on("error",()=>{})});function F(a){return a.code==="ECONNREFUSED"||a.code==="ECONNRESET"||a.code==="EHOSTUNREACH"?"local-unreachable":a.code==="ETIMEDOUT"?"local-timeout":"local-error"}function Y(a,w){let g={};for(let[N,C]of Object.entries(a))C===void 0||N.startsWith(":")||N.startsWith("x-localians-")||(g[N]=C);let y=String(a[W.originalHost]??"");return g.host=s==="preserve"&&y?y:`localhost:${t}`,w&&(g.connection="Upgrade",g.upgrade="websocket"),g}function V(a,w){let g=Date.now(),y=String(w[":method"]??"GET"),N=String(w[":path"]??"/"),C=w[W.upgrade]==="websocket",k=!1;a.on("error",()=>{});let v=$=>{let j=Z.default.request({host:$,port:t,method:y,path:N,headers:Y(w,C),agent:C?!1:n});j.on("response",_=>{if(a.destroyed)return _.destroy();a.respond({":status":_.statusCode??502,...ce(_.headers)}),_.pipe(a),_.on("end",()=>e.onRequest?.({method:y,path:N,status:_.statusCode??0,ms:Date.now()-g})),_.on("error",()=>a.close(_e))}),j.on("upgrade",(_,E,X)=>{if(a.destroyed)return E.destroy();a.respond({":status":200,[W.upstreamStatus]:"101",...ce(_.headers)}),X.length&&a.write(X),E.pipe(a),a.pipe(E);let ne=()=>{E.destroy(),a.closed||a.close()};E.on("close",ne),E.on("error",ne),a.on("close",ne),e.onRequest?.({method:y,path:N,status:101,ms:Date.now()-g,websocket:!0})}),j.on("error",_=>{if(!k&&_.code==="ECONNREFUSED"&&!a.headersSent){k=!0;let E=ie.find(X=>X!==$);return r=E,v(E)}if(!a.destroyed)if(a.headersSent)a.close(_e);else{let E=F(_);a.respond({":status":502,[W.error]:E},{endStream:!0}),e.onRequest?.({method:y,path:N,status:502,ms:Date.now()-g})}}),a.on("close",()=>{!j.destroyed&&!j.writableEnded&&j.destroy()}),C?j.end():a.pipe(j)};v(r)}function K(a){return new Promise((w,g)=>{let y=new URL($e,e.gatewayUrl),C=(y.protocol==="https:"?Pe.default:Z.default).request({hostname:y.hostname,port:y.port||(y.protocol==="https:"?443:80),path:y.pathname,method:"GET",servername:y.protocol==="https:"&&!(0,Le.isIP)(y.hostname)?y.hostname:void 0,headers:{connection:"Upgrade",upgrade:Ce,authorization:`Bearer ${a}`,"user-agent":e.userAgent??"localians-cli"},timeout:15e3});C.on("timeout",()=>C.destroy(new R("Timed out connecting to the Localians gateway.",null,"timeout"))),C.on("error",k=>g(k instanceof R?k:new R(k.message,null,k.code??"network"))),C.on("response",k=>{let v="";k.setEncoding("utf8"),k.on("data",$=>v=(v+$).slice(0,2e3)),k.on("end",()=>{let $="rejected";try{$=JSON.parse(v).error??$}catch{}g(new R(`Gateway refused the tunnel (HTTP ${k.statusCode}, ${$}).`,k.statusCode??null,$))})}),C.on("upgrade",(k,v,$)=>{v.on("error",()=>v.destroy()),$.length&&v.unshift($),v.setNoDelay?.(!0),v.setKeepAlive?.(!0,2e4),"alpnProtocol"in v&&(v.alpnProtocol="h2"),H.once("session",j=>w(j)),H.emit("connection",v)}),C.end()})}async function q(a){if(u)return;let w;try{w=await e.getConnectToken(a)}catch(g){return g instanceof R&&g.status&&it.has(g.status)?re(g.status===410?"ended":"auth",g.message):p(a,g instanceof Error?g.message:"could not reach Localians")}try{let g=await K(w);if(u){g.destroy();return}o=g,O("connected",{attempt:a}),g.on("close",()=>{o===g&&(o=null),te(),u||p(0,"connection lost")}),S(g)}catch(g){let y=g instanceof R?g.status:null;return y===410?re("ended","This preview was stopped. Run `localians share` again for a new link."):p(a,g instanceof Error?g.message:"connection failed",y===503?5e3:void 0)}}function p(a,w,g){if(u)return;let y=Math.max(g??0,xe(a));O("reconnecting",{attempt:a+1,delayMs:y,reason:w}),m=setTimeout(()=>{q(a+1)},y)}function S(a){te(),f=setInterval(()=>{let w=setTimeout(()=>a.destroy(),1e4);try{a.ping(g=>{clearTimeout(w),g&&a.destroy()})}catch{clearTimeout(w),a.destroy()}},e.pingIntervalMs??25e3),f.unref()}function te(){f&&clearInterval(f),f=null}function re(a,w){u||(u=!0,m&&clearTimeout(m),te(),o&&!o.destroyed&&(o.close(),setTimeout(()=>o?.destroy(),1500).unref()),n.destroy(),O("stopped",{stopReason:a,reason:w}),L(a))}return O("connecting"),q(0),{get state(){return i},get localAddress(){return r},done:U,stop:()=>re("user")}}var x=class extends Error{constructor(r,s,n){super(r);this.status=s;this.code=n}};function D(e,t,r){let s=e.replace(/\/+$/,"");async function n(i,o,u){let m;try{m=await fetch(`${s}${o}`,{method:i,headers:{"content-type":"application/json","user-agent":r,...t?{authorization:`Bearer ${t}`}:{}},body:u===void 0?void 0:JSON.stringify(u),signal:AbortSignal.timeout(2e4)})}catch(U){let O=U.cause?.code;throw new x(`Can't reach Localians at ${s}${O?` (${O})`:""}.`,null,"network")}let f=await m.text(),L={};try{L=f?JSON.parse(f):{}}catch{}if(!m.ok)throw new x(String(L.message??`Request failed (HTTP ${m.status}).`),m.status,String(L.error??"http_error"));return L}return{startDeviceLogin:i=>n("POST","/api/cli/device/code",{client_name:i}),pollDeviceLogin:i=>n("POST","/api/cli/device/token",{device_code:i}),me:()=>n("GET","/api/cli/me"),createProject:i=>n("POST","/api/cli/projects",i),redeemSetup:(i,o,u)=>n("POST","/api/cli/setup",{code:i,port:o,projectName:u?.name,framework:u?.framework}),createTunnel:(i,o)=>n("POST","/api/cli/tunnels",{projectId:i,localPort:o}),connectToken:async i=>{try{return(await n("POST",`/api/cli/tunnels/${encodeURIComponent(i)}/connect-token`)).connectToken}catch(o){throw o instanceof x?new R(o.message,o.status,o.code):o}},protectProject:(i,o)=>n("POST",`/api/cli/projects/${encodeURIComponent(i)}/access`,{password:o}),closeTunnel:i=>n("POST",`/api/cli/tunnels/${encodeURIComponent(i)}/close`),listTunnels:()=>n("GET","/api/cli/tunnels"),logout:()=>n("POST","/api/cli/logout")}}var I=T(require("node:fs")),Q=T(require("node:os")),P=T(require("node:path"));function Te(){return process.env.LOCALIANS_CONFIG_DIR?process.env.LOCALIANS_CONFIG_DIR:process.platform==="win32"?P.default.join(process.env.APPDATA??P.default.join(Q.default.homedir(),"AppData","Roaming"),"localians"):process.platform==="darwin"?P.default.join(Q.default.homedir(),"Library","Application Support","localians"):P.default.join(process.env.XDG_CONFIG_HOME??P.default.join(Q.default.homedir(),".config"),"localians")}var ue=()=>P.default.join(Te(),"credentials.json"),ee=()=>P.default.join(Te(),"shares");function at(e){I.default.mkdirSync(e,{recursive:!0,mode:448})}function je(e,t){at(P.default.dirname(e));let r=`${e}.${process.pid}.tmp`;I.default.writeFileSync(r,JSON.stringify(t,null,2),{mode:384}),I.default.renameSync(r,e),process.platform!=="win32"&&I.default.chmodSync(e,384)}function B(){try{let e=JSON.parse(I.default.readFileSync(ue(),"utf8"));return e.token&&e.apiUrl?e:null}catch{return null}}var Re=e=>je(ue(),e);function pe(){I.default.rmSync(ue(),{force:!0})}function Ie(e){je(P.default.join(ee(),`${e.pid}.json`),e)}function de(e=process.pid){I.default.rmSync(P.default.join(ee(),`${e}.json`),{force:!0})}function ct(e){try{return process.kill(e,0),!0}catch(t){return t.code==="EPERM"}}function Oe(){let e=[];try{e=I.default.readdirSync(ee()).filter(r=>r.endsWith(".json"))}catch{return[]}let t=[];for(let r of e)try{let s=JSON.parse(I.default.readFileSync(P.default.join(ee(),r),"utf8"));ct(s.pid)?t.push(s):de(s.pid)}catch{}return t}var Ne=()=>process.stdout.isTTY&&!process.env.NO_COLOR&&process.env.TERM!=="dumb",M=(e,t)=>r=>Ne()?`\x1B[${e}m${r}\x1B[${t}m`:r,c={bold:M(1,22),dim:M(2,22),green:M(32,39),lime:e=>Ne()?`\x1B[38;2;184;255;59m${e}\x1B[39m`:e,yellow:M(33,39),red:M(31,39),cyan:M(36,39)},l=(e="")=>process.stdout.write(`${e}
|
|
32
|
-
`),ge=(e="")=>process.stderr.write(`${e}
|
|
33
|
-
`)
|
|
34
|
-
|
|
35
|
-
You're
|
|
36
|
-
|
|
37
|
-
${c.dim("Run `
|
|
38
|
-
`),d.
|
|
39
|
-
`),d.ok
|
|
40
|
-
${
|
|
41
|
-
`),d.ok):
|
|
42
|
-
`),d.ok;case"error":return
|
|
42
|
+
Switch accounts: "localians logout", then "localians login".
|
|
43
|
+
`,Pt=["login","share","status","whoami","logout"];function Je(e){let t;try{t=(0,Ge.parseArgs)({args:e,allowPositionals:!0,strict:!0,options:{"api-url":{type:"string"},project:{type:"string"},setup:{type:"string"},duration:{type:"string",short:"d"},"host-header":{type:"string"},"no-browser":{type:"boolean"},verbose:{type:"boolean",short:"v"},yes:{type:"boolean",short:"y"},new:{type:"boolean"},name:{type:"string"},help:{type:"boolean",short:"h"},version:{type:"boolean"}}})}catch(b){return{kind:"error",message:b.message.replace(/^TypeError \[[A-Z_]+\]: /,"")}}let{values:n,positionals:r}=t;if(n.version)return{kind:"version"};if(n.help||r.length===0||r[0]==="help")return{kind:"help"};let[o,...s]=r;if(!Pt.includes(o))return{kind:"error",message:`Unknown command "${o}".`};let i=n["host-header"]??"rewrite";if(i!=="rewrite"&&i!=="preserve")return{kind:"error",message:'--host-header must be "rewrite" or "preserve".'};if(n["api-url"])try{let b=new URL(n["api-url"]);if(b.protocol!=="https:"&&b.protocol!=="http:")throw new Error}catch{return{kind:"error",message:"--api-url must be an http(s) URL."}}let l;if(n.duration!==void 0){if(o!=="share")return{kind:"error",message:"--duration only applies to share."};let b=He(n.duration);if(b===null)return{kind:"error",message:`"${n.duration}" isn't a duration Localians offers. Use 5m, 15m, 30m, 1h, 12h or 24h (3d or 7d with a Preview Pass).`};l=b}if((n.new||n.name!==void 0)&&o!=="share")return{kind:"error",message:"--new and --name only apply to share."};if(n.name!==void 0&&!n.new)return{kind:"error",message:"--name names a new project: use it with --new."};if(n.new&&n.project)return{kind:"error",message:"Use --project for an existing project, or --new for a new one, not both."};if(n.new&&n.setup)return{kind:"error",message:"--setup already says which project to use; leave out --new."};if(n.name!==void 0&&!n.name.trim())return{kind:"error",message:"--name can't be empty."};let y=null;if(o==="share"){if(s.length!==1)return{kind:"error",message:"Usage: localians share <port> (for example: localians share 3000)"};if(y=De(s[0]),y===null)return{kind:"error",message:`"${s[0]}" isn't a valid port. Use a number from 1 to 65535.`}}else if(s.length)return{kind:"error",message:`"${o}" doesn't take arguments.`};return{kind:"command",command:o,port:y,options:{apiUrl:n["api-url"],project:n.project,setup:n.setup,durationMinutes:l,hostHeader:i,browser:!n["no-browser"],verbose:!!n.verbose,yes:!!n.yes,newProject:!!n.new,name:n.name?.trim().replace(/\s+/g," ").slice(0,80)}}}var st=require("node:child_process"),it=I(require("node:os")),at=require("node:readline/promises");var oe=I(require("node:http")),we=I(require("node:http2")),Ke=I(require("node:https")),Ve=require("node:net");function qe(e,{base:t=1e3,cap:n=3e4,min:r=250,random:o=Math.random}={}){let s=Math.min(n,t*2**Math.max(0,e));return Math.max(r,Math.round(o()*s))}var Tt=new Set(["connection","keep-alive","proxy-connection","proxy-authenticate","proxy-authorization","te","trailer","transfer-encoding","upgrade","http2-settings","host"]);var Et="x-localians-";function Lt(e){let t=e.connection,n=Array.isArray(t)?t.join(","):String(t??"");return new Set(n.split(",").map(r=>r.trim().toLowerCase()).filter(Boolean))}function he(e){let t=Lt(e),n={};for(let[r,o]of Object.entries(e)){let s=r.toLowerCase();o===void 0||s.startsWith(":")||Tt.has(s)||t.has(s)||s.startsWith(Et)||(n[s]=Array.isArray(o)?o.map(String):String(o))}return n}var We="localians-tunnel/1",Be="/v1/connect",z={originalHost:"x-localians-original-host",upgrade:"x-localians-upgrade",upstreamStatus:"x-localians-upstream-status",error:"x-localians-error"};var{NGHTTP2_CANCEL:ze}=we.default.constants,D=class extends Error{constructor(n,r,o){super(n);this.status=r;this.code=o}},Ot=new Set([401,403,404,410]);function Xe(e){let t=e.localPort,n=e.localAddress,r=e.hostHeader??"rewrite",o=new oe.default.Agent({keepAlive:!0,maxSockets:64}),s="connecting",i=null,l=!1,y=null,b=null,p,h=new Promise(u=>p=u),x=(u,w={})=>{s=u,e.onState?.(u,w)},N=we.default.createServer({settings:{enablePush:!1,maxConcurrentStreams:256,initialWindowSize:1024*1024}});N.on("stream",(u,w)=>q(u,w)),N.on("session",u=>{u.on("error",()=>{})});function Z(u){return u.code==="ECONNREFUSED"||u.code==="ECONNRESET"||u.code==="EHOSTUNREACH"?"local-unreachable":u.code==="ETIMEDOUT"?"local-timeout":"local-error"}function Q(u,w){let m={};for(let[M,T]of Object.entries(u))T===void 0||M.startsWith(":")||M.startsWith("x-localians-")||(m[M]=T);let S=String(u[z.originalHost]??"");return m.host=r==="preserve"&&S?S:`localhost:${t}`,w&&(m.connection="Upgrade",m.upgrade="websocket"),m}function q(u,w){let m=Date.now(),S=String(w[":method"]??"GET"),M=String(w[":path"]??"/"),T=w[z.upgrade]==="websocket",C=!1;u.on("error",()=>{});let k=E=>{let U=oe.default.request({host:E,port:t,method:S,path:M,headers:Q(w,T),agent:T?!1:o});U.on("response",L=>{if(u.destroyed)return L.destroy();u.respond({":status":L.statusCode??502,...he(L.headers)}),L.pipe(u),L.on("end",()=>e.onRequest?.({method:S,path:M,status:L.statusCode??0,ms:Date.now()-m})),L.on("error",()=>u.close(ze))}),U.on("upgrade",(L,O,ne)=>{if(u.destroyed)return O.destroy();u.respond({":status":200,[z.upstreamStatus]:"101",...he(L.headers)}),ne.length&&u.write(ne),O.pipe(u),u.pipe(O);let de=()=>{O.destroy(),u.closed||u.close()};O.on("close",de),O.on("error",de),u.on("close",de),e.onRequest?.({method:S,path:M,status:101,ms:Date.now()-m,websocket:!0})}),U.on("error",L=>{if(!C&&L.code==="ECONNREFUSED"&&!u.headersSent){C=!0;let O=fe.find(ne=>ne!==E);return n=O,k(O)}if(!u.destroyed)if(u.headersSent)u.close(ze);else{let O=Z(L);u.respond({":status":502,[z.error]:O},{endStream:!0}),e.onRequest?.({method:S,path:M,status:502,ms:Date.now()-m})}}),u.on("close",()=>{!U.destroyed&&!U.writableEnded&&U.destroy()}),T?U.end():u.pipe(U)};k(n)}function ee(u){return new Promise((w,m)=>{let S=new URL(Be,e.gatewayUrl),T=(S.protocol==="https:"?Ke.default:oe.default).request({hostname:S.hostname,port:S.port||(S.protocol==="https:"?443:80),path:S.pathname,method:"GET",servername:S.protocol==="https:"&&!(0,Ve.isIP)(S.hostname)?S.hostname:void 0,headers:{connection:"Upgrade",upgrade:We,authorization:`Bearer ${u}`,"user-agent":e.userAgent??"localians-cli"},timeout:15e3});T.on("timeout",()=>T.destroy(new D("Timed out connecting to the Localians gateway.",null,"timeout"))),T.on("error",C=>m(C instanceof D?C:new D(C.message,null,C.code??"network"))),T.on("response",C=>{let k="";C.setEncoding("utf8"),C.on("data",E=>k=(k+E).slice(0,2e3)),C.on("end",()=>{let E="rejected";try{E=JSON.parse(k).error??E}catch{}m(new D(`Gateway refused the tunnel (HTTP ${C.statusCode}, ${E}).`,C.statusCode??null,E))})}),T.on("upgrade",(C,k,E)=>{k.on("error",()=>k.destroy()),E.length&&k.unshift(E),k.setNoDelay?.(!0),k.setKeepAlive?.(!0,2e4),"alpnProtocol"in k&&(k.alpnProtocol="h2"),N.once("session",U=>w(U)),N.emit("connection",k)}),T.end()})}async function W(u){if(l)return;let w;try{w=await e.getConnectToken(u)}catch(m){return m instanceof D&&m.status&&Ot.has(m.status)?j(m.status===410?"ended":"auth",m.message):H(u,m instanceof Error?m.message:"could not reach Localians")}try{let m=await ee(w);if(l){m.destroy();return}i=m,x("connected",{attempt:u}),m.on("close",()=>{i===m&&(i=null),$(),l||H(0,"connection lost")}),te(m)}catch(m){let S=m instanceof D?m.status:null;return S===410?j("ended","This preview was stopped. Run `localians share` again for a new link."):H(u,m instanceof Error?m.message:"connection failed",S===503?5e3:void 0)}}function H(u,w,m){if(l)return;let S=Math.max(m??0,qe(u));x("reconnecting",{attempt:u+1,delayMs:S,reason:w}),y=setTimeout(()=>{W(u+1)},S)}function te(u){$(),b=setInterval(()=>{let w=setTimeout(()=>u.destroy(),1e4);try{u.ping(m=>{clearTimeout(w),m&&u.destroy()})}catch{clearTimeout(w),u.destroy()}},e.pingIntervalMs??25e3),b.unref()}function $(){b&&clearInterval(b),b=null}function j(u,w){l||(l=!0,y&&clearTimeout(y),$(),i&&!i.destroyed&&(i.close(),setTimeout(()=>i?.destroy(),1500).unref()),o.destroy(),x("stopped",{stopReason:u,reason:w}),p(u))}return x("connecting"),W(0),{get state(){return s},get localAddress(){return n},done:h,stop:()=>j("user")}}var A=class extends Error{constructor(n,r,o){super(n);this.status=r;this.code=o}};function F(e,t,n){let r=e.replace(/\/+$/,"");async function o(s,i,l){let y;try{y=await fetch(`${r}${i}`,{method:s,headers:{"content-type":"application/json","user-agent":n,...t?{authorization:`Bearer ${t}`}:{}},body:l===void 0?void 0:JSON.stringify(l),signal:AbortSignal.timeout(2e4)})}catch(h){let x=h.cause?.code;throw new A(`Can't reach Localians at ${r}${x?` (${x})`:""}.`,null,"network")}let b=await y.text(),p={};try{p=b?JSON.parse(b):{}}catch{}if(!y.ok)throw new A(String(p.message??`Request failed (HTTP ${y.status}).`),y.status,String(p.error??"http_error"));return p}return{startDeviceLogin:s=>o("POST","/api/cli/device/code",{client_name:s}),pollDeviceLogin:s=>o("POST","/api/cli/device/token",{device_code:s}),me:()=>o("GET","/api/cli/me"),createProject:s=>o("POST","/api/cli/projects",s),redeemSetup:(s,i,l)=>o("POST","/api/cli/setup",{code:s,port:i,projectName:l?.name,framework:l?.framework}),createTunnel:(s,i,l,y)=>o("POST","/api/cli/tunnels",{projectId:s,localPort:i,...l!==void 0?{durationMinutes:l}:{},...y?{url:y}:{}}),connectToken:async s=>{try{return(await o("POST",`/api/cli/tunnels/${encodeURIComponent(s)}/connect-token`)).connectToken}catch(i){throw i instanceof A?new D(i.message,i.status,i.code):i}},protectProject:(s,i)=>o("POST",`/api/cli/projects/${encodeURIComponent(s)}/access`,{password:i}),closeTunnel:s=>o("POST",`/api/cli/tunnels/${encodeURIComponent(s)}/close`),listTunnels:()=>o("GET","/api/cli/tunnels"),logout:()=>o("POST","/api/cli/logout")}}var R=I(require("node:fs")),se=I(require("node:os")),P=I(require("node:path"));function be(){return process.env.LOCALIANS_CONFIG_DIR?process.env.LOCALIANS_CONFIG_DIR:process.platform==="win32"?P.default.join(process.env.APPDATA??P.default.join(se.default.homedir(),"AppData","Roaming"),"localians"):process.platform==="darwin"?P.default.join(se.default.homedir(),"Library","Application Support","localians"):P.default.join(process.env.XDG_CONFIG_HOME??P.default.join(se.default.homedir(),".config"),"localians")}var $e=()=>P.default.join(be(),"credentials.json"),Se=()=>P.default.join(be(),"projects.json"),ie=()=>P.default.join(be(),"shares");function It(e){R.default.mkdirSync(e,{recursive:!0,mode:448})}function ae(e,t){It(P.default.dirname(e));let n=`${e}.${process.pid}.tmp`;R.default.writeFileSync(n,JSON.stringify(t,null,2),{mode:384}),R.default.renameSync(n,e),process.platform!=="win32"&&R.default.chmodSync(e,384)}function G(){try{let e=JSON.parse(R.default.readFileSync($e(),"utf8"));return e.token&&e.apiUrl?e:null}catch{return null}}var ve=e=>ae($e(),e);function K(){R.default.rmSync($e(),{force:!0})}function Ze(e){ae(P.default.join(ie(),`${e.pid}.json`),e)}function xe(e=process.pid){R.default.rmSync(P.default.join(ie(),`${e}.json`),{force:!0})}function Rt(e){try{return process.kill(e,0),!0}catch(t){return t.code==="EPERM"}}function ce(){let e=[];try{e=R.default.readdirSync(ie()).filter(n=>n.endsWith(".json"))}catch{return[]}let t=[];for(let n of e)try{let r=JSON.parse(R.default.readFileSync(P.default.join(ie(),n),"utf8"));Rt(r.pid)?t.push(r):xe(r.pid)}catch{}return t}function V(e){return`${e.apiUrl.replace(/\/+$/,"")}|${e.workspace.id}|${e.user.id??e.user.email.toLowerCase()}`}function ye(e){let t=P.default.resolve(e);return process.platform==="win32"||process.platform==="darwin"?t.toLowerCase():t}function Ae(){try{let e=JSON.parse(R.default.readFileSync(Se(),"utf8"));if(e?.version===1&&e.accounts&&typeof e.accounts=="object")return e}catch{}return{version:1,accounts:{}}}function Qe(e,t,n){let r=Ae().accounts[e]??{},o=r[`${ye(t)}#${n}`];if(o)return o;let s=Object.entries(r).filter(([i])=>i.slice(0,i.lastIndexOf("#"))===ye(t));return s.length===1?s[0][1]:null}function ke(e,t,n,r){let o=Ae(),s=o.accounts[e]??={};s[`${ye(t)}#${n}`]={projectId:r.id,projectName:r.name,port:n,updatedAt:new Date().toISOString()},ae(Se(),o)}function _e(e,t){let n=Ae(),r=n.accounts[e];if(r){for(let[o,s]of Object.entries(r))s.projectId===t&&delete r[o];ae(Se(),n)}}var et=()=>process.stdout.isTTY&&!process.env.NO_COLOR&&process.env.TERM!=="dumb",Y=(e,t)=>n=>et()?`\x1B[${e}m${n}\x1B[${t}m`:n,a={bold:Y(1,22),dim:Y(2,22),green:Y(32,39),lime:e=>et()?`\x1B[38;2;184;255;59m${e}\x1B[39m`:e,yellow:Y(33,39),red:Y(31,39),cyan:Y(36,39)},c=(e="")=>process.stdout.write(`${e}
|
|
44
|
+
`),je=(e="")=>process.stderr.write(`${e}
|
|
45
|
+
`);function X(){c(),c(` ${a.lime("\u25A0")} ${a.bold("LOCALIANS")}`),c()}var v=(e,t)=>c(` ${a.dim(e.padEnd(18))}${t}`);function g(e,t){je(`
|
|
46
|
+
${a.red("\u2716")} ${e}`),t&&je(` ${a.dim(t)}`),je()}function tt(e){let t=Math.floor(e/1e3),n=Math.floor(t/3600),r=Math.floor(t%3600/60);return n?`${n}h ${r}m`:r?`${r}m ${t%60}s`:`${t}s`}var d={ok:0,error:1,usage:2,auth:3,localUnreachable:4,gatewayUnavailable:5,interrupted:130};var Pe="0.2.0",J=`localians-cli/${Pe} (${process.platform}; node ${process.versions.node})`,Nt="https://localians.com";function Te(e=process.env){return e.npm_command==="exec"?"npx localians":"localians"}var f=e=>`${Te()} ${e}`;function nt(e){return e.username?`@${e.username}`:e.email}var pe=e=>e.username?`${a.bold(`@${e.username}`)} ${a.dim(`(${e.email})`)}`:a.bold(e.email);function ct(e){return(e.apiUrl??process.env.LOCALIANS_API_URL??G()?.apiUrl??Nt)?.replace(/\/+$/,"")??null}function Ut(e,t=process.platform){return/^https?:\/\/[^\s"'`<>|^]+$/.test(e)?t==="win32"?["rundll32.exe",["url.dll,FileProtocolHandler",e]]:t==="darwin"?["open",[e]]:["xdg-open",[e]]:null}function Dt(e){let t=Ut(e);if(!t)return;let[n,r]=t;try{(0,st.spawn)(n,r,{stdio:"ignore",detached:!0,windowsHide:!0}).on("error",()=>{}).unref()}catch{}}var Mt=e=>new Promise(t=>setTimeout(t,e)),ue=()=>!!(process.stdin.isTTY&&process.stdout.isTTY);async function Ee(e){let t=(0,at.createInterface)({input:process.stdin,output:process.stdout});try{return(await t.question(e)).trim()}finally{t.close()}}var ut=e=>e===""||/^y(es)?$/i.test(e);function Le(e,t){let n={...e,user:{...e.user,...t.user},workspace:t.workspace,...t.credential?{expiresAt:t.credential.expiresAt}:{}};return JSON.stringify(n)!==JSON.stringify(e)&&ve(n),n}async function lt(e,t){let n=F(e,null,J),r;try{r=await n.startDeviceLogin(`CLI on ${it.default.hostname()} (${process.platform})`.slice(0,80))}catch(i){return g(i instanceof Error?i.message:"Couldn't start login.","Check your internet connection and try again."),d.gatewayUnavailable}c(` Confirm this code in your browser: ${a.bold(a.lime(r.user_code))}`),c(` ${a.dim(r.verification_uri_complete)}`),c(` ${a.dim(`(Or open ${r.verification_uri} and type the code.)`)}`),c(),t.browser&&process.stdout.isTTY&&Dt(r.verification_uri_complete),c(` ${a.dim("Waiting for you to approve it\u2026 (Ctrl+C to cancel)")}`);let o=r.interval*1e3,s=Date.now()+r.expires_in*1e3;for(;Date.now()<s;){await Mt(o);try{let i=await n.pollDeviceLogin(r.device_code),l={apiUrl:e,token:i.access_token,expiresAt:i.expires_at,user:i.user,workspace:i.workspace,createdAt:new Date().toISOString()};return ve(l),c(),c(` ${a.green("\u2714")} Signed in as ${pe(i.user)}. You're ready to share.`),c(` ${a.dim(`Workspace: ${i.workspace.name}. This computer stays signed in while you use it; revoke it any time in Settings \u2192 CLI.`)}`),c(),l}catch(i){if(i instanceof A){if(i.code==="authorization_pending")continue;if(i.code==="slow_down"){o+=5e3;continue}if(i.code==="access_denied")return g("Sign-in was denied in the browser.",`Run \`${f("login")}\` to try again.`),d.auth;if(i.code==="expired_token"||i.code==="invalid_grant")return g("The sign-in code expired before it was approved.",`Run \`${f("login")}\` again and approve it in your browser within 10 minutes.`),d.auth;if(i.code==="network")continue}return g(i instanceof Error?i.message:"Sign-in failed."),d.error}}return g("The sign-in code expired before it was approved.",`Run \`${f("login")}\` again and approve it in your browser within 10 minutes.`),d.auth}function pt(){return g("This copy of the CLI doesn't know which Localians server to use.","Pass --api-url <url> or set LOCALIANS_API_URL."),d.usage}async function Oe(e){let t=!1;try{await F(e.apiUrl,e.token,J).logout(),t=!0}catch(n){t=n instanceof A&&n.status===401}return K(),t}async function dt(e){let t=ct(e);if(!t)return pt();X();let n=G();if(n&&n.apiUrl===t)try{let o=await F(t,n.token,J).me(),s=Le(n,o);return c(` ${a.green("\u2714")} Already signed in as ${pe(s.user)}. You're ready to share.`),c(` ${a.dim(`To use another account, run \`${f("logout")}\`, then \`${f("login")}\`.`)}`),c(),d.ok}catch(o){if(!(o instanceof A&&o.status===401))return g(o instanceof Error?o.message:"Couldn't reach Localians.","Check your internet connection and try again."),d.gatewayUnavailable;c(` ${a.yellow("!")} Your saved login expired or was revoked. Let's sign in again.`),c(),K()}else n&&await Oe(n);let r=await lt(t,e);return typeof r=="number"?r:(c(` Next: start your app, then run ${a.bold(f("share <port>"))} ${a.dim(`(for example: ${f("share 3000")})`)}`),c(),d.ok)}async function mt(){let e=G();if(!e)return c(`
|
|
47
|
+
You're not signed in.
|
|
48
|
+
`),d.ok;let t=ce().filter(r=>r.pid!==process.pid),n=await Oe(e);return c(),c(` ${a.green("\u2714")} Signed out of ${pe(e.user)}. This computer's CLI access was revoked.`),t.length&&c(` ${a.dim(`${t.length===1?"The preview":`The ${t.length} previews`} shared from this computer stop${t.length===1?"s":""} too.`)}`),n||(c(` ${a.yellow("!")} Couldn't reach Localians to revoke this computer's access.`),c(` ${a.dim("It was removed locally. Revoke it from Settings \u2192 CLI in the dashboard to be certain.")}`)),c(` ${a.dim(`Sign in again (with any account): ${f("login")}`)}`),c(),d.ok}async function gt(e){let t=G();if(!t)return g("You're not signed in.",`Run: ${f("login")}`),d.auth;try{let n=await F(e.apiUrl??t.apiUrl,t.token,J).me();return{credentials:Le(t,n),me:n}}catch(n){return n instanceof A&&n.status===401?(K(),g("Your login on this computer expired or was revoked.",`Run: ${f("login")}`),d.auth):{credentials:t,me:null}}}async function ft(e){let t=await gt(e);if(typeof t=="number")return t;let{credentials:n,me:r}=t;return c(),c(` Signed in as ${pe(n.user)}${r?".":` ${a.yellow("(couldn't reach Localians to confirm)")}`}`),c(),n.user.id&&v("User ID",n.user.id),v("Workspace",n.workspace.name),v("Server",n.apiUrl),v("Login renews",`while used (next expiry ${new Date(n.expiresAt).toDateString()})`),c(),c(` ${a.dim(`Switch accounts: ${f("logout")}, then ${f("login")}.`)}`),c(),r?d.ok:d.gatewayUnavailable}async function ht(e){X();let t=await gt(e);if(typeof t=="number")return t;let{credentials:n,me:r}=t;v("Authentication",r?a.green("Signed in"):a.yellow("Couldn't verify (offline?)")),v("Account",n.user.username?`@${n.user.username} (${n.user.email})`:n.user.email),v("Workspace",n.workspace.name),v("Server",n.apiUrl),c();let o=ce();if(!o.length)return c(` ${a.dim(`Nothing is being shared from this computer. Start one with: ${f("share <port>")}`)}
|
|
49
|
+
`),d.ok;c(` ${a.bold(`${o.length} ${o.length===1?"app":"apps"} shared from this computer`)}${r?.limits?a.dim(` (up to ${r.limits.activePreviews} at once per account)`):""}`),c();for(let s of o.sort((i,l)=>i.localPort-l.localPort)){let i=s.state==="connected"?a.green:s.state==="reconnecting"?a.yellow:a.dim;v("Project",s.project.name),v("Local",`http://localhost:${s.localPort}`),v("Preview link",a.lime(s.publicUrl)),v("Status",i(s.state[0].toUpperCase()+s.state.slice(1))),v("Uptime",tt(Date.now()-new Date(s.startedAt).getTime())),s.expiresAt&&v("Expires",wt(s.expiresAt)),c()}return c(` ${a.dim("Stop one: press Ctrl+C in its terminal, or use Live Previews in the dashboard.")}
|
|
50
|
+
`),d.ok}function Ht(e,t){return g("Project not found. Select an existing project or create a new one.",e.projects.length?`Your projects: ${e.projects.map(n=>n.slug).join(", ")}. Run \`${f(`share ${t} --project <slug>`)}\`, or \`${f(`share ${t} --new`)}\` for a new one.`:`Run \`${f(`share ${t} --new`)}\` to create one.`),d.usage}async function le(e,t,n,r){try{let{project:o}=await e.createProject({name:t,framework:n,defaultPort:r});return c(` ${a.green("\u2714")} Created project ${a.bold(o.name)} ${a.dim(`(uses port ${r})`)}`),o}catch(o){return g(o instanceof Error?o.message:"Couldn't create a project."),d.error}}async function Ce(e,t,n,r,o,s){let i=t.projects.filter(x=>!s.includes(x)),l=[...s,...i].slice(0,9);c(` ${o}`),c(),c(` ${a.lime("1")} Create a new project called ${a.bold(`"${r.name}"`)}${s.length?"":a.dim(" (recommended)")}`),l.forEach((x,N)=>c(` ${a.lime(String(N+2))} Use ${x.name}${x.defaultPort?a.dim(` (port ${x.defaultPort})`):""}`)),c();let y=s.length?2:1,b=await Ee(` Choose 1\u2013${l.length+1} [${y}]: `);c();let p=b===""?y:Number(b);if(p===1)return le(e,r.name,r.framework,n);let h=Number.isInteger(p)?l[p-2]:void 0;return h||(g(`"${b}" isn't one of the options.`,`Run \`${f(`share ${n}`)}\` again.`),d.usage)}async function Ft(e,t,n,r,o){let s=re(process.cwd()),i=V(n);if(o.newProject){let p=await le(e,o.name??s.name,s.framework,r);return typeof p=="number"?p:{project:p,how:"new project"}}if(o.project){let p=o.project.toLowerCase(),h=t.projects.find(x=>x.id===o.project||x.slug===p||x.name.toLowerCase()===p);return h?{project:h,how:"--project"}:Ht(t,r)}let l=Qe(i,s.root,r);if(l){let p=t.projects.find(h=>h.id===l.projectId);if(p)return{project:p,how:"remembered for this folder"};_e(i,l.projectId)}let y=t.projects.filter(p=>p.defaultPort===r);if(y.length===1){let[p]=y;if(!ue()||o.yes)return c(` ${a.dim(`Sharing under "${p.name}" (it uses port ${r}). Use --project or --new to choose another.`)}`),{project:p,how:"uses this port"};let h=await Ee(` Share localhost:${r} as ${a.bold(p.name)}? ${a.dim("(it uses this port)")} [Y/n]: `);if(ut(h))return{project:p,how:"uses this port"};c();let x=await Ce(e,t,r,s,"Which project should this app be shared under?",[]);return typeof x=="number"?x:{project:x,how:"chosen"}}if(y.length>1){if(!ue())return g(`Several projects use port ${r}: ${y.map(h=>h.slug).join(", ")}.`,`Say which one: \`${f(`share ${r} --project <slug>`)}\`, or create a new one with \`${f(`share ${r} --new`)}\`.`),d.usage;let p=await Ce(e,t,r,s,`Several projects use port ${r}. Which one is this app?`,y);return typeof p=="number"?p:{project:p,how:"chosen"}}if(o.yes){let p=await le(e,s.name,s.framework,r);return typeof p=="number"?p:{project:p,how:"new project"}}if(!ue())return g(`No project in ${t.workspace.name} uses port ${r} yet.`,`Run \`${f(`share ${r} --new`)}\` to create "${s.name}", or \`${f(`share ${r} --project <slug>`)}\` to use an existing one.`),d.usage;let b=await Ce(e,t,r,s,`No project in ${a.bold(t.workspace.name)} uses port ${r} yet.`,[]);return typeof b=="number"?b:{project:b,how:"chosen"}}function wt(e){let t=new Date(e);if(Number.isNaN(t.getTime()))return"\u2014";let n=t.getTime()-Date.now(),r=t.toDateString()===new Date().toDateString(),o=t.toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"});return`in ${Fe(n)} (${r?`at ${o}`:`${t.toLocaleDateString([],{weekday:"short",month:"short",day:"numeric"})} ${o}`})`}async function rt(e,t){let n=G();n&&n.apiUrl!==e&&(n=null);for(let r=0;r<2;r++){if(!n){X(),c(` ${a.bold("First, connect this computer to your Localians account.")}`),c(` ${a.dim("Your browser will ask you to approve it. You only do this once.")}`),c();let s=await lt(e,t);if(typeof s=="number")return s;n=s}let o=F(e,n.token,J);try{let s=await o.me();return{api:o,me:s,credentials:Le(n,s)}}catch(s){if(s instanceof A&&s.status===401&&r===0){c(` ${a.yellow("!")} Your login on this computer expired or was revoked. Let's sign in again.`),K(),n=null;continue}return s instanceof A&&s.status===401?(g("You're not signed in.",`Run: ${f("login")}`),d.auth):(g(s instanceof Error?s.message:"Couldn't reach Localians.","Check your internet connection and try again."),d.gatewayUnavailable)}}return d.auth}async function yt(e,t){let n=$=>t.verbose&&c(` ${a.dim($)}`),r=await Me(e);if(!r)return g(`Nothing is running on localhost:${e}. Start your app and try again.`,`Start it as usual (for example \`npm run dev\`) and check the port it prints. If it uses another port, run \`${f("share <that port>")}\`.`),d.localUnreachable;n(`local app found on ${r==="::1"?"[::1]":r}:${e}`);let o=ce().find($=>$.localPort===e&&$.pid!==process.pid);if(o)return g(`localhost:${e} is already being shared from this computer (${o.project.name}): ${o.publicUrl}`,"Keep using that link, or stop it with Ctrl+C in its terminal first. Other apps can be shared at the same time on their own ports."),d.usage;let s=t.apiUrl?.replace(/\/+$/,"")??ct(t);if(!s)return pt();let i=await rt(s,t);if(typeof i=="number")return i;let{api:l,me:y,credentials:b}=i,p=null;if(t.setup){let $=t.setup,j=re(process.cwd());for(let u=0;;u++)try{let w=await l.redeemSetup($,e,{name:j.name,framework:j.framework});p=w.project,w.durationMinutes&&t.durationMinutes===void 0&&(t={...t,durationMinutes:w.durationMinutes}),w.url&&(t={...t,url:w.url}),n(p?`using project "${p.name}" from the dashboard setup`:"dashboard setup: project chosen automatically");break}catch(w){if(w instanceof A&&w.code==="setup_wrong_account"&&u===0&&ue()){c(` ${a.yellow("!")} ${w.message}`);let m=await Ee(` You're signed in as ${nt(b.user)}. Sign out and sign in with the account that created this code? [Y/n]: `);if(c(),!ut(m))return ot(w,e);await Oe(b);let S=await rt(s,t);if(typeof S=="number")return S;({api:l,me:y,credentials:b}=S);continue}return ot(w,e)}if(p||(p=y.projects.find(u=>u.defaultPort===e)??null),!p){let u=await le(l,j.name,j.framework,e);if(typeof u=="number")return u;p=u}ke(V(b),j.root,e,p)}else{let $=await Ft(l,y,b,e,t);if(typeof $=="number")return $;p=$.project,ke(V(b),re(process.cwd()).root,e,p),n(`sharing under project "${p.name}" (${$.how})`)}let h;try{h=await l.createTunnel(p.id,e,t.durationMinutes,t.url)}catch($){return Gt($,e,p,s,b)}let x=new Date,N=h.connectToken,Z=$=>Ze({pid:process.pid,sessionId:h.sessionId,publicUrl:h.publicUrl,localPort:e,project:{id:h.project.id,name:h.project.name},startedAt:x.toISOString(),expiresAt:h.expiresAt,state:$});Z("connecting");let Q=!1,q,ee=Xe({gatewayUrl:h.gatewayUrl,localPort:e,localAddress:r,hostHeader:t.hostHeader,userAgent:J,getConnectToken:async()=>{if(N){let $=N;return N=null,$}return l.connectToken(h.sessionId)},onRequest:t.verbose?$=>c(` ${a.dim(new Date().toLocaleTimeString())} ${$.websocket?a.cyan("WS "):""}${$.method.padEnd(6)} ${String($.status).padStart(3)} ${$.path.split("?")[0]} ${a.dim(`${$.ms}ms`)}`):void 0,onState:($,j)=>{$==="stopped"?q=j.reason:Z($),$==="connected"?Q?c(` ${a.green("\u25CF")} Reconnected \u2014 ${h.publicUrl}`):(Q=!0,X(),v("Signed in as",nt(y.user)),v("Workspace",y.workspace.name),v("Project",p.name),v("Local",`http://localhost:${e}`),v("Preview link",a.bold(a.lime(h.publicUrl))),v("Status",a.green("\u25CF Connected")),v("Plan",h.plan==="pass"?a.green("Preview Pass"):h.plan==="free"?"Free preview":"\u2014"),v("Access",h.access==="password"?a.green("Password protected"):a.yellow("Public \u2014 anyone with the link")),v("Expires",wt(h.expiresAt)),c(),c(" Send the preview link to anyone you want to show your app."),h.access==="password"?c(` ${a.dim("Visitors enter the project password first. Change it in the dashboard.")}`):c(` ${a.yellow("!")} Anyone with this link can open your app while sharing is on. Protect it with a password in the dashboard.`),c(` ${a.dim(`Keep this window open while you share. Press Ctrl+C to stop sharing. To share another app, run \`${f("share <port>")}\` in a new terminal.`)}`),c()):$==="reconnecting"&&c(` ${a.yellow("\u25CF")} Connection lost${j.reason?` (${j.reason})`:""}. Reconnecting in ${Math.ceil((j.delayMs??0)/1e3)}s\u2026`)}}),W=!1,H=()=>{W&&process.exit(d.interrupted),W=!0,c(`
|
|
51
|
+
${a.dim("Stopping\u2026")}`),ee.stop()};process.on("SIGINT",H),process.on("SIGTERM",H);let te=await ee.done;xe();try{await l.closeTunnel(h.sessionId)}catch{}return te==="user"?(c(` ${a.green("\u2714")} Stopped sharing. ${h.publicUrl} is no longer serving your app.
|
|
52
|
+
`),d.ok):te==="auth"?(g("Sharing stopped: this computer's login was revoked or expired.",`Run: ${f("login")}`),d.auth):(g(`Sharing stopped. ${q??"This preview session has ended."}`,q?void 0:`Run \`${f(`share ${e}`)}\` for a new link.`),d.error)}function ot(e,t){let n=e instanceof A?e.code:"",r="Copy a fresh command from Share App in the dashboard.";return n==="setup_wrong_account"?g("This setup code belongs to another account. Sign in with the correct account or generate a new code.",`Switch accounts: \`${f("logout")}\`, then \`${f("login")}\`. Or ${r.charAt(0).toLowerCase()}${r.slice(1)}`):n==="setup_expired"||n==="setup_used"||n==="setup_invalid"?g(e instanceof Error?e.message:"That setup code can't be used.",`${r} Or run \`${f(`share ${t}`)}\` without --setup.`):g(e instanceof Error?e.message:"Couldn't use that setup code.",`Or run \`${f(`share ${t}`)}\` without --setup.`),n==="setup_wrong_account"?d.auth:d.usage}function Gt(e,t,n,r,o){return e instanceof A&&e.status===401?(g("You're not signed in: this computer's login expired or was revoked.",`Run: ${f("login")}`),d.auth):e instanceof A&&e.status===404?(_e(V(o),n.id),g("Project not found. Select an existing project or create a new one.",`Run \`${f(`share ${t}`)}\` to choose, or \`${f(`share ${t} --new`)}\`.`),d.usage):e instanceof A&&(e.code==="free_used"||e.code==="free_limit")?(g(e.code==="free_used"?"This project's preview period has ended. Upgrade or renew your Preview Pass.":e.message,`${e.code==="free_used"?`${e.message} `:""}Get or renew it in the dashboard: ${r}/dashboard/projects/${n.id}#plan`),d.error):e instanceof A&&(e.code==="plan_required"||e.code==="invalid_duration"||e.code==="no_custom_name")?(g(e.message,`For example: ${f(`share ${t} --duration 1h`)}`),d.usage):e instanceof A&&e.code==="too_many_tunnels"?(g(e.message,`See what's running: ${f("status")}, or Live Previews in the dashboard.`),d.error):e instanceof A&&e.code==="project_archived"?(g(e.message,`Or share under another project: ${f(`share ${t} --project <slug>`)}`),d.usage):(g(e instanceof Error?e.message:"Couldn't create the preview.",e instanceof A&&e.code==="tunnels_unavailable"?"Sharing isn't set up on this Localians server yet.":void 0),d.gatewayUnavailable)}async function Yt(e){let t=Je(e);switch(t.kind){case"help":return process.stdout.write(Ye),d.ok;case"version":return process.stdout.write(`${Pe}
|
|
53
|
+
`),d.ok;case"error":return g(t.message,`Run \`${Te()} --help\` for usage.`),d.usage}let{command:n,port:r,options:o}=t;return n==="login"?dt(o):n==="logout"?mt():n==="status"?ht(o):n==="whoami"?ft(o):yt(r,o)}var[Jt]=process.versions.node.split(".").map(Number);Jt<20&&(g(`Localians needs Node.js 20 or newer (you have ${process.versions.node}).`),process.exit(d.error));process.on("uncaughtException",e=>{g(`Unexpected error: ${e.message}`,process.argv.includes("--verbose")||process.argv.includes("-v")?e.stack:"Re-run with --verbose for details."),process.exit(d.error)});Yt(process.argv.slice(2)).then(e=>process.exit(e),e=>{g(e instanceof Error?e.message:"Unexpected error."),process.exit(d.error)});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "localians",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Share your localhost with a secure HTTPS preview link — public or password protected.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
"dist/localians.js",
|
|
11
11
|
"README.md",
|
|
12
|
+
"CHANGELOG.md",
|
|
12
13
|
"LICENSE"
|
|
13
14
|
],
|
|
14
15
|
"scripts": {
|