from __future__ import annotations import os from dataclasses import dataclass, field from typing import Any @dataclass class SetupWizardStep: step_id: str title: str = "" description: str = "" required: bool = True completed: bool = False config_key: str = "" config_value: Any = None @dataclass class SetupWizardState: steps: list[SetupWizardStep] = field(default_factory=list) current_step: int = 0 @property def is_complete(self) -> bool: return all(s.completed for s in self.steps if s.required) @property def current(self) -> SetupWizardStep | None: if 0 <= self.current_step < len(self.steps): return self.steps[self.current_step] return None def advance(self) -> None: if self.current_step < len(self.steps): self.steps[self.current_step].completed = True self.current_step += 1 def to_config(self) -> dict[str, Any]: config: dict[str, Any] = {} for step in self.steps: if step.config_key and step.config_value is not None: config[step.config_key] = step.config_value return config _ENV_TOKEN_KEYS = [ ("SLACK_BOT_TOKEN", "token", "bot_token"), ("SLACK_APP_TOKEN", "app_token", "app_token"), ("SLACK_SIGNING_SECRET", "signing_secret", "signing_secret"), ] def detect_env_tokens() -> dict[str, str]: detected: dict[str, str] = {} for env_key, _config_key, display_key in _ENV_TOKEN_KEYS: value = os.environ.get(env_key, "") if value: detected[display_key] = value[:8] + "..." return detected def is_env_token_configured(key: str) -> bool: for env_key, _, display_key in _ENV_TOKEN_KEYS: if display_key == key or env_key == key: return bool(os.environ.get(env_key, "")) return False def create_setup_wizard_steps(config: dict[str, Any] | None = None) -> list[SetupWizardStep]: existing = config or {} env_tokens = detect_env_tokens() token_env = env_tokens.get("bot_token") token_desc = "Enter your Slack Bot Token (xoxb-...)" if token_env: token_desc = f"Bot Token detected from env ({token_env}). Leave blank to use env value." app_token_env = env_tokens.get("app_token") app_token_desc = "Enter your Slack App-Level Token (xapp-...) for Socket Mode" if app_token_env: app_token_desc = f"App Token detected from env ({app_token_env}). Leave blank to use env value." signing_env = env_tokens.get("signing_secret") signing_desc = "Enter your Slack Signing Secret for Events API" if signing_env: signing_desc = "Signing Secret detected from env. Leave blank to use env value." return [ SetupWizardStep( step_id="token", title="Bot Token", description=token_desc, required=True, completed=bool(existing.get("token") or is_env_token_configured("bot_token")), config_key="token", ), SetupWizardStep( step_id="app_token", title="App-Level Token", description=app_token_desc, required=True, completed=bool(existing.get("app_token") or is_env_token_configured("app_token")), config_key="app_token", ), SetupWizardStep( step_id="signing_secret", title="Signing Secret", description=signing_desc, required=False, completed=bool(existing.get("signing_secret") or is_env_token_configured("signing_secret")), config_key="signing_secret", ), SetupWizardStep( step_id="security_mode", title="Security Mode", description="Choose security mode: open or pairing", required=True, completed=bool(existing.get("security", {}).get("mode")), config_key="security.mode", ), SetupWizardStep( step_id="allow_from", title="Allowed Users", description="Configure allowed users (comma-separated IDs or '*' for all)", required=False, completed=bool(existing.get("security", {}).get("allow_from")), config_key="security.allow_from", ), ] def setup_wizard_to_config(state: SetupWizardState) -> dict[str, Any]: return state.to_config()