From 847e901e39588caa8ab4ca9a3a7a1a03735345c1 Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Sun, 14 Jun 2026 08:59:42 +0000 Subject: [PATCH] fix(ci-setup): probe the resolved --port for availability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit detectEnvironment ran checkPortAvailable(4747) eagerly, before options were parsed, and the result was printed as a hardcoded "Port 4747: available" line — wrong/irrelevant whenever the user passed a different --port. Export checkPortAvailable, drop the eager hardcoded probe (and the now-unused portAvailable field from DetectResult), and probe resolved.port after option resolution, reporting the actual port. This also removes a latent real-socket bind on 4747 from the default unit-test path. Co-Authored-By: Claude Opus 4.8 (1M context) --- gitnexus/src/cli/ci-setup.ts | 13 +++++++---- gitnexus/src/cli/ci-setup/detect.ts | 13 +++++++---- gitnexus/src/cli/ci-setup/types.ts | 1 - gitnexus/test/unit/ci-setup-templates.test.ts | 1 - gitnexus/test/unit/ci-setup.test.ts | 23 ++++++++++++++++++- 5 files changed, 39 insertions(+), 12 deletions(-) diff --git a/gitnexus/src/cli/ci-setup.ts b/gitnexus/src/cli/ci-setup.ts index ba9ed6bd0..fb87e9689 100644 --- a/gitnexus/src/cli/ci-setup.ts +++ b/gitnexus/src/cli/ci-setup.ts @@ -12,7 +12,7 @@ import fs from 'fs/promises'; import path from 'path'; -import { detectEnvironment } from './ci-setup/detect.js'; +import { detectEnvironment, checkPortAvailable } from './ci-setup/detect.js'; import { resolveOptions } from './ci-setup/prompts.js'; import { generateFiles } from './ci-setup/templates.js'; import type { CiSetupOptions, CiSetupResult, GeneratedFile } from './ci-setup/types.js'; @@ -68,9 +68,6 @@ export const ciSetupCommand = async (options?: { if (detect.hasDocker) { console.log(' ✓ Docker: docker-compose or Dockerfile found'); } - console.log( - ` ${detect.portAvailable ? '✓' : '⚠'} Port 4747: ${detect.portAvailable ? 'available' : 'in use (serve may already be running)'}`, - ); console.log(' ⚠ License: PolyForm-Noncommercial — confirm non-commercial use\n'); // Parse and validate options from commander flags. An explicitly-passed but @@ -105,6 +102,14 @@ export const ciSetupCommand = async (options?: { } const resolved = await resolveOptions(detect, partial); + + // Probe the *resolved* port (now that --port / prompts are settled) rather + // than a hardcoded default at detection time. + const portAvailable = await checkPortAvailable(resolved.port); + console.log( + ` ${portAvailable ? '✓' : '⚠'} Port ${resolved.port}: ${portAvailable ? 'available' : 'in use (serve may already be running)'}`, + ); + const files = generateFiles(resolved, detect); const result: CiSetupResult = { generated: [], skipped: [], errors: [] }; diff --git a/gitnexus/src/cli/ci-setup/detect.ts b/gitnexus/src/cli/ci-setup/detect.ts index 2bd4fd293..febc82ef7 100644 --- a/gitnexus/src/cli/ci-setup/detect.ts +++ b/gitnexus/src/cli/ci-setup/detect.ts @@ -105,7 +105,12 @@ async function detectPrimaryLanguage(repoRoot: string): Promise { return top; } -function checkPortAvailable(port: number): Promise { +/** + * Whether `port` can be bound on the loopback interface. Exported so the caller + * can probe the *resolved* --port (known only after option parsing) rather than + * a hardcoded default at detection time. + */ +export function checkPortAvailable(port: number): Promise { return new Promise((resolve) => { const server = net.createServer(); server.once('error', () => resolve(false)); @@ -124,17 +129,15 @@ export async function detectEnvironment(cwd: string): Promise { gitRoot: null, detectedCi: null, hasDocker: false, - portAvailable: false, primaryLanguage: 'Unknown', }; } - const [detectedCi, hasDocker, portAvailable, primaryLanguage] = await Promise.all([ + const [detectedCi, hasDocker, primaryLanguage] = await Promise.all([ detectCiSystem(gitRoot), detectDocker(gitRoot), - checkPortAvailable(4747), detectPrimaryLanguage(gitRoot), ]); - return { gitRoot, detectedCi, hasDocker, portAvailable, primaryLanguage }; + return { gitRoot, detectedCi, hasDocker, primaryLanguage }; } diff --git a/gitnexus/src/cli/ci-setup/types.ts b/gitnexus/src/cli/ci-setup/types.ts index 47ca019d4..0b767dd71 100644 --- a/gitnexus/src/cli/ci-setup/types.ts +++ b/gitnexus/src/cli/ci-setup/types.ts @@ -19,7 +19,6 @@ export interface DetectResult { gitRoot: string | null; detectedCi: CiSystem | null; hasDocker: boolean; - portAvailable: boolean; primaryLanguage: string; } diff --git a/gitnexus/test/unit/ci-setup-templates.test.ts b/gitnexus/test/unit/ci-setup-templates.test.ts index 8c4de2f0a..0d20c1839 100644 --- a/gitnexus/test/unit/ci-setup-templates.test.ts +++ b/gitnexus/test/unit/ci-setup-templates.test.ts @@ -7,7 +7,6 @@ const DEFAULT_DETECT: DetectResult = { gitRoot: '/repo', detectedCi: 'github-actions', hasDocker: true, - portAvailable: true, primaryLanguage: 'TypeScript', }; diff --git a/gitnexus/test/unit/ci-setup.test.ts b/gitnexus/test/unit/ci-setup.test.ts index 01a5f3931..5b8866b02 100644 --- a/gitnexus/test/unit/ci-setup.test.ts +++ b/gitnexus/test/unit/ci-setup.test.ts @@ -231,6 +231,28 @@ describe('ciSetupCommand', () => { const entries = await fs.readdir(tempDir); expect(entries).toHaveLength(0); }); + + it('probes the resolved --port for availability (not a hardcoded 4747)', async () => { + vi.resetModules(); + vi.doMock('../../src/cli/ci-setup/detect.js', async () => { + const actual = await vi.importActual( + '../../src/cli/ci-setup/detect.js', + ); + return { ...actual, checkPortAvailable: vi.fn(() => Promise.resolve(true)) }; + }); + const detectMod = await import('../../src/cli/ci-setup/detect.js'); + const { ciSetupCommand } = await import('../../src/cli/ci-setup.js'); + await ciSetupCommand({ + ci: 'github-actions', + deploy: 'docker', + auth: 'token', + port: '4748', + dryRun: true, + outputDir: tempDir, + }); + expect(vi.mocked(detectMod.checkPortAvailable)).toHaveBeenCalledWith(4748); + vi.doUnmock('../../src/cli/ci-setup/detect.js'); + }); }); describe('resolveOptions (U1: interactive prompts reachable)', () => { @@ -238,7 +260,6 @@ describe('resolveOptions (U1: interactive prompts reachable)', () => { gitRoot: '/tmp/repo', detectedCi: null, hasDocker: false, - portAvailable: true, primaryLanguage: 'TypeScript', }; let originalTTY: boolean | undefined;