from __future__ import annotations import re from datetime import datetime, timezone from pathlib import Path from typing import Any import typer from rich.console import Console from rich.prompt import Prompt from skylattice.core.decisions import create_decision from skylattice.core.intent import load_intent, mark_wizard_answered, save_intent from skylattice.core.paths import blueprint_dir, find_customer_root, load_yaml, set_by_path console = Console() def _parse_list(raw: str) -> list[str]: if not raw or raw.strip().lower() == "none": return [] return [p.strip() for p in raw.split(",") if p.strip()] def _slug_decision(prefix: str, name: str) -> str: day = datetime.now(timezone.utc).strftime("%Y-%m-%d") safe = re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-") return f"{day}-{prefix}-{safe}" def _apply_answer( intent: dict[str, Any], customer_root: Path, q: dict[str, Any], raw: str, *, answer_map: dict[str, Any] | None = None, ) -> None: qtype = q.get("type") qid = q["id"] answer_map = answer_map or {} def _rationale_for(name: str) -> str: key = f"{qid}_rationale" if key in answer_map: return str(answer_map[key]).format(name=name) per_name = answer_map.get(f"{qid}_rationales") or {} if isinstance(per_name, dict) and name in per_name: return str(per_name[name]) if answer_map: # Non-interactive answers file: do not block on Prompt return str( answer_map.get("default_rationale") or f"Deferred via wizard answers for {name}" ) return Prompt.ask(q.get("decision_prompt", "Rationale?").format(name=name)) if qtype in ("string",) and q.get("intent_path"): set_by_path(intent, q["intent_path"], raw.strip()) elif qtype == "choice" and q.get("intent_path"): set_by_path(intent, q["intent_path"], raw.strip()) elif qtype == "list" and q.get("intent_path"): set_by_path(intent, q["intent_path"], _parse_list(raw) or q.get("default")) elif qtype == "domain_list" and q.get("intent_path"): names = _parse_list(raw) profile = q.get("default_access_profile") or "standard" intent["domains"] = [ {"name": n, "access_profile": profile, "zones": ["landing", "raw", "curated", "marts"]} for n in names ] elif qtype == "restricted_domains": names = _parse_list(raw) for d in intent.get("domains") or []: if d["name"] in names: d["access_profile"] = "restricted" elif qtype == "override_with_decision": names = _parse_list(raw) overrides = list(intent.get("overrides") or []) for name in names: rationale = _rationale_for(name) did = _slug_decision("override", name) create_decision( customer_root, decision_id=did, applies_to=q.get("override_path_template", "domains.{name}").format(name=name), rationale=rationale, ) overrides.append( { "path": q.get("override_path_template", "domains.{name}.prod_curated_read").format(name=name), "value": True, "decision_id": did, } ) intent["overrides"] = overrides elif qtype == "unmanaged_list": names = _parse_list(raw) unmanaged = list(intent.get("unmanaged") or []) kind = q.get("kind") or "role" for name in names: rationale = _rationale_for(name) did = _slug_decision("unmanaged", name) create_decision( customer_root, decision_id=did, applies_to=f"unmanaged.{kind}.{name}", rationale=rationale, ) unmanaged.append({"kind": kind, "name": name, "reason": rationale, "decision_id": did}) intent["unmanaged"] = unmanaged else: console.print(f"[yellow]Unknown question type {qtype}; skipped[/yellow]") mark_wizard_answered(intent, qid) def run( mode: str | None = typer.Option(None, "--mode", help="greenfield|brownfield (default: from intent)"), resume: bool = typer.Option(False, "--resume", help="Only ask unanswered questions"), path: Path | None = typer.Option(None, "--path", help="Customer project path"), answers: Path | None = typer.Option(None, "--answers", help="Non-interactive answers YAML"), ) -> None: """Guided interview that writes intent.yaml and decisions/.""" customer_root = path.resolve() if path else find_customer_root() intent = load_intent(customer_root) mode = mode or intent.get("mode") or "greenfield" catalog_name = f"catalog.{mode}.yaml" catalog = load_yaml(blueprint_dir() / "wizard" / catalog_name) questions = catalog.get("questions") or [] answered = set((intent.get("wizard") or {}).get("answered") or []) answer_map: dict[str, Any] = {} if answers: answer_map = load_yaml(answers) or {} console.print(f"[bold]Sky Lattice wizard[/bold] ({mode}) — {customer_root}") if "discover" in (catalog.get("prerequisites") or []): console.print("[dim]Tip: run platformctl discover before brownfield adoption.[/dim]") for q in questions: qid = q["id"] if resume and qid in answered: continue if qid in answer_map: raw = answer_map[qid] if isinstance(raw, list): raw = ",".join(str(x) for x in raw) else: raw = str(raw) console.print(f"{q['prompt']} → {raw}") else: default = q.get("default") if isinstance(default, list): default_s = ",".join(str(x) for x in default) else: default_s = "" if default is None else str(default) raw = Prompt.ask(q["prompt"], default=default_s) _apply_answer(intent, customer_root, q, raw, answer_map=answer_map) intent["mode"] = mode save_intent(customer_root, intent) console.print("[green]Wizard complete.[/green] Run: platformctl validate && platformctl plan")