fix(bar): keep sticky port across ccs bar stop via launch.json fallback

`ccs bar stop` deletes bar.json, so a bar.json-only sticky port is lost on
every stop/start cycle: the next launch reverted to 3000 and the probe could
no longer find a server still running on the previously chosen port.
resolveBarPort now falls back to the --port recorded in launch.json (written
by launch, not deleted by stop), which restores both the sticky port and
probe discovery after a stop.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
poomscandClaude Fable 5 committed 2026-08-08 14:50:18 +07:00
1 parent 34608ce291
commit d27b53f235
2 files changed
+82 -4

No files matched your search

+22 -4
View File
@@ -26,8 +26,11 @@ export interface DashboardInfo {
}
/**
* Read the port recorded in an existing bar.json.
* Returns null when the file is absent or malformed.
* Read the port recorded in an existing bar.json, falling back to the --port
* in launch.json's args. `ccs bar stop` deletes bar.json but leaves
* launch.json, so the fallback is what keeps the sticky port (and the probe's
* ability to find a server on a non-default port) across a stop/start cycle.
* Returns null when neither file records a port.
*/
export function resolveBarPort(ccsDir: string): number | null {
@@ -35,10 +38,25 @@ export function resolveBarPort(ccsDir: string): number | null {
try {
const raw = fs.readFileSync(barJsonPath, 'utf8');
const parsed = JSON.parse(raw) as Partial<{ port: number }>;
return typeof parsed.port === 'number' ? parsed.port : null;
if (typeof parsed.port === 'number') return parsed.port;
} catch {
return null;
/* fall through to launch.json */
}
const launchJsonPath = path.join(ccsDir, 'bar', 'launch.json');
try {
const raw = fs.readFileSync(launchJsonPath, 'utf8');
const parsed = JSON.parse(raw) as Partial<{ args: unknown[] }>;
const args = Array.isArray(parsed.args) ? parsed.args : [];
const idx = args.indexOf('--port');
if (idx !== -1 && idx + 1 < args.length) {
const n = parseInt(String(args[idx + 1]), 10);
if (Number.isFinite(n) && n > 0 && n < 65536) return n;
}
} catch {
/* absent or malformed -> null */
}
return null;
}
/**
+60
View File
@@ -3814,3 +3814,63 @@ describe('launch: --port selects the server port', () => {
expect(seen.spawnPort).toBe(3777);
});
});
describe('resolveBarPort: launch.json fallback survives `ccs bar stop`', () => {
// `ccs bar stop` deletes bar.json, so bar.json alone cannot carry the sticky
// port across a stop/start cycle. launch.json (refreshed by launch, NOT
// deleted by stop) records the port in its args and acts as the fallback.
it('falls back to the --port recorded in launch.json when bar.json is absent', async () => {
const ccsDir = path.join(tempHome, '.ccs');
fs.mkdirSync(path.join(ccsDir, 'bar'), { recursive: true });
fs.writeFileSync(
path.join(ccsDir, 'bar', 'launch.json'),
JSON.stringify({
schema: 1,
runtime: '/usr/bin/node',
args: ['/x/ccs.js', 'bar', 'serve', '--port', '3456'],
home: tempHome,
})
);
const { resolveBarPort } = await loadLaunchSubcommand();
expect(resolveBarPort(ccsDir)).toBe(3456);
});
it('bar.json port wins over launch.json when both exist', async () => {
const ccsDir = path.join(tempHome, '.ccs');
fs.mkdirSync(path.join(ccsDir, 'bar'), { recursive: true });
fs.writeFileSync(
path.join(ccsDir, 'bar.json'),
JSON.stringify({ baseUrl: 'http://127.0.0.1:4000', port: 4000, authMode: 'loopback' })
);
fs.writeFileSync(
path.join(ccsDir, 'bar', 'launch.json'),
JSON.stringify({
schema: 1,
runtime: '/usr/bin/node',
args: ['/x/ccs.js', 'bar', 'serve', '--port', '3456'],
home: tempHome,
})
);
const { resolveBarPort } = await loadLaunchSubcommand();
expect(resolveBarPort(ccsDir)).toBe(4000);
});
it('returns null when launch.json has no --port and bar.json is absent', async () => {
const ccsDir = path.join(tempHome, '.ccs');
fs.mkdirSync(path.join(ccsDir, 'bar'), { recursive: true });
fs.writeFileSync(
path.join(ccsDir, 'bar', 'launch.json'),
JSON.stringify({
schema: 1,
runtime: '/usr/bin/node',
args: ['/x/ccs.js', 'bar', 'serve'],
home: tempHome,
})
);
const { resolveBarPort } = await loadLaunchSubcommand();
expect(resolveBarPort(ccsDir)).toBeNull();
});
});