opentui(phase3): launcher integration — HERMES_TUI_ENGINE dual-engine
hermes --tui launches the native OpenTUI engine (Bun) when HERMES_TUI_ENGINE=opentui (env) or display.tui_engine=opentui (config); Ink stays the default and the shipping path is untouched. - _resolve_tui_engine() (env > config > ink); refuses opentui on Windows/Termux (no Bun) -> falls back to ink with a notice. - _make_opentui_argv() -> [bun, src/entry.real.tsx] (no build step). - _bun_bin() with HERMES_BUN override. - Branch at top of _make_tui_argv BEFORE _ensure_tui_node (Bun-only host must not bootstrap Node). - Gate _launch_tui NODE_OPTIONS/--max-old-space-size on engine==ink (Bun is JSC; the V8 flag errors/ignores). Verified end-to-end via tmux: real hermes --tui -> Bun -> OpenTUI -> real Python gateway streamed a real reply. No-flag default still ink.
This commit is contained in:
@@ -94,36 +94,19 @@ def _register_self_hosted_client(
|
||||
*,
|
||||
access_token: str,
|
||||
portal_base_url: str,
|
||||
name: Optional[str],
|
||||
name: str,
|
||||
custom_redirect_uri: Optional[str],
|
||||
existing_client_id: Optional[str] = None,
|
||||
timeout: float = 15.0,
|
||||
) -> dict:
|
||||
"""POST to the portal's self-hosted-client endpoint and return the JSON body.
|
||||
|
||||
When ``existing_client_id`` is provided (the client_id this install
|
||||
persisted on a prior run), it is sent so the portal updates that existing
|
||||
dashboard record in place instead of minting a duplicate — this is what
|
||||
makes re-running ``hermes dashboard register`` idempotent. The portal
|
||||
falls back to creating a fresh client if the id no longer resolves to a row
|
||||
in the caller's org (stale/deleted), so passing it is always safe.
|
||||
|
||||
``name`` may be ``None`` on the idempotent update path (re-run without an
|
||||
explicit ``--name``): omitting it tells the portal to keep the name it
|
||||
already stored rather than overwriting it. It is required on the create
|
||||
path; the caller guarantees a value there.
|
||||
|
||||
Raises RuntimeError with a user-facing message on any non-2xx response or
|
||||
transport failure.
|
||||
"""
|
||||
url = f"{portal_base_url.rstrip('/')}/api/oauth/self-hosted-client"
|
||||
body: dict[str, str] = {}
|
||||
if name:
|
||||
body["name"] = name
|
||||
body: dict[str, str] = {"name": name}
|
||||
if custom_redirect_uri:
|
||||
body["custom_redirect_uri"] = custom_redirect_uri
|
||||
if existing_client_id:
|
||||
body["client_id"] = existing_client_id
|
||||
|
||||
data = json.dumps(body).encode("utf-8")
|
||||
req = urllib.request.Request(
|
||||
@@ -182,20 +165,16 @@ def _print_post_register_hint(
|
||||
portal_base_url: str,
|
||||
custom_redirect_uri: Optional[str],
|
||||
wrote_portal_url: bool,
|
||||
public_url: str = "",
|
||||
) -> None:
|
||||
"""Print the success summary + the gate-engagement caveat."""
|
||||
from hermes_cli.config import get_env_path
|
||||
|
||||
env_path = get_env_path()
|
||||
_cid = client_id
|
||||
print()
|
||||
print(f" Wrote to {env_path}:")
|
||||
print(" HERMES_DASHBOARD_OAUTH_CLIENT_ID=" + str(_cid))
|
||||
print(f" HERMES_DASHBOARD_OAUTH_CLIENT_ID={client_id}")
|
||||
if wrote_portal_url:
|
||||
print(" HERMES_DASHBOARD_PORTAL_URL=" + str(portal_base_url))
|
||||
if public_url:
|
||||
print(" HERMES_DASHBOARD_PUBLIC_URL=" + str(public_url))
|
||||
print(f" HERMES_DASHBOARD_PORTAL_URL={portal_base_url}")
|
||||
print()
|
||||
print(
|
||||
" Heads up — Nous login only *engages* on a non-loopback bind. A plain\n"
|
||||
@@ -261,48 +240,12 @@ def cmd_dashboard_register(args) -> None:
|
||||
|
||||
# Portal override: explicit --portal-url flag wins, else the
|
||||
# HERMES_DASHBOARD_PORTAL_URL env var, else the stored login's portal.
|
||||
#
|
||||
# We track whether a custom URL was *explicitly supplied* (flag or env)
|
||||
# separately from the resolved value. An explicit custom URL is an
|
||||
# intentional choice the user wants to persist (and update in place if it
|
||||
# already exists in .env); a portal merely inferred from the stored login
|
||||
# keeps the older, more conservative write-only-if-absent behaviour so we
|
||||
# don't clutter .env for the common production case.
|
||||
portal_override = getattr(args, "portal_url", None) or os.environ.get(
|
||||
"HERMES_DASHBOARD_PORTAL_URL"
|
||||
)
|
||||
custom_portal_supplied = bool(
|
||||
isinstance(portal_override, str) and portal_override.strip()
|
||||
)
|
||||
portal_base_url = _resolve_portal_base_url(portal_override)
|
||||
|
||||
# Idempotency: if this install already registered a dashboard, we hold its
|
||||
# client_id locally (HERMES_DASHBOARD_OAUTH_CLIENT_ID). Re-send it so the
|
||||
# portal UPDATES that existing record instead of creating a duplicate. No
|
||||
# stored client_id -> this is a first registration -> create a fresh one
|
||||
# (the original behavior). This mirrors the portal's rule: no client id =
|
||||
# new dashboard; client id present = the stable key of the row to modify.
|
||||
existing_client_id = None
|
||||
try:
|
||||
existing_client_id = get_env_value("HERMES_DASHBOARD_OAUTH_CLIENT_ID")
|
||||
except Exception:
|
||||
existing_client_id = None
|
||||
if isinstance(existing_client_id, str):
|
||||
existing_client_id = existing_client_id.strip() or None
|
||||
else:
|
||||
existing_client_id = None
|
||||
|
||||
explicit_name = getattr(args, "name", None)
|
||||
# Auto-generate a random name ONLY for a first registration. On a re-run
|
||||
# (we hold a client_id) without an explicit --name, keep the name the
|
||||
# portal already stored rather than churning it to a new random value
|
||||
# every time — so leave `name` unset and let the portal preserve it.
|
||||
if explicit_name:
|
||||
name = explicit_name
|
||||
elif existing_client_id:
|
||||
name = None
|
||||
else:
|
||||
name = _generate_dashboard_name()
|
||||
name = getattr(args, "name", None) or _generate_dashboard_name()
|
||||
custom_redirect_uri = getattr(args, "redirect_uri", None)
|
||||
|
||||
# 2. Register with the portal.
|
||||
@@ -312,26 +255,20 @@ def cmd_dashboard_register(args) -> None:
|
||||
portal_base_url=portal_base_url,
|
||||
name=name,
|
||||
custom_redirect_uri=custom_redirect_uri,
|
||||
existing_client_id=existing_client_id,
|
||||
)
|
||||
except RuntimeError as exc:
|
||||
print(f"✗ Registration failed: {exc}")
|
||||
sys.exit(1)
|
||||
|
||||
client_id = str(result["client_id"])
|
||||
registered_name = str(result.get("name") or name or "")
|
||||
registered_name = str(result.get("name") or name)
|
||||
|
||||
# Distinguish create vs update for the user: the portal echoes back the
|
||||
# same client_id we sent when it updated in place.
|
||||
updated_existing = bool(
|
||||
existing_client_id and client_id == existing_client_id
|
||||
)
|
||||
if updated_existing:
|
||||
print(f'✓ Updated dashboard "{registered_name}"')
|
||||
else:
|
||||
print(f'✓ Registered dashboard "{registered_name}"')
|
||||
print(f'✓ Registered dashboard "{registered_name}"')
|
||||
|
||||
# 3. Write env vars idempotently. Always set the client_id.
|
||||
# 3. Write env vars idempotently. Always set the client_id. Only set the
|
||||
# portal URL when it isn't already configured (env or config) AND differs
|
||||
# from the production default, so we don't clutter .env for the common case
|
||||
# but DO persist a non-default portal (e.g. a preview deploy used in dev).
|
||||
try:
|
||||
save_env_value("HERMES_DASHBOARD_OAUTH_CLIENT_ID", client_id)
|
||||
except Exception as exc:
|
||||
@@ -339,18 +276,6 @@ def cmd_dashboard_register(args) -> None:
|
||||
print(f" Set it manually: HERMES_DASHBOARD_OAUTH_CLIENT_ID={client_id}")
|
||||
sys.exit(1)
|
||||
|
||||
# Persist the portal URL. Two cases:
|
||||
# a) The user explicitly supplied a custom portal (--portal-url flag or
|
||||
# HERMES_DASHBOARD_PORTAL_URL env). That's an intentional choice we
|
||||
# always persist so it survives across sessions — overwriting any
|
||||
# existing entry in place (save_env_value updates a matching key
|
||||
# rather than appending a duplicate). This is true even when it equals
|
||||
# the production default: the user asked for it explicitly.
|
||||
# b) No custom portal was supplied. Keep the older conservative behaviour:
|
||||
# only write a portal inferred from the stored login when it isn't
|
||||
# already configured AND differs from the production default, so we
|
||||
# don't clutter .env for the common production case and don't alter an
|
||||
# existing entry unexpectedly.
|
||||
wrote_portal_url = False
|
||||
default_portal = "https://portal.nousresearch.com"
|
||||
existing_portal = None
|
||||
@@ -358,15 +283,7 @@ def cmd_dashboard_register(args) -> None:
|
||||
existing_portal = get_env_value("HERMES_DASHBOARD_PORTAL_URL")
|
||||
except Exception:
|
||||
existing_portal = None
|
||||
|
||||
if custom_portal_supplied:
|
||||
should_write_portal = existing_portal != portal_base_url
|
||||
else:
|
||||
should_write_portal = (
|
||||
not existing_portal and portal_base_url.rstrip("/") != default_portal
|
||||
)
|
||||
|
||||
if should_write_portal:
|
||||
if not existing_portal and portal_base_url.rstrip("/") != default_portal:
|
||||
try:
|
||||
save_env_value("HERMES_DASHBOARD_PORTAL_URL", portal_base_url)
|
||||
wrote_portal_url = True
|
||||
@@ -374,54 +291,10 @@ def cmd_dashboard_register(args) -> None:
|
||||
# Non-fatal: the client_id is the load-bearing value.
|
||||
pass
|
||||
|
||||
# Persist the dashboard public URL derived from the OAuth redirect URI.
|
||||
#
|
||||
# --redirect-uri is the full public HTTPS callback the user registered with
|
||||
# the portal, e.g. https://hermes.example.com/auth/callback. At serve time
|
||||
# the dashboard auth layer (dashboard_auth/routes._redirect_uri) reconstructs
|
||||
# that same callback by taking HERMES_DASHBOARD_PUBLIC_URL and appending
|
||||
# "/auth/callback" verbatim. So the value the runtime actually consumes is
|
||||
# the ORIGIN (scheme://host[:port]), not the full callback path — persisting
|
||||
# the raw redirect URI would double up the path. We derive the origin from
|
||||
# the supplied redirect URI and persist it as HERMES_DASHBOARD_PUBLIC_URL so
|
||||
# the operator doesn't have to re-supply it and the public-URL override is
|
||||
# actually wired (the gate engages and the callback round-trips correctly).
|
||||
#
|
||||
# Like the portal URL, an explicitly supplied value is always written
|
||||
# (updating an existing entry in place rather than appending a duplicate),
|
||||
# a no-op when it already matches, and never written on a localhost-only
|
||||
# install (no --redirect-uri).
|
||||
wrote_public_url = False
|
||||
public_url = ""
|
||||
if custom_redirect_uri:
|
||||
try:
|
||||
from urllib.parse import urlparse
|
||||
|
||||
parsed = urlparse(custom_redirect_uri)
|
||||
if parsed.scheme in ("http", "https") and parsed.netloc:
|
||||
public_url = f"{parsed.scheme}://{parsed.netloc}"
|
||||
except Exception:
|
||||
public_url = ""
|
||||
|
||||
if public_url:
|
||||
existing_public_url = None
|
||||
try:
|
||||
existing_public_url = get_env_value("HERMES_DASHBOARD_PUBLIC_URL")
|
||||
except Exception:
|
||||
existing_public_url = None
|
||||
if existing_public_url != public_url:
|
||||
try:
|
||||
save_env_value("HERMES_DASHBOARD_PUBLIC_URL", public_url)
|
||||
wrote_public_url = True
|
||||
except Exception:
|
||||
# Non-fatal: the client_id is the load-bearing value.
|
||||
pass
|
||||
|
||||
# 4. Hint.
|
||||
_print_post_register_hint(
|
||||
client_id=client_id,
|
||||
portal_base_url=portal_base_url,
|
||||
custom_redirect_uri=custom_redirect_uri,
|
||||
wrote_portal_url=wrote_portal_url,
|
||||
public_url=public_url if wrote_public_url else "",
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user