146 lines
4.9 KiB
TypeScript
146 lines
4.9 KiB
TypeScript
import { access, readFile, realpath } from "node:fs/promises";
|
|
import { constants } from "node:fs";
|
|
import { dirname, join, parse, resolve } from "node:path";
|
|
import { z } from "zod";
|
|
|
|
const dockerCommand = [
|
|
"docker",
|
|
"run",
|
|
"-i",
|
|
"--rm",
|
|
"--init",
|
|
"--pull=always",
|
|
"mcr.microsoft.com/playwright/mcp",
|
|
];
|
|
const directPackageManifest = z.object({
|
|
name: z.literal("@playwright/mcp"),
|
|
bin: z.string(),
|
|
});
|
|
const namedPackageManifest = z.object({
|
|
name: z.literal("@playwright/mcp"),
|
|
bin: z.object({ "playwright-mcp": z.string() }),
|
|
});
|
|
const playwrightPackageManifest = z.object({
|
|
name: z.literal("playwright"),
|
|
bin: z.object({ playwright: z.string() }),
|
|
});
|
|
const playwrightTestPackageManifest = z.object({
|
|
name: z.literal("@playwright/test"),
|
|
bin: z.object({ playwright: z.string() }),
|
|
dependencies: z.object({ playwright: z.string() }),
|
|
});
|
|
|
|
async function packageExecutable(packageRoot: string) {
|
|
try {
|
|
const input = JSON.parse(await readFile(join(packageRoot, "package.json"), "utf8"));
|
|
const direct = directPackageManifest.safeParse(input);
|
|
const bin = direct.success
|
|
? direct.data.bin
|
|
: namedPackageManifest.parse(input).bin["playwright-mcp"];
|
|
const executable = resolve(packageRoot, bin);
|
|
await access(executable, constants.X_OK);
|
|
return executable;
|
|
} catch {
|
|
return;
|
|
}
|
|
}
|
|
|
|
async function ordinaryPackageExecutable(packageRoot: string) {
|
|
try {
|
|
const manifest = playwrightPackageManifest.parse(
|
|
JSON.parse(await readFile(join(packageRoot, "package.json"), "utf8")),
|
|
);
|
|
const executable = resolve(packageRoot, manifest.bin.playwright);
|
|
await access(executable, constants.X_OK);
|
|
return executable;
|
|
} catch {
|
|
return;
|
|
}
|
|
}
|
|
|
|
async function playwrightDependency(packageRoot: string) {
|
|
let current = await realpath(packageRoot);
|
|
const root = parse(current).root;
|
|
while (true) {
|
|
const executable = await ordinaryPackageExecutable(
|
|
join(current, "node_modules/playwright"),
|
|
);
|
|
if (executable) return executable;
|
|
if (current === root) return;
|
|
current = dirname(current);
|
|
}
|
|
}
|
|
|
|
async function testPackageExecutable(packageRoot: string) {
|
|
try {
|
|
const manifest = playwrightTestPackageManifest.parse(
|
|
JSON.parse(await readFile(join(packageRoot, "package.json"), "utf8")),
|
|
);
|
|
if (!(await playwrightDependency(packageRoot))) return;
|
|
const executable = resolve(packageRoot, manifest.bin.playwright);
|
|
await access(executable, constants.X_OK);
|
|
return executable;
|
|
} catch {
|
|
return;
|
|
}
|
|
}
|
|
|
|
async function verifiedPathExecutable(candidate: string, kind: "mcp" | "playwright") {
|
|
try {
|
|
await access(candidate, constants.X_OK);
|
|
const target = await realpath(candidate);
|
|
let directory = dirname(target);
|
|
const root = parse(directory).root;
|
|
while (true) {
|
|
const executable =
|
|
kind === "mcp"
|
|
? await packageExecutable(directory)
|
|
: ((await ordinaryPackageExecutable(directory)) ??
|
|
(await testPackageExecutable(directory)));
|
|
if (executable && (await realpath(executable)) === target) return candidate;
|
|
if (directory === root) return;
|
|
directory = dirname(directory);
|
|
}
|
|
} catch {
|
|
return;
|
|
}
|
|
}
|
|
|
|
export async function playwrightMcpCommand(
|
|
directory: string,
|
|
path = process.env.PATH ?? "",
|
|
): Promise<string[]> {
|
|
let current = resolve(directory);
|
|
const root = parse(current).root;
|
|
while (true) {
|
|
const executable = await packageExecutable(join(current, "node_modules/@playwright/mcp"));
|
|
if (executable) return [executable];
|
|
if (current === root) break;
|
|
current = dirname(current);
|
|
}
|
|
|
|
for (const entry of path.split(process.platform === "win32" ? ";" : ":")) {
|
|
if (!entry) continue;
|
|
const executable = await verifiedPathExecutable(join(entry, "playwright-mcp"), "mcp");
|
|
if (executable) return [executable];
|
|
}
|
|
|
|
current = resolve(directory);
|
|
while (true) {
|
|
const playwright = await ordinaryPackageExecutable(
|
|
join(current, "node_modules/playwright"),
|
|
);
|
|
const test = await testPackageExecutable(join(current, "node_modules/@playwright/test"));
|
|
if (playwright || test) return ["npx", "-y", "@playwright/mcp@latest"];
|
|
if (current === root) break;
|
|
current = dirname(current);
|
|
}
|
|
|
|
for (const entry of path.split(process.platform === "win32" ? ";" : ":")) {
|
|
if (!entry) continue;
|
|
const executable = await verifiedPathExecutable(join(entry, "playwright"), "playwright");
|
|
if (executable) return ["npx", "-y", "@playwright/mcp@latest"];
|
|
}
|
|
return [...dockerCommand];
|
|
}
|