Skip to content

Commit 54ce7ce

Browse files
committed
✨ Add auto-port allocation for multiple TDD servers
When starting a TDD server without --port, automatically find an available port instead of failing if 47392 is busy. This enables running multiple projects simultaneously without manual port config. - Add findAvailablePort() to ServerRegistry - Check both registry and actual TCP binding - Show "Auto-assigned port :47393" when using non-default - Add 17 tests for server registry functionality
1 parent 2975c09 commit 54ce7ce

4 files changed

Lines changed: 485 additions & 111 deletions

File tree

src/cli.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
#!/usr/bin/env node
22
import 'dotenv/config';
33
import { program } from 'commander';
4-
import { saveUserPath } from './utils/global-config.js';
54
import { doctorCommand, validateDoctorOptions } from './commands/doctor.js';
65
import {
76
finalizeCommand,
@@ -41,6 +40,7 @@ import { openBrowser } from './utils/browser.js';
4140
import { colors } from './utils/colors.js';
4241
import { loadConfig } from './utils/config-loader.js';
4342
import { getContext } from './utils/context.js';
43+
import { saveUserPath } from './utils/global-config.js';
4444
import * as output from './utils/output.js';
4545
import { getPackageVersion } from './utils/package-info.js';
4646

src/commands/tdd-daemon.js

Lines changed: 102 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ import {
88
} from 'node:fs';
99
import { homedir } from 'node:os';
1010
import { basename, join } from 'node:path';
11+
import { getServerRegistry } from '../tdd/server-registry.js';
1112
import * as output from '../utils/output.js';
1213
import { tddCommand } from './tdd.js';
13-
import { getServerRegistry } from '../tdd/server-registry.js';
1414

1515
/**
1616
* Start TDD server in daemon mode
@@ -24,37 +24,69 @@ export async function tddStartCommand(options = {}, globalOptions = {}) {
2424
color: !globalOptions.noColor,
2525
});
2626

27-
// Check if server already running
28-
if (await isServerRunning(options.port || 47392)) {
29-
const port = options.port || 47392;
30-
let colors = output.getColors();
27+
let registry = getServerRegistry();
28+
let colors = output.getColors();
3129

32-
output.header('tdd', 'local');
33-
output.print(` ${output.statusDot('success')} Already running`);
34-
output.blank();
35-
output.printBox(
36-
colors.brand.info(colors.underline(`http://localhost:${port}`)),
37-
{
38-
title: 'Dashboard',
39-
style: 'branded',
40-
}
41-
);
30+
// Check if THIS directory already has a server running
31+
let existingServer = registry.find({ directory: process.cwd() });
32+
if (existingServer) {
33+
// Verify it's actually running
34+
if (await isServerRunning(existingServer.port)) {
35+
output.header('tdd', 'local');
36+
output.print(` ${output.statusDot('success')} Already running`);
37+
output.blank();
38+
output.printBox(
39+
colors.brand.info(
40+
colors.underline(`http://localhost:${existingServer.port}`)
41+
),
42+
{
43+
title: 'Dashboard',
44+
style: 'branded',
45+
}
46+
);
4247

43-
if (options.open) {
44-
openDashboard(port);
48+
if (options.open) {
49+
openDashboard(existingServer.port);
50+
}
51+
return;
52+
} else {
53+
// Stale entry - clean it up
54+
registry.unregister({ directory: process.cwd() });
4555
}
56+
}
57+
58+
// Determine port: user-specified or auto-allocate
59+
let port;
60+
let autoAllocated = false;
61+
62+
if (options.port) {
63+
// User specified a port - use it (will fail if busy)
64+
port = options.port;
65+
} else {
66+
// Auto-allocate an available port
67+
port = await registry.findAvailablePort();
68+
autoAllocated = port !== 47392;
69+
}
70+
71+
// If user specified a port, check if it's in use
72+
if (options.port && (await isServerRunning(port))) {
73+
output.header('tdd', 'local');
74+
output.print(
75+
` ${output.statusDot('error')} Port ${port} is already in use`
76+
);
77+
output.blank();
78+
output.hint('Try a different port: vizzly tdd start --port 47393');
79+
output.hint('Or let Vizzly auto-allocate: vizzly tdd start');
4680
return;
4781
}
4882

4983
try {
5084
// Ensure .vizzly directory exists
51-
const vizzlyDir = join(process.cwd(), '.vizzly');
85+
let vizzlyDir = join(process.cwd(), '.vizzly');
5286
if (!existsSync(vizzlyDir)) {
5387
mkdirSync(vizzlyDir, { recursive: true });
5488
}
5589

56-
const port = options.port || 47392;
57-
5890
// Show header first so debug messages appear below it
5991
output.header('tdd', 'local');
6092

@@ -166,10 +198,10 @@ export async function tddStartCommand(options = {}, globalOptions = {}) {
166198

167199
// Register server in global registry (for menubar app)
168200
try {
169-
let registry = getServerRegistry()
201+
let registry = getServerRegistry();
170202

171203
// Clean up any stale servers first
172-
registry.cleanupStale()
204+
registry.cleanupStale();
173205

174206
// Register this server
175207
registry.register({
@@ -178,7 +210,7 @@ export async function tddStartCommand(options = {}, globalOptions = {}) {
178210
directory: process.cwd(),
179211
name: basename(process.cwd()),
180212
startedAt: new Date().toISOString(),
181-
})
213+
});
182214
} catch {
183215
// Non-fatal
184216
}
@@ -200,8 +232,13 @@ export async function tddStartCommand(options = {}, globalOptions = {}) {
200232
// Non-fatal, SDK can still use health check
201233
}
202234

203-
// Get colors for styled output
204-
let colors = output.getColors();
235+
// Show auto-allocated port message if applicable
236+
if (autoAllocated) {
237+
output.print(
238+
` ${output.statusDot('info')} Auto-assigned port ${colors.brand.textTertiary(`:${port}`)}`
239+
);
240+
output.blank();
241+
}
205242

206243
// Show dashboard URL in a branded box
207244
let dashboardUrl = `http://localhost:${port}`;
@@ -288,8 +325,8 @@ export async function runDaemonChild(options = {}, globalOptions = {}) {
288325

289326
// Unregister from global registry (for menubar app)
290327
try {
291-
let registry = getServerRegistry()
292-
registry.unregister({ port: port, directory: process.cwd() })
328+
let registry = getServerRegistry();
329+
registry.unregister({ port: port, directory: process.cwd() });
293330
} catch {
294331
// Non-fatal
295332
}
@@ -420,8 +457,8 @@ export async function tddStopCommand(options = {}, globalOptions = {}) {
420457

421458
// Unregister from global registry (for menubar app)
422459
try {
423-
let registry = getServerRegistry()
424-
registry.unregister({ port: port, directory: process.cwd() })
460+
let registry = getServerRegistry();
461+
registry.unregister({ port: port, directory: process.cwd() });
425462
} catch {
426463
// Non-fatal
427464
}
@@ -436,8 +473,8 @@ export async function tddStopCommand(options = {}, globalOptions = {}) {
436473

437474
// Still unregister from registry
438475
try {
439-
let registry = getServerRegistry()
440-
registry.unregister({ port: port, directory: process.cwd() })
476+
let registry = getServerRegistry();
477+
registry.unregister({ port: port, directory: process.cwd() });
441478
} catch {
442479
// Non-fatal
443480
}
@@ -601,60 +638,66 @@ export async function tddListCommand(_options, globalOptions = {}) {
601638
color: !globalOptions.noColor,
602639
});
603640

604-
let registry = getServerRegistry()
641+
let registry = getServerRegistry();
605642

606643
// Clean up stale servers first
607-
let cleaned = registry.cleanupStale()
644+
let cleaned = registry.cleanupStale();
608645
if (cleaned > 0 && globalOptions.verbose) {
609-
output.debug('tdd', `Cleaned up ${cleaned} stale server(s)`)
646+
output.debug('tdd', `Cleaned up ${cleaned} stale server(s)`);
610647
}
611648

612-
let servers = registry.list()
649+
let servers = registry.list();
613650

614651
// JSON output
615652
if (globalOptions.json) {
616-
console.log(JSON.stringify({ servers }, null, 2))
617-
return
653+
console.log(JSON.stringify({ servers }, null, 2));
654+
return;
618655
}
619656

620657
// No servers
621658
if (servers.length === 0) {
622-
output.info('No TDD servers running')
623-
output.hint('Start one with: vizzly tdd start')
624-
return
659+
output.info('No TDD servers running');
660+
output.hint('Start one with: vizzly tdd start');
661+
return;
625662
}
626663

627664
// Table output
628-
let colors = output.getColors()
665+
let colors = output.getColors();
629666

630-
output.header('tdd', 'servers')
631-
output.blank()
667+
output.header('tdd', 'servers');
668+
output.blank();
632669

633670
for (let server of servers) {
634-
let uptimeStr = ''
671+
let uptimeStr = '';
635672
if (server.startedAt) {
636-
let startTime = new Date(server.startedAt).getTime()
637-
let uptime = Math.floor((Date.now() - startTime) / 1000)
638-
let hours = Math.floor(uptime / 3600)
639-
let minutes = Math.floor((uptime % 3600) / 60)
640-
if (hours > 0) uptimeStr += `${hours}h `
641-
if (minutes > 0 || hours > 0) uptimeStr += `${minutes}m`
642-
else uptimeStr = '<1m'
673+
let startTime = new Date(server.startedAt).getTime();
674+
let uptime = Math.floor((Date.now() - startTime) / 1000);
675+
let hours = Math.floor(uptime / 3600);
676+
let minutes = Math.floor((uptime % 3600) / 60);
677+
if (hours > 0) uptimeStr += `${hours}h `;
678+
if (minutes > 0 || hours > 0) uptimeStr += `${minutes}m`;
679+
else uptimeStr = '<1m';
643680
}
644681

645-
let name = server.name || basename(server.directory)
646-
let portStr = colors.brand.textTertiary(`:${server.port}`)
647-
let uptimeLabel = uptimeStr ? colors.brand.textMuted(` · ${uptimeStr}`) : ''
682+
let name = server.name || basename(server.directory);
683+
let portStr = colors.brand.textTertiary(`:${server.port}`);
684+
let uptimeLabel = uptimeStr
685+
? colors.brand.textMuted(` · ${uptimeStr}`)
686+
: '';
648687

649-
output.print(` ${output.statusDot('success')} ${name}${portStr}${uptimeLabel}`)
650-
output.print(` ${colors.brand.textMuted(server.directory)}`)
688+
output.print(
689+
` ${output.statusDot('success')} ${name}${portStr}${uptimeLabel}`
690+
);
691+
output.print(` ${colors.brand.textMuted(server.directory)}`);
651692

652693
if (globalOptions.verbose) {
653-
output.print(` ${colors.brand.textMuted(`PID: ${server.pid}`)}`)
694+
output.print(` ${colors.brand.textMuted(`PID: ${server.pid}`)}`);
654695
}
655696

656-
output.blank()
697+
output.blank();
657698
}
658699

659-
output.print(` ${colors.brand.textTertiary(`${servers.length} server(s) running`)}`)
700+
output.print(
701+
` ${colors.brand.textTertiary(`${servers.length} server(s) running`)}`
702+
);
660703
}

0 commit comments

Comments
 (0)