From ae11a310582ac936cbbffc516891cc2bd9fdd458 Mon Sep 17 00:00:00 2001 From: vincez-hms-coder <265218533+vincez-hms-coder@users.noreply.github.com> Date: Mon, 27 Apr 2026 18:35:16 -0400 Subject: [PATCH] feat(profiles): add profile setup command endpoint and wrapper creation --- hermes_cli/web_server.py | 16 +++++++ tests/hermes_cli/test_web_server.py | 65 +++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+) diff --git a/hermes_cli/web_server.py b/hermes_cli/web_server.py index 1fe36a5666..6f3fadc873 100644 --- a/hermes_cli/web_server.py +++ b/hermes_cli/web_server.py @@ -2189,6 +2189,12 @@ def _resolve_profile_dir(name: str) -> Path: return profiles_mod.get_profile_dir(name) +def _profile_setup_command(name: str) -> str: + """Return the shell command used to configure a profile in the CLI.""" + _resolve_profile_dir(name) + return "hermes setup" if name == "default" else f"{name} setup" + + @app.get("/api/profiles") async def list_profiles_endpoint(): from hermes_cli import profiles as profiles_mod @@ -2208,6 +2214,11 @@ async def create_profile_endpoint(body: ProfileCreate): clone_from="default" if body.clone_from_default else None, clone_config=body.clone_from_default, ) + # Match the CLI's profile-create flow: named profiles should get a + # wrapper in ~/.local/bin when the alias is safe to create. + collision = profiles_mod.check_alias_collision(body.name) + if not collision: + profiles_mod.create_wrapper_script(body.name) except (ValueError, FileExistsError, FileNotFoundError) as e: raise HTTPException(status_code=400, detail=str(e)) except Exception as e: @@ -2216,6 +2227,11 @@ async def create_profile_endpoint(body: ProfileCreate): return {"ok": True, "name": body.name, "path": str(path)} +@app.get("/api/profiles/{name}/setup-command") +async def get_profile_setup_command(name: str): + return {"command": _profile_setup_command(name)} + + @app.patch("/api/profiles/{name}") async def rename_profile_endpoint(name: str, body: ProfileRename): from hermes_cli import profiles as profiles_mod diff --git a/tests/hermes_cli/test_web_server.py b/tests/hermes_cli/test_web_server.py index 09ed088036..b6537f2cc8 100644 --- a/tests/hermes_cli/test_web_server.py +++ b/tests/hermes_cli/test_web_server.py @@ -651,6 +651,71 @@ class TestNewEndpoints: names = [p["name"] for p in self.client.get("/api/profiles").json()["profiles"]] assert "test-prof-2" not in names + def test_profile_setup_command_uses_named_profile_wrapper(self): + from hermes_constants import get_hermes_home + + (get_hermes_home() / "profiles" / "coder").mkdir(parents=True) + + resp = self.client.get("/api/profiles/coder/setup-command") + + assert resp.status_code == 200 + assert resp.json()["command"] == "coder setup" + + def test_profile_setup_command_uses_hermes_for_default_profile(self): + from hermes_constants import get_hermes_home + + get_hermes_home().mkdir(parents=True, exist_ok=True) + + resp = self.client.get("/api/profiles/default/setup-command") + + assert resp.status_code == 200 + assert resp.json()["command"] == "hermes setup" + + def test_profiles_create_creates_wrapper_alias_when_safe(self): + from pathlib import Path + + resp = self.client.post( + "/api/profiles", + json={"name": "writer", "clone_from_default": False}, + ) + + assert resp.status_code == 200 + wrapper_path = Path.home() / ".local" / "bin" / "writer" + assert wrapper_path.exists() + assert wrapper_path.read_text() == '#!/bin/sh\nexec hermes -p writer "$@"\n' + + def test_profile_open_terminal_uses_macos_terminal(self, monkeypatch): + from hermes_constants import get_hermes_home + import hermes_cli.web_server as web_server + + (get_hermes_home() / "profiles" / "coder").mkdir(parents=True) + calls = [] + monkeypatch.setattr(web_server.sys, "platform", "darwin") + monkeypatch.setattr(web_server.subprocess, "Popen", lambda args, **kwargs: calls.append(args)) + + resp = self.client.post("/api/profiles/coder/open-terminal") + + assert resp.status_code == 200 + assert calls + assert calls[0][0] == "osascript" + assert "coder setup" in " ".join(calls[0]) + + def test_profile_open_terminal_uses_windows_cmd(self, monkeypatch): + from hermes_constants import get_hermes_home + import hermes_cli.web_server as web_server + + (get_hermes_home() / "profiles" / "coder").mkdir(parents=True) + calls = [] + monkeypatch.setattr(web_server.sys, "platform", "win32") + monkeypatch.setattr(web_server.subprocess, "Popen", lambda args, **kwargs: calls.append(args)) + + resp = self.client.post("/api/profiles/coder/open-terminal") + + assert resp.status_code == 200 + assert calls + assert calls[0][:4] == ["cmd.exe", "/c", "start", ""] + assert calls[0][-1] == "coder setup" + def test_profiles_create_rejects_invalid_name(self): resp = self.client.post("/api/profiles", json={"name": "Has Spaces"}) assert resp.status_code == 400