refactor(install): one completion tail shared by install and update
The installer ladder stopped at node-deps/path/desktop with its own semantics while an update ran launchers, product builds and post-build maintenance, so a fresh install and a finished update ended in different states: after re-running the installer at HEAD the products had no receipts and the read-only source acceptance failed. hermes_cli/source_completion.py now owns that tail -- publish launchers, build the products, run the maintenance -- and update_completion's _complete_selected calls it, so there is one implementation. install.sh and install.ps1 keep the bootstrap stages (prerequisites, repository, venv, python-deps, config) and hand off to it in a single `products` stage; --include-desktop selects the desktop product inside that stage instead of adding a second build stage, and `desktop` stays dispatchable via --stage for external callers. Windows keeps its installer-owned PATH publication (expose_cli answers "windows-installer-owned" on Windows) plus the packaged-artifact probe, ACL grant and shortcuts. The desktop stage no longer pre-syncs wake/voice: pm lazy-installs them at first use, as the update path does.
This commit is contained in:
@@ -76,7 +76,7 @@ def installation_command(repo_root: Path, args=(), *, module: str = "hermes_cli.
|
||||
return [str(root / ".hermes" / "bin" / "hermes"), *prefix, *args]
|
||||
|
||||
#: Launcher command names — keep in lockstep with scripts/install.ps1
|
||||
#: Stage-Path and hermes_cli/_install_repair.py.
|
||||
#: Publish-UserCommand and hermes_cli/_install_repair.py.
|
||||
WINDOWS_BIN_LAUNCHERS = ("hermes", "hermes-acp")
|
||||
|
||||
#: command name -> (entry module, callable) — mirrors pyproject.toml
|
||||
|
||||
107
hermes_cli/source_completion.py
Normal file
107
hermes_cli/source_completion.py
Normal file
@@ -0,0 +1,107 @@
|
||||
"""The tail every source install and update shares.
|
||||
|
||||
A fresh install (``scripts/install.sh`` / ``scripts/install.ps1``) and a finished
|
||||
update must land in the same state: launchers published, product builds current,
|
||||
and the post-build maintenance (skills sync, config migration) applied. One
|
||||
implementation, two callers -- an install and an update cannot drift apart.
|
||||
|
||||
``main()`` is the installer entry. It is handed the bootstrap interpreter the
|
||||
installer happens to have (uv, PM and no application dependencies), so it
|
||||
re-executes itself in PM's selected interpreter before touching any product.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# The installers run this file directly under an isolated interpreter, which
|
||||
# leaves the checkout off sys.path. Same idiom as hermes_cli/_launchers.py.
|
||||
if __name__ == "__main__":
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
|
||||
#: Second-phase marker: the re-executed interpreter, not the bootstrap one.
|
||||
_PREPARED = "--prepared"
|
||||
|
||||
|
||||
def complete_source_checkout(
|
||||
root: Path,
|
||||
*,
|
||||
desktop: bool,
|
||||
assume_yes: bool,
|
||||
gateway_mode: bool = False,
|
||||
pre_update_snapshot_id: str | None = None,
|
||||
pre_update_version: str | None = None,
|
||||
completion_message: str | None = None,
|
||||
announce: str | None = None,
|
||||
) -> bool:
|
||||
"""Publish commands, build the products, then run post-build maintenance.
|
||||
|
||||
Returns the SQLite runtime verdict: a positive unsafe-runtime probe withholds
|
||||
success here exactly as it does at the end of an update.
|
||||
"""
|
||||
from hermes_cli.source_build import build_update_products
|
||||
from hermes_cli.update_cmd_maint import _run_post_update_maintenance
|
||||
from hermes_cli.venv_sync import publish_launchers
|
||||
|
||||
root = Path(root)
|
||||
publish_launchers(root)
|
||||
build_update_products(root, desktop=desktop)
|
||||
if announce:
|
||||
print(announce)
|
||||
return _run_post_update_maintenance(
|
||||
assume_yes=assume_yes,
|
||||
gateway_mode=gateway_mode,
|
||||
pre_update_snapshot_id=pre_update_snapshot_id,
|
||||
had_desktop_app_before_update=desktop,
|
||||
pre_update_version=pre_update_version,
|
||||
completion_message=completion_message,
|
||||
)
|
||||
|
||||
|
||||
def _bootstrap_command(root: Path, argv: list[str]) -> list[str]:
|
||||
"""Re-enter the checkout on PM's selected interpreter with its environment."""
|
||||
from hermes_cli.runtime_paths import selected_venv
|
||||
from hermes_constants import venv_python_path
|
||||
|
||||
return [str(venv_python_path(selected_venv(root))), "-I", "-B", "-u",
|
||||
str(Path(__file__).resolve()), "--source", str(root), *argv, _PREPARED]
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
argv = list(sys.argv[1:] if argv is None else argv)
|
||||
prepared = _PREPARED in argv
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--source", type=Path, required=True)
|
||||
parser.add_argument("--desktop", action="store_true",
|
||||
help="Also build the packaged desktop app.")
|
||||
parser.add_argument("--interactive", action="store_true",
|
||||
help="Allow prompts; installers pass no flag and stay unattended.")
|
||||
args = parser.parse_args([argument for argument in argv if argument != _PREPARED])
|
||||
root = args.source.resolve()
|
||||
if not (root / "hermes_cli/source_completion.py").is_file():
|
||||
print(f"✗ {root} is not a Hermes source checkout", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
if prepared:
|
||||
# Dependencies are selected before any application import: this is the
|
||||
# same ordering the update completion guarantees.
|
||||
from hermes_cli.runtime_paths import activate_dependencies
|
||||
|
||||
activate_dependencies(root)
|
||||
ok = complete_source_checkout(
|
||||
root, desktop=args.desktop, assume_yes=not args.interactive,
|
||||
completion_message="✓ Install complete!",
|
||||
)
|
||||
return 0 if ok else 1
|
||||
|
||||
from hermes_cli.runtime_paths import activation_environment
|
||||
|
||||
command = _bootstrap_command(root, ["--desktop"] if args.desktop else [])
|
||||
return subprocess.call(command, cwd=root, env=activation_environment(root))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -160,10 +160,8 @@ def _prepare(request: dict, request_path: Path, result_path: Path) -> int:
|
||||
|
||||
def _complete_selected(request: dict) -> None:
|
||||
from hermes_cli import main, update_cmd, update_cmd_config
|
||||
from hermes_cli.source_completion import complete_source_checkout
|
||||
from hermes_cli.update_inventory import RuntimeRecord, UpdatePlan
|
||||
from hermes_cli.update_cmd_maint import _run_post_update_maintenance
|
||||
from hermes_cli.source_build import build_update_products
|
||||
from hermes_cli.venv_sync import publish_launchers
|
||||
|
||||
root = Path(request["source"])
|
||||
main.PROJECT_ROOT = root
|
||||
@@ -172,15 +170,14 @@ def _complete_selected(request: dict) -> None:
|
||||
plan = None if plan_data is None else UpdatePlan(**{
|
||||
**plan_data, "runtimes": [RuntimeRecord(**row) for row in plan_data.get("runtimes", [])]})
|
||||
update_cmd._sweep_bytecode_after_update(request["branch"])
|
||||
publish_launchers(root)
|
||||
build_update_products(root, desktop=request["desktop"])
|
||||
if not request.get("completion_message"):
|
||||
print("\n✓ Code updated!")
|
||||
complete = _run_post_update_maintenance(
|
||||
assume_yes=request["assume_yes"], gateway_mode=request["gateway_mode"],
|
||||
pre_update_snapshot_id=request["snapshot_id"],
|
||||
had_desktop_app_before_update=request["desktop"], pre_update_version=request["pre_update_version"],
|
||||
completion_message=request.get("completion_message"))
|
||||
# Launchers, products and post-build maintenance live in one place so an
|
||||
# install and an update cannot end in different states.
|
||||
complete = complete_source_checkout(
|
||||
root, desktop=request["desktop"], assume_yes=request["assume_yes"],
|
||||
gateway_mode=request["gateway_mode"], pre_update_snapshot_id=request["snapshot_id"],
|
||||
pre_update_version=request["pre_update_version"],
|
||||
completion_message=request.get("completion_message"),
|
||||
announce=None if request.get("completion_message") else "\n✓ Code updated!")
|
||||
# systemctl's KillMode=mixed fallback can kill this whole cgroup. Publish the
|
||||
# gateway watcher's status BEFORE that operation, and demote on later failure.
|
||||
if request["gateway_mode"]:
|
||||
|
||||
@@ -476,20 +476,20 @@ function Emit-Frame([bool]$ok, [string]$name, [bool]$skipped, [string]$reason =
|
||||
$frame | ConvertTo-Json -Compress | Write-Output
|
||||
}
|
||||
|
||||
$ProductTitle = if ($IncludeDesktop) { "Install command and app + desktop" } else { "Install command and app" }
|
||||
$Stages = @(
|
||||
@{ name = "prerequisites"; title = "System prerequisites"; category = "runtime"; needs_user_input = $false },
|
||||
@{ name = "repository"; title = "Download Hermes Agent"; category = "runtime"; needs_user_input = $false },
|
||||
@{ name = "venv"; title = "Create Python environment"; category = "runtime"; needs_user_input = $false },
|
||||
@{ name = "python-deps"; title = "Install Python dependencies"; category = "runtime"; needs_user_input = $false },
|
||||
@{ name = "node-deps"; title = "Install tool dependencies"; category = "runtime"; needs_user_input = $false },
|
||||
@{ name = "path"; title = "Install hermes command"; category = "runtime"; needs_user_input = $false },
|
||||
@{ name = "config"; title = "Prepare config and skills"; category = "configuration"; needs_user_input = $false },
|
||||
# The shared completion tail -- the same call `hermes update` makes -- so
|
||||
# the manifest and the run cannot disagree. -IncludeDesktop selects the
|
||||
# desktop product inside this stage instead of adding a second build stage.
|
||||
@{ name = "products"; title = $ProductTitle; category = "runtime"; needs_user_input = $false },
|
||||
@{ name = "setup"; title = "Configure API keys and settings"; category = "configuration"; needs_user_input = $true },
|
||||
@{ name = "gateway"; title = "Configure gateway service"; category = "configuration"; needs_user_input = $true }
|
||||
)
|
||||
if ($IncludeDesktop) {
|
||||
$Stages += @{ name = "desktop"; title = "Build desktop app"; category = "runtime"; needs_user_input = $false }
|
||||
}
|
||||
$Stages += @{ name = "complete"; title = "Finish install"; category = "runtime"; needs_user_input = $false }
|
||||
function Stage-Prerequisites {
|
||||
if (-not (Ensure-Git)) {
|
||||
@@ -558,11 +558,32 @@ function Stage-PythonDeps {
|
||||
Invoke-BootstrapPm
|
||||
}
|
||||
|
||||
function Stage-NodeDeps {
|
||||
Log "tool dependencies are managed by pm (hermes pm install)"
|
||||
function Invoke-SourceCompletion([bool]$Desktop) {
|
||||
# The whole tail in one place, by calling the completion an update calls:
|
||||
# publish the commands, build the products (tui/web, plus the desktop app
|
||||
# when asked), then run the post-build maintenance that syncs bundled
|
||||
# skills and migrates config. Node, browsers and the frontend build tools
|
||||
# arrive through pm as the build asks for them; the bootstrap interpreter
|
||||
# itself only re-enters the tree on PM's selected Python.
|
||||
$bootPy = Get-BootstrapPython
|
||||
$completionArgs = @('-I', '-B', '-X', 'utf8', 'hermes_cli/source_completion.py', '--source', $InstallDir)
|
||||
if ($Desktop) { $completionArgs += '--desktop' }
|
||||
Push-Location $InstallDir
|
||||
try {
|
||||
& $bootPy @completionArgs
|
||||
$code = $LASTEXITCODE
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
if ($code) { Fail "app products or command publication failed (exit $code)" }
|
||||
Log "app products and hermes command ready"
|
||||
}
|
||||
|
||||
function Stage-Path {
|
||||
function Publish-UserCommand {
|
||||
# PATH exposure stays installer-owned on Windows: expose_cli() answers
|
||||
# "windows-installer-owned" rather than creating the user-facing command,
|
||||
# so the install-scoped launchers the completion publishes are not the ones
|
||||
# the user's PATH points at.
|
||||
$binDir = Join-Path $HermesHome "bin"
|
||||
$bootPy = Get-BootstrapPython
|
||||
Push-Location $InstallDir
|
||||
@@ -577,6 +598,12 @@ function Stage-Path {
|
||||
Log "hermes command installed at $binDir"
|
||||
}
|
||||
|
||||
function Stage-Products {
|
||||
Invoke-SourceCompletion ([bool]$IncludeDesktop)
|
||||
Publish-UserCommand
|
||||
if ($IncludeDesktop) { Confirm-DesktopArtifact }
|
||||
}
|
||||
|
||||
function Set-LauncherUserPath([string]$binDir) {
|
||||
$userPath = [Environment]::GetEnvironmentVariable("Path", "User")
|
||||
if ($userPath -notlike "*$binDir*") {
|
||||
@@ -619,29 +646,20 @@ function Stage-Gateway {
|
||||
}
|
||||
|
||||
function Stage-Desktop {
|
||||
# Desktop support via the CURRENT runtime paths only. wake/voice extras
|
||||
# ride pm's venv sync ([all] does not include them; lazy install at
|
||||
# first use remains the fallback -- policy: Teknium, July 2026, #70509).
|
||||
# The build is `hermes desktop --build-only`, the same authority as
|
||||
# `hermes gui` and the update flow; the deleted installer-local
|
||||
# npm/Electron helpers must not reappear here.
|
||||
$bootPy = Get-BootstrapPython
|
||||
# External-caller contract: -Stage desktop stays dispatchable on its own
|
||||
# (see Invoke-StageByName). The work is the same completion call with the
|
||||
# desktop product selected. Voice and wake extras are not synced here: pm
|
||||
# lazy-installs them at first use (policy: Teknium, July 2026, #70509).
|
||||
Invoke-SourceCompletion $true
|
||||
Publish-UserCommand
|
||||
Confirm-DesktopArtifact
|
||||
}
|
||||
|
||||
function Confirm-DesktopArtifact {
|
||||
# Probe the packaged artifact the completion just built -- the same
|
||||
# candidates hermes_cli/main_desktop._desktop_packaged_executable resolves.
|
||||
Push-Location $InstallDir
|
||||
try {
|
||||
Log "ensuring desktop voice/wake dependencies via pm venv sync"
|
||||
& $bootPy -I -c "import sys; sys.path.insert(0, sys.argv[1]); from pm import sync_venv; sync_venv(['wake', 'voice'], explicit=True)" $InstallDir
|
||||
if ($LASTEXITCODE) {
|
||||
Write-Host "[hermes] voice/wake dependency sync failed (exit $LASTEXITCODE) -- they will lazy-install at first use" -ForegroundColor Yellow
|
||||
}
|
||||
Log "building desktop app (hermes desktop --build-only)"
|
||||
Invoke-InstalledHermes @('desktop', '--build-only')
|
||||
$code = $LASTEXITCODE
|
||||
if ($code) { Fail "desktop build failed (hermes desktop --build-only exited $code)" }
|
||||
|
||||
# Probe the produced artifact -- the same candidates
|
||||
# hermes_cli/main_desktop._desktop_packaged_executable resolves
|
||||
# (verified: --build-only returns the packaged app under
|
||||
# apps/desktop/release/, not the --source dist/).
|
||||
$desktopDir = Join-Path $InstallDir "apps\desktop"
|
||||
$candidates = @(
|
||||
(Join-Path $desktopDir "release\win-unpacked\Hermes.exe"),
|
||||
@@ -762,8 +780,7 @@ function Invoke-StageByName([string]$name) {
|
||||
"repository" { Stage-Repository }
|
||||
"venv" { Stage-Venv }
|
||||
"python-deps" { Stage-PythonDeps }
|
||||
"node-deps" { Stage-NodeDeps }
|
||||
"path" { Stage-Path }
|
||||
"products" { Stage-Products }
|
||||
"config" { Stage-Config }
|
||||
"setup" { Stage-Setup }
|
||||
"gateway" { Stage-Gateway }
|
||||
@@ -805,9 +822,8 @@ if ($Stage) {
|
||||
# The $Stages table is the single authoritative list: it drives the
|
||||
# -Manifest output AND the no-flag ladder, so -IncludeDesktop affects
|
||||
# the real run exactly as the manifest advertises. "desktop" stays
|
||||
# directly dispatchable via -Stage even without the flag (long-standing
|
||||
# external-caller contract; the bootstrap frontend always pairs it
|
||||
# with -IncludeDesktop when it lists the stage).
|
||||
# directly dispatchable via -Stage even though it is never listed
|
||||
# (long-standing external-caller contract).
|
||||
$known = @($Stages | ForEach-Object { $_.name })
|
||||
if ($known -notcontains $Stage -and $Stage -ne "desktop") {
|
||||
if ($Json) { Emit-Frame $false $Stage $false "unknown stage: $Stage" }
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
# Hermes Agent bootstrap: git checkout + venv + hermes command on PATH.
|
||||
# Heavy dependencies (tool binaries, browsers, node) are pm's job after
|
||||
# this: `hermes pm install`. Stage protocol kept for Hermes-Setup:
|
||||
# Hermes Agent bootstrap: clone, acquire uv/Python, then hand the checkout to
|
||||
# the same completion an update runs -- command publication, product builds and
|
||||
# post-build maintenance -- so a fresh install and a finished update land in one
|
||||
# state. Heavy dependencies (tool binaries, browsers, node) are pm's job:
|
||||
# `hermes pm install`.
|
||||
#
|
||||
# Stage protocol kept for Hermes-Setup:
|
||||
# --manifest print the stage list as JSON
|
||||
# --stage NAME [--json] run one stage
|
||||
# --non-interactive skip stages that need input
|
||||
# --include-desktop add the desktop build stage
|
||||
# --include-desktop build the desktop app too (products stage)
|
||||
set -u
|
||||
|
||||
# Prevent uv from discovering config files (uv.toml, pyproject.toml) from the
|
||||
@@ -227,12 +231,22 @@ stage_result() {
|
||||
}
|
||||
|
||||
# The single authoritative stage list: emit_manifest prints it AND the
|
||||
# no-flag ladder runs it, so --include-desktop affects the real run
|
||||
# exactly as the manifest advertises.
|
||||
# no-flag ladder runs it. `products` is the shared completion tail -- the same
|
||||
# call `hermes update` makes -- so the manifest and the run cannot disagree.
|
||||
# `desktop` stays directly dispatchable via --stage for external callers, but
|
||||
# is never listed: --include-desktop selects the desktop product inside
|
||||
# `products` instead of adding a second build stage.
|
||||
stage_names() {
|
||||
printf '%s\n' prerequisites repository venv python-deps node-deps path config setup gateway
|
||||
[ "$INCLUDE_DESKTOP" = true ] && printf '%s\n' desktop
|
||||
printf '%s\n' complete
|
||||
printf '%s\n' prerequisites repository venv python-deps config products setup gateway complete
|
||||
}
|
||||
|
||||
# "title|category|needs_user_input".
|
||||
products_record() {
|
||||
if [ "$INCLUDE_DESKTOP" = true ]; then
|
||||
echo "Install command and app + desktop|runtime|false"
|
||||
else
|
||||
echo "Install command and app|runtime|false"
|
||||
fi
|
||||
}
|
||||
|
||||
# "$1" stage name -> its manifest record fields (title|category|needs_user_input).
|
||||
@@ -242,9 +256,8 @@ stage_record() {
|
||||
repository) echo "Download Hermes Agent|runtime|false" ;;
|
||||
venv) echo "Create Python environment|runtime|false" ;;
|
||||
python-deps) echo "Install Python dependencies|runtime|false" ;;
|
||||
node-deps) echo "Install tool dependencies|runtime|false" ;;
|
||||
path) echo "Install hermes command|runtime|false" ;;
|
||||
config) echo "Prepare config and skills|configuration|false" ;;
|
||||
products) products_record ;;
|
||||
setup) echo "Configure API keys and settings|configuration|true" ;;
|
||||
gateway) echo "Configure gateway service|configuration|true" ;;
|
||||
desktop) echo "Build desktop app|runtime|false" ;;
|
||||
@@ -363,22 +376,28 @@ stage_python_deps() {
|
||||
bootstrap_pm
|
||||
}
|
||||
|
||||
stage_node_deps() {
|
||||
# Tool binaries, node, browsers: pm packages, installed on demand or
|
||||
# via `hermes pm install`. Nothing to do at bootstrap time.
|
||||
log "tool dependencies are managed by pm (hermes pm install)"
|
||||
stage_products() {
|
||||
# The whole tail in one place, by calling the completion an update calls:
|
||||
# publish the commands, build the products (tui/web, plus the desktop app
|
||||
# under --include-desktop), then run the post-build maintenance that syncs
|
||||
# bundled skills and migrates config. Node, browsers and the frontend build
|
||||
# tools arrive through pm as the build asks for them; the bootstrap
|
||||
# interpreter itself only re-enters the tree on PM's selected Python.
|
||||
local boot_py
|
||||
local args=(--source "$INSTALL_DIR")
|
||||
bootstrap_python
|
||||
[ "$INCLUDE_DESKTOP" = true ] && args+=(--desktop)
|
||||
(cd "$INSTALL_DIR" && "$boot_py" -I -B -X utf8 hermes_cli/source_completion.py "${args[@]}") \
|
||||
|| fail "app products or command publication failed"
|
||||
log "app products and hermes command ready"
|
||||
}
|
||||
|
||||
stage_path() {
|
||||
local link_dir="$HOME/.local/bin"
|
||||
local boot_py
|
||||
bootstrap_python
|
||||
(cd "$INSTALL_DIR" && "$boot_py" -I -X utf8 hermes_cli/_launchers.py "$link_dir") || fail "launcher publication failed"
|
||||
case ":$PATH:" in
|
||||
*":$link_dir:"*) : ;;
|
||||
*) log "add $link_dir to your PATH to use the hermes command" ;;
|
||||
esac
|
||||
log "hermes command installed at $link_dir/hermes"
|
||||
stage_desktop() {
|
||||
# External-caller contract: `--stage desktop` stays dispatchable on its own
|
||||
# (the manifest never lists it now -- --include-desktop selects the desktop
|
||||
# product inside `products`). Same completion call, desktop selected.
|
||||
INCLUDE_DESKTOP=true
|
||||
stage_products
|
||||
}
|
||||
|
||||
stage_config() {
|
||||
@@ -405,12 +424,6 @@ stage_gateway() {
|
||||
"$INSTALL_DIR/.hermes/bin/hermes" gateway install || fail "gateway installation failed"
|
||||
}
|
||||
|
||||
stage_desktop() {
|
||||
# `hermes desktop --build-only` is the current authority (same path as
|
||||
# `hermes gui` / the update flow); no installer-local node/electron code.
|
||||
"$INSTALL_DIR/.hermes/bin/hermes" desktop --build-only || fail "desktop build failed"
|
||||
}
|
||||
|
||||
stage_complete() {
|
||||
local commit
|
||||
commit="$INSTALL_COMMIT"
|
||||
@@ -440,9 +453,8 @@ run_stage() (
|
||||
repository) stage_repository ;;
|
||||
venv) stage_venv ;;
|
||||
python-deps) stage_python_deps ;;
|
||||
node-deps) stage_node_deps ;;
|
||||
path) stage_path ;;
|
||||
config) stage_config ;;
|
||||
products) stage_products ;;
|
||||
setup) stage_setup ;;
|
||||
gateway) stage_gateway ;;
|
||||
desktop) stage_desktop ;;
|
||||
|
||||
@@ -54,9 +54,8 @@ exit /b 0
|
||||
Assert-True ($recorded[1] -eq 'python find --managed-python --no-project 3.13') 'lookup ignores ambient project discovery'
|
||||
Assert-True ((Get-Content -LiteralPath $pythonArgsFile -Raw).Trim() -eq '-m pm.cli install') 'Python launches PM without a uv parent'
|
||||
|
||||
function Get-Uv { throw 'node stage attempted provisioning' }
|
||||
Stage-NodeDeps
|
||||
Write-Host 'PASS: node stage performs no separate install'
|
||||
# The installer owns no node stage: tool and frontend provisioning belongs
|
||||
# to pm, driven by the shared completion tail (install.ps1 "products").
|
||||
} finally {
|
||||
if (Test-Path $testRoot) { Remove-Item -LiteralPath $testRoot -Recurse -Force }
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"""The default `pm install` closure must stage the interpreter boot requires.
|
||||
|
||||
A fresh source install emits boot launchers (scripts/install.sh stage_path,
|
||||
hermes_cli/_launchers.py) that exec the pm STORE interpreter. The `python`
|
||||
A fresh source install emits boot launchers (the source completion's
|
||||
publish_launchers, hermes_cli/_launchers.py) that exec the pm STORE
|
||||
interpreter. The `python`
|
||||
package is marked optional (dev installs use their own venv; sealed bundles
|
||||
adopt a shipped one), so the old default closure — every non-optional
|
||||
lockfile package — skipped it and left `hermes` unbootable (audit C05).
|
||||
|
||||
@@ -27,7 +27,7 @@ def _frame(result):
|
||||
def test_stage_result_matches_the_actual_exit(tmp_path, case):
|
||||
env = _env(tmp_path)
|
||||
stage = {"explicit-failure": "repository", "write-failure": "complete",
|
||||
"unknown": 'unknown"\\\n\x1fstage', "skipped": "setup", "success": "node-deps"}[case]
|
||||
"unknown": 'unknown"\\\n\x1fstage', "skipped": "setup", "success": "config"}[case]
|
||||
env["PROBE_STAGE"] = stage
|
||||
if case == "explicit-failure":
|
||||
repo = tmp_path / "install"
|
||||
@@ -81,7 +81,7 @@ def test_manifest_accepts_the_desktop_home_argument(tmp_path, flag):
|
||||
text=True, encoding="utf-8", timeout=30)
|
||||
assert result.returncode == 0, result.stderr
|
||||
manifest = json.loads(result.stdout)
|
||||
assert any(row["name"] == "path" for row in manifest["stages"])
|
||||
assert any(row["name"] == "products" for row in manifest["stages"])
|
||||
assert not home.exists()
|
||||
env = dict(_env(tmp_path), PROBE_FLAG=flag, PROBE_HOME=home.as_posix())
|
||||
env.pop("HERMES_INSTALL_DIR")
|
||||
|
||||
@@ -36,7 +36,7 @@ def test_powershell_stage_publishes_without_a_checkout_venv(tmp_path, monkeypatc
|
||||
wrapper.write_text('''$ErrorActionPreference = 'Stop'
|
||||
. $env:PROBE_INSTALLER -InstallDir $env:PROBE_REPO -HermesHome $env:PROBE_HOME
|
||||
Initialize-ResolvedPaths
|
||||
# Replace acquisition only; Get-BootstrapPython and Stage-Path stay real.
|
||||
# Replace acquisition only; Get-BootstrapPython and Publish-UserCommand stay real.
|
||||
function Get-Uv { return 'Invoke-FixtureUv' }
|
||||
function Invoke-FixtureUv {
|
||||
$call = $args -join ' '
|
||||
@@ -59,7 +59,7 @@ function Set-LauncherUserPath([string]$binDir) {
|
||||
$script:publishedPath = $binDir
|
||||
Write-Output 'REACHED_PATH_PUBLICATION'
|
||||
}
|
||||
Stage-Path
|
||||
Publish-UserCommand
|
||||
if (-not $script:publishedPath) { throw 'registry-publication seam was bypassed' }
|
||||
exit 0
|
||||
''', encoding='utf-8-sig')
|
||||
|
||||
Reference in New Issue
Block a user