feat: rename to ddev-claude-cli, drop Codex CLI, add opt-in headless browser

This commit is contained in:
Björn Fromme committed 2026-11-08 12:13:11 +01:00
1 parent b6d88ce4dc
commit d8076fa5a0
8 files changed
+322 -64

No files matched your search

+46
View File
@@ -0,0 +1,46 @@
#!/usr/bin/env bash
#ddev-generated
# Keeps Claude Code's MCP config in step with the opt-in headless browser.
#
# Enabled (.ddev/docker-compose.claude-browser.yaml exists): registers the
# "playwright" MCP server at user scope, and refreshes an add-on-managed compose
# copy from the template after an add-on upgrade. Disabled: removes the
# registration, but only if it is still the one this script created.
#
# Called by install.sh with HOME already pointing at the persisted profile. Never
# fatal: a browser problem must not fail "ddev start" or "ddev claude".
set -uo pipefail
DDEV_DIR="${DDEV_APPROOT}/.ddev"
TEMPLATE="${DDEV_DIR}/claude-code/browser/docker-compose.claude-browser.yaml"
ENABLED="${DDEV_DIR}/docker-compose.claude-browser.yaml"
NAME="playwright"
URL="http://claude-browser:8931/mcp"
CLAUDE_JSON="${HOME}/.claude.json"
if ! command -v jq >/dev/null 2>&1; then
[ -f "${ENABLED}" ] && echo "claude-browser: jq missing, skipping MCP registration" >&2
exit 0
fi
# Read ~/.claude.json directly: "claude mcp get" would try to connect.
registered=0
jq -e --arg n "${NAME}" '.mcpServers[$n]' "${CLAUDE_JSON}" >/dev/null 2>&1 && registered=1
current_url="$(jq -r --arg n "${NAME}" '.mcpServers[$n].url // empty' "${CLAUDE_JSON}" 2>/dev/null)"
if [ -f "${ENABLED}" ]; then
if [ -f "${TEMPLATE}" ] && grep -q '#ddev-generated' "${ENABLED}" && ! cmp -s "${TEMPLATE}" "${ENABLED}"; then
cat "${TEMPLATE}" >"${ENABLED}"
echo "claude-browser: refreshed .ddev/docker-compose.claude-browser.yaml from the add-on; run 'ddev restart' to apply" >&2
fi
if [ "${registered}" -eq 0 ]; then
claude mcp add --transport http --scope user "${NAME}" "${URL}" >/dev/null \
&& echo "claude-browser: registered MCP server '${NAME}' (${URL})"
elif [ "${current_url}" != "${URL}" ]; then
echo "claude-browser: an MCP server named '${NAME}' already exists and is not the add-on's; leaving it alone" >&2
fi
elif [ "${registered}" -eq 1 ] && [ "${current_url}" = "${URL}" ]; then
claude mcp remove "${NAME}" -s user >/dev/null \
&& echo "claude-browser: unregistered MCP server '${NAME}'"
fi
exit 0
@@ -0,0 +1,76 @@
#ddev-generated
# Opt-in headless browser for Claude Code: Playwright MCP with headless Chromium.
#
# This is the template. It does nothing where it is: "ddev claude-browser enable"
# copies it to .ddev/docker-compose.claude-browser.yaml and restarts the project, and
# install.sh registers it with Claude Code as the "playwright" MCP server
# (http://claude-browser:8931/mcp). While the copy keeps the #ddev-generated line,
# install.sh refreshes it from this template after add-on upgrades; remove that line
# from the copy to customise it.
#
# Pick another release with CLAUDE_BROWSER_IMAGE in .ddev/.env, e.g.
# CLAUDE_BROWSER_IMAGE=mcr.microsoft.com/playwright/mcp:v0.0.84
services:
claude-browser:
image: ${CLAUDE_BROWSER_IMAGE:-mcr.microsoft.com/playwright/mcp:v0.0.83}-${DDEV_SITENAME}-built
build:
# The upstream image runs as "node" (uid 1000). Turn that user into the host user
# (same name, uid and gid) so screenshots and other files the browser writes into
# the mounted project belong to you. Same approach as ddev-selenium-standalone-chrome.
dockerfile_inline: |
ARG CLAUDE_BROWSER_IMAGE="scratch"
FROM $${CLAUDE_BROWSER_IMAGE}
USER root
ARG username
ARG uid
ARG gid
RUN set -eu; \
old_uid="$$(id -u node)"; old_gid="$$(id -g node)"; \
for u in "$$(getent passwd "$${uid}" | cut -d: -f1)" "$$(getent passwd "$${username}" | cut -d: -f1)"; do \
if [ -n "$${u}" ] && [ "$${u}" != "node" ]; then userdel "$${u}"; fi; \
done; \
for g in "$$(getent group "$${gid}" | cut -d: -f1)" "$$(getent group "$${username}" | cut -d: -f1)"; do \
if [ -n "$${g}" ] && [ "$${g}" != "node" ]; then groupdel "$${g}" || true; fi; \
done; \
groupmod -o -g "$${gid}" node; \
if ! getent group "$${username}" >/dev/null; then groupmod -n "$${username}" node; fi; \
usermod -o -u "$${uid}" -g "$${gid}" node; \
if [ "$${username}" != "node" ]; then usermod -l "$${username}" node; fi; \
find / -xdev -uid "$${old_uid}" -exec chown -h "$${uid}" {} +; \
find / -xdev -gid "$${old_gid}" -exec chgrp -h "$${gid}" {} +
USER $${uid}:$${gid}
args:
CLAUDE_BROWSER_IMAGE: ${CLAUDE_BROWSER_IMAGE:-mcr.microsoft.com/playwright/mcp:v0.0.83}
username: ${DDEV_USER}
uid: ${DDEV_UID}
gid: ${DDEV_GID}
container_name: ddev-${DDEV_SITENAME}-claude-browser
user: "${DDEV_UID}:${DDEV_GID}"
restart: "no"
# Chromium needs more than Docker's 64 MB default /dev/shm for heavier pages.
shm_size: 1gb
# Appended to the image's entrypoint (node cli.js --headless --browser chromium --no-sandbox).
command:
- --port=8931
- --host=0.0.0.0
# The port is only exposed on the project's Docker network, never published to
# the host, so the Host-header check adds nothing here.
- --allowed-hosts=*
# DDEV's mkcert CA is not in this container's trust store.
- --ignore-https-errors
# Fresh in-memory profile per session, so parallel "ddev claude" sessions never
# fight over one profile directory.
- --isolated
# Same path as in the web container, so paths in tool results work for Claude as-is.
- --output-dir=/var/www/html/.ddev/claude-code/browser-output
- --file-paths=absolute
expose:
- 8931
volumes:
# The project at the web container's path: file uploads and saved screenshots
# use paths Claude can read directly.
- "../:/var/www/html"
x-ddev:
describe-info: |
Playwright MCP: http://claude-browser:8931/mcp
Toggle with: ddev claude-browser enable|disable
+22 -10
View File
@@ -12,20 +12,24 @@
# they belong in ~/.gitignore_global, not to this add-on.
set -uo pipefail
BEGIN='# >>> ddev-codex-claude-cli >>>'
END='# <<< ddev-codex-claude-cli <<<'
BEGIN='# >>> ddev-claude-cli >>>'
END='# <<< ddev-claude-cli <<<'
# Block written by this add-on before it was renamed from ddev-codex-claude-cli; removed on sight.
LEGACY_BEGIN='# >>> ddev-codex-claude-cli >>>'
LEGACY_END='# <<< ddev-codex-claude-cli <<<'
PATTERNS=(
'.ddev/addon-metadata/ddev-codex-claude-cli/'
'.ddev/config.codex-claude.yaml'
'.ddev/addon-metadata/ddev-claude-cli/'
'.ddev/config.claude-cli.yaml'
'.ddev/commands/web/claude'
'.ddev/commands/web/claude-swap'
'.ddev/commands/web/claude-refs'
'.ddev/commands/web/codex'
'.ddev/commands/host/claude-browser'
'.ddev/docker-compose.claude-browser.yaml'
'.ddev/claude-code/'
'.ddev/codex-cli/'
'.ddev/claude-refs.txt'
'.ddev/plans/'
'.ddev/codex-cli/' # legacy: left behind by add-on versions that shipped Codex CLI
)
approot="${DDEV_APPROOT:-$(pwd)}"
@@ -50,15 +54,23 @@ block="$(
printf '%s\n' "${END}"
)"
# Existing block replaced in place, otherwise appended after a blank line.
if grep -qF "${BEGIN}" "${exclude}" && grep -qF "${END}" "${exclude}"; then
# Drop the pre-rename block first, then replace the current block in place or
# append it after a blank line.
current="$(awk -v begin="${LEGACY_BEGIN}" -v end="${LEGACY_END}" '
index($0, begin) == 1 { skip = 1; next }
skip && index($0, end) == 1 { skip = 0; eat = 1; next }
eat && $0 == "" { eat = 0; next }
{ eat = 0 }
!skip { print }
' "${exclude}")"
if grep -qF "${BEGIN}" <<<"${current}" && grep -qF "${END}" <<<"${current}"; then
updated="$(awk -v begin="${BEGIN}" -v end="${END}" -v block="${block}" '
index($0, begin) == 1 { print block; skip = 1; next }
skip && index($0, end) == 1 { skip = 0; next }
!skip { print }
' "${exclude}")"
' <<<"${current}")"
else
updated="$(cat "${exclude}"; [ -s "${exclude}" ] && printf '\n'; printf '%s\n' "${block}")"
updated="$(printf '%s' "${current}"; [ -n "${current}" ] && printf '\n\n'; printf '%s\n' "${block}")"
fi
# Only write on a real change, so "ddev start" does not touch the mtime every time.
+6
View File
@@ -80,6 +80,12 @@ if [ -x "${PERSIST_BASE}/git-exclude.sh" ]; then
"${PERSIST_BASE}/git-exclude.sh" || true
fi
# Headless browser, opt-in via "ddev claude-browser enable". Registers or removes the
# Playwright MCP server to match; local and quick, never fatal.
if [ -x "${PERSIST_BASE}/browser-setup.sh" ]; then
"${PERSIST_BASE}/browser-setup.sh" || true
fi
# Sibling-repo mirrors, opt-in via .ddev/claude-refs.txt. Setup is local and quick;
# the sync runs detached so neither ddev start nor ddev claude waits on the network,
# and refs-sync.sh throttles itself.