From 00caebbcb91d74a7ef574486e277cc99df43ce83 Mon Sep 17 00:00:00 2001 From: Lenucksi Date: Wed, 13 May 2026 19:53:04 +0200 Subject: [PATCH 1/4] BACK-473 - handle port congestion for backlog browser Add isPortAvailable() and findNextAvailablePort() to server/index.ts, pre-check port in CLI browser command, prompt user to start on next free port if taken. Simplify EADDRINUSE catch (UX now in CLI). --- src/cli.ts | 24 ++++++++++++++++++-- src/server/index.ts | 27 ++++++++++++++-------- src/test/server-port.test.ts | 44 ++++++++++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+), 12 deletions(-) create mode 100644 src/test/server-port.test.ts diff --git a/src/cli.ts b/src/cli.ts index 9ac4edd60..24e135135 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -3841,7 +3841,7 @@ program .action(async (options) => { try { const cwd = await requireProjectRoot(); - const { BacklogServer } = await import("./server/index.ts"); + const { BacklogServer, findNextAvailablePort, isPortAvailable } = await import("./server/index.ts"); const server = new BacklogServer(cwd); // Load config to get default port @@ -3849,12 +3849,32 @@ program const config = await core.filesystem.loadConfig(); const defaultPort = config?.defaultPort ?? 6420; - const port = Number.parseInt(options.port || defaultPort.toString(), 10); + let port = Number.parseInt(options.port || defaultPort.toString(), 10); if (Number.isNaN(port) || port < 1 || port > 65535) { console.error("Invalid port number. Must be between 1 and 65535."); process.exit(1); } + // Pre-check port availability and offer interactive retry + if (!(await isPortAvailable(port))) { + const nextPort = await findNextAvailablePort(port + 1); + const rl = createInterface({ input, output: process.stdout }); + const answer = ( + await rl.question( + `\nāš ļø Port ${port} is already in use.\nšŸ’” Port ${nextPort} is available. Start on port ${nextPort}? [Y/n] `, + ) + ) + .trim() + .toLowerCase(); + rl.close(); + if (answer === "" || answer === "y") { + port = nextPort; + } else { + console.log("Aborted."); + process.exit(0); + } + } + await server.start(port, options.open !== false); // Graceful shutdown on common termination signals (register once) diff --git a/src/server/index.ts b/src/server/index.ts index bb13bb4f6..f41669f93 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -1,3 +1,4 @@ +import net from "node:net"; import { dirname, join } from "node:path"; import type { Server, ServerWebSocket } from "bun"; import { $ } from "bun"; @@ -187,6 +188,21 @@ export function markHtmlBundleNoStore(bundle: Bun.HTMLBundle): Bun.HTMLBundle { const spaIndexHtml = markHtmlBundleNoStore(indexHtml); +export async function isPortAvailable(port: number): Promise { + if (port < 1 || port > 65535) return false; + return new Promise((resolve) => { + const srv = net.createServer(); + srv.listen(port, "127.0.0.1", () => srv.close(() => resolve(true))); + srv.on("error", () => resolve(false)); + }); +} + +export async function findNextAvailablePort(startPort: number): Promise { + let port = startPort; + while (!(await isPortAvailable(port))) port++; + return port; +} + export class BacklogServer { private core: Core; private server: Server | null = null; @@ -465,16 +481,7 @@ export class BacklogServer { const errorCode = (error as { code?: string })?.code; const errorMessage = (error as Error)?.message; if (errorCode === "EADDRINUSE" || errorMessage?.includes("address already in use")) { - console.error(`\nāŒ Error: Port ${finalPort} is already in use.\n`); - console.log("šŸ’” Suggestions:"); - console.log(` 1. Try a different port: backlog browser --port ${finalPort + 1}`); - console.log(` 2. Find what's using port ${finalPort}:`); - if (process.platform === "darwin" || process.platform === "linux") { - console.log(` Run: lsof -i :${finalPort}`); - } else if (process.platform === "win32") { - console.log(` Run: netstat -ano | findstr :${finalPort}`); - } - console.log(" 3. Or kill the process using the port and try again\n"); + console.error(`\nāŒ Error: Port ${finalPort} is already in use. Use --port to specify a different port.\n`); process.exit(1); } diff --git a/src/test/server-port.test.ts b/src/test/server-port.test.ts new file mode 100644 index 000000000..300d4f6a9 --- /dev/null +++ b/src/test/server-port.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from "bun:test"; +import net from "node:net"; +import { findNextAvailablePort, isPortAvailable } from "../server/index.ts"; + +describe("isPortAvailable", () => { + it("returns true for a free port", async () => { + const result = await isPortAvailable(49999); + expect(result).toBe(true); + }); + + it("returns false when a server already occupies the port", async () => { + const srv = net.createServer(); + await new Promise((resolve) => srv.listen(50001, "127.0.0.1", () => resolve())); + try { + const result = await isPortAvailable(50001); + expect(result).toBe(false); + } finally { + await new Promise((resolve) => srv.close(() => resolve())); + } + }); + + it("returns false for port 0 (out-of-range for browser use)", async () => { + const result = await isPortAvailable(0); + expect(result).toBe(false); + }); +}); + +describe("findNextAvailablePort", () => { + it("returns startPort when it is free", async () => { + const port = await findNextAvailablePort(49990); + expect(port).toBe(49990); + }); + + it("skips occupied ports and returns first free one", async () => { + const srv = net.createServer(); + await new Promise((resolve) => srv.listen(49985, "127.0.0.1", () => resolve())); + try { + const port = await findNextAvailablePort(49985); + expect(port).toBeGreaterThan(49985); + } finally { + await new Promise((resolve) => srv.close(() => resolve())); + } + }); +}); From 32daf5c7b396f2457dc921b92a72f8710a1161ec Mon Sep 17 00:00:00 2001 From: Lenucksi Date: Wed, 13 May 2026 19:55:51 +0200 Subject: [PATCH 2/4] BACK-473 - include updated backlog ticket in PR --- ...dle-port-congestion-for-backlog-browser.md | 356 ++++++++++++++++++ 1 file changed, 356 insertions(+) create mode 100644 backlog/tasks/back-473 - handle-port-congestion-for-backlog-browser.md diff --git a/backlog/tasks/back-473 - handle-port-congestion-for-backlog-browser.md b/backlog/tasks/back-473 - handle-port-congestion-for-backlog-browser.md new file mode 100644 index 000000000..7a58ca2e2 --- /dev/null +++ b/backlog/tasks/back-473 - handle-port-congestion-for-backlog-browser.md @@ -0,0 +1,356 @@ +--- +id: BACK-473 +title: handle port congestion for backlog browser +status: Done +assignee: + - '@claude' +created_date: '2026-05-08 14:29' +updated_date: '2026-05-13 17:53' +labels: + - webui +dependencies: [] +ordinal: 166000 +--- + +## Description + + +If port 6420 is taken, ask user to try a different one. Ideally just increment port number (e.g. 6421), check if free and start if user accepts this. + +oh, and check if port is free before starting the backlog browser mode anyway. this seems to not happen correctly O_o + + +## Definition of Done + +- [x] #1 bunx tsc --noEmit passes when TypeScript touched +- [x] #2 bun run check . passes when formatting/linting touched +- [x] #3 bun test (or scoped test) passes + + +## Acceptance Criteria + +- [x] #1 Port is checked for availability before Bun.serve() is called (proactive check, not just catching EADDRINUSE) +- [x] #2 If port is taken, user is shown the next available port (port+1 or higher) and asked to confirm interactively +- [x] #3 If user accepts (Y/enter), server starts on the suggested port successfully +- [x] #4 If user declines (n/N), process exits cleanly with code 0 +- [x] #5 isPortAvailable() and findNextAvailablePort() are exported from src/server/index.ts and unit-tested (min 3 cases, ≄1 error/edge case) + + +## Implementation Plan + + +## Implementation Plan (pre-approved, executor can start immediately) + +### Architecture + +Keep `BacklogServer.start()` pure — accepts a port, tries to bind, throws on failure. +Put all pre-check and interactive retry UX in the CLI browser command action (`src/cli.ts`). +This keeps the server class testable without stdin mocking. + +### Env / Tooling Constraints (non-negotiable) + +- **All code reads/writes via Serena MCP** — `mcp__plugin_serena_serena__read_file`, `replace_content`, `replace_symbol_body`, `insert_after_symbol`. Never use Read/Edit/Write tools or grep via Bash for source code. +- **Backlog CLI**: `/home/jo/kit/claude-code-llm-kram/Backlog.md/dist/backlog` (absolute path only; `~/.bun/bin/backlog` is unreliable) +- **Bash only for**: git ops, `bun test`, backlog CLI +- **Tests**: always `bun test --only-failures 2>&1` — never bare `bun test` +- **TDD strictly**: write failing tests (RED) before any implementation; confirm RED before writing impl code +- **AC/DoD check-off**: check each item immediately after implementing+verifying it, not batch at end +- **`--final-summary` is mandatory** at task close — always with Heredoc + +--- + +### Step 0: Worktree + Backlog Setup + +```bash +# Verify upstream-master == origin/main (zero delta expected) +git fetch +git log upstream-master..origin/main --oneline +git log origin/main..upstream-master --oneline + +# Create worktree +git worktree add ./worktrees/back-473-port-congestion upstream-master +cd ./worktrees/back-473-port-congestion && bun i --frozen-lockfile +``` + +Activate Serena on the worktree **before editing any files**: +``` +mcp__plugin_serena_serena__activate_project({ project: "/home/jo/kit/claude-code-llm-kram/Backlog.md/worktrees/back-473-port-congestion" }) +``` + +Mark task In Progress: +```bash +BACKLOG=/home/jo/kit/claude-code-llm-kram/Backlog.md/dist/backlog +$BACKLOG task edit BACK-473 --status "In Progress" --assignee "@claude" +``` + +--- + +### Step 1: Write Failing Tests First (RED) + +Create `src/test/server-port.test.ts`: + +```typescript +import { afterEach, beforeEach, describe, expect, it } from "bun:test"; +import net from "net"; +import { findNextAvailablePort, isPortAvailable } from "../server/index.ts"; + +describe("isPortAvailable", () => { + it("returns true for a free port", async () => { + const result = await isPortAvailable(49999); + expect(result).toBe(true); + }); + + it("returns false when a server already occupies the port", async () => { + const srv = net.createServer(); + await new Promise((resolve) => srv.listen(50001, "127.0.0.1", () => resolve())); + try { + const result = await isPortAvailable(50001); + expect(result).toBe(false); + } finally { + await new Promise((resolve) => srv.close(() => resolve())); + } + }); + + it("returns false for port 0 (out-of-range for browser use)", async () => { + const result = await isPortAvailable(0); + expect(result).toBe(false); + }); +}); + +describe("findNextAvailablePort", () => { + it("returns startPort when it is free", async () => { + const port = await findNextAvailablePort(49990); + expect(port).toBe(49990); + }); + + it("skips occupied ports and returns first free one", async () => { + const srv = net.createServer(); + await new Promise((resolve) => srv.listen(49985, "127.0.0.1", () => resolve())); + try { + const port = await findNextAvailablePort(49985); + expect(port).toBeGreaterThan(49985); + } finally { + await new Promise((resolve) => srv.close(() => resolve())); + } + }); +}); +``` + +Confirm RED: +```bash +bun test src/test/server-port.test.ts --only-failures 2>&1 +``` +(Should fail with "isPortAvailable is not exported" or similar) + +--- + +### Step 2: Implement Helpers in `src/server/index.ts` (GREEN) + +Add after the existing imports at the top of `src/server/index.ts`: + +```typescript +import net from "net"; + +export async function isPortAvailable(port: number): Promise { + if (port < 1 || port > 65535) return false; + return new Promise((resolve) => { + const srv = net.createServer(); + srv.listen(port, "127.0.0.1", () => srv.close(() => resolve(true))); + srv.on("error", () => resolve(false)); + }); +} + +export async function findNextAvailablePort(startPort: number): Promise { + let port = startPort; + while (!(await isPortAvailable(port))) port++; + return port; +} +``` + +Confirm GREEN: +```bash +bun test src/test/server-port.test.ts --only-failures 2>&1 +``` + +Check off AC #5: +```bash +$BACKLOG task edit BACK-473 --check-ac 5 +``` + +--- + +### Step 3: Update CLI Browser Command (`src/cli.ts`) + +Current browser command action is at approximately line 3857 in `src/cli.ts`. Read the surrounding context with Serena first to get exact line numbers. + +The current code has: +```typescript +const port = Number.parseInt(options.port || defaultPort.toString(), 10); +if (Number.isNaN(port) || port < 1 || port > 65535) { ... } +await server.start(port, options.open !== false); +``` + +Changes needed: +1. Change `const port` → `let port` (needs reassignment on retry) +2. Add import for `isPortAvailable` and `findNextAvailablePort` from `./server/index.ts` (check if already imported) +3. Add import for `readline` from `"readline"` (Node built-in, available in Bun) +4. Insert port pre-check block **after** the port validation check and **before** `await server.start(...)`: + +```typescript +// Pre-check port availability and offer interactive retry +if (!(await isPortAvailable(port))) { + const nextPort = await findNextAvailablePort(port + 1); + const answer = await new Promise((resolve) => { + const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); + rl.question( + `\nāš ļø Port ${port} is already in use.\nšŸ’” Port ${nextPort} is available. Start on port ${nextPort}? [Y/n] `, + (ans) => { + rl.close(); + resolve(ans.trim().toLowerCase()); + } + ); + }); + if (answer === "" || answer === "y") { + port = nextPort; + } else { + console.log("Aborted."); + process.exit(0); + } +} +``` + +Check off AC #1–4 as each behavior is wired up: +```bash +$BACKLOG task edit BACK-473 --check-ac 1 +$BACKLOG task edit BACK-473 --check-ac 2 +$BACKLOG task edit BACK-473 --check-ac 3 +$BACKLOG task edit BACK-473 --check-ac 4 +``` + +--- + +### Step 4: Simplify EADDRINUSE Catch in `src/server/index.ts` + +The catch block in `BacklogServer.start()` (around line 463–481 in the original file) currently: +- Detects EADDRINUSE +- Prints `port+1` suggestion +- Exits with code 1 + +Since the CLI now handles the UX before `start()` is called, simplify to: +```typescript +if (errorCode === "EADDRINUSE" || errorMessage?.includes("address already in use")) { + console.error(`\nāŒ Error: Port ${finalPort} is already in use. Use --port to specify a different port.\n`); + process.exit(1); +} +``` +(Remove the suggestions block — CLI handles that now.) + +--- + +### Step 5: Verify Everything + +```bash +# Type check (DoD #1) +bunx tsc --noEmit +$BACKLOG task edit BACK-473 --check-dod 1 + +# Lint/format (DoD #2) +bun run check . +$BACKLOG task edit BACK-473 --check-dod 2 + +# Full test suite (DoD #3) +bun test --only-failures 2>&1 +$BACKLOG task edit BACK-473 --check-dod 3 +``` + +--- + +### Step 6: Commit + PR + +```bash +cd ./worktrees/back-473-port-congestion +git checkout -b tasks/back-473-port-congestion +git add src/server/index.ts src/cli.ts src/test/server-port.test.ts +git commit -m "$(cat <<'EOF' +BACK-473 - handle port congestion for backlog browser + +Add isPortAvailable() and findNextAvailablePort() to server/index.ts, +pre-check port in CLI browser command, prompt user to start on next +free port if taken. Simplify EADDRINUSE catch (UX now in CLI). + +Co-Authored-By: Claude Sonnet 4.6 +EOF +)" +git push origin tasks/back-473-port-congestion +gh pr create --title "BACK-473 - handle port congestion for backlog browser" \ + --body "$(cat <<'EOF' +## Summary +- Add `isPortAvailable()` and `findNextAvailablePort()` helpers to `src/server/index.ts` +- Pre-check port availability in CLI browser command before calling `server.start()` +- If port is taken: suggest next free port, prompt user interactively (readline), start there if accepted +- Simplify EADDRINUSE catch block in `BacklogServer.start()` (UX handled upstream in CLI) +- Unit tests for port helpers (5 cases, including 2 error/edge cases) + +## Test plan +- [ ] `bun test src/test/server-port.test.ts --only-failures` passes +- [ ] `bun test --only-failures` — no new failures +- [ ] `bunx tsc --noEmit` passes +- [ ] `bun run check .` passes + +šŸ¤– Generated with [Claude Code](https://claude.com/claude-code) +EOF +)" +``` + +--- + +### Step 7: Finalize Task + +```bash +$BACKLOG task edit BACK-473 \ + --notes "$(cat <<'EOF' +Files changed: +- src/server/index.ts: added isPortAvailable(), findNextAvailablePort() exports; simplified EADDRINUSE catch block +- src/cli.ts: added pre-check + readline interactive retry loop in browser command; changed const port → let port +- src/test/server-port.test.ts: NEW - 5 unit tests for port helpers (3 for isPortAvailable, 2 for findNextAvailablePort) + +net module available in Bun via Node compat layer — no extra deps needed. +EOF +)" \ + --final-summary "$(cat <<'EOF' +Implemented proactive port availability check and interactive retry UX for backlog browser. +Server stays pure (throws on error); CLI handles user interaction via readline. +Commit: (fill in shorthash from git log --oneline -1) +EOF +)" \ + --status Done +``` + +--- + +### Files Summary + +| File | Change | +|------|--------| +| `src/server/index.ts` | Add `isPortAvailable()` + `findNextAvailablePort()` exports; simplify catch | +| `src/cli.ts` | Pre-check + readline prompt in browser command; `const` → `let` for port | +| `src/test/server-port.test.ts` | NEW: 5 unit tests for port helpers | + + +## Implementation Notes + + +Files changed: +- src/server/index.ts: added isPortAvailable(), findNextAvailablePort() exports; simplified EADDRINUSE catch block +- src/cli.ts: added pre-check + readline interactive retry loop in browser command; changed const port → let port +- src/test/server-port.test.ts: NEW - 5 unit tests for port helpers (3 for isPortAvailable, 2 for findNextAvailablePort) + + +## Final Summary + + +Implemented proactive port availability check and interactive retry UX for backlog browser. +Server stays pure (throws on error); CLI handles user interaction via readline. +Commit: 00caebb - BACK-473 - handle port congestion for backlog browser +PR: https://github.com/MrLesk/Backlog.md/pull/651 + From 0a59593b52e64d274dbc1755532b14d1a53b5b4c Mon Sep 17 00:00:00 2001 From: Lenucksi Date: Wed, 13 May 2026 20:46:53 +0200 Subject: [PATCH 3/4] BACK-473 - add --non-interactive flag for auto port selection --- src/cli.ts | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 24e135135..04b622c59 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -3838,6 +3838,7 @@ program .description("open browser interface for task management (press Ctrl+C or Cmd+C to stop)") .option("-p, --port ", "port to run server on") .option("--no-open", "don't automatically open browser") + .option("--non-interactive", "automatically use next free port without asking") .action(async (options) => { try { const cwd = await requireProjectRoot(); @@ -3858,20 +3859,25 @@ program // Pre-check port availability and offer interactive retry if (!(await isPortAvailable(port))) { const nextPort = await findNextAvailablePort(port + 1); - const rl = createInterface({ input, output: process.stdout }); - const answer = ( - await rl.question( - `\nāš ļø Port ${port} is already in use.\nšŸ’” Port ${nextPort} is available. Start on port ${nextPort}? [Y/n] `, - ) - ) - .trim() - .toLowerCase(); - rl.close(); - if (answer === "" || answer === "y") { + if (options.nonInteractive) { + console.log(`āš ļø Port ${port} is already in use. Using port ${nextPort} instead.`); port = nextPort; } else { - console.log("Aborted."); - process.exit(0); + const rl = createInterface({ input, output: process.stdout }); + const answer = ( + await rl.question( + `\nāš ļø Port ${port} is already in use.\nšŸ’” Port ${nextPort} is available. Start on port ${nextPort}? [Y/n] `, + ) + ) + .trim() + .toLowerCase(); + rl.close(); + if (answer === "" || answer === "y") { + port = nextPort; + } else { + console.log("Aborted."); + process.exit(0); + } } } From ae997887131d4d99de0ed7eb991c74dc47cc787c Mon Sep 17 00:00:00 2001 From: Lenucksi Date: Wed, 13 May 2026 20:52:45 +0200 Subject: [PATCH 4/4] BACK-473 - add acceptance criterion for --non-interactive flag --- .../back-473 - handle-port-congestion-for-backlog-browser.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/backlog/tasks/back-473 - handle-port-congestion-for-backlog-browser.md b/backlog/tasks/back-473 - handle-port-congestion-for-backlog-browser.md index 7a58ca2e2..2fb4d6027 100644 --- a/backlog/tasks/back-473 - handle-port-congestion-for-backlog-browser.md +++ b/backlog/tasks/back-473 - handle-port-congestion-for-backlog-browser.md @@ -5,7 +5,7 @@ status: Done assignee: - '@claude' created_date: '2026-05-08 14:29' -updated_date: '2026-05-13 17:53' +updated_date: '2026-05-13 18:52' labels: - webui dependencies: [] @@ -34,8 +34,11 @@ oh, and check if port is free before starting the backlog browser mode anyway. t - [x] #3 If user accepts (Y/enter), server starts on the suggested port successfully - [x] #4 If user declines (n/N), process exits cleanly with code 0 - [x] #5 isPortAvailable() and findNextAvailablePort() are exported from src/server/index.ts and unit-tested (min 3 cases, ≄1 error/edge case) +- [ ] #6 --non-interactive flag skips prompt and auto-selects next free port + + ## Implementation Plan