From 8078d10441c37d811096afa7f7fb6a18f6453596 Mon Sep 17 00:00:00 2001 From: Tommaso Casaburi Date: Mon, 30 Mar 2026 17:32:01 +0700 Subject: [PATCH] fix(portless): avoid route collisions across worktrees --- AGENTS.md | 2 +- README.md | 2 +- docs/agent-playbooks/known-surprises.md | 10 ++++ scripts/AGENTS.md | 2 +- scripts/start-dev.js | 64 ++++++++++++++++++++++++- 5 files changed, 75 insertions(+), 5 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 4a0d8966..06a06cd1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -174,7 +174,7 @@ src/ ## Local Development URLs -This project uses [Portless](https://github.com/vercel-labs/portless) for local dev. The dev server is available at http://5chan.localhost:1355 instead of a random port. Other Bitsocial projects use the same proxy (seedit, mintpass, bitsocial at `.localhost:1355`), so they can all run simultaneously without port conflicts. +This project uses [Portless](https://github.com/vercel-labs/portless) for local dev. The canonical dev URL is http://5chan.localhost:1355, and non-`master` branches can automatically fall back to a branch-scoped `*.5chan.localhost:1355` route when needed so parallel worktrees do not collide. Other Bitsocial projects use the same proxy (seedit, mintpass, bitsocial at `.localhost:1355`), so they can all run simultaneously without port conflicts. To bypass Portless: `PORTLESS=0 yarn start` diff --git a/README.md b/README.md index e9a7e5d7..f8d3b27c 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,7 @@ To have your board appear in a directory on the 5chan homepage: 2. Install dependencies: `yarn install` 3. Start the web client: `yarn start` -The dev server runs at http://5chan.localhost:1355 via [Portless](https://port1355.dev/), which gives each Bitsocial project a stable, named URL instead of a random port. To bypass Portless and use a plain Vite dev server, run `PORTLESS=0 yarn start`; it will start at `http://5chan.localhost:1355` and automatically fall forward to the next free port if `1355` is already in use. +The dev server normally runs at http://5chan.localhost:1355 via [Portless](https://port1355.dev/), which gives each Bitsocial project a stable, named URL instead of a random port. On non-`master` branches, or when another legacy process is already holding the canonical route, `yarn start` will automatically use a branch-scoped `*.5chan.localhost:1355` URL instead of failing. To bypass Portless and use a plain Vite dev server, run `PORTLESS=0 yarn start`; it will start at `http://5chan.localhost:1355` and automatically fall forward to the next free port if `1355` is already in use. For device testing on a USB-connected Android phone (without relying on `5chan.localhost` DNS from the device): diff --git a/docs/agent-playbooks/known-surprises.md b/docs/agent-playbooks/known-surprises.md index df16e191..bcc0bb42 100644 --- a/docs/agent-playbooks/known-surprises.md +++ b/docs/agent-playbooks/known-surprises.md @@ -67,3 +67,13 @@ If uncertain, ask the developer before adding an entry. - **Impact:** Contributors could lose the fallback dev path or interrupt their startup flow when `1355` was already busy. - **Mitigation:** Keep the fallback behind `scripts/start-dev.js`, which now probes from `1355` upward and starts Vite on the next free port instead of exiting. - **Status:** confirmed + +### Fixed Portless app names collide across 5chan worktrees + +- **Date:** 2026-03-30 +- **Observed by:** Codex +- **Context:** Starting `yarn start` in one 5chan worktree while another 5chan worktree was already serving through Portless +- **What was surprising:** Using the literal Portless app name `5chan` in every worktree makes the route itself collide, even when the backing ports are different, so the second process fails with `"5chan.localhost" is already registered`. +- **Impact:** Parallel 5chan branches can block each other even though Portless is meant to let them coexist safely. +- **Mitigation:** Keep Portless startup behind `scripts/start-dev.js`, which now uses a branch-scoped `*.5chan.localhost:1355` route outside the canonical case and falls back to a branch-scoped route when the bare `5chan.localhost` name is already occupied. +- **Status:** confirmed diff --git a/scripts/AGENTS.md b/scripts/AGENTS.md index b929d452..adc406cb 100644 --- a/scripts/AGENTS.md +++ b/scripts/AGENTS.md @@ -4,7 +4,7 @@ These rules apply to `scripts/**`. Follow the repo-root `AGENTS.md` first, then - Keep scripts non-interactive and idempotent. Print the command, URL, branch, or path being acted on so failures are diagnosable. - Use repo-relative paths and environment variables instead of user-specific absolute paths. -- For dev-server helpers, default to `http://5chan.localhost:1355` and respect the existing `PORTLESS=0` fallback instead of hard-coding alternate ports. For USB Android preview, `scripts/start-android-usb.mjs` mirrors bitsocial-web: `adb reverse` plus Vite on `127.0.0.1`, then `am start` VIEW to open the default browser when the port is listening (disable with `ANDROID_USB_OPEN_BROWSER=0`). +- For dev-server helpers, default to `http://5chan.localhost:1355`, but allow a branch-scoped `*.5chan.localhost:1355` route when the launcher is avoiding a Portless name collision. Respect the existing `PORTLESS=0` fallback instead of hard-coding alternate ports. For USB Android preview, `scripts/start-android-usb.mjs` mirrors bitsocial-web: `adb reverse` plus Vite on `127.0.0.1`, then `am start` VIEW to open the default browser when the port is listening (disable with `ANDROID_USB_OPEN_BROWSER=0`). - Keep shell helpers thin. When logic becomes stateful or cross-platform, prefer a Node script. - Git and worktree helpers must validate input and default to safe operations. - If a helper deletes local branches automatically, document the exact eligibility checks and keep the behavior conservative. diff --git a/scripts/start-dev.js b/scripts/start-dev.js index 172aa56d..0ca4679b 100644 --- a/scripts/start-dev.js +++ b/scripts/start-dev.js @@ -1,6 +1,6 @@ import { existsSync } from 'node:fs'; import { join } from 'node:path'; -import { spawn } from 'node:child_process'; +import { spawn, spawnSync } from 'node:child_process'; import { resolvePort } from './dev-server-utils.mjs'; const isWindows = process.platform === 'win32'; @@ -13,11 +13,71 @@ const fallbackHost = '127.0.0.1'; const fallbackUrlHost = '5chan.localhost'; const fallbackRequestedPort = 1355; +function sanitizeLabel(value) { + return value + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, '') + .replace(/-{2,}/g, '-'); +} + +function getCurrentBranch() { + const result = spawnSync('git', ['branch', '--show-current'], { + cwd: process.cwd(), + encoding: 'utf8', + }); + + if (result.status !== 0) { + return null; + } + + const branch = result.stdout.trim(); + + return branch || null; +} + +function isCanonicalRouteBusy() { + const result = spawnSync(portlessBin, ['list'], { + cwd: process.cwd(), + encoding: 'utf8', + env: process.env, + }); + + if (result.status !== 0) { + return false; + } + + return result.stdout.includes('http://5chan.localhost:1355'); +} + +function getPortlessAppName() { + const branch = getCurrentBranch(); + const branchLabel = sanitizeLabel(branch || 'current'); + + if (branch && branch !== 'master' && branch !== 'main') { + return `${branchLabel}.5chan`; + } + + if (isCanonicalRouteBusy()) { + return `${branchLabel}.5chan`; + } + + return '5chan'; +} + const command = usePortless && existsSync(portlessBin) ? portlessBin : viteBin; let args; +let publicUrl = null; if (command === portlessBin) { - args = ['5chan', 'vite']; + const appName = getPortlessAppName(); + + publicUrl = `http://${appName}.localhost:1355`; + args = [appName, 'vite']; + + if (appName !== '5chan') { + console.log(`Starting Portless dev server at ${publicUrl}`); + } } else { const port = await resolvePort(fallbackRequestedPort); const fallbackUrl = `http://${fallbackUrlHost}:${port}`;