1- import { parseTailscaleServeHttpsBaseUrlForPort , runTailscaleServeStatus as runSharedTailscaleServeStatus } from "@happier-dev/cli-common/tailscale" ;
1+ import { execFile } from "node:child_process" ;
2+ import { promisify } from "node:util" ;
3+
24import { parseBooleanEnv , parseIntEnv } from "@/config/env" ;
35
46type TailscaleServeStatusRunner = ( params : Readonly < {
@@ -7,6 +9,99 @@ type TailscaleServeStatusRunner = (params: Readonly<{
79 tailscaleBin ?: string ;
810} > ) => Promise < string > ;
911
12+ const execFileAsync = promisify ( execFile ) ;
13+
14+ function stripTrailingSlash ( url : string ) : string {
15+ return url . replace ( / \/ + $ / , "" ) ;
16+ }
17+
18+ function normalizeHttpsUrl ( raw : string ) : string | null {
19+ const value = String ( raw ?? "" ) . trim ( ) ;
20+ if ( ! value ) return null ;
21+
22+ let parsed : URL ;
23+ try {
24+ parsed = new URL ( value ) ;
25+ } catch {
26+ return null ;
27+ }
28+
29+ if ( parsed . protocol !== "https:" ) return null ;
30+ parsed . username = "" ;
31+ parsed . password = "" ;
32+ parsed . search = "" ;
33+ parsed . hash = "" ;
34+ return stripTrailingSlash ( parsed . toString ( ) ) ;
35+ }
36+
37+ function tryParseProxyTargetFromLine ( line : string ) : URL | null {
38+ const trimmed = String ( line ?? "" ) . trim ( ) ;
39+ const match = trimmed . match ( / \b p r o x y \s + ( \S + ) / i) ;
40+ const raw = match ?. [ 1 ] ? String ( match [ 1 ] ) . trim ( ) : "" ;
41+ if ( ! raw ) return null ;
42+
43+ try {
44+ return new URL ( raw ) ;
45+ } catch {
46+ return null ;
47+ }
48+ }
49+
50+ function extractTailscaleServeHttpsUrl ( serveStatusText : string ) : string | null {
51+ const line = String ( serveStatusText ?? "" )
52+ . split ( / \r ? \n / )
53+ . map ( ( value ) => value . trim ( ) )
54+ . find ( ( value ) => value . toLowerCase ( ) . includes ( "https://" ) ) ;
55+ if ( ! line ) return null ;
56+
57+ const match = line . match ( / h t t p s : \/ \/ \S + / i) ;
58+ if ( ! match ) return null ;
59+ return normalizeHttpsUrl ( match [ 0 ] ) ;
60+ }
61+
62+ function parseTailscaleServeHttpsBaseUrlForPort ( statusText : string , port : number ) : string | null {
63+ const wantedPort = Number . isFinite ( port ) && port > 0 ? String ( Math . trunc ( port ) ) : "" ;
64+ if ( ! wantedPort ) return null ;
65+
66+ let currentBase : string | null = null ;
67+ const lines = String ( statusText ?? "" ) . split ( / \r ? \n / ) ;
68+ for ( const rawLine of lines ) {
69+ const line = String ( rawLine ?? "" ) . trim ( ) ;
70+ if ( ! line ) continue ;
71+
72+ const maybeHttps = line . match ( / ^ ( h t t p s : \/ \/ \S + ) / i) ?. [ 1 ] ;
73+ if ( maybeHttps && ! line . toLowerCase ( ) . includes ( "proxy" ) ) {
74+ currentBase = normalizeHttpsUrl ( maybeHttps ) ;
75+ continue ;
76+ }
77+
78+ if ( ! currentBase ) continue ;
79+ const proxyTarget = tryParseProxyTargetFromLine ( line ) ;
80+ if ( ! proxyTarget ) continue ;
81+ if ( proxyTarget . port === wantedPort ) {
82+ return currentBase ;
83+ }
84+ }
85+
86+ return null ;
87+ }
88+
89+ async function runLocalTailscaleServeStatus ( params : Readonly < {
90+ timeoutMs : number ;
91+ env : NodeJS . ProcessEnv ;
92+ tailscaleBin ?: string ;
93+ } > ) : Promise < string > {
94+ const command = String ( params . tailscaleBin ?? params . env . HAPPIER_TAILSCALE_BIN ?? "tailscale" ) . trim ( ) || "tailscale" ;
95+ const timeoutMs = Math . max ( 1 , Math . min ( 10_000 , Math . trunc ( params . timeoutMs ) ) ) ;
96+ const mergedEnv = { ...process . env , ...params . env } ;
97+ const result = await execFileAsync ( command , [ "serve" , "status" ] , {
98+ env : mergedEnv ,
99+ timeout : timeoutMs ,
100+ maxBuffer : 2 * 1024 * 1024 ,
101+ } ) ;
102+ return String ( result . stdout ?? "" ) ;
103+ }
104+
10105function resolveTailscaleServeStatusTimeoutMs ( env : NodeJS . ProcessEnv ) : number {
11106 const raw = String ( env . HAPPIER_TAILSCALE_SERVE_STATUS_TIMEOUT_MS ?? "" ) . trim ( ) ;
12107 return parseIntEnv ( raw , 750 , { min : 1 , max : 10_000 } ) ;
@@ -32,11 +127,11 @@ export async function inferAndApplyTailscaleServePublicServerUrl(
32127 const statusTimeoutMs = resolveTailscaleServeStatusTimeoutMs ( env ) ;
33128
34129 try {
35- const status = await ( deps ?. runTailscaleServeStatus ?? runSharedTailscaleServeStatus ) ( {
130+ const status = await ( deps ?. runTailscaleServeStatus ?? runLocalTailscaleServeStatus ) ( {
36131 timeoutMs : statusTimeoutMs ,
37132 env,
38133 } ) ;
39- const inferred = parseTailscaleServeHttpsBaseUrlForPort ( status , port ) ;
134+ const inferred = parseTailscaleServeHttpsBaseUrlForPort ( status , port ) ?? extractTailscaleServeHttpsUrl ( status ) ;
40135 if ( ! inferred ) return null ;
41136 if ( String ( env . HAPPIER_PUBLIC_SERVER_URL ?? "" ) . trim ( ) ) return null ;
42137 env . HAPPIER_PUBLIC_SERVER_URL = inferred ;
0 commit comments