From f6cae21bfa53aee28709815cba2e1c7bbf7d34b7 Mon Sep 17 00:00:00 2001 From: StanPonomarev Date: Tue, 15 Sep 2026 10:18:43 +0200 Subject: [PATCH] feat: make playwright optional --- README.md | 10 +++++++++- install-local.sh | 19 ++++++++++++++++--- install.sh | 19 ++++++++++++++++--- src/config.ts | 1 + src/index.ts | 6 ++++-- tests/install-local.test.ts | 12 ++++++++++++ tests/install.test.ts | 19 +++++++++++++++++-- tests/plugin.test.ts | 21 ++++++++++++++++++--- 8 files changed, 93 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 46a7779..ee09f22 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,12 @@ Run the installer without sudo: curl -fsSL https://git.krtss.de/StanPonomarev/stanp-opencode/raw/branch/main/install.sh | bash ``` +Pass `--playwright` to let the plugin automatically configure a Playwright MCP server: + +```sh +curl -fsSL https://git.krtss.de/StanPonomarev/stanp-opencode/raw/branch/main/install.sh | bash -s -- --playwright +``` + Rerun the command to upgrade to the latest published release. ## Install from a local checkout @@ -22,6 +28,8 @@ This requires Bash, jq, and Bun 1.3.14+: bash /path/to/stanp-opencode/install-local.sh ``` +Add `--playwright` to this command to enable automatic Playwright MCP configuration. + The script installs dependencies and builds the checkout before installing it. After either installation, restart `opencode2` or run `opencode2 service restart` for an existing service. Connect OpenAI and select `coordinate`. The required models must be available for the workers to run. @@ -36,7 +44,7 @@ For an installed plugin, use `./plugins/stanp-opencode` as the plugin package pa ## Playwright -The plugin prefers a local Playwright MCP installation. If ordinary Playwright is installed, it can download and run the MCP package with `npx`; otherwise Docker is required for the automatic fallback. Automatically configured servers allow five minutes for startup because the first launch may need to download a package or container image. Docker reuses its cached image on later launches. Existing `playwright` configuration, including its timeout, is preserved. +When installed with `--playwright`, the plugin prefers a local Playwright MCP installation. If ordinary Playwright is installed, it can download and run the MCP package with `npx`; otherwise Docker is required for the automatic fallback. Automatically configured servers allow five minutes for startup because the first launch may need to download a package or container image. Docker reuses its cached image on later launches. Existing `playwright` configuration, including its timeout, is preserved. If `opencode mcp list` reports `MCP error -32001: Request timed out`, prefetch the Docker fallback with `docker pull mcr.microsoft.com/playwright/mcp`, or install `@playwright/mcp` in the project so the plugin can launch it directly. Then reconnect the server from `/mcps` or restart OpenCode. A custom `playwright` entry under `mcp.servers` can also set its own `timeout.startup` in milliseconds. diff --git a/install-local.sh b/install-local.sh index f9888fd..d3f2472 100755 --- a/install-local.sh +++ b/install-local.sh @@ -1,6 +1,15 @@ #!/usr/bin/env bash set -euo pipefail +playwright=false +if (($#)); then + if [[ "$#" -ne 1 || "$1" != "--playwright" ]]; then + printf 'Usage: %s [--playwright]\n' "$0" >&2 + exit 2 + fi + playwright=true +fi + for command in bun jq; do command -v "$command" >/dev/null || { printf 'Required command not found: %s\n' "$command" >&2; exit 1; } done @@ -43,15 +52,19 @@ if [[ ! -e "$config_source" ]]; then config_source="$install/empty-config.json" printf '{}\n' > "$config_source" fi -jq -e ' +jq -e --argjson playwright "$playwright" ' if type != "object" then error("opencode.json must contain an object") else . end | if (.plugins // []) | type != "array" then error("opencode.json plugins must be an array") else . end | .["$schema"] = (.["$schema"] // "https://opencode.ai/config.json") | .plugins = (.plugins // []) - | if any(.plugins[]?; .package == "./plugins/stanp-opencode") then . else + | if any(.plugins[]?; .package == "./plugins/stanp-opencode") then + if $playwright then + .plugins |= map(if .package == "./plugins/stanp-opencode" then .options = ((.options // {}) + { playwright: true }) else . end) + else . end + else .plugins += [{ package: "./plugins/stanp-opencode", - options: { mode: "gpt", provider: "openai", lunaPriority: true } + options: ({ mode: "gpt", provider: "openai", lunaPriority: true } + if $playwright then { playwright: true } else {} end) }] end ' "$config_source" > "$install/opencode.json" diff --git a/install.sh b/install.sh index c3c57d4..fd21c49 100755 --- a/install.sh +++ b/install.sh @@ -1,6 +1,15 @@ #!/usr/bin/env bash set -euo pipefail +playwright=false +if (($#)); then + if [[ "$#" -ne 1 || "$1" != "--playwright" ]]; then + printf 'Usage: %s [--playwright]\n' "$0" >&2 + exit 2 + fi + playwright=true +fi + for command in bun curl jq sha256sum tar; do command -v "$command" >/dev/null || { printf 'Required command not found: %s\n' "$command" >&2; exit 1; } done @@ -57,15 +66,19 @@ if [[ ! -e "$config_source" ]]; then config_source="$install/empty-config.json" printf '{}\n' > "$config_source" fi -jq -e ' +jq -e --argjson playwright "$playwright" ' if type != "object" then error("opencode.json must contain an object") else . end | if (.plugins // []) | type != "array" then error("opencode.json plugins must be an array") else . end | .["$schema"] = (.["$schema"] // "https://opencode.ai/config.json") | .plugins = (.plugins // []) - | if any(.plugins[]?; .package == "./plugins/stanp-opencode") then . else + | if any(.plugins[]?; .package == "./plugins/stanp-opencode") then + if $playwright then + .plugins |= map(if .package == "./plugins/stanp-opencode" then .options = ((.options // {}) + { playwright: true }) else . end) + else . end + else .plugins += [{ package: "./plugins/stanp-opencode", - options: { mode: "gpt", provider: "openai", lunaPriority: true } + options: ({ mode: "gpt", provider: "openai", lunaPriority: true } + if $playwright then { playwright: true } else {} end) }] end ' "$config_source" > "$install/opencode.json" diff --git a/src/config.ts b/src/config.ts index 4c35ef6..0a8b1b4 100644 --- a/src/config.ts +++ b/src/config.ts @@ -17,6 +17,7 @@ const assignment = z.strictObject({ const common = { provider: z.string().trim().min(1).default("openai"), lunaPriority: z.boolean().default(true), + playwright: z.boolean().default(false), }; export const settings = z.discriminatedUnion("mode", [ diff --git a/src/index.ts b/src/index.ts index 3a93cfa..9880f3c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -114,10 +114,12 @@ export default Plugin.define({ const assignments = selected.mode === "gpt" ? gptAgents : selected.agents; let workers: (typeof workerNames)[number][] = []; const active = new Set(); - const playwrightCommand = await playwrightMcpCommand(context.location.directory); + const playwrightCommand = selected.playwright + ? await playwrightMcpCommand(context.location.directory) + : undefined; await context.mcp.transform((editor) => { - if (!editor.get("playwright")) + if (playwrightCommand && !editor.get("playwright")) editor.set("playwright", { type: "local", command: playwrightCommand, diff --git a/tests/install-local.test.ts b/tests/install-local.test.ts index 83a70cc..6be6993 100644 --- a/tests/install-local.test.ts +++ b/tests/install-local.test.ts @@ -199,6 +199,18 @@ test("reinstall replaces bundles while preserving existing settings and plugin o expect(await readFile(join(setup.installed, "index.js"), "utf8")).toContain('return "second"'); }); +test("enables Playwright only when requested", async () => { + const setup = await fixture(); + const result = await run( + ["bash", join(setup.checkout, "install-local.sh"), "--playwright"], + setup.cwd, + setup.env, + ); + expect(result, result.stderr).toMatchObject({ exitCode: 0 }); + const installed = JSON.parse(await readFile(join(setup.config, "opencode.json"), "utf8")); + expect(installed.plugins.at(-1).options.playwright).toBe(true); +}); + for (const mode of ["dependency", "build", "validation", "config"] as const) { test(`${mode} failure leaves the previous installation and configuration untouched`, async () => { const invalidConfig = mode === "config" ? { plugins: "not-an-array" } : undefined; diff --git a/tests/install.test.ts b/tests/install.test.ts index ca5be83..0dce8ca 100644 --- a/tests/install.test.ts +++ b/tests/install.test.ts @@ -44,7 +44,11 @@ async function run(command: string[], cwd: string, env: NodeJS.ProcessEnv) { return { exitCode, stdout, stderr }; } -async function install(mode: "native" | "checksum" | "legacy", configured = false) { +async function install( + mode: "native" | "checksum" | "legacy", + configured = false, + playwright = false, +) { const directory = await mkdtemp(join(tmpdir(), "stanp-install-")); directories.push(directory); const config = @@ -187,7 +191,11 @@ exec sha256sum "$@" ), ]); await Promise.all([chmod(join(bin, "curl"), 0o755), chmod(join(bin, "sha256sum"), 0o755)]); - const result = await run(["bash", installer], directory, env); + const result = await run( + ["bash", installer, ...(playwright ? ["--playwright"] : [])], + directory, + env, + ); return { directory, config, env, result }; } @@ -249,6 +257,13 @@ test("preserves an existing stanp-opencode configuration", async () => { expect(installed.permissions).toEqual(jsonConfig.permissions); }, 60_000); +test("enables Playwright only when requested", async () => { + const { config, result } = await install("native", false, true); + expect(result, result.stderr).toMatchObject({ exitCode: 0 }); + const installed = JSON.parse(await readFile(join(config, "opencode.json"), "utf8")); + expect(installed.plugins.at(-1).options.playwright).toBe(true); +}, 60_000); + for (const mode of ["checksum", "legacy"] as const) { test(`${mode} failure preserves the installed plugin in a custom path containing spaces`, async () => { const { directory, config, env, result } = await install(mode); diff --git a/tests/plugin.test.ts b/tests/plugin.test.ts index 1ae4611..4ed9321 100644 --- a/tests/plugin.test.ts +++ b/tests/plugin.test.ts @@ -442,7 +442,13 @@ test("playwright MCP prefers direct MCP, uses npx for Playwright, and preserves await writeFile(executable, "#!/usr/bin/env node\n"); await chmod(executable, 0o755); - const local = await host(allModels, { provider: "test" }, {}, [], localProject); + const local = await host( + allModels, + { provider: "test", playwright: true }, + {}, + [], + localProject, + ); expect(local.mcpServers.get("playwright")).toEqual({ type: "local", command: [executable], @@ -475,7 +481,13 @@ test("playwright MCP prefers direct MCP, uses npx for Playwright, and preserves ); await writeFile(ordinaryExecutable, "#!/usr/bin/env node\n"); await chmod(ordinaryExecutable, 0o755); - const ordinary = await host(allModels, { provider: "test" }, {}, [], ordinaryProject); + const ordinary = await host( + allModels, + { provider: "test", playwright: true }, + {}, + [], + ordinaryProject, + ); expect(ordinary.mcpServers.get("playwright")?.command).toEqual([ "npx", "-y", @@ -534,7 +546,7 @@ test("playwright MCP prefers direct MCP, uses npx for Playwright, and preserves await chmod(fakePlaywright, 0o755); expect((await playwrightMcpCommand(globalProject, ordinaryBin))[0]).toBe("docker"); - const fallback = await host(); + const fallback = await host(allModels, { provider: "test", playwright: true }); expect(fallback.mcpServers.get("playwright")).toEqual({ type: "local", command: ["docker", "run", "-i", "--rm", "--init", "mcr.microsoft.com/playwright/mcp"], @@ -561,6 +573,9 @@ test("playwright MCP prefers direct MCP, uses npx for Playwright, and preserves timeout: { startup: 12_345 }, }); expect(existing.mcpServers.get("docs")?.codemode).toBe(true); + + const disabled = await host(); + expect(disabled.mcpServers.has("playwright")).toBe(false); } finally { if (originalPath === undefined) delete process.env.PATH; else process.env.PATH = originalPath; -- 2.54.0