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 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 login` | Sign this computer in |
28
- | `localians status` | Show your login and what this computer is sharing |
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: `-y, --yes` (create a project without asking, for scripts), `--host-header preserve`, `-v, --verbose`,
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 Ve=Object.create;var fe=Object.defineProperty;var Ke=Object.getOwnPropertyDescriptor;var Xe=Object.getOwnPropertyNames;var Ze=Object.getPrototypeOf,Qe=Object.prototype.hasOwnProperty;var et=(e,t,r,s)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of Xe(t))!Qe.call(e,n)&&n!==r&&fe(e,n,{get:()=>t[n],enumerable:!(s=Ke(t,n))||s.enumerable});return e};var T=(e,t,r)=>(r=e!=null?Ve(Ze(e)):{},et(t||!e||!e.__esModule?fe(r,"default",{value:e,enumerable:!0}):r,e));var ve=require("node:util");var se=T(require("node:fs")),we=T(require("node:net")),ye=T(require("node:os")),A=T(require("node:path")),ie=["127.0.0.1","::1"];function be(e){if(!e||!/^\d{1,5}$/.test(e.trim()))return null;let t=Number(e);return t>=1&&t<=65535?t:null}function tt(e,t,r=1500){return new Promise(s=>{let n=we.default.connect({host:e,port:t}),i=o=>{n.destroy(),s(o)};n.setTimeout(r,()=>i(!1)),n.once("connect",()=>i(!0)),n.once("error",()=>i(!1))})}async function Se(e){for(let t of ie)if(await tt(t,e))return t;return null}var J=e=>{try{return se.default.readFileSync(e,"utf8")}catch{return null}};function oe(e,t){let r=e.indexOf(`[${t}]`);return r<0?null:e.slice(r+t.length+2).split(/^\[/m)[0].match(/^\s*name\s*=\s*["']([^"']+)["']/m)?.[1]??null}function rt(e){let t=J(A.default.join(e,"package.json"));if(t!==null)try{let o=JSON.parse(t),u={...o.dependencies,...o.devDependencies},m=u.next?"NEXTJS":u["@angular/core"]?"ANGULAR":u.vue?"VUE":u.vite||u.react?"REACT_VITE":Object.keys(u).length?"NODEJS":null;return{name:o.name?.replace(/^@[^/]+\//,"")||null,framework:m}}catch{return{name:null,framework:null}}let r=J(A.default.join(e,"pyproject.toml"));if(r!==null)return{name:oe(r,"project")??oe(r,"tool.poetry"),framework:null};let s=J(A.default.join(e,"Cargo.toml"));if(s!==null)return{name:oe(s,"package"),framework:null};let n=J(A.default.join(e,"composer.json"));if(n!==null)try{return{name:JSON.parse(n).name?.split("/").pop()||null,framework:null}}catch{return{name:null,framework:null}}let i=J(A.default.join(e,"go.mod"));if(i!==null)return{name:i.match(/^module\s+(\S+)/m)?.[1].split("/").pop()??null,framework:null};for(let o of["Gemfile","manage.py","requirements.txt","index.html"])if(se.default.existsSync(A.default.join(e,o)))return{name:null,framework:null};return null}var he=e=>e.trim().replace(/\s+/g," ").slice(0,80);function ae(e,t=ye.default.homedir()){let r=new Set([A.default.resolve(t),A.default.parse(A.default.resolve(e)).root]),s=A.default.resolve(e);for(let i=0;i<6&&!r.has(s);i++){let o=rt(s);if(o)return{name:he(o.name??A.default.basename(s))||"My app",framework:o.framework};let u=A.default.dirname(s);if(u===s)break;s=u}let n=A.default.resolve(e);return{name:r.has(n)?"My app":he(A.default.basename(n))||"My app",framework:null}}var Ae=`Usage: localians <command> [options]
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
- status Show your login and active shares
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 project that uses this port)
12
- -y, --yes Create a project for this app without asking, if none uses the port
13
- --setup <code> Use the project and access chosen in the dashboard's Share App page
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
- `);function z(){l(),l(` ${c.lime("\u25A0")} ${c.bold("LOCALIANS")}`),l()}var b=(e,t)=>l(` ${c.dim(e.padEnd(18))}${t}`);function h(e,t){ge(`
34
- ${c.red("\u2716")} ${e}`),t&&ge(` ${c.dim(t)}`),ge()}function Ue(e){let t=Math.floor(e/1e3),r=Math.floor(t/3600),s=Math.floor(t%3600/60);return r?`${r}h ${s}m`:s?`${s}m ${t%60}s`:`${t}s`}var d={ok:0,error:1,usage:2,auth:3,localUnreachable:4,gatewayUnavailable:5,interrupted:130};var me="0.1.0",G=`localians-cli/${me} (${process.platform}; node ${process.versions.node})`,lt="https://localians.com";function Fe(e){return(e.apiUrl??process.env.LOCALIANS_API_URL??B()?.apiUrl??lt)?.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 pt(e){let t=ut(e);if(!t)return;let[r,s]=t;try{(0,De.spawn)(r,s,{stdio:"ignore",detached:!0,windowsHide:!0}).on("error",()=>{}).unref()}catch{}}var dt=e=>new Promise(t=>setTimeout(t,e)),gt=()=>!!(process.stdin.isTTY&&process.stdout.isTTY);async function mt(e){let t=(0,Ge.createInterface)({input:process.stdin,output:process.stdout});try{return(await t.question(e)).trim()}finally{t.close()}}async function qe(e,t){let r=D(e,null,G),s;try{s=await r.startDeviceLogin(`CLI on ${Me.default.hostname()} (${process.platform})`.slice(0,80))}catch(o){return h(o instanceof Error?o.message:"Couldn't start login.","Check your internet connection."),d.gatewayUnavailable}l(` Confirm this code in your browser: ${c.bold(c.lime(s.user_code))}`),l(` ${c.dim(s.verification_uri_complete)}`),l(),t.browser&&process.stdout.isTTY&&pt(s.verification_uri_complete),l(` ${c.dim("Waiting for you to approve it\u2026 (Ctrl+C to cancel)")}`);let n=s.interval*1e3,i=Date.now()+s.expires_in*1e3;for(;Date.now()<i;){await dt(n);try{let o=await r.pollDeviceLogin(s.device_code),u={apiUrl:e,token:o.access_token,expiresAt:o.expires_at,user:o.user,workspace:o.workspace,createdAt:new Date().toISOString()};return Re(u),l(),l(` ${c.green("\u2714")} Logged in as ${c.bold(o.user.email)} ${c.dim(`(${o.workspace.name})`)}`),l(` ${c.dim(`Access expires ${new Date(o.expires_at).toDateString()}. Revoke it any time in Settings \u2192 CLI.`)}`),l(),u}catch(o){if(o instanceof x){if(o.code==="authorization_pending")continue;if(o.code==="slow_down"){n+=5e3;continue}if(o.code==="access_denied")return h("Login was denied in the browser."),d.auth;if(o.code==="expired_token"||o.code==="invalid_grant")return h("The login request expired before it was approved.","Run the command again and approve it in your browser."),d.auth;if(o.code==="network")continue}return h(o instanceof Error?o.message:"Login failed."),d.error}}return h("The login request expired before it was approved.","Run the command again and approve it in your browser."),d.auth}function Je(){return h("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 We(e){let t=Fe(e);if(!t)return Je();z();let r=await qe(t,e);return typeof r=="number"?r:d.ok}async function Be(e){let t=B();if(!t)return l(`
35
- You're not logged in.
36
- `),d.ok;let r=D(e.apiUrl??t.apiUrl,t.token,G),s=!1;try{await r.logout(),s=!0}catch(n){s=n instanceof x&&n.status===401}return pe(),l(),l(` ${c.green("\u2714")} Logged out of ${t.user.email}.`),s||(l(` ${c.yellow("!")} Couldn't reach Localians to revoke this computer's token.`),l(` ${c.dim("It was removed locally. Revoke it from Settings \u2192 CLI in the dashboard to be certain.")}`)),l(),d.ok}async function ze(e){z();let t=B();if(!t)return b("Authentication",c.yellow("Not logged in")),l(`
37
- ${c.dim("Run `localians share <port>` to sign in and share an app.")}
38
- `),d.auth;let r=D(e.apiUrl??t.apiUrl,t.token,G),s=c.green("Logged in");try{await r.me()}catch(i){s=i instanceof x&&i.status===401?c.red("Expired or revoked \u2014 run `localians login`"):c.yellow("Couldn't verify (offline?)")}b("Authentication",s),b("Account",t.user.email),b("Workspace",t.workspace.name),b("Server",t.apiUrl),l();let n=Oe();if(!n.length)return l(` ${c.dim("Nothing is being shared from this computer.")}
39
- `),d.ok;for(let i of n){let o=i.state==="connected"?c.green:i.state==="reconnecting"?c.yellow:c.dim;b("Project",i.project.name),b("Local",`http://localhost:${i.localPort}`),b("Preview link",c.lime(i.publicUrl)),b("Status",o(i.state[0].toUpperCase()+i.state.slice(1))),b("Uptime",Ue(Date.now()-new Date(i.startedAt).getTime())),l()}return d.ok}function He(e,t,r){return r?e.find(s=>s.id===r||s.slug===r.toLowerCase())??null:e.find(s=>s.defaultPort===t)??null}async function ft(e,t,r,s){let n=ae(process.cwd()),i=async()=>{try{let{project:f}=await e.createProject({name:n.name,framework:n.framework,defaultPort:r});return l(` ${c.green("\u2714")} Created project ${c.bold(f.name)} ${c.dim(`(uses port ${r})`)}`),f}catch(f){return h(f instanceof Error?f.message:"Couldn't create a project."),d.error}};if(s.yes)return i();if(!gt())return h(`No project in ${t.workspace.name} uses port ${r} yet.`,`Run \`localians share ${r} --yes\` to create "${n.name}", or \`localians share ${r} --project <name>\` to use an existing one.`),d.usage;l(` No project in ${c.bold(t.workspace.name)} uses port ${r} yet.`),l(),l(` ${c.lime("1")} Create a new project called ${c.bold(`"${n.name}"`)} ${c.dim("(recommended)")}`),t.projects.slice(0,9).forEach((f,L)=>l(` ${c.lime(String(L+2))} Use ${f.name}${f.defaultPort?c.dim(` (port ${f.defaultPort})`):""}`)),l();let o=await mt(` Choose 1\u2013${Math.min(t.projects.length,9)+1} [1]: `);l();let u=o===""?1:Number(o);if(u===1)return i();let m=Number.isInteger(u)?t.projects[u-2]:void 0;return m||(h(`"${o}" isn't one of the options.`,`Run \`localians share ${r}\` again.`),d.usage)}async function Ye(e,t){let r=p=>t.verbose&&l(` ${c.dim(p)}`),s=await Se(e);if(!s)return h(`We couldn't find a website running on port ${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 `localians share <that port>`."),d.localUnreachable;r(`local app found on ${s==="::1"?"[::1]":s}:${e}`);let n=B(),i=t.apiUrl??n?.apiUrl??Fe(t);if(!i)return Je();let o=D(i,n?.token??null,G),u;for(let p=0;p<2&&!u;p++){if(!n){z(),l(` ${c.bold("First, connect this computer to your Localians account.")}`),l(` ${c.dim("Your browser will ask you to approve it. You only do this once.")}`),l();let S=await qe(i,t);if(typeof S=="number")return S;n=S,o=D(i,n.token,G)}try{u=await o.me()}catch(S){if(S instanceof x&&S.status===401&&p===0){l(` ${c.yellow("!")} Your login on this computer expired or was revoked. Let's sign in again.`),pe(),n=null;continue}return S instanceof x&&S.status===401?(h("Localians didn't accept this computer's login.","Run `localians login` and try again."),d.auth):(h(S instanceof Error?S.message:"Couldn't reach Localians.","Check your internet connection."),d.gatewayUnavailable)}}if(!u)return d.auth;let m=null;if(t.setup){try{m=(await o.redeemSetup(t.setup,e,ae(process.cwd()))).project,r(m?`using project "${m.name}" from the dashboard setup`:"dashboard setup: project chosen automatically")}catch(p){return h(p instanceof Error?p.message:"Couldn't use that setup code.",`Or run \`localians share ${e}\` without --setup.`),d.usage}m||(m=He(u.projects,e)),m||(t={...t,yes:!0})}if(m||(m=He(u.projects,e,t.project)),!m&&t.project)return h(`Project "${t.project}" wasn't found in ${u.workspace.name}.`,u.projects.length?`Available: ${u.projects.map(p=>p.slug).join(", ")}`:"Leave out --project to create one."),d.error;if(!m){let p=await ft(o,u,e,t);if(typeof p=="number")return p;m=p}r(`sharing under project "${m.name}"`);let f;try{f=await o.createTunnel(m.id,e)}catch(p){return p instanceof x&&p.status===401?(h("Your CLI login has expired or was revoked.","Run `localians login` to sign in again."),d.auth):p instanceof x&&p.status===404?(h("You don't have access to that project.","Check the project in the dashboard, or omit --project."),d.auth):(h(p instanceof Error?p.message:"Couldn't create the preview.",p instanceof x&&p.code==="tunnels_unavailable"?"Sharing isn't set up on this Localians server yet.":void 0),d.gatewayUnavailable)}let L=new Date,U=f.connectToken,O=p=>Ie({pid:process.pid,sessionId:f.sessionId,publicUrl:f.publicUrl,localPort:e,project:{id:f.project.id,name:f.project.name},startedAt:L.toISOString(),state:p}),H=!1,F,Y=Ee({gatewayUrl:f.gatewayUrl,localPort:e,localAddress:s,hostHeader:t.hostHeader,userAgent:G,getConnectToken:async()=>{if(U){let p=U;return U=null,p}return o.connectToken(f.sessionId)},onRequest:t.verbose?p=>l(` ${c.dim(new Date().toLocaleTimeString())} ${p.websocket?c.cyan("WS "):""}${p.method.padEnd(6)} ${String(p.status).padStart(3)} ${p.path.split("?")[0]} ${c.dim(`${p.ms}ms`)}`):void 0,onState:(p,S)=>{p==="stopped"?F=S.reason:O(p),p==="connected"?H?l(` ${c.green("\u25CF")} Reconnected \u2014 ${f.publicUrl}`):(H=!0,z(),b("Signed in as",u.user.email),b("Workspace",u.workspace.name),b("Project",m.name),b("Local",`http://localhost:${e}`),b("Preview link",c.bold(c.lime(f.publicUrl))),b("Status",c.green("\u25CF Connected")),b("Access",f.access==="password"?c.green("Password protected"):c.yellow("Public \u2014 anyone with the link")),l(),l(" Send the preview link to anyone you want to show your app."),f.access==="password"?l(` ${c.dim("Visitors enter the project password first. Change it in the dashboard.")}`):l(` ${c.yellow("!")} Anyone with this link can open your app while sharing is on. Protect it with a password in the dashboard.`),l(` ${c.dim("Keep this window open while you share. Press Ctrl+C to stop sharing.")}`),l()):p==="reconnecting"&&l(` ${c.yellow("\u25CF")} Connection lost${S.reason?` (${S.reason})`:""}. Reconnecting in ${Math.ceil((S.delayMs??0)/1e3)}s\u2026`)}}),V=!1,K=()=>{V&&process.exit(d.interrupted),V=!0,l(`
40
- ${c.dim("Stopping\u2026")}`),Y.stop()};process.on("SIGINT",K),process.on("SIGTERM",K);let q=await Y.done;de();try{await o.closeTunnel(f.sessionId)}catch{}return q==="user"?(l(` ${c.green("\u2714")} Stopped sharing. ${f.publicUrl} is no longer serving your app.
41
- `),d.ok):q==="auth"?(h("Sharing stopped: your CLI access was revoked or expired.","Run `localians login` to sign in again."),d.auth):(h(`Sharing stopped. ${F??"This preview session has ended."}`,F?void 0:`Run \`localians share ${e}\` for a new link.`),d.error)}async function ht(e){let t=ke(e);switch(t.kind){case"help":return process.stdout.write(Ae),d.ok;case"version":return process.stdout.write(`${me}
42
- `),d.ok;case"error":return h(t.message,"Run `localians --help` for usage."),d.usage}let{command:r,port:s,options:n}=t;return r==="login"?We(n):r==="logout"?Be(n):r==="status"?ze(n):Ye(s,n)}var[wt]=process.versions.node.split(".").map(Number);wt<20&&(h(`Localians needs Node.js 20 or newer (you have ${process.versions.node}).`),process.exit(d.error));process.on("uncaughtException",e=>{h(`Unexpected error: ${e.message}`,process.argv.includes("--verbose")||process.argv.includes("-v")?e.stack:"Re-run with --verbose for details."),process.exit(d.error)});ht(process.argv.slice(2)).then(e=>process.exit(e),e=>{h(e instanceof Error?e.message:"Unexpected error."),process.exit(d.error)});
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.1.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": {