#!/usr/bin/env python3 """Atomically add a BSL handler, form command, and visible form button.""" from __future__ import annotations import argparse import base64 import json from pathlib import Path from typing import Any from diff_1c_patch_workspace import build_diff from edit_1c_bsl_routine import edit_workspace as edit_bsl_routine from edit_1c_form_button import edit_workspace as edit_form_button from edit_1c_form_command import edit_workspace as edit_form_command from validate_1c_patch_workspace_semantics import validate_workspace def load_json(path: Path) -> dict[str, Any]: return json.loads(path.read_text(encoding="utf-8-sig")) def write_json(path: Path, data: dict[str, Any]) -> None: path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") def read_bytes(path: Path) -> bytes: return path.read_bytes() def workspace_working_files(workspace: Path) -> list[Path]: manifest = load_json(workspace / "manifest.json") paths = [] for record in manifest.get("files") or []: relative = Path(str(record.get("relative_path") or "")) if relative.is_absolute() or ".." in relative.parts: raise SystemExit(f"Unsafe manifest relative path: {relative}") path = workspace / "working" / relative if path.exists(): paths.append(path) return paths def snapshot_working_files(workspace: Path) -> dict[Path, bytes]: return {path: read_bytes(path) for path in workspace_working_files(workspace)} def restore_snapshot(snapshot: dict[Path, bytes]) -> None: for path, content in snapshot.items(): path.parent.mkdir(parents=True, exist_ok=True) path.write_bytes(content) def decode_routine_text(args: argparse.Namespace) -> str: sources = [bool(args.routine_text), bool(args.routine_text_b64), bool(args.routine_file)] if sum(sources) != 1: raise SystemExit("Use exactly one of --routine-text, --routine-text-b64, or --routine-file.") if args.routine_text is not None: return args.routine_text if args.routine_text_b64: return base64.b64decode(args.routine_text_b64).decode("utf-8") return Path(args.routine_file).read_text(encoding="utf-8-sig") def build_result( *, workspace: Path, form_relative_path: str, bsl_relative_path: str, operation: str, routine_text: str, command_name: str, command_title: str, command_action: str, button_parent_name: str, button_name: str, button_title: str, keep_on_failure: bool, max_patch_chars: int, ) -> dict[str, Any]: snapshot = snapshot_working_files(workspace) steps: list[dict[str, Any]] = [] rolled_back = False error: dict[str, Any] | None = None try: bsl = edit_bsl_routine( workspace, bsl_relative_path, routine_text, operation=operation, keep_on_failure=True, ) steps.append({"name": "bsl_routine", "result": bsl}) if not bsl.get("semantic_validation", {}).get("passed"): raise RuntimeError("BSL routine edit failed semantic validation.") command = edit_form_command( workspace, form_relative_path, name=command_name, title=command_title, action=command_action, tooltip=None, command_id=None, call_type="Override", operation=operation, keep_on_failure=True, ) steps.append({"name": "form_command", "result": command}) if not command.get("semantic_validation", {}).get("passed"): raise RuntimeError("Form command edit failed semantic validation.") button = edit_form_button( workspace, form_relative_path, parent_name=button_parent_name, name=button_name, title=button_title, command_name=command_name, button_id=None, button_type="CommandBarButton", operation=operation, keep_on_failure=True, ) steps.append({"name": "form_button", "result": button}) if not button.get("semantic_validation", {}).get("passed"): raise RuntimeError("Form button edit failed semantic validation.") semantic = validate_workspace(workspace) if not semantic.get("passed"): raise RuntimeError("Final semantic validation failed.") diff = build_diff(workspace, max_patch_chars=max_patch_chars) except (Exception, SystemExit) as exc: error = {"type": type(exc).__name__, "message": str(exc)} semantic = validate_workspace(workspace) diff = build_diff(workspace, max_patch_chars=max_patch_chars) if not keep_on_failure: restore_snapshot(snapshot) rolled_back = True semantic = validate_workspace(workspace) diff = build_diff(workspace, max_patch_chars=max_patch_chars) passed = error is None and bool(semantic.get("passed")) and bool(diff.get("passed")) return { "schema": "onec_form_button_workflow.v1", "workspace": str(workspace), "operation": operation, "inputs": { "form_relative_path": form_relative_path, "bsl_relative_path": bsl_relative_path, "command_name": command_name, "command_title": command_title, "command_action": command_action, "button_parent_name": button_parent_name, "button_name": button_name, "button_title": button_title, }, "passed": passed, "rolled_back": rolled_back, "error": error, "steps": steps, "semantic_validation": { "schema": semantic.get("schema"), "passed": semantic.get("passed"), "counts": semantic.get("counts"), "findings": semantic.get("findings"), }, "diff_summary": diff.get("counts"), } def main() -> int: parser = argparse.ArgumentParser(description="Atomically add BSL handler, form command, and visible button in a 1C patch workspace.") parser.add_argument("--workspace", type=Path, required=True) parser.add_argument("--form-relative-path", required=True) parser.add_argument("--bsl-relative-path", required=True) parser.add_argument("--operation", choices=["append", "replace", "upsert"], default="upsert") parser.add_argument("--routine-text") parser.add_argument("--routine-text-b64") parser.add_argument("--routine-file", type=Path) parser.add_argument("--command-name", required=True) parser.add_argument("--command-title", required=True) parser.add_argument("--command-action", required=True) parser.add_argument("--button-parent-name", required=True) parser.add_argument("--button-name", required=True) parser.add_argument("--button-title", required=True) parser.add_argument("--keep-on-failure", action="store_true", help="Keep partial edits when any step fails.") parser.add_argument("--max-patch-chars", type=int, default=200000) parser.add_argument("--output", type=Path) args = parser.parse_args() result = build_result( workspace=args.workspace, form_relative_path=args.form_relative_path, bsl_relative_path=args.bsl_relative_path, operation=args.operation, routine_text=decode_routine_text(args), command_name=args.command_name, command_title=args.command_title, command_action=args.command_action, button_parent_name=args.button_parent_name, button_name=args.button_name, button_title=args.button_title, keep_on_failure=args.keep_on_failure, max_patch_chars=args.max_patch_chars, ) if args.output: write_json(args.output, result) print( json.dumps( { "output": str(args.output) if args.output else None, "passed": result["passed"], "rolled_back": result["rolled_back"], "error": result["error"], "semantic": result["semantic_validation"]["counts"], "diff": result["diff_summary"], }, ensure_ascii=False, ) ) return 0 if result["passed"] else 2 if __name__ == "__main__": raise SystemExit(main())