Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions hermes_cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2911,6 +2911,79 @@ def _sync_bundled_skills_quietly() -> None:
pass


def _migrate_profile_config(profile) -> None:
"""Run non-interactive config migration for a single profile.

Called from ``cmd_update`` to catch up named profiles whose config.yaml
is still at an older version after the active profile was migrated.
Only version-bump-only migrations are applied silently; if new required
settings are needed, a warning is printed telling the user to run
``hermes config migrate`` with that profile active.
"""
from hermes_constants import (
reset_hermes_home_override,
set_hermes_home_override,
)
token = set_hermes_home_override(profile.path)
try:
from hermes_cli.config import (
get_missing_env_vars,
get_missing_config_fields,
check_config_version,
migrate_config,
)
current_ver, latest_ver = check_config_version()
if current_ver < latest_ver:
missing_env = get_missing_env_vars(required_only=True)
missing_config = get_missing_config_fields()
if not (missing_env or missing_config):
migrate_config(interactive=False, quiet=True)
else:
print(
f"⚠️ Profile '{profile.name}' config v{current_ver} is "
f"outdated (current: v{latest_ver}). Run "
f"`hermes --profile {profile.name} config migrate` to update.",
file=sys.stderr,
)
finally:
reset_hermes_home_override(token)


def _migrate_active_profile_on_startup() -> None:
"""Auto-migrate the active profile's config before serve/dashboard startup.

``hermes update`` only migrates the active profile's config; when the
desktop app spawns ``hermes serve --profile <name>``, that profile's
config may still be at an older version, causing agent init failures.
Apply the same non-interactive migration for version-bump-only bumps;
if new required settings are needed, warn on stderr.

Never raises — startup must not be blocked by a migration hiccup.
"""
try:
from hermes_cli.config import (
get_missing_env_vars,
get_missing_config_fields,
check_config_version,
migrate_config,
)
current_ver, latest_ver = check_config_version()
if current_ver < latest_ver:
missing_env = get_missing_env_vars(required_only=True)
missing_config = get_missing_config_fields()
has_new_options = bool(missing_env or missing_config)
if not has_new_options:
migrate_config(interactive=False, quiet=True)
else:
print(
f"⚠️ Config v{current_ver} is outdated (current: v{latest_ver}). "
f"Run `hermes config migrate` to update.",
file=sys.stderr,
)
except Exception:
pass # non-fatal — let start_server surface real config errors


def _resolve_use_tui(args) -> bool:
"""Decide whether to launch the TUI for a chat/bare invocation.

Expand Down Expand Up @@ -11676,6 +11749,9 @@ def cmd_dashboard(args):
logger.debug("terminal config → env bridge failed for dashboard/serve",
exc_info=True)

# Auto-migrate config if the version is stale.
_migrate_active_profile_on_startup()

if _headless_backend:
# Don't build the SPA, and tell mount_spa() (read at web_server import
# below) to disable it even if a stray dist exists. Set it first.
Expand Down
30 changes: 30 additions & 0 deletions hermes_cli/update_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -5735,6 +5735,33 @@ def _rebuild_desktop_after_update(
return True


def _migrate_all_profiles() -> None:
"""Run config migration for every named profile (best-effort).

``hermes update`` runs with the default profile active, so named
profiles keep their stale config version and break when the desktop
app spawns ``hermes serve --profile <name>``. This loop catches them
up. Per-profile failures are surfaced as visible stderr warnings.
"""
try:
from hermes_cli.main import _migrate_profile_config
from hermes_cli.profiles import list_profiles

all_profiles = list_profiles()
for p in all_profiles:
try:
_migrate_profile_config(p)
except Exception as pe:
print(
f" ⚠️ Config migration for profile '{p.name}' "
f"failed: {pe}. Run `hermes --profile {p.name} "
f"config migrate` manually.",
file=sys.stderr,
)
except Exception:
pass # profiles module not available or no profiles


def _cmd_update_impl(args, gateway_mode: bool):
"""Body of ``cmd_update`` — kept separate so the wrapper can always
restore stdio even on ``sys.exit``."""
Expand Down Expand Up @@ -7265,6 +7292,9 @@ def _print_items(items, label, key, fallback_key=None):
except Exception as exc:
logger.debug("Sibling config migration failed: %s", exc)

# Migrate config for ALL profiles, not just the active one.
_migrate_all_profiles()

# Safety net: config-version migrations have been observed to leave
# cron/jobs.json valid-but-empty, silently dropping every scheduled
# job (issue #34600). The desktop scheduler can also overwrite with
Expand Down
Loading
Loading