From 8d14a2facd6bc3c0f85284064d250adea00ba606 Mon Sep 17 00:00:00 2001 From: Florian <45694132+flo-bit@users.noreply.github.com> Date: Mon, 2 Mar 2026 21:00:12 +0100 Subject: [PATCH] add tunnel command --- package.json | 2 +- src/lib/atproto/scripts/tunnel.ts | 194 ++++++++++++++++++++++++++++++ vite.config.ts | 3 +- 3 files changed, 197 insertions(+), 2 deletions(-) create mode 100644 src/lib/atproto/scripts/tunnel.ts diff --git a/package.json b/package.json index 34ed9da..b8bb856 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "env:generate-key": "npx tsx src/lib/atproto/scripts/generate-key.ts", "env:generate-secret": "npx tsx src/lib/atproto/scripts/generate-secret.ts", "env:setup-dev": "npx tsx src/lib/atproto/scripts/setup-dev.ts", - "tunnel": "cloudflared tunnel --url http://localhost:5183" + "tunnel": "npx tsx src/lib/atproto/scripts/tunnel.ts" }, "devDependencies": { "@atcute/atproto": "^3.1.10", diff --git a/src/lib/atproto/scripts/tunnel.ts b/src/lib/atproto/scripts/tunnel.ts new file mode 100644 index 0000000..16323a9 --- /dev/null +++ b/src/lib/atproto/scripts/tunnel.ts @@ -0,0 +1,194 @@ +import { readFileSync, writeFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { spawn } from 'node:child_process'; + +const cwd = process.cwd(); +const envPath = resolve(cwd, '.env'); +const vitePath = resolve(cwd, 'vite.config.ts'); + +let tunnelUrl: string | null = null; +let statusBarActive = false; + +// ── ANSI status bar ────────────────────────────────────────────── +// Reserves the bottom row of the terminal for a persistent status line. +// Logs scroll in the region above it. + +function getColumns(): number { + return process.stdout.columns || 80; +} + +function getRows(): number { + return process.stdout.rows || 24; +} + +function setupScrollRegion(): void { + if (!process.stdout.isTTY) return; + statusBarActive = true; + const rows = getRows(); + // Set scroll region to all rows except the last + process.stdout.write(`\x1b[1;${rows - 1}r`); + // Move cursor into scroll region + process.stdout.write(`\x1b[${rows - 1};1H`); +} + +function drawStatusBar(text: string): void { + if (!process.stdout.isTTY) { + process.stdout.write(text + '\n'); + return; + } + const rows = getRows(); + const cols = getColumns(); + // Save cursor, move to bottom row, clear it, write status, restore cursor + process.stdout.write('\x1b7'); + process.stdout.write(`\x1b[${rows};1H`); + process.stdout.write('\x1b[2K'); + // Inverse video for the bar + process.stdout.write(`\x1b[7m ${text.padEnd(cols - 1)}\x1b[0m`); + process.stdout.write('\x1b8'); +} + +function clearStatusBar(): void { + if (!process.stdout.isTTY || !statusBarActive) return; + const rows = getRows(); + // Reset scroll region to full terminal + process.stdout.write(`\x1b[1;${rows}r`); + // Clear the bottom row + process.stdout.write(`\x1b[${rows};1H\x1b[2K`); + // Move cursor up + process.stdout.write(`\x1b[${rows - 1};1H`); + statusBarActive = false; +} + +function writeLog(text: string): void { + if (statusBarActive && process.stdout.isTTY) { + // Write inside the scroll region, which auto-scrolls above the bar + process.stdout.write(text); + } else { + process.stdout.write(text); + } +} + +// Redraw on terminal resize +process.stdout.on('resize', () => { + if (!statusBarActive || !tunnelUrl) return; + setupScrollRegion(); + drawStatusBar(`Tunnel: ${tunnelUrl} | Ctrl+C to stop`); +}); + +// ── .env helpers ───────────────────────────────────────────────── + +function readEnv(): string { + return readFileSync(envPath, 'utf8'); +} + +function writeEnv(content: string): void { + writeFileSync(envPath, content); +} + +function setEnvVar(key: string, value: string): void { + let env = readEnv(); + const re = new RegExp(`^(#\\s*)?${key}=.*$`, 'm'); + const line = `${key}=${value}`; + + if (re.test(env)) { + env = env.replace(re, line); + } else { + env = env.trimEnd() + '\n' + line + '\n'; + } + writeEnv(env); +} + +function clearEnvVar(key: string): void { + let env = readEnv(); + const re = new RegExp(`^${key}=.*$`, 'm'); + + if (re.test(env)) { + env = env.replace(re, `# ${key}=`); + writeEnv(env); + } +} + +// ── vite config helpers ────────────────────────────────────────── + +function setViteAllowedHosts(hostname: string): void { + let vite = readFileSync(vitePath, 'utf8'); + + if (/allowedHosts\s*:/.test(vite)) { + vite = vite.replace(/allowedHosts\s*:\s*\[.*?\]/s, `allowedHosts: ['${hostname}']`); + } else if (/server\s*:\s*\{/.test(vite)) { + vite = vite.replace(/server\s*:\s*\{/, `server: {\n\t\tallowedHosts: ['${hostname}'],`); + } + + writeFileSync(vitePath, vite); +} + +function clearViteAllowedHosts(): void { + let vite = readFileSync(vitePath, 'utf8'); + + if (/allowedHosts\s*:/.test(vite)) { + vite = vite.replace(/allowedHosts\s*:\s*\[.*?\]/s, 'allowedHosts: []'); + } + + writeFileSync(vitePath, vite); +} + +// ── cleanup ────────────────────────────────────────────────────── + +function cleanup(): void { + clearStatusBar(); + console.log('\nCleaning up...'); + if (tunnelUrl) { + clearEnvVar('OAUTH_PUBLIC_URL'); + console.log(' Cleared OAUTH_PUBLIC_URL from .env'); + clearViteAllowedHosts(); + console.log(' Cleared allowedHosts from vite.config.ts'); + } +} + +// ── main ───────────────────────────────────────────────────────── + +const child = spawn('cloudflared', ['tunnel', '--url', 'http://localhost:5183'], { + stdio: ['ignore', 'pipe', 'pipe'] +}); + +child.stderr.on('data', (data: Buffer) => { + const output = data.toString(); + + if (!tunnelUrl) { + const match = output.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/); + if (match) { + tunnelUrl = match[0]; + const hostname = new URL(tunnelUrl).hostname; + + setEnvVar('OAUTH_PUBLIC_URL', tunnelUrl); + setViteAllowedHosts(hostname); + + writeLog(`\n Set OAUTH_PUBLIC_URL=${tunnelUrl}\n`); + writeLog(` Set vite allowedHosts to [${hostname}]\n`); + writeLog(` Tunnel is ready! Restart your dev server to pick up the new URL.\n\n`); + + setupScrollRegion(); + drawStatusBar(`Tunnel: ${tunnelUrl} | Ctrl+C to stop`); + return; + } + } + + writeLog(output); +}); + +child.stdout.on('data', (data: Buffer) => { + writeLog(data.toString()); +}); + +child.on('close', (code) => { + cleanup(); + process.exit(code ?? 0); +}); + +process.on('SIGINT', () => { + child.kill('SIGINT'); +}); + +process.on('SIGTERM', () => { + child.kill('SIGTERM'); +}); diff --git a/vite.config.ts b/vite.config.ts index 63e78a3..14c8378 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -6,6 +6,7 @@ export default defineConfig({ plugins: [sveltekit(), tailwindcss()], server: { host: '127.0.0.1', - port: 5183 + port: 5183, + allowedHosts: [] } }); -- 2.51.2