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.
131 lines
5.0 KiB
JavaScript
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) }))
|
|
}
|