Files
hermes-agent/scripts/desktop-cli/cli-entrypoints.mjs
ethernet 3d12e86ef1 feat(pm): unified package manager — pm store foundation
Introduce the pm store: a unified, hash-verified package store that
replaces lazy_deps and the old installer's ad-hoc tool downloads.
Store tools are provisioned on PATH (ffmpeg, node/npm via pinned uv),
with a resumable 8-way downloader, verify() returning failure reasons,
and adopt() made EPERM-safe. chromium ships in the payload for every
target. The 3600-line install.sh is replaced by a staged bootstrapper
(heavy deps are pm's job after this); setup-hermes.sh, Dockerfile and
nix pin tables are rewired onto the store. Old install-script tests,
lazy_deps/managed_uv/build_info, and the ps1/bash installer test
batteries are removed with the machinery they tested.

Rebuilt from ethie/pm onto upstream/main (ac6c8028e0) after the
utf-8-sig sweep. 16 hot files (main also churned them) hand-merged:
platform adapters, main.py, electron/main.ts, tui_gateway/server.py,
cua_backend, installer-tests workflow, install.sh (full rewrite),
setup-hermes.sh, plugins doc.
2026-08-31 18:00:48 -04:00

131 lines
5.0 KiB
JavaScript

/**
* cli-entrypoints.mjs — the bundled payload's CLI entrypoint generators.
*
* The bundled payload ships three CLI entrypoints (hermes / hermes-agent /
* hermes-acp — mirrors [project.scripts] in pyproject.toml) staged into
* agent-payload/bin/. They are fully self-relative, so a bundled artifact
* works wherever it lands (and read-only, MSIX included):
*
* win32 — distlib-MINTED launchers (scripts/desktop-cli/mint-launchers.py
* runs on the payload's own store python, whose architecture is by
* construction the target's). The minted exe is a plain PE + shebang +
* zip overlay of scripts/desktop-cli/launcher-wrapper.py. The shebang is
* `#!<launcher_dir>\..\tools\<entry>\python.exe` — the <launcher_dir>
* literal is resolved by the launcher at run time relative to its own
* directory, which is the relocatability mechanism (live-proved).
*
* POSIX — $0-relative bash trampolines generated below. No PE is needed:
* a plain script exec'ing the store python is the entrypoint. They follow
* the $0 symlink chain so a link out on PATH (e.g. ~/.local/bin) resolves
* back into the install.
*
* Pure module: no side effects, importable from tests.
*/
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const HERE = path.dirname(fileURLToPath(import.meta.url))
/** The three CLI entrypoints, mirroring [project.scripts] in pyproject.toml. */
export const CLI_LAUNCHER_SPECS = [
{ name: 'hermes', module: 'hermes_cli.main', func: 'main' },
{ name: 'hermes-agent', module: 'run_agent', func: 'main' },
{ name: 'hermes-acp', module: 'acp_adapter.entry', func: 'main' }
]
/**
* Render scripts/desktop-cli/launcher-wrapper.py for one entry. The
* relative paths are the payload's bin-relative layout facts, forward
* slashes (same convention as the payload manifest — the wrapper splits
* them itself, so one text works cross-platform).
*
* @param {{ module: string, func: string }} spec
* @param {string} relRepo bin-relative repo dir, e.g. ../hermes-agent
* @param {string} relSite bin-relative venv site-packages
* @returns {string} the rendered wrapper source
*/
export function renderWinWrapper(spec, relRepo, relSite) {
const template = fs.readFileSync(path.join(HERE, 'launcher-wrapper.py'), 'utf8')
const substitutions = {
__HERMES_ENTRY_MODULE__: spec.module,
__HERMES_ENTRY_FUNC__: spec.func,
__HERMES_REPO_REL__: relRepo,
__HERMES_SITE_REL__: relSite
}
let text = template
for (const [placeholder, value] of Object.entries(substitutions)) {
if (value.includes('__')) {
throw new Error(`bad substitution for ${placeholder}: ${JSON.stringify(value)}`)
}
text = text.replaceAll(placeholder, value)
}
for (const placeholder of Object.keys(substitutions)) {
if (text.includes(placeholder)) {
throw new Error(`launcher-wrapper.py has an unsubstituted ${placeholder}`)
}
}
return text
}
/**
* One POSIX trampoline. Rel paths are bin-relative with FORWARD slashes
* (the same strings the win32 side bakes); the bash resolves them against
* the trampoline's own directory.
*
* @param {{ name: string, module: string }} spec
* @param {{ relPython: string, relSite: string, relRepo: string }} rels
* @returns {string} executable bash text
*/
export function posixTrampolineScript(spec, rels) {
const join = (rel) => ['"$BIN_DIR"', ...rel.split('/')].join('/')
return `#!/usr/bin/env bash
# Generated by scripts/build-bundled-desktop.mjs — the bundled payload's
# POSIX CLI entrypoint (${spec.name}). Fully $0-relative: follows the
# symlink chain so a link out on PATH (~/.local/bin) resolves back into
# the install, then execs the store python with the payload's own import
# roots. Do not edit the generated copy — change the generator.
set -euo pipefail
self="$0"
while [ -L "$self" ]; do
target="$(readlink "$self")"
case "$target" in
/*) self="$target" ;;
*) self="$(dirname "$self")/$target" ;;
esac
done
BIN_DIR="$(cd -- "$(dirname -- "$self")" && pwd)"
PYTHON=${join(rels.relPython)}
REPO=${join(rels.relRepo)}
SITE=${join(rels.relSite)}
[ -x "$PYTHON" ] || {
echo "${spec.name}: bundled interpreter missing at $PYTHON — the Hermes install is damaged; reinstall from the website" >&2
exit 2
}
# A sealed payload has no working editable install (its pointer names the
# BUILD machine), so its own import roots REPLACE any inherited PYTHONPATH.
# Repo first — its hermes_cli wins over anything stale in site-packages.
unset PYTHONPATH PYTHONHOME
export PYTHONPATH="$REPO:$SITE"
# Keep __pycache__ writes out of the (possibly read-only) payload.
if [ -z "\${PYTHONPYCACHEPREFIX:-}" ] && [ -n "\${HOME:-}" ]; then
export PYTHONPYCACHEPREFIX="$HOME/.cache/hermes-pycache"
fi
exec "$PYTHON" -m ${spec.module} "$@"
`
}
/**
* All three POSIX trampolines.
* @param {{ relPython: string, relSite: string, relRepo: string }} rels
* @returns {{ name: string, text: string }[]}
*/
export function posixTrampolineScripts(rels) {
return CLI_LAUNCHER_SPECS.map((spec) => ({ name: spec.name, text: posixTrampolineScript(spec, rels) }))
}