fix: auto-detect free port in `ao init` instead of hardcoding 3000
When port 3000 is occupied, `ao init` now scans upward (3001, 3002, ...) until a free port is found. Applies to both interactive and --auto modes. Uses the existing isPortAvailable() from web-dir.ts (now exported). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
0e533840ba
commit
287a9cdab3
|
|
@ -1,7 +1,8 @@
|
|||
import { describe, it, expect, vi, afterEach } from "vitest";
|
||||
import { mkdtempSync, writeFileSync, rmSync, existsSync } from "node:fs";
|
||||
import { mkdtempSync, writeFileSync, readFileSync, rmSync, existsSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { createServer } from "node:net";
|
||||
|
||||
import { Command } from "commander";
|
||||
import { registerInit } from "../../src/commands/init.js";
|
||||
|
|
@ -35,4 +36,53 @@ describe("init command", () => {
|
|||
// Original file should be untouched
|
||||
expect(existsSync(outputPath)).toBe(true);
|
||||
});
|
||||
|
||||
it("auto mode uses port 3000 when it is available", async () => {
|
||||
tmpDir = mkdtempSync(join(tmpdir(), "ao-init-test-"));
|
||||
const outputPath = join(tmpDir, "agent-orchestrator.yaml");
|
||||
|
||||
const program = new Command();
|
||||
program.exitOverride();
|
||||
registerInit(program);
|
||||
|
||||
vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
|
||||
await program.parseAsync(["node", "test", "init", "--auto", "--output", outputPath]);
|
||||
|
||||
const content = readFileSync(outputPath, "utf-8");
|
||||
expect(content).toContain("port: 3000");
|
||||
});
|
||||
|
||||
it("auto mode picks next free port when 3000 is occupied", async () => {
|
||||
tmpDir = mkdtempSync(join(tmpdir(), "ao-init-test-"));
|
||||
const outputPath = join(tmpDir, "agent-orchestrator.yaml");
|
||||
|
||||
// Occupy port 3000
|
||||
const blocker = createServer();
|
||||
await new Promise<void>((resolve) => {
|
||||
blocker.listen(3000, "127.0.0.1", () => resolve());
|
||||
});
|
||||
|
||||
try {
|
||||
const program = new Command();
|
||||
program.exitOverride();
|
||||
registerInit(program);
|
||||
|
||||
vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
|
||||
await program.parseAsync(["node", "test", "init", "--auto", "--output", outputPath]);
|
||||
|
||||
const content = readFileSync(outputPath, "utf-8");
|
||||
// Should NOT be 3000 since we're occupying it
|
||||
expect(content).not.toContain("port: 3000");
|
||||
// Should pick 3001 (or higher if 3001 is also taken)
|
||||
const portMatch = content.match(/port: (\d+)/);
|
||||
expect(portMatch).toBeTruthy();
|
||||
const port = parseInt(portMatch![1], 10);
|
||||
expect(port).toBeGreaterThan(3000);
|
||||
expect(port).toBeLessThan(3100);
|
||||
} finally {
|
||||
await new Promise<void>((resolve) => blocker.close(() => resolve()));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,12 +6,24 @@ import { stringify as yamlStringify } from "yaml";
|
|||
import chalk from "chalk";
|
||||
import type { Command } from "commander";
|
||||
import { git, gh, execSilent } from "../lib/shell.js";
|
||||
import { isPortAvailable } from "../lib/web-dir.js";
|
||||
import {
|
||||
detectProjectType,
|
||||
generateRulesFromTemplates,
|
||||
formatProjectTypeForDisplay,
|
||||
} from "../lib/project-detection.js";
|
||||
|
||||
const DEFAULT_PORT = 3000;
|
||||
const MAX_PORT_SCAN = 100;
|
||||
|
||||
/** Find the first available port starting from `start`, scanning upward. */
|
||||
async function findFreePort(start: number): Promise<number> {
|
||||
for (let port = start; port < start + MAX_PORT_SCAN; port++) {
|
||||
if (await isPortAvailable(port)) return port;
|
||||
}
|
||||
return start; // fallback — will fail at bind time with a clear error
|
||||
}
|
||||
|
||||
async function prompt(
|
||||
rl: ReturnType<typeof createInterface>,
|
||||
question: string,
|
||||
|
|
@ -231,7 +243,8 @@ export function registerInit(program: Command): void {
|
|||
"~/.agent-orchestrator",
|
||||
);
|
||||
const worktreeDir = await prompt(rl, "Worktree directory", "~/.worktrees");
|
||||
const portStr = await prompt(rl, "Dashboard port", "3000");
|
||||
const freePort = await findFreePort(DEFAULT_PORT);
|
||||
const portStr = await prompt(rl, "Dashboard port", String(freePort));
|
||||
const port = parseInt(portStr, 10);
|
||||
if (isNaN(port) || port < 1 || port > 65535) {
|
||||
console.error(chalk.red("\nInvalid port number. Must be 1-65535."));
|
||||
|
|
@ -430,10 +443,11 @@ async function handleAutoMode(outputPath: string, smart: boolean): Promise<void>
|
|||
const path = env.isGitRepo ? workingDir : `~/${projectId}`;
|
||||
const defaultBranch = env.defaultBranch || "main";
|
||||
|
||||
const port = await findFreePort(DEFAULT_PORT);
|
||||
const config: Record<string, unknown> = {
|
||||
dataDir: "~/.agent-orchestrator",
|
||||
worktreeDir: "~/.worktrees",
|
||||
port: 3000,
|
||||
port,
|
||||
defaults: {
|
||||
runtime: "tmux",
|
||||
agent: "claude-code",
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ const DEFAULT_TERMINAL_PORT = 14800;
|
|||
* Check if a TCP port is available by attempting to bind to it.
|
||||
* Returns true if the port is free, false if in use.
|
||||
*/
|
||||
function isPortAvailable(port: number): Promise<boolean> {
|
||||
export function isPortAvailable(port: number): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
const server = createServer();
|
||||
server.once("error", () => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue