Files
Panama/setup/scripts/link-vicinae-scripts
T

194 lines
7.8 KiB
Bash
Executable File

#!/usr/bin/env bash
# Install Panama's searchable launcher actions as one narrow directory link.
# Keeping the runtime path stable lets Vicinae watch it while every command
# remains authored and reviewed in the Panama repository.
set -euo pipefail
_collect_vicinae_inputs() {
local extension="$1" output="$2"
[[ -d "$extension" && ! -L "$extension" \
&& -f "$extension/package.json" && ! -L "$extension/package.json" \
&& -f "$extension/package-lock.json" && ! -L "$extension/package-lock.json" ]] \
|| return 1
# Everything authored below the extension affects its build. npm's
# dependency tree is the sole exception and is reproduced from the lock.
find "$extension" -mindepth 1 \
\( -path "$extension/node_modules" -prune \) -o \
! -type d -print0 >"$output" || return 1
LC_ALL=C sort -z -o "$output" "$output" || return 1
}
_write_vicinae_manifest() {
local extension="$1" inputs="$2" output="$3"
local input relative digest
: >"$output" || return 1
while IFS= read -r -d '' input; do
[[ -f "$input" && ! -L "$input" && -r "$input" ]] || return 1
relative="${input#"$extension"/}"
[[ "$relative" != "$input" && -n "$relative" ]] || return 1
digest="$(sha256sum -- "$input" | awk '{ print $1 }')" || return 1
[[ "$digest" =~ ^[0-9a-f]{64}$ ]] || return 1
printf '%s\0%s\0' "$relative" "$digest" >>"$output" || return 1
done <"$inputs"
}
_vicinae_extension_digest() (
local extension="${1%/}" work=""
trap '[[ -z "$work" ]] || rm -rf -- "$work"' EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
work="$(mktemp -u -d -t panama-vicinae-digest.XXXXXX)" || exit 1
if ! mkdir -m 700 -- "$work"; then
work=""
exit 1
fi
_collect_vicinae_inputs "$extension" "$work/inputs.before" || exit 1
_write_vicinae_manifest \
"$extension" "$work/inputs.before" "$work/manifest.before" || exit 1
_collect_vicinae_inputs "$extension" "$work/inputs.after" || exit 1
_write_vicinae_manifest \
"$extension" "$work/inputs.after" "$work/manifest.after" || exit 1
cmp -s -- "$work/inputs.before" "$work/inputs.after" || exit 1
cmp -s -- "$work/manifest.before" "$work/manifest.after" || exit 1
sha256sum -- "$work/manifest.before" | awk '{ print $1 }'
)
_record_vicinae_digest() (
local built="$1" digest="$2" receipt temporary=""
trap '[[ -z "$temporary" ]] || rm -f -- "$temporary"' EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
[[ -d "$built" && ! -L "$built" ]] || exit 1
receipt="$built/.panama-source-sha256"
temporary="$(mktemp -u "$built/.panama-source-sha256.XXXXXX")" || exit 1
umask 077
if ! (set -o noclobber; : >"$temporary") 2>/dev/null; then
temporary=""
exit 1
fi
printf '%s\n' "$digest" >"$temporary" || exit 1
mv -f -- "$temporary" "$receipt" || exit 1
temporary=""
)
panama_path="${PANAMA_PATH:-$HOME/.local/share/Panama}"
vicinae_data_dir="${VICINAE_DATA_DIR:-$HOME/.local/share/vicinae}"
source_dir="$panama_path/config/local/share/vicinae/scripts"
scripts_dir="$vicinae_data_dir/scripts"
target_dir="$scripts_dir/panama"
backup_root="$vicinae_data_dir/backups"
backup_dir="$backup_root/panama.pre-panama"
legacy_backup="$scripts_dir/panama.pre-panama"
[[ -d "$source_dir" ]] || {
printf 'Panama command source is missing: %s\n' "$source_dir" >&2
exit 1
}
mkdir -p "$scripts_dir" "$backup_root"
# Older Panama builds preserved this directory alongside searchable commands.
# Move it out before reloading so those scripts cannot appear twice.
if [[ -e "$legacy_backup" ]]; then
if [[ -e "$backup_dir" ]]; then
printf 'Refusing to move %s; backup already exists at %s\n' \
"$legacy_backup" "$backup_dir" >&2
exit 1
fi
mv "$legacy_backup" "$backup_dir"
fi
if [[ -L "$target_dir" ]]; then
rm "$target_dir"
elif [[ -e "$target_dir" ]]; then
if [[ -e "$backup_dir" ]]; then
printf 'Refusing to replace %s; backup already exists at %s\n' \
"$target_dir" "$backup_dir" >&2
exit 1
fi
mv "$target_dir" "$backup_dir"
fi
ln -s "$source_dir" "$target_dir"
# ── Extensions ───────────────────────────────────────────────────────────────
#
# Script commands are a file and a shebang, so they are linked. Extensions are
# not: they are TypeScript that has to be compiled, and `vici build` writes its
# output straight into Vicinae's data directory rather than leaving a bundle to
# link. So the source lives in this repository and the build is what installs
# it.
#
# Never fatal, and never a reason to fail a stage. A build wants npm and the
# network, and neither is guaranteed at this point in an install -- npm arrives
# with nvm earlier in install-packages, which can itself be skipped. An
# extension that did not build is a launcher missing one command, not a desktop
# that failed to install.
extensions_source="$panama_path/config/local/share/vicinae/extensions"
# npm comes from nvm, and nvm is a shell function in a file that only an
# interactive shell sources. A stage is not one, so without this npm is simply
# absent and the build below is skipped -- silently, which is the whole problem.
#
# This machine hid the bug: /usr/sbin/npm existed, but only because Claude
# Desktop depends on nodejs and dragged it in. A machine without that would have
# installed the launcher and quietly not built its search extension.
if [[ -s /etc/profile.d/nvm.sh ]] && ! command -v npm >/dev/null 2>&1; then
# nvm's script reads unset variables, which `set -u` treats as fatal.
set +u
# shellcheck source=/dev/null
source /etc/profile.d/nvm.sh
set -u
fi
if [[ -d "$extensions_source" ]] && command -v npm >/dev/null 2>&1; then
for extension in "$extensions_source"/*/; do
[[ -f "$extension/package.json" ]] || continue
name="$(basename "$extension")"
# Skip only when a prior successful build records the digest of both
# manifests and every source byte. Directory mtimes do not change when
# an existing source file is edited.
built="$vicinae_data_dir/extensions/$name"
receipt="$built/.panama-source-sha256"
if ! source_digest="$(_vicinae_extension_digest "$extension")"; then
printf 'Vicinae extension %s inputs could not be verified; skipping\n' \
"$name" >&2
continue
fi
if [[ -f "$receipt" && ! -L "$receipt" ]] \
&& cmp -s <(printf '%s\n' "$source_digest") "$receipt"; then
printf 'Vicinae extension %s is already built\n' "$name"
continue
fi
printf 'Building Vicinae extension %s\n' "$name"
if ! (cd "$extension" && npm ci --silent >/dev/null 2>&1 \
&& npm run build >/dev/null 2>&1); then
printf 'Vicinae extension %s did not build; skipping\n' "$name" >&2
continue
fi
if ! final_digest="$(_vicinae_extension_digest "$extension")" \
|| [[ "$final_digest" != "$source_digest" ]]; then
printf 'Vicinae extension %s changed while building; receipt withheld\n' \
"$name" >&2
continue
fi
if ! _record_vicinae_digest "$built" "$source_digest"; then
printf 'Vicinae extension %s receipt could not be recorded\n' "$name" >&2
fi
done
elif [[ -d "$extensions_source" ]]; then
printf 'npm is not available, so Vicinae extensions were not built\n' >&2
fi
# The server also rescans periodically, but an explicit reload makes a setup
# run deterministic. If Vicinae is not active yet, its startup scan is enough.
if command -v vicinae >/dev/null 2>&1 && vicinae ping >/dev/null 2>&1; then
vicinae cmd launch core:reload-scripts >/dev/null 2>&1 || true
fi