Files
hermes-agent/scripts/_activation.sh
ethernet d6bb42e8ba feat(activate): add activate.fish for the fish shell
`activate` is bash syntax, so fish users had no way to take on the dev
environment. `activate.fish` does what `activate` does: snapshots the keys it
will change, applies the composed env, defines a `hermes` function that
refuses outside the worktree, adds the prompt prefix, and defines
`deactivate`. It shells out to the same bash library, so composition is not
reimplemented.

`pm.environments --format fish` emits the env as `set -gx` statements.
fish's read-only names (`PWD`, `SHLVL`, `_`, ...) are skipped because
`set -gx PWD` aborts the script, and values named `*PATH` stay lists.
`hermes_compose_env` takes the dialect (`sh` or `fish`) as its second argument.
2026-10-02 18:43:30 -04:00

127 lines
5.5 KiB
Bash

# Sourced, never executed: how a POSIX shell gets the Hermes dev environment.
# `activate` (interactive, reversible) and `scripts/run-in-hermes-env` (a child
# process, one-way) both build on it; neither owns any of it.
#
# hermes_sync REPO [FLAG] bring the install up to date (setup-hermes.sh)
# hermes_compose_env REPO DIALECT print the composed environment, `sh` or `fish`
# hermes_apply_env SCRIPT evaluate a composed `sh` script into this shell
# hermes_activation_current REPO is the inherited environment still REPO's?
# hermes_ensure_env REPO sync, compose and apply unless it is current
#
# pm records, beside the installed-state file named by __HERMES_ACTIVATED, one
# stamp per dependency input carrying the exact mtime that input had when the
# install was last verified against it (pm.environments.record_activation_inputs).
# Any input whose mtime DIFFERS from its stamp (newer or older, since a branch
# switch can move it either way) means the inherited environment may not match
# its inputs. `-nt`/`-ot` are bash builtins, so the check costs no process
# spawn, and every successful sync re-records, so one re-sync settles it.
# Missing stamps or inputs, a dangling sentinel or a literal "1" all read as
# stale, and so does an install that did not build the test environment:
# every environment this library composes includes it.
# hermes_sync REPO [--test-environment[=EXTRAS]]: run setup in a child, so a
# failure cannot exit or half-change the caller's shell and setup never
# republishes launchers or shell config. Progress goes to stderr.
hermes_sync() {
local repo="$1" test_environment="${2:---test-environment}"
(
unset PYTHONHOME PYTHONPATH VIRTUAL_ENV
bash "$repo/setup-hermes.sh" --runtime-only "$test_environment"
) >&2
}
# hermes_bootstrap_python REPO: print the interpreter that can run pm before
# any dependency is importable. It only emits the environment; it installs nothing.
hermes_bootstrap_python() {
local repo="$1" store candidate
for candidate in "$repo/.venv/bin/python" "$repo/.venv/Scripts/python.exe" \
"$repo/venv/bin/python" "$repo/venv/Scripts/python.exe"; do
[ -x "$candidate" ] && { printf '%s\n' "$candidate"; return 0; }
done
for store in "${HERMES_RUNTIME_DIR:-}" "$repo/../tools" "${HERMES_HOME:-$HOME/.hermes}/tools"; do
[ -n "$store" ] || continue
for candidate in "$store"/python-*/bin/python3 "$store"/python-*/python.exe \
"$store"/python-*/bin/python "$store"/python-*/bin/python.exe; do
[ -x "$candidate" ] && { printf '%s\n' "$candidate"; return 0; }
done
done
return 1
}
# hermes_compose_env REPO DIALECT: print the environment of the installed state
# as a script for a shell of DIALECT (`sh` or `fish`).
hermes_compose_env() {
local repo="$1" dialect="$2" python script
python="$(hermes_bootstrap_python "$repo")" || {
echo "no bootstrap Python found; run setup-hermes.sh" >&2
return 1
}
script="$(PYTHONHOME= PYTHONPATH="$repo" "$python" -m pm.environments --format "$dialect")" &&
[ -n "$script" ] || {
echo "could not read pm env (run ./setup-hermes.sh first)" >&2
return 1
}
printf '%s\n' "$script"
}
# hermes_apply_env SCRIPT: evaluate a script from `hermes_compose_env REPO sh`.
hermes_apply_env() {
# Resolved before the eval replaces PATH.
local cygpath name
cygpath="$(command -v cygpath 2>/dev/null || :)"
eval "$1"
# The MSYS/Cygwin runtime hands a native Windows Python these variables in
# Windows form (`C:\a;C:\b`) and converts them back only for its own
# children. Exported verbatim, bash splits PATH on ':' and finds no commands.
if [ -n "$cygpath" ]; then
PATH="$("$cygpath" -u -p "$PATH")"
for name in HOME TMPDIR TMP TEMP; do
if [ -n "${!name-}" ]; then
printf -v "$name" '%s' "$("$cygpath" -u "${!name}")"
fi
done
fi
}
# hermes_activation_current REPO: status 0 when the inherited environment
# matches REPO's current inputs.
hermes_activation_current() {
local repo="$1" sentinel stamps stamp input stamped=0
[ -n "${__HERMES_ACTIVATED:-}" ] && [ -e "${__HERMES_ACTIVATED}" ] || return 1
# activate.ps1 records a Windows path; Git Bash accepts it with slashes.
sentinel="${__HERMES_ACTIVATED//\\//}"
stamps="${sentinel%/*}/inputs"
[ -f "$stamps/.project-root" ] && [ -f "$stamps/.test-environment" ] || return 1
local owner="$repo" recorded
recorded="$(< "$stamps/.project-root")"
if command -v cygpath >/dev/null 2>&1; then
owner="$(cygpath -am "$repo")" || return 1
recorded="${recorded//\\//}"
[ "${owner,,}" = "${recorded,,}" ] || return 1
else
[ "$(cd "$owner" && pwd -P)" = "$recorded" ] || return 1
fi
for stamp in "$stamps"/* "$stamps"/*/*; do
[ -f "$stamp" ] || continue
stamped=1
input="$repo/${stamp#"$stamps"/}"
if [ ! -e "$input" ] || [ "$input" -nt "$stamp" ] || [ "$input" -ot "$stamp" ]; then
return 1
fi
done
[ "$stamped" = 1 ]
}
# hermes_ensure_env REPO: leave this shell with REPO's environment, doing
# nothing when the inherited one is current.
hermes_ensure_env() {
local repo="$1" script
hermes_activation_current "$repo" && return 0
hermes_sync "$repo" || {
echo "setup failed" >&2
return 1
}
script="$(hermes_compose_env "$repo" sh)" || return 1
hermes_apply_env "$script"
}