from __future__ import annotations import json import os import subprocess import sys import tempfile from pathlib import Path from typing import Any, Literal from reuse_surface.patches import is_safe_patch PromptAction = Literal["apply", "skip", "edit", "quit", "apply_all_safe"] class NonInteractiveError(ValueError): pass def is_tty() -> bool: return sys.stdin.isatty() and sys.stdout.isatty() def format_patch_summary(patch: dict[str, Any]) -> str: lines = [ f" capability: {patch['capability_id']}", f" kind: {patch['kind']}", f" confidence: {patch.get('confidence', 'n/a')}", f" rationale: {patch.get('rationale', '')}", ] for key in ("append", "value", "field_path", "dimension", "from_level", "to_level"): if patch.get(key) is not None: lines.append(f" {key}: {patch[key]}") if patch.get("evidence_citations"): lines.append(f" evidence: {', '.join(patch['evidence_citations'])}") return "\n".join(lines) def emit_event(event: str, payload: dict[str, Any]) -> None: print(json.dumps({"event": event, **payload}, sort_keys=True)) def prompt_patch(patch: dict[str, Any]) -> PromptAction: print("\n--- Registry patch ---") print(format_patch_summary(patch)) while True: choice = input("[a]pply [s]kip [e]dit [q]uit [A]pply all safe? ").strip().lower() if choice in {"a", "apply"}: return "apply" if choice in {"s", "skip"}: return "skip" if choice in {"e", "edit"}: return "edit" if choice in {"q", "quit"}: return "quit" if choice == "": continue if choice.upper() == "A" or choice == "apply all safe": return "apply_all_safe" print("Invalid choice.") def edit_patch(patch: dict[str, Any]) -> dict[str, Any]: editor = os.environ.get("EDITOR", "nano") with tempfile.NamedTemporaryFile("w", suffix=".yaml", delete=False) as handle: import yaml yaml.safe_dump(patch, handle, sort_keys=False) temp_path = handle.name subprocess.run([editor, temp_path], check=False) import yaml edited = yaml.safe_load(Path(temp_path).read_text(encoding="utf-8")) Path(temp_path).unlink(missing_ok=True) if not isinstance(edited, dict): return patch return edited def prompt_batch( patches: list[dict[str, Any]], *, assume_yes: bool = False, auto_mode: bool = False, emit_json: bool = False, ) -> list[dict[str, Any]]: if auto_mode or assume_yes: return list(patches) if not is_tty(): if emit_json: for patch in patches: emit_event("suggestion", {"patch": patch, "default": "skip"}) raise NonInteractiveError( "non-interactive stdin; use --auto or --yes to apply patches" ) raise NonInteractiveError( "non-interactive stdin; use --auto or --yes to apply patches" ) selected: list[dict[str, Any]] = [] index = 0 while index < len(patches): patch = patches[index] action = prompt_patch(patch) if action == "apply_all_safe": selected.extend(p for p in patches[index:] if is_safe_patch(p)) index = len(patches) break if action == "quit": break if action == "skip": index += 1 continue if action == "edit": patch = edit_patch(patch) selected.append(patch) index += 1 return selected