feat: full pi config in containers — git: packages vendored at bake (ignore-scripts), npm shim for runtime installs, dotfiles mount + POST /api/pi-config/resync rebuilds worker+ops images (95% cover, race clean)

This commit is contained in:
Raphael Westphal
2026-08-19 11:00:39 +02:00
parent 910a244376
commit cbf52d2b76
8 changed files with 505 additions and 17 deletions
+38 -17
View File
@@ -4,7 +4,7 @@
set -euo pipefail
SRC="${LVMH_PI_AGENT_DIR:-$HOME/.dotfiles/pi/agent}"
DEST="$(dirname "$0")/../docker/pi-agent"
DEST="${LVMH_PI_BAKE_DIR:-$(dirname "$0")/../docker/pi-agent}"
# DEST must exist even without dotfiles: the worker Dockerfile COPYs it
# unconditionally (a missing dir would fail the build).
@@ -31,20 +31,41 @@ rsync -a --delete \
--exclude '.pi/' \
"$SRC/" "$DEST/"
# Container sessions cannot run git: package postinstalls (e.g. husky) — keep
# registry (npm:) packages only in the baked settings.json.
python3 - "$DEST/settings.json" <<'PY'
import json, sys
p = sys.argv[1]
with open(p) as f:
s = json.load(f)
pkgs = s.get("packages") or []
kept = [x for x in pkgs if x.startswith("npm:")]
if pkgs != kept:
s["packages"] = kept
with open(p, "w") as f:
json.dump(s, f, indent=2)
f.write("\n")
print(f"rsync-pi-agent: dropped {len(pkgs) - len(kept)} git: packages from baked settings")
# Pre-seed the git: package cache: clone each pinned git: package into
# docker/pi-agent/git/... where pi's package manager installs them, with
# lifecycle scripts ignored (postinstalls like husky crash headless).
# Keeps subagents/todo/async tooling present with no runtime network need.
# Requires network access to github.com at bake time.
python3 - "$DEST" <<'PY'
import json, re, shutil, subprocess, sys
from pathlib import Path
dest = Path(sys.argv[1])
settings = json.loads((dest / "settings.json").read_text())
git_root = dest / "git"
if git_root.exists():
shutil.rmtree(git_root)
for pkg in settings.get("packages") or []:
if not pkg.startswith("git:"):
continue
rest = pkg[4:]
m = re.match(r"^(.+)@([0-9a-f]{7,40})$", rest)
repo, ref = (m.group(1), m.group(2)) if m else (rest, None)
target = git_root / repo
target.parent.mkdir(parents=True, exist_ok=True)
url = f"https://{repo}" if not repo.startswith("https://") else repo
print(f"rsync-pi-agent: cloning {repo}@{ref or 'HEAD'}")
subprocess.run(["git", "clone", "--quiet", url, str(target)], check=True)
if ref:
subprocess.run(["git", "checkout", "--quiet", ref], cwd=target, check=True)
shutil.rmtree(target / ".git", ignore_errors=True)
if (target / "package.json").exists():
npm = "/usr/bin/npm" if Path("/usr/bin/npm").exists() else "npm"
subprocess.run(
[npm, "install", "--omit=dev", "--ignore-scripts", "--no-audit", "--no-fund"],
cwd=target, check=True, capture_output=True,
)
PY
echo "rsync-pi-agent: synced $(find "$DEST" -type f | wc -l) files from $SRC"
echo "rsync-pi-agent: synced $(find "$DEST" -type f | wc -l) files from $SRC (git: packages preserved)"