Skip to content

Commit 2216b3a

Browse files
authored
chore: implement chrome-devtools CLI (#1079)
Note that the implementation for response handling and daemon start/stop is still a placeholder. The proper handling will be done in follow-ups. The `generateCustomHelp` function is also fully experimental and it is likely to get removed before the release. The goal of this PR is to scaffold the e2e workflow for early manual testing. Automated tests will be also added in a follow-up. Note this does not expose the CLI as a binary on the package.
1 parent e5973fd commit 2216b3a

5 files changed

Lines changed: 1010 additions & 2 deletions

File tree

src/bin/chrome-devtools.ts

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
#!/usr/bin/env node
2+
3+
/**
4+
* @license
5+
* Copyright 2026 Google LLC
6+
* SPDX-License-Identifier: Apache-2.0
7+
*/
8+
9+
import process from 'node:process';
10+
11+
import yargs, {type Options, type PositionalOptions} from 'yargs';
12+
import {hideBin} from 'yargs/helpers';
13+
14+
import {startDaemon, stopDaemon, sendCommand} from '../daemon/client.js';
15+
import {isDaemonRunning} from '../daemon/utils.js';
16+
import {VERSION} from '../version.js';
17+
18+
import {commands} from './cliDefinitions.js';
19+
import {generateCustomHelp} from './customHelp.js';
20+
21+
const argv = hideBin(process.argv);
22+
23+
if (argv.length === 0 || argv[0] === '--custom-help') {
24+
console.log(generateCustomHelp(VERSION, commands));
25+
process.exit(0);
26+
}
27+
28+
const y = yargs(argv)
29+
.scriptName('chrome-devtools')
30+
.showHelpOnFail(true)
31+
.demandCommand()
32+
.version(VERSION)
33+
.strict()
34+
.help(true);
35+
36+
y.command(
37+
'start',
38+
'Start or restart chrome-devtools-mcp',
39+
y => y.help(false), // Disable help for start command to avoid parsing issues with passed args
40+
async () => {
41+
if (isDaemonRunning()) {
42+
await stopDaemon();
43+
}
44+
// Extract args after 'start'
45+
const startIndex = process.argv.indexOf('start');
46+
const args = startIndex !== -1 ? process.argv.slice(startIndex + 1) : [];
47+
await startDaemon([...args, '--via-cli']);
48+
},
49+
);
50+
51+
y.command('status', 'Checks if chrome-devtools-mcp is running', async () => {
52+
if (isDaemonRunning()) {
53+
console.log('chrome-devtools-mcp daemon is running.');
54+
} else {
55+
console.log('chrome-devtools-mcp daemon is not running');
56+
}
57+
});
58+
59+
y.command('stop', 'Stop chrome-devtools-mcp if any', async () => {
60+
await stopDaemon();
61+
});
62+
63+
for (const [commandName, commandDef] of Object.entries(commands)) {
64+
const args = commandDef.args;
65+
const requiredArgNames = Object.keys(args).filter(
66+
name => args[name].required,
67+
);
68+
69+
let commandStr = commandName;
70+
for (const arg of requiredArgNames) {
71+
commandStr += ` <${arg}>`;
72+
}
73+
74+
y.command(
75+
commandStr,
76+
commandDef.description,
77+
y => {
78+
for (const [argName, opt] of Object.entries(args)) {
79+
const type =
80+
opt.type === 'integer' || opt.type === 'number'
81+
? 'number'
82+
: opt.type === 'boolean'
83+
? 'boolean'
84+
: opt.type === 'array'
85+
? 'array'
86+
: 'string';
87+
88+
if (opt.required) {
89+
const options: PositionalOptions = {
90+
describe: opt.description,
91+
type: type as PositionalOptions['type'],
92+
};
93+
if (opt.default !== undefined) {
94+
options.default = opt.default;
95+
}
96+
if (opt.enum) {
97+
options.choices = opt.enum as Array<string | number>;
98+
}
99+
y.positional(argName, options);
100+
} else {
101+
const options: Options = {
102+
describe: opt.description,
103+
type: type as Options['type'],
104+
};
105+
if (opt.default !== undefined) {
106+
options.default = opt.default;
107+
}
108+
if (opt.enum) {
109+
options.choices = opt.enum as Array<string | number>;
110+
}
111+
y.option(argName, options);
112+
}
113+
}
114+
},
115+
async argv => {
116+
try {
117+
if (!isDaemonRunning()) {
118+
await startDaemon(['--via-cli']);
119+
}
120+
121+
const commandArgs: Record<string, unknown> = {};
122+
for (const argName of Object.keys(args)) {
123+
if (argName in argv) {
124+
commandArgs[argName] = argv[argName];
125+
}
126+
}
127+
128+
const response = await sendCommand({
129+
method: 'invoke_tool',
130+
tool: commandName,
131+
args: commandArgs,
132+
});
133+
134+
if (response.success) {
135+
console.log(response.result);
136+
} else {
137+
console.error('Error:', response.error);
138+
process.exit(1);
139+
}
140+
} catch (error) {
141+
console.error('Failed to execute command:', error);
142+
process.exit(1);
143+
}
144+
},
145+
);
146+
}
147+
148+
await y.parse();

0 commit comments

Comments
 (0)