chore: no idea?

This commit is contained in:
Björn Fromme committed 2026-10-16 15:10:26 +02:00
1 parent 63f3199076
commit afc232ac27
8 files changed
+411 -18

No files matched your search

+68
View File
@@ -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
+16
View File
@@ -73,4 +73,20 @@ if command -v git >/dev/null 2>&1 && [ -f "${PERSIST_GITIGNORE_GLOBAL}" ]; then
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)"
+195
View File
@@ -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}"