Skip to content

Commit fbdbb85

Browse files
skulidropekqwencodercodexCodex-Agentgithub-actions[bot]
authored
feat: enable Playwright MCP for existing projects (#30)
* feat(docker-git): format ps output * feat(docker-git): default to .docker-git and sync codex config * feat(docker-git): improve TUI ssh flow * fix(docker-git): suspend TUI during ssh * fix(tui): disable input during ssh * chore: add pre-commit large file guard * feat(docker-git): add tmux attach * fix(docker-git): stabilize tmux panes * fix(docker-git): stabilize select view and ssh return * fix(docker-git): reset tmux layout on mismatch * fix(docker-git): correct tmux actions pane * fix(docker-git): bump tmux layout version * refactor(cli): move docker-git cli and tmux to app * Restore web package files from local backup Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * feat(docker-git): improve sessions and codex bootstrap * feat(docker-git): enable zsh autosuggest * feat(docker-git): enforce gh scopes and protect branches * feat(docker-git): wait for ssh and track sessions * feat(docker-git): add codex resume hint * fix(docker-git): clean ssh login and sync templates - Print codex resume hint on interactive shells\n- Disable Ubuntu MOTD/last-login noise inside containers\n- Silence SSH host-key warnings via LogLevel=ERROR\n- Force docker compose output to plain/no-ANSI to avoid escape sequences\n- Keep generated project templates in sync on compose up * fix(docker-git): keep ssh port stable for running projects Skip port re-selection when docker compose is already running for the project. * fix(docker-git): escape newline in codex resume hint * refactor(docker-git): remove legacy greeting scaffold * fix(docker-git): make generated projects portable * feat(docker-git): add down-all and per-project stop * fix(docker-git): show only running containers in stop selector * refactor(docker-git): remove redundant up from TUI menu * fix(docker-git): disable mouse scroll for Ink TUI * feat(docker-git): show connection info via selector * feat(docker-git): add git-synced state commands * feat(docker-git): clone state repo on init * fix(docker-git): wipe volumes on --force - When --force is set, run docker compose down -v before up to ensure /home/dev volume is clean - Install pnpm via corepack in generated Dockerfiles for deterministic pnpm availability - Update CLI/TUI wording to reflect force semantics * update knowlenge * feat(docker-git): add safe gitignore for state repo * fix(docker-git): init state repo for empty remotes * feat(docker-git): use ~/.docker-git as default projects root * feat(docker-git): sync state repo with git - Add "state sync" (commit + fetch/rebase + push)\n- Auto-sync state after create/update operations\n- On conflict/divergence, push a PR branch and print a compare URL * feat(docker-git): delete project via selector - Add TUI action to delete a project folder from the state root\n- Best-effort: run docker compose down before deleting\n- Auto-sync state repo after deletion * feat(docker-git): confirm before deleting project - Delete flow now requires a second Enter to confirm\n- Esc cancels confirmation\n- Confirmation resets on selection change * fix(docker-git): auth state git operations via github token - Read GITHUB_TOKEN from state .orch/env/global.env (or env vars)\n- Use GIT_ASKPASS for non-interactive https push/pull/sync * fix(docker-git): normalize state sync + refresh stop selector * fix(hooks): guard push against oversized .knowledge blobs * feat(docker-git): share Codex auth + better SSH zsh UX - Share Codex auth.json across projects (symlink) to avoid refresh_token_reused - Keep per-project Codex sessions/logs under .orch - Improve SSH terminal UX (ncurses-term, zsh completion ordering, autosuggest from history+completion) - Support GitHub tree/blob URLs and add parser tests * feat(docker-git): add Playwright MCP sidecar - Add --mcp-playwright flag to generate Chromium sidecar container - Auto-configure Codex MCP server (playwright) in entrypoint - Provide wrapper to convert CDP HTTP endpoint -> usable WS endpoint - Update tests + CLI usage * feat(docker-git): isolate Playwright MCP contexts by default * fix(docker-git): remove stale Playwright MCP block when disabled * feat(app): expose Playwright MCP in TUI and help * docs: add docker-git README * fix(docker-git): make container IP reachable from external containers - Connect managed containers to the default bridge network after compose up - Prefer bridge IP for access logging so other containers can reach services by IP * feat(docker-git): update default Codex config - Default model: gpt-5.3-codex (xhigh) with pragmatic personality - Enable live web search + web_search_request - Enable shell_snapshot/collab/apps by default * docs(agents): add sleep and gh workflows guidance - Allow waiting for remote actions via sleep-based polling - Prefer gh for issues/PRs and CI checks; wait for Actions after push * fix(docker-git): propagate default Codex config to new containers - Rewrite docker-git-managed Codex config.toml when defaults change - Ensure per-project .orch/auth/codex/config.toml is updated on create and up * feat(state): track full state dir in git * feat(templates): allow committing .orch in state repo * feat(state): ignore volatile Codex artifacts * refactor(repo): move effect-template workspace to root * fix(ci): restore checkout permissions - Grant GITHUB_TOKEN contents:read so actions/checkout can fetch. - Reduce lint complexity in resolveCreateInputs. * fix(test): build lib before running app tests CI test job runs `pnpm test` without building @effect-template/lib, but the package exports types from dist. Add `pretest` to build lib so lint:tests + vitest can resolve imports. * chore(ci): add lib to checks * fix(lib): pass effect lint * fix(lib): pass vibecode-linter * fix(ci): fail on lib lint errors * fix(codex-config): remove deprecated web_search_request flag (#6) Co-authored-by: codex-agent <codex-agent@users.noreply.github.com> * ci(workflows): align with effect-template and add deps prune check (#8) * ci(workflows): align with effect-template and add deps prune check * fix(ci): restore snapshot checkout permission for private repo * chore(release): version packages * fix(shell): sync github auth env and improve docker error handling * feat: parallel issue/PR workspaces in one repository (#9) * feat(core): isolate issue/pr workspaces for parallel work * fix(app): remove duplicate workspace path logic * feat(shell): add issue workspace AGENTS context * feat(cli): add --force-env soft reset mode * fix(lib): resolve lint regressions in templates and file writer * refactor(lib): split long functions for lint constraints * refactor(lib): replace force flags params with options objects * refactor(lib): remove forbidden casts in create-project options * fix(lib): harden issue workspace env reset flow * fix(lib): split auth sync copy helpers for lint * fix(lib): manage AGENTS blocks without full overwrite --------- Co-authored-by: skulidropek <skulidropek@users.noreply.github.com> * chore(release): version packages * fix(release): publish app CLI as @prover-coder-ai/docker-git (#16) * fix(ci): disable npm publish in release workflow * fix(ci): gate npm publish on token/access checks * fix(release): publish app cli as @prover-coder-ai/docker-git * chore(ci): drop custom release workflow changes * fix(ci): keep app dist free of test-only deps --------- Co-authored-by: skulidropek <skulidropek@users.noreply.github.com> * chore(release): version packages * fix: bridge GH token to git auth (#15) * fix(shell): bridge gh token to git credentials * fix(shell): auto-configure git credentials after gh auth * test(docker-git): assert generated auth helper in entrypoint * refactor(lib): split entrypoint git template blocks --------- Co-authored-by: skulidropek <skulidropek@users.noreply.github.com> * chore(release): version packages * fix(release): publish only @prover-coder-ai/docker-git (#18) Co-authored-by: skulidropek <skulidropek@users.noreply.github.com> * chore(release): version packages * fix(force-env): refresh managed files and rebuild on recreate (#19) Co-authored-by: skulidropek <skulidropek@users.noreply.github.com> * chore(release): version packages * fix: handle docker socket permission failures early (#20) * fix(shell): fail fast on docker socket permission errors * fix(lib): reduce error renderer complexity --------- Co-authored-by: skulidropek <skulidropek@users.noreply.github.com> * chore(release): version packages * fix(ci): validate app and lib in check workflow (#22) Co-authored-by: skulidropek <skulidropek@users.noreply.github.com> * fix(ci): enforce effect-ts analyzer on tests (#23) * fix(lint): include tests in effect-ts analyzer * chore(ci): retrigger checks after runner stall --------- Co-authored-by: skulidropek <skulidropek@users.noreply.github.com> * chore(release): version packages * fix: support nested docker-git auth bootstrap and compose v2 (#25) * fix(lib): bootstrap nested docker-git auth and compose v2 * refactor(lib): split nested docker-git entrypoint template * refactor(lib): reduce lint complexity in path normalization * chore(release): version packages * fix(lib): resolve playwright MCP env interpolation in wrapper script * feat(cli): add mcp-playwright command for existing projects - Adds docker-git mcp-playwright to enable Playwright MCP + Chromium sidecar after create\n- Rewrites managed templates + docker-git.json, preserves env files and volumes\n- Includes parser + usecase tests * fix(test): make vibecode-linter resolve pnpm binaries vibecode-linter shells out to npx biome/tsc, but npm's npx can miss local pnpm workspace bins.\nAdd a small npx shim (pnpm exec) and prepend it to PATH for lint scripts. * fix(cli): avoid pipe overload in command matcher Effect\x27s pipe typings cap at 20 args; split Match.orElse into a second pipe call. * chore(lint): apply autofixes * feat(cli): add mcp-playwright command for existing projects - Adds docker-git mcp-playwright to enable Playwright MCP + Chromium sidecar after create\n- Rewrites managed templates + docker-git.json, preserves env files and volumes\n- Includes parser + usecase tests * fix(test): make vibecode-linter resolve pnpm binaries vibecode-linter shells out to npx biome/tsc, but npm's npx can miss local pnpm workspace bins.\nAdd a small npx shim (pnpm exec) and prepend it to PATH for lint scripts. * fix(scripts): make npx shim executable --------- Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Co-authored-by: codex <codex@users.noreply.github.com> Co-authored-by: codex-agent <codex-agent@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: skulidropek <skulidropek@users.noreply.github.com>
1 parent c720f7c commit fbdbb85

File tree

10 files changed

+298
-6
lines changed

10 files changed

+298
-6
lines changed

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,9 @@ Disable sharing (per-project auth):
9393
Enable during create/clone:
9494
- Add `--mcp-playwright`
9595

96+
Enable for an existing project directory (preserves `.orch/env/project.env` and volumes):
97+
- `docker-git mcp-playwright [<url>] [--project-dir <path>]`
98+
9699
This will:
97100
- Create a Chromium sidecar container: `dg-<repo>-browser`
98101
- Configure Codex MCP server `playwright` inside the dev container
@@ -119,7 +122,8 @@ Common toggles:
119122
MCP errors in `codex` UI:
120123
- `No such file or directory (os error 2)` for `playwright`:
121124
- `~/.codex/config.toml` contains `[mcp_servers.playwright]`, but the container was created without `--mcp-playwright`.
122-
- Fix: recreate with `--force --mcp-playwright` (or remove the block from `config.toml`).
125+
- Fix (recommended): run `docker-git mcp-playwright [<url>]` to enable it for the existing project.
126+
- Fix (recreate): recreate with `--force-env --mcp-playwright` (keeps volumes) or `--force --mcp-playwright` (wipes volumes).
123127
- `handshaking ... initialize response`:
124128
- The configured MCP command is not a real MCP server (example: `command="echo"`).
125129

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { Either } from "effect"
2+
3+
import { type McpPlaywrightUpCommand, type ParseError } from "@effect-template/lib/core/domain"
4+
5+
import { parseProjectDirWithOptions } from "./parser-shared.js"
6+
7+
// CHANGE: parse "mcp-playwright" command for existing docker-git projects
8+
// WHY: allow enabling Playwright MCP in an already created container/project dir
9+
// QUOTE(ТЗ): "Добавить возможность поднимать MCP Playrgiht в контейнере который уже создан"
10+
// REF: issue-29
11+
// SOURCE: n/a
12+
// FORMAT THEOREM: forall argv: parseMcpPlaywright(argv) = cmd -> deterministic(cmd)
13+
// PURITY: CORE
14+
// EFFECT: Effect<McpPlaywrightUpCommand, ParseError, never>
15+
// INVARIANT: projectDir is never empty
16+
// COMPLEXITY: O(n) where n = |argv|
17+
export const parseMcpPlaywright = (
18+
args: ReadonlyArray<string>
19+
): Either.Either<McpPlaywrightUpCommand, ParseError> =>
20+
Either.map(parseProjectDirWithOptions(args), ({ projectDir, raw }) => ({
21+
_tag: "McpPlaywrightUp",
22+
projectDir,
23+
runUp: raw.up ?? true
24+
}))
25+

packages/app/src/docker-git/cli/parser.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { parseAttach } from "./parser-attach.js"
66
import { parseAuth } from "./parser-auth.js"
77
import { parseClone } from "./parser-clone.js"
88
import { buildCreateCommand } from "./parser-create.js"
9+
import { parseMcpPlaywright } from "./parser-mcp-playwright.js"
910
import { parseRawOptions } from "./parser-options.js"
1011
import { parsePanes } from "./parser-panes.js"
1112
import { parseScrap } from "./parser-scrap.js"
@@ -61,6 +62,7 @@ export const parseArgs = (args: ReadonlyArray<string>): Either.Either<Command, P
6162
Match.when("terminals", () => parsePanes(rest)),
6263
Match.when("sessions", () => parseSessions(rest)),
6364
Match.when("scrap", () => parseScrap(rest)),
65+
Match.when("mcp-playwright", () => parseMcpPlaywright(rest)),
6466
Match.when("help", () => Either.right(helpCommand)),
6567
Match.when("ps", () => Either.right(statusCommand)),
6668
Match.when("status", () => Either.right(statusCommand)),
@@ -69,8 +71,10 @@ export const parseArgs = (args: ReadonlyArray<string>): Either.Either<Command, P
6971
Match.when("kill-all", () => Either.right(downAllCommand)),
7072
Match.when("menu", () => Either.right(menuCommand)),
7173
Match.when("ui", () => Either.right(menuCommand)),
72-
Match.when("auth", () => parseAuth(rest)),
73-
Match.when("state", () => parseState(rest))
74+
Match.when("auth", () => parseAuth(rest))
75+
)
76+
.pipe(
77+
Match.when("state", () => parseState(rest)),
78+
Match.orElse(() => Either.left(unknownCommandError))
7479
)
75-
.pipe(Match.orElse(() => Either.left(unknownCommandError)))
7680
}

packages/app/src/docker-git/cli/usage.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { ParseError } from "@effect-template/lib/core/domain"
55
export const usageText = `docker-git menu
66
docker-git create --repo-url <url> [options]
77
docker-git clone <url> [options]
8+
docker-git mcp-playwright [<url>] [options]
89
docker-git attach [<url>] [options]
910
docker-git panes [<url>] [options]
1011
docker-git scrap <action> [<url>] [options]
@@ -20,6 +21,7 @@ Commands:
2021
menu Interactive menu (default when no args)
2122
create, init Generate docker development environment
2223
clone Create + run container and clone repo
24+
mcp-playwright Enable Playwright MCP + Chromium sidecar for an existing project dir
2325
attach, tmux Open tmux workspace for a docker-git project
2426
panes, terms List tmux panes for a docker-git project
2527
scrap Export/import project scrap (session snapshot + rebuildable deps)

packages/app/src/docker-git/program.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
} from "@effect-template/lib/usecases/auth"
1111
import type { AppError } from "@effect-template/lib/usecases/errors"
1212
import { renderError } from "@effect-template/lib/usecases/errors"
13+
import { mcpPlaywrightUp } from "@effect-template/lib/usecases/mcp-playwright"
1314
import { downAllDockerGitProjects, listProjectStatus } from "@effect-template/lib/usecases/projects"
1415
import { exportScrap, importScrap } from "@effect-template/lib/usecases/scrap"
1516
import {
@@ -92,7 +93,10 @@ const handleNonBaseCommand = (command: NonBaseCommand) =>
9293
Match.when({ _tag: "ScrapExport" }, (cmd) => exportScrap(cmd)),
9394
Match.when({ _tag: "ScrapImport" }, (cmd) => importScrap(cmd))
9495
)
95-
.pipe(Match.exhaustive)
96+
.pipe(
97+
Match.when({ _tag: "McpPlaywrightUp" }, (cmd) => mcpPlaywrightUp(cmd)),
98+
Match.exhaustive
99+
)
96100

97101
// CHANGE: compose CLI program with typed errors and shell effects
98102
// WHY: keep a thin entry layer over pure parsing and template generation

packages/app/tests/docker-git/parser.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,34 @@ describe("parseArgs", () => {
152152
expect(command.projectDir).toBe(".docker-git/org/repo/issue-7")
153153
}))
154154

155+
it.effect("parses mcp-playwright command in current directory", () =>
156+
Effect.sync(() => {
157+
const command = parseOrThrow(["mcp-playwright"])
158+
if (command._tag !== "McpPlaywrightUp") {
159+
throw new Error("expected McpPlaywrightUp command")
160+
}
161+
expect(command.projectDir).toBe(".")
162+
expect(command.runUp).toBe(true)
163+
}))
164+
165+
it.effect("parses mcp-playwright command with --no-up", () =>
166+
Effect.sync(() => {
167+
const command = parseOrThrow(["mcp-playwright", "--no-up"])
168+
if (command._tag !== "McpPlaywrightUp") {
169+
throw new Error("expected McpPlaywrightUp command")
170+
}
171+
expect(command.runUp).toBe(false)
172+
}))
173+
174+
it.effect("parses mcp-playwright with positional repo url into project dir", () =>
175+
Effect.sync(() => {
176+
const command = parseOrThrow(["mcp-playwright", "https://github.com/org/repo.git"])
177+
if (command._tag !== "McpPlaywrightUp") {
178+
throw new Error("expected McpPlaywrightUp command")
179+
}
180+
expect(command.projectDir).toBe(".docker-git/org/repo")
181+
}))
182+
155183
it.effect("parses down-all command", () =>
156184
Effect.sync(() => {
157185
const command = parseOrThrow(["down-all"])

packages/lib/src/core/domain.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,12 @@ export interface ScrapImportCommand {
9999
readonly mode: ScrapMode
100100
}
101101

102+
export interface McpPlaywrightUpCommand {
103+
readonly _tag: "McpPlaywrightUp"
104+
readonly projectDir: string
105+
readonly runUp: boolean
106+
}
107+
102108
export interface HelpCommand {
103109
readonly _tag: "Help"
104110
readonly message: string
@@ -214,6 +220,7 @@ export type Command =
214220
| PanesCommand
215221
| SessionsCommand
216222
| ScrapCommand
223+
| McpPlaywrightUpCommand
217224
| HelpCommand
218225
| StatusCommand
219226
| DownAllCommand
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import type { CommandExecutor } from "@effect/platform/CommandExecutor"
2+
import type { PlatformError } from "@effect/platform/Error"
3+
import type { FileSystem } from "@effect/platform/FileSystem"
4+
import type { Path } from "@effect/platform/Path"
5+
import { Effect } from "effect"
6+
7+
import type { McpPlaywrightUpCommand, TemplateConfig } from "../core/domain.js"
8+
import { readProjectConfig } from "../shell/config.js"
9+
import { ensureDockerDaemonAccess } from "../shell/docker.js"
10+
import type {
11+
ConfigDecodeError,
12+
ConfigNotFoundError,
13+
DockerAccessError,
14+
DockerCommandError,
15+
FileExistsError,
16+
PortProbeError
17+
} from "../shell/errors.js"
18+
import { writeProjectFiles } from "../shell/files.js"
19+
import { ensureCodexConfigFile } from "./auth-sync.js"
20+
import { runDockerComposeUpWithPortCheck } from "./projects-up.js"
21+
22+
type McpPlaywrightFilesError = ConfigNotFoundError | ConfigDecodeError | FileExistsError | PlatformError
23+
type McpPlaywrightFilesEnv = FileSystem | Path
24+
25+
const enableInTemplate = (template: TemplateConfig): TemplateConfig => ({
26+
...template,
27+
enableMcpPlaywright: true
28+
})
29+
30+
// CHANGE: enable Playwright MCP in an existing docker-git project directory (files only)
31+
// WHY: allow adding the browser sidecar + MCP server config without wiping env or volumes
32+
// QUOTE(ТЗ): "Добавить возможность поднимать MCP Playrgiht в контейнере который уже создан"
33+
// REF: issue-29
34+
// SOURCE: n/a
35+
// FORMAT THEOREM: forall p: enable(p) -> template(p).enableMcpPlaywright = true
36+
// PURITY: SHELL
37+
// EFFECT: Effect<TemplateConfig, ConfigNotFoundError | ConfigDecodeError | FileExistsError | PlatformError, FileSystem | Path>
38+
// INVARIANT: does not rewrite .orch/env/project.env (only managed templates + docker-git.json)
39+
// COMPLEXITY: O(n) where n = |managed_files|
40+
export const enableMcpPlaywrightProjectFiles = (
41+
projectDir: string
42+
): Effect.Effect<TemplateConfig, McpPlaywrightFilesError, McpPlaywrightFilesEnv> =>
43+
Effect.gen(function*(_) {
44+
const config = yield* _(readProjectConfig(projectDir))
45+
const alreadyEnabled = config.template.enableMcpPlaywright
46+
const updated = alreadyEnabled ? config.template : enableInTemplate(config.template)
47+
48+
yield* _(
49+
alreadyEnabled
50+
? Effect.log("Playwright MCP is already enabled for this project.")
51+
: Effect.log("Enabling Playwright MCP for this project (templates only)...")
52+
)
53+
54+
yield* _(writeProjectFiles(projectDir, updated, true))
55+
yield* _(ensureCodexConfigFile(projectDir, updated.codexAuthPath))
56+
57+
return updated
58+
})
59+
60+
export type McpPlaywrightUpError =
61+
| McpPlaywrightFilesError
62+
| DockerAccessError
63+
| DockerCommandError
64+
| PortProbeError
65+
66+
type McpPlaywrightUpEnv = McpPlaywrightFilesEnv | CommandExecutor
67+
68+
// CHANGE: enable Playwright MCP in an existing project dir and bring docker compose up
69+
// WHY: upgrade already created containers to support browser automation without forcing full recreation flows
70+
// QUOTE(ТЗ): "Добавить возможность поднимать MCP Playrgiht в контейнере который уже создан"
71+
// REF: issue-29
72+
// SOURCE: n/a
73+
// FORMAT THEOREM: forall p: up(p) -> running(p-browser) OR docker_error
74+
// PURITY: SHELL
75+
// EFFECT: Effect<TemplateConfig, McpPlaywrightUpError, FileSystem | Path | CommandExecutor>
76+
// INVARIANT: volumes are preserved (no docker compose down -v)
77+
// COMPLEXITY: O(command)
78+
export const mcpPlaywrightUp = (
79+
command: McpPlaywrightUpCommand
80+
): Effect.Effect<TemplateConfig, McpPlaywrightUpError, McpPlaywrightUpEnv> =>
81+
Effect.gen(function*(_) {
82+
const updated = yield* _(enableMcpPlaywrightProjectFiles(command.projectDir))
83+
84+
if (!command.runUp) {
85+
return updated
86+
}
87+
88+
yield* _(ensureDockerDaemonAccess(process.cwd()))
89+
return yield* _(runDockerComposeUpWithPortCheck(command.projectDir))
90+
})
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import * as FileSystem from "@effect/platform/FileSystem"
2+
import * as Path from "@effect/platform/Path"
3+
import { NodeContext } from "@effect/platform-node"
4+
import { describe, expect, it } from "@effect/vitest"
5+
import { Effect } from "effect"
6+
7+
import type { TemplateConfig } from "../../src/core/domain.js"
8+
import { enableMcpPlaywrightProjectFiles } from "../../src/usecases/mcp-playwright.js"
9+
import { prepareProjectFiles } from "../../src/usecases/actions/prepare-files.js"
10+
11+
const withTempDir = <A, E, R>(
12+
use: (tempDir: string) => Effect.Effect<A, E, R>
13+
): Effect.Effect<A, E, R | FileSystem.FileSystem> =>
14+
Effect.scoped(
15+
Effect.gen(function*(_) {
16+
const fs = yield* _(FileSystem.FileSystem)
17+
const tempDir = yield* _(
18+
fs.makeTempDirectoryScoped({
19+
prefix: "docker-git-mcp-playwright-"
20+
})
21+
)
22+
return yield* _(use(tempDir))
23+
})
24+
)
25+
26+
const makeGlobalConfig = (root: string, path: Path.Path): TemplateConfig => ({
27+
containerName: "dg-test",
28+
serviceName: "dg-test",
29+
sshUser: "dev",
30+
sshPort: 2222,
31+
repoUrl: "https://github.com/org/repo.git",
32+
repoRef: "main",
33+
targetDir: "/home/dev/org/repo",
34+
volumeName: "dg-test-home",
35+
dockerGitPath: path.join(root, ".docker-git"),
36+
authorizedKeysPath: path.join(root, "authorized_keys"),
37+
envGlobalPath: path.join(root, ".orch/env/global.env"),
38+
envProjectPath: path.join(root, ".orch/env/project.env"),
39+
codexAuthPath: path.join(root, ".orch/auth/codex"),
40+
codexSharedAuthPath: path.join(root, ".orch/auth/codex-shared"),
41+
codexHome: "/home/dev/.codex",
42+
enableMcpPlaywright: false,
43+
pnpmVersion: "10.27.0"
44+
})
45+
46+
const makeProjectConfig = (
47+
outDir: string,
48+
enableMcpPlaywright: boolean,
49+
path: Path.Path
50+
): TemplateConfig => ({
51+
containerName: "dg-test",
52+
serviceName: "dg-test",
53+
sshUser: "dev",
54+
sshPort: 2222,
55+
repoUrl: "https://github.com/org/repo.git",
56+
repoRef: "main",
57+
targetDir: "/home/dev/org/repo",
58+
volumeName: "dg-test-home",
59+
dockerGitPath: path.join(outDir, ".docker-git"),
60+
authorizedKeysPath: path.join(outDir, "authorized_keys"),
61+
envGlobalPath: path.join(outDir, ".orch/env/global.env"),
62+
envProjectPath: path.join(outDir, ".orch/env/project.env"),
63+
codexAuthPath: path.join(outDir, ".orch/auth/codex"),
64+
codexSharedAuthPath: path.join(outDir, ".orch/auth/codex-shared"),
65+
codexHome: "/home/dev/.codex",
66+
enableMcpPlaywright,
67+
pnpmVersion: "10.27.0"
68+
})
69+
70+
const isRecord = (value: unknown): value is Record<string, unknown> =>
71+
typeof value === "object" && value !== null
72+
73+
const readEnableMcpPlaywrightFlag = (value: unknown): boolean | undefined => {
74+
if (!isRecord(value)) {
75+
return undefined
76+
}
77+
78+
const template = value.template
79+
if (!isRecord(template)) {
80+
return undefined
81+
}
82+
83+
const flag = template.enableMcpPlaywright
84+
return typeof flag === "boolean" ? flag : undefined
85+
}
86+
87+
describe("enableMcpPlaywrightProjectFiles", () => {
88+
it.effect("enables Playwright MCP for an existing project without rewriting env files", () =>
89+
withTempDir((root) =>
90+
Effect.gen(function*(_) {
91+
const fs = yield* _(FileSystem.FileSystem)
92+
const path = yield* _(Path.Path)
93+
const outDir = path.join(root, "project")
94+
const globalConfig = makeGlobalConfig(root, path)
95+
const withoutMcp = makeProjectConfig(outDir, false, path)
96+
97+
yield* _(
98+
prepareProjectFiles(outDir, root, globalConfig, withoutMcp, {
99+
force: false,
100+
forceEnv: false
101+
})
102+
)
103+
104+
const envProjectPath = path.join(outDir, ".orch/env/project.env")
105+
yield* _(fs.writeFileString(envProjectPath, "# custom env\nCUSTOM_KEY=1\n"))
106+
107+
yield* _(enableMcpPlaywrightProjectFiles(outDir))
108+
109+
const envAfter = yield* _(fs.readFileString(envProjectPath))
110+
expect(envAfter).toContain("CUSTOM_KEY=1")
111+
112+
const composeAfter = yield* _(fs.readFileString(path.join(outDir, "docker-compose.yml")))
113+
expect(composeAfter).toContain("dg-test-browser")
114+
expect(composeAfter).toContain('MCP_PLAYWRIGHT_ENABLE: "1"')
115+
116+
const dockerfileAfter = yield* _(fs.readFileString(path.join(outDir, "Dockerfile")))
117+
expect(dockerfileAfter).toContain("@playwright/mcp")
118+
119+
const browserDockerfileExists = yield* _(fs.exists(path.join(outDir, "Dockerfile.browser")))
120+
const startExtraExists = yield* _(fs.exists(path.join(outDir, "mcp-playwright-start-extra.sh")))
121+
expect(browserDockerfileExists).toBe(true)
122+
expect(startExtraExists).toBe(true)
123+
124+
const configAfterText = yield* _(fs.readFileString(path.join(outDir, "docker-git.json")))
125+
const configAfter = yield* _(Effect.sync((): unknown => JSON.parse(configAfterText)))
126+
expect(readEnableMcpPlaywrightFlag(configAfter)).toBe(true)
127+
})
128+
).pipe(Effect.provide(NodeContext.layer)))
129+
})

scripts/npx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,3 @@ if [ "${1-}" = "" ]; then
3434
fi
3535

3636
exec pnpm exec "$@"
37-

0 commit comments

Comments
 (0)