Files
LLeMbas-Desktop/scripts/fetch-vendor.sh
T
HomerandClaude Opus 5.5 ac0d4ce623
ci / check (push) Failing after 2s
ci / start (push) Failing after 2s
Terminal: a shell on the agent chat's computer, before the first message too
The Terminal panel opens a shell on the device an agent chat works on, in its
folder: for a new agent chat as soon as the device and folder are chosen (the
server's /api/v2/devices/{device}/terminal/ws?directory=, which the chat's first
message takes over -- the same socket carries on), and for an open agent chat
from the header (/api/v2/chats/{chat}/terminal/ws, the web page's shell).
Offered where GET /devices says the device opens a terminal.

The socket is Rust's (src-tauri/src/terminal.rs, tungstenite on the rustls
ureq already brings): the bearer token goes in the handshake and never reaches
the page, which gets the frames as `terminal` events and sends keys, sizes and
close through commands. 4401 signs the app out as a 401 does.

xterm and its fit addon are the server's vendored build, pinned through
fetch-vendor.sh like CodeMirror and loaded on the first open. Send to chat puts
the selection, the last marked command and its output, or the screen into the
composer, never sent. The terminal and the activity panel take turns in the
panel column. Continue in a terminal stays in the chat's menu.

tests/ui: seven B3 cases with a mocked socket, at 1280 and 390 px.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 09:06:01 +00:00

126 lines
7.1 KiB
Bash
Executable File

#!/usr/bin/env bash
# Put the server's vendored CodeMirror build and xterm where the app's own pages read them from:
# ui/vendor/*.mjs (copies of the server's static/vendor/cm-*.mjs and lezer-*.mjs)
# ui/vendor/xterm.js, xterm.css, xterm-addon-fit.js (the terminal panel's, copied unchanged:
# classic scripts the panel loads when it is first opened)
#
# scripts/fetch-vendor.sh <server-ref> copy the modules at that ref (a tag such as
# v2.0.0-beta.1, or a branch) and write vendor.lock.json
# scripts/fetch-vendor.sh verify: ui/vendor matches vendor.lock.json
#
# The server is a git checkout, LLeMbas beside this repository by default (LLEMBAS_SERVER names
# another). Each module is checked against the server's own pin (its scripts/vendor.lock.json, at
# the same ref) before it is copied, so what the app carries is exactly what the server serves.
#
# The one change made to a copy: the server loads these modules through an import map
# ("@codemirror/view" -> its file), which the app's pages do not have -- an inline import map is a
# script the content security policy would have to allow. So the bare names in `import`/`from`
# are rewritten to the sibling files ("./cm-view.mjs"), and nothing else is touched. esm.sh's
# Node polyfill ("/node/process.mjs", which @lezer/lr imports to read process.env.LOG) becomes
# "./node-process.mjs", the server's own one-line stub (static/js/node-process.mjs), copied too:
# an absolute path is a 404 in the app, and a failed @lezer/lr takes every editor language with it.
#
# vendor.lock.json records the ref, the commit it resolved to, and for every file the server's
# SHA-256 and the copy's, which is what verify checks. Like fetch-tokens.sh: the file in the tree
# says which server version the editor comes from, and CI refuses a copy that drifted.
set -euo pipefail
cd "$(dirname "$0")/.."
SERVER="${LEMBAS_SERVER:-../LLeMbas}"
SOURCE="src/lembas/web/static/vendor"
SERVER_LOCK="scripts/vendor.lock.json"
OUT="ui/vendor"
LOCK="vendor.lock.json"
sha() { sha256sum "$1" | cut -d' ' -f1; }
if [ $# -eq 0 ]; then
[ -f "$LOCK" ] || { echo "fetch-vendor: no $LOCK; run with a server ref first" >&2; exit 1; }
ref="$(sed -n -E 's/^ *"tag": *"([^"]+)".*/\1/p' "$LOCK" | head -n1)"
bad=0
count=0
# One file a line: "name": {"server": "…", "copy": "…"}
while IFS=$'\t' read -r name want; do
count=$((count + 1))
if [ ! -f "$OUT/$name" ]; then
echo "fetch-vendor: $OUT/$name is missing" >&2
bad=1
elif [ "$(sha "$OUT/$name")" != "$want" ]; then
echo "fetch-vendor: $OUT/$name does not match $LOCK (server $ref)" >&2
bad=1
fi
done < <(sed -n -E 's/^ *"([a-z0-9-]+\.(mjs|js|css))": *\{"server": *"[0-9a-f]{64}", *"copy": *"([0-9a-f]{64})"\}.*/\1\t\3/p' "$LOCK")
for f in "$OUT"/*; do
grep -q "\"$(basename "$f")\"" "$LOCK" || { echo "fetch-vendor: $f is not in $LOCK" >&2; bad=1; }
done
[ "$count" -gt 0 ] || { echo "fetch-vendor: $LOCK lists no file" >&2; exit 1; }
[ "$bad" -eq 0 ] || exit 1
echo "fetch-vendor: $count files in $OUT match $LOCK (server $ref)"
exit 0
fi
REF="$1"
[ -d "$SERVER/.git" ] || [ -f "$SERVER/.git" ] \
|| { echo "fetch-vendor: $SERVER is not a git checkout of the LLeMbas server (set LEMBAS_SERVER)" >&2; exit 1; }
commit="$(git -C "$SERVER" rev-parse --verify --quiet "$REF^{commit}")" \
|| { echo "fetch-vendor: $REF is not a commit in $SERVER" >&2; exit 1; }
work="$(mktemp -d)"
trap 'rm -rf "$work"' EXIT
git -C "$SERVER" show "$commit:$SERVER_LOCK" > "$work/server-lock.json" \
|| { echo "fetch-vendor: $SERVER_LOCK is not in $SERVER at $REF" >&2; exit 1; }
names="$(git -C "$SERVER" ls-tree --name-only "$commit" "$SOURCE/" | sed -n -E 's#^.*/((cm|lezer)-[a-z0-9-]+\.mjs)$#\1#p' | sort)"
[ -n "$names" ] || { echo "fetch-vendor: no CodeMirror modules in $SOURCE at $REF" >&2; exit 1; }
mkdir -p "$work/out"
entries=()
for name in $names; do
git -C "$SERVER" show "$commit:$SOURCE/$name" > "$work/$name"
pinned="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get(sys.argv[2], {}).get("sha256", ""))' "$work/server-lock.json" "$name")"
have="$(sha "$work/$name")"
[ -n "$pinned" ] || { echo "fetch-vendor: $name is not pinned in the server's $SERVER_LOCK" >&2; exit 1; }
[ "$pinned" = "$have" ] || { echo "fetch-vendor: $name does not match the server's pin ($have, pinned $pinned)" >&2; exit 1; }
# "@codemirror/view" -> "./cm-view.mjs", "@lezer/lr" -> "./lezer-lr.mjs", "codemirror" -> "./cm-codemirror.mjs".
perl -pe 's#\b(from|import)(\s*)"\@codemirror/([a-z-]+)"#$1$2"./cm-$3.mjs"#g; s#\b(from|import)(\s*)"\@lezer/([a-z-]+)"#$1$2"./lezer-$3.mjs"#g; s#\b(from|import)(\s*)"codemirror"#$1$2"./cm-codemirror.mjs"#g; s#\b(from|import)(\s*)"/node/process\.mjs"#$1$2"./node-process.mjs"#g' \
"$work/$name" > "$work/out/$name"
if grep -E -q '(from|import)\s*"((@codemirror|@lezer)/|/)' "$work/out/$name"; then
echo "fetch-vendor: a bare module name or an absolute path survived in $name; fix the script" >&2
exit 1
fi
entries+=("$(printf ' "%s": {"server": "%s", "copy": "%s"}' "$name" "$have" "$(sha "$work/out/$name")")")
done
# xterm and its fit addon, for the terminal panel: copied as they are, each checked against the
# server's pin like the modules.
for name in xterm.js xterm.css xterm-addon-fit.js; do
git -C "$SERVER" show "$commit:$SOURCE/$name" > "$work/out/$name" \
|| { echo "fetch-vendor: $name is not in $SOURCE at $REF" >&2; exit 1; }
pinned="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get(sys.argv[2], {}).get("sha256", ""))' "$work/server-lock.json" "$name")"
have="$(sha "$work/out/$name")"
[ "$pinned" = "$have" ] || { echo "fetch-vendor: $name does not match the server's pin ($have, pinned $pinned)" >&2; exit 1; }
entries+=("$(printf ' "%s": {"server": "%s", "copy": "%s"}' "$name" "$have" "$have")")
done
# The polyfill stub, from the server's own scripts (it is not a download, so it has no pin there).
stub="src/lembas/web/static/js/node-process.mjs"
git -C "$SERVER" show "$commit:$stub" > "$work/out/node-process.mjs" \
|| { echo "fetch-vendor: $stub is not in $SERVER at $REF" >&2; exit 1; }
entries+=("$(printf ' "%s": {"server": "%s", "copy": "%s"}' node-process.mjs "$(sha "$work/out/node-process.mjs")" "$(sha "$work/out/node-process.mjs")")")
# Every module a copy imports must be one of the copies.
for f in "$work"/out/*.mjs; do
while read -r dep; do
[ -f "$work/out/$dep" ] || { echo "fetch-vendor: $(basename "$f") imports $dep, which is not copied" >&2; exit 1; }
done < <(grep -o -E '(from|import)\s*"\./[a-z0-9-]+\.mjs"' "$f" | sed -E 's/.*"\.\/([^"]+)"/\1/' | sort -u)
done
rm -rf "$OUT"
mkdir -p "$OUT"
cp "$work"/out/*.mjs "$work"/out/xterm.js "$work"/out/xterm.css "$work"/out/xterm-addon-fit.js "$OUT/"
{
printf '{\n "tag": "%s",\n "commit": "%s",\n "files": {\n' "$REF" "$commit"
last=$((${#entries[@]} - 1))
for i in "${!entries[@]}"; do
if [ "$i" -lt "$last" ]; then printf '%s,\n' "${entries[$i]}"; else printf '%s\n' "${entries[$i]}"; fi
done
printf ' }\n}\n'
} > "$LOCK"
echo "fetch-vendor: LLeMbas $REF (${commit:0:12}) $SOURCE -> $OUT (${#entries[@]} files); $LOCK written"