refactor(hermes_cli): setup cluster — print_header(gap=), yes/no answer table, bws _step helper, sync row table
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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.", "",
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)")
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user