Skip to content

feat: Windows bootstrap via install.ps1 -Ensure/-PostInstall + dep_ensure Windows awareness#26620

Closed
alt-glitch wants to merge 13 commits into
mainfrom
sid/windows-bootstrap
Closed

feat: Windows bootstrap via install.ps1 -Ensure/-PostInstall + dep_ensure Windows awareness#26620
alt-glitch wants to merge 13 commits into
mainfrom
sid/windows-bootstrap

Conversation

@alt-glitch

@alt-glitch alt-glitch commented May 15, 2026

Copy link
Copy Markdown
Collaborator

Summary

Windows pip install hermes-agent now bootstraps non-Python deps (Node.js, browser engine, ripgrep, ffmpeg) via install.ps1 -Ensure / -PostInstall, mirroring the existing install.sh --ensure / --postinstall flow on Linux/macOS. E2E verified on Windows 11 (PS 5.1) with a real PyPI wheel install.

Follow-up to #26593 (initial PyPI packaging) and #26234 (ACP browser bootstrap).


What changed (by file)

scripts/install.ps1

  • New modes: -Ensure <dep> and -PostInstall params added to the top-level param() block. Entry point dispatches: -EnsureInvoke-EnsureMode, -PostInstallInvoke-PostInstallMode, else → Main (full git-clone install).
  • Invoke-EnsureMode — accepts comma-separated dep names (node, browser, ripgrep, ffmpeg). Each dep has its own switch case. Unknown deps warn and skip. Commas, spaces, duplicates handled gracefully.
  • Invoke-PostInstallMode — runs the full bootstrap: Test-Node → Install-SystemPackages → Install-AgentBrowser → Find-SystemBrowser → hermes setup.
  • Install-AgentBrowser — rewritten to use npm install -g --prefix "$HermesHome\node" (canonical approach from PR feat(acp): hermes acp --setup-browser bootstraps browser tools for registry installs #26234). Previously used a local package.json + npm install in $HermesHome\agent-browser\, which put the binary at a path nothing in Hermes searches. Now the binary lands at $HermesHome\node\agent-browser.cmd, which browser_tool.py::_browser_candidate_path_dirs() already discovers.
  • Browser engine: Uses agent-browser install (v0.26+) instead of npx playwright install chromium. The npx approach fails with npm -g --prefix installs because there is no local package.json with playwright in the dependency tree — npx emits a "install your project deps first" warning and exits without downloading. agent-browser install downloads Chrome directly into ~/.agent-browser/browsers/.
  • Resolve-NpmCmd / Resolve-NpxCmd — extracted from 3 duplicate inline blocks. Handles the PS 5.1 execution-policy gotcha where npm.ps1 is found before npm.cmd.
  • Find-SystemBrowser / Write-BrowserEnv — ported from PR feat(acp): hermes acp --setup-browser bootstraps browser tools for registry installs #26234 ACP bootstrap. Detects system Chrome/Edge/Chromium at standard Windows paths, writes AGENT_BROWSER_EXECUTABLE_PATH to .env.
  • PS 5.1 compat: All em-dashes replaced with ASCII --. UTF-8 BOM added (PS 5.1 reads .ps1 from disk with ANSI codepage — box-drawing chars, arrows, emoji break without BOM). No PS7-only syntax (?., ternary, &&).
  • $ErrorActionPreference guards around agent-browser install — it writes download progress to stderr, which PS 5.1 wraps as ErrorRecord and throws on with Stop. Same pattern already used for uv in the script.
  • Install method stamp: Main writes "git" to $HermesHome\.install_method. Invoke-PostInstallMode does not stamp (Python side handles it).

hermes_cli/dep_ensure.py

  • _IS_WINDOWS — module-level platform.system() == "Windows" flag.
  • _find_install_script — returns (path, shell) tuple. On Windows: looks for install.ps1 only. On POSIX: looks for install.sh only. No cross-platform fallback (previously POSIX fell back to .ps1, giving confusing "PowerShell not found" errors on Linux).
  • ensure_dependency — when shell == "powershell", builds [powershell, -ExecutionPolicy, Bypass, -File, <script>, -Ensure, <dep>, -HermesHome, <path>] command. Falls back to bash path for POSIX.
  • _has_hermes_agent_browser — checks two paths: canonical $HERMES_HOME/node/agent-browser.cmd (Windows) or $HERMES_HOME/node/bin/agent-browser (POSIX) from npm -g --prefix, plus legacy $HERMES_HOME/node_modules/.bin/agent-browser from install.sh.
  • _has_system_browser — Windows-specific binary names: chrome, msedge, chromium (vs Linux google-chrome, google-chrome-stable, etc.).
  • Unknown dep bugfix: ensure_dependency("foobar") previously returned True because check = _DEP_CHECKS.get(dep)None, script runs (exits 0 with warning), then if check: is falsy → fell through to return True. Now returns False — can't claim success without a verification function.

hermes_cli/config.py

  • detect_install_method — new resolution order: (1) .install_method stamp file, (2) HERMES_MANAGED env / .managed marker, (3) is_container()"docker", (4) .git dir → "git", (5) fallback "pip".
  • stamp_install_method — writes method string to ~/.hermes/.install_method. Silently swallows OSError (best-effort).
  • recommended_update_command_for_method("docker") — returns "docker pull nousresearch/hermes-agent:latest".
  • _NIX_UPDATE_MSG — replaces hardcoded "sudo nixos-rebuild switch" with generic Nix flake guidance.

hermes_cli/main.py

  • cmd_postinstall — calls stamp_install_method("pip") before bootstrapping deps.

tools/browser_tool.py

  • Post-install recheck — after ensure_dependency("browser") triggers installation, the recheck now also searches $HERMES_HOME/node/ and $HERMES_HOME/node/bin/ (the canonical npm -g --prefix paths), not just node_modules/.bin/.

pyproject.toml

  • package-datahermes_cli/scripts/install.ps1 added alongside install.sh.

.github/workflows/upload_to_pypi.yml

  • Bundle step — copies install.ps1 into hermes_cli/scripts/ before wheel build.

Dockerfile

  • Install method stampRUN echo "docker" > /opt/data/.install_method.

scripts/install.sh

  • Install method stampecho "git" > "$HERMES_HOME/.install_method" at end of main().

website/docs/getting-started/quickstart.md + updating.md

  • pip installuv pip install in code examples.

Bugs found and fixed during review

Bug Severity Root cause Fix
agent-browser installed to undiscoverable path HIGH Install-AgentBrowser used local package.json + npm install → binary at $HermesHome\agent-browser\node_modules\.bin\ which nothing searches Rewrote to npm -g --prefix $HermesHome\node (canonical, matches browser_tool.py discovery)
npx playwright install chromium always fails HIGH No local package.json with playwright dep → npx shows warning banner and exits without downloading Replaced with agent-browser install (v0.26+ downloads Chrome directly)
PS 5.1 can't parse install.ps1 from disk MEDIUM Unicode box-drawing/arrow/emoji chars + no BOM → PS 5.1 reads with ANSI codepage Added UTF-8 BOM
PS 5.1 throws on agent-browser install stderr MEDIUM Download progress goes to stderr → PS 5.1 wraps as ErrorRecord → throws with $ErrorActionPreference = Stop Temporarily relax to Continue (same pattern as uv)
ensure_dependency("unknown") returns True MEDIUM check = None → script runs (exits 0) → if check: falsy → return True Changed to return False
POSIX fallback to install.ps1 LOW _find_install_script fell back to .ps1 on Linux → confusing "PowerShell not found" error Removed cross-platform fallback

UX pathways

Path A: pip install hermes-agenthermes postinstall (interactive)

  1. cmd_postinstall() stamps "pip" into ~/.hermes/.install_method
  2. For each dep (node, browser, ripgrep, ffmpeg):
    • ensure_dependency(dep) checks if already present
    • If missing + TTY: prompts "Install now? [Y/n]"
    • On Windows: invokes powershell -File install.ps1 -Ensure <dep> -HermesHome <path>
    • On POSIX: invokes bash install.sh --ensure <dep>
  3. If no provider configured → runs hermes setup

Path B: pip install hermes-agent → browser tool triggers lazy install

  1. User sends a prompt that needs browser → browser_tool._find_agent_browser() runs
  2. agent-browser not on PATH → calls ensure_dependency("browser")
  3. _DEP_CHECKS["browser"] checks: shutil.which_has_system_browser()_has_hermes_agent_browser()
  4. All False → install.ps1 runs → npm -g --prefix installs agent-browser → agent-browser install downloads Chrome
  5. Post-install recheck: searches $HERMES_HOME/node/ → finds agent-browser.cmd → browser tools register

Path C: install.ps1 -PostInstall (non-interactive, called from hermes postinstall)

  1. Write-BannerTest-NodeInstall-SystemPackages (ripgrep + ffmpeg via winget/choco/scoop)
  2. If node available: Install-AgentBrowserFind-SystemBrowserWrite-BrowserEnv
  3. If hermes on PATH: runs hermes setup
  4. If not: prints manual instructions

Path D: install.ps1 -Ensure node,browser (targeted deps)

  1. Splits on comma, trims whitespace, filters empty
  2. For each dep: switch on name → calls the appropriate installer function
  3. Unknown deps: warns "Unknown dep '<name>' -- skipping", continues to next
  4. Exit 0 always (warning-only for unknown deps)

Path E: detect_install_method() resolution chain

  1. ~/.hermes/.install_method stamp file → highest priority (written by installers)
  2. HERMES_MANAGED env / .managed marker → NixOS / Homebrew
  3. is_container() (dockerenv / containerenv / cgroup) → "docker"
  4. .git directory in project root → "git"
  5. Fallback → "pip"

Test results

Unit tests (23/23 pass)

  • 16 dep_ensure tests (13 original + 3 new: Windows/POSIX node-prefix detection, no cross-platform fallback)
  • 7 pip_install_detection tests (5 original + 2 new: stamp priority, docker detection)

E2E on Windows 11 (PS 5.1.26100, Python 3.11.15, Node 24.15, uv 0.11.11)

# Test Result
1 _IS_WINDOWS flag
2 detect_install_method() = pip
3 install.ps1 bundled in wheel
4 install.sh bundled in wheel
5 _find_install_script → ps1/powershell
6 ensure_dependency('node') (already present)
7 stamp_install_method + detect
8 recommended_update_command('pip')
9 recommended_update_command('docker')
10 install.ps1 -Ensure node (live PS 5.1)
11 _has_hermes_agent_browser after -Ensure browser
12 hermes --version from wheel
13 hermes doctor
14 hermes --tui (exits cleanly: "no TTY")
15 _find_bundled_tui() → entry.js
16 web_dist assets complete (27 files)
17 tui_dist/entry.js = 2.9MB real bundle
18 All key module imports clean
19 node --check entry.js syntax valid
20 install.ps1 -Ensure browser full pipeline

Full browser pipeline: npm -g --prefix → agent-browser 0.26.0 → agent-browser install → Chrome 148.0.7778.167 (182MB) → _has_hermes_agent_browser() = True.

Edge case coverage (48 tests via 3 parallel subagents)

dep_ensure.py edge cases (17 tests): empty stamp falls through ✅, nonexistent HERMES_HOME ✅, unreadable stamp file ✅, unknown dep returns False ✅, stamp priority over .git ✅, no cross-platform leakage ✅, browser short-circuit when present ✅

install.ps1 edge cases (21 tests): unknown dep warns+skips ✅, multiple deps ✅, spaces in path ✅, duplicate deps idempotent ✅, mixed known/unknown ✅, injection attempt safe ✅, -PostInstall without hermes on PATH ✅, no package managers graceful ✅, -Ensure browser with no node (warns) ✅

TUI/web/pip (10 tests): hermes --version ✅, hermes doctor ✅, hermes --tui graceful no-TTY exit ✅, web_dist 27 files + asset integrity ✅, tui_dist 2.9MB bundle ✅, all module imports ✅, node syntax check ✅

@github-actions

github-actions Bot commented May 15, 2026

Copy link
Copy Markdown
Contributor

🔎 Lint report: sid/windows-bootstrap vs origin/main

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 8322 on HEAD, 8322 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 4349 pre-existing issues carried over.

Diagnostics are surfaced as warnings — this check never fails the build.

@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/cli CLI entry point, hermes_cli/, setup wizard labels May 15, 2026
@alt-glitch alt-glitch marked this pull request as ready for review May 15, 2026 23:03
@alt-glitch alt-glitch requested a review from a team May 15, 2026 23:03
@alt-glitch alt-glitch marked this pull request as draft May 15, 2026 23:31
@alt-glitch

Copy link
Copy Markdown
Collaborator Author

Review

PR #26620@alt-glitch

What it does: Adds Windows-aware bootstrap to dep_ensure.py (PS1 discovery, PowerShell invocation) and gives install.ps1 the same -Ensure/-PostInstall modes that install.sh already has. Also adds install-method stamping (~/.hermes/.install_method) and Docker container detection.

What it solves: Windows pip users had no bootstrap path — ensure_dependency() only knew about bash/install.sh. hermes postinstall and runtime lazy-install were POSIX-only.

How: 14 files, +434/-58. dep_ensure.py returns (path, shell) tuple, prefers PS1 on Windows with fallback. install.ps1 gains Resolve-NpmCmd/Resolve-NpxCmd DRY helpers, Install-AgentBrowser, Invoke-EnsureMode, Invoke-PostInstallMode. config.py adds stamp_install_method() + detect_install_method() stamp-file priority. 13 new tests.

Problems with it: None critical.

  1. Minor: Install-AgentBrowser package.json pins agent-browser to "*" (latest) rather than ^0.26.0 — fixed in follow-up refactor(bootstrap): consolidate ACP browser bootstrap into install.{sh,ps1} #26668.
  2. Minor: Write-BrowserEnv silently skips if .env absent — fixed in follow-up refactor(bootstrap): consolidate ACP browser bootstrap into install.{sh,ps1} #26668.
  3. Pre-existing: docs changes (pipuv pip) are tangential but harmless.

Verified against current main: All 5 feature areas (dep_ensure Windows, install.ps1 -Ensure/-PostInstall, stamp_install_method, Docker detection, PS1 wheel bundling) are genuinely new — none exist on origin/main. DRY refactor of Resolve-NpmCmd/NpxCmd matches the old inline code exactly. stamp_install_method degrades gracefully on write failure. Test mocks patch correct module paths.

Recommendation: Merge. Follow-up #26668 builds on this to consolidate the ACP bootstrap scripts.

@alt-glitch alt-glitch marked this pull request as ready for review May 16, 2026 00:30
Adds -Ensure <dep> and -PostInstall switch params so that dep_ensure.py
can lazily bootstrap non-Python deps on Windows (mirrors install.sh
--ensure / --postinstall). Also stamps .install_method="git" in Main.
- Add Resolve-NpmCmd and Resolve-NpxCmd helpers to deduplicate the
  npm.cmd sibling and npx resolution logic repeated across three call sites
- Replace inline resolution in Install-NodeDeps, Invoke-EnsureMode, and
  Invoke-PostInstallMode with the new helpers
- Fix Invoke-EnsureMode browser branch: collapse double Push-Location into
  a single try/finally wrapping both npm install and playwright install
- Fix Invoke-PostInstallMode: add agent-browser npm install before running
  npx playwright install chromium so Playwright is resolvable via npx
… pip docs

- Add docker container detection (_is_docker) and .install_method stamp file
- detect_install_method() checks stamp first, then heuristics
- stamp_install_method() called by postinstall, install.sh, Dockerfile
- Nix update message is guidance-style, not prescriptive command
- Docs default to `uv pip install` everywhere
- Remove hermes_cli/scripts/ from .gitignore
…ract Install-AgentBrowser, fix TOCTOU+encoding
@alt-glitch alt-glitch force-pushed the sid/windows-bootstrap branch from a6c3479 to a367b15 Compare May 16, 2026 05:51
Bug 1 (HIGH): Install-AgentBrowser used local package.json + npm install
in $HermesHome/agent-browser/, putting the binary at a path that nothing
searches. Rewrote to use npm install -g --prefix $HermesHome/node, matching
the canonical ACP bootstrap approach (PR #26234). browser_tool.py's
_browser_candidate_path_dirs() already searches $HERMES_HOME/node/bin.

Bug 2 (LOW): _find_install_script fell back from POSIX to install.ps1,
giving confusing 'PowerShell not found' errors on Linux. Removed the
cross-platform fallback — POSIX only looks for .sh, Windows only for .ps1.

Bug 3 (LOW): Out-File -Encoding utf8 wrote BOM on PS 5.1 — eliminated
entirely since npm -g --prefix doesn't need a package.json.

Also updated:
- _has_hermes_agent_browser() checks canonical node/ prefix path
- browser_tool.py post-install recheck includes node/ prefix
- 3 new tests for node-prefix detection + no-cross-platform-fallback
When install.ps1 is bundled in the wheel and executed from disk via
'powershell -File', PS 5.1 reads it with the system ANSI codepage
(not UTF-8). The box-drawing characters, arrows, and emoji in string
literals cause parse failures. The BOM tells PS 5.1 to use UTF-8.

This was previously attempted but reverted in favor of replacing
em-dashes with ASCII. The em-dash replacement was correct but
insufficient — other Unicode chars (box drawing ┌─┐, arrows →,
checkmarks ✓✗, emoji 🚀📁⚡) still need the BOM.
npx playwright install chromium fails with npm -g --prefix installs because
there's no local package.json with playwright in the dependency tree — npx
emits a 'install your project dependencies first' warning and exits without
downloading Chromium.

agent-browser v0.26+ has its own 'agent-browser install' command that
downloads Chrome directly into ~/.agent-browser/browsers/ (182MB), bypassing
the playwright npm dependency chain entirely. This is the correct approach
for the pip/wheel install path where there is no project-level package.json.
ensure_dependency() with an unrecognized dep name (not in _DEP_CHECKS)
would run the install script (which warns and exits 0), then hit the
final 'return True' — claiming the dep is available when we have no
check function to verify it. Return False instead so callers don't
proceed on a false positive.
@alt-glitch alt-glitch closed this May 18, 2026
alt-glitch added a commit that referenced this pull request May 18, 2026
…sh,ps1}

Eliminates 687 lines of duplicated browser bootstrap code by routing all
bootstrap paths through dep_ensure.py -> install.{sh,ps1} --ensure.

install.sh:
- New ensure_browser() with agent-browser + camofox install, system browser
  detection + .env writing, per-distro Playwright deps (apt/arch/fedora/suse)
- macOS app-bundle paths added to find_system_browser()
- configure_browser_env_from_system_browser() creates .env if missing
- postinstall_mode() uses ensure_browser() instead of inline duplication

install.ps1:
- New -Ensure and -PostInstall params (coexists with stage protocol)
- New functions: Resolve-NpmCmd, Resolve-NpxCmd, Find-SystemBrowser,
  Write-BrowserEnv, Install-AgentBrowser (with -SkipPlaywright)
- Invoke-EnsureMode dispatches node/browser/ripgrep/ffmpeg
- Invoke-PostInstallMode runs full post-pip-install bootstrap
- ErrorActionPreference guards on all native command calls
- ASCII-only convention maintained (no Unicode)
- Mutual exclusion guard: -Ensure + -Stage = error

dep_ensure.py:
- Windows-aware: _IS_WINDOWS, _find_install_script returns (path, shell) tuple
- PowerShell invocation with powershell/pwsh guard + -ExecutionPolicy Bypass
- _has_hermes_agent_browser() checks platform-correct paths
- _has_system_browser() checks Windows browser names (chrome, msedge, chromium)
- env_extra parameter for forwarding install flags

config.py:
- stamp_install_method() writes ~/.hermes/.install_method
- detect_install_method() checks stamp first (before heuristics)

acp_adapter:
- _run_setup_browser() rewritten: ensure_dependency('node') + ensure_dependency('browser')
- acp_adapter/bootstrap/ deleted (399 + 288 lines)

Rebased onto main -- drops #26620 dependency (upstream stage protocol merged
via #27224). Closes follow-up from #26593.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cli CLI entry point, hermes_cli/, setup wizard P3 Low — cosmetic, nice to have type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant