mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-08-02 05:01:04 -07:00
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0ea49d486b | |||
| 4d4fc80f25 | |||
| dfa56c37b4 | |||
| 4249de5e23 | |||
| 536bbdf73f | |||
| 201efbd07f | |||
| 8ae078b4d7 | |||
| d84b173330 | |||
| 33d654e7d9 | |||
| 6eb100cc41 | |||
| dca1ef09cb | |||
| 616c944350 | |||
| c0f919310e | |||
| 92a09c8d8d | |||
| ecae62e25a | |||
| abb4583554 | |||
| fc7a269f0a | |||
| 9c85ef1fda | |||
| bd60ba9e68 | |||
| 2e466bd4cb | |||
| 55778583b9 | |||
| 661bd39362 | |||
| 04eb2ee9a3 | |||
| c6f6fb00c9 | |||
| 02eff4efb3 | |||
| db9eef50ef |
@@ -7,5 +7,15 @@
|
|||||||
},
|
},
|
||||||
"general": {
|
"general": {
|
||||||
"devtools": true
|
"devtools": true
|
||||||
|
},
|
||||||
|
"agents": {
|
||||||
|
"overrides": {
|
||||||
|
"browser_agent": { "enabled": true }
|
||||||
|
},
|
||||||
|
"browser": {
|
||||||
|
"headless": true,
|
||||||
|
"sessionMode": "isolated",
|
||||||
|
"allowedDomains": ["*.com"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,16 +34,20 @@ describe('extension install', () => {
|
|||||||
writeFileSync(testServerPath, extension);
|
writeFileSync(testServerPath, extension);
|
||||||
try {
|
try {
|
||||||
const result = await rig.runCommand(
|
const result = await rig.runCommand(
|
||||||
['extensions', 'install', `${rig.testDir!}`],
|
['--debug', 'extensions', 'install', `${rig.testDir!}`],
|
||||||
{ stdin: 'y\n' },
|
{ stdin: 'y\n' },
|
||||||
);
|
);
|
||||||
expect(result).toContain('test-extension-install');
|
expect(result).toContain('test-extension-install');
|
||||||
|
|
||||||
const listResult = await rig.runCommand(['extensions', 'list']);
|
const listResult = await rig.runCommand([
|
||||||
|
'--debug',
|
||||||
|
'extensions',
|
||||||
|
'list',
|
||||||
|
]);
|
||||||
expect(listResult).toContain('test-extension-install');
|
expect(listResult).toContain('test-extension-install');
|
||||||
writeFileSync(testServerPath, extensionUpdate);
|
writeFileSync(testServerPath, extensionUpdate);
|
||||||
const updateResult = await rig.runCommand(
|
const updateResult = await rig.runCommand(
|
||||||
['extensions', 'update', `test-extension-install`],
|
['--debug', 'extensions', 'update', `test-extension-install`],
|
||||||
{ stdin: 'y\n' },
|
{ stdin: 'y\n' },
|
||||||
);
|
);
|
||||||
expect(updateResult).toContain('0.0.2');
|
expect(updateResult).toContain('0.0.2');
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ describe('extension reloading', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const result = await rig.runCommand(
|
const result = await rig.runCommand(
|
||||||
['extensions', 'install', `${rig.testDir!}`],
|
['--debug', 'extensions', 'install', `${rig.testDir!}`],
|
||||||
{ stdin: 'y\n' },
|
{ stdin: 'y\n' },
|
||||||
);
|
);
|
||||||
expect(result).toContain('test-extension');
|
expect(result).toContain('test-extension');
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import {
|
|||||||
ValidationRequiredError,
|
ValidationRequiredError,
|
||||||
type AdminControlsSettings,
|
type AdminControlsSettings,
|
||||||
debugLogger,
|
debugLogger,
|
||||||
|
isHeadlessMode,
|
||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
|
|
||||||
import { loadCliConfig, parseArguments } from './config/config.js';
|
import { loadCliConfig, parseArguments } from './config/config.js';
|
||||||
@@ -296,6 +297,7 @@ export async function main() {
|
|||||||
const isDebugMode = cliConfig.isDebugMode(argv);
|
const isDebugMode = cliConfig.isDebugMode(argv);
|
||||||
const consolePatcher = new ConsolePatcher({
|
const consolePatcher = new ConsolePatcher({
|
||||||
stderr: true,
|
stderr: true,
|
||||||
|
interactive: isHeadlessMode() ? false : true,
|
||||||
debugMode: isDebugMode,
|
debugMode: isDebugMode,
|
||||||
onNewMessage: (msg) => {
|
onNewMessage: (msg) => {
|
||||||
coreEvents.emitConsoleLog(msg.type, msg.content);
|
coreEvents.emitConsoleLog(msg.type, msg.content);
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ export async function runNonInteractive({
|
|||||||
return promptIdContext.run(prompt_id, async () => {
|
return promptIdContext.run(prompt_id, async () => {
|
||||||
const consolePatcher = new ConsolePatcher({
|
const consolePatcher = new ConsolePatcher({
|
||||||
stderr: true,
|
stderr: true,
|
||||||
|
interactive: false,
|
||||||
debugMode: config.getDebugMode(),
|
debugMode: config.getDebugMode(),
|
||||||
onNewMessage: (msg) => {
|
onNewMessage: (msg) => {
|
||||||
coreEvents.emitConsoleLog(msg.type, msg.content);
|
coreEvents.emitConsoleLog(msg.type, msg.content);
|
||||||
|
|||||||
@@ -0,0 +1,236 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2026 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* eslint-disable no-console */
|
||||||
|
|
||||||
|
import { describe, it, expect, vi, afterEach } from 'vitest';
|
||||||
|
import { ConsolePatcher } from './ConsolePatcher.js';
|
||||||
|
|
||||||
|
describe('ConsolePatcher', () => {
|
||||||
|
let patcher: ConsolePatcher;
|
||||||
|
const onNewMessage = vi.fn();
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (patcher) {
|
||||||
|
patcher.cleanup();
|
||||||
|
}
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should patch and restore console methods', () => {
|
||||||
|
const beforeLog = console.log;
|
||||||
|
const beforeWarn = console.warn;
|
||||||
|
const beforeError = console.error;
|
||||||
|
const beforeDebug = console.debug;
|
||||||
|
const beforeInfo = console.info;
|
||||||
|
|
||||||
|
patcher = new ConsolePatcher({ onNewMessage, debugMode: false });
|
||||||
|
patcher.patch();
|
||||||
|
|
||||||
|
expect(console.log).not.toBe(beforeLog);
|
||||||
|
expect(console.warn).not.toBe(beforeWarn);
|
||||||
|
expect(console.error).not.toBe(beforeError);
|
||||||
|
expect(console.debug).not.toBe(beforeDebug);
|
||||||
|
expect(console.info).not.toBe(beforeInfo);
|
||||||
|
|
||||||
|
patcher.cleanup();
|
||||||
|
|
||||||
|
expect(console.log).toBe(beforeLog);
|
||||||
|
expect(console.warn).toBe(beforeWarn);
|
||||||
|
expect(console.error).toBe(beforeError);
|
||||||
|
expect(console.debug).toBe(beforeDebug);
|
||||||
|
expect(console.info).toBe(beforeInfo);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Interactive mode', () => {
|
||||||
|
it('should ignore log and info when it is not interactive and debugMode is false', () => {
|
||||||
|
patcher = new ConsolePatcher({
|
||||||
|
onNewMessage,
|
||||||
|
debugMode: false,
|
||||||
|
interactive: false,
|
||||||
|
});
|
||||||
|
patcher.patch();
|
||||||
|
|
||||||
|
console.log('test log');
|
||||||
|
console.info('test info');
|
||||||
|
expect(onNewMessage).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not ignore log and info when it is not interactive and debugMode is true', () => {
|
||||||
|
patcher = new ConsolePatcher({
|
||||||
|
onNewMessage,
|
||||||
|
debugMode: true,
|
||||||
|
interactive: false,
|
||||||
|
});
|
||||||
|
patcher.patch();
|
||||||
|
|
||||||
|
console.log('test log');
|
||||||
|
expect(onNewMessage).toHaveBeenCalledWith({
|
||||||
|
type: 'log',
|
||||||
|
content: 'test log',
|
||||||
|
count: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.info('test info');
|
||||||
|
expect(onNewMessage).toHaveBeenCalledWith({
|
||||||
|
type: 'info',
|
||||||
|
content: 'test info',
|
||||||
|
count: 1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not ignore log and info when it is interactive', () => {
|
||||||
|
patcher = new ConsolePatcher({
|
||||||
|
onNewMessage,
|
||||||
|
debugMode: false,
|
||||||
|
interactive: true,
|
||||||
|
});
|
||||||
|
patcher.patch();
|
||||||
|
|
||||||
|
console.log('test log');
|
||||||
|
expect(onNewMessage).toHaveBeenCalledWith({
|
||||||
|
type: 'log',
|
||||||
|
content: 'test log',
|
||||||
|
count: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.info('test info');
|
||||||
|
expect(onNewMessage).toHaveBeenCalledWith({
|
||||||
|
type: 'info',
|
||||||
|
content: 'test info',
|
||||||
|
count: 1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when stderr is false', () => {
|
||||||
|
it('should call onNewMessage for log, warn, error, and info', () => {
|
||||||
|
patcher = new ConsolePatcher({
|
||||||
|
onNewMessage,
|
||||||
|
debugMode: false,
|
||||||
|
stderr: false,
|
||||||
|
});
|
||||||
|
patcher.patch();
|
||||||
|
|
||||||
|
console.log('test log');
|
||||||
|
expect(onNewMessage).toHaveBeenCalledWith({
|
||||||
|
type: 'log',
|
||||||
|
content: 'test log',
|
||||||
|
count: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.warn('test warn');
|
||||||
|
expect(onNewMessage).toHaveBeenCalledWith({
|
||||||
|
type: 'warn',
|
||||||
|
content: 'test warn',
|
||||||
|
count: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.error('test error');
|
||||||
|
expect(onNewMessage).toHaveBeenCalledWith({
|
||||||
|
type: 'error',
|
||||||
|
content: 'test error',
|
||||||
|
count: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.info('test info');
|
||||||
|
expect(onNewMessage).toHaveBeenCalledWith({
|
||||||
|
type: 'info',
|
||||||
|
content: 'test info',
|
||||||
|
count: 1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not call onNewMessage for debug when debugMode is false', () => {
|
||||||
|
patcher = new ConsolePatcher({
|
||||||
|
onNewMessage,
|
||||||
|
debugMode: false,
|
||||||
|
stderr: false,
|
||||||
|
});
|
||||||
|
patcher.patch();
|
||||||
|
|
||||||
|
console.debug('test debug');
|
||||||
|
expect(onNewMessage).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onNewMessage for debug when debugMode is true', () => {
|
||||||
|
patcher = new ConsolePatcher({
|
||||||
|
onNewMessage,
|
||||||
|
debugMode: true,
|
||||||
|
stderr: false,
|
||||||
|
});
|
||||||
|
patcher.patch();
|
||||||
|
|
||||||
|
console.debug('test debug');
|
||||||
|
expect(onNewMessage).toHaveBeenCalledWith({
|
||||||
|
type: 'debug',
|
||||||
|
content: 'test debug',
|
||||||
|
count: 1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should format multiple arguments using util.format', () => {
|
||||||
|
patcher = new ConsolePatcher({
|
||||||
|
onNewMessage,
|
||||||
|
debugMode: false,
|
||||||
|
stderr: false,
|
||||||
|
});
|
||||||
|
patcher.patch();
|
||||||
|
|
||||||
|
console.log('test %s %d', 'string', 123);
|
||||||
|
expect(onNewMessage).toHaveBeenCalledWith({
|
||||||
|
type: 'log',
|
||||||
|
content: 'test string 123',
|
||||||
|
count: 1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when stderr is true', () => {
|
||||||
|
it('should redirect warn and error to originalConsoleError', () => {
|
||||||
|
const spyError = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
patcher = new ConsolePatcher({ debugMode: false, stderr: true });
|
||||||
|
patcher.patch();
|
||||||
|
|
||||||
|
console.warn('test warn');
|
||||||
|
expect(spyError).toHaveBeenCalledWith('test warn');
|
||||||
|
|
||||||
|
console.error('test error');
|
||||||
|
expect(spyError).toHaveBeenCalledWith('test error');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should redirect log and info to originalConsoleError when debugMode is true', () => {
|
||||||
|
const spyError = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
patcher = new ConsolePatcher({ debugMode: true, stderr: true });
|
||||||
|
patcher.patch();
|
||||||
|
|
||||||
|
console.log('test log');
|
||||||
|
expect(spyError).toHaveBeenCalledWith('test log');
|
||||||
|
|
||||||
|
console.info('test info');
|
||||||
|
expect(spyError).toHaveBeenCalledWith('test info');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore debug when debugMode is false', () => {
|
||||||
|
const spyError = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
patcher = new ConsolePatcher({ debugMode: false, stderr: true });
|
||||||
|
patcher.patch();
|
||||||
|
|
||||||
|
console.debug('test debug');
|
||||||
|
expect(spyError).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should redirect debug to originalConsoleError when debugMode is true', () => {
|
||||||
|
const spyError = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
patcher = new ConsolePatcher({ debugMode: true, stderr: true });
|
||||||
|
patcher.patch();
|
||||||
|
|
||||||
|
console.debug('test debug');
|
||||||
|
expect(spyError).toHaveBeenCalledWith('test debug');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -13,6 +13,7 @@ interface ConsolePatcherParams {
|
|||||||
onNewMessage?: (message: Omit<ConsoleMessageItem, 'id'>) => void;
|
onNewMessage?: (message: Omit<ConsoleMessageItem, 'id'>) => void;
|
||||||
debugMode: boolean;
|
debugMode: boolean;
|
||||||
stderr?: boolean;
|
stderr?: boolean;
|
||||||
|
interactive?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ConsolePatcher {
|
export class ConsolePatcher {
|
||||||
@@ -49,12 +50,19 @@ export class ConsolePatcher {
|
|||||||
private patchConsoleMethod =
|
private patchConsoleMethod =
|
||||||
(type: 'log' | 'warn' | 'error' | 'debug' | 'info') =>
|
(type: 'log' | 'warn' | 'error' | 'debug' | 'info') =>
|
||||||
(...args: unknown[]) => {
|
(...args: unknown[]) => {
|
||||||
if (this.params.stderr) {
|
// When it is non interactive mode, do not show info logging unless
|
||||||
if (type !== 'debug' || this.params.debugMode) {
|
// it is debug mode. default to true if it is undefined.
|
||||||
this.originalConsoleError(this.formatArgs(args));
|
if (this.params.interactive === false) {
|
||||||
|
if ((type === 'info' || type === 'log') && !this.params.debugMode) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
if (type !== 'debug' || this.params.debugMode) {
|
// When it is in the debug mode, redirect console output to stderr
|
||||||
|
// depending on if it is stderr only mode.
|
||||||
|
if (type !== 'debug' || this.params.debugMode) {
|
||||||
|
if (this.params.stderr) {
|
||||||
|
this.originalConsoleError(this.formatArgs(args));
|
||||||
|
} else {
|
||||||
this.params.onNewMessage?.({
|
this.params.onNewMessage?.({
|
||||||
type,
|
type,
|
||||||
content: this.formatArgs(args),
|
content: this.formatArgs(args),
|
||||||
|
|||||||
@@ -83,18 +83,55 @@ async function bundle() {
|
|||||||
}
|
}
|
||||||
fs.cpSync(srcThirdParty, destThirdParty, {
|
fs.cpSync(srcThirdParty, destThirdParty, {
|
||||||
recursive: true,
|
recursive: true,
|
||||||
filter: (src) => {
|
|
||||||
// Skip large/unnecessary bundles that are either explicitly excluded
|
|
||||||
// or not required for the browser agent functionality.
|
|
||||||
return (
|
|
||||||
!src.includes('lighthouse-devtools-mcp-bundle.js') &&
|
|
||||||
!src.includes('devtools-formatter-worker.js')
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
console.warn(`Warning: third_party assets not found at ${srcThirdParty}`);
|
console.warn(`Warning: third_party assets not found at ${srcThirdParty}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Copy watchdog scripts and dependencies
|
||||||
|
const srcTelemetry = path.resolve(
|
||||||
|
__dirname,
|
||||||
|
'../../../node_modules/chrome-devtools-mcp/build/src/telemetry',
|
||||||
|
);
|
||||||
|
const destWatchdog = path.resolve(
|
||||||
|
__dirname,
|
||||||
|
'../dist/bundled/watchdog',
|
||||||
|
);
|
||||||
|
if (fs.existsSync(srcTelemetry)) {
|
||||||
|
fs.mkdirSync(destWatchdog, { recursive: true });
|
||||||
|
// Copy main watchdog directory
|
||||||
|
fs.cpSync(path.join(srcTelemetry, 'watchdog'), destWatchdog, { recursive: true });
|
||||||
|
// Copy shared types needed by watchdog
|
||||||
|
fs.copyFileSync(
|
||||||
|
path.join(srcTelemetry, 'types.js'),
|
||||||
|
path.resolve(__dirname, '../dist/bundled/types.js')
|
||||||
|
);
|
||||||
|
// Copy logger needed by watchdog
|
||||||
|
fs.copyFileSync(
|
||||||
|
path.join(srcTelemetry, '../logger.js'),
|
||||||
|
path.resolve(__dirname, '../dist/bundled/logger.js')
|
||||||
|
);
|
||||||
|
|
||||||
|
// Patch imports in watchdog files to reflect the flattened structure in dist/bundled/
|
||||||
|
const watchdogFiles = fs.readdirSync(destWatchdog);
|
||||||
|
for (const file of watchdogFiles) {
|
||||||
|
if (file.endsWith('.js')) {
|
||||||
|
const filePath = path.join(destWatchdog, file);
|
||||||
|
let content = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
content = content.replace(/\.\.\/\.\.\/logger\.js/g, '../logger.js');
|
||||||
|
content = content.replace(/\.\.\/types\.js/g, '../types.js');
|
||||||
|
fs.writeFileSync(filePath, content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn(`Warning: telemetry directory not found at ${srcTelemetry}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Patch the bundled file to point to the correct watchdog path
|
||||||
|
// The original code uses new URL("./watchdog/main.js", import.meta.url)
|
||||||
|
// which resolves relative to the bundled file.
|
||||||
|
// Our bundling script copies the watchdog to ./watchdog/main.js relative to the bundle.
|
||||||
|
// So the original code should work IF esbuild doesn't mangle import.meta.url.
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error bundling chrome-devtools-mcp:', error);
|
console.error('Error bundling chrome-devtools-mcp:', error);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|||||||
@@ -386,7 +386,7 @@ export class BrowserManager {
|
|||||||
})
|
})
|
||||||
.join(', ');
|
.join(', ');
|
||||||
mcpArgs.push(
|
mcpArgs.push(
|
||||||
`--chromeArg="--host-rules=MAP * 127.0.0.1, ${exclusionRules}, EXCLUDE 127.0.0.1"`,
|
`--chromeArg=--host-rules=MAP * 127.0.0.1, ${exclusionRules}, EXCLUDE 127.0.0.1`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -427,10 +427,12 @@ export class BrowserManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.mcpTransport.onclose = () => {
|
this.mcpTransport.onclose = () => {
|
||||||
debugLogger.error(
|
if (this.rawMcpClient) {
|
||||||
'chrome-devtools-mcp transport closed unexpectedly. ' +
|
debugLogger.error(
|
||||||
'The MCP server process may have crashed.',
|
'chrome-devtools-mcp transport closed unexpectedly. ' +
|
||||||
);
|
'The MCP server process may have crashed.',
|
||||||
|
);
|
||||||
|
}
|
||||||
this.rawMcpClient = undefined;
|
this.rawMcpClient = undefined;
|
||||||
};
|
};
|
||||||
this.mcpTransport.onerror = (error: Error) => {
|
this.mcpTransport.onerror = (error: Error) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user