fix(profiles): preserve skills on dashboard profile creation
This commit is contained in:
@@ -7490,7 +7490,7 @@ def cmd_profile(args):
|
|||||||
if clone_all:
|
if clone_all:
|
||||||
print(f"Full copy from {source_label}.")
|
print(f"Full copy from {source_label}.")
|
||||||
else:
|
else:
|
||||||
print(f"Cloned config, .env, SOUL.md from {source_label}.")
|
print(f"Cloned config, .env, SOUL.md, and skills from {source_label}.")
|
||||||
|
|
||||||
# Auto-clone Honcho config for the new profile (only with --clone/--clone-all)
|
# Auto-clone Honcho config for the new profile (only with --clone/--clone-all)
|
||||||
if clone or clone_all:
|
if clone or clone_all:
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ zero migration needed.
|
|||||||
Usage::
|
Usage::
|
||||||
|
|
||||||
hermes profile create coder # fresh profile + bundled skills
|
hermes profile create coder # fresh profile + bundled skills
|
||||||
hermes profile create coder --clone # also copy config, .env, SOUL.md
|
hermes profile create coder --clone # also copy config, .env, SOUL.md, skills
|
||||||
hermes profile create coder --clone-all # full copy of source profile
|
hermes profile create coder --clone-all # full copy of source profile
|
||||||
coder chat # use via wrapper alias
|
coder chat # use via wrapper alias
|
||||||
hermes -p coder chat # or via flag
|
hermes -p coder chat # or via flag
|
||||||
@@ -388,7 +388,8 @@ def create_profile(
|
|||||||
clone_all:
|
clone_all:
|
||||||
If True, do a full copytree of the source (all state).
|
If True, do a full copytree of the source (all state).
|
||||||
clone_config:
|
clone_config:
|
||||||
If True, copy only config files (config.yaml, .env, SOUL.md).
|
If True, copy config files (config.yaml, .env, SOUL.md), installed
|
||||||
|
skills, and selected profile identity files from the source profile.
|
||||||
no_alias:
|
no_alias:
|
||||||
If True, skip wrapper script creation.
|
If True, skip wrapper script creation.
|
||||||
|
|
||||||
@@ -442,6 +443,14 @@ def create_profile(
|
|||||||
if src.exists():
|
if src.exists():
|
||||||
shutil.copy2(src, profile_dir / filename)
|
shutil.copy2(src, profile_dir / filename)
|
||||||
|
|
||||||
|
# Clone installed skills from the source profile. The dashboard's
|
||||||
|
# "clone from default" flow is expected to preserve both bundled
|
||||||
|
# and user-installed skills so the new profile immediately has the
|
||||||
|
# same agent capabilities as the source profile.
|
||||||
|
source_skills = source_dir / "skills"
|
||||||
|
if source_skills.is_dir():
|
||||||
|
shutil.copytree(source_skills, profile_dir / "skills", dirs_exist_ok=True)
|
||||||
|
|
||||||
# Clone memory and other subdirectory files
|
# Clone memory and other subdirectory files
|
||||||
for relpath in _CLONE_SUBDIR_FILES:
|
for relpath in _CLONE_SUBDIR_FILES:
|
||||||
src = source_dir / relpath
|
src = source_dir / relpath
|
||||||
|
|||||||
@@ -2214,6 +2214,13 @@ async def create_profile_endpoint(body: ProfileCreate):
|
|||||||
clone_from="default" if body.clone_from_default else None,
|
clone_from="default" if body.clone_from_default else None,
|
||||||
clone_config=body.clone_from_default,
|
clone_config=body.clone_from_default,
|
||||||
)
|
)
|
||||||
|
# Match the CLI's profile-create flow: fresh named profiles get the
|
||||||
|
# bundled skills installed. When cloning from default, create_profile()
|
||||||
|
# has already copied the source profile's skills, including any
|
||||||
|
# user-installed skills.
|
||||||
|
if not body.clone_from_default:
|
||||||
|
profiles_mod.seed_profile_skills(path, quiet=True)
|
||||||
|
|
||||||
# Match the CLI's profile-create flow: named profiles should get a
|
# Match the CLI's profile-create flow: named profiles should get a
|
||||||
# wrapper in ~/.local/bin when the alias is safe to create.
|
# wrapper in ~/.local/bin when the alias is safe to create.
|
||||||
collision = profiles_mod.check_alias_collision(body.name)
|
collision = profiles_mod.check_alias_collision(body.name)
|
||||||
|
|||||||
@@ -149,6 +149,23 @@ class TestCreateProfile:
|
|||||||
assert (profile_dir / ".env").read_text() == "KEY=val"
|
assert (profile_dir / ".env").read_text() == "KEY=val"
|
||||||
assert (profile_dir / "SOUL.md").read_text() == "Be helpful."
|
assert (profile_dir / "SOUL.md").read_text() == "Be helpful."
|
||||||
|
|
||||||
|
def test_clone_config_copies_source_skills(self, profile_env):
|
||||||
|
tmp_path = profile_env
|
||||||
|
default_home = tmp_path / ".hermes"
|
||||||
|
skill_dir = default_home / "skills" / "custom" / "installed-skill"
|
||||||
|
skill_dir.mkdir(parents=True)
|
||||||
|
(skill_dir / "SKILL.md").write_text("---\nname: installed-skill\n---\n")
|
||||||
|
|
||||||
|
profile_dir = create_profile("coder", clone_config=True, no_alias=True)
|
||||||
|
|
||||||
|
assert (
|
||||||
|
profile_dir
|
||||||
|
/ "skills"
|
||||||
|
/ "custom"
|
||||||
|
/ "installed-skill"
|
||||||
|
/ "SKILL.md"
|
||||||
|
).read_text() == "---\nname: installed-skill\n---\n"
|
||||||
|
|
||||||
def test_clone_all_copies_entire_tree(self, profile_env):
|
def test_clone_all_copies_entire_tree(self, profile_env):
|
||||||
tmp_path = profile_env
|
tmp_path = profile_env
|
||||||
default_home = tmp_path / ".hermes"
|
default_home = tmp_path / ".hermes"
|
||||||
|
|||||||
@@ -684,6 +684,51 @@ class TestNewEndpoints:
|
|||||||
assert wrapper_path.exists()
|
assert wrapper_path.exists()
|
||||||
assert wrapper_path.read_text() == '#!/bin/sh\nexec hermes -p writer "$@"\n'
|
assert wrapper_path.read_text() == '#!/bin/sh\nexec hermes -p writer "$@"\n'
|
||||||
|
|
||||||
|
def test_profiles_create_with_clone_from_default_copies_default_skills(self, monkeypatch):
|
||||||
|
from hermes_constants import get_hermes_home
|
||||||
|
import hermes_cli.profiles as profiles_mod
|
||||||
|
|
||||||
|
monkeypatch.setattr(profiles_mod, "create_wrapper_script", lambda name: None)
|
||||||
|
default_skill = get_hermes_home() / "skills" / "custom" / "new-skill"
|
||||||
|
default_skill.mkdir(parents=True)
|
||||||
|
(default_skill / "SKILL.md").write_text("---\nname: new-skill\n---\n", encoding="utf-8")
|
||||||
|
|
||||||
|
resp = self.client.post(
|
||||||
|
"/api/profiles",
|
||||||
|
json={"name": "cloned", "clone_from_default": True},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert resp.status_code == 200
|
||||||
|
cloned_skill = get_hermes_home() / "profiles" / "cloned" / "skills" / "custom" / "new-skill" / "SKILL.md"
|
||||||
|
assert cloned_skill.exists()
|
||||||
|
profiles = {p["name"]: p for p in self.client.get("/api/profiles").json()["profiles"]}
|
||||||
|
assert profiles["cloned"]["skill_count"] == 1
|
||||||
|
|
||||||
|
def test_profiles_create_without_clone_seeds_bundled_skills(self, monkeypatch):
|
||||||
|
from hermes_constants import get_hermes_home
|
||||||
|
import hermes_cli.profiles as profiles_mod
|
||||||
|
|
||||||
|
monkeypatch.setattr(profiles_mod, "create_wrapper_script", lambda name: None)
|
||||||
|
|
||||||
|
def fake_seed(profile_dir, quiet=False):
|
||||||
|
skill_dir = profile_dir / "skills" / "software-development" / "plan"
|
||||||
|
skill_dir.mkdir(parents=True)
|
||||||
|
(skill_dir / "SKILL.md").write_text("---\nname: plan\n---\n", encoding="utf-8")
|
||||||
|
return {"copied": ["plan"]}
|
||||||
|
|
||||||
|
monkeypatch.setattr(profiles_mod, "seed_profile_skills", fake_seed)
|
||||||
|
|
||||||
|
resp = self.client.post(
|
||||||
|
"/api/profiles",
|
||||||
|
json={"name": "fresh", "clone_from_default": False},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert resp.status_code == 200
|
||||||
|
seeded_skill = get_hermes_home() / "profiles" / "fresh" / "skills" / "software-development" / "plan" / "SKILL.md"
|
||||||
|
assert seeded_skill.exists()
|
||||||
|
profiles = {p["name"]: p for p in self.client.get("/api/profiles").json()["profiles"]}
|
||||||
|
assert profiles["fresh"]["skill_count"] == 1
|
||||||
|
|
||||||
def test_profile_open_terminal_uses_macos_terminal(self, monkeypatch):
|
def test_profile_open_terminal_uses_macos_terminal(self, monkeypatch):
|
||||||
from hermes_constants import get_hermes_home
|
from hermes_constants import get_hermes_home
|
||||||
import hermes_cli.web_server as web_server
|
import hermes_cli.web_server as web_server
|
||||||
|
|||||||
Reference in New Issue
Block a user