diff --git a/hermes_cli/main.py b/hermes_cli/main.py index df379e7918b..41e8f9d503b 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -10525,6 +10525,44 @@ def main(): ) fallback_parser.set_defaults(func=cmd_fallback) + # ========================================================================= + # migrate command + # ========================================================================= + from hermes_cli.migrate import cmd_migrate, cmd_migrate_xai + + migrate_parser = subparsers.add_parser( + "migrate", + help="Migrate configuration for retired models or deprecated settings", + description=( + "Diagnose and (optionally) rewrite the active config.yaml to " + "replace references to retired models or deprecated settings." + ), + ) + migrate_subparsers = migrate_parser.add_subparsers(dest="migrate_type") + + migrate_xai = migrate_subparsers.add_parser( + "xai", + help="Migrate xAI models scheduled for retirement on May 15, 2026", + description=( + "Scan config.yaml for references to xAI models retiring on " + "May 15, 2026 and, with --apply, rewrite them in-place to the " + "official replacements per the xAI migration guide. The original " + "config.yaml is backed up before any rewrite." + ), + ) + migrate_xai.add_argument( + "--apply", + action="store_true", + help="Rewrite config.yaml in-place (default: dry-run, no writes)", + ) + migrate_xai.add_argument( + "--no-backup", + action="store_true", + help="Skip the timestamped backup of config.yaml when applying", + ) + migrate_xai.set_defaults(func=cmd_migrate_xai) + migrate_parser.set_defaults(func=cmd_migrate) + # ========================================================================= # gateway command # ========================================================================= diff --git a/hermes_cli/migrate.py b/hermes_cli/migrate.py new file mode 100644 index 00000000000..0c947f632e1 --- /dev/null +++ b/hermes_cli/migrate.py @@ -0,0 +1,115 @@ +"""CLI handlers for ``hermes migrate ...``. + +Currently exposes only ``hermes migrate xai`` — diagnoses and (with --apply) +rewrites references to xAI models retired on May 15, 2026. +""" +from __future__ import annotations + +import sys +from pathlib import Path +from typing import Any + +from hermes_cli.colors import Colors, color +from hermes_cli.config import load_config + + +def cmd_migrate(args: Any) -> int: + """Dispatcher for ``hermes migrate ``.""" + sub = getattr(args, "migrate_type", None) + if sub == "xai": + return cmd_migrate_xai(args) + + print("usage: hermes migrate xai [--apply] [--no-backup]", file=sys.stderr) + return 2 + + +def cmd_migrate_xai(args: Any) -> int: + """Run xAI May-15 model migration in dry-run or apply mode.""" + from hermes_cli.xai_retirement import ( + MIGRATION_GUIDE_URL, + RETIREMENT_DATE, + apply_migration, + find_retired_xai_refs, + format_issue, + ) + + apply = bool(getattr(args, "apply", False)) + no_backup = bool(getattr(args, "no_backup", False)) + + config = load_config() + issues = find_retired_xai_refs(config) + + print() + print(color( + f"◆ xAI Model Retirement Migration ({RETIREMENT_DATE})", + Colors.CYAN, Colors.BOLD, + )) + print() + + if not issues: + print(f" {color('✓', Colors.GREEN)} No retired xAI models in config — nothing to migrate.") + return 0 + + print(f" Found {len(issues)} retired xAI model reference(s):") + print() + for issue in issues: + print(f" {color('⚠', Colors.YELLOW)} {format_issue(issue)}") + print() + print(f" {color('→', Colors.CYAN)} Migration guide: {MIGRATION_GUIDE_URL}") + print() + + config_path = _resolve_config_path() + + if not apply: + print(color("Dry-run mode — no changes written.", Colors.DIM)) + print(color( + "Re-run with `hermes migrate xai --apply` to rewrite " + f"{config_path} in-place (backup created automatically).", + Colors.DIM, + )) + return 0 + + if not config_path or not config_path.exists(): + print( + f" {color('✗', Colors.RED)} Could not locate config.yaml " + f"(looked at: {config_path})", + file=sys.stderr, + ) + return 1 + + try: + result = apply_migration( + config_path=config_path, + issues=issues, + backup=not no_backup, + ) + except Exception as exc: + print( + f" {color('✗', Colors.RED)} Migration failed: {exc}", + file=sys.stderr, + ) + return 1 + + if not result.config_changed: + print(f" {color('⚠', Colors.YELLOW)} No changes written.") + return 0 + + if result.backup_path is not None: + print(f" {color('✓', Colors.GREEN)} Backup: {result.backup_path}") + print( + f" {color('✓', Colors.GREEN)} Updated {len(result.issues_resolved)} " + f"slot(s) in {result.file_path}" + ) + print() + print(color( + "Run `hermes doctor` to confirm no retired xAI models remain.", + Colors.DIM, + )) + return 0 + + +def _resolve_config_path() -> Path: + """Best-effort: locate the active config.yaml on disk.""" + from hermes_cli.config import get_hermes_home + + return get_hermes_home() / "config.yaml"