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
@@ -1,45 +1,30 @@
|
|||||||
# ddev-codex-claude-cli
|
# ddev-claude-cli
|
||||||
|
|
||||||
`ddev-codex-claude-cli` is a DDEV add-on that runs Codex CLI and Claude Code CLI inside the DDEV `web` container while keeping runtime state under the project's `.ddev` directory on the host (bind-mounted into the container).
|
`ddev-claude-cli` is a DDEV add-on that runs Claude Code CLI inside the DDEV `web` container while keeping runtime state under the project's `.ddev` directory on the host (bind-mounted into the container).
|
||||||
|
|
||||||
|
> Formerly `ddev-codex-claude-cli`. See [Upgrading from ddev-codex-claude-cli](#upgrading-from-ddev-codex-claude-cli).
|
||||||
|
|
||||||
## What it does
|
## What it does
|
||||||
|
|
||||||
- exposes `ddev codex`
|
|
||||||
- exposes `ddev claude`
|
- exposes `ddev claude`
|
||||||
- optionally exposes `ddev claude-swap` for juggling multiple Claude accounts (opt-in; see [Multiple Claude accounts](#multiple-claude-accounts))
|
- optionally exposes `ddev claude-swap` for juggling multiple Claude accounts (opt-in; see [Multiple Claude accounts](#multiple-claude-accounts))
|
||||||
|
- optionally runs a headless Chromium as a sidecar container and wires it into Claude Code as the `playwright` MCP server (opt-in; see [Headless browser](#headless-browser))
|
||||||
- optionally keeps read-only mirrors of sibling repos under `.ddev/claude-code/home/refs/` for Claude to grep (opt-in; see [Sibling repo mirrors](#sibling-repo-mirrors))
|
- optionally keeps read-only mirrors of sibling repos under `.ddev/claude-code/home/refs/` for Claude to grep (opt-in; see [Sibling repo mirrors](#sibling-repo-mirrors))
|
||||||
- installs Codex CLI lazily the first time you run `ddev codex` (or whenever the persisted binary is missing) by downloading the latest release from GitHub: `curl -fsSL https://github.com/openai/codex/releases/latest/download/install.sh | sh`
|
|
||||||
- installs Claude Code CLI during `ddev start` when the persisted binary is missing or broken by downloading the stable native build: `curl -fsSL https://claude.ai/install.sh | bash -s stable`
|
- installs Claude Code CLI during `ddev start` when the persisted binary is missing or broken by downloading the stable native build: `curl -fsSL https://claude.ai/install.sh | bash -s stable`
|
||||||
- keeps normal `ddev start` fast by skipping Claude Code installation when the persisted binary is already runnable
|
- keeps normal `ddev start` fast by skipping Claude Code installation when the persisted binary is already runnable
|
||||||
- persists each CLI's binary, HOME, and XDG paths under **project-local** storage so each DDEV project has its own profiles
|
- persists the CLI's binary, HOME, and XDG paths under **project-local** storage so each DDEV project has its own profile
|
||||||
- seeds the project-local HOME from the container home on first bootstrap for `.gitignore_global` when present
|
- seeds the project-local HOME from the container home on first bootstrap for `.gitignore_global` when present
|
||||||
- keeps its own artifacts out of the project's git via a managed block in `.git/info/exclude`, refreshed on every `ddev start` and `ddev claude` (see [Keeping add-on files out of git](#keeping-add-on-files-out-of-git))
|
- keeps its own artifacts out of the project's git via a managed block in `.git/info/exclude`, refreshed on every `ddev start` and `ddev claude` (see [Keeping add-on files out of git](#keeping-add-on-files-out-of-git))
|
||||||
- always runs Codex with `--sandbox danger-full-access --ask-for-approval untrusted`
|
|
||||||
|
|
||||||
## Runtime storage
|
## Runtime storage
|
||||||
|
|
||||||
Inside the web container, Codex CLI state lives under:
|
Inside the web container, Claude Code CLI state lives under:
|
||||||
|
|
||||||
${DDEV_APPROOT}/.ddev/codex-cli
|
|
||||||
|
|
||||||
Claude Code CLI state lives under:
|
|
||||||
|
|
||||||
${DDEV_APPROOT}/.ddev/claude-code
|
${DDEV_APPROOT}/.ddev/claude-code
|
||||||
|
|
||||||
On the host, those are your project's **`.ddev/codex-cli/`** and **`.ddev/claude-code/`** directories. They survive `ddev restart`, `ddev stop`, and `ddev poweroff` like any other project file. The same directories hold the add-on's **install scripts** (managed files) alongside runtime data created after the first start.
|
On the host, that is your project's **`.ddev/claude-code/`** directory. It survives `ddev restart`, `ddev stop`, and `ddev poweroff` like any other project file. The same directory holds the add-on's **install scripts** (managed files) alongside runtime data created after the first start.
|
||||||
|
|
||||||
Codex layout:
|
Layout:
|
||||||
|
|
||||||
| Path | Role |
|
|
||||||
|------|------|
|
|
||||||
| `.ddev/codex-cli/install.sh` | Codex install script |
|
|
||||||
| `.ddev/codex-cli/bin/codex` | Codex CLI binary |
|
|
||||||
| `.ddev/codex-cli/home` | `HOME` |
|
|
||||||
| `.ddev/codex-cli/config` | `XDG_CONFIG_HOME` |
|
|
||||||
| `.ddev/codex-cli/data` | `XDG_DATA_HOME` |
|
|
||||||
| `.ddev/codex-cli/state` | `XDG_STATE_HOME` |
|
|
||||||
|
|
||||||
Claude Code layout:
|
|
||||||
|
|
||||||
| Path | Role |
|
| Path | Role |
|
||||||
|------|------|
|
|------|------|
|
||||||
@@ -56,7 +41,7 @@ Authentication and settings are **per project**: sign in once per project; they
|
|||||||
|
|
||||||
From a DDEV project root:
|
From a DDEV project root:
|
||||||
|
|
||||||
ddev add-on get /path/to/ddev-codex-claude-cli
|
ddev add-on get /path/to/ddev-claude-cli
|
||||||
|
|
||||||
Then restart:
|
Then restart:
|
||||||
|
|
||||||
@@ -64,10 +49,6 @@ Then restart:
|
|||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
ddev codex
|
|
||||||
|
|
||||||
ddev codex --version
|
|
||||||
|
|
||||||
ddev claude
|
ddev claude
|
||||||
|
|
||||||
ddev claude --version
|
ddev claude --version
|
||||||
@@ -78,9 +59,9 @@ Then restart:
|
|||||||
|
|
||||||
ddev claude-refs # opt-in; see "Sibling repo mirrors"
|
ddev claude-refs # opt-in; see "Sibling repo mirrors"
|
||||||
|
|
||||||
## Updates
|
ddev claude-browser enable # opt-in; see "Headless browser"
|
||||||
|
|
||||||
Codex CLI is installed lazily when `ddev codex` runs and the persisted binary is missing or broken.
|
## Updates
|
||||||
|
|
||||||
Claude Code CLI is installed automatically on `ddev start` only when the persisted binary is missing or broken. Later starts run a fast health check and skip installation when Claude is already runnable.
|
Claude Code CLI is installed automatically on `ddev start` only when the persisted binary is missing or broken. Later starts run a fast health check and skip installation when Claude is already runnable.
|
||||||
|
|
||||||
@@ -155,6 +136,57 @@ The first install needs outbound network access to `astral.sh`, `pypi.org`,
|
|||||||
`claude-swap` captures whichever account is signed in at `cswap add` time, so the
|
`claude-swap` captures whichever account is signed in at `cswap add` time, so the
|
||||||
initial multi-account setup is a few manual `/login` round-trips.
|
initial multi-account setup is a few manual `/login` round-trips.
|
||||||
|
|
||||||
|
## Headless browser
|
||||||
|
|
||||||
|
The add-on can run a headless Chromium next to the web container so Claude can open the
|
||||||
|
site, click through it, fill in forms, read the console and network log, and take
|
||||||
|
screenshots. It uses Microsoft's [Playwright MCP](https://github.com/microsoft/playwright-mcp)
|
||||||
|
server image (`mcr.microsoft.com/playwright/mcp`) and needs no other add-on. It is **opt-in
|
||||||
|
per project**, because the extra container costs a few hundred MB of memory while the
|
||||||
|
project runs.
|
||||||
|
|
||||||
|
ddev claude-browser enable # copies the compose file into .ddev/ and restarts
|
||||||
|
ddev claude-browser status
|
||||||
|
ddev claude-browser disable # removes it again and restarts
|
||||||
|
|
||||||
|
What enabling does:
|
||||||
|
|
||||||
|
- copies `.ddev/claude-code/browser/docker-compose.claude-browser.yaml` (the template) to
|
||||||
|
`.ddev/docker-compose.claude-browser.yaml`, which adds a `claude-browser` service. The
|
||||||
|
first start builds a small layer on top of the upstream image so the browser runs as
|
||||||
|
your user; later starts reuse it.
|
||||||
|
- on that restart, and on every `ddev claude`, `install.sh` registers the service with
|
||||||
|
Claude Code at user scope as the MCP server `playwright`
|
||||||
|
(`http://claude-browser:8931/mcp`). After `disable`, the next start removes that
|
||||||
|
registration again, but only if it still points at the add-on's URL. A `playwright`
|
||||||
|
server you added yourself is never touched (and blocks the add-on's registration;
|
||||||
|
the start-up output says so).
|
||||||
|
|
||||||
|
Then, inside `ddev claude`, ask for things like "open the site and check the login form
|
||||||
|
works". `/mcp` should list `playwright` as connected. The first browser call in a session
|
||||||
|
asks for permission as usual; allow `mcp__playwright` in `/permissions` if you want it
|
||||||
|
unprompted.
|
||||||
|
|
||||||
|
How it is set up:
|
||||||
|
|
||||||
|
- The browser reaches the project at its normal URL (`https://<project>.ddev.site`) through
|
||||||
|
the DDEV router, and at `http://web` directly. It ignores certificate errors, since
|
||||||
|
DDEV's mkcert CA is not installed in the container.
|
||||||
|
- Each MCP session gets a fresh in-memory profile (`--isolated`), so several `ddev claude`
|
||||||
|
sessions can use the browser at once. Logins do not survive the session.
|
||||||
|
- The project is mounted at `/var/www/html`, the same path as in the web container. File
|
||||||
|
uploads and saved screenshots use paths Claude can open directly. Auto-named output goes
|
||||||
|
to `.ddev/claude-code/browser-output/`, which is gitignored with the rest of
|
||||||
|
`.ddev/claude-code/`.
|
||||||
|
- Port 8931 is only exposed on the project's Docker network, never published to the host.
|
||||||
|
- The image is pinned to a tested release. To use another one, set `CLAUDE_BROWSER_IMAGE` in
|
||||||
|
`.ddev/.env`, e.g. `CLAUDE_BROWSER_IMAGE=mcr.microsoft.com/playwright/mcp:v0.0.84`, then
|
||||||
|
`ddev restart`.
|
||||||
|
- While the enabled copy still has its `#ddev-generated` line, `install.sh` refreshes it from
|
||||||
|
the template after add-on upgrades and asks you to `ddev restart`. To customise the
|
||||||
|
service, delete that line from `.ddev/docker-compose.claude-browser.yaml`. Updates then
|
||||||
|
stop, and `ddev claude-browser disable` leaves the file for you to delete yourself.
|
||||||
|
|
||||||
## Sibling repo mirrors
|
## Sibling repo mirrors
|
||||||
|
|
||||||
If a project works alongside sibling repos on the same self-hosted Git host, Claude can be given
|
If a project works alongside sibling repos on the same self-hosted Git host, Claude can be given
|
||||||
@@ -305,41 +337,63 @@ changing the URL or rotating the token, run `claude mcp remove <name> -s user` a
|
|||||||
|
|
||||||
## Installed managed files
|
## Installed managed files
|
||||||
|
|
||||||
.ddev/config.codex-claude.yaml
|
.ddev/config.claude-cli.yaml
|
||||||
.ddev/commands/web/codex
|
|
||||||
.ddev/commands/web/claude
|
.ddev/commands/web/claude
|
||||||
.ddev/commands/web/claude-swap
|
.ddev/commands/web/claude-swap
|
||||||
.ddev/commands/web/claude-refs
|
.ddev/commands/web/claude-refs
|
||||||
.ddev/codex-cli/install.sh
|
.ddev/commands/host/claude-browser
|
||||||
.ddev/claude-code/install.sh
|
.ddev/claude-code/install.sh
|
||||||
.ddev/claude-code/install-swap.sh
|
.ddev/claude-code/install-swap.sh
|
||||||
.ddev/claude-code/refs-sync.sh
|
.ddev/claude-code/refs-sync.sh
|
||||||
.ddev/claude-code/git-exclude.sh
|
.ddev/claude-code/git-exclude.sh
|
||||||
|
.ddev/claude-code/browser-setup.sh
|
||||||
|
.ddev/claude-code/browser/docker-compose.claude-browser.yaml
|
||||||
|
|
||||||
|
Plus `.ddev/docker-compose.claude-browser.yaml` while the [headless browser](#headless-browser) is enabled.
|
||||||
|
|
||||||
## Keeping add-on files out of git
|
## Keeping add-on files out of git
|
||||||
|
|
||||||
Every artifact the add-on drops into a project — `.ddev/claude-code/`, `.ddev/codex-cli/`, the
|
Every artifact the add-on drops into a project — `.ddev/claude-code/`, the
|
||||||
`commands/web/*` wrappers, `.ddev/config.codex-claude.yaml`, `.ddev/addon-metadata/ddev-codex-claude-cli`,
|
command wrappers, `.ddev/config.claude-cli.yaml`, `.ddev/docker-compose.claude-browser.yaml`,
|
||||||
`.ddev/plans`, and `.ddev/claude-refs.txt` — should stay out of the project's git history.
|
`.ddev/addon-metadata/ddev-claude-cli`, `.ddev/plans`, `.ddev/claude-refs.txt`, and the legacy
|
||||||
|
`.ddev/codex-cli/` — should stay out of the project's git history.
|
||||||
|
|
||||||
`.ddev/claude-code/git-exclude.sh` maintains a marker-delimited block for this in the project's
|
`.ddev/claude-code/git-exclude.sh` maintains a marker-delimited block for this in the project's
|
||||||
`.git/info/exclude` (not `.gitignore`): a per-clone file that every git process honours, including
|
`.git/info/exclude` (not `.gitignore`): a per-clone file that every git process honours, including
|
||||||
git run from the host, regardless of `$HOME` or container config. `install.sh` runs it on every
|
git run from the host, regardless of `$HOME` or container config. `install.sh` runs it on every
|
||||||
`ddev start` and every `ddev claude` (and the codex installer runs the same script too), so the
|
`ddev start` and every `ddev claude`, so the
|
||||||
block always matches the add-on's current file list — nothing to copy or keep in sync by hand.
|
block always matches the add-on's current file list — nothing to copy or keep in sync by hand.
|
||||||
|
|
||||||
Content outside the `>>> ddev-codex-claude-cli >>>` / `<<< ddev-codex-claude-cli <<<` markers is
|
Content outside the `>>> ddev-claude-cli >>>` / `<<< ddev-claude-cli <<<` markers is
|
||||||
left untouched; only the block between them is rewritten. Personal patterns (`.DS_Store`, `.idea`,
|
left untouched; only the block between them is rewritten. A block left by the add-on's former
|
||||||
|
name (`ddev-codex-claude-cli`) is removed. Personal patterns (`.DS_Store`, `.idea`,
|
||||||
`CLAUDE.md`, …) are not part of this block — they belong in your own `~/.gitignore_global`, wired
|
`CLAUDE.md`, …) are not part of this block — they belong in your own `~/.gitignore_global`, wired
|
||||||
up via `core.excludesfile` as described below, since whether a given project tracks its own
|
up via `core.excludesfile` as described below, since whether a given project tracks its own
|
||||||
`CLAUDE.md` is not the add-on's call.
|
`CLAUDE.md` is not the add-on's call.
|
||||||
|
|
||||||
- if the container home has `.gitignore_global` (typically from host `~/.ddev/homeadditions/.gitignore_global`), it is copied into each project-local CLI `HOME` on first bootstrap; git's `core.excludesfile` is pointed at that project-local copy — this only affects git run *inside* the container, which is why the add-on's own artifacts use the `.git/info/exclude` block instead
|
- if the container home has `.gitignore_global` (typically from host `~/.ddev/homeadditions/.gitignore_global`), it is copied into the project-local `HOME` on first bootstrap; git's `core.excludesfile` is pointed at that project-local copy — this only affects git run *inside* the container, which is why the add-on's own artifacts use the `.git/info/exclude` block instead
|
||||||
- the first `ddev codex` in a project can take some time because it downloads the Codex CLI binary; later runs reuse `.ddev/codex-cli/bin/codex`
|
|
||||||
- the first `ddev start` after installing the add-on can take some time because it downloads Claude Code; later starts reuse `.ddev/claude-code/home/.local/bin/claude`
|
- the first `ddev start` after installing the add-on can take some time because it downloads Claude Code; later starts reuse `.ddev/claude-code/home/.local/bin/claude`
|
||||||
|
|
||||||
Removing the add-on deletes `.ddev/codex-cli/` and `.ddev/claude-code/` as well (local auth, config, and CLI binaries). Back up those directories first if you need to keep them. The `.git/info/exclude` block is left in place; an orphaned entry for a path that no longer exists is harmless.
|
Removing the add-on deletes `.ddev/claude-code/` and the browser compose file as well (local auth, config, and CLI binary). Back up that directory first if you need to keep them. The `.git/info/exclude` block is left in place; an orphaned entry for a path that no longer exists is harmless.
|
||||||
|
|
||||||
|
## Upgrading from ddev-codex-claude-cli
|
||||||
|
|
||||||
|
The add-on used to be called `ddev-codex-claude-cli` and also shipped Codex CLI
|
||||||
|
(`ddev codex`). Codex has been removed. To upgrade a project, install the new add-on over the
|
||||||
|
old one:
|
||||||
|
|
||||||
|
ddev add-on get /path/to/ddev-claude-cli
|
||||||
|
ddev restart
|
||||||
|
|
||||||
|
**Do not run `ddev add-on remove ddev-codex-claude-cli` first.** Its removal actions delete
|
||||||
|
`.ddev/claude-code/`, including your Claude login, settings, and claude-swap accounts.
|
||||||
|
|
||||||
|
Installing `ddev-claude-cli` takes over `.ddev/claude-code/` as it is. It also removes the old
|
||||||
|
add-on's managed files (`config.codex-claude.yaml`, `commands/web/codex`,
|
||||||
|
`codex-cli/install.sh`, each only while still marked `#ddev-generated`) and its
|
||||||
|
`addon-metadata` entry, so DDEV no longer lists it. `.ddev/codex-cli/`, which holds Codex
|
||||||
|
auth and config, is left in place, still gitignored, for you to back up or delete.
|
||||||
|
|
||||||
## macOS / Linux / WSL2
|
## macOS / Linux / WSL2
|
||||||
|
|
||||||
This add-on runs Codex CLI and Claude Code CLI in the DDEV container, so host OS differences are minimized as long as DDEV works normally.
|
This add-on runs Claude Code CLI in the DDEV container, so host OS differences are minimized as long as DDEV works normally.
|
||||||
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.
|
# they belong in ~/.gitignore_global, not to this add-on.
|
||||||
set -uo pipefail
|
set -uo pipefail
|
||||||
|
|
||||||
BEGIN='# >>> ddev-codex-claude-cli >>>'
|
BEGIN='# >>> ddev-claude-cli >>>'
|
||||||
END='# <<< ddev-codex-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=(
|
PATTERNS=(
|
||||||
'.ddev/addon-metadata/ddev-codex-claude-cli/'
|
'.ddev/addon-metadata/ddev-claude-cli/'
|
||||||
'.ddev/config.codex-claude.yaml'
|
'.ddev/config.claude-cli.yaml'
|
||||||
'.ddev/commands/web/claude'
|
'.ddev/commands/web/claude'
|
||||||
'.ddev/commands/web/claude-swap'
|
'.ddev/commands/web/claude-swap'
|
||||||
'.ddev/commands/web/claude-refs'
|
'.ddev/commands/web/claude-refs'
|
||||||
'.ddev/commands/web/codex'
|
'.ddev/commands/host/claude-browser'
|
||||||
|
'.ddev/docker-compose.claude-browser.yaml'
|
||||||
'.ddev/claude-code/'
|
'.ddev/claude-code/'
|
||||||
'.ddev/codex-cli/'
|
|
||||||
'.ddev/claude-refs.txt'
|
'.ddev/claude-refs.txt'
|
||||||
'.ddev/plans/'
|
'.ddev/plans/'
|
||||||
|
'.ddev/codex-cli/' # legacy: left behind by add-on versions that shipped Codex CLI
|
||||||
)
|
)
|
||||||
|
|
||||||
approot="${DDEV_APPROOT:-$(pwd)}"
|
approot="${DDEV_APPROOT:-$(pwd)}"
|
||||||
@@ -50,15 +54,23 @@ block="$(
|
|||||||
printf '%s\n' "${END}"
|
printf '%s\n' "${END}"
|
||||||
)"
|
)"
|
||||||
|
|
||||||
# Existing block replaced in place, otherwise appended after a blank line.
|
# Drop the pre-rename block first, then replace the current block in place or
|
||||||
if grep -qF "${BEGIN}" "${exclude}" && grep -qF "${END}" "${exclude}"; then
|
# 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}" '
|
updated="$(awk -v begin="${BEGIN}" -v end="${END}" -v block="${block}" '
|
||||||
index($0, begin) == 1 { print block; skip = 1; next }
|
index($0, begin) == 1 { print block; skip = 1; next }
|
||||||
skip && index($0, end) == 1 { skip = 0; next }
|
skip && index($0, end) == 1 { skip = 0; next }
|
||||||
!skip { print }
|
!skip { print }
|
||||||
' "${exclude}")"
|
' <<<"${current}")"
|
||||||
else
|
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
|
fi
|
||||||
|
|
||||||
# Only write on a real change, so "ddev start" does not touch the mtime every time.
|
# 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
|
"${PERSIST_BASE}/git-exclude.sh" || true
|
||||||
fi
|
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;
|
# 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,
|
# the sync runs detached so neither ddev start nor ddev claude waits on the network,
|
||||||
# and refs-sync.sh throttles itself.
|
# and refs-sync.sh throttles itself.
|
||||||
|
|||||||
Executable
+55
@@ -0,0 +1,55 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#ddev-generated
|
||||||
|
## Description: Turn the headless browser (Playwright MCP) for Claude Code on or off
|
||||||
|
## Usage: claude-browser [enable|disable|status]
|
||||||
|
## Example: "ddev claude-browser enable"
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
TEMPLATE="${DDEV_APPROOT}/.ddev/claude-code/browser/docker-compose.claude-browser.yaml"
|
||||||
|
TARGET="${DDEV_APPROOT}/.ddev/docker-compose.claude-browser.yaml"
|
||||||
|
CONTAINER="ddev-${DDEV_SITENAME}-claude-browser"
|
||||||
|
|
||||||
|
case "${1:-status}" in
|
||||||
|
enable|on)
|
||||||
|
if [ ! -f "${TEMPLATE}" ]; then
|
||||||
|
echo "Missing ${TEMPLATE}; reinstall the add-on with 'ddev add-on get'." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ -f "${TARGET}" ]; then
|
||||||
|
echo "The headless browser is already enabled (.ddev/docker-compose.claude-browser.yaml)."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
cp "${TEMPLATE}" "${TARGET}"
|
||||||
|
echo "Enabled the headless browser; restarting so the claude-browser service starts ..."
|
||||||
|
ddev restart
|
||||||
|
;;
|
||||||
|
disable|off)
|
||||||
|
if [ ! -f "${TARGET}" ]; then
|
||||||
|
echo "The headless browser is not enabled."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
if ! grep -q '#ddev-generated' "${TARGET}"; then
|
||||||
|
echo "${TARGET} has been customised (no #ddev-generated line); delete it yourself and run 'ddev restart'." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
rm -f "${TARGET}"
|
||||||
|
echo "Disabled the headless browser; restarting so the claude-browser service stops ..."
|
||||||
|
ddev restart
|
||||||
|
;;
|
||||||
|
status)
|
||||||
|
if [ -f "${TARGET}" ]; then
|
||||||
|
state="enabled"
|
||||||
|
else
|
||||||
|
state="disabled"
|
||||||
|
fi
|
||||||
|
running="unknown"
|
||||||
|
if command -v docker >/dev/null 2>&1; then
|
||||||
|
running="$(docker inspect -f '{{.State.Status}}' "${CONTAINER}" 2>/dev/null || echo "not running")"
|
||||||
|
fi
|
||||||
|
echo "Headless browser: ${state} (container ${CONTAINER}: ${running})"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Usage: ddev claude-browser [enable|disable|status]" >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
@@ -6,4 +6,3 @@ webimage_extra_packages:
|
|||||||
hooks:
|
hooks:
|
||||||
post-start:
|
post-start:
|
||||||
- exec: .ddev/claude-code/install.sh
|
- exec: .ddev/claude-code/install.sh
|
||||||
- exec: .ddev/codex-cli/install.sh
|
|
||||||
+19
-9
@@ -1,28 +1,38 @@
|
|||||||
name: ddev-codex-claude-cli
|
name: ddev-claude-cli
|
||||||
|
|
||||||
project_files:
|
project_files:
|
||||||
- config.codex-claude.yaml
|
- config.claude-cli.yaml
|
||||||
- commands/web/codex
|
|
||||||
- commands/web/claude
|
- commands/web/claude
|
||||||
- commands/web/claude-swap
|
- commands/web/claude-swap
|
||||||
- commands/web/claude-refs
|
- commands/web/claude-refs
|
||||||
- codex-cli/install.sh
|
- commands/host/claude-browser
|
||||||
- claude-code/install.sh
|
- claude-code/install.sh
|
||||||
- claude-code/install-swap.sh
|
- claude-code/install-swap.sh
|
||||||
- claude-code/refs-sync.sh
|
- claude-code/refs-sync.sh
|
||||||
- claude-code/git-exclude.sh
|
- claude-code/git-exclude.sh
|
||||||
|
- claude-code/browser-setup.sh
|
||||||
|
- claude-code/browser/docker-compose.claude-browser.yaml
|
||||||
|
|
||||||
post_install_actions:
|
post_install_actions:
|
||||||
- |
|
- |
|
||||||
#ddev-description: Make AI CLI scripts executable
|
#ddev-description: Make Claude CLI scripts executable
|
||||||
set -eu
|
set -eu
|
||||||
chmod +x commands/web/codex commands/web/claude commands/web/claude-swap commands/web/claude-refs codex-cli/install.sh claude-code/install.sh claude-code/install-swap.sh claude-code/refs-sync.sh claude-code/git-exclude.sh
|
chmod +x commands/web/claude commands/web/claude-swap commands/web/claude-refs commands/host/claude-browser claude-code/install.sh claude-code/install-swap.sh claude-code/refs-sync.sh claude-code/git-exclude.sh claude-code/browser-setup.sh
|
||||||
|
- |
|
||||||
|
#ddev-description: Clean up after ddev-codex-claude-cli, this add-on's former name
|
||||||
|
# Files are removed only while still marked #ddev-generated. .ddev/claude-code/ is shared
|
||||||
|
# with the old add-on and kept as is; .ddev/codex-cli/ (Codex auth and config) is kept too.
|
||||||
|
for f in config.codex-claude.yaml commands/web/codex codex-cli/install.sh; do
|
||||||
|
if [ -f "$f" ] && grep -q '#ddev-generated' "$f"; then rm -f "$f"; fi
|
||||||
|
done
|
||||||
|
# Forget the old add-on, so it is not listed twice and nobody runs its removal
|
||||||
|
# actions, which would delete .ddev/claude-code/.
|
||||||
|
rm -rf addon-metadata/ddev-codex-claude-cli
|
||||||
|
|
||||||
removal_actions:
|
removal_actions:
|
||||||
- |
|
- |
|
||||||
#ddev-description: Remove AI CLI add-on artifacts
|
#ddev-description: Remove Claude CLI add-on artifacts
|
||||||
rm -f config.codex-claude.yaml commands/web/codex commands/web/claude commands/web/claude-swap commands/web/claude-refs 2>/dev/null || true
|
rm -f config.claude-cli.yaml commands/web/claude commands/web/claude-swap commands/web/claude-refs commands/host/claude-browser docker-compose.claude-browser.yaml 2>/dev/null || true
|
||||||
rm -rf codex-cli 2>/dev/null || true
|
|
||||||
rm -rf claude-code 2>/dev/null || true
|
rm -rf claude-code 2>/dev/null || true
|
||||||
|
|
||||||
ddev_version_constraint: ">= v1.24.3"
|
ddev_version_constraint: ">= v1.24.3"
|
||||||
Reference in new issue
Block a user