From bde8c4e1083cabe65fc2868ea5e2321301418a96 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Tue, 4 Aug 2026 12:07:29 -0600 Subject: [PATCH] feat(cli): /export and /import slash commands for profile sharing /export [profile] [-o output.tar.gz] bundles a profile into the shareable archive; /import [--name ] adopts one as a new profile (wrapper alias created when safe). Registry-driven, cli_only, so the CLI and TUI both pick them up in autocomplete and help. --- cli.py | 4 ++ hermes_cli/cli_commands_mixin.py | 74 ++++++++++++++++++++++++++++++++ hermes_cli/commands.py | 4 ++ 3 files changed, 82 insertions(+) diff --git a/cli.py b/cli.py index aed3992922b4a..9583383f61d99 100644 --- a/cli.py +++ b/cli.py @@ -10249,6 +10249,10 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin): self._handle_rollback_command(cmd_original) elif canonical == "snapshot": self._handle_snapshot_command(cmd_original) + elif canonical == "export": + self._handle_export_command(cmd_original) + elif canonical == "import": + self._handle_import_command(cmd_original) elif canonical == "stop": self._handle_stop_command() elif canonical == "agents": diff --git a/hermes_cli/cli_commands_mixin.py b/hermes_cli/cli_commands_mixin.py index 5ec16a5fe4df0..9f9071ea94ef0 100644 --- a/hermes_cli/cli_commands_mixin.py +++ b/hermes_cli/cli_commands_mixin.py @@ -363,6 +363,80 @@ class CLICommandsMixin: print(f" Unknown subcommand: {subcmd}") print(" Usage: /snapshot [list|create [label]|restore |prune [N]]") + def _handle_export_command(self, command: str): + """Handle /export — export a profile to a shareable .tar.gz archive. + + Syntax: + /export — export the active profile + /export — export a named profile + /export [profile] -o — choose the output path + """ + from hermes_cli.profiles import export_profile, get_active_profile_name + + parts = command.split()[1:] + output = None + if "-o" in parts: + idx = parts.index("-o") + if idx + 1 >= len(parts): + print(" Usage: /export [profile] [-o output.tar.gz]") + return + output = parts[idx + 1] + parts = parts[:idx] + parts[idx + 2:] + + name = parts[0] if parts else (get_active_profile_name() or "default") + if not output: + output = f"{name}.tar.gz" + + try: + result = export_profile(name, output) + print(f" ✓ Exported '{name}' to {result}") + print(" Share it: the other user runs /import or `hermes profile import `.") + except (ValueError, FileNotFoundError) as e: + print(f" Error: {e}") + + def _handle_import_command(self, command: str): + """Handle /import — import a shared profile archive as a new profile. + + Syntax: + /import [--name ] + """ + from hermes_cli.profiles import ( + check_alias_collision, create_wrapper_script, import_profile, + ) + + parts = command.split()[1:] + name = None + if "--name" in parts: + idx = parts.index("--name") + if idx + 1 >= len(parts): + print(" Usage: /import [--name ]") + return + name = parts[idx + 1] + parts = parts[:idx] + parts[idx + 2:] + + if not parts: + print(" Usage: /import [--name ]") + return + + archive = " ".join(parts) # paths may contain spaces + + try: + profile_dir = import_profile(archive, name=name) + except (ValueError, FileExistsError, FileNotFoundError) as e: + print(f" Error: {e}") + return + + imported = profile_dir.name + print(f" ✓ Imported profile '{imported}' at {profile_dir}") + try: + if not check_alias_collision(imported): + wrapper_path = create_wrapper_script(imported) + if wrapper_path: + print(f" Wrapper created: {wrapper_path}") + except Exception: + pass + print(f" Use it: hermes -p {imported}") + def _handle_stop_command(self): """Handle /stop — kill all running background processes and background (async) delegations. diff --git a/hermes_cli/commands.py b/hermes_cli/commands.py index 34280abc2464f..a803ee925f30e 100644 --- a/hermes_cli/commands.py +++ b/hermes_cli/commands.py @@ -133,6 +133,10 @@ COMMAND_REGISTRY: list[CommandDef] = [ args_hint="[number]"), CommandDef("snapshot", "Create or restore state snapshots of Hermes config/state", "Session", cli_only=True, aliases=("snap",), args_hint="[create|restore |prune]"), + CommandDef("export", "Export a profile (config, skills, theme) to a shareable archive", "Configuration", + cli_only=True, args_hint="[profile] [-o output.tar.gz]"), + CommandDef("import", "Import a shared profile archive as a new profile", "Configuration", + cli_only=True, args_hint=" [--name ]"), CommandDef("stop", "Kill all running background processes", "Session", busy_policy="interrupt_then_dispatch", busy_handler="stop"), CommandDef("approve", "Approve a pending dangerous command", "Session",