Mos amended ruling (2): with no environment location for
MOSAIC_LEASE_VERSION_PROBE_COMMAND, the wrapper sends only
`__lease-capability` to ~/.npm-global/bin/mosaic-old, so the old CLI's
lease gate still gets its answer. Every other subcommand reaches the
pinned worktree; undo removes the forward with the wrapper. update now
rewrites the wrapper, and status flags a wrapper that differs from what
the script writes. Checked live at 21:17:58Z: the probe answered
{"name":"lease-runtime-activation","version":1}, exit 0.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
152 lines
6.5 KiB
Bash
Executable File
152 lines
6.5 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Q81 (sage-0042): put the mosaic-stack CLI on PATH as `mosaic`.
|
|
#
|
|
# mosaic-cli.sh install [REF] rename the npm-global `mosaic` to `mosaic-old`,
|
|
# add a pinned worktree and the wrapper
|
|
# mosaic-cli.sh update [REF] move the pinned worktree to REF and
|
|
# rewrite the wrapper
|
|
# mosaic-cli.sh undo remove the wrapper and the worktree, rename
|
|
# `mosaic-old` back to `mosaic`
|
|
# mosaic-cli.sh status what is installed, and whether it drifted
|
|
#
|
|
# REF defaults to origin/refactor after a fetch, and must already be on
|
|
# origin/refactor: the wrapper runs pushed code only, never the working
|
|
# checkout. The worktree is a detached linked worktree of the canonical
|
|
# checkout, so `mosaic queue` refuses there (the queue runs only in the
|
|
# canonical checkout); queue work stays on scripts/mosaic.
|
|
#
|
|
# The wrapper sends only `__lease-capability` to mosaic-old, so the old
|
|
# CLI's lease gate (activation_version_gate.py) still gets its answer.
|
|
#
|
|
# Writes only: ~/.npm-global/bin/{mosaic,mosaic-old}, the worktree and
|
|
# state directory below, and the canonical checkout's worktree list.
|
|
set -euo pipefail
|
|
|
|
CANON=/mnt/storage/src/mosaic-stack
|
|
BIN="$HOME/.npm-global/bin"
|
|
STATE_DIR="$HOME/.local/share/mosaic-cli"
|
|
WT="$STATE_DIR/stack"
|
|
LOG="$STATE_DIR/install.log"
|
|
OLD_TARGET=../lib/node_modules/@mosaicstack/mosaic/dist/cli.js
|
|
MARKER="# mosaic-cli wrapper (Q81, agents/sage/work/q81/mosaic-cli.sh)"
|
|
|
|
die() { echo "mosaic-cli: $*" >&2; exit 1; }
|
|
now() { date -u +%FT%TZ; }
|
|
log() { mkdir -p "$STATE_DIR"; echo "$(now) $*" >>"$LOG"; }
|
|
|
|
is_wrapper() { [ -f "$BIN/mosaic" ] && [ ! -L "$BIN/mosaic" ] && sed -n 2p "$BIN/mosaic" | grep -qxF "$MARKER"; }
|
|
is_old_link() { [ -L "$1" ] && [ "$(readlink "$1")" = "$OLD_TARGET" ]; }
|
|
|
|
# Fetch, then resolve REF to a full sha that origin/refactor contains.
|
|
pinned_sha() {
|
|
local ref="${1:-origin/refactor}" sha
|
|
git -C "$CANON" fetch -q origin refactor || die "git fetch origin refactor failed"
|
|
sha="$(git -C "$CANON" rev-parse --verify -q "$ref^{commit}")" || die "$ref is not a commit"
|
|
git -C "$CANON" merge-base --is-ancestor "$sha" origin/refactor || die "$ref ($sha) is not on origin/refactor; only pushed code is pinned"
|
|
echo "$sha"
|
|
}
|
|
|
|
# npm ci only when the lock differs from what node_modules was built from.
|
|
deps() {
|
|
if [ -f "$WT/node_modules/.package-lock.json" ] && [ -f "$STATE_DIR/lock.sha256" ] \
|
|
&& (cd "$WT" && sha256sum -c --status "$STATE_DIR/lock.sha256"); then
|
|
echo "node_modules: lock unchanged, kept"
|
|
return
|
|
fi
|
|
[ -f "$WT/package.json" ] || die "$WT/package.json missing; refusing npm ci (it would walk up)"
|
|
(cd "$WT" && npm ci --prefer-offline --no-audit --no-fund >/dev/null) || die "npm ci in $WT failed"
|
|
(cd "$WT" && sha256sum package-lock.json) >"$STATE_DIR/lock.sha256"
|
|
echo "node_modules: npm ci done"
|
|
}
|
|
|
|
render_wrapper() {
|
|
cat <<EOF
|
|
#!/usr/bin/env bash
|
|
$MARKER
|
|
# Runs the mosaic-stack CLI pinned in $WT.
|
|
# The estate CLI it replaced is $BIN/mosaic-old. Its lease probe still
|
|
# answers there, for old-path launches (Mos, Q81, amending ruling (2)).
|
|
[ "\${1:-}" = __lease-capability ] && exec "$BIN/mosaic-old" "\$@"
|
|
exec "$WT/scripts/mosaic" "\$@"
|
|
EOF
|
|
}
|
|
|
|
write_wrapper() {
|
|
local tmp
|
|
tmp="$(mktemp "$BIN/.mosaic.XXXXXX")"
|
|
render_wrapper >"$tmp"
|
|
chmod 0755 "$tmp"
|
|
mv -f "$tmp" "$BIN/mosaic"
|
|
}
|
|
|
|
cmd_install() {
|
|
is_wrapper && die "already installed; use update or undo"
|
|
is_old_link "$BIN/mosaic" || die "$BIN/mosaic is not the expected symlink to $OLD_TARGET; refusing"
|
|
[ -e "$BIN/mosaic-old" ] || [ -L "$BIN/mosaic-old" ] && die "$BIN/mosaic-old already exists; refusing"
|
|
[ -e "$WT" ] && die "$WT already exists; refusing"
|
|
local sha; sha="$(pinned_sha "${1:-}")"
|
|
mkdir -p "$STATE_DIR"
|
|
git -C "$CANON" worktree add -q --detach "$WT" "$sha" || die "worktree add failed"
|
|
deps
|
|
mv -n "$BIN/mosaic" "$BIN/mosaic-old"
|
|
is_old_link "$BIN/mosaic-old" || die "rename to mosaic-old did not happen"
|
|
write_wrapper
|
|
log "install $sha"
|
|
echo "installed: $BIN/mosaic -> $WT/scripts/mosaic at $sha"
|
|
}
|
|
|
|
cmd_update() {
|
|
is_wrapper || die "not installed (no wrapper at $BIN/mosaic)"
|
|
local sha; sha="$(pinned_sha "${1:-}")"
|
|
[ -z "$(git -C "$WT" status --porcelain --untracked-files=no)" ] || die "$WT has tracked changes; refusing"
|
|
git -C "$WT" checkout -q --detach "$sha"
|
|
deps
|
|
write_wrapper
|
|
log "update $sha"
|
|
echo "updated: $WT at $sha"
|
|
}
|
|
|
|
cmd_undo() {
|
|
is_wrapper || die "$BIN/mosaic is not this wrapper; refusing (status says why)"
|
|
is_old_link "$BIN/mosaic-old" || die "$BIN/mosaic-old is not the expected symlink; refusing"
|
|
rm -f "$BIN/mosaic"
|
|
mv -n "$BIN/mosaic-old" "$BIN/mosaic"
|
|
is_old_link "$BIN/mosaic" || die "rename back to mosaic did not happen"
|
|
if [ -e "$WT" ]; then git -C "$CANON" worktree remove --force "$WT"; fi
|
|
rm -f "$STATE_DIR/lock.sha256"
|
|
log "undo"
|
|
echo "undone: $BIN/mosaic -> $OLD_TARGET again; worktree removed"
|
|
}
|
|
|
|
cmd_status() {
|
|
local bad=0 sha origin
|
|
if is_wrapper; then
|
|
if cmp -s "$BIN/mosaic" <(render_wrapper); then echo "wrapper: $BIN/mosaic (marker ok, matches this script)"
|
|
else echo "DRIFT: wrapper differs from what this script writes; run update"; bad=1; fi
|
|
elif is_old_link "$BIN/mosaic"; then echo "wrapper: none; $BIN/mosaic is the estate symlink"
|
|
[ -e "$WT" ] && { echo "DRIFT: worktree exists but the wrapper is gone (an npm i -g --force?)"; bad=1; }
|
|
else echo "DRIFT: $BIN/mosaic is neither the wrapper nor the estate symlink"; bad=1; fi
|
|
if is_old_link "$BIN/mosaic-old"; then echo "mosaic-old: $BIN/mosaic-old -> $OLD_TARGET"
|
|
elif [ -e "$BIN/mosaic-old" ] || [ -L "$BIN/mosaic-old" ]; then echo "DRIFT: $BIN/mosaic-old is not the expected symlink"; bad=1
|
|
else echo "mosaic-old: none"; fi
|
|
if [ -d "$WT" ]; then
|
|
sha="$(git -C "$WT" rev-parse HEAD)"
|
|
origin="$(git -C "$CANON" rev-parse origin/refactor)"
|
|
echo "worktree: $WT at $sha"
|
|
if [ "$sha" = "$origin" ]; then echo "pin: origin/refactor (as last fetched)"
|
|
else echo "pin: behind origin/refactor $origin by $(git -C "$CANON" rev-list --count "$sha..$origin") commits"; fi
|
|
[ -z "$(git -C "$WT" status --porcelain --untracked-files=no)" ] || { echo "DRIFT: worktree has tracked changes"; bad=1; }
|
|
else echo "worktree: none"; fi
|
|
echo "on PATH: $(command -v mosaic || echo none)"
|
|
[ -f "$LOG" ] && echo "last: $(tail -n 1 "$LOG")"
|
|
return "$bad"
|
|
}
|
|
|
|
case "${1:-}" in
|
|
install) shift; cmd_install "$@" ;;
|
|
update) shift; cmd_update "$@" ;;
|
|
undo) cmd_undo ;;
|
|
status) cmd_status ;;
|
|
*) echo "usage: mosaic-cli.sh install [REF] | update [REF] | undo | status" >&2; exit 4 ;;
|
|
esac
|