diff --git a/src/bin/chrome-devtools.ts b/src/bin/chrome-devtools.ts index 227ee7b54..d8dfbdf1e 100644 --- a/src/bin/chrome-devtools.ts +++ b/src/bin/chrome-devtools.ts @@ -10,14 +10,14 @@ import process from 'node:process'; import type {Options, PositionalOptions} from 'yargs'; -import {parseArguments} from '../cli.js'; +import {cliOptions, parseArguments} from '../cli.js'; import { startDaemon, stopDaemon, sendCommand, handleResponse, } from '../daemon/client.js'; -import {isDaemonRunning} from '../daemon/utils.js'; +import {isDaemonRunning, serializeArgs} from '../daemon/utils.js'; import {logDisclaimers} from '../server.js'; import {hideBin, yargs, type CallToolResult} from '../third_party/index.js'; import {VERSION} from '../version.js'; @@ -26,7 +26,7 @@ import {commands} from './cliDefinitions.js'; async function start(args: string[]) { const combinedArgs = [...args, ...defaultArgs]; - await startDaemon([...args, ...defaultArgs]); + await startDaemon(combinedArgs); logDisclaimers(parseArguments(VERSION, combinedArgs)); } @@ -50,19 +50,17 @@ y.command( 'Start or restart chrome-devtools-mcp', y => y - .help(false) // Disable help for start command to avoid parsing issues with passed args. + .options(cliOptions) .example( - '$0 start --port 8080 --url http://localhost:8080', - 'Start the server on port 8080 with a specific URL', + '$0 start --browserUrl http://localhost:9222', + 'Start the server connecting to an existing browser', ) - .strict(false), // Don't validate arguments for start, as they are passed through to the daemon. - async () => { + .strict(), + async argv => { if (isDaemonRunning()) { await stopDaemon(); } - // Extract args after 'start' - const startIndex = process.argv.indexOf('start'); - const args = startIndex !== -1 ? process.argv.slice(startIndex + 1) : []; + const args = serializeArgs(cliOptions, argv); await start(args); process.exit(0); }, diff --git a/src/daemon/utils.ts b/src/daemon/utils.ts index 47603e8d5..db414def0 100644 --- a/src/daemon/utils.ts +++ b/src/daemon/utils.ts @@ -9,7 +9,9 @@ import os from 'node:os'; import path from 'node:path'; import process from 'node:process'; +import type {ParsedArguments} from '../cli.js'; import {logger} from '../logger.js'; +import type {YargsOptions} from '../third_party/index.js'; export const DAEMON_SCRIPT_PATH = path.join(import.meta.dirname, 'daemon.js'); export const INDEX_SCRIPT_PATH = path.join( @@ -97,3 +99,32 @@ export function isDaemonRunning(pid = getDaemonPid()): pid is number { } return false; } + +export function serializeArgs( + options: Record, + argv: ParsedArguments, +): string[] { + const args: string[] = []; + for (const key of Object.keys(options)) { + if (argv[key] === undefined || argv[key] === null) { + continue; + } + const value = argv[key]; + const kebabKey = key.replace(/[A-Z]/g, m => `-${m.toLowerCase()}`); + + if (typeof value === 'boolean') { + if (value) { + args.push(`--${kebabKey}`); + } else { + args.push(`--no-${kebabKey}`); + } + } else if (Array.isArray(value)) { + for (const item of value) { + args.push(`--${kebabKey}`, String(item)); + } + } else { + args.push(`--${kebabKey}`, String(value)); + } + } + return args; +} diff --git a/tests/daemon/utils.test.ts b/tests/daemon/utils.test.ts new file mode 100644 index 000000000..8bf0f7198 --- /dev/null +++ b/tests/daemon/utils.test.ts @@ -0,0 +1,85 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'node:assert'; +import {describe, it} from 'node:test'; + +import type {ParsedArguments} from '../../src/cli.js'; +import {serializeArgs} from '../../src/daemon/utils.js'; +import type {YargsOptions} from '../../src/third_party/index.js'; + +describe('serializeArgs', () => { + it('should ignore undefined or null values', () => { + const options: Record = { + foo: {}, + bar: {}, + baz: {}, + }; + const argv = { + foo: undefined, + bar: null, + baz: 'value', + _: [], + $0: 'test', + } as unknown as ParsedArguments; + const result = serializeArgs(options, argv); + assert.deepStrictEqual(result, ['--baz', 'value']); + }); + + it('should handle boolean values', () => { + const options: Record = {foo: {}, bar: {}}; + const argv = { + foo: true, + bar: false, + _: [], + $0: 'test', + } as unknown as ParsedArguments; + const result = serializeArgs(options, argv); + assert.deepStrictEqual(result, ['--foo', '--no-bar']); + }); + + it('should handle array values', () => { + const options: Record = {foo: {}}; + const argv = { + foo: ['val1', 'val2'], + _: [], + $0: 'test', + } as unknown as ParsedArguments; + const result = serializeArgs(options, argv); + assert.deepStrictEqual(result, ['--foo', 'val1', '--foo', 'val2']); + }); + + it('should handle primitive values', () => { + const options: Record = {foo: {}, bar: {}}; + const argv = { + foo: 'string', + bar: 42, + _: [], + $0: 'test', + } as unknown as ParsedArguments; + const result = serializeArgs(options, argv); + assert.deepStrictEqual(result, ['--foo', 'string', '--bar', '42']); + }); + + it('should convert camelCase keys to kebab-case', () => { + const options: Record = { + camelCaseKey: {}, + anotherKey: {}, + }; + const argv = { + camelCaseKey: 'value1', + anotherKey: true, + _: [], + $0: 'test', + } as unknown as ParsedArguments; + const result = serializeArgs(options, argv); + assert.deepStrictEqual(result, [ + '--camel-case-key', + 'value1', + '--another-key', + ]); + }); +});