fix(dev): bind hermes to the active worktree after activation

Activation defined PATH and PYTHONPATH, but `hermes` still resolved to a
global command or an MSIX alias. The shell now defines `hermes` as a
function for this worktree. It runs this checkout's CLI only while the
shell is inside the tree, and it refuses outside it. The prompt gains a
branch prefix and drops it outside the tree. `deactivate` removes the
function and the prefix.
This commit is contained in:
ethernet
2026-09-22 12:21:25 -04:00
parent a9b931585b
commit 012bc02af1
8 changed files with 206 additions and 46 deletions

View File

@@ -79,14 +79,14 @@ En Bash, desde la raíz del repositorio:
```bash
source ./activate
python hermes --version
hermes --version
```
En PowerShell:
```powershell
. .\activate.ps1
python hermes --version
hermes --version
```
### Entorno independiente de pruebas

View File

@@ -125,20 +125,22 @@ Bash:
```bash
source ./activate
python hermes --version
hermes --version
```
PowerShell:
```powershell
. .\activate.ps1
python hermes --version
hermes --version
```
Run `python hermes` for this checkout, not a global `hermes` alias. PM activation
Run `hermes` for this checkout. Activation defines it as a function for this
worktree, so it hides a global `hermes` command or MSIX alias and refuses
outside the worktree. PM activation
syncs tools and Python dependencies before adding them to the shell. It does not
install JS workspaces or rewrite launchers and shell configuration. `deactivate`
restores the prior shell environment.
restores the prior shell environment and removes the function.
### Manual development and test environment
@@ -176,7 +178,7 @@ This test environment does not replace PM's application selection or tool
store. Do not point a bundled app at it or install into an MSIX payload.
For an isolated development instance, select a disposable `HERMES_HOME` before
starting the source command. Use `python hermes setup` to configure it rather
starting the source command. Use `hermes setup` to configure it rather
than copying production credentials into the checkout.
### JavaScript workspaces and website

View File

@@ -8,6 +8,10 @@
# Emits the composed pm env (PATH + tool vars) into 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.
#
# Sync through setup before selecting the environment. PM owns freshness;
# activation does not maintain a second dependency stamp.
# ============================================================================
@@ -82,6 +86,65 @@ for k, v in json.load(sys.stdin).items():
continue
print("export %s=%s" % (k, shlex.quote(str(v))))')"
# 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
@@ -94,9 +157,21 @@ deactivate() {
fi
unset "__HERMES_SAVED_$_hermes_k" "__HERMES_PRIOR_$_hermes_k"
done
unset __HERMES_SAVED_PATH __HERMES_KEY_LIST __HERMES_ACTIVATED
unset -f deactivate
unset _hermes_k _hermes_was _hermes_old
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)
@@ -104,4 +179,4 @@ __HERMES_KEY_LIST="$_hermes_keys"
unset _hermes_repo _hermes_os _hermes_arch _hermes_target _hermes_store \
_hermes_py _hermes_json _hermes_keys _hermes_k _hermes_entry _hermes_venvpy \
_hermes_winarch
_hermes_winarch _hermes_top

View File

@@ -65,13 +65,87 @@ foreach ($key in $global:_hermesKeys) {
foreach ($property in $composed.PSObject.Properties) {
Set-Item -Path "env:$($property.Name)" -Value ([string]$property.Value)
}
# This checkout, not whichever `hermes` PATH finds. A function beats PATH,
# an alias, and the MSIX execution alias. It runs only while the shell is
# inside this worktree.
$global:_hermesWorktree = $repo
# The branch names the worktree. A checkout cannot share a branch with another.
# git's "not a repository" is not an activation failure: the directory name is
# the label, and the command still refuses outside this tree.
$branch = $null
try { $branch = & git -C $repo rev-parse --abbrev-ref HEAD 2>$null } catch { $branch = $null }
if ($branch -and $branch -ne 'HEAD') {
$global:_hermesWorktreeName = $branch
} else {
$global:_hermesWorktreeName = Split-Path -Leaf $repo
}
if (Test-Path function:prompt) {
$global:_hermesSavedPrompt = (Get-Item function:prompt).ScriptBlock
} else {
$global:_hermesSavedPrompt = $null
}
function global:_hermesWorktreeHere {
# Prompt calls this after every command. Keep the user's exit code.
$saved = $global:LASTEXITCODE
$top = $null
try { $top = & git rev-parse --show-toplevel 2>$null } catch { $top = $null }
$global:LASTEXITCODE = $saved
if (-not $top) { return $false }
$here = [System.IO.Path]::GetFullPath($top).TrimEnd('\')
$root = [System.IO.Path]::GetFullPath($global:_hermesWorktree).TrimEnd('\')
return $here.Equals($root, [System.StringComparison]::OrdinalIgnoreCase)
}
function global:hermes {
if (-not (_hermesWorktreeHere)) {
$here = (Get-Location).Path
Write-Error "hermes: $here is outside $($global:_hermesWorktree); refusing (the installed command is hidden while this checkout is active)" -ErrorAction Continue
$global:LASTEXITCODE = 1
return
}
Push-Location -LiteralPath $global:_hermesWorktree
try {
$py = $env:PYTHON
if (-not $py) {
foreach ($candidate in @(
'.venv\Scripts\python.exe', 'venv\Scripts\python.exe',
'.venv\bin\python', 'venv\bin\python'
)) {
if (Test-Path -LiteralPath $candidate) { $py = $candidate; break }
}
}
if (-not $py) { $py = 'python' }
& $py hermes @args
} finally {
Pop-Location
}
}
function global:prompt {
$prefix = ''
if (_hermesWorktreeHere) { $prefix = "($($global:_hermesWorktreeName)) " }
if ($global:_hermesSavedPrompt) {
return $prefix + (& $global:_hermesSavedPrompt)
}
return "$prefix$($(Get-Location).Path)> "
}
function global:deactivate {
foreach ($key in $global:_hermesKeys) {
$saved = $global:_hermesSaved[$key]
if ($saved.WasSet) { Set-Item -Path "env:$key" -Value $saved.Value }
else { Remove-Item -Path "env:$key" -ErrorAction SilentlyContinue }
}
if ($global:_hermesSavedPrompt) {
Set-Item -Path function:prompt -Value $global:_hermesSavedPrompt
} else {
Remove-Item function:prompt -ErrorAction SilentlyContinue
}
$global:_hermesKeys = $null
$global:_hermesSaved = $null
Remove-Item function:deactivate
$global:_hermesWorktree = $null
$global:_hermesWorktreeName = $null
$global:_hermesSavedPrompt = $null
Remove-Item function:deactivate, function:hermes, function:_hermesWorktreeHere -ErrorAction SilentlyContinue
}

View File

@@ -50,19 +50,20 @@ Bash:
```bash
source ./activate
python hermes --version
hermes --version
```
PowerShell:
```powershell
. .\activate.ps1
python hermes --version
hermes --version
```
Run `python hermes` for this checkout, not a global `hermes` alias. PM activation
syncs tools and Python dependencies before adding them to the shell. It does
not install JS workspaces or rewrite launchers and shell configuration. `deactivate` restores the prior shell environment.
Run `hermes` for this checkout. Activation defines it as a function for this
worktree, so it hides a global `hermes` alias and refuses outside the worktree.
PM activation syncs tools and Python dependencies before adding them to the shell. It does
not install JS workspaces or rewrite launchers and shell configuration. `deactivate` restores the prior shell environment and removes the function.
### Manual development and test environment {#manual-development-and-test-environment}
@@ -100,7 +101,7 @@ This test environment does not replace PM's application selection or tool
store. Do not point a bundled app at it or install into an MSIX payload.
For an isolated development instance, select a disposable `HERMES_HOME` before
starting the source command. Use `python hermes setup` to configure it rather
starting the source command. Use `hermes setup` to configure it rather
than copying production credentials into the checkout.
### JavaScript workspaces and website

View File

@@ -325,10 +325,16 @@ rebuild. A setup failure returns an error before changing the activated shell
environment, including when re-sourcing an already active environment.
After sync, activation prepends installed PM tools to `PATH` and sets
`PYTHONPATH` to this checkout and its selected dependency tree. It does not
change an OS-wide PATH or activate a conventional venv prompt.
`PYTHONPATH` to this checkout and its selected dependency tree. It also
defines `hermes` as a shell function for this worktree. The function runs
this checkout's CLI and hides the installed command, including an MSIX alias.
It runs only while the shell is inside this worktree and refuses outside it,
so a sibling worktree does not inherit the command. The prompt gains a prefix
naming the branch, and drops it outside the tree. It does not
change an OS-wide PATH or install a conventional venv prompt.
Start in a clean shell rather than nesting this inside another venv.
`deactivate` restores the environment values captured by the activation script.
`deactivate` restores the environment values captured by the activation script,
and removes the function and the prompt prefix.
It does not uninstall packages or stop processes that you started.
Verify the interpreter and source before doing work:
@@ -338,7 +344,7 @@ python -c "import sys, pm; print(sys.executable); print(pm.__file__)"
python -c "import httpx; print(httpx.__file__)"
node --version
npm --version
python hermes --version
hermes --version
```
`python` must resolve to the PM store interpreter. `pm.__file__` must point
@@ -348,13 +354,14 @@ attention, even if `source ./activate` itself returned successfully.
### Work on this source tree
Use checkout-qualified commands so a global `hermes` command or MSIX alias
cannot run a different installation:
`hermes` is this worktree's CLI while the shell is inside it. Outside the
worktree the function refuses, so it cannot run another checkout's tree or
fall through to an installed command:
```bash
python hermes setup
python hermes
python hermes --tui
hermes setup
hermes
hermes --tui
python -m pm.cli status
```

View File

@@ -56,29 +56,29 @@ Do not clone into a signed app package or overwrite the packaged runtime.
Read the [developer workflow](../reference/package-management.md#developer-workflow)
for native build prerequisites and current bootstrap limitations. Select your
intended `HERMES_HOME` before preparation, then use the checkout's PM bootstrap:
intended `HERMES_HOME` before preparation, then activate. Activation runs the
bootstrap itself:
```bash
bash setup-hermes.sh
source ./activate
python hermes --version
hermes --version
```
On native Windows, use PowerShell:
```powershell
.\setup-hermes.ps1
. .\activate.ps1
python hermes --version
hermes --version
```
The bootstrap reads tool pins from `pm/lock.json` and delegates installation
to PM. Current first-party code requires Python 3.14 (`>=3.14,<3.15`).
The source default is the `all` extra, not the desktop bundle's `--all-extras`.
Activation composes the installed tool environment. `python hermes` explicitly
runs this checkout and avoids an older `hermes` command or MSIX alias on PATH.
`deactivate` restores the shell environment when you finish.
Activation composes the installed tool environment and defines `hermes` as this
worktree's CLI. The function hides an older `hermes` command or MSIX alias and
refuses outside the worktree.
`deactivate` restores the shell environment and removes the function when you finish.
For test dependencies and manual environments, use the
[development setup](../developer-guide/contributing.md).
@@ -95,23 +95,23 @@ POSIX example:
```bash
export HERMES_HOME="$HOME/hermes-source-data"
python hermes setup
python hermes
hermes setup
hermes
```
PowerShell example:
```powershell
$env:HERMES_HOME = Join-Path $HOME 'hermes-source-data'
python hermes setup
python hermes
hermes setup
hermes
```
If you change the home after preparing PM state, run the bootstrap for that
home before relying on its selected dependencies. Do not assume that changing
the environment variable moves data or copies runtime state.
To build a source desktop, run `python hermes desktop` from the prepared
To build a source desktop, run `hermes desktop` from the prepared
checkout. Opening the old packaged app still starts its packaged backend.
## Docker users
@@ -158,11 +158,11 @@ diagnostics and garbage collection rather than deleting the shared data root.
## Troubleshooting
- **Wrong version:** inspect command resolution, then use `python hermes --version`
- **Wrong version:** inspect command resolution, then use `hermes --version`
from the activated checkout.
- **Missing dependencies:** run `python -m pm.cli install` from the intended
source environment, then restart the affected Hermes process.
- **Gateway already running:** inspect `python hermes gateway status` for the
- **Gateway already running:** inspect `hermes gateway status` for the
selected profile. Stop the identified owner; do not kill unrelated processes.
- **Different skills after first run:** newer code can sync bundled skills into
the data home. A source checkout is not a read-only view of that home.

View File

@@ -45,19 +45,20 @@ Bash:
```bash
source ./activate
python hermes --version
hermes --version
```
PowerShell:
```powershell
. .\activate.ps1
python hermes --version
hermes --version
```
PowerShell 开头的点和空格用于 dot-source,不能省略。
激活通过 PM 准备工具并同步依赖,但不创建 JS workspaces,也不设置常规 venv 提示符。
使用 `python hermes` 明确运行当前 checkout,避免命中全局命令或 MSIX 别名。
激活把 `hermes` 定义成当前 worktree 的函数,因此会盖住全局命令和 MSIX 别名,
并在离开该 worktree 时拒绝运行。
`deactivate` 恢复激活前的环境,不卸载依赖或停止已启动的进程。
### 独立开发和测试环境 {#manual-development-and-test-environment}
@@ -79,7 +80,7 @@ PM 不会自动删除已有目录。不要通过原始 pip 或 uv 命令修改 H
Windows 上通过 Bash 运行 `scripts/run_tests.sh`,并预先准备本机 C++ 编译环境。
独立测试环境不替代 PM 工具存储或应用的依赖选择。不要修改签名应用的载荷。
运行开发实例前,选择临时的 `HERMES_HOME`,再使用 `python hermes setup` 配置它。
运行开发实例前,选择临时的 `HERMES_HOME`,再使用 `hermes setup` 配置它。
不要把生产凭据复制到 checkout。
从仓库根目录运行 `npm ci` 安装 JS workspaces。网站单独使用: