chore: no idea?
This commit is contained in:
1 parent
63f3199076
commit
afc232ac27
8 files changed
+411
-18
No files matched your search
@@ -7,11 +7,13 @@
|
|||||||
- exposes `ddev codex`
|
- 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 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 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 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
|
- 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`
|
- always runs Codex with `--sandbox danger-full-access --ask-for-approval untrusted`
|
||||||
|
|
||||||
## Runtime storage
|
## Runtime storage
|
||||||
@@ -74,6 +76,8 @@ Then restart:
|
|||||||
|
|
||||||
ddev claude-swap list # opt-in; see "Multiple Claude accounts"
|
ddev claude-swap list # opt-in; see "Multiple Claude accounts"
|
||||||
|
|
||||||
|
ddev claude-refs # opt-in; see "Sibling repo mirrors"
|
||||||
|
|
||||||
## Updates
|
## Updates
|
||||||
|
|
||||||
Codex CLI is installed lazily when `ddev codex` runs and the persisted binary is missing or broken.
|
Codex CLI is installed lazily when `ddev codex` runs and the persisted binary is missing or broken.
|
||||||
@@ -151,34 +155,127 @@ 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.
|
||||||
|
|
||||||
|
## Sibling repo mirrors
|
||||||
|
|
||||||
|
If a project works alongside sibling repos on the same self-hosted Git host, Claude can be given
|
||||||
|
read-only, auto-refreshing local clones of them to grep for prior art — plus, optionally, the
|
||||||
|
`gitea` MCP server for their issues and PRs. This is **opt-in per project** and driven entirely by
|
||||||
|
one manifest, `.ddev/claude-refs.txt`; projects without that file see no change at all.
|
||||||
|
|
||||||
|
`.ddev/claude-refs.txt` is **local by default**: the add-on's [managed git-exclude
|
||||||
|
block](#keeping-add-on-files-out-of-git) ignores it, since it typically names a LAN-only Git host,
|
||||||
|
a bot token, and mirrors specific to one person's sandbox. A project that wants the manifest shared
|
||||||
|
with the rest of the team can still track it with `git add -f .ddev/claude-refs.txt`.
|
||||||
|
|
||||||
|
### Manifest: `.ddev/claude-refs.txt`
|
||||||
|
|
||||||
|
```
|
||||||
|
# Settings: one per line, starting with @
|
||||||
|
@host git.example.com # required
|
||||||
|
@user claude-ro # bot account; with @token, drives git credentials
|
||||||
|
@token ~/.gitea-ro-token # default; 0600 file in the sandbox home, never committed
|
||||||
|
@depth 50 # default
|
||||||
|
@mcp gitea https://mcp.example.com/mcp # optional: name + URL, registered with the token as Bearer
|
||||||
|
|
||||||
|
# Mirrors: <dir-name> <owner/repo> <description, rest of line, becomes the README row>
|
||||||
|
sibling-api org/sibling-api Go service this project talks to over HTTP.
|
||||||
|
shared-lib org/shared-lib Shared component library, imported as a dependency.
|
||||||
|
```
|
||||||
|
|
||||||
|
Inline `#` comments are only allowed on setting lines, after all the values the setting reads.
|
||||||
|
Keep them off mirror lines, or they end up in the description.
|
||||||
|
|
||||||
|
Environment overrides, mainly for testing: `CLAUDE_REFS_MANIFEST`, `CLAUDE_REFS_HOME`,
|
||||||
|
`CLAUDE_REFS_MAX_AGE` (seconds), `CLAUDE_REFS_TIMEOUT` (seconds per clone or fetch), and
|
||||||
|
`CLAUDE_REFS_DETACHED` (set by `install.sh`; sends output to `refs/.sync.log`).
|
||||||
|
|
||||||
|
### What happens once a project has a manifest
|
||||||
|
|
||||||
|
- On `ddev start` and on every `ddev claude`, `install.sh` runs the sync in the background
|
||||||
|
(`setsid nohup`) so neither command waits on the network. A stamp file throttles real syncs to
|
||||||
|
at most one every 6h (`CLAUDE_REFS_MAX_AGE`), written only after a fully successful run, so a
|
||||||
|
failure retries next launch. If the Git host is unreachable, the sync logs a line and exits
|
||||||
|
cleanly rather than failing `ddev start`.
|
||||||
|
- Mirrors are shallow (`--depth 50 --single-branch` by default), live under
|
||||||
|
`.ddev/claude-code/home/refs/` (gitignored, survives `ddev restart`), and are **throwaway**:
|
||||||
|
every sync hard-resets them to the remote's current state, so local changes are lost and nothing
|
||||||
|
is ever merged. A directory that drops out of the manifest is left in place with a warning
|
||||||
|
rather than deleted.
|
||||||
|
- An entry whose repo resolves to the project's own `origin` is skipped, so the project never ends
|
||||||
|
up with a second, stale copy of itself.
|
||||||
|
- `refs/README.md` is regenerated from the manifest on every sync — descriptions come from the
|
||||||
|
manifest, so the table can't drift from what's on disk.
|
||||||
|
- The bot account's credential file and git config are rewritten from the manifest on every run,
|
||||||
|
so a renamed bot account heals itself on the next `ddev claude` instead of failing silently.
|
||||||
|
- Run `ddev claude-refs` to sync immediately instead of waiting for the throttle; inside a Claude
|
||||||
|
session, running `.ddev/claude-code/refs-sync.sh --force` does the same and is already on the
|
||||||
|
permission allowlist.
|
||||||
|
- If `@mcp` is set, the MCP server is registered once at user scope (`claude mcp add --scope
|
||||||
|
user`), and required permission-allowlist entries are added once — both idempotently, so re-runs
|
||||||
|
never duplicate them.
|
||||||
|
|
||||||
|
Access control (which repos the mirrors can see) stays entirely in the Git host: the bot account's
|
||||||
|
collaborator access decides what's visible. The manifest only names repos; it grants nothing by
|
||||||
|
itself.
|
||||||
|
|
||||||
|
### Adopting in a project
|
||||||
|
|
||||||
|
1. On the Git host, add the bot account as a **read-only** collaborator to each sibling repo.
|
||||||
|
2. Create `.ddev/claude-refs.txt` with `@host`, `@user`, and the mirrors you want. It stays local
|
||||||
|
unless you `git add -f` it (see above).
|
||||||
|
3. Put the bot's token at `.ddev/claude-code/home/.gitea-ro-token`, mode `0600` (matches the
|
||||||
|
manifest's default `@token ~/.gitea-ro-token`; never commit it).
|
||||||
|
4. `ddev restart`, then check `.ddev/claude-code/home/refs/.sync.log` after a minute or so.
|
||||||
|
5. Paste this into the project's `CLAUDE.md`, adding a sentence about the most relevant sibling:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Sibling repos
|
||||||
|
|
||||||
|
Read-only shallow clones of sibling projects live in `.ddev/claude-code/home/refs/`
|
||||||
|
(gitignored, declared in `.ddev/claude-refs.txt`). Grep them for prior art before
|
||||||
|
re-deriving a solution; `refs/README.md` says what each one is. They are reference
|
||||||
|
material: never commit or push from them. They sync in the background at most every
|
||||||
|
6h; run `ddev claude-refs` before trusting anything time-sensitive.
|
||||||
|
```
|
||||||
|
|
||||||
|
The container still needs to resolve and reach the Git host (and the MCP host, if used).
|
||||||
|
|
||||||
## Installed managed files
|
## Installed managed files
|
||||||
|
|
||||||
.ddev/config.codex-cli.yaml
|
.ddev/config.codex-claude.yaml
|
||||||
.ddev/config.claude-code.yaml
|
|
||||||
.ddev/commands/web/codex
|
.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/codex-cli/install.sh
|
.ddev/codex-cli/install.sh
|
||||||
.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/git-exclude.sh
|
||||||
|
|
||||||
## Notes
|
## 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.
|
||||||
|
|
||||||
|
`.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
|
||||||
|
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`,
|
||||||
|
`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`
|
- 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`
|
||||||
- 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
|
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.
|
||||||
/.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
|
## macOS / Linux / WSL2
|
||||||
|
|
||||||
|
|||||||
Executable
+68
@@ -0,0 +1,68 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#ddev-generated
|
||||||
|
# Keep the add-on's own artifacts out of git, in every project it is installed in.
|
||||||
|
#
|
||||||
|
# Rewrites a marker-delimited block in the project's .git/info/exclude. That file is
|
||||||
|
# per-clone and never committed, and every git process honours it -- unlike
|
||||||
|
# core.excludesfile, which is set on the container's git config only and so does
|
||||||
|
# nothing for git run from the host. Called by install.sh, which runs on "ddev start"
|
||||||
|
# and on every "ddev claude", so the list refreshes itself as the add-on changes.
|
||||||
|
#
|
||||||
|
# Personal patterns (.DS_Store, .idea, CLAUDE.md, ...) are deliberately NOT here:
|
||||||
|
# they belong in ~/.gitignore_global, not to this add-on.
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
BEGIN='# >>> ddev-codex-claude-cli >>>'
|
||||||
|
END='# <<< ddev-codex-claude-cli <<<'
|
||||||
|
|
||||||
|
PATTERNS=(
|
||||||
|
'.ddev/addon-metadata/ddev-codex-claude-cli/'
|
||||||
|
'.ddev/config.codex-claude.yaml'
|
||||||
|
'.ddev/commands/web/claude'
|
||||||
|
'.ddev/commands/web/claude-swap'
|
||||||
|
'.ddev/commands/web/claude-refs'
|
||||||
|
'.ddev/commands/web/codex'
|
||||||
|
'.ddev/claude-code/'
|
||||||
|
'.ddev/codex-cli/'
|
||||||
|
'.ddev/claude-refs.txt'
|
||||||
|
'.ddev/plans/'
|
||||||
|
)
|
||||||
|
|
||||||
|
approot="${DDEV_APPROOT:-$(pwd)}"
|
||||||
|
|
||||||
|
# --git-common-dir resolves a worktree's .git file to the real directory. Anything
|
||||||
|
# that is not a git checkout is not an error: ddev start must never fail over this.
|
||||||
|
gitdir="$(git -C "${approot}" rev-parse --git-common-dir 2>/dev/null)" || exit 0
|
||||||
|
[ -n "${gitdir}" ] || exit 0
|
||||||
|
case "${gitdir}" in /*) ;; *) gitdir="${approot}/${gitdir}" ;; esac
|
||||||
|
[ -d "${gitdir}" ] || exit 0
|
||||||
|
|
||||||
|
info="${gitdir}/info"
|
||||||
|
exclude="${info}/exclude"
|
||||||
|
mkdir -p "${info}" 2>/dev/null || exit 0
|
||||||
|
[ -e "${exclude}" ] || : >"${exclude}" 2>/dev/null || exit 0
|
||||||
|
[ -w "${exclude}" ] || exit 0
|
||||||
|
|
||||||
|
block="$(
|
||||||
|
printf '%s\n' "${BEGIN}"
|
||||||
|
printf '%s\n' "# Managed by .ddev/claude-code/git-exclude.sh. Edits inside this block are lost."
|
||||||
|
printf '%s\n' "${PATTERNS[@]}"
|
||||||
|
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
|
||||||
|
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}")"
|
||||||
|
else
|
||||||
|
updated="$(cat "${exclude}"; [ -s "${exclude}" ] && printf '\n'; printf '%s\n' "${block}")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Only write on a real change, so "ddev start" does not touch the mtime every time.
|
||||||
|
# cat > rather than mv: the file may be bind-mounted.
|
||||||
|
if [ "${updated}" != "$(cat "${exclude}")" ]; then
|
||||||
|
printf '%s\n' "${updated}" >"${exclude}" || exit 0
|
||||||
|
fi
|
||||||
@@ -73,4 +73,20 @@ if command -v git >/dev/null 2>&1 && [ -f "${PERSIST_GITIGNORE_GLOBAL}" ]; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Keep the add-on's own artifacts out of this project's git, via a managed block in
|
||||||
|
# .git/info/exclude. Runs every time so the list follows the add-on, not a file that
|
||||||
|
# was copied once. Never fatal: a project need not be a git checkout.
|
||||||
|
if [ -x "${PERSIST_BASE}/git-exclude.sh" ]; then
|
||||||
|
"${PERSIST_BASE}/git-exclude.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.
|
||||||
|
REFS_SYNC="${PERSIST_BASE}/refs-sync.sh"
|
||||||
|
if [ -f "${DDEV_APPROOT}/.ddev/claude-refs.txt" ] && [ -x "${REFS_SYNC}" ]; then
|
||||||
|
"${REFS_SYNC}" --setup-only || echo "claude-refs: setup failed, continuing" >&2
|
||||||
|
CLAUDE_REFS_DETACHED=1 setsid nohup "${REFS_SYNC}" >/dev/null 2>&1 </dev/null &
|
||||||
|
fi
|
||||||
|
|
||||||
echo "Claude Code version: $("${PERSIST_BIN}" --version)"
|
echo "Claude Code version: $("${PERSIST_BIN}" --version)"
|
||||||
Executable
+195
@@ -0,0 +1,195 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#ddev-generated
|
||||||
|
# Read-only sibling-repo mirrors, declared in .ddev/claude-refs.txt.
|
||||||
|
#
|
||||||
|
# refs-sync.sh throttled sync (skips if the last good sync is recent)
|
||||||
|
# refs-sync.sh --force sync now
|
||||||
|
# refs-sync.sh --setup-only credentials, permissions and MCP registration; no network
|
||||||
|
#
|
||||||
|
# Mirrors are throwaway: existing ones are hard-reset, never merged. Does nothing
|
||||||
|
# at all when the project has no manifest.
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
MANIFEST="${CLAUDE_REFS_MANIFEST:-${DDEV_APPROOT}/.ddev/claude-refs.txt}"
|
||||||
|
PERSIST_HOME="${CLAUDE_REFS_HOME:-${DDEV_APPROOT}/.ddev/claude-code/home}"
|
||||||
|
REFS="${PERSIST_HOME}/refs"
|
||||||
|
STAMP="${REFS}/.last-sync"
|
||||||
|
MAX_AGE="${CLAUDE_REFS_MAX_AGE:-21600}" # seconds; 6h
|
||||||
|
NET_TIMEOUT="${CLAUDE_REFS_TIMEOUT:-120}" # seconds per clone/fetch
|
||||||
|
SELF="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/$(basename "${BASH_SOURCE[0]}")"
|
||||||
|
|
||||||
|
[ -f "${MANIFEST}" ] || exit 0
|
||||||
|
|
||||||
|
MODE=sync
|
||||||
|
case "${1:-}" in
|
||||||
|
'') ;;
|
||||||
|
--force) MODE=force ;;
|
||||||
|
--setup-only) MODE=setup ;;
|
||||||
|
*) echo "usage: $(basename "$0") [--force|--setup-only]" >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
export HOME="${PERSIST_HOME}"
|
||||||
|
export GIT_TERMINAL_PROMPT=0
|
||||||
|
|
||||||
|
# --- manifest ---------------------------------------------------------------
|
||||||
|
host=""; user=""; token="~/.gitea-ro-token"; depth=50; mcp_name=""; mcp_url=""
|
||||||
|
names=(); repos=(); descs=()
|
||||||
|
while IFS= read -r line || [ -n "${line}" ]; do
|
||||||
|
line="${line%$'\r'}"
|
||||||
|
case "${line}" in ''|'#'*) continue ;; esac
|
||||||
|
read -r key val rest <<<"${line}"
|
||||||
|
case "${key}" in
|
||||||
|
@host) host="${val}" ;;
|
||||||
|
@user) user="${val}" ;;
|
||||||
|
@token) token="${val}" ;;
|
||||||
|
@depth) depth="${val}" ;;
|
||||||
|
@mcp) mcp_name="${val}"; mcp_url="${rest}" ;;
|
||||||
|
@*) echo "claude-refs: unknown setting ${key}, ignored" >&2 ;;
|
||||||
|
*) names+=("${key}"); repos+=("${val}"); descs+=("${rest}") ;;
|
||||||
|
esac
|
||||||
|
done <"${MANIFEST}"
|
||||||
|
token="${token/#\~/${HOME}}"
|
||||||
|
|
||||||
|
if [ -z "${host}" ]; then
|
||||||
|
echo "claude-refs: no @host in ${MANIFEST}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- credentials (every run, so a renamed bot account heals itself) --------
|
||||||
|
# Host-scoped on purpose: a global helper would hand the project's own HTTPS
|
||||||
|
# pushes the bot's pull-only credentials.
|
||||||
|
creds="${HOME}/.git-credentials-${host}"
|
||||||
|
if [ -n "${user}" ] && [ -r "${token}" ]; then
|
||||||
|
(umask 077; printf 'https://%s:%s@%s\n' "${user}" "$(tr -d '[:space:]' <"${token}")" "${host}" >"${creds}")
|
||||||
|
git config --global "credential.https://${host}.helper" "store --file=${creds}"
|
||||||
|
git config --global "credential.https://${host}.username" "${user}"
|
||||||
|
elif [ -n "${user}" ]; then
|
||||||
|
echo "claude-refs: token ${token} not readable; relying on existing git credentials" >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- setup: permissions and MCP (no network) --------------------------------
|
||||||
|
if [ "${MODE}" = setup ]; then
|
||||||
|
mkdir -p "${REFS}" "${HOME}/.claude"
|
||||||
|
if ! command -v jq >/dev/null 2>&1; then
|
||||||
|
echo "claude-refs: jq missing, skipping permission and MCP seeding" >&2
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
settings="${HOME}/.claude/settings.json"
|
||||||
|
[ -s "${settings}" ] || echo '{}' >"${settings}"
|
||||||
|
want=("Bash(git -C ${REFS}/*)" "Bash(${SELF})" "Bash(${SELF} --force)")
|
||||||
|
[ -n "${mcp_name}" ] && want+=("mcp__${mcp_name}")
|
||||||
|
tmp="$(mktemp)"
|
||||||
|
# With --args every trailing word is an argument, so the file comes in on stdin.
|
||||||
|
if jq '
|
||||||
|
.permissions.allow = reduce $ARGS.positional[] as $e
|
||||||
|
(.permissions.allow // []; if any(.[]; . == $e) then . else . + [$e] end)
|
||||||
|
' --args "${want[@]}" <"${settings}" >"${tmp}"; then
|
||||||
|
cat "${tmp}" >"${settings}"
|
||||||
|
fi
|
||||||
|
rm -f "${tmp}"
|
||||||
|
|
||||||
|
# Checked against ~/.claude.json rather than "claude mcp get", which connects.
|
||||||
|
if [ -n "${mcp_name}" ] && [ -n "${mcp_url}" ] && [ -r "${token}" ] \
|
||||||
|
&& ! jq -e --arg n "${mcp_name}" '.mcpServers[$n]' "${HOME}/.claude.json" >/dev/null 2>&1; then
|
||||||
|
claude mcp add --transport http --scope user "${mcp_name}" "${mcp_url}" \
|
||||||
|
--header "Authorization: Bearer $(tr -d '[:space:]' <"${token}")"
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- sync -------------------------------------------------------------------
|
||||||
|
mkdir -p "${REFS}"
|
||||||
|
exec 9>"${REFS}/.sync.lock"
|
||||||
|
if ! flock -n 9; then
|
||||||
|
echo "claude-refs: another sync is running"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${MODE}" = sync ] && [ -f "${STAMP}" ] \
|
||||||
|
&& [ $(( $(date +%s) - $(stat -c %Y "${STAMP}") )) -lt "${MAX_AGE}" ]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Detached runs (ddev start, ddev claude) keep only the latest real sync's output.
|
||||||
|
[ -n "${CLAUDE_REFS_DETACHED:-}" ] && exec >"${REFS}/.sync.log" 2>&1
|
||||||
|
date '+claude-refs sync %F %T'
|
||||||
|
|
||||||
|
if ! curl -sS -o /dev/null --max-time 10 "https://${host}/" 2>/dev/null; then
|
||||||
|
echo "claude-refs: ${host} unreachable (off-LAN?), mirrors left as they are"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# git@h:o/r.git, https://u@h/o/r.git -> h/o/r (lowercase), to spot the project itself
|
||||||
|
normalize() {
|
||||||
|
local u="$1"
|
||||||
|
u="${u#*://}"; u="${u#*@}"; u="${u/://}"; u="${u%/}"; u="${u%.git}"
|
||||||
|
printf '%s' "${u,,}"
|
||||||
|
}
|
||||||
|
self="$(normalize "$(git -C "${DDEV_APPROOT}" remote get-url origin 2>/dev/null)")"
|
||||||
|
|
||||||
|
declare -A listed=()
|
||||||
|
rows=()
|
||||||
|
failed=0
|
||||||
|
for i in "${!names[@]}"; do
|
||||||
|
name="${names[$i]}"; repo="${repos[$i]}"; dir="${REFS}/${name}"
|
||||||
|
url="https://${host}/${repo}.git"
|
||||||
|
|
||||||
|
if [ -n "${self}" ] && [ "$(normalize "${url}")" = "${self}" ]; then
|
||||||
|
printf '== %-16s skipped: this is the project itself, read it live\n' "${name}"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
listed[${name}]=1
|
||||||
|
rows+=("| \`${name}/\` | \`${repo}\` | ${descs[$i]//|/\\|} |")
|
||||||
|
|
||||||
|
printf '== %-16s ' "${name}"
|
||||||
|
if [ -d "${dir}/.git" ]; then
|
||||||
|
if ! timeout "${NET_TIMEOUT}" git -C "${dir}" fetch --depth "${depth}" --quiet origin \
|
||||||
|
|| ! git -C "${dir}" reset --hard --quiet '@{u}'; then
|
||||||
|
echo "FAILED (fetch)"; failed=1; continue
|
||||||
|
fi
|
||||||
|
elif [ -e "${dir}" ]; then
|
||||||
|
echo "FAILED (exists but is not a git checkout)"; failed=1; continue
|
||||||
|
elif ! timeout "${NET_TIMEOUT}" git clone --depth "${depth}" --single-branch --quiet "${url}" "${dir}"; then
|
||||||
|
rm -rf "${dir}" # did not exist before this run
|
||||||
|
echo "FAILED (clone)"; failed=1; continue
|
||||||
|
fi
|
||||||
|
git -C "${dir}" log -1 --format='%ad %s' --date=short
|
||||||
|
done
|
||||||
|
|
||||||
|
for dir in "${REFS}"/*/; do
|
||||||
|
[ -d "${dir}" ] || continue
|
||||||
|
name="$(basename "${dir}")"
|
||||||
|
[ -n "${listed[${name}]:-}" ] || echo "!! ${name}/ is not in the manifest; left in place, remove it by hand"
|
||||||
|
done
|
||||||
|
|
||||||
|
{
|
||||||
|
cat <<EOF
|
||||||
|
# Sibling repo mirrors
|
||||||
|
|
||||||
|
<!-- Generated by refs-sync.sh from .ddev/claude-refs.txt. Edit the manifest, not this file. -->
|
||||||
|
|
||||||
|
Read-only shallow clones of sibling projects on \`${host}\`, kept here so
|
||||||
|
cross-project questions ("how did we solve X over there?") can be answered by
|
||||||
|
grepping real code instead of guessing or re-deriving it.
|
||||||
|
|
||||||
|
**These are reference material, not working copies.** Never commit, never push,
|
||||||
|
never edit. The account behind them (\`${user:-unknown}\`) should have pull access only,
|
||||||
|
but treat them as read-only regardless. To change one of these projects, open
|
||||||
|
its real checkout.
|
||||||
|
|
||||||
|
Clones are \`--depth ${depth} --single-branch\` off the default branch. They sync
|
||||||
|
themselves in the background on \`ddev start\` and \`ddev claude\`, at most every
|
||||||
|
$(( MAX_AGE / 3600 ))h. Run \`${SELF} --force\` before trusting anything time-sensitive.
|
||||||
|
Syncing hard-resets, so local changes are lost. The last run's output is in \`.sync.log\`.
|
||||||
|
|
||||||
|
| Directory | Repo | What it is |
|
||||||
|
|---|---|---|
|
||||||
|
EOF
|
||||||
|
printf '%s\n' "${rows[@]}"
|
||||||
|
} >"${REFS}/README.md"
|
||||||
|
|
||||||
|
if [ "${failed}" -eq 0 ]; then
|
||||||
|
touch "${STAMP}"
|
||||||
|
fi
|
||||||
|
exit "${failed}"
|
||||||
@@ -71,4 +71,11 @@ if command -v git >/dev/null 2>&1 && [ -f "${PERSIST_GITIGNORE_GLOBAL}" ]; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Shared with the Claude side: keeps the add-on's artifacts out of the project's git
|
||||||
|
# even when only the codex installer runs.
|
||||||
|
GIT_EXCLUDE="${DDEV_APPROOT}/.ddev/claude-code/git-exclude.sh"
|
||||||
|
if [ -x "${GIT_EXCLUDE}" ]; then
|
||||||
|
"${GIT_EXCLUDE}" || true
|
||||||
|
fi
|
||||||
|
|
||||||
echo "Codex CLI version: $("${PERSIST_BIN}" --version)"
|
echo "Codex CLI version: $("${PERSIST_BIN}" --version)"
|
||||||
Executable
+7
@@ -0,0 +1,7 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#ddev-generated
|
||||||
|
## Description: Sync the read-only sibling-repo mirrors declared in .ddev/claude-refs.txt now
|
||||||
|
## Usage: claude-refs
|
||||||
|
## Example: "ddev claude-refs"
|
||||||
|
set -euo pipefail
|
||||||
|
exec "${DDEV_APPROOT}/.ddev/claude-code/refs-sync.sh" --force
|
||||||
File renamed without changes.
+6
-3
@@ -1,24 +1,27 @@
|
|||||||
name: ddev-codex-claude-cli
|
name: ddev-codex-claude-cli
|
||||||
|
|
||||||
project_files:
|
project_files:
|
||||||
- config.codex-claude-cli.yaml
|
- config.codex-claude.yaml
|
||||||
- commands/web/codex
|
- commands/web/codex
|
||||||
- commands/web/claude
|
- commands/web/claude
|
||||||
- commands/web/claude-swap
|
- commands/web/claude-swap
|
||||||
|
- commands/web/claude-refs
|
||||||
- codex-cli/install.sh
|
- codex-cli/install.sh
|
||||||
- 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/git-exclude.sh
|
||||||
|
|
||||||
post_install_actions:
|
post_install_actions:
|
||||||
- |
|
- |
|
||||||
#ddev-description: Make AI CLI scripts executable
|
#ddev-description: Make AI CLI scripts executable
|
||||||
set -eu
|
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
|
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
|
||||||
|
|
||||||
removal_actions:
|
removal_actions:
|
||||||
- |
|
- |
|
||||||
#ddev-description: Remove AI CLI add-on artifacts
|
#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 -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
|
rm -rf codex-cli 2>/dev/null || true
|
||||||
rm -rf claude-code 2>/dev/null || true
|
rm -rf claude-code 2>/dev/null || true
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user