refactor(hermes_cli): setup cluster — print_header(gap=), yes/no answer table, bws _step helper, sync row table

This commit is contained in:
Teknium
2026-09-02 21:33:24 -07:00
parent 71e0cb0301
commit f2b25bebe7
5 changed files with 42 additions and 63 deletions

View File

@@ -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)

View File

@@ -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.", "",

View File

@@ -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)

View File

@@ -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)")

View File

@@ -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 <name> --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.