commit 63f3199076aa554f6893d8ceae8d4d15eaa9801d Author: Björn Fromme Date: Wed Sep 16 12:37:50 2026 +0200 chore: initial commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..d0206f1 --- /dev/null +++ b/README.md @@ -0,0 +1,185 @@ +# ddev-codex-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). + +## 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)) +- 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 +- seeds the project-local HOME from the container home on first bootstrap for `.gitignore_global` when present +- 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: + + ${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. + +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: + +| Path | Role | +|------|------| +| `.ddev/claude-code/install.sh` | Claude Code install script | +| `.ddev/claude-code/home/.local/bin/claude` | Claude Code CLI binary | +| `.ddev/claude-code/home` | `HOME` | +| `.ddev/claude-code/config` | `XDG_CONFIG_HOME` | +| `.ddev/claude-code/data` | `XDG_DATA_HOME` | +| `.ddev/claude-code/state` | `XDG_STATE_HOME` | + +Authentication and settings are **per project**: sign in once per project; they persist across container restarts. + +## Install + +From a DDEV project root: + + ddev add-on get /path/to/ddev-codex-claude-cli + +Then restart: + + ddev restart + +## Usage + + ddev codex + + ddev codex --version + + ddev claude + + ddev claude --version + + ddev claude update + + ddev claude-swap list # opt-in; see "Multiple Claude accounts" + +## Updates + +Codex CLI is installed lazily when `ddev codex` runs and the persisted binary is missing or broken. + +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. + +To check for and install a newer Claude Code stable release, run: + + ddev claude update + +## Multiple Claude accounts + +The add-on can integrate [`claude-swap`](https://github.com/realiti4/claude-swap) +(`cswap`) so one project can hold several Claude accounts and swap between them — +mainly to keep working past a 5-hour or weekly rate limit. It is **opt-in**: nothing +is installed during `ddev start`; the first `ddev claude-swap` call (or first +`ddev claude --account …`) installs `uv` and `claude-swap` into the project-local +Claude profile under `.ddev/claude-code/`. + +Accounts are stored **per project**, next to the rest of the Claude state: + + .ddev/claude-code/data/claude-swap/ + +They persist across restarts and are deleted when the add-on is removed. Back them up +first with `ddev claude-swap export backup.cswap` if you need to keep them. + +### Swapping accounts without losing your session + +A running `ddev claude` conversation is not tied to the auth token, and Claude Code +re-reads its credentials file whenever it changes. So: + +1. Terminal 1: you are in `ddev claude`, working, and hit (or approach) the limit. +2. Terminal 2 (on the host): `ddev claude-swap switch ` + (or `ddev claude-swap switch --strategy best`). +3. Back in terminal 1: send the next message — it goes out on the new account, in the + **same session** with all its context. If a request just failed with the limit + error, resend it. + +To automate step 2, run `ddev claude-swap auto` in a spare terminal (or an +`ddev ssh` shell); it watches usage and switches around 90%. + +### One-time setup + + ddev claude # sign in as your first account + ddev claude-swap add --alias a # capture it + ddev claude # /login inside Claude Code as the next account, then exit + ddev claude-swap add --alias b # capture it + ddev claude-swap list # all accounts with usage and reset times + ddev claude-swap status # currently active account + +`ddev claude-swap` forwards every argument straight to `cswap`, so any `cswap` +subcommand works (`switch`, `run`, `map`, `disable`, `export`, `import`, `tui`, …). +`ddev claude-swap upgrade` updates `claude-swap` to the latest release. + +### Starting a session pinned to an account + +Separately from the hot-swap flow, you can launch a session bound to one account +(with its own per-account chat history): + + ddev claude --account b + ddev claude --account b --resume # remaining args pass straight to Claude Code + +Set a project default so plain `ddev claude` always uses it: + + echo b > .ddev/claude-code/claude-swap-account + # or export DDEV_CLAUDE_SWAP_ACCOUNT=b + +Remove the file (or unset the variable) to go back to the normal single-account +launch. + +### Requirements + +The first install needs outbound network access to `astral.sh`, `pypi.org`, +`files.pythonhosted.org`, and `github.com` (for the `uv`-managed Python 3.12). +`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. + +## Installed managed files + + .ddev/config.codex-cli.yaml + .ddev/config.claude-code.yaml + .ddev/commands/web/codex + .ddev/commands/web/claude + .ddev/commands/web/claude-swap + .ddev/codex-cli/install.sh + .ddev/claude-code/install.sh + .ddev/claude-code/install-swap.sh + +## Notes + +- 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` +- 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 +- if you do not want add-on or CLI runtime files in your repository, ignore them in git, for example: + + /.ddev/addon-metadata/ddev-codex-claude-cli + /.ddev/config.codex-cli.yaml + /.ddev/config.claude-code.yaml + /.ddev/commands/web/codex + /.ddev/commands/web/claude + /.ddev/commands/web/claude-swap + /.ddev/codex-cli/ + /.ddev/claude-code/ + +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. + +## 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. diff --git a/claude-code/install-swap.sh b/claude-code/install-swap.sh new file mode 100755 index 0000000..0c2ce4e --- /dev/null +++ b/claude-code/install-swap.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +#ddev-generated +set -euo pipefail + +# Lazy installer for claude-swap (cswap): a multi-account manager for Claude Code. +# It is NOT installed during "ddev start" - the first "ddev claude-swap ..." call +# (or "ddev claude --account ...") runs this script, which installs uv and then +# "claude-swap" into the same project-local, persisted profile that the Claude +# Code CLI uses. Pass --upgrade to update an existing install. +# +# All state lives under .ddev/claude-code so it survives container rebuilds +# without being committed to the project repository, exactly like install.sh. +PERSIST_BASE="${DDEV_APPROOT}/.ddev/claude-code" +PERSIST_HOME="${PERSIST_BASE}/home" # replaces $HOME inside the container +PERSIST_CONFIG_HOME="${PERSIST_BASE}/config" # XDG_CONFIG_HOME +PERSIST_DATA_HOME="${PERSIST_BASE}/data" # XDG_DATA_HOME +PERSIST_STATE_HOME="${PERSIST_BASE}/state" # XDG_STATE_HOME +PERSIST_BIN_DIR="${PERSIST_HOME}/.local/bin" # shared with the claude launcher +UV_BIN="${PERSIST_BIN_DIR}/uv" +CSWAP_BIN="${PERSIST_BIN_DIR}/cswap" + +mkdir -p \ + "${PERSIST_HOME}" \ + "${PERSIST_CONFIG_HOME}" \ + "${PERSIST_DATA_HOME}" \ + "${PERSIST_STATE_HOME}" \ + "${PERSIST_BIN_DIR}" + +# Point HOME / XDG paths at the persisted profile before any install or health +# check so cswap's own data (Linux: ${XDG_DATA_HOME}/claude-swap) is per-project +# and survives restarts with no extra configuration. +export HOME="${PERSIST_HOME}" +export XDG_CONFIG_HOME="${PERSIST_CONFIG_HOME}" +export XDG_DATA_HOME="${PERSIST_DATA_HOME}" +export XDG_STATE_HOME="${PERSIST_STATE_HOME}" +export PATH="${PERSIST_BIN_DIR}:${PATH}" + +# Keep every artifact uv creates inside the persisted tree. +export UV_INSTALL_DIR="${PERSIST_BIN_DIR}" # uv / uvx binaries +export UV_TOOL_BIN_DIR="${PERSIST_BIN_DIR}" # tool entry points (cswap) +export UV_TOOL_DIR="${XDG_DATA_HOME}/uv/tools" # tool virtualenvs +export UV_PYTHON_INSTALL_DIR="${XDG_DATA_HOME}/uv/python" # uv-managed CPython +export UV_CACHE_DIR="${XDG_DATA_HOME}/uv/cache" # download / build cache + +# Remove an unrunnable persisted uv (e.g. wrong arch after a host OS change). +if [ -x "${UV_BIN}" ] && ! "${UV_BIN}" --version >/dev/null 2>&1; then + echo "Persisted uv binary is not runnable, reinstalling ..." + rm -f "${UV_BIN}" "${PERSIST_BIN_DIR}/uvx" +fi + +# Install uv when missing. uv fetches its own CPython 3.12 below, so the DDEV web +# image's system Python (3.11) is never relied upon. Do not let the installer +# edit shell rc files in the persisted HOME - PATH is set by the launchers. +if [ ! -x "${UV_BIN}" ]; then + echo "Installing uv ..." + curl -fsSL https://astral.sh/uv/install.sh | env INSTALLER_NO_MODIFY_PATH=1 sh +fi + +if [ ! -x "${UV_BIN}" ]; then + echo "uv installation completed but no executable was found at ${UV_BIN}." >&2 + echo "If outbound access to astral.sh is blocked this step cannot succeed." >&2 + exit 1 +fi + +if [ "${1:-}" = "--upgrade" ]; then + echo "Upgrading claude-swap ..." + "${UV_BIN}" tool upgrade claude-swap + echo "claude-swap version: $("${CSWAP_BIN}" --version)" + exit 0 +fi + +# Install claude-swap when missing or unrunnable. +if [ ! -x "${CSWAP_BIN}" ] || ! "${CSWAP_BIN}" --version >/dev/null 2>&1; then + echo "Installing claude-swap ..." + "${UV_BIN}" tool install --force --python 3.12 claude-swap +fi + +if [ ! -x "${CSWAP_BIN}" ] || ! "${CSWAP_BIN}" --version >/dev/null 2>&1; then + echo "claude-swap installation completed but ${CSWAP_BIN} is not runnable." >&2 + echo "First install needs outbound access to astral.sh, pypi.org," >&2 + echo "files.pythonhosted.org and github.com (uv-managed CPython)." >&2 + exit 1 +fi + +echo "claude-swap version: $("${CSWAP_BIN}" --version)" diff --git a/claude-code/install.sh b/claude-code/install.sh new file mode 100755 index 0000000..60596f6 --- /dev/null +++ b/claude-code/install.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +#ddev-generated +set -euo pipefail + +# All Claude Code state is stored under .ddev/claude-code so it survives +# container rebuilds without being committed to the project repository. +PERSIST_BASE="${DDEV_APPROOT}/.ddev/claude-code" +PERSIST_HOME="${PERSIST_BASE}/home" # replaces $HOME inside the container +PERSIST_CONFIG_HOME="${PERSIST_BASE}/config" # XDG_CONFIG_HOME (e.g. ~/.config) +PERSIST_DATA_HOME="${PERSIST_BASE}/data" # XDG_DATA_HOME (e.g. ~/.local/share) +PERSIST_STATE_HOME="${PERSIST_BASE}/state" # XDG_STATE_HOME (e.g. ~/.local/state) +PERSIST_BIN_DIR="${PERSIST_HOME}/.local/bin" # Claude native installer target +PERSIST_BIN="${PERSIST_BIN_DIR}/claude" # full path to the claude executable +PERSIST_GITIGNORE_GLOBAL="${PERSIST_HOME}/.gitignore_global" # mirrors $HOME/.gitignore_global + +# Capture the container's original HOME before we override it below, so we can +# seed the persisted gitignore from the container image on first run. +CONTAINER_HOME="${HOME:?HOME must be set}" +CONTAINER_GITIGNORE_GLOBAL="${CONTAINER_HOME}/.gitignore_global" + +# Ensure all persistence directories exist. +mkdir -p \ + "${PERSIST_HOME}" \ + "${PERSIST_CONFIG_HOME}" \ + "${PERSIST_DATA_HOME}" \ + "${PERSIST_STATE_HOME}" \ + "${PERSIST_BIN_DIR}" + +# Seed the persisted gitignore from the container image on first run only. +if [ -f "${CONTAINER_GITIGNORE_GLOBAL}" ] && [ ! -f "${PERSIST_GITIGNORE_GLOBAL}" ]; then + cp -f "${CONTAINER_GITIGNORE_GLOBAL}" "${PERSIST_GITIGNORE_GLOBAL}" +fi + +# Claude Code's native installer stores its launcher, auth, settings, downloads, +# and versioned package data relative to HOME/XDG paths. +export HOME="${PERSIST_HOME}" +export XDG_CONFIG_HOME="${PERSIST_CONFIG_HOME}" +export XDG_DATA_HOME="${PERSIST_DATA_HOME}" +export XDG_STATE_HOME="${PERSIST_STATE_HOME}" +export PATH="${PERSIST_BIN_DIR}:${PATH}" + +# If a persisted binary exists but fails to run (e.g. wrong architecture after +# a host OS change), remove it so the install step below fetches a fresh copy. +if [ -x "${PERSIST_BIN}" ] && ! "${PERSIST_BIN}" --version >/dev/null 2>&1; then + echo "Persisted Claude Code binary is not runnable, reinstalling ..." + rm -f "${PERSIST_BIN}" +fi + +# Download and install the current stable Claude Code native build only when +# missing. Use "ddev claude update" to check for and install newer versions. +if [ ! -x "${PERSIST_BIN}" ]; then + echo "Installing Claude Code CLI ..." + curl -fsSL https://claude.ai/install.sh | bash -s stable +fi + +# Verify the binary is present and runnable after installation. +if [ ! -x "${PERSIST_BIN}" ]; then + echo "Claude Code installation completed but no executable was found at ${PERSIST_BIN}." >&2 + exit 1 +fi + +if ! "${PERSIST_BIN}" --version >/dev/null 2>&1; then + echo "Claude Code installation completed but ${PERSIST_BIN} is not runnable." >&2 + exit 1 +fi + +# Point git's global excludes file at the persisted copy when available, +# so git inside the container respects user-defined ignore patterns. +if command -v git >/dev/null 2>&1 && [ -f "${PERSIST_GITIGNORE_GLOBAL}" ]; then + CURRENT_EXCLUDESFILE="$(git config --global --get core.excludesfile || true)" + if [ "${CURRENT_EXCLUDESFILE}" != "${PERSIST_GITIGNORE_GLOBAL}" ]; then + git config --global core.excludesfile "${PERSIST_GITIGNORE_GLOBAL}" + fi +fi + +echo "Claude Code version: $("${PERSIST_BIN}" --version)" diff --git a/codex-cli/install.sh b/codex-cli/install.sh new file mode 100755 index 0000000..3eccf5a --- /dev/null +++ b/codex-cli/install.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +#ddev-generated +set -euo pipefail + +# All Codex CLI state is stored under .ddev/codex-cli so it survives container +# rebuilds without being committed to the project repository. +PERSIST_BASE="${DDEV_APPROOT}/.ddev/codex-cli" +PERSIST_HOME="${PERSIST_BASE}/home" # replaces $HOME inside the container +PERSIST_CONFIG_HOME="${PERSIST_BASE}/config" # XDG_CONFIG_HOME (e.g. ~/.config) +PERSIST_DATA_HOME="${PERSIST_BASE}/data" # XDG_DATA_HOME (e.g. ~/.local/share) +PERSIST_STATE_HOME="${PERSIST_BASE}/state" # XDG_STATE_HOME (e.g. ~/.local/state) +PERSIST_BIN_DIR="${PERSIST_BASE}/bin" # directory that holds the codex binary +PERSIST_BIN="${PERSIST_BIN_DIR}/codex" # full path to the codex executable +PERSIST_GITIGNORE_GLOBAL="${PERSIST_HOME}/.gitignore_global" # mirrors $HOME/.gitignore_global + +# Capture the container's original HOME before we override it below, so we can +# seed the persisted gitignore from the container image on first run. +CONTAINER_HOME="${HOME:?HOME must be set}" +CONTAINER_GITIGNORE_GLOBAL="${CONTAINER_HOME}/.gitignore_global" + +# Ensure all persistence directories exist. +mkdir -p \ + "${PERSIST_HOME}" \ + "${PERSIST_CONFIG_HOME}" \ + "${PERSIST_DATA_HOME}" \ + "${PERSIST_STATE_HOME}" \ + "${PERSIST_BIN_DIR}" + +# Seed the persisted gitignore from the container image on first run only. +if [ -f "${CONTAINER_GITIGNORE_GLOBAL}" ] && [ ! -f "${PERSIST_GITIGNORE_GLOBAL}" ]; then + cp -f "${CONTAINER_GITIGNORE_GLOBAL}" "${PERSIST_GITIGNORE_GLOBAL}" +fi + +# Codex stores its standalone package tree under $HOME/.codex and links +# $CODEX_INSTALL_DIR/codex into that tree. Point HOME there before any install +# or health check so package data survives container restarts. +export HOME="${PERSIST_HOME}" +export CODEX_HOME="${PERSIST_HOME}/.codex" +export XDG_CONFIG_HOME="${PERSIST_CONFIG_HOME}" +export XDG_DATA_HOME="${PERSIST_DATA_HOME}" +export XDG_STATE_HOME="${PERSIST_STATE_HOME}" +export PATH="${PERSIST_BIN_DIR}:${HOME}/.local/bin:${PATH}" + +# If a persisted binary exists but fails to run (e.g. wrong architecture after +# a host OS change), remove it so the install step below fetches a fresh copy. +if [ -x "${PERSIST_BIN}" ] && ! "${PERSIST_BIN}" --version >/dev/null 2>&1; then + echo "Persisted Codex CLI binary is not runnable, reinstalling ..." + rm -f "${PERSIST_BIN}" +fi + +# Download and install the Codex CLI into the persisted bin directory. +if [ ! -x "${PERSIST_BIN}" ]; then + echo "Installing Codex CLI ..." + export CODEX_INSTALL_DIR="${PERSIST_BIN_DIR}" + export CODEX_NON_INTERACTIVE=1 + curl -fsSL https://github.com/openai/codex/releases/latest/download/install.sh | sh +fi + +# Verify the binary is present after installation. +if [ ! -x "${PERSIST_BIN}" ]; then + echo "Codex CLI installation completed but no executable was found at ${PERSIST_BIN}." >&2 + exit 1 +fi + +# Point git's global excludes file at the persisted copy when available, +# so git inside the container respects user-defined ignore patterns. +if command -v git >/dev/null 2>&1 && [ -f "${PERSIST_GITIGNORE_GLOBAL}" ]; then + CURRENT_EXCLUDESFILE="$(git config --global --get core.excludesfile || true)" + if [ "${CURRENT_EXCLUDESFILE}" != "${PERSIST_GITIGNORE_GLOBAL}" ]; then + git config --global core.excludesfile "${PERSIST_GITIGNORE_GLOBAL}" + fi +fi + +echo "Codex CLI version: $("${PERSIST_BIN}" --version)" diff --git a/commands/web/claude b/commands/web/claude new file mode 100755 index 0000000..a2c77e0 --- /dev/null +++ b/commands/web/claude @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +#ddev-generated +## Description: Run Claude Code CLI inside the web container +## Usage: claude [--account ] +## Example: "ddev claude --version" +set -euo pipefail + +INSTALL_SCRIPT="${DDEV_APPROOT}/.ddev/claude-code/install.sh" +SWAP_INSTALL_SCRIPT="${DDEV_APPROOT}/.ddev/claude-code/install-swap.sh" +PERSIST_BASE="${DDEV_APPROOT}/.ddev/claude-code" +PERSIST_HOME="${PERSIST_BASE}/home" +PERSIST_BIN="${PERSIST_HOME}/.local/bin/claude" +CSWAP_BIN="${PERSIST_HOME}/.local/bin/cswap" + +# Optional multi-account routing: "ddev claude --account " starts a session +# pinned to a claude-swap account via "cswap run". With no flag, a project +# default can come from $DDEV_CLAUDE_SWAP_ACCOUNT or the first line of +# .ddev/claude-code/claude-swap-account. When neither is set this behaves +# exactly like a plain Claude Code launch. +ACCOUNT="" +ARGS=() +while [ $# -gt 0 ]; do + case "$1" in + --account=*) ACCOUNT="${1#--account=}"; shift ;; + --account|-A) + if [ $# -lt 2 ]; then + echo "ddev claude: --account requires an argument" >&2 + exit 1 + fi + ACCOUNT="$2"; shift 2 ;; + -A*) ACCOUNT="${1#-A}"; shift ;; + *) ARGS+=("$1"); shift ;; + esac +done + +if [ -z "${ACCOUNT}" ]; then + if [ -n "${DDEV_CLAUDE_SWAP_ACCOUNT:-}" ]; then + ACCOUNT="${DDEV_CLAUDE_SWAP_ACCOUNT}" + elif [ -f "${PERSIST_BASE}/claude-swap-account" ]; then + ACCOUNT="$(head -n1 "${PERSIST_BASE}/claude-swap-account" | tr -d '[:space:]')" + fi +fi + +"${INSTALL_SCRIPT}" + +export HOME="${PERSIST_HOME}" +export XDG_CONFIG_HOME="${PERSIST_BASE}/config" +export XDG_DATA_HOME="${PERSIST_BASE}/data" +export XDG_STATE_HOME="${PERSIST_BASE}/state" +export PATH="${PERSIST_HOME}/.local/bin:${PATH}" +unset SSH_AUTH_SOCK + +cd "${DDEV_APPROOT}" + +if [ -n "${ACCOUNT}" ]; then + "${SWAP_INSTALL_SCRIPT}" + if [ "${#ARGS[@]}" -gt 0 ]; then + exec "${CSWAP_BIN}" run "${ACCOUNT}" -- "${ARGS[@]}" + fi + exec "${CSWAP_BIN}" run "${ACCOUNT}" +fi + +exec "${PERSIST_BIN}" ${ARGS[@]+"${ARGS[@]}"} diff --git a/commands/web/claude-swap b/commands/web/claude-swap new file mode 100755 index 0000000..462eaf7 --- /dev/null +++ b/commands/web/claude-swap @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +#ddev-generated +## Description: Manage multiple Claude accounts (claude-swap / cswap) +## Usage: claude-swap [command] +## Example: "ddev claude-swap list" +set -euo pipefail + +INSTALL_SCRIPT="${DDEV_APPROOT}/.ddev/claude-code/install-swap.sh" +PERSIST_BASE="${DDEV_APPROOT}/.ddev/claude-code" +PERSIST_HOME="${PERSIST_BASE}/home" +CSWAP_BIN="${PERSIST_HOME}/.local/bin/cswap" + +# Lazy install / health check (also handles "ddev claude-swap upgrade"). +if [ "${1:-}" = "upgrade" ]; then + exec "${INSTALL_SCRIPT}" --upgrade +fi +"${INSTALL_SCRIPT}" + +export HOME="${PERSIST_HOME}" +export XDG_CONFIG_HOME="${PERSIST_BASE}/config" +export XDG_DATA_HOME="${PERSIST_BASE}/data" +export XDG_STATE_HOME="${PERSIST_BASE}/state" +export PATH="${PERSIST_HOME}/.local/bin:${PATH}" +unset SSH_AUTH_SOCK + +cd "${DDEV_APPROOT}" +exec "${CSWAP_BIN}" "$@" diff --git a/commands/web/codex b/commands/web/codex new file mode 100755 index 0000000..189be17 --- /dev/null +++ b/commands/web/codex @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +#ddev-generated +## Description: Run Codex CLI inside the web container +## Usage: codex +## Example: "ddev codex --version" +set -euo pipefail + +INSTALL_SCRIPT="${DDEV_APPROOT}/.ddev/codex-cli/install.sh" +PERSIST_BASE="${DDEV_APPROOT}/.ddev/codex-cli" +PERSIST_BIN="${PERSIST_BASE}/bin/codex" + +"${INSTALL_SCRIPT}" + +export HOME="${PERSIST_BASE}/home" +export CODEX_HOME="${PERSIST_BASE}/home/.codex" +export XDG_CONFIG_HOME="${PERSIST_BASE}/config" +export XDG_DATA_HOME="${PERSIST_BASE}/data" +export XDG_STATE_HOME="${PERSIST_BASE}/state" +export PATH="${PERSIST_BASE}/bin:${HOME}/.local/bin:${PATH}" +unset SSH_AUTH_SOCK + +cd "${DDEV_APPROOT}" +exec "${PERSIST_BIN}" --sandbox danger-full-access --ask-for-approval on-request "$@" diff --git a/config.codex-claude-cli.yaml b/config.codex-claude-cli.yaml new file mode 100644 index 0000000..fc8a352 --- /dev/null +++ b/config.codex-claude-cli.yaml @@ -0,0 +1,9 @@ +#ddev-generated +webimage_extra_packages: + - ripgrep + - bubblewrap + - socat +hooks: + post-start: + - exec: .ddev/claude-code/install.sh + - exec: .ddev/codex-cli/install.sh diff --git a/install.yaml b/install.yaml new file mode 100644 index 0000000..a663515 --- /dev/null +++ b/install.yaml @@ -0,0 +1,25 @@ +name: ddev-codex-claude-cli + +project_files: + - config.codex-claude-cli.yaml + - commands/web/codex + - commands/web/claude + - commands/web/claude-swap + - codex-cli/install.sh + - claude-code/install.sh + - claude-code/install-swap.sh + +post_install_actions: + - | + #ddev-description: Make AI CLI scripts executable + set -eu + chmod +x commands/web/codex commands/web/claude commands/web/claude-swap codex-cli/install.sh claude-code/install.sh claude-code/install-swap.sh + +removal_actions: + - | + #ddev-description: Remove AI CLI add-on artifacts + rm -f config.codex-claude-cli.yaml commands/web/codex commands/web/claude commands/web/claude-swap 2>/dev/null || true + rm -rf codex-cli 2>/dev/null || true + rm -rf claude-code 2>/dev/null || true + +ddev_version_constraint: ">= v1.24.3"