diff --git a/hermes_cli/setup_platforms.py b/hermes_cli/setup_platforms.py index 596e33ef41..bb1e9ee675 100644 --- a/hermes_cli/setup_platforms.py +++ b/hermes_cli/setup_platforms.py @@ -35,16 +35,20 @@ def _setup_telegram_auto_result(): return auto_setup_telegram_bot_result(profile_name=profile_name) -def _declines_reconfigure(env_var: str, label: str, question: str) -> bool: - """True when ``env_var`` is already set and the user does NOT want to reconfigure.""" +def declines_reconfigure(label: str, question: str, *env_vars: str) -> bool: + """True when any of ``env_vars`` is already set and the user does NOT want to reconfigure. + + Shared by the core wizards and every platform plugin's ``interactive_setup`` so the + "already configured? Reconfigure? [y/N]" gate has one wording and one default. + """ from hermes_cli.setup import get_env_value, print_info, prompt_yes_no - if not get_env_value(env_var): + if not any(get_env_value(v) for v in env_vars): return False print_info(f"{label}: already configured") return not prompt_yes_no(question, False) -def _save_prompted(env_var: str, question: str, *, password: bool = False, success_msg: str | None = None, +def save_prompted(env_var: str, question: str, *, password: bool = False, success_msg: str | None = None, skip_msg: str | None = None, transform=None) -> str: """Prompt, persist the (optionally transformed) answer when non-empty, and report either way. @@ -143,7 +147,7 @@ def _setup_telegram(): """Configure Telegram bot credentials and allowlist.""" from hermes_cli.setup import _info, print_info, print_header, print_success, prompt, prompt_yes_no, save_env_value print_header("Telegram") - if _declines_reconfigure("TELEGRAM_BOT_TOKEN", "Telegram", "Reconfigure Telegram?"): + if declines_reconfigure("Telegram", "Reconfigure Telegram?", "TELEGRAM_BOT_TOKEN"): _telegram_allowlist_nudge() return token, setup_result = _obtain_telegram_token() @@ -172,12 +176,12 @@ def _setup_telegram(): first_user_id = allowed_users.split(",")[0].strip() if allowed_users else "" if not first_user_id: print_info(" You can also set this later by typing /set-home in your Telegram chat.") - _save_prompted("TELEGRAM_HOME_CHANNEL", "Home channel ID (leave empty to set later)") + save_prompted("TELEGRAM_HOME_CHANNEL", "Home channel ID (leave empty to set later)") elif prompt_yes_no(f"Use your user ID ({first_user_id}) as the home channel?", True): save_env_value("TELEGRAM_HOME_CHANNEL", first_user_id) print_success(f"Telegram home channel set to {first_user_id}") else: - _save_prompted("TELEGRAM_HOME_CHANNEL", "Home channel ID (or leave empty to set later with /set-home in Telegram)") + save_prompted("TELEGRAM_HOME_CHANNEL", "Home channel ID (or leave empty to set later with /set-home in Telegram)") # _setup_slack and _write_slack_manifest_and_instruct moved to the slack plugin: @@ -187,7 +191,7 @@ def _setup_bluebubbles(): """Configure BlueBubbles iMessage gateway.""" from hermes_cli.setup import _info, print_header, print_success, prompt, prompt_yes_no print_header("BlueBubbles (iMessage)") - if _declines_reconfigure("BLUEBUBBLES_SERVER_URL", "BlueBubbles", "Reconfigure BlueBubbles?"): + if declines_reconfigure("BlueBubbles", "Reconfigure BlueBubbles?", "BLUEBUBBLES_SERVER_URL"): return _info("Connects Hermes to iMessage via BlueBubbles — a free, open-source", "macOS server that bridges iMessage to any device.", @@ -199,7 +203,7 @@ def _setup_bluebubbles(): lambda v: v.rstrip("/")), ("BlueBubbles server password", "BLUEBUBBLES_PASSWORD", True, "Password", None), ): - if not _save_prompted(env_var, label, password=secret, transform=transform, + if not save_prompted(env_var, label, password=secret, transform=transform, skip_msg=f"{what} is required — skipping BlueBubbles setup"): return print_success("BlueBubbles credentials saved") @@ -209,7 +213,7 @@ def _setup_bluebubbles(): "BlueBubbles allowlist configured", "⚠️ No allowlist set — anyone who can iMessage you can use the bot!") _info(None, "📬 Home Channel: phone or email for cron job delivery and notifications.", " You can also set this later with /set-home in your iMessage chat.") - _save_prompted("BLUEBUBBLES_HOME_CHANNEL", "Home channel address (leave empty to set later)") + save_prompted("BLUEBUBBLES_HOME_CHANNEL", "Home channel address (leave empty to set later)") _info(None, "Advanced settings (defaults are fine for most setups):") if prompt_yes_no("Configure webhook listener settings?", False): _save_port("BLUEBUBBLES_WEBHOOK_PORT", prompt("Webhook listener port (default: 8645)"), "8645") @@ -222,7 +226,7 @@ def _setup_webhooks(): """Configure webhook integration.""" from hermes_cli.setup import _info, print_header, print_success, print_warning, prompt, save_env_value print_header("Webhooks") - if _declines_reconfigure("WEBHOOK_ENABLED", "Webhooks", "Reconfigure webhooks?"): + if declines_reconfigure("Webhooks", "Reconfigure webhooks?", "WEBHOOK_ENABLED"): return print() print_warning("⚠ Webhook and SMS platforms require exposing gateway ports to the") @@ -231,7 +235,7 @@ def _setup_webhooks(): print() _info(" Full guide: https://hermes-agent.nousresearch.com/docs/user-guide/messaging/webhooks/", None) _save_port("WEBHOOK_PORT", prompt("Webhook port (default 8644)"), "8644") - _save_prompted("WEBHOOK_SECRET", "Global HMAC secret (shared across all routes)", password=True, + save_prompted("WEBHOOK_SECRET", "Global HMAC secret (shared across all routes)", password=True, success_msg="Webhook secret saved", skip_msg="No secret set — you must configure per-route secrets in config.yaml") save_env_value("WEBHOOK_ENABLED", "true") diff --git a/plugins/platforms/buzz/adapter.py b/plugins/platforms/buzz/adapter.py index 9b3230f2c3..f9488b8aa2 100644 --- a/plugins/platforms/buzz/adapter.py +++ b/plugins/platforms/buzz/adapter.py @@ -2053,15 +2053,14 @@ def interactive_setup() -> None: from hermes_cli.setup import ( prompt, prompt_yes_no, save_env_value, get_env_value, print_header, print_info, print_warning, print_success, ) + from hermes_cli.setup_platforms import declines_reconfigure def ask(label: str, env: str) -> str: return prompt(label, default=get_env_value(env) or "") print_header("Buzz") existing_relay = get_env_value("BUZZ_RELAY_URL") - if existing_relay: - print_info(f"Buzz: already configured (relay: {existing_relay})") - if not prompt_yes_no("Reconfigure Buzz?", False): - return + if declines_reconfigure("Buzz", "Reconfigure Buzz?", "BUZZ_RELAY_URL"): + return print_info("Connect Hermes to a Buzz community (Block's Nostr-based human+agent platform).") print_info(" Requires the buzz CLI binary and a Nostr key that is a community member.") print() diff --git a/plugins/platforms/dingtalk/adapter.py b/plugins/platforms/dingtalk/adapter.py index 442d64edff..dfe3e80a9a 100644 --- a/plugins/platforms/dingtalk/adapter.py +++ b/plugins/platforms/dingtalk/adapter.py @@ -659,11 +659,10 @@ def interactive_setup() -> None: from hermes_cli.config import get_env_value, save_env_value from hermes_cli.setup import prompt_choice from hermes_cli.cli_output import prompt, prompt_yes_no, print_header, print_success, print_warning + from hermes_cli.setup_platforms import declines_reconfigure print_header("DingTalk") - if existing := get_env_value("DINGTALK_CLIENT_ID"): - print_success(f"DingTalk is already configured (Client ID: {existing}).") - if not prompt_yes_no("Reconfigure DingTalk?", False): - return + if declines_reconfigure("DingTalk", "Reconfigure DingTalk?", "DINGTALK_CLIENT_ID"): + return choices = ["QR Code Scan (Recommended, auto-obtain Client ID and Client Secret)", "Manual Input (Client ID and Client Secret)"] result = None if prompt_choice("Choose setup method", choices, default=0) == 0: diff --git a/plugins/platforms/discord/adapter.py b/plugins/platforms/discord/adapter.py index da74bd30f3..812c249cd0 100644 --- a/plugins/platforms/discord/adapter.py +++ b/plugins/platforms/discord/adapter.py @@ -6850,6 +6850,7 @@ def interactive_setup() -> None: from hermes_cli.cli_output import ( prompt, prompt_yes_no, print_header, print_info, print_success, ) + from hermes_cli.setup_platforms import declines_reconfigure def _info_lines(*lines: str) -> None: for line in lines: print_info(line) @@ -6859,22 +6860,19 @@ def interactive_setup() -> None: print_success("Discord allowlist configured") print_header("Discord") - existing = get_env_value("DISCORD_BOT_TOKEN") - if existing: - print_info("Discord: already configured") - if not prompt_yes_no("Reconfigure Discord?", False): - if not get_env_value("DISCORD_ALLOWED_USERS"): - print_info( - "⚠️ Discord has no user allowlist. With the fail-closed default, " - "messages are denied unless you configure allowed users, roles, " - "or channels, or set DISCORD_ALLOW_ALL_USERS=true." - ) - if prompt_yes_no("Add allowed users now?", True): - print_info(" To find Discord ID: Enable Developer Mode, right-click name → Copy ID") - allowed_users = prompt("Allowed user IDs (comma-separated)") - if allowed_users: - _save_allowlist(allowed_users) - return + if declines_reconfigure("Discord", "Reconfigure Discord?", "DISCORD_BOT_TOKEN"): + if not get_env_value("DISCORD_ALLOWED_USERS"): + print_info( + "⚠️ Discord has no user allowlist. With the fail-closed default, " + "messages are denied unless you configure allowed users, roles, " + "or channels, or set DISCORD_ALLOW_ALL_USERS=true." + ) + if prompt_yes_no("Add allowed users now?", True): + print_info(" To find Discord ID: Enable Developer Mode, right-click name → Copy ID") + allowed_users = prompt("Allowed user IDs (comma-separated)") + if allowed_users: + _save_allowlist(allowed_users) + return _info_lines( "Create a bot at https://discord.com/developers/applications", "On Bot → Privileged Gateway Intents, enable:", diff --git a/plugins/platforms/feishu/adapter.py b/plugins/platforms/feishu/adapter.py index bc7e9cbabc..ea3d340bee 100644 --- a/plugins/platforms/feishu/adapter.py +++ b/plugins/platforms/feishu/adapter.py @@ -4162,14 +4162,11 @@ def interactive_setup() -> None: from hermes_cli.config import get_env_value, remove_env_value, save_env_value from hermes_cli.setup import prompt_choice from hermes_cli.cli_output import prompt, prompt_yes_no, print_header, print_info, print_success, print_warning + from hermes_cli.setup_platforms import declines_reconfigure print_header("Feishu / Lark") - existing_app_id = get_env_value("FEISHU_APP_ID") - existing_secret = get_env_value("FEISHU_APP_SECRET") - if existing_app_id and existing_secret: - print_success("Feishu / Lark is already configured.") - if not prompt_yes_no("Reconfigure Feishu / Lark?", False): - return + if declines_reconfigure("Feishu / Lark", "Reconfigure Feishu / Lark?", "FEISHU_APP_ID"): + return method_idx = prompt_choice( "How would you like to set up Feishu / Lark?", diff --git a/plugins/platforms/google_chat/adapter.py b/plugins/platforms/google_chat/adapter.py index 082d7e4c25..a2094fb79b 100644 --- a/plugins/platforms/google_chat/adapter.py +++ b/plugins/platforms/google_chat/adapter.py @@ -1579,11 +1579,9 @@ def interactive_setup() -> None: """``hermes setup`` wizard: print GCP instructions, prompt for env vars, persist to ``~/.hermes/.env``.""" from hermes_cli.cli_output import print_info, print_success, print_warning, prompt, prompt_yes_no from hermes_cli.config import get_env_value, save_env_value - existing_sub = get_env_value("GOOGLE_CHAT_SUBSCRIPTION_NAME") - if existing_sub: - print_info(f"Google Chat: already configured (subscription: {existing_sub})") - if not prompt_yes_no("Reconfigure Google Chat?", False): - return + from hermes_cli.setup_platforms import declines_reconfigure + if declines_reconfigure("Google Chat", "Reconfigure Google Chat?", "GOOGLE_CHAT_SUBSCRIPTION_NAME"): + return for line in _SETUP_WALKTHROUGH.splitlines(): print_info(line) for question, env_name, required_label, password in ( diff --git a/plugins/platforms/irc/adapter.py b/plugins/platforms/irc/adapter.py index 641aa10f52..47f66f3ac2 100644 --- a/plugins/platforms/irc/adapter.py +++ b/plugins/platforms/irc/adapter.py @@ -338,6 +338,7 @@ def interactive_setup() -> None: """`hermes gateway setup` flow (lazy hermes_cli imports keep the plugin importable outside the CLI).""" from hermes_cli.setup import ( prompt, prompt_yes_no, save_env_value, get_env_value, print_header, print_info, print_warning, print_success) + from hermes_cli.setup_platforms import declines_reconfigure def info(*lines: str) -> None: for line in lines: @@ -352,10 +353,8 @@ def interactive_setup() -> None: return True print_header("IRC") existing_server = get_env_value("IRC_SERVER") - if existing_server: - print_info(f"IRC: already configured (server: {existing_server})") - if not prompt_yes_no("Reconfigure IRC?", False): - return + if declines_reconfigure("IRC", "Reconfigure IRC?", "IRC_SERVER"): + return info("Connect Hermes to an IRC network. Uses Python stdlib — no extra packages needed.", " Works with Libera.Chat, OFTC, your own ZNC/InspIRCd, etc.") print() diff --git a/plugins/platforms/line/adapter.py b/plugins/platforms/line/adapter.py index 89344b8a8a..766dcf7800 100644 --- a/plugins/platforms/line/adapter.py +++ b/plugins/platforms/line/adapter.py @@ -950,30 +950,20 @@ _SETUP_PROMPTS = ( # (env var, prompt, masked) def interactive_setup() -> None: - """Minimal stdin wizard for ``hermes setup line`` (writes ``~/.hermes/.env``).""" - print("\nLINE Messaging API setup\n------------------------\n" - "Create a Messaging API channel at https://developers.line.biz/console/\nthen copy the values below.\n") - try: - from hermes_cli.config import get_env_value as _get_env, save_env_value as _set_env - except ImportError: - print("hermes_cli.config not available; set LINE_* vars manually in ~/.hermes/.env") + """``hermes setup line`` wizard (writes ``~/.hermes/.env``); CLI helpers are lazy-imported.""" + from hermes_cli.config import get_env_value, save_env_value + from hermes_cli.cli_output import print_header, print_info, prompt + from hermes_cli.setup_platforms import declines_reconfigure + print_header("LINE Messaging API") + if declines_reconfigure("LINE", "Reconfigure LINE?", "LINE_CHANNEL_ACCESS_TOKEN"): return - - for var, prompt, secret in _SETUP_PROMPTS: - existing = _get_env(var) if callable(_get_env) else None - suffix = " [keep current]" if existing else "" - try: - if secret: - from hermes_cli.secret_prompt import masked_secret_prompt - value = masked_secret_prompt(f"{prompt}{suffix}: ") - else: - value = input(f"{prompt}{suffix}: ").strip() - except (EOFError, KeyboardInterrupt): - print() - continue + print_info("Create a Messaging API channel at https://developers.line.biz/console/ then copy the values below.") + for var, question, secret in _SETUP_PROMPTS: + suffix = " [keep current]" if get_env_value(var) else "" + value = prompt(f"{question}{suffix}", password=secret) if value: - _set_env(var, value) - print("Done. Set the webhook URL in the LINE console to /line/webhook and enable 'Use webhook'.") + save_env_value(var, value) + print_info("Done. Set the webhook URL in the LINE console to /line/webhook and enable 'Use webhook'.") def register(ctx) -> None: diff --git a/plugins/platforms/matrix/adapter.py b/plugins/platforms/matrix/adapter.py index b43b2f6069..a1038884a2 100644 --- a/plugins/platforms/matrix/adapter.py +++ b/plugins/platforms/matrix/adapter.py @@ -2923,12 +2923,10 @@ def interactive_setup() -> None: """Interactive credential setup (setup_fn); CLI helpers are lazy-imported.""" from hermes_cli.config import get_env_value, remove_env_value, save_env_value from hermes_cli.cli_output import prompt, prompt_yes_no, print_header, print_info, print_success, print_warning + from hermes_cli.setup_platforms import declines_reconfigure print_header("Matrix") - existing = get_env_value("MATRIX_ACCESS_TOKEN") or get_env_value("MATRIX_PASSWORD") - if existing: - print_info("Matrix: already configured") - if not prompt_yes_no("Reconfigure Matrix?", False): - return + if declines_reconfigure("Matrix", "Reconfigure Matrix?", "MATRIX_ACCESS_TOKEN", "MATRIX_PASSWORD"): + return for line in ("Works with any Matrix homeserver (Synapse, Conduit, Dendrite, or matrix.org).", " 1. Create a bot user on your homeserver, or use your own account", " 2. Get an access token from Element, or provide user ID + password"): diff --git a/plugins/platforms/mattermost/adapter.py b/plugins/platforms/mattermost/adapter.py index 8548ec8bc2..640fee25a3 100644 --- a/plugins/platforms/mattermost/adapter.py +++ b/plugins/platforms/mattermost/adapter.py @@ -659,16 +659,15 @@ def interactive_setup() -> None: """Guide the user through Mattermost bot setup (URL + token, allowlist, home channel).""" from hermes_cli.config import get_env_value, remove_env_value, save_env_value from hermes_cli.cli_output import prompt, prompt_yes_no, print_header, print_info, print_success + from hermes_cli.setup_platforms import declines_reconfigure def info(*lines: str) -> None: for line in lines: print_info(line) print_header("Mattermost") - if get_env_value("MATTERMOST_TOKEN"): - print_info("Mattermost: already configured") - if not prompt_yes_no("Reconfigure Mattermost?", False): - return + if declines_reconfigure("Mattermost", "Reconfigure Mattermost?", "MATTERMOST_TOKEN"): + return info("Works with any self-hosted Mattermost instance.", " 1. In Mattermost: Integrations → Bot Accounts → Add Bot Account", " 2. Copy the bot token") print() diff --git a/plugins/platforms/raft/adapter.py b/plugins/platforms/raft/adapter.py index a3af407d83..d85c92b124 100644 --- a/plugins/platforms/raft/adapter.py +++ b/plugins/platforms/raft/adapter.py @@ -529,13 +529,12 @@ def interactive_setup() -> None: CLI helpers are lazy-imported so the plugin stays importable in gateway runtime and tests.""" from hermes_cli.cli_output import print_header, print_info, print_success, print_warning, prompt, prompt_yes_no from hermes_cli.config import get_env_value, save_env_value + from hermes_cli.setup_platforms import declines_reconfigure print_header("Raft") existing_profile = get_env_value("RAFT_PROFILE") - if existing_profile: - print_info(f"Raft: already configured (profile: {existing_profile})") - if not prompt_yes_no("Reconfigure Raft?", False): - print_info(f"Keeping RAFT_PROFILE={existing_profile}.") - return + if declines_reconfigure("Raft", "Reconfigure Raft?", "RAFT_PROFILE"): + print_info(f"Keeping RAFT_PROFILE={existing_profile}.") + return for line in ("Connect Hermes to Raft as an external agent.", "Create the External Agent in Raft first, then run:", " raft agent login --server --agent --profile-slug "): print_info(line) diff --git a/plugins/platforms/simplex/adapter.py b/plugins/platforms/simplex/adapter.py index 94f8fa00f6..b4da32ea91 100644 --- a/plugins/platforms/simplex/adapter.py +++ b/plugins/platforms/simplex/adapter.py @@ -639,28 +639,22 @@ _SETUP_PROMPTS = ( def interactive_setup() -> None: - """Minimal stdin wizard for ``hermes setup gateway`` → SimpleX; writes ``~/.hermes/.env``.""" - print( - "\nSimpleX Chat setup\n------------------\nRequirements:\n" - " 1. simplex-chat daemon running (e.g. `simplex-chat -p 5225`).\n" - " 2. Python package `websockets` installed (`pip install websockets`).\n") - try: - from hermes_cli.config import get_env_value, save_env_value - except ImportError: - print("hermes_cli.config not available; set SIMPLEX_* vars manually in ~/.hermes/.env") + """``hermes setup gateway`` → SimpleX wizard (writes ``~/.hermes/.env``); CLI helpers are lazy-imported.""" + from hermes_cli.config import get_env_value, save_env_value + from hermes_cli.cli_output import print_header, print_info, prompt + from hermes_cli.setup_platforms import declines_reconfigure + print_header("SimpleX Chat") + if declines_reconfigure("SimpleX", "Reconfigure SimpleX?", "SIMPLEX_WS_URL"): return - - for var, prompt in _SETUP_PROMPTS: - existing = get_env_value(var) if callable(get_env_value) else None - suffix = " [keep current]" if existing else "" - try: - value = input(f"{prompt}{suffix}: ").strip() - except (EOFError, KeyboardInterrupt): - print() - continue + for line in ("Requirements:", " 1. simplex-chat daemon running (e.g. `simplex-chat -p 5225`).", + " 2. Python package `websockets` installed (`pip install websockets`)."): + print_info(line) + for var, question in _SETUP_PROMPTS: + suffix = " [keep current]" if get_env_value(var) else "" + value = prompt(f"{question}{suffix}") if value: save_env_value(var, value) - print("Done. Make sure the simplex-chat daemon is running before starting the gateway.") + print_info("Done. Make sure the simplex-chat daemon is running before starting the gateway.") def register(ctx) -> None: diff --git a/plugins/platforms/slack/adapter.py b/plugins/platforms/slack/adapter.py index 16da69ef73..71b317a6f4 100644 --- a/plugins/platforms/slack/adapter.py +++ b/plugins/platforms/slack/adapter.py @@ -6421,17 +6421,16 @@ def interactive_setup() -> None: from hermes_cli.config import get_env_value, remove_env_value, save_env_value from hermes_cli.cli_output import ( prompt, prompt_yes_no, print_header, print_info, print_success, print_warning) + from hermes_cli.setup_platforms import declines_reconfigure print_header("Slack") - if get_env_value("SLACK_BOT_TOKEN"): - print_info("Slack: already configured") - if not prompt_yes_no("Reconfigure Slack?", False): - # Still offer a manifest refresh so new commands get registered. - if prompt_yes_no( - "Regenerate the Slack app manifest with the latest command " - "list? (recommended after `hermes update`)", True): - _write_slack_manifest_and_instruct() - return + if declines_reconfigure("Slack", "Reconfigure Slack?", "SLACK_BOT_TOKEN"): + # Still offer a manifest refresh so new commands get registered. + if prompt_yes_no( + "Regenerate the Slack app manifest with the latest command " + "list? (recommended after `hermes update`)", True): + _write_slack_manifest_and_instruct() + return for line in _SETUP_STEPS: print_info(line) print() diff --git a/plugins/platforms/teams/adapter.py b/plugins/platforms/teams/adapter.py index 9bb8164dc0..3d19403a9b 100644 --- a/plugins/platforms/teams/adapter.py +++ b/plugins/platforms/teams/adapter.py @@ -747,11 +747,9 @@ _SETUP_INTRO = ( # "" → blank line def interactive_setup() -> None: from hermes_cli.config import get_env_value, save_env_value from hermes_cli.cli_output import prompt, prompt_yes_no, print_info, print_success, print_warning - existing_id = get_env_value("TEAMS_CLIENT_ID") - if existing_id: - print_info(f"Teams: already configured (app ID: {existing_id})") - if not prompt_yes_no("Reconfigure Teams?", False): - return + from hermes_cli.setup_platforms import declines_reconfigure + if declines_reconfigure("Teams", "Reconfigure Teams?", "TEAMS_CLIENT_ID"): + return for line in _SETUP_INTRO: print_info(line) if line else print() for label, env_key, prompt_kwargs in _SETUP_CREDENTIALS: diff --git a/plugins/platforms/wecom/adapter.py b/plugins/platforms/wecom/adapter.py index d74ad4db9e..f6c4bb426c 100644 --- a/plugins/platforms/wecom/adapter.py +++ b/plugins/platforms/wecom/adapter.py @@ -757,11 +757,10 @@ def interactive_setup() -> None: from hermes_cli.config import get_env_value, remove_env_value, save_env_value from hermes_cli.setup import prompt_choice from hermes_cli.cli_output import prompt, prompt_yes_no, print_header, print_info, print_success, print_warning + from hermes_cli.setup_platforms import declines_reconfigure print_header("WeCom (Enterprise WeChat)") - if get_env_value("WECOM_BOT_ID") and get_env_value("WECOM_SECRET"): - print_success("WeCom is already configured.") - if not prompt_yes_no("Reconfigure WeCom?", False): - return + if declines_reconfigure("WeCom", "Reconfigure WeCom?", "WECOM_BOT_ID"): + return method_idx = prompt_choice("How would you like to set up WeCom?", ["Scan QR code to obtain Bot ID and Secret automatically (recommended)", "Enter existing Bot ID and Secret manually"], 0) bot_id = secret = None if method_idx == 0: diff --git a/tests/plugins/platforms/test_interactive_setup_reconfigure_gate.py b/tests/plugins/platforms/test_interactive_setup_reconfigure_gate.py new file mode 100644 index 0000000000..e9a4c3b26a --- /dev/null +++ b/tests/plugins/platforms/test_interactive_setup_reconfigure_gate.py @@ -0,0 +1,71 @@ +"""Every platform plugin's ``interactive_setup`` routes its "already configured? Reconfigure?" +gate through ``hermes_cli.setup_platforms.declines_reconfigure``. + +The wizards used to hand-roll the gate (env check + info line + ``prompt_yes_no(..., False)``) +with drifting wording and, for two of them, raw ``input()`` loops. Fixes to the shared gate +(wording, default, non-interactive handling) only reach a wizard that actually calls it, so the +test spies on the real primitive: with the primary env var set and the user answering "no", the +wizard must have asked the shared gate about that env var and returned without persisting or +prompting anything else. +""" + +import importlib + +import pytest + +import hermes_cli.setup_platforms as setup_platforms_mod + +# (plugin module, primary env var the shared gate keys on) +_WIZARDS = [ + ("plugins.platforms.buzz.adapter", "BUZZ_RELAY_URL"), + ("plugins.platforms.dingtalk.adapter", "DINGTALK_CLIENT_ID"), + ("plugins.platforms.discord.adapter", "DISCORD_BOT_TOKEN"), + ("plugins.platforms.feishu.adapter", "FEISHU_APP_ID"), + ("plugins.platforms.google_chat.adapter", "GOOGLE_CHAT_SUBSCRIPTION_NAME"), + ("plugins.platforms.irc.adapter", "IRC_SERVER"), + ("plugins.platforms.line.adapter", "LINE_CHANNEL_ACCESS_TOKEN"), + ("plugins.platforms.matrix.adapter", "MATRIX_PASSWORD"), + ("plugins.platforms.mattermost.adapter", "MATTERMOST_TOKEN"), + ("plugins.platforms.raft.adapter", "RAFT_PROFILE"), + ("plugins.platforms.simplex.adapter", "SIMPLEX_WS_URL"), + ("plugins.platforms.slack.adapter", "SLACK_BOT_TOKEN"), + ("plugins.platforms.teams.adapter", "TEAMS_CLIENT_ID"), + ("plugins.platforms.wecom.adapter", "WECOM_BOT_ID"), +] + + +@pytest.mark.parametrize("module_name, env_var", _WIZARDS, ids=[m.split(".")[-2] for m, _ in _WIZARDS]) +def test_declining_reconfigure_goes_through_shared_gate(monkeypatch, tmp_path, module_name, env_var): + import hermes_cli.cli_output as cli_output_mod + import hermes_cli.config as config_mod + import hermes_cli.setup as setup_mod + + monkeypatch.setenv("HERMES_HOME", str(tmp_path)) + monkeypatch.setenv(env_var, "already-set") + + gated: list[tuple[str, ...]] = [] + real_gate = setup_platforms_mod.declines_reconfigure + + def _spy_gate(label, question, *env_vars): + gated.append(env_vars) + return real_gate(label, question, *env_vars) + + def _no_save(*_a, **_kw): + raise AssertionError("wizard persisted env after the user declined to reconfigure") + + def _no_prompt(*_a, **_kw): + raise AssertionError("wizard fell through to its own prompts after the user declined") + + # Wizards import the gate lazily from setup_platforms; the gate reads prompt_yes_no through + # hermes_cli.setup. Everything else a wizard could do after declining must not happen. + monkeypatch.setattr(setup_platforms_mod, "declines_reconfigure", _spy_gate) + monkeypatch.setattr(setup_mod, "prompt_yes_no", lambda *_a, **_kw: False) + monkeypatch.setattr(cli_output_mod, "prompt_yes_no", lambda *_a, **_kw: False) + for mod in (setup_mod, cli_output_mod): + monkeypatch.setattr(mod, "prompt", _no_prompt) + monkeypatch.setattr(mod, "save_env_value", _no_save, raising=False) + monkeypatch.setattr(config_mod, "save_env_value", _no_save) + + importlib.import_module(module_name).interactive_setup() + + assert gated and env_var in gated[0], f"{module_name}: gate not routed through declines_reconfigure: {gated}"