feat: rename to ddev-claude-cli, drop Codex CLI, add opt-in headless browser
This commit is contained in:
1 parent
b6d88ce4dc
commit
d8076fa5a0
8 files changed
+322
-64
No files matched your search
Executable
+46
@@ -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
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user