diff --git a/docs/agent-playbooks/known-surprises.md b/docs/agent-playbooks/known-surprises.md index bcc0bb42..c70bea4f 100644 --- a/docs/agent-playbooks/known-surprises.md +++ b/docs/agent-playbooks/known-surprises.md @@ -75,5 +75,5 @@ If uncertain, ask the developer before adding an entry. - **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. +- **Mitigation:** Keep Portless startup behind `scripts/start-dev.js`, which now uses a branch-scoped `*.5chan.localhost:1355` route outside the canonical case and automatically increments a `-2`, `-3`, ... suffix when that branch-scoped route is already occupied. - **Status:** confirmed diff --git a/scripts/start-dev.js b/scripts/start-dev.js index 0ca4679b..27f2e32b 100644 --- a/scripts/start-dev.js +++ b/scripts/start-dev.js @@ -36,7 +36,7 @@ function getCurrentBranch() { return branch || null; } -function isCanonicalRouteBusy() { +function getActivePortlessRoutes() { const result = spawnSync(portlessBin, ['list'], { cwd: process.cwd(), encoding: 'utf8', @@ -44,13 +44,19 @@ function isCanonicalRouteBusy() { }); if (result.status !== 0) { - return false; + return new Set(); } - return result.stdout.includes('http://5chan.localhost:1355'); + const matches = result.stdout.match(/http:\/\/[a-z0-9.-]+\.localhost:1355/g) || []; + + return new Set(matches); } -function getPortlessAppName() { +function isRouteBusy(activeRoutes, appName) { + return activeRoutes.has(`http://${appName}.localhost:1355`); +} + +function getPreferredPortlessAppName(activeRoutes) { const branch = getCurrentBranch(); const branchLabel = sanitizeLabel(branch || 'current'); @@ -58,13 +64,32 @@ function getPortlessAppName() { return `${branchLabel}.5chan`; } - if (isCanonicalRouteBusy()) { + if (isRouteBusy(activeRoutes, '5chan')) { return `${branchLabel}.5chan`; } return '5chan'; } +function getPortlessAppName() { + const activeRoutes = getActivePortlessRoutes(); + const preferredAppName = getPreferredPortlessAppName(activeRoutes); + + if (!isRouteBusy(activeRoutes, preferredAppName)) { + return preferredAppName; + } + + for (let suffix = 2; suffix < 1000; suffix += 1) { + const candidate = `${preferredAppName}-${suffix}`; + + if (!isRouteBusy(activeRoutes, candidate)) { + return candidate; + } + } + + return `${preferredAppName}-${Date.now()}`; +} + const command = usePortless && existsSync(portlessBin) ? portlessBin : viteBin; let args; let publicUrl = null;