From 012bc02af136334f8165d8a014cda199eb33a344 Mon Sep 17 00:00:00 2001 From: ethernet Date: Tue, 22 Sep 2026 12:21:25 -0400 Subject: [PATCH] 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. --- CONTRIBUTING.es.md | 4 +- CONTRIBUTING.md | 12 +-- activate | 83 ++++++++++++++++++- activate.ps1 | 76 ++++++++++++++++- website/docs/developer-guide/contributing.md | 13 +-- website/docs/reference/package-management.md | 25 ++++-- .../docs/user-guide/switching-to-source.md | 30 +++---- .../current/developer-guide/contributing.md | 9 +- 8 files changed, 206 insertions(+), 46 deletions(-) diff --git a/CONTRIBUTING.es.md b/CONTRIBUTING.es.md index 8eaff6e207..7768e855db 100644 --- a/CONTRIBUTING.es.md +++ b/CONTRIBUTING.es.md @@ -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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 714b0119a5..281f463b5d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 diff --git a/activate b/activate index b9329ac1e5..6e40f1380a 100755 --- a/activate +++ b/activate @@ -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 diff --git a/activate.ps1 b/activate.ps1 index 4e5c69d9fa..a77b6e641d 100755 --- a/activate.ps1 +++ b/activate.ps1 @@ -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 } diff --git a/website/docs/developer-guide/contributing.md b/website/docs/developer-guide/contributing.md index ab301b95a2..1a0585ff5b 100644 --- a/website/docs/developer-guide/contributing.md +++ b/website/docs/developer-guide/contributing.md @@ -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 diff --git a/website/docs/reference/package-management.md b/website/docs/reference/package-management.md index e3f00b2fa4..0f3e08db10 100644 --- a/website/docs/reference/package-management.md +++ b/website/docs/reference/package-management.md @@ -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 ``` diff --git a/website/docs/user-guide/switching-to-source.md b/website/docs/user-guide/switching-to-source.md index 47aaf5e1b0..3169095a50 100644 --- a/website/docs/user-guide/switching-to-source.md +++ b/website/docs/user-guide/switching-to-source.md @@ -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. diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/developer-guide/contributing.md b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/developer-guide/contributing.md index 042ac52393..73994f434b 100644 --- a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/developer-guide/contributing.md +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/developer-guide/contributing.md @@ -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。网站单独使用: