Files
hermes-agent/activate
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

182 lines
7.1 KiB
Bash
Executable File

#!/usr/bin/env bash
# ============================================================================
# venv-style activation of the Hermes dev environment for an interactive shell.
#
# source ./activate (bash / zsh, repo root)
# source ./activate.fish (fish)
# .\activate.ps1 (PowerShell)
#
# Applies the composed pm env (PATH + tool vars) to the CURRENT shell, with
# save/restore: `deactivate` undoes exactly what activation changed.
#
# `hermes` becomes a shell function for this checkout. It runs only while the
# shell is inside this worktree and refuses outside it, so a sibling worktree
# keeps its own command. A prompt prefix names the worktree.
#
# Scripts and other processes do not source this: they run under
# `scripts/run-in-hermes-env`. Both build on scripts/_activation.sh.
#
# Sync through setup before selecting the environment. PM owns freshness;
# activation does not maintain a second dependency stamp. It trusts the
# recorded tool digest instead of re-hashing every entry. `hermes pm
# install` and `hermes update` keep that check.
# ============================================================================
_HERMES_REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Options: `--test-extras a,b` selects the test environment's runtime extras
# (default: [all]); `--` ends options.
# A bare `source ./activate` inherits the caller's positional parameters.
# Ignore unrelated words so scripts with their own arguments can source it.
_hermes_test_env="--test-environment"
_hermes_expect_extras=""
for _hermes_arg in "$@"; do
if [ -n "$_hermes_expect_extras" ]; then
_hermes_test_env="--test-environment=$_hermes_arg"
_hermes_expect_extras=""
continue
fi
case "$_hermes_arg" in
--) break ;;
--test-extras) _hermes_expect_extras=1 ;;
--test-extras=*) _hermes_test_env="--test-environment=${_hermes_arg#--test-extras=}" ;;
*) : ;;
esac
done
if [ -n "$_hermes_expect_extras" ]; then
printf '%s\n' 'activate: --test-extras needs a comma-separated list' >&2
unset _hermes_arg _hermes_test_env _hermes_expect_extras
return 2 2>/dev/null || exit 2
fi
unset _hermes_arg _hermes_expect_extras
# shellcheck source=scripts/_activation.sh
. "$_HERMES_REPO/scripts/_activation.sh"
if ! hermes_sync "$_HERMES_REPO" "$_hermes_test_env"; then
printf '%s\n' 'activate: setup failed; shell environment unchanged' >&2
unset _hermes_test_env
return 1 2>/dev/null || exit 1
fi
unset _hermes_test_env
# Guard against double-sourcing: venv activate precedent (re-activating is a
# no-op re-save; we keep it idempotent by deactivating first). Key on the
# function, not the sentinel: child shells inherit __HERMES_ACTIVATED from an
# activated parent but never the function, and there is nothing to undo there.
if [ "$(type -t deactivate 2>/dev/null)" = "function" ]; then
deactivate
fi
_hermes_script="$(hermes_compose_env "$_HERMES_REPO" sh)" || {
echo "activate: could not compose the environment (see above)" >&2
unset _hermes_script
return 1 2>/dev/null || exit 1
}
_hermes_keys="$(printf '%s\n' "$_hermes_script" | sed -n 's/^export \([A-Za-z_][A-Za-z0-9_]*\)=.*/\1/p' | tr '\n' ' ')"
# --- snapshot what we are about to change (deactivate restores this) ---
# __HERMES_ACTIVATED is part of the composed env below (it carries the
# installed-state path this environment was built from), so it is snapshotted
# and exported with every other key — no separate assignment here.
__HERMES_SAVED_PATH="$PATH"
for _hermes_k in $_hermes_keys; do
eval "__HERMES_SAVED_$_hermes_k=\"\${$_hermes_k+set}\""
eval "__HERMES_PRIOR_$_hermes_k=\${$_hermes_k-__HERMES_UNSET__}"
done
hermes_apply_env "$_hermes_script"
# This checkout, not whichever `hermes` PATH finds. A function beats PATH, an
# alias, and a hashed command, including the MSIX execution alias.
__HERMES_WORKTREE="$_HERMES_REPO"
# The branch names the worktree. A checkout cannot share a branch with another.
__HERMES_WORKTREE_NAME="$(git -C "$_HERMES_REPO" rev-parse --abbrev-ref HEAD 2>/dev/null || :)"
if [ -z "$__HERMES_WORKTREE_NAME" ] || [ "$__HERMES_WORKTREE_NAME" = HEAD ]; then
__HERMES_WORKTREE_NAME="$(basename "$_HERMES_REPO")"
fi
__HERMES_SAVED_PS1="${PS1-__HERMES_UNSET__}"
__HERMES_SAVED_PROMPT_COMMAND="${PROMPT_COMMAND-__HERMES_UNSET__}"
_hermes_worktree_here() {
_hermes_top="$(git rev-parse --show-toplevel 2>/dev/null)" || return 1
[ "$(cd "$_hermes_top" && pwd -P)" = "$(cd "$__HERMES_WORKTREE" && pwd -P)" ]
}
hermes() {
_hermes_worktree_here || {
printf '%s\n' "hermes: $(pwd) is outside $__HERMES_WORKTREE; refusing (the installed command is hidden while this checkout is active)" >&2
return 1
}
(
cd "$__HERMES_WORKTREE" || exit 1
if [ -n "${PYTHON-}" ]; then
exec "$PYTHON" hermes "$@"
elif [ -x .venv/Scripts/python.exe ]; then
exec .venv/Scripts/python.exe hermes "$@"
elif [ -x .venv/bin/python ]; then
exec .venv/bin/python hermes "$@"
elif [ -x venv/Scripts/python.exe ]; then
exec venv/Scripts/python.exe hermes "$@"
elif [ -x venv/bin/python ]; then
exec venv/bin/python hermes "$@"
else
exec python hermes "$@"
fi
)
}
_hermes_prompt() {
if [ "$__HERMES_SAVED_PS1" = "__HERMES_UNSET__" ]; then
_hermes_base_ps1=""
else
_hermes_base_ps1="$__HERMES_SAVED_PS1"
fi
if _hermes_worktree_here; then
PS1="($__HERMES_WORKTREE_NAME) $_hermes_base_ps1"
else
PS1="$_hermes_base_ps1"
fi
}
if [ -n "${PROMPT_COMMAND-}" ]; then
PROMPT_COMMAND="_hermes_prompt; ${PROMPT_COMMAND}"
else
PROMPT_COMMAND=_hermes_prompt
fi
_hermes_prompt
deactivate() {
export PATH="$__HERMES_SAVED_PATH"
for _hermes_k in $__HERMES_KEY_LIST; do
eval "_hermes_was=\"\$__HERMES_SAVED_$_hermes_k\""
eval "_hermes_old=\"\$__HERMES_PRIOR_$_hermes_k\""
if [ "$_hermes_was" != "set" ]; then
unset "$_hermes_k"
else
export "$_hermes_k=$_hermes_old"
fi
unset "__HERMES_SAVED_$_hermes_k" "__HERMES_PRIOR_$_hermes_k"
done
if [ "$__HERMES_SAVED_PS1" = "__HERMES_UNSET__" ]; then
unset PS1
else
PS1="$__HERMES_SAVED_PS1"
fi
if [ "$__HERMES_SAVED_PROMPT_COMMAND" = "__HERMES_UNSET__" ]; then
unset PROMPT_COMMAND
else
PROMPT_COMMAND="$__HERMES_SAVED_PROMPT_COMMAND"
fi
unset __HERMES_SAVED_PATH __HERMES_KEY_LIST __HERMES_ACTIVATED \
__HERMES_WORKTREE __HERMES_WORKTREE_NAME \
__HERMES_SAVED_PS1 __HERMES_SAVED_PROMPT_COMMAND
unset -f deactivate hermes _hermes_worktree_here _hermes_prompt
unset _hermes_k _hermes_was _hermes_old _hermes_top
}
# remember the key list for deactivate (kept out of the loop vars above)
__HERMES_KEY_LIST="$_hermes_keys"
# The composition library is not part of the user's shell.
unset -f hermes_sync hermes_bootstrap_python hermes_compose_env hermes_apply_env \
hermes_activation_current hermes_ensure_env
unset _HERMES_REPO _hermes_script _hermes_keys _hermes_k _hermes_top