@@ -117,6 +117,97 @@ if [[ -n "$WORKSPACE" ]]; then
117117 [[ -n " $VSCODE_URL " ]] && VSCODE_PART=$( link " $VSCODE_URL " " ⧉ VS Code" )
118118fi
119119
120+ # ── Portless URL detection ─────────────────────────────────
121+ # Maps the current workspace to its Portless route by checking each route
122+ # process's cwd. Subdomain-based guessing breaks when multiple worktrees
123+ # run dev servers concurrently (e.g. one auto-named route can match a
124+ # different worktree's slug). cwd is the source of truth — `dev.mjs` is
125+ # spawned from the workspace, so its portless child's cwd equals it.
126+ #
127+ # Cache: pid → cwd in CACHE_DIR for 30s. Portless restarts cycle pids
128+ # quickly, so a short TTL trades minimal staleness for avoiding lsof per
129+ # tick (lsof on macOS spawns ~30–100ms per call).
130+ PORTLESS_PART=" "
131+ if [[ -n " $WORKSPACE " ]] && command -v portless > /dev/null 2>&1 && command -v lsof > /dev/null 2>&1 ; then
132+ PORTLESS_TTL=30
133+ WS_REAL=$( python3 -c ' import os,sys; print(os.path.realpath(sys.argv[1]))' " $WORKSPACE " 2> /dev/null || printf ' %s' " $WORKSPACE " )
134+ PORTLESS_LIST=$( portless list 2> /dev/null || true)
135+
136+ PORTLESS_URL=" "
137+ PORTLESS_NOTE=" "
138+ if [[ -n " $PORTLESS_LIST " ]]; then
139+ while IFS= read -r _line; do
140+ [[ " $_line " =~ pid\ ([0-9]+) ]] || continue
141+ _pid=" ${BASH_REMATCH[1]} "
142+ _pid_cache=" ${CACHE_DIR} /portless-pid-${_pid} .cwd"
143+ _cwd=" "
144+ if [[ -f " $_pid_cache " ]]; then
145+ _age=$(( $(date +% s) - $(file_mtime "$_pid_cache ") ))
146+ if (( _age < PORTLESS_TTL )) ; then
147+ _cwd=$( cat " $_pid_cache " 2> /dev/null) || _cwd=" "
148+ fi
149+ fi
150+ if [[ -z " $_cwd " ]]; then
151+ _cwd=$( lsof -p " $_pid " -a -d cwd -Fn 2> /dev/null | awk ' /^n/{print substr($0,2); exit}' || true)
152+ if [[ -n " $_cwd " ]]; then
153+ printf ' %s' " $_cwd " > " $_pid_cache " 2> /dev/null || true
154+ fi
155+ fi
156+ [[ -z " $_cwd " ]] && continue
157+ _cwd_real=$( python3 -c ' import os,sys; print(os.path.realpath(sys.argv[1]))' " $_cwd " 2> /dev/null || printf ' %s' " $_cwd " )
158+ if [[ " $_cwd_real " == " $WS_REAL " ]]; then
159+ PORTLESS_URL=$( printf ' %s' " $_line " | grep -oE ' https?://[a-z0-9-]+\.localhost:[0-9]+' | head -1 || true)
160+ break
161+ fi
162+ done <<< " $PORTLESS_LIST"
163+ fi
164+
165+ # Fallback: portless route not registered (e.g. lost via the 0.10.x race
166+ # where a dying prior owner's onCleanup wipes the new owner's entry).
167+ # Scan for any `portless` CLI process whose cwd matches this workspace —
168+ # if found, the dev server IS running, just unrouted. Derive subdomain
169+ # from argv (explicit `portless <name>`) or package.json (auto-name).
170+ if [[ -z " $PORTLESS_URL " ]]; then
171+ for _pid in $( pgrep -f ' portless/dist/cli\.js' 2> /dev/null || true) ; do
172+ _cwd=$( lsof -p " $_pid " -a -d cwd -Fn 2> /dev/null | awk ' /^n/{print substr($0,2); exit}' || true)
173+ [[ -z " $_cwd " ]] && continue
174+ _cwd_real=$( python3 -c ' import os,sys; print(os.path.realpath(sys.argv[1]))' " $_cwd " 2> /dev/null || printf ' %s' " $_cwd " )
175+ [[ " $_cwd_real " != " $WS_REAL " ]] && continue
176+ _argv=$( ps -o command= -p " $_pid " 2> /dev/null || true)
177+ # Skip the proxy daemon itself (`portless proxy start ...`)
178+ [[ " $_argv " =~ cli\. js[[:space:]]+proxy ]] && continue
179+ _sub=$( printf ' %s' " $_argv " | grep -oE ' cli\.js[[:space:]]+[a-zA-Z0-9_.-]+' | awk ' {print $2}' | head -1 || true)
180+ if [[ -z " $_sub " || " $_sub " == " run" ]]; then
181+ if [[ -f " $WS_REAL /package.json" ]]; then
182+ _sub=$( python3 - " $WS_REAL /package.json" << 'PYEOF ' 2>/dev/null || true
183+ import json, re, sys
184+ try:
185+ name = json.load(open(sys.argv[1])).get("name", "") or ""
186+ name = re.sub(r"[^a-z0-9-]+", "-", name.lower())
187+ name = re.sub(r"-+", "-", name).strip("-")
188+ print(name)
189+ except Exception:
190+ print("")
191+ PYEOF
192+ )
193+ fi
194+ fi
195+ if [[ -n " $_sub " ]]; then
196+ PORTLESS_URL=" https://${_sub} .localhost:1355"
197+ PORTLESS_NOTE=" (route lost)"
198+ break
199+ fi
200+ done
201+ fi
202+
203+ if [[ -n " $PORTLESS_URL " ]]; then
204+ PORTLESS_LABEL=$( printf ' \033[32mPortless\033[0m' )
205+ PORTLESS_PART=$( link " $PORTLESS_URL " " $PORTLESS_LABEL " )
206+ else
207+ PORTLESS_PART=$( printf ' \033[90mPortless\033[0m' )
208+ fi
209+ fi
210+
120211# Fallback output when there is no Jira/Teams context:
121212# line 1 = worktree info + VS Code link (if available), line 2 = model/tokens.
122213emit_fallback () {
@@ -128,6 +219,10 @@ emit_fallback() {
128219 [[ -n " $line1 " ]] && line1+=" "
129220 line1+=" $VSCODE_PART "
130221 fi
222+ if [[ -n " ${PORTLESS_PART:- } " ]]; then
223+ [[ -n " $line1 " ]] && line1+=" "
224+ line1+=" $PORTLESS_PART "
225+ fi
131226 if [[ -n " $line1 " ]]; then
132227 printf ' %b\n%b' " $line1 " " $MODEL_PART "
133228 else
@@ -253,6 +348,11 @@ if [[ -n "$VSCODE_PART" ]]; then
253348 LINE1+=" "
254349fi
255350
351+ if [[ -n " ${PORTLESS_PART:- } " ]]; then
352+ LINE1+=" $PORTLESS_PART "
353+ LINE1+=" "
354+ fi
355+
256356if [[ -n " $TEAMS_URL " ]]; then
257357 LINE1+=$( link " $TEAMS_URL " " Teams" )
258358 LINE1+=" "
0 commit comments