Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6cae21bfa |
@@ -12,6 +12,12 @@ Run the installer without sudo:
|
|||||||
curl -fsSL https://git.krtss.de/StanPonomarev/stanp-opencode/raw/branch/main/install.sh | bash
|
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.
|
Rerun the command to upgrade to the latest published release.
|
||||||
|
|
||||||
## Install from a local checkout
|
## 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
|
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.
|
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.
|
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
|
## 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.
|
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.
|
||||||
|
|
||||||
|
|||||||
+16
-3
@@ -1,6 +1,15 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
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
|
for command in bun jq; do
|
||||||
command -v "$command" >/dev/null || { printf 'Required command not found: %s\n' "$command" >&2; exit 1; }
|
command -v "$command" >/dev/null || { printf 'Required command not found: %s\n' "$command" >&2; exit 1; }
|
||||||
done
|
done
|
||||||
@@ -43,15 +52,19 @@ if [[ ! -e "$config_source" ]]; then
|
|||||||
config_source="$install/empty-config.json"
|
config_source="$install/empty-config.json"
|
||||||
printf '{}\n' > "$config_source"
|
printf '{}\n' > "$config_source"
|
||||||
fi
|
fi
|
||||||
jq -e '
|
jq -e --argjson playwright "$playwright" '
|
||||||
if type != "object" then error("opencode.json must contain an object") else . end
|
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
|
| if (.plugins // []) | type != "array" then error("opencode.json plugins must be an array") else . end
|
||||||
| .["$schema"] = (.["$schema"] // "https://opencode.ai/config.json")
|
| .["$schema"] = (.["$schema"] // "https://opencode.ai/config.json")
|
||||||
| .plugins = (.plugins // [])
|
| .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 += [{
|
.plugins += [{
|
||||||
package: "./plugins/stanp-opencode",
|
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
|
end
|
||||||
' "$config_source" > "$install/opencode.json"
|
' "$config_source" > "$install/opencode.json"
|
||||||
|
|||||||
+16
-3
@@ -1,6 +1,15 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
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
|
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; }
|
command -v "$command" >/dev/null || { printf 'Required command not found: %s\n' "$command" >&2; exit 1; }
|
||||||
done
|
done
|
||||||
@@ -57,15 +66,19 @@ if [[ ! -e "$config_source" ]]; then
|
|||||||
config_source="$install/empty-config.json"
|
config_source="$install/empty-config.json"
|
||||||
printf '{}\n' > "$config_source"
|
printf '{}\n' > "$config_source"
|
||||||
fi
|
fi
|
||||||
jq -e '
|
jq -e --argjson playwright "$playwright" '
|
||||||
if type != "object" then error("opencode.json must contain an object") else . end
|
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
|
| if (.plugins // []) | type != "array" then error("opencode.json plugins must be an array") else . end
|
||||||
| .["$schema"] = (.["$schema"] // "https://opencode.ai/config.json")
|
| .["$schema"] = (.["$schema"] // "https://opencode.ai/config.json")
|
||||||
| .plugins = (.plugins // [])
|
| .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 += [{
|
.plugins += [{
|
||||||
package: "./plugins/stanp-opencode",
|
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
|
end
|
||||||
' "$config_source" > "$install/opencode.json"
|
' "$config_source" > "$install/opencode.json"
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const assignment = z.strictObject({
|
|||||||
const common = {
|
const common = {
|
||||||
provider: z.string().trim().min(1).default("openai"),
|
provider: z.string().trim().min(1).default("openai"),
|
||||||
lunaPriority: z.boolean().default(true),
|
lunaPriority: z.boolean().default(true),
|
||||||
|
playwright: z.boolean().default(false),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const settings = z.discriminatedUnion("mode", [
|
export const settings = z.discriminatedUnion("mode", [
|
||||||
|
|||||||
+4
-2
@@ -114,10 +114,12 @@ export default Plugin.define({
|
|||||||
const assignments = selected.mode === "gpt" ? gptAgents : selected.agents;
|
const assignments = selected.mode === "gpt" ? gptAgents : selected.agents;
|
||||||
let workers: (typeof workerNames)[number][] = [];
|
let workers: (typeof workerNames)[number][] = [];
|
||||||
const active = new Set<string>();
|
const active = new Set<string>();
|
||||||
const playwrightCommand = await playwrightMcpCommand(context.location.directory);
|
const playwrightCommand = selected.playwright
|
||||||
|
? await playwrightMcpCommand(context.location.directory)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
await context.mcp.transform((editor) => {
|
await context.mcp.transform((editor) => {
|
||||||
if (!editor.get("playwright"))
|
if (playwrightCommand && !editor.get("playwright"))
|
||||||
editor.set("playwright", {
|
editor.set("playwright", {
|
||||||
type: "local",
|
type: "local",
|
||||||
command: playwrightCommand,
|
command: playwrightCommand,
|
||||||
|
|||||||
@@ -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"');
|
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) {
|
for (const mode of ["dependency", "build", "validation", "config"] as const) {
|
||||||
test(`${mode} failure leaves the previous installation and configuration untouched`, async () => {
|
test(`${mode} failure leaves the previous installation and configuration untouched`, async () => {
|
||||||
const invalidConfig = mode === "config" ? { plugins: "not-an-array" } : undefined;
|
const invalidConfig = mode === "config" ? { plugins: "not-an-array" } : undefined;
|
||||||
|
|||||||
+17
-2
@@ -44,7 +44,11 @@ async function run(command: string[], cwd: string, env: NodeJS.ProcessEnv) {
|
|||||||
return { exitCode, stdout, stderr };
|
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-"));
|
const directory = await mkdtemp(join(tmpdir(), "stanp-install-"));
|
||||||
directories.push(directory);
|
directories.push(directory);
|
||||||
const config =
|
const config =
|
||||||
@@ -187,7 +191,11 @@ exec sha256sum "$@"
|
|||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
await Promise.all([chmod(join(bin, "curl"), 0o755), chmod(join(bin, "sha256sum"), 0o755)]);
|
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 };
|
return { directory, config, env, result };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,6 +257,13 @@ test("preserves an existing stanp-opencode configuration", async () => {
|
|||||||
expect(installed.permissions).toEqual(jsonConfig.permissions);
|
expect(installed.permissions).toEqual(jsonConfig.permissions);
|
||||||
}, 60_000);
|
}, 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) {
|
for (const mode of ["checksum", "legacy"] as const) {
|
||||||
test(`${mode} failure preserves the installed plugin in a custom path containing spaces`, async () => {
|
test(`${mode} failure preserves the installed plugin in a custom path containing spaces`, async () => {
|
||||||
const { directory, config, env, result } = await install(mode);
|
const { directory, config, env, result } = await install(mode);
|
||||||
|
|||||||
+18
-3
@@ -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 writeFile(executable, "#!/usr/bin/env node\n");
|
||||||
await chmod(executable, 0o755);
|
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({
|
expect(local.mcpServers.get("playwright")).toEqual({
|
||||||
type: "local",
|
type: "local",
|
||||||
command: [executable],
|
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 writeFile(ordinaryExecutable, "#!/usr/bin/env node\n");
|
||||||
await chmod(ordinaryExecutable, 0o755);
|
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([
|
expect(ordinary.mcpServers.get("playwright")?.command).toEqual([
|
||||||
"npx",
|
"npx",
|
||||||
"-y",
|
"-y",
|
||||||
@@ -534,7 +546,7 @@ test("playwright MCP prefers direct MCP, uses npx for Playwright, and preserves
|
|||||||
await chmod(fakePlaywright, 0o755);
|
await chmod(fakePlaywright, 0o755);
|
||||||
expect((await playwrightMcpCommand(globalProject, ordinaryBin))[0]).toBe("docker");
|
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({
|
expect(fallback.mcpServers.get("playwright")).toEqual({
|
||||||
type: "local",
|
type: "local",
|
||||||
command: ["docker", "run", "-i", "--rm", "--init", "mcr.microsoft.com/playwright/mcp"],
|
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 },
|
timeout: { startup: 12_345 },
|
||||||
});
|
});
|
||||||
expect(existing.mcpServers.get("docs")?.codemode).toBe(true);
|
expect(existing.mcpServers.get("docs")?.codemode).toBe(true);
|
||||||
|
|
||||||
|
const disabled = await host();
|
||||||
|
expect(disabled.mcpServers.has("playwright")).toBe(false);
|
||||||
} finally {
|
} finally {
|
||||||
if (originalPath === undefined) delete process.env.PATH;
|
if (originalPath === undefined) delete process.env.PATH;
|
||||||
else process.env.PATH = originalPath;
|
else process.env.PATH = originalPath;
|
||||||
|
|||||||
Reference in New Issue
Block a user