bootstrap
Adom Hydrogen (platform base)
Public Made by Adomby adom
Layer Hydrogen's workspace onto a standard Adom workstation.
master
John Lauer
Publish 0.3.18
e3ce262
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
#!/usr/bin/env bash
# migrate-retired-slugs.sh — adom/hydrogen-bootstrap
#
# Heal lockfile roots left behind by slug renames (John 2026-08-13). This is a
# PREREQUISITE for deleting the old wiki pages: the rename redirects are going
# away entirely, so every container must be moved off retired slugs while they
# still resolve. Containers provisioned before the consolidations carry the OLD
# slug as a separate pinned root in adom_modules/.lock.json (live example:
# adom/adom-desktop pinned 1.9.211 next to adom/adom-bridge 2.0.30).
#
# WHY THE PURGE PHASE EXISTS (verified live 2026-08-13): `adom-wiki pkg
# uninstall` removes the entry from `packages` but leaves it in the lockfile's
# `resolved` + `order` arrays; the NEXT `pkg install` re-syncs `packages` from
# that closure and RESURRECTS the removed entry as a ghost that uninstall then
# refuses ("not installed") while `packages` still lists it. Until the CLI bug
# is fixed, this script phases all installs BEFORE all uninstalls, then purges
# retired refs from packages/resolved/order directly (lockfile-only removal:
# uninstall itself is registry-independent, so this is safe offline too).
#
# Idempotent: a clean container matches nothing and exits in <1s.
# Best-effort: NEVER fails the bootstrap install (no set -e).
set -u
mlog() { echo "[slug-migration] $*"; }
WIKI="${ADOM_WIKI_BIN:-$HOME/.local/bin/adom-wiki}"
command -v "$WIKI" >/dev/null 2>&1 || WIKI="adom-wiki"
command -v "$WIKI" >/dev/null 2>&1 || { mlog "adom-wiki not found; skipping"; exit 0; }
MOD=""
for c in "$HOME/project/adom_modules" "$HOME/adom_modules"; do
[ -f "$c/.lock.json" ] && MOD="$c" && break
done
[ -n "$MOD" ] || { mlog "no adom_modules lockfile found; skipping"; exit 0; }
LOCK="$MOD/.lock.json"
# ── The migration table (bootstraps first: [email protected] carries the dep
# edge on adom/adom-desktop; migrating it drops that edge before we prune).
# retired-ref:successor-ref (empty successor = absorbed, hub re-delivers).
TABLE="adom/hd-bootstrap:adom/hydrogen-bootstrap
adom/hd-windows-bootstrap:adom/hydrogen-windows-bootstrap
adom/hd-mac-bootstrap:adom/hydrogen-mac-bootstrap
adom/adom-desktop:adom/adom-bridge
adom/step2glb:adom/adom-step2glb
adom/adom-theme-system:adom/adom-theme
adom/adom-workspace-control:"
has_pkg() {
python3 - "$LOCK" "$1" <<'PY'
import json, sys
d = json.load(open(sys.argv[1]))
sys.exit(0 if sys.argv[2] in (d.get("packages") or {}) else 1)
PY
}
PRESENT=""
while IFS=: read -r old new; do
[ -n "$old" ] && has_pkg "$old" && PRESENT="$PRESENT$old:$new
"
done <<< "$TABLE"
# Nested `pkg install` DEADLOCKS when this script runs inside the CLI's own
# install (the CLI holds the install lock while running postinstall — verified
# live 2026-08-13: the first migration attempt hung on it). Every nested call
# is timeout-bounded; a timeout on the FIRST one means the lock is held above
# us — defer the whole migration to the next STANDALONE postinstall run (ah's
# update-packages step re-runs postinstall explicitly, lock-free).
LOCK_BUSY=0
inst() { # inst <ref> -> 0 ok, nonzero failed; sets LOCK_BUSY only on a REAL lock wait
# 300s: distro installs are legitimately slow (a windows-bootstrap install
# measured >120s); a timeout alone is NOT proof of a held lock — only the
# CLI's own "waiting for another adom-wiki install" line is.
local out; out=$(mktemp)
timeout 300 "$WIKI" pkg install "$1" >"$out" 2>&1
local rc=$?
if grep -q "waiting for another adom-wiki install" "$out" 2>/dev/null && [ "$rc" != "0" ]; then
LOCK_BUSY=1
fi
rm -f "$out"
return $rc
}
if [ -z "$PRESENT" ]; then
mlog "clean: no retired slugs present"
exit 0
fi
# ── Phase 1: install every successor (ALL installs happen before ANY removal,
# so the resurrect-on-install bug can't undo phase 2/3).
SECURED=""
while IFS=: read -r old new; do
[ -n "$old" ] || continue
mlog "MIGRATING $old -> ${new:-'(absorbed into adom/adom)'}"
if [ -z "$new" ]; then
SECURED="$SECURED$old
"
continue
fi
if has_pkg "$new"; then
mlog " successor $new already present"
SECURED="$SECURED$old
"
elif inst "$new"; then
mlog " installed successor $new"
SECURED="$SECURED$old
"
else
mlog " ⚠ could not install $new — KEEPING $old (re-run heals)"
if [ "$LOCK_BUSY" = "1" ]; then
mlog "DEFERRED: install lock held above us (running inside pkg install?); next standalone postinstall completes this"
exit 0
fi
fi
done <<< "$PRESENT"
# The absorbed-skill case: adom/adom (the hub) owns adom-workspace-control's
# skill now — re-install it so the shared skill file survives phase 2's prune.
if echo "$PRESENT" | grep -q "adom/adom-workspace-control" && has_pkg "adom/adom"; then
inst "adom/adom" \
&& mlog " re-installed adom/adom (owns the absorbed workspace-control skill)"
fi
# ── Phase 2: uninstall the retired refs (registry-independent; may fail on
# ghosts — phase 3 purges whatever this leaves).
while IFS= read -r old; do
[ -n "$old" ] || continue
timeout 60 "$WIKI" pkg uninstall "$old" >/dev/null 2>&1 \
&& mlog " removed $old" \
|| mlog " (uninstall of $old refused; purging from lockfile directly)"
done <<< "$SECURED"
# ── Phase 3: purge residue from packages/resolved/order so no later install
# can resurrect the retired refs.
SECURED_CSV=$(echo "$SECURED" | grep -v '^$' | paste -sd, -)
SECURED_REFS="$SECURED_CSV" python3 - "$LOCK" <<'PY'
import json, os, sys
RETIRED = set((os.environ.get("SECURED_REFS") or "").split(",")) - {""}
path = sys.argv[1]
d = json.load(open(path))
removed = []
pk = d.get("packages") or {}
for r in list(pk):
if r in RETIRED:
del pk[r]; removed.append(r)
def ref_of(entry):
if not isinstance(entry, dict): return ""
o, s = entry.get("owner"), entry.get("slug")
return f"{o}/{s}" if o else (s or "")
d["resolved"] = [e for e in (d.get("resolved") or []) if ref_of(e) not in RETIRED]
d["order"] = [o for o in (d.get("order") or []) if o not in RETIRED]
if removed:
json.dump(d, open(path, "w"), indent=2)
print("[slug-migration] purged from lockfile:", ", ".join(sorted(removed)))
else:
print("[slug-migration] no lockfile residue to purge")
PY
# Module-dir leftovers of purged refs (uninstall removes these when it works;
# the ghost path doesn't). Delivered ~/.claude/skills files are deliberately
# LEFT alone: deleting by name could hit a file the successor now owns — the
# successor installs above already overwrote every shared name.
while IFS= read -r old; do
[ -n "$old" ] || continue
[ -d "$MOD/$old" ] && rm -rf "$MOD/$old" 2>/dev/null && mlog " removed module dir $old"
done <<< "$SECURED"
# ── Phase 4: verify every successor's delivered skills exist on disk; one
# re-install heals a collateral prune (safe now: retired refs are purged, so
# installs can no longer resurrect them).
for ref in adom/hydrogen-bootstrap adom/hydrogen-windows-bootstrap adom/hydrogen-mac-bootstrap \
adom/adom-bridge adom/adom-step2glb adom/adom-theme adom/adom; do
pkgdir="$MOD/$ref"
[ -d "$pkgdir/skills" ] || continue
missing=0
for dsk in "$pkgdir"/skills/*/; do
[ -d "$dsk" ] || continue
n=$(basename "$dsk")
[ -f "$HOME/.claude/skills/$n/SKILL.md" ] || { mlog " ⚠ $ref skill '$n' missing on disk"; missing=1; }
done
if [ "$missing" = "1" ]; then
mlog " re-installing $ref to re-deliver missing skills"
inst "$ref" || mlog " ⚠ re-install of $ref failed (heals on next standalone run)"
fi
done
N=$(echo "$SECURED" | grep -c . || true)
mkdir -p "$HOME/.adom" 2>/dev/null
echo "{\"version\":1,\"migrated\":$N,\"at\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}" > "$HOME/.adom/slug-migration.json" 2>/dev/null
mlog "DONE: $N retired slug(s) migrated"
exit 0