diff --git a/README.md b/README.md index 5ef16c4..d708c4d 100644 --- a/README.md +++ b/README.md @@ -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 -- exposes `ddev codex` - exposes `ddev claude` - 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)) -- 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` - 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 - 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 -Inside the web container, Codex CLI state lives under: - - ${DDEV_APPROOT}/.ddev/codex-cli - -Claude Code CLI state lives under: +Inside the web container, Claude Code CLI state lives under: ${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: - -| 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: +Layout: | Path | Role | |------|------| @@ -56,7 +41,7 @@ Authentication and settings are **per project**: sign in once per project; they 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: @@ -64,10 +49,6 @@ Then restart: ## Usage - ddev codex - - ddev codex --version - ddev claude ddev claude --version @@ -78,9 +59,9 @@ Then restart: 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. @@ -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 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://.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 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 -s user` a ## Installed managed files - .ddev/config.codex-claude.yaml - .ddev/commands/web/codex + .ddev/config.claude-cli.yaml .ddev/commands/web/claude .ddev/commands/web/claude-swap .ddev/commands/web/claude-refs - .ddev/codex-cli/install.sh + .ddev/commands/host/claude-browser .ddev/claude-code/install.sh .ddev/claude-code/install-swap.sh .ddev/claude-code/refs-sync.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 -Every artifact the add-on drops into a project — `.ddev/claude-code/`, `.ddev/codex-cli/`, the -`commands/web/*` wrappers, `.ddev/config.codex-claude.yaml`, `.ddev/addon-metadata/ddev-codex-claude-cli`, -`.ddev/plans`, and `.ddev/claude-refs.txt` — should stay out of the project's git history. +Every artifact the add-on drops into a project — `.ddev/claude-code/`, the +command wrappers, `.ddev/config.claude-cli.yaml`, `.ddev/docker-compose.claude-browser.yaml`, +`.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 `.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 -`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. -Content outside the `>>> ddev-codex-claude-cli >>>` / `<<< ddev-codex-claude-cli <<<` markers is -left untouched; only the block between them is rewritten. Personal patterns (`.DS_Store`, `.idea`, +Content outside the `>>> ddev-claude-cli >>>` / `<<< ddev-claude-cli <<<` markers is +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 up via `core.excludesfile` as described below, since whether a given project tracks its own `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 -- 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` +- 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 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 -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. diff --git a/claude-code/browser-setup.sh b/claude-code/browser-setup.sh new file mode 100755 index 0000000..8f9bd03 --- /dev/null +++ b/claude-code/browser-setup.sh @@ -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 diff --git a/claude-code/browser/docker-compose.claude-browser.yaml b/claude-code/browser/docker-compose.claude-browser.yaml new file mode 100644 index 0000000..e4e5bd7 --- /dev/null +++ b/claude-code/browser/docker-compose.claude-browser.yaml @@ -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 diff --git a/claude-code/git-exclude.sh b/claude-code/git-exclude.sh index 49b5a6c..2aa862c 100755 --- a/claude-code/git-exclude.sh +++ b/claude-code/git-exclude.sh @@ -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. diff --git a/claude-code/install.sh b/claude-code/install.sh index 273a871..30ff738 100755 --- a/claude-code/install.sh +++ b/claude-code/install.sh @@ -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. diff --git a/commands/host/claude-browser b/commands/host/claude-browser new file mode 100755 index 0000000..a4b474b --- /dev/null +++ b/commands/host/claude-browser @@ -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 diff --git a/config.codex-claude.yaml b/config.claude-cli.yaml similarity index 78% rename from config.codex-claude.yaml rename to config.claude-cli.yaml index fc8a352..54571bd 100644 --- a/config.codex-claude.yaml +++ b/config.claude-cli.yaml @@ -6,4 +6,3 @@ webimage_extra_packages: hooks: post-start: - exec: .ddev/claude-code/install.sh - - exec: .ddev/codex-cli/install.sh diff --git a/install.yaml b/install.yaml index 813be6f..fc7f707 100644 --- a/install.yaml +++ b/install.yaml @@ -1,28 +1,38 @@ -name: ddev-codex-claude-cli +name: ddev-claude-cli project_files: - - config.codex-claude.yaml - - commands/web/codex + - config.claude-cli.yaml - commands/web/claude - commands/web/claude-swap - commands/web/claude-refs - - codex-cli/install.sh + - 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 + - claude-code/browser/docker-compose.claude-browser.yaml post_install_actions: - | - #ddev-description: Make AI CLI scripts executable + #ddev-description: Make Claude CLI scripts executable 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: - | - #ddev-description: Remove AI 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 -rf codex-cli 2>/dev/null || true + #ddev-description: Remove Claude CLI add-on artifacts + 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 claude-code 2>/dev/null || true ddev_version_constraint: ">= v1.24.3"