From 822a5884ff9b4e1e1e8245641190dea258cc0130 Mon Sep 17 00:00:00 2001 From: evan-claw Date: Thu, 19 Mar 2026 21:26:33 +0000 Subject: [PATCH] fix(dev-cli): handle shared-port services in status command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Services that share a port (auto-fix/db-proxy on 8792, kiloclaw/git-token on 8795) were each shown as 'running' when only one was active, producing false positives. Group services by port before checking. For shared ports, show an ambiguous indicator (yellow) with '(shared — cannot distinguish)' instead of falsely marking both services as running. For unique ports, behavior is unchanged. Adds status.test.ts with 3 tests covering shared-port grouping logic. --- dev/cli/src/commands/status.ts | 42 ++++++++++++++++++++++--- dev/cli/test/status.test.ts | 57 ++++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 4 deletions(-) create mode 100644 dev/cli/test/status.test.ts diff --git a/dev/cli/src/commands/status.ts b/dev/cli/src/commands/status.ts index ec681e2a4e..3fcbad8d02 100644 --- a/dev/cli/src/commands/status.ts +++ b/dev/cli/src/commands/status.ts @@ -16,11 +16,45 @@ export async function status(root: string) { ); const portServices = services.filter(s => s.port && s.type !== 'infra'); + + // Group services by port to detect shared-port conflicts + const portGroups = new Map(); for (const svc of portServices) { - const listening = await isPortListening(svc.port!); - console.log( - ` ${listening ? ui.green('●') : ui.dim('○')} ${svc.name.padEnd(12)} ${listening ? `port ${svc.port}` : ui.dim('not running')}` - ); + const group = portGroups.get(svc.port!) ?? []; + group.push(svc); + portGroups.set(svc.port!, group); + } + + // Check each unique port once + const portStatus = new Map(); + for (const port of portGroups.keys()) { + portStatus.set(port, await isPortListening(port)); + } + + for (const [port, group] of portGroups) { + const listening = portStatus.get(port)!; + + if (group.length === 1) { + // Unique port — show definitive status + const svc = group[0]; + console.log( + ` ${listening ? ui.green('●') : ui.dim('○')} ${svc.name.padEnd(12)} ${listening ? `port ${port}` : ui.dim('not running')}` + ); + } else { + // Shared port — cannot determine which service is actually running + const names = group.map(s => s.name); + if (listening) { + console.log( + ` ${ui.yellow('●')} ${names.join(', ').padEnd(12)} port ${port} ${ui.yellow('(shared — cannot distinguish)')}` + ); + } else { + for (const svc of group) { + console.log( + ` ${ui.dim('○')} ${svc.name.padEnd(12)} ${ui.dim('not running')}` + ); + } + } + } } console.log(); diff --git a/dev/cli/test/status.test.ts b/dev/cli/test/status.test.ts new file mode 100644 index 0000000000..1e2d7a7edf --- /dev/null +++ b/dev/cli/test/status.test.ts @@ -0,0 +1,57 @@ +import { describe, expect, test } from 'bun:test'; +import { services } from '../src/services/registry'; + +describe('status: shared-port detection', () => { + test('identifies services that share a port', () => { + const portServices = services.filter(s => s.port && s.type !== 'infra'); + const portGroups = new Map(); + for (const svc of portServices) { + const group = portGroups.get(svc.port!) ?? []; + group.push(svc.name); + portGroups.set(svc.port!, group); + } + const sharedPorts = [...portGroups.entries()].filter(([, names]) => names.length > 1); + + // Verify the known shared ports are detected + expect(sharedPorts.length).toBeGreaterThanOrEqual(2); + + const port8792 = portGroups.get(8792); + expect(port8792).toBeDefined(); + expect(port8792).toContain('auto-fix'); + expect(port8792).toContain('db-proxy'); + + const port8795 = portGroups.get(8795); + expect(port8795).toBeDefined(); + expect(port8795).toContain('kiloclaw'); + expect(port8795).toContain('git-token'); + }); + + test('unique-port services are not grouped', () => { + const portServices = services.filter(s => s.port && s.type !== 'infra'); + const portGroups = new Map(); + for (const svc of portServices) { + const group = portGroups.get(svc.port!) ?? []; + group.push(svc.name); + portGroups.set(svc.port!, group); + } + + // Port 3000 belongs only to nextjs + const port3000 = portGroups.get(3000); + expect(port3000).toEqual(['nextjs']); + }); + + test('all non-infra port services are accounted for in port groups', () => { + const portServices = services.filter(s => s.port && s.type !== 'infra'); + const portGroups = new Map(); + for (const svc of portServices) { + const group = portGroups.get(svc.port!) ?? []; + group.push(svc.name); + portGroups.set(svc.port!, group); + } + + // Every port-having service should be in exactly one group + const allGroupedNames = [...portGroups.values()].flat(); + const serviceNames = portServices.map(s => s.name); + expect(allGroupedNames.sort()).toEqual(serviceNames.sort()); + }); +});