""" Workbook resolution helpers for the Wuling overseas dealer expansion skill. Rules: - An explicit --excel path always wins. - Otherwise, prefer an existing project workbook in the current directory or parents. - Preview mode never creates a workbook. - Write mode may copy the blank skill template into the project directory. - Never write directly to the skill template. """ from __future__ import annotations import shutil from pathlib import Path from typing import Any, Dict, Optional CANONICAL_WORKBOOK_NAME = "\u6469\u6d1b\u54e5\u5ba2\u6237\u5efa\u8054\u8868-\u6309\u6e20\u9053\u5206\u7c7b.xlsx" TEMPLATE_RELATIVE_PATH = Path("assets") / "blank_customer_outreach_workbook.xlsx" WORKBOOK_NAME_MARKERS = ( "\u6469\u6d1b\u54e5\u5ba2\u6237\u5efa\u8054\u8868-\u6309\u6e20\u9053\u5206\u7c7b", "\u5ba2\u6237\u5efa\u8054\u8868", ) EXCLUDE_NAME_MARKERS = ( "~$", "_backup_", "backup_before", "_with_", "sent_", "preview", "candidate", "filtered", ) def skill_root() -> Path: return Path(__file__).resolve().parents[2] def template_workbook_path(root: Optional[Path] = None) -> Path: return (root or skill_root()) / TEMPLATE_RELATIVE_PATH def _resolve_user_path(excel_path: str) -> Path: path = Path(excel_path).expanduser() if not path.is_absolute(): path = Path.cwd() / path return path.resolve() def _excluded(path: Path) -> bool: name = path.name lowered = name.casefold() return name.startswith("~$") or any(marker.casefold() in lowered for marker in EXCLUDE_NAME_MARKERS) def _looks_like_workbook(path: Path) -> bool: if path.suffix.casefold() != ".xlsx" or _excluded(path): return False return any(marker in path.name for marker in WORKBOOK_NAME_MARKERS) def find_project_workbook(start_dir: Optional[Path] = None) -> Optional[Path]: """Find the newest likely outreach workbook in start_dir or its parents.""" current = (start_dir or Path.cwd()).resolve() search_dirs = [current, *current.parents] for directory in search_dirs: if not directory.exists() or not directory.is_dir(): continue candidates = [path for path in directory.glob("*.xlsx") if _looks_like_workbook(path)] if not candidates: continue exact = [path for path in candidates if path.name == CANONICAL_WORKBOOK_NAME] return sorted(exact or candidates, key=lambda path: path.stat().st_mtime, reverse=True)[0] return None def _copy_template_to(target: Path, root: Optional[Path] = None) -> Path: template = template_workbook_path(root) if not template.exists(): raise FileNotFoundError(f"Blank workbook template not found: {template}") if template.resolve() == target.resolve(): raise ValueError("Refusing to write directly to the skill blank workbook template.") target.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(template, target) return target def resolve_workbook_path( excel_path: str = "", *, start_dir: Optional[Path] = None, create_from_template: bool = False, template_root: Optional[Path] = None, ) -> Dict[str, Any]: """ Resolve the workbook path according to skill rules. Returns a dict with: path, source, created. source is one of explicit, explicit_template_copy, project, template_copy, missing. """ if excel_path: path = _resolve_user_path(excel_path) if path.resolve() == template_workbook_path(template_root).resolve(): raise ValueError("Do not write to the skill blank workbook template. Copy it to the project first.") if create_from_template and not path.exists(): _copy_template_to(path, template_root) return {"path": path, "source": "explicit_template_copy", "created": True} return {"path": path, "source": "explicit", "created": False} project = find_project_workbook(start_dir) if project: return {"path": project, "source": "project", "created": False} if create_from_template: target_dir = (start_dir or Path.cwd()).resolve() target = target_dir / CANONICAL_WORKBOOK_NAME _copy_template_to(target, template_root) return {"path": target, "source": "template_copy", "created": True} return {"path": None, "source": "missing", "created": False}