From f2b25bebe741f260771bd90ba55edbcbff35dbc9 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Wed, 2 Sep 2026 21:33:24 -0700 Subject: [PATCH] =?UTF-8?q?refactor(hermes=5Fcli):=20setup=20cluster=20?= =?UTF-8?q?=E2=80=94=20print=5Fheader(gap=3D),=20yes/no=20answer=20table,?= =?UTF-8?q?=20bws=20=5Fstep=20helper,=20sync=20row=20table?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- hermes_cli/secrets_cli.py | 48 ++++++++++++++++------------------- hermes_cli/setup.py | 18 ++++++------- hermes_cli/setup_migration.py | 6 ++--- hermes_cli/setup_platforms.py | 6 +---- hermes_cli/setup_quick.py | 27 +++++++------------- 5 files changed, 42 insertions(+), 63 deletions(-) diff --git a/hermes_cli/secrets_cli.py b/hermes_cli/secrets_cli.py index 98c662926c..139e2e2fcc 100644 --- a/hermes_cli/secrets_cli.py +++ b/hermes_cli/secrets_cli.py @@ -97,10 +97,14 @@ def register_cli(parent_parser: argparse.ArgumentParser) -> None: # --------------------------------------------------------------------------- +def _step(console: Console, n: int, title: str) -> None: + console.print() + console.print(f"[bold]Step {n}[/bold] {title}") + + def _setup_binary(bw, console: Console) -> Optional[Path]: """Step 1: locate or download bws; None (after printing) on failure.""" - console.print() - console.print("[bold]Step 1[/bold] Install the bws CLI") + _step(console, 1, "Install the bws CLI") try: binary = bw.find_bws(install_if_missing=False) if binary is None: @@ -126,8 +130,7 @@ def _missing_noninteractive_flags(args: argparse.Namespace) -> list[str]: def _setup_token(args: argparse.Namespace, console: Console, token_env: str) -> Optional[str]: """Step 2: take the token from ``--access-token`` or a masked prompt and persist it.""" - console.print() - console.print("[bold]Step 2[/bold] Provide your access token") + _step(console, 2, "Provide your access token") token = (args.access_token or "").strip() or masked_secret_prompt(f" Paste access token ({token_env}): ").strip() if not token: console.print(" [red]Empty token, aborting.[/red]") @@ -142,8 +145,7 @@ def _setup_token(args: argparse.Namespace, console: Console, token_env: str) -> def _setup_project(binary: Path, token: str, console: Console, server_url: str) -> Optional[str]: """Step 4: list projects and let the user pick one; None (after printing) when none usable.""" - console.print() - console.print("[bold]Step 4[/bold] Pick a project") + _step(console, 4, "Pick a project") projects = _list_projects(binary, token, console, server_url=server_url) if projects is None: return None @@ -195,8 +197,7 @@ def cmd_setup(args: argparse.Namespace) -> int: if token is None: return 1 - console.print() - console.print("[bold]Step 3[/bold] Pick a Bitwarden region") + _step(console, 3, "Pick a Bitwarden region") server_url = _resolve_server_url(args, secrets_cfg, console) if server_url is None: return 1 @@ -210,8 +211,7 @@ def cmd_setup(args: argparse.Namespace) -> int: if project_id is None: return 1 - console.print() - console.print(f"[bold]Step {4 if project_given else 5}[/bold] Test fetch") + _step(console, 4 if project_given else 5, "Test fetch") try: secrets, warnings = bw.fetch_bitwarden_secrets( access_token=token, project_id=project_id, binary=binary, use_cache=False, server_url=server_url, @@ -228,13 +228,10 @@ def cmd_setup(args: argparse.Namespace) -> int: for w in warnings: console.print(f" [yellow]warning:[/yellow] {w}") - secrets_cfg["enabled"] = True - secrets_cfg["project_id"] = project_id - secrets_cfg["server_url"] = server_url - secrets_cfg.setdefault("access_token_env", token_env) - secrets_cfg.setdefault("cache_ttl_seconds", 300) - secrets_cfg.setdefault("override_existing", True) - secrets_cfg.setdefault("auto_install", True) + secrets_cfg.update(enabled=True, project_id=project_id, server_url=server_url) + for key, default in (("access_token_env", token_env), ("cache_ttl_seconds", 300), + ("override_existing", True), ("auto_install", True)): + secrets_cfg.setdefault(key, default) save_config(cfg) console.print() @@ -387,19 +384,18 @@ def cmd_sync(args: argparse.Namespace) -> int: rows = [] applied = 0 for key in sorted(secrets): - if key == token_env: - rows.append((key, "[dim]skip (bootstrap token)[/dim]")) - continue already = bool(os.environ.get(key)) - if already and not override: - rows.append((key, "[dim]skip (already set)[/dim]")) - continue - if args.apply: + if key == token_env: + action = "[dim]skip (bootstrap token)[/dim]" + elif already and not override: + action = "[dim]skip (already set)[/dim]" + elif args.apply: os.environ[key] = secrets[key] applied += 1 - rows.append((key, "[green]exported[/green]" + (" (overrode)" if already else ""))) + action = "[green]exported[/green]" + (" (overrode)" if already else "") else: - rows.append((key, "[green]would export[/green]" + (" (overrides)" if already else ""))) + action = "[green]would export[/green]" + (" (overrides)" if already else "") + rows.append((key, action)) print_table(console, (("Name", {"style": "cyan"}), "Action"), rows, warnings) diff --git a/hermes_cli/setup.py b/hermes_cli/setup.py index a468e8cc48..b1fc3fb53e 100644 --- a/hermes_cli/setup.py +++ b/hermes_cli/setup.py @@ -38,8 +38,10 @@ _DOCS_BASE = "https://hermes-agent.nousresearch.com/docs" _BRACKETED_PASTE_PATTERN = re.compile(r"\x1b\[\s*200~|\x1b\[\s*201~") -def print_header(title: str): - """Print a section header.""" +def print_header(title: str, *, gap: bool = False): + """Print a section header (``gap`` adds an extra blank line before it).""" + if gap: + print() print() print(color(f"◆ {title}", Colors.CYAN, Colors.BOLD)) @@ -302,12 +304,9 @@ def prompt_yes_no(question: str, default: bool = True) -> bool: # proceeds unattended instead of failing the whole command. print() return default - if not value: - return default - if value in {"y", "yes"}: - return True - if value in {"n", "no"}: - return False + answer = {"": default, "y": True, "yes": True, "n": False, "no": False}.get(value) + if answer is not None: + return answer print_error("Please enter 'y' or 'n'") @@ -781,8 +780,7 @@ def _run_setup_wizard_impl(args): if quick_requested: _run_setup_steps([("Quick Setup", lambda: _run_quick_setup(config, hermes_home))]) return - print() - print_header("Reconfigure") + print_header("Reconfigure", gap=True) print_success("You already have Hermes configured.") _info("Running the full wizard — each prompt shows your current value.", "Press Enter to keep it, or type a new value to change it.", "", diff --git a/hermes_cli/setup_migration.py b/hermes_cli/setup_migration.py index 8a2ca0563c..252dd50090 100644 --- a/hermes_cli/setup_migration.py +++ b/hermes_cli/setup_migration.py @@ -266,8 +266,7 @@ def _offer_openclaw_migration(hermes_home: Path) -> bool: if not openclaw_dir.is_dir() or not _OPENCLAW_SCRIPT.exists(): return False - print() - print_header("OpenClaw Installation Detected") + print_header("OpenClaw Installation Detected", gap=True) _info(f"Found OpenClaw data at {openclaw_dir}", "Hermes can preview what would be imported before making any changes.", None) if not prompt_yes_no("Would you like to see what can be imported?", default=True): @@ -299,8 +298,7 @@ def _offer_openclaw_migration(hermes_home: Path) -> bool: if preview_count == 0: _info(None, "Nothing to import from OpenClaw.") return False - print() - print_header(f"Migration Preview — {preview_count} item(s) would be imported") + print_header(f"Migration Preview — {preview_count} item(s) would be imported", gap=True) _info("No changes have been made yet. Review the list below:", None) _print_migration_preview(preview_report) diff --git a/hermes_cli/setup_platforms.py b/hermes_cli/setup_platforms.py index 9903b382d6..8b678e6b7e 100644 --- a/hermes_cli/setup_platforms.py +++ b/hermes_cli/setup_platforms.py @@ -158,11 +158,7 @@ def _setup_telegram(): print_success(f"Detected your Telegram user ID: {detected_id}") if prompt_yes_no("Allow this Telegram account to use the bot?", True): extra = prompt("Additional allowed user IDs (comma-separated, optional)") - ids = [detected_id] - for uid in extra.replace(" ", "").split(","): - if uid and uid not in ids: - ids.append(uid) - allowed_users = ",".join(ids) + allowed_users = ",".join(dict.fromkeys([detected_id, *filter(None, extra.replace(" ", "").split(","))])) if allowed_users is None: allowed_users = prompt("Allowed user IDs (comma-separated, leave empty for open access)") diff --git a/hermes_cli/setup_quick.py b/hermes_cli/setup_quick.py index ec15d2d4b6..76536208f7 100644 --- a/hermes_cli/setup_quick.py +++ b/hermes_cli/setup_quick.py @@ -85,8 +85,7 @@ def _run_first_time_quick_setup(config: dict, hermes_home, is_existing: bool): setup_terminal_backend, ) # Step 1: Nous Portal — OAuth login + model selection (provider set to "nous" by the save). - print() - print_header("Nous Portal") + print_header("Nous Portal", gap=True) _info("One subscription, 300+ models, plus the Tool Gateway:", " web search, image generation, TTS, browser automation.", "Sign up: https://portal.nousresearch.com/manage-subscription", None) @@ -217,8 +216,7 @@ def _run_blank_slate_setup(config: dict, hermes_home, is_existing: bool): print_header, print_info, _print_setup_summary, print_success, prompt_choice, save_config, setup_model_provider, setup_terminal_backend, ) - print() - print_header("Blank Slate Setup") + print_header("Blank Slate Setup", gap=True) _info("Everything starts OFF. First we force-enable only what's required", "to run an agent, then you choose whether to stop there or walk", "through enabling more — opting in to exactly what you want.", "", @@ -247,8 +245,7 @@ def _run_blank_slate_setup(config: dict, hermes_home, is_existing: bool): print_info(" Compression, memory, checkpoints, smart routing: off") # The fork: stop here, or walk through enabling things - print() - print_header("How far do you want to go?") + print_header("How far do you want to go?", gap=True) path = prompt_choice("Your minimal agent is ready. What next?", [ "Start with everything disabled — finish now (most minimal)", "Walk through all configurations — opt in to tools, skills, plugins, MCP", @@ -276,8 +273,7 @@ def _blank_slate_walkthrough(config: dict, hermes_home): save_config, setup_gateway, ) # Bundled skills — default to NONE, offer to seed all - print() - print_header("Bundled Skills") + print_header("Bundled Skills", gap=True) print_info("Blank Slate ships with NO bundled skills by default.") seed_skills = prompt_yes_no("Seed the full bundled skill catalog? (No = start with zero skills)", default=False) @@ -298,8 +294,7 @@ def _blank_slate_walkthrough(config: dict, hermes_home): ) # Walk through enabling additional tools - print() - print_header("Tools") + print_header("Tools", gap=True) _info("Pick exactly which additional toolsets to turn on.", "(file and terminal are already on; leave the rest off if you want", " the most minimal agent.)") if prompt_yes_no("Open the tool selector to enable more tools?", default=False): @@ -322,8 +317,7 @@ def _blank_slate_walkthrough(config: dict, hermes_home): "Add servers with `hermes mcp add --url ... | --command ...`.", "No MCP servers configured. Add later with `hermes mcp add`."), ): - print() - print_header(header) + print_header(header, gap=True) print_info(yes_msg if prompt_yes_no(question, default=False) else no_msg) # Optional messaging gateway @@ -348,8 +342,7 @@ def _run_quick_setup(config: dict, hermes_home): _prompt_and_save_env_var, _prompt_api_key, prompt_checklist, save_config, ) from hermes_cli.config import (get_missing_env_vars, get_missing_config_fields, check_config_version) - print() - print_header("Quick Setup — Missing Items Only") + print_header("Quick Setup — Missing Items Only", gap=True) # Check what's missing missing_required = [v for v in get_missing_env_vars(required_only=False) if v.get("is_required")] @@ -380,8 +373,7 @@ def _run_quick_setup(config: dict, hermes_home): missing_messaging = [v for v in missing_optional if v.get("category") == "messaging" and not v.get("advanced")] if missing_tools: # checklist, then the API-key screen for each pick - print() - print_header("Tool API Keys") + print_header("Tool API Keys", gap=True) labels = [] for var in missing_tools: tools = var.get("tools", []) @@ -391,8 +383,7 @@ def _run_quick_setup(config: dict, hermes_home): _prompt_api_key(missing_tools[idx]) if missing_messaging: # checklist, then prompt for each selected platform's vars - print() - print_header("Messaging Platforms") + print_header("Messaging Platforms", gap=True) _info("Connect Hermes to messaging apps to chat from anywhere.", "You can configure these later with 'hermes setup gateway'.") # Group by platform in first-seen order; vars matching no platform are dropped.