Summary
getLanIp() returns the first non-internal IPv4 address from os.networkInterfaces(). On a
machine running Tailscale that is the 100.64.0.0/10 CGNAT address; on one running WSL or Docker
Desktop it is often a 172.x virtual adapter. The address goes into the QR code, the phone cannot
reach it, and the phone reports "dev server offline".
That message points the developer at the server, where nothing is wrong, instead of at the address it
was handed. We deleted and reinstalled the miniapp twice before checking what the QR actually
encoded.
Cause
Identical code in two places — src/dev.ts and src/release.ts:
function getLanIp(): string | null {
const interfaces = os.networkInterfaces()
for (const name of Object.keys(interfaces)) {
for (const iface of interfaces[name] ?? []) {
if (iface.family === 'IPv4' && !iface.internal) {
return iface.address
}
}
}
return null
}
Enumeration order is not preference order. On our machine it yields Tailscale first:
PICKED Tailscale 100.96.198.122 <- what the QR encoded
Wi-Fi 192.168.1.199 <- what the phone could actually reach
vEthernet (WSL) 172.22.176.1
This matters more in release.ts than in dev.ts, since installing is the entire point of that
command.
Reproduction
- On a machine with Tailscale running (or WSL installed), start
bunx @mentra/miniapp-cli dev.
- Scan the printed QR with a phone on the same Wi-Fi but not on the tailnet.
- The miniapp installs but reports "dev server offline".
curl from the same LAN to
http://<wifi-ip>:3000/miniapp.json returns 200, so the server is healthy.
Suggested fix
Rank the candidates rather than trusting order:
const candidates: Array<{address: string; rank: number}> = []
for (const name of Object.keys(interfaces)) {
for (const iface of interfaces[name] ?? []) {
if (iface.family !== 'IPv4' || iface.internal) continue
const address = iface.address
const isCgnat = /^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\./.test(address)
const isVirtual = /vEthernet|WSL|Hyper-V|VirtualBox|VMware|Tailscale|ZeroTier/i.test(name)
if (isCgnat || isVirtual) candidates.push({address, rank: 3})
else if (/^192\.168\./.test(address)) candidates.push({address, rank: 0})
else if (/^10\./.test(address)) candidates.push({address, rank: 1})
else candidates.push({address, rank: 2})
}
}
candidates.sort((a, b) => a.rank - b.rank)
return candidates.length > 0 ? candidates[0].address : null
Still returns something when every interface is virtual — a wrong address the developer can see
beats a null that exits the CLI before printing anything.
Two smaller suggestions that would have saved us more time than the fix itself:
- Print the URL next to the QR. The QR is currently the only place the chosen address appears, so
a wrong pick is invisible without decoding the image.
- Consider a
--host flag for machines where the heuristic still guesses wrong.
Environment
@mentra/miniapp-cli 0.1.0-dev.0
- Host: Windows 11 with Tailscale and WSL both present
- Device: Mentra Live, MentraOS app
staging.20260808.*
Summary
getLanIp()returns the first non-internal IPv4 address fromos.networkInterfaces(). On amachine running Tailscale that is the
100.64.0.0/10CGNAT address; on one running WSL or DockerDesktop it is often a
172.xvirtual adapter. The address goes into the QR code, the phone cannotreach it, and the phone reports "dev server offline".
That message points the developer at the server, where nothing is wrong, instead of at the address it
was handed. We deleted and reinstalled the miniapp twice before checking what the QR actually
encoded.
Cause
Identical code in two places —
src/dev.tsandsrc/release.ts:Enumeration order is not preference order. On our machine it yields Tailscale first:
This matters more in
release.tsthan indev.ts, since installing is the entire point of thatcommand.
Reproduction
bunx @mentra/miniapp-cli dev.curlfrom the same LAN tohttp://<wifi-ip>:3000/miniapp.jsonreturns 200, so the server is healthy.Suggested fix
Rank the candidates rather than trusting order:
Still returns something when every interface is virtual — a wrong address the developer can see
beats a
nullthat exits the CLI before printing anything.Two smaller suggestions that would have saved us more time than the fix itself:
a wrong pick is invisible without decoding the image.
--hostflag for machines where the heuristic still guesses wrong.Environment
@mentra/miniapp-cli0.1.0-dev.0staging.20260808.*