cli, webui: bus, Console and mosaic CLI on one pinned copy (#1579)

Row 85 round 2. Built by Rocko, packet agents/rocko/work/queue-85/r2/,
candidate manifest
bd7ea899c7cacef43b415b8e9ecb164e1f36718ffbe64347881a2c5653170c98.
Filbert approved on #1579, comment 28256; Darkwing approved, comment
28258.

stack-copy.sh builds shared read-only copies under
XDG_DATA_HOME/mosaic-stack/copies; the bus and Console services render
and install against a copy with --copy and --pm. No live step is run
by this commit; those need Mos's go.

Sage's gate rerun on 3a1e11b83 with rows 85, 102 and 103 applied,
Docker up: node suites bus 128, business 67, cli 165, control-board
124, conversation 182, discord 178, harness 73, ledger 78, mosaic 69,
queue 155, runs 43, seat 113, tasks 92, webui 108; scripts auth 15,
conductor 17, config 24, discord 66, extension-package 18,
foundation 44, queue 28, release 14, task 98. All pass, 0 fail.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-13 16:34:47 -05:00
co-authored by Claude Opus 5.5
parent 3a1e11b83e
commit 305a012b13
11 changed files with 532 additions and 133 deletions
+74 -8
View File
@@ -24,13 +24,16 @@ set -euo pipefail
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TEMPLATE="$REPO/packages/cli/systemd/mosaic-bus.service.in"
UNIT_NAME="[email protected]"
USAGE="usage: scripts/bus-service.sh render [--claude PATH] | install [--dir DIR] [--no-reload] [--claude PATH] | uninstall [--dir DIR] [--no-reload] | status <business>"
USAGE="usage: scripts/bus-service.sh render [--copy ID [--copies DIR]] [--pm] [--claude PATH] | install [--copy ID [--copies DIR]] [--pm] [--dir DIR] [--no-reload] [--claude PATH] | uninstall [--dir DIR] [--no-reload] | status <business>"
die() { echo "bus-service: $*" >&2; exit 1; }
usage() { echo "$USAGE" >&2; exit 4; }
CLAUDE_BIN=""
CLAUDE_SET=0
COPY_ID=""
COPIES="${XDG_DATA_HOME:-$HOME/.local/share}/mosaic-stack/copies"
PM=0
# The unit's MOSAIC_CLAUDE_BIN line, or a comment when there is no claude to
# pin. systemd expands % and splits Environment= on spaces, so the path is
@@ -55,6 +58,16 @@ claude_line() {
render() {
[ -f "$TEMPLATE" ] || die "template missing: $TEMPLATE"
local root="$REPO"
if [ -n "$COPY_ID" ]; then
[[ "$COPY_ID" =~ ^[0-9a-f]{40}$ ]] || die "--copy must be a full 40-character commit id"
case "$COPIES" in /*) ;; *) die "copies directory must be absolute" ;; esac
case "$COPIES" in *[!A-Za-z0-9/._+-]*) die "copies directory contains a character the unit cannot carry" ;; esac
root="$COPIES/$COPY_ID"
[ -f "$root/scripts/mosaic" ] || die "no copy $COPY_ID in $COPIES"
fi
local pm_flags=""
[ "$PM" = 0 ] || pm_flags=" --pm"
local node_dir
node_dir="$(dirname "$(command -v node || true)")"
[ -n "$node_dir" ] && [ "$node_dir" != "." ] || die "node not found on PATH"
@@ -63,7 +76,7 @@ render() {
case "$REPO" in *@*|*'|'*) die "repository path contains a character the template cannot carry: $REPO" ;; esac
local claude
claude="$(claude_line)" || exit 1
sed -e "s|@REPO@|$REPO|g" -e "s|@PATH@|$path|g" -e "s|^@CLAUDE_ENV@$|$claude|" "$TEMPLATE"
sed -e "s|@REPO@|$root|g" -e "s|@PATH@|$path|g" -e "s|@PM_FLAGS@|$pm_flags|g" -e "s|^@CLAUDE_ENV@$|$claude|" "$TEMPLATE"
}
UNIT_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
@@ -73,14 +86,52 @@ parse_flags() {
case "$1" in
--dir) [ $# -ge 2 ] || usage; UNIT_DIR="$2"; shift 2 ;;
--no-reload) RELOAD=0; shift ;;
--copy) [ $# -ge 2 ] || usage; COPY_ID="$2"; shift 2 ;;
--copies) [ $# -ge 2 ] || usage; COPIES="$2"; shift 2 ;;
--pm) PM=1; shift ;;
--claude) [ $# -ge 2 ] || usage; CLAUDE_BIN="$2"; CLAUDE_SET=1; shift 2 ;;
*) usage ;;
esac
done
}
# Inspect effective unit metadata: drop-ins can override the installed template.
# Any failed read refuses before a write, including --no-reload installations.
install_guard() {
local out unit file
out="$(systemctl --user list-units --all --no-legend --plain 'mosaic-bus@*.service' 2>/dev/null)" || die "refusing: systemctl --user list-units failed, so the bus state is unknown; nothing was changed"
while read -r unit load state rest; do
[[ "$unit" == mosaic-bus@*.service ]] || continue
case "$state" in inactive|failed) ;; *) die "refusing: $unit is running ($state); Jason or Mos stops it first, then install, then start it" ;; esac
done <<< "$out"
# systemctl show rejects a template name. The lookup-path scan also
# covers stopped/unloaded instances that list-units does not enumerate.
local paths directory dropdir content
paths="$(systemd-analyze --user unit-paths 2>/dev/null)" || die "refusing: systemd-analyze --user unit-paths failed; drop-in paths are unknown; nothing was changed"
[ -n "$paths" ] || die "refusing: systemd unit paths are unknown; nothing was changed"
while IFS= read -r directory; do
[ -n "$directory" ] || continue
case "$directory" in /*) ;; *) die "refusing: invalid systemd unit path $directory; nothing was changed" ;; esac
[ -e "$directory" ] || [ -L "$directory" ] || continue
[ -d "$directory" ] && [ -r "$directory" ] && [ -x "$directory" ] || die "refusing: cannot inspect unit path $directory; nothing was changed"
# * includes the empty instance, i.e. [email protected].
for dropdir in "$directory"/mosaic-bus@*.service.d; do
[ -e "$dropdir" ] || [ -L "$dropdir" ] || continue
[ -d "$dropdir" ] && [ -r "$dropdir" ] && [ -x "$dropdir" ] || die "refusing: cannot inspect drop-ins $dropdir; nothing was changed"
for file in "$dropdir"/*.conf; do
[ -e "$file" ] || [ -L "$file" ] || continue
content="$(cat "$file" 2>/dev/null)" || die "refusing: cannot read drop-in $file; nothing was changed"
if grep -Eq '^[[:space:]]*(ExecStart|WorkingDirectory)[[:space:]]*=' <<< "$content"; then
die "refusing: drop-in $file overrides ExecStart or WorkingDirectory; Mos removes it first and reloads systemd"
fi
done
done
done <<< "$paths"
}
install_unit() {
parse_flags "$@"
install_guard
mkdir -p "$UNIT_DIR"
local target="$UNIT_DIR/$UNIT_NAME" tmp unit
unit="$(render)" || exit 1
@@ -120,13 +171,12 @@ MSG
uninstall_unit() {
parse_flags "$@"
[ "$CLAUDE_SET" = 0 ] || usage
[ "$CLAUDE_SET" = 0 ] && [ -z "$COPY_ID" ] && [ "$PM" = 0 ] || usage
local target="$UNIT_DIR/$UNIT_NAME"
if [ "$RELOAD" = 1 ]; then
local active
active="$(systemctl --user list-units --no-legend --plain 'mosaic-bus@*' 2>/dev/null | awk '$3 == "active" || $3 == "activating" || $3 == "deactivating" {print $1}')"
[ -z "$active" ] || die "refusing: instance(s) still running: $(echo "$active" | tr '\n' ' ')stop them first"
fi
local out active
out="$(systemctl --user list-units --all --no-legend --plain 'mosaic-bus@*.service' 2>/dev/null)" || die "refusing: systemctl read failed; nothing was changed"
active="$(awk '$3 != "inactive" && $3 != "failed" {print $1}' <<< "$out")"
[ -z "$active" ] || die "refusing: instance(s) still running: $active; Jason or Mos stops them first"
if [ -f "$target" ]; then
rm -f "$target"
echo "removed: $target"
@@ -148,6 +198,19 @@ status_unit() {
echo " enabled: $(systemctl --user is-enabled "$unit" 2>&1 || true)"
echo " active: $(systemctl --user is-active "$unit" 2>&1 || true)"
systemctl --user show "$unit" -p MainPID -p NRestarts -p ExecMainStartTimestamp -p Result -p Environment 2>/dev/null | sed 's/^/ /'
local effective root exec_start drops copy=checkout
effective="$(systemctl --user show "$unit" -p LoadState -p WorkingDirectory -p ExecStart -p DropInPaths 2>/dev/null)" || die "cannot read effective unit $unit"
root="$(sed -n 's/^WorkingDirectory=//p' <<< "$effective")"
exec_start="$(sed -n 's/^ExecStart=//p' <<< "$effective")"
drops="$(sed -n 's/^DropInPaths=//p' <<< "$effective")"
if [[ "${root##*/}" =~ ^[0-9a-f]{40}$ ]] && [ -f "$root/scripts/mosaic" ];then copy="${root##*/}";fi
echo "tree: ${root:-absent}"
echo "copy: $copy"
if [[ "$exec_start" =~ [[:space:]]--pm([[:space:];]|$) ]];then echo 'pm: on';else echo 'pm: off';fi
echo "drop-ins: ${drops:-none}"
local head
head="$(git -C "$REPO" rev-parse refs/heads/refactor 2>/dev/null || true)"
if [ "$copy" != checkout ] && [ "$head" != "$copy" ];then echo "refactor: ${head:-unknown} (another commit)";else echo "refactor: ${head:-unknown}";fi
"$REPO/scripts/mosaic" bus status | sed 's/^/ /' || true
}
@@ -157,6 +220,9 @@ case "$cmd" in
render)
while [ $# -gt 0 ]; do
case "$1" in
--copy) [ $# -ge 2 ] || usage; COPY_ID="$2"; shift 2 ;;
--copies) [ $# -ge 2 ] || usage; COPIES="$2"; shift 2 ;;
--pm) PM=1; shift ;;
--claude) [ $# -ge 2 ] || usage; CLAUDE_BIN="$2"; CLAUDE_SET=1; shift 2 ;;
*) usage ;;
esac
+103
View File
@@ -0,0 +1,103 @@
#!/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
+10 -64
View File
@@ -10,7 +10,7 @@
# write `git archive` of the paths the Console
# loads at COMMIT into DIR/<full id>/, read-only;
# DIR defaults to
# ${XDG_DATA_HOME:-~/.local/share}/mosaic-console
# ${XDG_DATA_HOME:-~/.local/share}/mosaic-stack/copies
# render [--copy ID [--copies DIR]] [FLAGS]
# print the rendered unit on stdout; without
# --copy it runs from this checkout
@@ -37,8 +37,7 @@
# --copy ID` with the same flags; Jason or Mos starts it. Rollback is the
# same with the earlier id. Copies stay until someone removes them. install
# refuses unless the bus unit runs from the same copy (bus_on_copy below),
# and nothing puts the bus on a copy yet, so the step can't be used until a
# later row does.
# row 85 supplies the shared copy and bus unit flags; Mos owns switching.
#
# FLAGS are serve's: --port N (default 7332; 7330 is the operator's own
# Console), --business ID, --board URL, and the preview set --public-host
@@ -72,7 +71,7 @@ die() { echo "webui-service: $*" >&2; exit 1; }
usage() { echo "$USAGE" >&2; exit 4; }
UNIT_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
COPIES="${XDG_DATA_HOME:-$HOME/.local/share}/mosaic-console"
COPIES="${XDG_DATA_HOME:-$HOME/.local/share}/mosaic-stack/copies"
COPY_ID=""
COPY_FLAGS=0
ALLOW_WORKTREE=0
@@ -152,33 +151,8 @@ serve_flags() {
echo "$flags"
}
# The paths the Console loads, the only ones a copy holds: serve's import
# closure (packages/webui/src/cli.mjs and the human transport child,
# packages/bus/src/human-cli.mjs) reaches these nine src directories
# (packages/tasks/src since row 61: reads.mjs imports its field table
# loader, which reads packages/tasks/fields.json);
# serve runs scripts/mosaic-config.mjs as its config check and reads roles/
# and RELEASE; the unit's Documentation= names the README. The queue read
# runs from the checkout (serve --checkout), so packages/queue is not here.
# The pinned-copy test serves from a copy: a missing import fails it at
# load, and a missing file read at request time (fields.json, RELEASE,
# roles/) fails the route that reads it, which the test also GETs.
COPY_PATHS=(
packages/bus/src
packages/business/src
packages/cli/src
packages/conversation/src
packages/discord/src
packages/harness/src
packages/seat/src
packages/webui/src
packages/webui/README.md
packages/tasks/src
packages/tasks/fields.json
scripts/mosaic-config.mjs
roles
RELEASE
)
# Row 85: stack-copy.sh owns the pushed runtime closure and npm install.
# This script delegates copy and shares its copies directory.
# N2 (row 75 review): copy and install read and point at this checkout, so
# they run from the canonical one, never a worktree someone may remove.
@@ -243,7 +217,7 @@ running() {
# has loaded, and none loaded refuses. Like the state read, a failed call
# refuses with one line.
bus_on_copy() {
local copy="$1" units unit out load paths p
local copy="$1" units unit out load paths p working
if [ -n "$BUSINESS" ]; then
units="mosaic-bus@$BUSINESS.service"
else
@@ -252,7 +226,7 @@ bus_on_copy() {
[ -n "$units" ] || die "refusing: no mosaic-bus@ unit is loaded, and the Console on the copy $copy needs its bus on that copy; name the business with --business ID"
fi
for unit in $units; do
out="$(systemctl --user show "$unit" -p LoadState -p ExecStart 2>/dev/null)" || die "refusing: systemctl --user show $unit failed, so the bus unit is unknown; nothing was changed"
out="$(systemctl --user show "$unit" -p LoadState -p WorkingDirectory -p ExecStart 2>/dev/null)" || die "refusing: systemctl --user show $unit failed, so the bus unit is unknown; nothing was changed"
load="$(printf '%s\n' "$out" | sed -n 's/^LoadState=//p')"
[ "$load" = loaded ] || die "refusing: no bus unit $unit is installed (LoadState ${load:-unknown}); the Console on the copy $copy needs its bus on that copy"
paths="$(printf '%s\n' "$out" | sed -n 's/^ExecStart=//p' | grep -o 'path=[^ ;]*' | sed 's/^path=//' || true)"
@@ -260,6 +234,8 @@ bus_on_copy() {
for p in $paths; do
case "$p" in "$copy"/*) ;; *) die "refusing: $unit runs from $p, not the copy $copy; the bus accepts only its own human-cli.mjs, so every Console bus call would fail human-required until the bus runs from the same copy" ;; esac
done
working="$(printf '%s\n' "$out" | sed -n 's/^WorkingDirectory=//p')"
[ "$working" = "$copy" ] || die "refusing: $unit WorkingDirectory is ${working:-unknown}, not the copy $copy; nothing was changed"
done
}
@@ -376,37 +352,7 @@ fetch(process.argv[1], { signal: AbortSignal.timeout(5000) })
# permission off every file, then renames it into place, so a copy at its
# final path is always whole and read-only. An existing copy is left alone.
copy_commit() {
[ $# -ge 1 ] || usage
local commit="$1"; shift
case "$commit" in -*) usage ;; esac
parse_flags "$@"
[ "$SERVE_SET" = 0 ] && [ "$DIR_FLAGS" = 0 ] && [ -z "$COPY_ID" ] || usage
canonical
copies_ok
case "$commit" in [A-Za-z0-9]*) ;; *) die "commit must be a commit id or branch name: $commit" ;; esac
case "$commit" in *[!A-Za-z0-9._/-]*) die "commit must be a commit id or branch name: $commit" ;; esac
git -C "$REPO" rev-parse --verify --quiet refs/heads/refactor >/dev/null || die "no refs/heads/refactor in $REPO"
local id
id="$(git -C "$REPO" rev-parse --verify --quiet "$commit^{commit}")" || die "not a commit: $commit"
git -C "$REPO" merge-base --is-ancestor "$id" refs/heads/refactor || die "refusing: $id is not reachable from refs/heads/refactor"
local target="$COPIES/$id"
if [ -e "$target" ]; then echo "exists: $target"; return 0; fi
mkdir -p "$COPIES"
COPY_TMP="$(mktemp -d "$COPIES/.$id.XXXXXX")"
trap 'chmod -R u+w "$COPY_TMP" 2>/dev/null; rm -rf "$COPY_TMP"' EXIT
git -C "$REPO" archive --format=tar "$id" -- "${COPY_PATHS[@]}" | tar -x -f - -C "$COPY_TMP" || die "git archive of $id failed; no copy written"
# Read-only files and directories: a writer that replaces by rename
# (git, editors, sed -i) can't change a copy either.
find "$COPY_TMP" -type f -exec chmod a-w {} +
find "$COPY_TMP" -type d -exec chmod 0555 {} +
if ! mv -T "$COPY_TMP" "$target" 2>/dev/null; then
[ -e "$target" ] || die "could not rename the copy into $target"
echo "exists: $target"
return 0
fi
trap - EXIT
echo "copied: $target"
echo "install it with: scripts/webui-service.sh install --copy $id, with the unit's flags"
exec "$REPO/scripts/stack-copy.sh" copy "$@"
}
[ $# -ge 1 ] || usage