Files

1 line
11 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"repo": "adriel007/matrioska", "files": {"src/matrioska/cli/init_wizard.py": {"7b5683c5cd36cb1e791c80e4eff8c1bd266d92ba": {"t": 1785674548.011795, "n": 9943, "h": "8195412eb2da4e0d", "c": "\"\"\"Interactive `.env` scaffolding for `matrioska init`.\n\nPrompts the user for provider, model, and API key, detects existing\n`.env`, and writes a Matrioska-ready environment file. Optionally\nseeds `MATRIOSKA.md` with project conventions.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport os\nimport sys\nfrom pathlib import Path\nfrom typing import Dict, List, Optional, Tuple\n\n# Provider catalog: (id, display name, default base_url, suggested model, key_hint)\n_PROVIDERS: List[Tuple[str, str, str, str, str]] = [\n (\"openai\", \"OpenAI\", \"https://api.openai.com/v1\", \"gpt-4o-mini\", \"sk-...\"),\n (\"anthropic\", \"Anthropic\", \"https://api.anthropic.com\", \"claude-sonnet-4\", \"sk-ant-...\"),\n (\"groq\", \"Groq\", \"https://api.groq.com/openai/v1\", \"llama-3.3-70b-versatile\", \"gsk_...\"),\n (\"openrouter\", \"OpenRouter\", \"https://openrouter.ai/api/v1\", \"anthropic/claude-sonnet-4\", \"sk-or-...\"),\n (\"deepseek\", \"DeepSeek\", \"https://api.deepseek.com/v1\", \"deepseek-chat\", \"ds_...\"),\n (\"xai\", \"xAI (Grok)\", \"https://api.x.ai/v1\", \"grok-2-latest\", \"xai-...\"),\n (\"mistral\", \"Mistral\", \"https://api.mistral.ai/v1\", \"mistral-large-latest\", \"...\"),\n (\"together\", \"Together\", \"https://api.together.xyz/v1\", \"Qwen/Qwen2.5-Coder-32B-Instruct\", \"...\"),\n (\"nvidia\", \"NVIDIA NIM\", \"https://integrate.api.nvidia.com/v1\", \"meta/llama-3.1-70b-instruct\", \"nvapi-...\"),\n (\"ollama\", \"Ollama (local)\", \"http://localhost:11434/v1\", \"llama3.1:8b\", \"ollama\"),\n]\n\n\ndef _ask(prompt: str, default: str = \"\") -> str:\n \"\"\"Prompt the user, returning the input (or default if blank).\"\"\"\n suffix = f\" [{default}]\" if default else \"\"\n try:\n ans = input(f\"{prompt}{suffix}: \").strip()\n except EOFError:\n return default\n return ans or default\n\n\ndef _ask_choice(prompt: str, choices: List[str], default_idx: int = 0) -> int:\n \"\"\"Numeric multiple-choice prompt. Returns the chosen index.\"\"\"\n print(prompt)\n for i, label in enumerate(choices, 1):\n marker = \"*\" if i - 1 == default_idx else \" \"\n print(f\" {marker} {i}) {label}\")\n while True:\n raw = _ask(\"Choose\", default=str(default_idx + 1))\n if not raw.isdigit():\n print(\" → please enter a number\")\n continue\n n = int(raw)\n if 1 <= n <= len(choices):\n return n - 1\n print(f\" → out of range (1–{len(choices)})\")\n\n\ndef _ask_yes_no(prompt: str, default: bool = True) -> bool:\n suffix = \"Y/n\" if default else \"y/N\"\n raw = _ask(f\"{prompt} ({suffix})\", default=\"Y\" if default else \"N\").lower()\n return raw.startswith(\"y\") if raw else default\n\n\ndef _read_existing(env_path: Path) -> Dict[str, str]:\n out: Dict[str, str] = {}\n if not env_path.exists():\n return out\n for raw in env_path.read_text(encoding=\"utf-8\", errors=\"ignore\").splitlines():\n line = raw.strip()\n if not line or line.startswith(\"#\") or \"=\" not in line:\n continue\n k, _, v = line.partition(\"=\")\n out[k.strip()] = v.strip().strip('\"').strip(\"'\")\n return out\n\n\ndef _render_env(values: Dict[str, str]) -> str:\n \"\"\"Render the .env file with grouped sections + comments.\"\"\"\n sections = [\n (\"Provider\", [\"MATRIOSKA_PROVIDER\", \"MATRIOSKA_BASE_URL\",\n \"MATRIOSKA_API_KEY\", \"MATRIOSKA_API_KEYS\", \"MATRIOSKA_EXTRA_ENDPOINTS\"]),\n (\"Models\", [\"MATRIOSKA_MODEL\", \"MATRIOSKA_ARCHITECT_MODEL\",\n \"MATRIOSKA_GENERATOR_MODEL\", \"MATRIOSKA_VALIDATOR_MODEL\",\n \"MATRIOSKA_JUDGE_MODEL\", \"MATRIOSKA_REPAIRER_MODEL\"]),\n (\"Pipeline\", [\"MATRIOSKA_MAX_REPAIRS\", \"MATRIOSKA_MAX_DEPTH\",\n \"MATRIOSKA_PARALLEL\", \"MATRIOSKA_ARCHITECT_CANDIDATES\"]),\n (\"Phases\", [\"MATRIOSKA_ENABLE_TOT\", \"MATRIOSKA_ENABLE_REFLEXION\",\n \"MATRIOSKA_ENABLE_TEST_DESIGN\", \"MATRIOSKA_USE_ACI_REPAIR\",\n \"MATRIOSKA_ENABLE_SANDBOX\"]),\n (\"Vault & misc\", [\"MATRIOSKA_ENABLE_VAULT\", \"MATRIOSKA_VAULT_DIR\",\n \"MATRIOSKA_LOG_LEVEL\"]),\n ]\n lines = [\n \"# Generated by `matrioska init`\",\n \"# Edit freely — every key can also be overridden via CLI flag.\",\n \"\",\n ]\n written = set()\n for title, keys in sections:\n block: List[str] = []\n for k in keys:\n if k in values and values[k] != \"\":\n block.append(f'{k}={values[k]}')\n written.add(k)\n if block:\n lines.append(f\"# ── {title} ─────────────────────────────────────────\")\n lines.extend(block)\n lines.append(\"\")\n # Spill remaining keys\n leftovers = [k for k in values if k not in written and values[k] != \"\"]\n if leftovers:\n lines.append(\"# ── Other ──────────────────────────────────────────────\")\n for k in leftovers:\n lines.append(f'{k}={values[k]}')\n lines.append(\"\")\n return \"\\n\".join(lines).rstrip() + \"\\n\"\n\n\n_MATRIOSKA_MD_TEMPLATE = \"\"\"# {project_name}\n\n## Project conventions\n\nNotes injected into the Architect on every run. Edit freely.\n\n### Stack\n- Language: Python 3.11+\n- Frameworks: <add yours>\n- Storage: <add yours>\n\n### Style\n- Prefer small, focused modules\n- Avoid unnecessary abstractions until 3rd repetition\n- Type hints on public functions; docstrings only when the why is non-obvious\n\n### Don'ts\n- No global mutable state\n- No silent exception swallowing\n\n### Notes for the agent\n- <add domain notes or constraints here>\n\"\"\"\n\n\ndef run_init(cwd: Optional[Path] = None) -> int:\n \"\"\"Interactive `.env` scaffolder. Returns process exit code.\"\"\"\n cwd = cwd or Path.cwd()\n env_path = cwd / \".env\"\n\n print(\"─\" * 60)\n print(\" Matrioska init\")\n print(\"─\" * 60)\n print(f\" Target: {env_path}\")\n print()\n\n existing = _read_existing(env_path)\n if existing:\n print(f\"Found existing .env with {len(existing)} key(s).\")\n action = _ask_choice(\n \"What should I do?\",\n [\"Merge (keep existing values, fill missing)\",\n \"Overwrite (replace with new values)\",\n \"Abort\"],\n default_idx=0,\n )\n if action == 2:\n print(\"Aborted.\")\n return 0\n merge = action == 0\n else:\n merge = False\n\n # Provider selection\n labels = [f\"{disp} — model: {model}\" for _id, disp, _url, model, _hint in _PROVIDERS]\n current_provider = existing.get(\"MATRIOSKA_PROVIDER\", \"\")\n default_idx = next(\n (i for i, (pid, *_r) in enumerate(_PROVIDERS) if pid == current_provider),\n 0,\n )\n idx = _ask_choice(\"\\nPick a provider:\", labels, default_idx=default_idx)\n pid, _disp, default_url, default_model, key_hint = _PROVIDERS[idx]\n\n base_url = _ask(\n \"Base URL\",\n default=existing.get(\"MATRIOSKA_BASE_URL\") or default_url,\n )\n\n api_key = \"\"\n if pid != \"ollama\":\n api_key_default = existing.get(\"MATRIOSKA_API_KEY\", \"\")\n prompt_label = \"API key\" + (f\" (hint: {key_hint})\" if not api_key_default else \" (press Enter to keep current)\")\n api_key = _ask(prompt_label, default=api_key_default)\n else:\n api_key = \"ollama\"\n\n model = _ask(\"Default model\", default=existing.get(\"MATRIOSKA_MODEL\") or default_model)\n\n use_role_models = _ask_yes_no(\"\\nSet per-role model overrides? (architect/generator/validator/judge)\", default=False)\n arch_model = gen_model = val_model = judge_model = rep_model = \"\"\n if use_role_models:\n arch_model = _ask(\"Architect model\", default=existing.get(\"MATRIOSKA_ARCHITECT_MODEL\") or model)\n gen_model = _ask(\"Generator model\", default=existing.get(\"MATRIOSKA_GENERATOR_MODEL\") or model)\n val_model = _ask(\"Validator model\", default=existing.get(\"MATRIOSKA_VALIDATOR_MODEL\") or model)\n judge_model = _ask(\"Judge model\", default=existing.get(\"MATRIOSKA_JUDGE_MODEL\") or model)\n rep_model = _ask(\"Repairer model\", default=existing.get(\"MATRIOSKA_REPAIRER_MODEL\") or model)\n\n multikey = \"\"\n if _ask_yes_no(\"\\nUse multiple API keys for rotation? (comma-separated)\", default=False):\n multikey = _ask(\"Comma-separated API keys\", default=existing.get(\"MATRIOSKA_API_KEYS\", \"\"))\n\n # Build values dict (merge-respecting)\n values: Dict[str, str] = dict(existing) if merge else {}\n values.update({\n \"MATRIOSKA_PROVIDER\": pid,\n \"MATRIOSKA_BASE_URL\": base_url,\n \"MATRIOSKA_API_KEY\": api_key,\n \"MATRIOSKA_MODEL\": model,\n })\n if multikey:\n values[\"MATRIOSKA_API_KEYS\"] = multikey\n if use_role_models:\n values.update({\n \"MATRIOSKA_ARCHITECT_MODEL\": arch_model,\n \"MATRIOSKA_GENERATOR_MODEL\": gen_model,\n \"MATRIOSKA_VALIDATOR_MODEL\": val_model,\n \"MATRIOSKA_JUDGE_MODEL\": judge_model,\n \"MATRIOSKA_REPAIRER_MODEL\": rep_model,\n })\n\n # Write .env\n env_path.write_text(_render_env(values), encoding=\"utf-8\")\n print(f\"\\n✓ Wrote {env_path}\")\n\n # MATRIOSKA.md scaffold\n md_path = cwd / \"MATRIOSKA.md\"\n if not md_path.exists():\n if _ask_yes_no(\"\\nCreate MATRIOSKA.md (project conventions injected into the Architect)?\", default=True):\n project_name = _ask(\"Project name\", default=cwd.name)\n md_path.write_text(\n _MATRIOSKA_MD_TEMPLATE.format(project_name=project_name),\n encoding=\"utf-8\",\n )\n print(f\"✓ Wrote {md_path}\")\n\n print(\"\\nNext steps:\")\n print(f\" matrioska run --task \\\"Create a CLI todo app with SQLite\\\"\")\n print(f\" matrioska run --task '…' --quick # rapid iteration\")\n print(f\" matrioska run --task '…' --mode plan # plan only\")\n return 0\n"}}}}