#!/usr/bin/env bash # One immutable pushed tree for the bus, Console and human CLI (#1579). # copy creates files only; wrapper is a separately Mos-gated live step. set -euo pipefail REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" COPIES="${XDG_DATA_HOME:-$HOME/.local/share}/mosaic-stack/copies" BIN="$HOME/.npm-global/bin" BUSINESS=mosaic-stack ALLOW_WORKTREE=0 MARKER='# mosaic-cli wrapper (Q81, agents/sage/work/q81/mosaic-cli.sh)' usage() { echo 'usage: stack-copy.sh copy COMMIT [--copies DIR] [--allow-worktree] | wrapper ID [--copies DIR] [--bin DIR] | check [--copies DIR] [--bin DIR] [--business ID]' >&2; exit "${1:-4}"; } die() { echo "stack-copy: $*" >&2; exit 1; } plain_path() { case "$1" in /*) ;; *) die "$2 must be an absolute path: $1" ;; esac; case "$1" in *[!A-Za-z0-9/._+-]*) die "$2 contains a character the unit cannot carry: $1" ;; esac; } parse() { while [ $# -gt 0 ]; do case "$1" in --copies) [ $# -ge 2 ] || usage; COPIES="$2"; shift 2;; --bin) [ "$CMD" != copy ] && [ $# -ge 2 ] || usage; BIN="$2";shift 2;; --business) [ "$CMD" = check ] && [ $# -ge 2 ] || usage; BUSINESS="$2";shift 2;; --allow-worktree) [ "$CMD" = copy ] || usage; ALLOW_WORKTREE=1;shift;; *) usage;; esac; done; plain_path "$COPIES" 'copies directory'; } copy_root() { [[ "$ID" =~ ^[0-9a-f]{40}$ ]] || die '--copy must be a full 40-character commit id'; [ -f "$COPIES/$ID/scripts/mosaic" ] || die "no copy $ID in $COPIES"; echo "$COPIES/$ID"; } # Closure of scripts/mosaic, bus process/notifier/launcher, Console serve, # human-cli and both harness adapters, including on-demand children/data. COPY_PATHS=( scripts/mosaic scripts/mosaic-config.mjs adapters roles RELEASE package.json package-lock.json packages/bus/src packages/bus/schema.sql packages/business/src packages/cli/src packages/conversation/src packages/discord/src packages/discord/bin packages/discord/extension packages/harness/src packages/seat/src packages/tasks/src packages/tasks/fields.json packages/tasks/requirements.json packages/webui/src packages/webui/README.md packages/cli/README.md packages/queue/src ) copy_commit() { [ -d "$REPO/.git" ] || [ "$ALLOW_WORKTREE" = 1 ] || die "refusing: $REPO is not the canonical checkout (its .git is not a directory); run copy from the canonical checkout" [[ "$ID" =~ ^[A-Za-z0-9][A-Za-z0-9._/-]*$ ]] || die "commit must be a commit id or branch name: $ID" local full ref target tmp full="$(git -C "$REPO" rev-parse --verify --quiet "$ID^{commit}")" || die "not a commit: $ID" for ref in refs/heads/refactor refs/remotes/origin/refactor; do git -C "$REPO" rev-parse --verify --quiet "$ref^{commit}" >/dev/null || die "no $ref in $REPO" git -C "$REPO" merge-base --is-ancestor "$full" "$ref" || die "refusing: $full is not reachable from $ref" done target="$COPIES/$full" [ ! -L "$target" ] || die "refusing: copy target is a symlink: $target" if [ -e "$target" ]; then echo "exists: $target"; return; fi mkdir -p "$COPIES" tmp="$(mktemp -d "$COPIES/.$full.XXXXXX")" trap "chmod -R u+w '$tmp' 2>/dev/null || true; rm -rf '$tmp'" EXIT git -C "$REPO" archive --format=tar "$full" -- "${COPY_PATHS[@]}" | tar -xf - -C "$tmp" || die "git archive failed; no copy written" [ -f "$tmp/package.json" ] && [ -f "$tmp/package-lock.json" ] || die 'package.json or package-lock.json missing in temp copy; npm ci not run' (cd "$tmp" && npm ci --no-audit --no-fund) >&2 || die 'npm ci failed; no copy written' find "$tmp" -type f -exec chmod a-w {} + find "$tmp" -type d -exec chmod 0555 {} + if ! mv -T "$tmp" "$target" 2>/dev/null; then [ -d "$target" ] && [ ! -L "$target" ] || die "could not rename copy into $target" echo "exists: $target"; return fi trap - EXIT echo "copied: $target" } wrapper() { plain_path "$BIN" 'bin directory' local root file lease tmp root="$(copy_root)";file="$BIN/mosaic" [ -f "$file" ] && [ ! -L "$file" ] || die "refusing: wrapper is absent, not regular, or a symlink: $file" [ "$(sed -n '2p' "$file")" = "$MARKER" ] || die "refusing: foreign wrapper (missing Q81 marker on line 2): $file" lease='[ "${1:-}" = __lease-capability ] && exec "'"$BIN"'/mosaic-old" "$@"' [ "$(grep -Fxc "$lease" "$file" || true)" = 1 ] || die 'refusing: Q81 lease line missing or ambiguous' tmp="$(mktemp "$BIN/.mosaic.XXXXXX")" trap 'rm -f "$tmp"' EXIT printf '#!/usr/bin/env bash\n%s\n%s\nexec "%s/scripts/mosaic" "$@"\n' "$MARKER" "$lease" "$root" > "$tmp" chmod 0755 "$tmp";mv -f "$tmp" "$file";trap - EXIT echo "wrapper: $root" } unit_root() { local out load root exec_start out="$(systemctl --user show "$1" -p LoadState -p WorkingDirectory -p ExecStart 2>/dev/null)" || { echo 'absent (systemctl read failed)';return; } load="$(sed -n 's/^LoadState=//p' <<< "$out")"; root="$(sed -n 's/^WorkingDirectory=//p' <<< "$out")" exec_start="$(sed -n 's/^ExecStart=//p' <<< "$out")" if [ "$load" != loaded ] || [ -z "$root" ];then echo absent;return;fi case "$1" in mosaic-bus@*) [[ "$exec_start" == *"path=$root/scripts/mosaic ;"* ]] || { echo "$root (ExecStart mismatch)";return; };; mosaic-webui*) [[ "$exec_start" == *"$root/packages/webui/src/cli.mjs serve"* ]] || { echo "$root (ExecStart mismatch)";return; };; esac echo "$root" } check() { plain_path "$BIN" 'bin directory';[[ "$BUSINESS" =~ ^[a-z0-9][a-z0-9-]*$ ]] || die 'invalid business id' local bus console cli=absent line path bus="$(unit_root "mosaic-bus@$BUSINESS.service")";console="$(unit_root mosaic-webui.service)" if [ -f "$BIN/mosaic" ] && [ ! -L "$BIN/mosaic" ] && [ "$(sed -n '2p' "$BIN/mosaic")" = "$MARKER" ]; then line="$(tail -n 1 "$BIN/mosaic")" path="${line#exec \"}";path="${path%\" \"\$@\"}" if [ "$line" = "exec \"$path\" \"\$@\"" ] && [[ "$path" == */scripts/mosaic ]];then cli="${path%/scripts/mosaic}";fi fi printf 'bus: %s\nConsole: %s\nCLI: %s\n' "$bus" "$console" "$cli" [[ "$bus" == "$COPIES/"* ]] && [[ "${bus#"$COPIES/"}" =~ ^[0-9a-f]{40}$ ]] && [ -f "$bus/scripts/mosaic" ] && [ "$bus" = "$console" ] && [ "$bus" = "$cli" ] } [ $# -gt 0 ] || usage CMD="$1";shift case "$CMD" in copy|wrapper) [ $# -gt 0 ] || usage;ID="$1";shift;case "$ID" in -*)usage;;esac;parse "$@";if [ "$CMD" = copy ];then copy_commit;else wrapper;fi;; check) parse "$@";check;; -h|--help) usage 0;; *) usage;;esac