chore: initial commit
This commit is contained in:
commit
63f3199076
9 files changed
+567
No files matched your search
@@ -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 <n|email|alias>`
|
||||||
|
(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.
|
||||||
Executable
+85
@@ -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)"
|
||||||
Executable
+76
@@ -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)"
|
||||||
Executable
+74
@@ -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)"
|
||||||
Executable
+63
@@ -0,0 +1,63 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#ddev-generated
|
||||||
|
## Description: Run Claude Code CLI inside the web container
|
||||||
|
## Usage: claude [--account <n|email|alias>]
|
||||||
|
## 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 <sel>" 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[@]}"}
|
||||||
Executable
+27
@@ -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}" "$@"
|
||||||
Executable
+23
@@ -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 "$@"
|
||||||
@@ -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
|
||||||
@@ -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"
|
||||||
Reference in new issue
Block a user