fix(cli): warn about version mismatch between cli and daemon (#2461)

Co-authored-by: Piotr Paulski <piotrpaulski@chromium.org>
This commit is contained in:
Piotr Paulski
2026-08-04 10:16:25 +02:00
committed by GitHub
parent ba4fe3eaa4
commit 3afc44dac9
5 changed files with 107 additions and 27 deletions
+20 -7
View File
@@ -17,7 +17,9 @@ import {
stopDaemon,
sendCommand,
handleResponse,
verifyDaemonVersion,
} from '../daemon/client.js';
import type {DaemonStatusResult} from '../daemon/types.js';
import {isDaemonRunning, serializeArgs} from '../daemon/utils.js';
import {logDisclaimers} from '../index.js';
import {hideBin, yargs, type CallToolResult} from '../third_party/index.js';
@@ -159,17 +161,16 @@ y.command(
argv.sessionId,
);
if (response.success) {
const data = JSON.parse(response.result) as {
pid: number | null;
socketPath: string;
startDate: string;
version: string;
args: string[];
};
const data: DaemonStatusResult = JSON.parse(response.result);
console.log(
`pid=${data.pid} socket=${data.socketPath} start-date=${data.startDate} version=${data.version}`,
);
console.log(`args=${JSON.stringify(data.args)}`);
if (data.version !== VERSION) {
console.warn(
`Warning: Daemon server version (${data.version}) does not match CLI version (${VERSION}). Run 'chrome-devtools start' to update and restart the daemon.`,
);
}
} else {
console.error('Error:', response.error);
process.exit(1);
@@ -262,6 +263,10 @@ for (const [commandName, commandDef] of Object.entries(commands)) {
async argv => {
const sessionId = argv.sessionId as string;
try {
const versionWarningPromise = isDaemonRunning(sessionId)
? verifyDaemonVersion(sessionId, VERSION)
: Promise.resolve(undefined);
if (!isDaemonRunning(sessionId)) {
await start(serializeArgs(cliOptions, argv), sessionId);
}
@@ -291,6 +296,14 @@ for (const [commandName, commandDef] of Object.entries(commands)) {
);
} else {
console.error('Error:', response.error);
}
const versionWarning = await versionWarningPromise;
if (versionWarning) {
console.warn(versionWarning);
}
if (!response.success) {
process.exit(1);
}
} catch (error) {
+26 -1
View File
@@ -13,7 +13,11 @@ import {PipeTransport} from '../third_party/index.js';
import {getTempFilePath} from '../utils/files.js';
import {logger} from '../utils/logger.js';
import type {DaemonMessage, DaemonResponse} from './types.js';
import type {
DaemonMessage,
DaemonResponse,
DaemonStatusResult,
} from './types.js';
import {
DAEMON_SCRIPT_PATH,
getSocketPath,
@@ -194,6 +198,27 @@ export async function stopDaemon(sessionId: string) {
await waitForFile(pidFilePath, /*removed=*/ true);
}
export async function verifyDaemonVersion(
sessionId: string,
cliVersion: string,
): Promise<string | undefined> {
if (!isDaemonRunning(sessionId)) {
return undefined;
}
try {
const response = await sendCommand({method: 'status'}, sessionId);
if (response.success) {
const data: DaemonStatusResult = JSON.parse(response.result);
if (data?.version && data.version !== cliVersion) {
return `Warning: Daemon server version (${data.version}) does not match CLI version (${cliVersion}). Run 'chrome-devtools start' to update and restart the daemon.`;
}
}
} catch {
// Suppress communication failures during check; command execution handles unreachable daemon errors.
}
return undefined;
}
export async function handleResponse(
response: CallToolResult,
format: 'json' | 'md',
+9 -8
View File
@@ -20,7 +20,7 @@ import {
import {logger} from '../utils/logger.js';
import {VERSION} from '../version.js';
import type {DaemonMessage} from './types.js';
import type {DaemonMessage, DaemonStatusResult} from './types.js';
import {
DAEMON_CLIENT_NAME,
getPidFilePath,
@@ -186,15 +186,16 @@ async function handleRequest(msg: DaemonMessage) {
};
} else if (msg.method === 'status') {
await started;
const statusResult: DaemonStatusResult = {
pid: process.pid,
socketPath,
startDate: startDate.toISOString(),
version: VERSION,
args: mcpServerArgs,
};
return {
success: true,
result: JSON.stringify({
pid: process.pid,
socketPath,
startDate: startDate.toISOString(),
version: VERSION,
args: mcpServerArgs,
}),
result: JSON.stringify(statusResult),
};
}
{
+8
View File
@@ -23,3 +23,11 @@ export interface DaemonResponse {
result: string;
error: unknown;
}
export interface DaemonStatusResult {
pid: number | null;
socketPath: string;
startDate: string;
version: string;
args: string[];
}
+44 -11
View File
@@ -14,22 +14,24 @@ import {
handleResponse,
startDaemon,
stopDaemon,
verifyDaemonVersion,
} from '../../src/daemon/client.js';
import {isDaemonRunning} from '../../src/daemon/utils.js';
import {VERSION} from '../../src/version.js';
describe('daemon client', () => {
let sessionId: string;
beforeEach(async () => {
sessionId = crypto.randomUUID();
await stopDaemon(sessionId);
});
afterEach(async () => {
await stopDaemon(sessionId);
});
describe('start/stop', () => {
let sessionId: string;
beforeEach(async () => {
sessionId = crypto.randomUUID();
await stopDaemon(sessionId);
});
afterEach(async () => {
await stopDaemon(sessionId);
});
it('should start and stop daemon', async () => {
assert.ok(
!isDaemonRunning(sessionId),
@@ -73,6 +75,37 @@ describe('daemon client', () => {
});
});
describe('verifyDaemonVersion', () => {
it('warns when daemon version does not match CLI version', async () => {
await startDaemon([], sessionId);
const warning = await verifyDaemonVersion(sessionId, '0.0.0-mismatch');
assert.ok(
warning &&
warning.includes('does not match CLI version (0.0.0-mismatch)'),
`Expected warning message about version mismatch, got: ${warning}`,
);
});
it('does not warn when daemon version matches CLI version', async () => {
await startDaemon([], sessionId);
const warning = await verifyDaemonVersion(sessionId, VERSION);
assert.strictEqual(
warning,
undefined,
'Should not return warning when version matches',
);
});
it('does nothing when daemon is not running', async () => {
const warning = await verifyDaemonVersion(sessionId, '0.0.0-mismatch');
assert.strictEqual(
warning,
undefined,
'Should not return warning when daemon is stopped',
);
});
});
describe('parsing', () => {
it('handles MCP response with text format', async () => {
const textResponse = {content: [{type: 'text' as const, text: 'test'}]};