mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 18:15:23 +00:00
feat(bar): support --port for ccs bar with sticky port persistence
ccs bar always forced the dashboard onto port 3000 (first free of a hardcoded candidate list), which collides with other local dev servers, and bar.json was rewritten to 3000 on every launch. - `ccs bar [launch] --port N` runs the server on exactly N: reuses a live server already on N, moves a running server from another port (SIGTERM via server.pid, wait for exit), errors clearly when N is busy or the value is invalid. - The chosen port is persisted into launch.json args, so the Swift app self-starts the server on the same port. - Without --port, launch and serve now try the port recorded in bar.json first (sticky), so the server keeps coming back on the port the user last chose instead of reverting to 3000. - Bare flags (`ccs bar --port N`) route to the launch subcommand; --port is documented in `ccs bar --help`. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7fb56d62e1
commit
34608ce291
9 files changed
+358
-54
No files matched your search
@@ -3643,3 +3643,174 @@ describe('bar install: --await-quit waits for the running app to quit (GH-1588)'
|
||||
expect(probes).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// --port support: `ccs bar [launch] --port N` (user-selectable dashboard port)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('bar dispatcher: bare flags route to launch', () => {
|
||||
beforeEach(() => {
|
||||
mock.module('../../../src/commands/bar/launch-subcommand', () => ({
|
||||
handleBarLaunch: async (args: string[]) => {
|
||||
calls.push(`launch:${args.join(' ')}`);
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
it('dispatches `ccs bar --port 3999` to launch with the flag preserved', async () => {
|
||||
const handleBarCommand = await loadHandleBarCommand();
|
||||
await handleBarCommand(['--port', '3999']);
|
||||
expect(calls).toEqual(['launch:--port 3999']);
|
||||
});
|
||||
|
||||
it('dispatches `ccs bar launch --port 3999` to launch with the flag preserved', async () => {
|
||||
const handleBarCommand = await loadHandleBarCommand();
|
||||
await handleBarCommand(['launch', '--port', '3999']);
|
||||
expect(calls).toEqual(['launch:--port 3999']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('launch: --port selects the server port', () => {
|
||||
function makePortDeps(ccsDir: string) {
|
||||
const seen: {
|
||||
spawnPort: number | null;
|
||||
getPortCandidates: number[] | null;
|
||||
stopped: boolean;
|
||||
descriptorPort: number | null;
|
||||
} = { spawnPort: null, getPortCandidates: null, stopped: false, descriptorPort: null };
|
||||
|
||||
const deps = {
|
||||
findRunningServer: async () => null,
|
||||
getPort: async (opts: { port: number[]; host: string }) => {
|
||||
seen.getPortCandidates = opts.port;
|
||||
return opts.port[0];
|
||||
},
|
||||
spawnDetachedServer: (p: number) => {
|
||||
seen.spawnPort = p;
|
||||
},
|
||||
waitForServerLive: async () => {},
|
||||
createLaunchDescriptor: (opts?: { port?: number }) => {
|
||||
seen.descriptorPort = opts?.port ?? null;
|
||||
return {
|
||||
schema: 1 as const,
|
||||
runtime: '/usr/bin/node',
|
||||
args: ['/x/ccs.js', 'bar', 'serve'],
|
||||
home: '/h',
|
||||
};
|
||||
},
|
||||
writeLaunchDescriptor: () => {},
|
||||
stopDetachedServer: () => {
|
||||
seen.stopped = true;
|
||||
},
|
||||
openApp: async () => {},
|
||||
getCcsDir: () => ccsDir,
|
||||
appInstallPath: path.join(tempHome, 'Applications', 'CCS Bar.app'),
|
||||
};
|
||||
return { deps, seen };
|
||||
}
|
||||
|
||||
it('spawns the detached server on the requested port and records it in bar.json', async () => {
|
||||
const ccsDir = path.join(tempHome, '.ccs');
|
||||
fs.mkdirSync(ccsDir, { recursive: true });
|
||||
const { handleBarLaunch } = await loadLaunchSubcommand();
|
||||
const { deps, seen } = makePortDeps(ccsDir);
|
||||
|
||||
await handleBarLaunch(['--port', '3999'], deps);
|
||||
|
||||
expect(seen.spawnPort).toBe(3999);
|
||||
const barJson = JSON.parse(fs.readFileSync(path.join(ccsDir, 'bar.json'), 'utf8')) as {
|
||||
port: number;
|
||||
baseUrl: string;
|
||||
};
|
||||
expect(barJson.port).toBe(3999);
|
||||
expect(barJson.baseUrl).toBe('http://127.0.0.1:3999');
|
||||
});
|
||||
|
||||
it('persists the chosen port into the launch descriptor for app self-start', async () => {
|
||||
const ccsDir = path.join(tempHome, '.ccs');
|
||||
fs.mkdirSync(ccsDir, { recursive: true });
|
||||
const { handleBarLaunch } = await loadLaunchSubcommand();
|
||||
const { deps, seen } = makePortDeps(ccsDir);
|
||||
|
||||
await handleBarLaunch(['--port', '3999'], deps);
|
||||
|
||||
expect(seen.descriptorPort).toBe(3999);
|
||||
});
|
||||
|
||||
it('errors without spawning when the requested port is busy', async () => {
|
||||
const ccsDir = path.join(tempHome, '.ccs');
|
||||
fs.mkdirSync(ccsDir, { recursive: true });
|
||||
const { handleBarLaunch } = await loadLaunchSubcommand();
|
||||
const { deps, seen } = makePortDeps(ccsDir);
|
||||
deps.getPort = async () => 4001; // get-port fell back: 3999 not free
|
||||
|
||||
await handleBarLaunch(['--port', '3999'], deps);
|
||||
|
||||
expect(seen.spawnPort).toBeNull();
|
||||
const allOutput = consoleOutput.join('\n');
|
||||
expect(allOutput).toMatch(/3999/);
|
||||
expect(allOutput.toLowerCase()).toMatch(/in use|busy|not free|unavailable/);
|
||||
});
|
||||
|
||||
it('errors on an invalid --port value', async () => {
|
||||
const ccsDir = path.join(tempHome, '.ccs');
|
||||
fs.mkdirSync(ccsDir, { recursive: true });
|
||||
const { handleBarLaunch } = await loadLaunchSubcommand();
|
||||
const { deps, seen } = makePortDeps(ccsDir);
|
||||
|
||||
await handleBarLaunch(['--port', 'banana'], deps);
|
||||
|
||||
expect(seen.spawnPort).toBeNull();
|
||||
expect(consoleOutput.join('\n').toLowerCase()).toMatch(/invalid.*port|port.*invalid/);
|
||||
});
|
||||
|
||||
it('reuses a running server already on the requested port', async () => {
|
||||
const ccsDir = path.join(tempHome, '.ccs');
|
||||
fs.mkdirSync(ccsDir, { recursive: true });
|
||||
const { handleBarLaunch } = await loadLaunchSubcommand();
|
||||
const { deps, seen } = makePortDeps(ccsDir);
|
||||
deps.findRunningServer = async () => ({ port: 3999, baseUrl: 'http://127.0.0.1:3999' });
|
||||
|
||||
await handleBarLaunch(['--port', '3999'], deps);
|
||||
|
||||
expect(seen.spawnPort).toBeNull(); // reuse, no new spawn
|
||||
expect(seen.stopped).toBe(false);
|
||||
const barJson = JSON.parse(fs.readFileSync(path.join(ccsDir, 'bar.json'), 'utf8')) as {
|
||||
port: number;
|
||||
};
|
||||
expect(barJson.port).toBe(3999);
|
||||
});
|
||||
|
||||
it('stops a running server on a different port, then starts on the requested one', async () => {
|
||||
const ccsDir = path.join(tempHome, '.ccs');
|
||||
fs.mkdirSync(ccsDir, { recursive: true });
|
||||
const { handleBarLaunch } = await loadLaunchSubcommand();
|
||||
const { deps, seen } = makePortDeps(ccsDir);
|
||||
deps.findRunningServer = async () => ({ port: 3000, baseUrl: 'http://127.0.0.1:3000' });
|
||||
|
||||
await handleBarLaunch(['--port', '3999'], deps);
|
||||
|
||||
expect(seen.stopped).toBe(true);
|
||||
expect(seen.spawnPort).toBe(3999);
|
||||
const barJson = JSON.parse(fs.readFileSync(path.join(ccsDir, 'bar.json'), 'utf8')) as {
|
||||
port: number;
|
||||
};
|
||||
expect(barJson.port).toBe(3999);
|
||||
});
|
||||
|
||||
it('without --port, prefers the port recorded in bar.json (sticky port)', async () => {
|
||||
const ccsDir = path.join(tempHome, '.ccs');
|
||||
fs.mkdirSync(ccsDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(ccsDir, 'bar.json'),
|
||||
JSON.stringify({ baseUrl: 'http://127.0.0.1:3777', port: 3777, authMode: 'loopback' })
|
||||
);
|
||||
const { handleBarLaunch } = await loadLaunchSubcommand();
|
||||
const { deps, seen } = makePortDeps(ccsDir);
|
||||
|
||||
await handleBarLaunch([], deps);
|
||||
|
||||
expect(seen.getPortCandidates?.[0]).toBe(3777);
|
||||
expect(seen.spawnPort).toBe(3777);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user