@@ -11,6 +11,7 @@ export interface Options {
1111 command : string [ ] ;
1212 host : string ;
1313 port ?: number ;
14+ tailscalePort ?: number ;
1415 timeoutMs : number ;
1516 openCheck : boolean ;
1617}
@@ -25,12 +26,15 @@ Options:
2526 --port <port> Expose this port instead of detecting one from output.
2627 --host <host> Local host to expose. Default: 127.0.0.1
2728 --timeout <ms> Port-detection timeout. Default: ${ DEFAULT_TIMEOUT_MS }
29+ --tailscale-port <port>
30+ Expose on this Tailscale HTTPS port instead of 443.
2831 --no-open-check Skip waiting for the local port to accept connections.
2932 -h, --help Show this help.
3033
3134Examples:
3235 lizardtail pnpm dev
3336 lizardtail --port 3000 npm run dev
37+ lizardtail --tailscale-port 8450 pnpm dev
3438` ) ;
3539}
3640
@@ -89,6 +93,23 @@ export function parseArgs(argv: string[]): Options {
8993 continue ;
9094 }
9195
96+ if ( arg === "--tailscale-port" || arg === "--https-port" ) {
97+ const value = argv [ ++ i ] ;
98+ if ( ! value ) usage ( ) ;
99+ options . tailscalePort = parsePort ( value ) ;
100+ continue ;
101+ }
102+
103+ if ( arg . startsWith ( "--tailscale-port=" ) ) {
104+ options . tailscalePort = parsePort ( arg . slice ( "--tailscale-port=" . length ) ) ;
105+ continue ;
106+ }
107+
108+ if ( arg . startsWith ( "--https-port=" ) ) {
109+ options . tailscalePort = parsePort ( arg . slice ( "--https-port=" . length ) ) ;
110+ continue ;
111+ }
112+
92113 if ( arg === "--timeout" ) {
93114 const value = argv [ ++ i ] ;
94115 if ( ! value ) usage ( ) ;
@@ -166,7 +187,18 @@ function isTailscaleServePermissionError(error: unknown): boolean {
166187 return message . includes ( "access denied" ) && ( message . includes ( "sudo tailscale serve" ) || message . includes ( "operator" ) ) ;
167188}
168189
169- function tailscaleServePermissionHelp ( target : string ) : string {
190+ function tailscaleServeCommand ( target : string , tailscalePort ?: number ) : string [ ] {
191+ const args = [ "serve" , "--bg" ] ;
192+ if ( tailscalePort !== undefined ) args . push ( "--https" , String ( tailscalePort ) ) ;
193+ args . push ( target ) ;
194+ return args ;
195+ }
196+
197+ function tailscaleUrl ( dnsName : string , tailscalePort ?: number ) : string {
198+ return tailscalePort === undefined ? `https://${ dnsName } ` : `https://${ dnsName } :${ tailscalePort } ` ;
199+ }
200+
201+ function tailscaleServePermissionHelp ( target : string , tailscalePort ?: number ) : string {
170202 return `Tailscale refused to update Serve config without elevated privileges.
171203
172204Run this once to let your user manage Tailscale Serve:
@@ -177,7 +209,7 @@ Then rerun lizardtail.
177209
178210Or expose this server manually with:
179211
180- sudo tailscale serve --bg ${ target } ` ;
212+ sudo tailscale ${ tailscaleServeCommand ( target , tailscalePort ) . join ( " " ) } ` ;
181213}
182214
183215async function exec ( command : string , args : string [ ] , opts : { input ?: string } = { } ) : Promise < { stdout : string ; stderr : string } > {
@@ -239,24 +271,24 @@ export async function waitForOpenPort(host: string, port: number, timeoutMs: num
239271 throw new Error ( `timed out waiting for ${ host } :${ port } to accept connections` ) ;
240272}
241273
242- export async function exposeWithTailscale ( host : string , port : number ) : Promise < string > {
274+ export async function exposeWithTailscale ( host : string , port : number , tailscalePort ?: number ) : Promise < string > {
243275 await exec ( "tailscale" , [ "status" ] ) ;
244276
245277 const target = `http://${ host } :${ port } ` ;
246278 try {
247- await exec ( "tailscale" , [ "serve" , "--bg" , target ] ) ;
279+ await exec ( "tailscale" , tailscaleServeCommand ( target , tailscalePort ) ) ;
248280 } catch ( firstError ) {
249281 if ( isTailscaleServePermissionError ( firstError ) ) {
250- throw new Error ( tailscaleServePermissionHelp ( target ) ) ;
282+ throw new Error ( tailscaleServePermissionHelp ( target , tailscalePort ) ) ;
251283 }
252284
253285 if ( host !== "127.0.0.1" && host !== "localhost" ) throw firstError ;
254286
255287 try {
256- await exec ( "tailscale" , [ "serve" , "--bg" , String ( port ) ] ) ;
288+ await exec ( "tailscale" , tailscaleServeCommand ( String ( port ) , tailscalePort ) ) ;
257289 } catch ( fallbackError ) {
258290 if ( isTailscaleServePermissionError ( fallbackError ) ) {
259- throw new Error ( tailscaleServePermissionHelp ( target ) ) ;
291+ throw new Error ( tailscaleServePermissionHelp ( target , tailscalePort ) ) ;
260292 }
261293 throw fallbackError ;
262294 }
@@ -266,10 +298,10 @@ export async function exposeWithTailscale(host: string, port: number): Promise<s
266298 const status = JSON . parse ( stdout ) as { Self ?: { DNSName ?: string ; TailscaleIPs ?: string [ ] } } ;
267299 const dnsName = status . Self ?. DNSName ?. replace ( / \. $ / , "" ) ;
268300
269- if ( dnsName ) return `https:// ${ dnsName } ` ;
301+ if ( dnsName ) return tailscaleUrl ( dnsName , tailscalePort ) ;
270302
271303 const ip = status . Self ?. TailscaleIPs ?. find ( ( value ) => / ^ \d + \. \d + \. \d + \. \d + $ / . test ( value ) ) ;
272- if ( ip ) return `https:// ${ ip } ` ;
304+ if ( ip ) return tailscaleUrl ( ip , tailscalePort ) ;
273305
274306 throw new Error ( "could not determine this device's Tailscale DNS name or IP" ) ;
275307}
@@ -298,7 +330,7 @@ export async function main(): Promise<void> {
298330 const localUrl = `http://${ options . host } :${ port } ` ;
299331 console . error ( `\nlizardtail: detected local server on ${ localUrl } ` ) ;
300332 if ( options . openCheck ) await waitForOpenPort ( options . host , port , 10_000 ) ;
301- const tailscaleUrl = await exposeWithTailscale ( options . host , port ) ;
333+ const tailscaleUrl = await exposeWithTailscale ( options . host , port , options . tailscalePort ) ;
302334 console . error ( `lizardtail: serving via Tailscale: ${ tailscaleUrl } \n` ) ;
303335 } ) ( ) . catch ( ( error : unknown ) => {
304336 console . error ( `lizardtail: failed to expose server: ${ errorMessage ( error ) } ` ) ;
0 commit comments