localians 0.0.0-stage → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 THINXIFY
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,49 @@
1
- # Temporary Holding Version
1
+ # localians
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Share the app running on your computer with a secure HTTPS preview link — public, or protected with a password.
4
+
5
+ ```bash
6
+ npx localians share 3000
7
+ ```
8
+
9
+ No install, no account setup in the terminal, no flags. Works on Windows, macOS and Linux with Node.js 20+.
10
+
11
+ 1. Start your app as usual (`npm run dev`) and note its port (3000 for Next.js, 5173 for Vite, …).
12
+ 2. In a second terminal, in your project folder, run `npx localians share <port>`. The first time, your browser opens
13
+ so you can approve this computer — once.
14
+ 3. Copy the preview link it prints and send it. Keep the command running while you share; press **Ctrl+C** to stop.
15
+
16
+ The project is named after your app (from `package.json`, `pyproject.toml`, `Cargo.toml`, `composer.json` or `go.mod`,
17
+ else the folder name). Prefer clicking? The dashboard’s **Share App** page gives you a ready-made command with your
18
+ project and password already chosen.
19
+
20
+ ## Commands
21
+
22
+ | Command | What it does |
23
+ | --- | --- |
24
+ | `localians share <port>` | Share `localhost:<port>` (signs in first if needed) |
25
+ | `localians share <port> --setup <code>` | Use the project and access chosen in the dashboard's Share App page |
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 |
29
+ | `localians logout` | Sign out and revoke this computer's access |
30
+
31
+ Options: `-y, --yes` (create a project without asking, for scripts), `--host-header preserve`, `-v, --verbose`,
32
+ `--no-browser`, `--api-url <url>` (only needed for a self-hosted server). Run `localians --help` for details.
33
+
34
+ Exit codes: `0` ok, `1` other error, `2` usage, `3` login needed, `4` nothing running on that port, `5` Localians unreachable.
35
+
36
+ ## Security
37
+
38
+ - The CLI only forwards to your own computer (127.0.0.1 / ::1) on the port you choose.
39
+ - It connects outward over TLS — no router or firewall changes.
40
+ - Its access token is stored in your user config directory (owner-only permissions), never printed, and can be revoked
41
+ from the dashboard at any time.
42
+ - Public previews can be opened by anyone with the link. Enable password protection in the dashboard to restrict access.
43
+
44
+ Requires Node.js 20 or newer on Windows, macOS or Linux. Docs: https://github.com/THINXIFY/Localians
45
+
46
+ ## License
47
+
48
+ MIT — see [LICENSE](LICENSE). This license covers the `localians` CLI package only; the Localians service is not
49
+ open source.
@@ -0,0 +1,42 @@
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]
3
+
4
+ Commands:
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
8
+ logout Sign out and revoke this computer's CLI access
9
+
10
+ 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
14
+ --host-header <mode> "rewrite" (default) sends Host: localhost:<port>;
15
+ "preserve" forwards the public preview host
16
+
17
+ Global options:
18
+ --api-url <url> Localians server URL (or LOCALIANS_API_URL)
19
+ --no-browser Don't open the browser during login
20
+ -v, --verbose Print every forwarded request and connection details
21
+ -h, --help Show this help
22
+ --version Print the CLI version
23
+
24
+ Examples:
25
+ npx localians share 3000 Share a Next.js / React app on port 3000
26
+ npx localians share 5173 Share a Vite app
27
+ npx localians share 3000 --project shop Share under an existing project
28
+ npx localians status See what this computer is sharing
29
+
30
+ 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)});
package/package.json CHANGED
@@ -1,6 +1,46 @@
1
1
  {
2
2
  "name": "localians",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Share your localhost with a secure HTTPS preview link — public or password protected.",
5
+ "license": "MIT",
6
+ "bin": {
7
+ "localians": "dist/localians.js"
8
+ },
9
+ "files": [
10
+ "dist/localians.js",
11
+ "README.md",
12
+ "LICENSE"
13
+ ],
14
+ "scripts": {
15
+ "prepack": "node ../../scripts/verify-cli-package.mjs"
16
+ },
17
+ "engines": {
18
+ "node": ">=20"
19
+ },
20
+ "os": [
21
+ "win32",
22
+ "darwin",
23
+ "linux"
24
+ ],
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/THINXIFY/Localians.git",
28
+ "directory": "tunnel/cli"
29
+ },
30
+ "homepage": "https://localians.com",
31
+ "bugs": {
32
+ "url": "https://github.com/THINXIFY/Localians/issues"
33
+ },
34
+ "keywords": [
35
+ "localhost",
36
+ "tunnel",
37
+ "preview",
38
+ "share",
39
+ "dev-server",
40
+ "https",
41
+ "ngrok-alternative"
42
+ ],
43
+ "publishConfig": {
44
+ "access": "public"
45
+ }
46
+ }