From b9c9339ae48bcb7a2693dd762f51b4a33c7c6b80 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 24 Nov 2025 11:45:20 +0100 Subject: [PATCH] Add guided funnel fallback: prompt to install Go/tailscaled when funnel disabled --- src/index.ts | 53 ++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 49 insertions(+), 4 deletions(-) diff --git a/src/index.ts b/src/index.ts index 2efc99270..395a00d20 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,6 +12,8 @@ import fs from 'node:fs'; import path from 'node:path'; import os from 'node:os'; import JSON5 from 'json5'; +import readline from 'node:readline/promises'; +import { stdin as input, stdout as output } from 'node:process'; dotenv.config(); @@ -82,6 +84,15 @@ async function ensureBinary(name: string): Promise { }); } +async function promptYesNo(question: string, defaultYes = false): Promise { + const rl = readline.createInterface({ input, output }); + const suffix = defaultYes ? ' [Y/n] ' : ' [y/N] '; + const answer = (await rl.question(`${question}${suffix}`)).trim().toLowerCase(); + rl.close(); + if (!answer) return defaultYes; + return answer.startsWith('y'); +} + function withWhatsAppPrefix(number: string): string { // Ensure number has whatsapp: prefix expected by Twilio. return number.startsWith('whatsapp:') ? number : `whatsapp:${number}`; @@ -301,16 +312,50 @@ async function getTailnetHostname() { throw new Error('Could not determine Tailscale DNS or IP'); } +async function ensureGoInstalled() { + // Ensure Go toolchain is present; offer Homebrew install if missing. + const hasGo = await runExec('go', ['version']).then( + () => true, + () => false + ); + if (hasGo) return; + const install = await promptYesNo('Go is not installed. Install via Homebrew (brew install go)?', true); + if (!install) { + console.error('Go is required to build tailscaled from source. Aborting.'); + process.exit(1); + } + await runExec('brew', ['install', 'go']); +} + +async function ensureTailscaledInstalled() { + // Ensure tailscaled binary exists; install via Homebrew tailscale if missing. + const hasTailscaled = await runExec('tailscaled', ['--version']).then( + () => true, + () => false + ); + if (hasTailscaled) return; + + const install = await promptYesNo('tailscaled not found. Install via Homebrew (tailscale package)?', true); + if (!install) { + console.error('tailscaled is required for user-space funnel. Aborting.'); + process.exit(1); + } + await runExec('brew', ['install', 'tailscale']); +} + async function ensureFunnel(port: number) { // Ensure Funnel is enabled and publish the webhook port. try { const statusOut = (await runExec('tailscale', ['funnel', 'status', '--json'])).stdout.trim(); const parsed = statusOut ? (JSON.parse(statusOut) as Record) : {}; if (!parsed || Object.keys(parsed).length === 0) { - console.error( - 'Tailscale Funnel is not enabled on this tailnet/device. Enable it in the Tailscale admin console, then re-run warelay setup.' - ); - process.exit(1); + console.error('Tailscale Funnel is not enabled on this tailnet/device.'); + console.error('Enable in admin console: https://login.tailscale.com/admin (see https://tailscale.com/kb/1223/funnel)'); + console.error('macOS user-space tailscaled docs: https://github.com/tailscale/tailscale/wiki/Tailscaled-on-macOS'); + const proceed = await promptYesNo('Attempt local setup with user-space tailscaled?', false); + if (!proceed) process.exit(1); + await ensureGoInstalled(); + await ensureTailscaledInstalled(); } const { stdout } = await runExec('tailscale', ['funnel', '--yes', '--bg', `${port}`], 200_000);