Merge remote-tracking branch 'origin/main' into hermes/hermes-6b48295e
This commit is contained in:
+83
-2
@@ -1623,7 +1623,11 @@ def _make_tui_argv(tui_dir: Path, tui_dev: bool) -> tuple[list[str], Path]:
|
||||
npm_cwd = _workspace_root(tui_dir)
|
||||
# --workspace ui-tui avoids resolving apps/desktop (Electron + node-pty).
|
||||
# See #38772.
|
||||
npm_workspace_args: tuple[str, ...] = ("--workspace", "ui-tui")
|
||||
# When ui-tui/ has its own package-lock.json (e.g. curl install),
|
||||
# _workspace_root() returns tui_dir itself. Passing --workspace in
|
||||
# that case fails because npm cannot find a workspace named "ui-tui"
|
||||
# inside ui-tui/. See #42973.
|
||||
npm_workspace_args: tuple[str, ...] = () if npm_cwd == tui_dir else ("--workspace", "ui-tui")
|
||||
if termux_startup:
|
||||
npm_cwd, npm_workspace_args = _termux_workspace_install_context(
|
||||
tui_dir,
|
||||
@@ -4661,7 +4665,9 @@ def _build_web_ui(web_dir: Path, *, fatal: bool = False) -> bool:
|
||||
# graph (including apps/desktop with its Electron + node-pty deps) is never
|
||||
# resolved here. Without --workspace the root package.json's apps/* glob
|
||||
# would pull in desktop on every web build. See #38772.
|
||||
npm_workspace_args: tuple[str, ...] = ("--workspace", "web")
|
||||
# When web/ has its own package-lock.json, _workspace_root() returns
|
||||
# web_dir itself and --workspace would fail. See #42973.
|
||||
npm_workspace_args: tuple[str, ...] = () if npm_cwd == web_dir else ("--workspace", "web")
|
||||
if _is_termux_startup_environment():
|
||||
npm_cwd, npm_workspace_args = _termux_workspace_install_context(web_dir)
|
||||
r1 = _run_npm_install_deterministic(
|
||||
@@ -10234,6 +10240,21 @@ def _report_dashboard_status() -> int:
|
||||
return len(pids)
|
||||
|
||||
|
||||
def _dashboard_listening(host: str, port: int) -> bool:
|
||||
"""True when something is accepting TCP connections at host:port.
|
||||
|
||||
Any listener counts — even a 401 response proves a dashboard is up.
|
||||
Used by the unified profile-launch routing to decide attach-vs-start.
|
||||
"""
|
||||
import socket
|
||||
|
||||
try:
|
||||
with socket.create_connection((host or "127.0.0.1", port), timeout=1.5):
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
def cmd_dashboard(args):
|
||||
"""Start the web UI server, or (with --stop/--status) manage running ones."""
|
||||
# --status: report running dashboards and exit, no deps needed.
|
||||
@@ -10254,6 +10275,65 @@ def cmd_dashboard(args):
|
||||
remaining = _find_stale_dashboard_pids()
|
||||
sys.exit(1 if remaining else 0)
|
||||
|
||||
# ── Unified profile launch routing ────────────────────────────────
|
||||
# The dashboard is a MACHINE management surface: it can read/write any
|
||||
# profile via the per-request ?profile= scoping. Running one dashboard
|
||||
# per profile just fragments that (port collisions, N processes, and a
|
||||
# "which dashboard am I on?" guessing game). So when a NAMED profile
|
||||
# launches the dashboard (`worker dashboard` → HERMES_HOME points into
|
||||
# profiles/), default to the machine dashboard:
|
||||
# - already running → open the browser at ?profile=<name> and exit
|
||||
# - not running → re-exec as the machine dashboard (pinned to the
|
||||
# default profile so _apply_profile_override can't re-route through
|
||||
# the sticky active_profile file) with the launching profile
|
||||
# preselected in the UI's switcher.
|
||||
# `--isolated` opts out and preserves the old per-profile behavior.
|
||||
try:
|
||||
from hermes_cli.profiles import get_active_profile_name
|
||||
_launch_profile = get_active_profile_name()
|
||||
except Exception:
|
||||
_launch_profile = "default"
|
||||
|
||||
if (
|
||||
_launch_profile not in ("default", "custom")
|
||||
and not getattr(args, "isolated", False)
|
||||
and not getattr(args, "open_profile", "")
|
||||
):
|
||||
url = f"http://{args.host or '127.0.0.1'}:{args.port}/?profile={_launch_profile}"
|
||||
if _dashboard_listening(args.host, args.port):
|
||||
print(f"Machine dashboard already running on port {args.port}.")
|
||||
print(f" Managing profile '{_launch_profile}': {url}")
|
||||
if not args.no_open:
|
||||
try:
|
||||
import webbrowser
|
||||
webbrowser.open(url)
|
||||
except Exception:
|
||||
pass
|
||||
sys.exit(0)
|
||||
|
||||
print(
|
||||
f"Routing to the machine dashboard (profile '{_launch_profile}' "
|
||||
f"preselected). Use --isolated for a dedicated per-profile server."
|
||||
)
|
||||
reexec_argv = [
|
||||
sys.executable, "-m", "hermes_cli.main",
|
||||
"-p", "default",
|
||||
"dashboard",
|
||||
"--port", str(args.port),
|
||||
"--host", args.host,
|
||||
"--open-profile", _launch_profile,
|
||||
]
|
||||
if args.no_open:
|
||||
reexec_argv.append("--no-open")
|
||||
if getattr(args, "insecure", False):
|
||||
reexec_argv.append("--insecure")
|
||||
if getattr(args, "skip_build", False):
|
||||
reexec_argv.append("--skip-build")
|
||||
env = os.environ.copy()
|
||||
# Drop the profile HERMES_HOME so the child binds the machine root.
|
||||
env.pop("HERMES_HOME", None)
|
||||
os.execvpe(sys.executable, reexec_argv, env)
|
||||
|
||||
# Attach gui.log early so dashboard startup/build failures are captured in
|
||||
# the same logs directory as every other Hermes surface.
|
||||
try:
|
||||
@@ -10327,6 +10407,7 @@ def cmd_dashboard(args):
|
||||
port=args.port,
|
||||
open_browser=not args.no_open,
|
||||
allow_public=getattr(args, "insecure", False),
|
||||
initial_profile=getattr(args, "open_profile", "") or "",
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user