2025-04-28 16:14:16 -07:00
|
|
|
/**
|
|
|
|
* Copyright (c) Microsoft Corporation.
|
|
|
|
*
|
|
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
* you may not use this file except in compliance with the License.
|
|
|
|
* You may obtain a copy of the License at
|
|
|
|
*
|
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
*
|
|
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
* See the License for the specific language governing permissions and
|
|
|
|
* limitations under the License.
|
|
|
|
*/
|
|
|
|
|
2025-04-28 16:35:33 -07:00
|
|
|
import fs from 'fs';
|
2025-04-28 16:14:16 -07:00
|
|
|
import net from 'net';
|
|
|
|
import os from 'os';
|
2025-04-28 16:35:33 -07:00
|
|
|
import path from 'path';
|
2025-04-29 19:51:00 -07:00
|
|
|
import { devices } from 'playwright';
|
2025-04-28 16:35:33 -07:00
|
|
|
|
2025-04-30 23:06:56 +02:00
|
|
|
import type { Config, ToolCapability } from '../config.js';
|
2025-04-29 19:51:00 -07:00
|
|
|
import type { BrowserContextOptions, LaunchOptions } from 'playwright';
|
2025-05-02 10:57:31 +02:00
|
|
|
import { sanitizeForFilePath } from './tools/utils.js';
|
2025-04-28 16:14:16 -07:00
|
|
|
|
2025-04-28 20:17:16 -07:00
|
|
|
export type CLIOptions = {
|
2025-05-13 14:40:03 -07:00
|
|
|
allowedOrigins?: string[];
|
|
|
|
blockedOrigins?: string[];
|
|
|
|
blockServiceWorkers?: boolean;
|
2025-04-28 20:17:16 -07:00
|
|
|
browser?: string;
|
|
|
|
caps?: string;
|
|
|
|
cdpEndpoint?: string;
|
2025-05-13 14:40:03 -07:00
|
|
|
config?: string;
|
|
|
|
device?: string;
|
2025-04-28 20:17:16 -07:00
|
|
|
executablePath?: string;
|
|
|
|
headless?: boolean;
|
|
|
|
host?: string;
|
2025-05-13 14:40:03 -07:00
|
|
|
ignoreHttpsErrors?: boolean;
|
|
|
|
isolated?: boolean;
|
2025-05-13 16:17:45 -07:00
|
|
|
imageResponses: boolean;
|
|
|
|
sandbox: boolean;
|
2025-05-13 14:40:03 -07:00
|
|
|
outputDir?: string;
|
|
|
|
port?: number;
|
|
|
|
proxyBypass?: string;
|
|
|
|
proxyServer?: string;
|
|
|
|
storageState?: string;
|
|
|
|
userAgent?: string;
|
|
|
|
userDataDir?: string;
|
|
|
|
viewportSize?: string;
|
|
|
|
vision?: boolean;
|
2025-04-28 16:14:16 -07:00
|
|
|
};
|
|
|
|
|
2025-04-28 20:17:16 -07:00
|
|
|
const defaultConfig: Config = {
|
|
|
|
browser: {
|
|
|
|
browserName: 'chromium',
|
|
|
|
launchOptions: {
|
|
|
|
channel: 'chrome',
|
|
|
|
headless: os.platform() === 'linux' && !process.env.DISPLAY,
|
2025-05-13 15:30:02 -07:00
|
|
|
chromiumSandbox: true,
|
2025-04-28 20:17:16 -07:00
|
|
|
},
|
|
|
|
contextOptions: {
|
|
|
|
viewport: null,
|
|
|
|
},
|
|
|
|
},
|
2025-05-05 11:28:14 -07:00
|
|
|
network: {
|
|
|
|
allowedOrigins: undefined,
|
|
|
|
blockedOrigins: undefined,
|
|
|
|
},
|
2025-04-28 20:17:16 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
export async function resolveConfig(cliOptions: CLIOptions): Promise<Config> {
|
|
|
|
const config = await loadConfig(cliOptions.config);
|
|
|
|
const cliOverrides = await configFromCLIOptions(cliOptions);
|
|
|
|
return mergeConfig(defaultConfig, mergeConfig(config, cliOverrides));
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function configFromCLIOptions(cliOptions: CLIOptions): Promise<Config> {
|
2025-04-28 16:14:16 -07:00
|
|
|
let browserName: 'chromium' | 'firefox' | 'webkit';
|
|
|
|
let channel: string | undefined;
|
2025-04-28 20:17:16 -07:00
|
|
|
switch (cliOptions.browser) {
|
2025-04-28 16:14:16 -07:00
|
|
|
case 'chrome':
|
|
|
|
case 'chrome-beta':
|
|
|
|
case 'chrome-canary':
|
|
|
|
case 'chrome-dev':
|
|
|
|
case 'chromium':
|
|
|
|
case 'msedge':
|
|
|
|
case 'msedge-beta':
|
|
|
|
case 'msedge-canary':
|
|
|
|
case 'msedge-dev':
|
|
|
|
browserName = 'chromium';
|
2025-04-28 20:17:16 -07:00
|
|
|
channel = cliOptions.browser;
|
2025-04-28 16:14:16 -07:00
|
|
|
break;
|
|
|
|
case 'firefox':
|
|
|
|
browserName = 'firefox';
|
|
|
|
break;
|
|
|
|
case 'webkit':
|
|
|
|
browserName = 'webkit';
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
browserName = 'chromium';
|
|
|
|
channel = 'chrome';
|
|
|
|
}
|
|
|
|
|
2025-05-13 14:40:03 -07:00
|
|
|
// Launch options
|
2025-04-28 16:14:16 -07:00
|
|
|
const launchOptions: LaunchOptions = {
|
|
|
|
channel,
|
2025-04-28 20:17:16 -07:00
|
|
|
executablePath: cliOptions.executablePath,
|
2025-04-30 08:41:19 -07:00
|
|
|
headless: cliOptions.headless,
|
2025-04-28 16:14:16 -07:00
|
|
|
};
|
|
|
|
|
2025-05-13 15:30:02 -07:00
|
|
|
if (browserName === 'chromium') {
|
2025-05-09 18:01:17 -07:00
|
|
|
(launchOptions as any).cdpPort = await findFreePort();
|
2025-05-13 16:17:45 -07:00
|
|
|
if (!cliOptions.sandbox) {
|
|
|
|
// --no-sandbox was passed, disable the sandbox
|
2025-05-13 15:30:02 -07:00
|
|
|
launchOptions.chromiumSandbox = false;
|
2025-05-13 16:17:45 -07:00
|
|
|
}
|
2025-05-13 15:30:02 -07:00
|
|
|
}
|
2025-04-28 16:14:16 -07:00
|
|
|
|
2025-05-13 14:40:03 -07:00
|
|
|
if (cliOptions.proxyServer) {
|
|
|
|
launchOptions.proxy = {
|
|
|
|
server: cliOptions.proxyServer
|
|
|
|
};
|
|
|
|
if (cliOptions.proxyBypass)
|
|
|
|
launchOptions.proxy.bypass = cliOptions.proxyBypass;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Context options
|
2025-05-13 13:14:04 -07:00
|
|
|
const contextOptions: BrowserContextOptions = cliOptions.device ? devices[cliOptions.device] : {};
|
|
|
|
if (cliOptions.storageState)
|
|
|
|
contextOptions.storageState = cliOptions.storageState;
|
2025-04-29 19:51:00 -07:00
|
|
|
|
2025-05-13 14:40:03 -07:00
|
|
|
if (cliOptions.userAgent)
|
|
|
|
contextOptions.userAgent = cliOptions.userAgent;
|
|
|
|
|
|
|
|
if (cliOptions.viewportSize) {
|
|
|
|
try {
|
|
|
|
const [width, height] = cliOptions.viewportSize.split(',').map(n => +n);
|
|
|
|
if (isNaN(width) || isNaN(height))
|
|
|
|
throw new Error('bad values');
|
|
|
|
contextOptions.viewport = { width, height };
|
|
|
|
} catch (e) {
|
|
|
|
throw new Error('Invalid viewport size format: use "width,height", for example --viewport-size="800,600"');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (cliOptions.ignoreHttpsErrors)
|
|
|
|
contextOptions.ignoreHTTPSErrors = true;
|
|
|
|
|
|
|
|
if (cliOptions.blockServiceWorkers)
|
|
|
|
contextOptions.serviceWorkers = 'block';
|
|
|
|
|
2025-05-13 16:17:45 -07:00
|
|
|
const result: Config = {
|
2025-04-28 20:17:16 -07:00
|
|
|
browser: {
|
|
|
|
browserName,
|
2025-05-13 13:14:04 -07:00
|
|
|
isolated: cliOptions.isolated,
|
2025-05-05 08:23:24 -07:00
|
|
|
userDataDir: cliOptions.userDataDir,
|
2025-04-28 20:17:16 -07:00
|
|
|
launchOptions,
|
2025-04-29 19:51:00 -07:00
|
|
|
contextOptions,
|
2025-04-28 20:17:16 -07:00
|
|
|
cdpEndpoint: cliOptions.cdpEndpoint,
|
|
|
|
},
|
|
|
|
server: {
|
|
|
|
port: cliOptions.port,
|
|
|
|
host: cliOptions.host,
|
|
|
|
},
|
|
|
|
capabilities: cliOptions.caps?.split(',').map((c: string) => c.trim() as ToolCapability),
|
|
|
|
vision: !!cliOptions.vision,
|
2025-05-05 11:28:14 -07:00
|
|
|
network: {
|
|
|
|
allowedOrigins: cliOptions.allowedOrigins,
|
|
|
|
blockedOrigins: cliOptions.blockedOrigins,
|
|
|
|
},
|
2025-05-04 12:11:17 +09:00
|
|
|
outputDir: cliOptions.outputDir,
|
2025-04-28 16:14:16 -07:00
|
|
|
};
|
2025-05-13 16:17:45 -07:00
|
|
|
|
|
|
|
if (!cliOptions.imageResponses) {
|
|
|
|
// --no-image-responses was passed, disable image responses
|
|
|
|
result.noImageResponses = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return result;
|
2025-04-28 16:14:16 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
async function findFreePort() {
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
const server = net.createServer();
|
|
|
|
server.listen(0, () => {
|
|
|
|
const { port } = server.address() as net.AddressInfo;
|
|
|
|
server.close(() => resolve(port));
|
|
|
|
});
|
|
|
|
server.on('error', reject);
|
|
|
|
});
|
|
|
|
}
|
2025-04-28 16:35:33 -07:00
|
|
|
|
2025-04-28 20:17:16 -07:00
|
|
|
async function loadConfig(configFile: string | undefined): Promise<Config> {
|
|
|
|
if (!configFile)
|
|
|
|
return {};
|
|
|
|
|
|
|
|
try {
|
|
|
|
return JSON.parse(await fs.promises.readFile(configFile, 'utf8'));
|
|
|
|
} catch (error) {
|
2025-04-29 08:53:03 -07:00
|
|
|
throw new Error(`Failed to load config file: ${configFile}, ${error}`);
|
2025-04-28 20:17:16 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-04-28 16:35:33 -07:00
|
|
|
export async function outputFile(config: Config, name: string): Promise<string> {
|
|
|
|
const result = config.outputDir ?? os.tmpdir();
|
|
|
|
await fs.promises.mkdir(result, { recursive: true });
|
|
|
|
const fileName = sanitizeForFilePath(name);
|
|
|
|
return path.join(result, fileName);
|
|
|
|
}
|
2025-04-28 20:17:16 -07:00
|
|
|
|
2025-04-30 08:41:19 -07:00
|
|
|
function pickDefined<T extends object>(obj: T | undefined): Partial<T> {
|
|
|
|
return Object.fromEntries(
|
|
|
|
Object.entries(obj ?? {}).filter(([_, v]) => v !== undefined)
|
|
|
|
) as Partial<T>;
|
|
|
|
}
|
|
|
|
|
2025-04-28 20:17:16 -07:00
|
|
|
function mergeConfig(base: Config, overrides: Config): Config {
|
|
|
|
const browser: Config['browser'] = {
|
2025-04-30 08:41:19 -07:00
|
|
|
...pickDefined(base.browser),
|
|
|
|
...pickDefined(overrides.browser),
|
2025-04-28 20:17:16 -07:00
|
|
|
launchOptions: {
|
2025-04-30 08:41:19 -07:00
|
|
|
...pickDefined(base.browser?.launchOptions),
|
|
|
|
...pickDefined(overrides.browser?.launchOptions),
|
2025-04-28 20:17:16 -07:00
|
|
|
...{ assistantMode: true },
|
|
|
|
},
|
|
|
|
contextOptions: {
|
2025-04-30 08:41:19 -07:00
|
|
|
...pickDefined(base.browser?.contextOptions),
|
|
|
|
...pickDefined(overrides.browser?.contextOptions),
|
2025-04-28 20:17:16 -07:00
|
|
|
},
|
|
|
|
};
|
|
|
|
|
2025-05-09 14:16:04 +02:00
|
|
|
if (browser.browserName !== 'chromium' && browser.launchOptions)
|
2025-04-30 08:41:19 -07:00
|
|
|
delete browser.launchOptions.channel;
|
|
|
|
|
2025-04-28 20:17:16 -07:00
|
|
|
return {
|
2025-04-30 08:41:19 -07:00
|
|
|
...pickDefined(base),
|
|
|
|
...pickDefined(overrides),
|
2025-04-28 20:17:16 -07:00
|
|
|
browser,
|
2025-05-05 11:28:14 -07:00
|
|
|
network: {
|
|
|
|
...pickDefined(base.network),
|
|
|
|
...pickDefined(overrides.network),
|
|
|
|
},
|
2025-04-28 20:17:16 -07:00
|
|
|
};
|
|
|
|
}
|