Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
93 commits
Select commit Hold shift + click to select a range
7cbe992
feat(docker-git): format ps output
skulidropek Jan 28, 2026
2db39a6
feat(docker-git): default to .docker-git and sync codex config
skulidropek Jan 29, 2026
c51bb93
feat(docker-git): improve TUI ssh flow
skulidropek Feb 2, 2026
95bbfa7
fix(docker-git): suspend TUI during ssh
skulidropek Feb 2, 2026
da0f320
fix(tui): disable input during ssh
skulidropek Feb 2, 2026
11b2454
chore: add pre-commit large file guard
skulidropek Feb 2, 2026
9a4f598
feat(docker-git): add tmux attach
skulidropek Feb 2, 2026
7055ee1
fix(docker-git): stabilize tmux panes
skulidropek Feb 2, 2026
2f5b40a
fix(docker-git): stabilize select view and ssh return
skulidropek Feb 2, 2026
94bfd27
fix(docker-git): reset tmux layout on mismatch
skulidropek Feb 2, 2026
6aba864
fix(docker-git): correct tmux actions pane
skulidropek Feb 2, 2026
77ebbd8
fix(docker-git): bump tmux layout version
skulidropek Feb 2, 2026
ebd40da
refactor(cli): move docker-git cli and tmux to app
skulidropek Feb 2, 2026
2008891
Restore web package files from local backup
skulidropek Feb 4, 2026
4b4ef32
feat(docker-git): improve sessions and codex bootstrap
skulidropek Feb 5, 2026
e65d631
feat(docker-git): enable zsh autosuggest
skulidropek Feb 5, 2026
0f5ba5c
feat(docker-git): enforce gh scopes and protect branches
skulidropek Feb 5, 2026
d339e3a
feat(docker-git): wait for ssh and track sessions
skulidropek Feb 6, 2026
3e26e1e
feat(docker-git): add codex resume hint
skulidropek Feb 6, 2026
88e2974
fix(docker-git): clean ssh login and sync templates
skulidropek Feb 6, 2026
cd6cbcc
fix(docker-git): keep ssh port stable for running projects
skulidropek Feb 6, 2026
20bd484
fix(docker-git): escape newline in codex resume hint
skulidropek Feb 6, 2026
58cf0e6
refactor(docker-git): remove legacy greeting scaffold
skulidropek Feb 6, 2026
a7ad09a
fix(docker-git): make generated projects portable
skulidropek Feb 6, 2026
36008cf
feat(docker-git): add down-all and per-project stop
skulidropek Feb 6, 2026
6e9effa
fix(docker-git): show only running containers in stop selector
skulidropek Feb 7, 2026
f861993
refactor(docker-git): remove redundant up from TUI menu
skulidropek Feb 7, 2026
a35ca50
fix(docker-git): disable mouse scroll for Ink TUI
skulidropek Feb 7, 2026
7e1afa8
feat(docker-git): show connection info via selector
skulidropek Feb 7, 2026
111ab36
feat(docker-git): add git-synced state commands
skulidropek Feb 7, 2026
5439634
feat(docker-git): clone state repo on init
skulidropek Feb 7, 2026
a3a0bb4
fix(docker-git): wipe volumes on --force
skulidropek Feb 8, 2026
dfaa1c6
update knowlenge
skulidropek Feb 8, 2026
03022f1
feat(docker-git): add safe gitignore for state repo
skulidropek Feb 8, 2026
cfa3e2e
fix(docker-git): init state repo for empty remotes
skulidropek Feb 8, 2026
2f44c69
feat(docker-git): use ~/.docker-git as default projects root
skulidropek Feb 8, 2026
65273a9
feat(docker-git): sync state repo with git
skulidropek Feb 9, 2026
b24d3fa
feat(docker-git): delete project via selector
skulidropek Feb 9, 2026
5a4d058
feat(docker-git): confirm before deleting project
skulidropek Feb 9, 2026
6b60666
fix(docker-git): auth state git operations via github token
skulidropek Feb 9, 2026
e46087f
fix(docker-git): normalize state sync + refresh stop selector
skulidropek Feb 9, 2026
54c9bea
fix(hooks): guard push against oversized .knowledge blobs
skulidropek Feb 9, 2026
f8ef92f
feat(docker-git): share Codex auth + better SSH zsh UX
skulidropek Feb 10, 2026
f63a460
feat(docker-git): add Playwright MCP sidecar
skulidropek Feb 10, 2026
eef5b8f
feat(docker-git): isolate Playwright MCP contexts by default
skulidropek Feb 10, 2026
11deac3
fix(docker-git): remove stale Playwright MCP block when disabled
skulidropek Feb 10, 2026
a838547
feat(app): expose Playwright MCP in TUI and help
skulidropek Feb 10, 2026
dae8535
docs: add docker-git README
skulidropek Feb 10, 2026
7717e61
fix(docker-git): make container IP reachable from external containers
skulidropek Feb 10, 2026
e26901f
feat(docker-git): update default Codex config
skulidropek Feb 10, 2026
6b1668b
docs(agents): add sleep and gh workflows guidance
skulidropek Feb 10, 2026
d3b06b8
fix(docker-git): propagate default Codex config to new containers
skulidropek Feb 10, 2026
e80467d
feat(state): track full state dir in git
skulidropek Feb 10, 2026
d69c761
feat(templates): allow committing .orch in state repo
skulidropek Feb 10, 2026
df27345
feat(state): ignore volatile Codex artifacts
skulidropek Feb 10, 2026
475d2a0
refactor(repo): move effect-template workspace to root
skulidropek Feb 10, 2026
29229de
fix(ci): restore checkout permissions
codex Feb 10, 2026
e341822
fix(test): build lib before running app tests
codex Feb 10, 2026
cef1583
chore(ci): add lib to checks
codex Feb 10, 2026
86ca11b
fix(lib): pass effect lint
codex Feb 10, 2026
d2d98f3
fix(lib): pass vibecode-linter
codex Feb 10, 2026
2d2018a
fix(ci): fail on lib lint errors
codex Feb 10, 2026
100c796
Merge pull request #2 from ProverCoderAI/fix/ci-checkout-permissions
skulidropek Feb 11, 2026
0e6465e
fix(codex-config): remove deprecated web_search_request flag (#6)
skulidropek Feb 11, 2026
6754ac0
ci(workflows): align with effect-template and add deps prune check (#8)
skulidropek Feb 11, 2026
5c19076
chore(release): version packages
github-actions[bot] Feb 11, 2026
645fb29
fix(shell): sync github auth env and improve docker error handling
skulidropek Feb 11, 2026
c544290
feat: parallel issue/PR workspaces in one repository (#9)
skulidropek Feb 11, 2026
f75efe1
chore(release): version packages
github-actions[bot] Feb 11, 2026
5fef380
fix(release): publish app CLI as @prover-coder-ai/docker-git (#16)
skulidropek Feb 11, 2026
03162d8
chore(release): version packages
github-actions[bot] Feb 11, 2026
256d6f1
fix: bridge GH token to git auth (#15)
skulidropek Feb 11, 2026
08cb800
chore(release): version packages
github-actions[bot] Feb 11, 2026
4606dee
fix(release): publish only @prover-coder-ai/docker-git (#18)
skulidropek Feb 11, 2026
df71239
chore(release): version packages
github-actions[bot] Feb 11, 2026
d95f628
fix(force-env): refresh managed files and rebuild on recreate (#19)
skulidropek Feb 11, 2026
d543ab2
chore(release): version packages
github-actions[bot] Feb 11, 2026
50dd421
fix: handle docker socket permission failures early (#20)
skulidropek Feb 11, 2026
fb0b74c
chore(release): version packages
github-actions[bot] Feb 11, 2026
3fe9d83
fix(ci): validate app and lib in check workflow (#22)
skulidropek Feb 11, 2026
d1e36c8
fix(ci): enforce effect-ts analyzer on tests (#23)
skulidropek Feb 11, 2026
0612c56
chore(release): version packages
github-actions[bot] Feb 11, 2026
034d133
fix: support nested docker-git auth bootstrap and compose v2 (#25)
skulidropek Feb 12, 2026
aa864e0
chore(release): version packages
github-actions[bot] Feb 12, 2026
d24de22
fix(lib): resolve playwright MCP env interpolation in wrapper script
skulidropek Feb 13, 2026
125e6f0
feat(cli): add mcp-playwright command for existing projects
skulidropek Feb 13, 2026
2918dd7
fix(test): make vibecode-linter resolve pnpm binaries
skulidropek Feb 13, 2026
6ca6709
fix(cli): avoid pipe overload in command matcher
skulidropek Feb 13, 2026
9edc2d3
chore(lint): apply autofixes
skulidropek Feb 14, 2026
3dcf6c4
feat(cli): add mcp-playwright command for existing projects
skulidropek Feb 13, 2026
c430779
fix(test): make vibecode-linter resolve pnpm binaries
skulidropek Feb 13, 2026
fdb5e21
fix(scripts): make npx shim executable
skulidropek Feb 15, 2026
0c99477
Merge issue-29-rebased into issue-29
skulidropek Feb 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ Disable sharing (per-project auth):
Enable during create/clone:
- Add `--mcp-playwright`

Enable for an existing project directory (preserves `.orch/env/project.env` and volumes):
- `docker-git mcp-playwright [<url>] [--project-dir <path>]`

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

Expand Down
25 changes: 25 additions & 0 deletions packages/app/src/docker-git/cli/parser-mcp-playwright.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Either } from "effect"

import { type McpPlaywrightUpCommand, type ParseError } from "@effect-template/lib/core/domain"

import { parseProjectDirWithOptions } from "./parser-shared.js"

// CHANGE: parse "mcp-playwright" command for existing docker-git projects
// WHY: allow enabling Playwright MCP in an already created container/project dir
// QUOTE(ТЗ): "Добавить возможность поднимать MCP Playrgiht в контейнере который уже создан"
// REF: issue-29
// SOURCE: n/a
// FORMAT THEOREM: forall argv: parseMcpPlaywright(argv) = cmd -> deterministic(cmd)
// PURITY: CORE
// EFFECT: Effect<McpPlaywrightUpCommand, ParseError, never>
// INVARIANT: projectDir is never empty
// COMPLEXITY: O(n) where n = |argv|
export const parseMcpPlaywright = (
args: ReadonlyArray<string>
): Either.Either<McpPlaywrightUpCommand, ParseError> =>
Either.map(parseProjectDirWithOptions(args), ({ projectDir, raw }) => ({
_tag: "McpPlaywrightUp",
projectDir,
runUp: raw.up ?? true
}))

10 changes: 7 additions & 3 deletions packages/app/src/docker-git/cli/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { parseAttach } from "./parser-attach.js"
import { parseAuth } from "./parser-auth.js"
import { parseClone } from "./parser-clone.js"
import { buildCreateCommand } from "./parser-create.js"
import { parseMcpPlaywright } from "./parser-mcp-playwright.js"
import { parseRawOptions } from "./parser-options.js"
import { parsePanes } from "./parser-panes.js"
import { parseScrap } from "./parser-scrap.js"
Expand Down Expand Up @@ -61,6 +62,7 @@ export const parseArgs = (args: ReadonlyArray<string>): Either.Either<Command, P
Match.when("terminals", () => parsePanes(rest)),
Match.when("sessions", () => parseSessions(rest)),
Match.when("scrap", () => parseScrap(rest)),
Match.when("mcp-playwright", () => parseMcpPlaywright(rest)),
Match.when("help", () => Either.right(helpCommand)),
Match.when("ps", () => Either.right(statusCommand)),
Match.when("status", () => Either.right(statusCommand)),
Expand All @@ -69,8 +71,10 @@ export const parseArgs = (args: ReadonlyArray<string>): Either.Either<Command, P
Match.when("kill-all", () => Either.right(downAllCommand)),
Match.when("menu", () => Either.right(menuCommand)),
Match.when("ui", () => Either.right(menuCommand)),
Match.when("auth", () => parseAuth(rest)),
Match.when("state", () => parseState(rest))
Match.when("auth", () => parseAuth(rest))
)
.pipe(
Match.when("state", () => parseState(rest)),
Match.orElse(() => Either.left(unknownCommandError))
)
.pipe(Match.orElse(() => Either.left(unknownCommandError)))
}
2 changes: 2 additions & 0 deletions packages/app/src/docker-git/cli/usage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { ParseError } from "@effect-template/lib/core/domain"
export const usageText = `docker-git menu
docker-git create --repo-url <url> [options]
docker-git clone <url> [options]
docker-git mcp-playwright [<url>] [options]
docker-git attach [<url>] [options]
docker-git panes [<url>] [options]
docker-git scrap <action> [<url>] [options]
Expand All @@ -20,6 +21,7 @@ Commands:
menu Interactive menu (default when no args)
create, init Generate docker development environment
clone Create + run container and clone repo
mcp-playwright Enable Playwright MCP + Chromium sidecar for an existing project dir
attach, tmux Open tmux workspace for a docker-git project
panes, terms List tmux panes for a docker-git project
scrap Export/import project scrap (session snapshot + rebuildable deps)
Expand Down
6 changes: 5 additions & 1 deletion packages/app/src/docker-git/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
} from "@effect-template/lib/usecases/auth"
import type { AppError } from "@effect-template/lib/usecases/errors"
import { renderError } from "@effect-template/lib/usecases/errors"
import { mcpPlaywrightUp } from "@effect-template/lib/usecases/mcp-playwright"
import { downAllDockerGitProjects, listProjectStatus } from "@effect-template/lib/usecases/projects"
import { exportScrap, importScrap } from "@effect-template/lib/usecases/scrap"
import {
Expand Down Expand Up @@ -92,7 +93,10 @@ const handleNonBaseCommand = (command: NonBaseCommand) =>
Match.when({ _tag: "ScrapExport" }, (cmd) => exportScrap(cmd)),
Match.when({ _tag: "ScrapImport" }, (cmd) => importScrap(cmd))
)
.pipe(Match.exhaustive)
.pipe(
Match.when({ _tag: "McpPlaywrightUp" }, (cmd) => mcpPlaywrightUp(cmd)),
Match.exhaustive
)

// CHANGE: compose CLI program with typed errors and shell effects
// WHY: keep a thin entry layer over pure parsing and template generation
Expand Down
28 changes: 28 additions & 0 deletions packages/app/tests/docker-git/parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,34 @@ describe("parseArgs", () => {
expect(command.projectDir).toBe(".docker-git/org/repo/issue-7")
}))

it.effect("parses mcp-playwright command in current directory", () =>
Effect.sync(() => {
const command = parseOrThrow(["mcp-playwright"])
if (command._tag !== "McpPlaywrightUp") {
throw new Error("expected McpPlaywrightUp command")
}
expect(command.projectDir).toBe(".")
expect(command.runUp).toBe(true)
}))

it.effect("parses mcp-playwright command with --no-up", () =>
Effect.sync(() => {
const command = parseOrThrow(["mcp-playwright", "--no-up"])
if (command._tag !== "McpPlaywrightUp") {
throw new Error("expected McpPlaywrightUp command")
}
expect(command.runUp).toBe(false)
}))

it.effect("parses mcp-playwright with positional repo url into project dir", () =>
Effect.sync(() => {
const command = parseOrThrow(["mcp-playwright", "https://github.com/org/repo.git"])
if (command._tag !== "McpPlaywrightUp") {
throw new Error("expected McpPlaywrightUp command")
}
expect(command.projectDir).toBe(".docker-git/org/repo")
}))

it.effect("parses down-all command", () =>
Effect.sync(() => {
const command = parseOrThrow(["down-all"])
Expand Down
7 changes: 7 additions & 0 deletions packages/lib/src/core/domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,12 @@ export interface ScrapImportCommand {
readonly mode: ScrapMode
}

export interface McpPlaywrightUpCommand {
readonly _tag: "McpPlaywrightUp"
readonly projectDir: string
readonly runUp: boolean
}

export interface HelpCommand {
readonly _tag: "Help"
readonly message: string
Expand Down Expand Up @@ -213,6 +219,7 @@ export type Command =
| PanesCommand
| SessionsCommand
| ScrapCommand
| McpPlaywrightUpCommand
| HelpCommand
| StatusCommand
| DownAllCommand
Expand Down
90 changes: 90 additions & 0 deletions packages/lib/src/usecases/mcp-playwright.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import type { CommandExecutor } from "@effect/platform/CommandExecutor"
import type { PlatformError } from "@effect/platform/Error"
import type { FileSystem } from "@effect/platform/FileSystem"
import type { Path } from "@effect/platform/Path"
import { Effect } from "effect"

import type { McpPlaywrightUpCommand, TemplateConfig } from "../core/domain.js"
import { readProjectConfig } from "../shell/config.js"
import { ensureDockerDaemonAccess } from "../shell/docker.js"
import type {
ConfigDecodeError,
ConfigNotFoundError,
DockerAccessError,
DockerCommandError,
FileExistsError,
PortProbeError
} from "../shell/errors.js"
import { writeProjectFiles } from "../shell/files.js"
import { ensureCodexConfigFile } from "./auth-sync.js"
import { runDockerComposeUpWithPortCheck } from "./projects-up.js"

type McpPlaywrightFilesError = ConfigNotFoundError | ConfigDecodeError | FileExistsError | PlatformError
type McpPlaywrightFilesEnv = FileSystem | Path

const enableInTemplate = (template: TemplateConfig): TemplateConfig => ({
...template,
enableMcpPlaywright: true
})

// CHANGE: enable Playwright MCP in an existing docker-git project directory (files only)
// WHY: allow adding the browser sidecar + MCP server config without wiping env or volumes
// QUOTE(ТЗ): "Добавить возможность поднимать MCP Playrgiht в контейнере который уже создан"
// REF: issue-29
// SOURCE: n/a
// FORMAT THEOREM: forall p: enable(p) -> template(p).enableMcpPlaywright = true
// PURITY: SHELL
// EFFECT: Effect<TemplateConfig, ConfigNotFoundError | ConfigDecodeError | FileExistsError | PlatformError, FileSystem | Path>
// INVARIANT: does not rewrite .orch/env/project.env (only managed templates + docker-git.json)
// COMPLEXITY: O(n) where n = |managed_files|
export const enableMcpPlaywrightProjectFiles = (
projectDir: string
): Effect.Effect<TemplateConfig, McpPlaywrightFilesError, McpPlaywrightFilesEnv> =>
Effect.gen(function*(_) {
const config = yield* _(readProjectConfig(projectDir))
const alreadyEnabled = config.template.enableMcpPlaywright
const updated = alreadyEnabled ? config.template : enableInTemplate(config.template)

yield* _(
alreadyEnabled
? Effect.log("Playwright MCP is already enabled for this project.")
: Effect.log("Enabling Playwright MCP for this project (templates only)...")
)

yield* _(writeProjectFiles(projectDir, updated, true))
yield* _(ensureCodexConfigFile(projectDir, updated.codexAuthPath))

return updated
})

export type McpPlaywrightUpError =
| McpPlaywrightFilesError
| DockerAccessError
| DockerCommandError
| PortProbeError

type McpPlaywrightUpEnv = McpPlaywrightFilesEnv | CommandExecutor

// CHANGE: enable Playwright MCP in an existing project dir and bring docker compose up
// WHY: upgrade already created containers to support browser automation without forcing full recreation flows
// QUOTE(ТЗ): "Добавить возможность поднимать MCP Playrgiht в контейнере который уже создан"
// REF: issue-29
// SOURCE: n/a
// FORMAT THEOREM: forall p: up(p) -> running(p-browser) OR docker_error
// PURITY: SHELL
// EFFECT: Effect<TemplateConfig, McpPlaywrightUpError, FileSystem | Path | CommandExecutor>
// INVARIANT: volumes are preserved (no docker compose down -v)
// COMPLEXITY: O(command)
export const mcpPlaywrightUp = (
command: McpPlaywrightUpCommand
): Effect.Effect<TemplateConfig, McpPlaywrightUpError, McpPlaywrightUpEnv> =>
Effect.gen(function*(_) {
const updated = yield* _(enableMcpPlaywrightProjectFiles(command.projectDir))

if (!command.runUp) {
return updated
}

yield* _(ensureDockerDaemonAccess(process.cwd()))
return yield* _(runDockerComposeUpWithPortCheck(command.projectDir))
})
129 changes: 129 additions & 0 deletions packages/lib/tests/usecases/mcp-playwright.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import * as FileSystem from "@effect/platform/FileSystem"
import * as Path from "@effect/platform/Path"
import { NodeContext } from "@effect/platform-node"
import { describe, expect, it } from "@effect/vitest"
import { Effect } from "effect"

import type { TemplateConfig } from "../../src/core/domain.js"
import { enableMcpPlaywrightProjectFiles } from "../../src/usecases/mcp-playwright.js"
import { prepareProjectFiles } from "../../src/usecases/actions/prepare-files.js"

const withTempDir = <A, E, R>(
use: (tempDir: string) => Effect.Effect<A, E, R>
): Effect.Effect<A, E, R | FileSystem.FileSystem> =>
Effect.scoped(
Effect.gen(function*(_) {
const fs = yield* _(FileSystem.FileSystem)
const tempDir = yield* _(
fs.makeTempDirectoryScoped({
prefix: "docker-git-mcp-playwright-"
})
)
return yield* _(use(tempDir))
})
)

const makeGlobalConfig = (root: string, path: Path.Path): TemplateConfig => ({
containerName: "dg-test",
serviceName: "dg-test",
sshUser: "dev",
sshPort: 2222,
repoUrl: "https://github.com/org/repo.git",
repoRef: "main",
targetDir: "/home/dev/org/repo",
volumeName: "dg-test-home",
dockerGitPath: path.join(root, ".docker-git"),
authorizedKeysPath: path.join(root, "authorized_keys"),
envGlobalPath: path.join(root, ".orch/env/global.env"),
envProjectPath: path.join(root, ".orch/env/project.env"),
codexAuthPath: path.join(root, ".orch/auth/codex"),
codexSharedAuthPath: path.join(root, ".orch/auth/codex-shared"),
codexHome: "/home/dev/.codex",
enableMcpPlaywright: false,
pnpmVersion: "10.27.0"
})

const makeProjectConfig = (
outDir: string,
enableMcpPlaywright: boolean,
path: Path.Path
): TemplateConfig => ({
containerName: "dg-test",
serviceName: "dg-test",
sshUser: "dev",
sshPort: 2222,
repoUrl: "https://github.com/org/repo.git",
repoRef: "main",
targetDir: "/home/dev/org/repo",
volumeName: "dg-test-home",
dockerGitPath: path.join(outDir, ".docker-git"),
authorizedKeysPath: path.join(outDir, "authorized_keys"),
envGlobalPath: path.join(outDir, ".orch/env/global.env"),
envProjectPath: path.join(outDir, ".orch/env/project.env"),
codexAuthPath: path.join(outDir, ".orch/auth/codex"),
codexSharedAuthPath: path.join(outDir, ".orch/auth/codex-shared"),
codexHome: "/home/dev/.codex",
enableMcpPlaywright,
pnpmVersion: "10.27.0"
})

const isRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" && value !== null

const readEnableMcpPlaywrightFlag = (value: unknown): boolean | undefined => {
if (!isRecord(value)) {
return undefined
}

const template = value.template
if (!isRecord(template)) {
return undefined
}

const flag = template.enableMcpPlaywright
return typeof flag === "boolean" ? flag : undefined
}

describe("enableMcpPlaywrightProjectFiles", () => {
it.effect("enables Playwright MCP for an existing project without rewriting env files", () =>
withTempDir((root) =>
Effect.gen(function*(_) {
const fs = yield* _(FileSystem.FileSystem)
const path = yield* _(Path.Path)
const outDir = path.join(root, "project")
const globalConfig = makeGlobalConfig(root, path)
const withoutMcp = makeProjectConfig(outDir, false, path)

yield* _(
prepareProjectFiles(outDir, root, globalConfig, withoutMcp, {
force: false,
forceEnv: false
})
)

const envProjectPath = path.join(outDir, ".orch/env/project.env")
yield* _(fs.writeFileString(envProjectPath, "# custom env\nCUSTOM_KEY=1\n"))

yield* _(enableMcpPlaywrightProjectFiles(outDir))

const envAfter = yield* _(fs.readFileString(envProjectPath))
expect(envAfter).toContain("CUSTOM_KEY=1")

const composeAfter = yield* _(fs.readFileString(path.join(outDir, "docker-compose.yml")))
expect(composeAfter).toContain("dg-test-browser")
expect(composeAfter).toContain('MCP_PLAYWRIGHT_ENABLE: "1"')

const dockerfileAfter = yield* _(fs.readFileString(path.join(outDir, "Dockerfile")))
expect(dockerfileAfter).toContain("@playwright/mcp")

const browserDockerfileExists = yield* _(fs.exists(path.join(outDir, "Dockerfile.browser")))
const startExtraExists = yield* _(fs.exists(path.join(outDir, "mcp-playwright-start-extra.sh")))
expect(browserDockerfileExists).toBe(true)
expect(startExtraExists).toBe(true)

const configAfterText = yield* _(fs.readFileString(path.join(outDir, "docker-git.json")))
const configAfter = yield* _(Effect.sync((): unknown => JSON.parse(configAfterText)))
expect(readEnableMcpPlaywrightFlag(configAfter)).toBe(true)
})
).pipe(Effect.provide(NodeContext.layer)))
})
1 change: 0 additions & 1 deletion scripts/npx
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,3 @@ if [ "${1-}" = "" ]; then
fi

exec pnpm exec "$@"