#!/usr/bin/env python3 """安全初始化 Vibe Coding 的 L1 或 L2 文档目录。""" from __future__ import annotations import argparse import datetime as dt import re import shutil import sys import tempfile from pathlib import Path SKILL_ROOT = Path(__file__).resolve().parent.parent TEMPLATES = SKILL_ROOT / "assets" / "templates" INVALID_NAME = re.compile(r'[<>:"/\\|?*\x00-\x1f]') def safe_name(value: str) -> str: cleaned = INVALID_NAME.sub("_", value).strip().rstrip(".") if not cleaned: raise ValueError("名称不能为空或只包含非法字符") return cleaned def render(template_name: str, replacements: dict[str, str]) -> str: text = (TEMPLATES / template_name).read_text(encoding="utf-8") for key, value in replacements.items(): text = text.replace("{{" + key + "}}", value) return text def write_new(path: Path, content: str) -> None: if path.exists(): raise FileExistsError(f"拒绝覆盖已有文件:{path}") path.parent.mkdir(parents=True, exist_ok=True) path.write_text(content, encoding="utf-8") def parse_subtask(raw: str) -> tuple[str, str]: if ":" not in raw: raise ValueError(f"子任务格式应为 ST-001:名称,实际为:{raw}") number, name = raw.split(":", 1) number = number.strip().upper() if not re.fullmatch(r"ST-\d{3}", number): raise ValueError(f"子任务编号格式错误:{number}") return number, safe_name(name) def build_l1(args: argparse.Namespace) -> Path: now = dt.datetime.now().astimezone().isoformat(timespec="seconds") replacements = { "需求名称": args.name, "编号": args.requirement_id or "待确认", "状态": "待确认", "时间": now, } if args.subtask_dir: target_dir = Path(args.subtask_dir).resolve() if not target_dir.is_dir(): raise FileNotFoundError(f"子任务目录不存在:{target_dir}") write_new( target_dir / "01.精简变更日志.md", render("L1-精简变更日志模板.md", replacements), ) return target_dir management_root = Path(args.management_root).resolve() management_root.mkdir(parents=True, exist_ok=True) pm_id = safe_name(args.requirement_id or "待确认") target = management_root / f"PM-{pm_id}-{safe_name(args.name)}" if target.exists(): raise FileExistsError(f"目标目录已存在:{target}") temporary = Path(tempfile.mkdtemp(prefix=".vibe-init-", dir=management_root)) try: write_new( temporary / "01.精简变更日志.md", render("L1-精简变更日志模板.md", replacements), ) temporary.replace(target) except Exception: shutil.rmtree(temporary, ignore_errors=True) raise return target def build_l2(args: argparse.Namespace) -> Path: management_root = Path(args.management_root).resolve() management_root.mkdir(parents=True, exist_ok=True) pm_id = safe_name(args.pm_id or "待确认") demand_name = safe_name(args.name) target = management_root / f"PM-{pm_id}-{demand_name}" if target.exists(): raise FileExistsError(f"目标目录已存在:{target}") subtasks = [parse_subtask(item) for item in args.subtask] if not subtasks: raise ValueError("L2 至少需要一个 --subtask ST-001:名称") numbers = [number for number, _ in subtasks] if len(numbers) != len(set(numbers)): raise ValueError("子任务编号不能重复") now = dt.datetime.now().astimezone().isoformat(timespec="seconds") common = { "PM编号": pm_id, "需求名称": demand_name, "名称": demand_name, "状态": "需求分析中", "阶段": "需求分析", "时间": now, } temporary = Path(tempfile.mkdtemp(prefix=".vibe-init-", dir=management_root)) try: source_version = safe_name(args.source_version) (temporary / "source" / source_version).mkdir(parents=True) (temporary / "code").mkdir(parents=True) (temporary / "tasks").mkdir(parents=True) overview = render("大需求总览模板.md", common) default_row = ( "| ST-001 | {{模块}} | 需求分析 | 需求分析中 | {{依赖}} | " "[查看](./tasks/ST-001-模块/01.需求分析.md) |" ) rows = "\n".join( f"| {number} | {name} | 需求分析 | 未开始 | 待梳理 | " f"[查看](./tasks/{number}-{name}/01.需求分析.md) |" for number, name in subtasks ) overview = overview.replace(default_row, rows) write_new(temporary / "README.md", overview) write_new(temporary / "STATUS.md", render("任务状态模板.md", common)) write_new( temporary / "技术侧需求分析.md", render("技术侧需求分析模板.md", common), ) source_index = render("原始需求版本索引模板.md", common) source_index = source_index.replace("| v1 |", f"| {source_version} |") source_index = source_index.replace("./v1/", f"./{source_version}/") write_new(temporary / "source" / "README.md", source_index) write_new( temporary / "code" / "MANIFEST.md", render("代码副本清单模板.md", common), ) phase_templates = [ ("01-需求分析模板.md", "01.需求分析.md"), ("02-技术实现方案模板.md", "02.技术实现方案.md"), ("03-冒烟与逻辑验证模板.md", "03.冒烟与逻辑验证.md"), ("04-技术实现记录模板.md", "04.技术实现记录.md"), ("05-代码Review报告模板.md", "05.代码Review报告.md"), ("06-验收与交付报告模板.md", "06.验收与交付报告.md"), ] for number, name in subtasks: task_dir = temporary / "tasks" / f"{number}-{name}" replacements = dict(common) replacements.update( { "ST编号": number, "子任务名称": name, "PM编号及链接": f"[{pm_id}](../../README.md)", "链接": "../../README.md", } ) for template_name, output_name in phase_templates: write_new( task_dir / output_name, render(template_name, replacements), ) temporary.replace(target) except Exception: shutil.rmtree(temporary, ignore_errors=True) raise return target def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( description="初始化 Vibe Coding L1/L2 文档,拒绝覆盖已有目标。", ) subparsers = parser.add_subparsers(dest="level", required=True) l1 = subparsers.add_parser("l1", help="初始化一份 L1 精简变更日志") l1.add_argument("--name", required=True, help="需求名称") l1.add_argument("--requirement-id", help="需求编号;未提供时使用“待确认”") l1_target = l1.add_mutually_exclusive_group(required=True) l1_target.add_argument("--management-root", help="独立需求的 management 目录") l1_target.add_argument("--subtask-dir", help="既有大需求下的子任务目录") l2 = subparsers.add_parser("l2", help="初始化完整 L2 PM 目录") l2.add_argument("--management-root", required=True, help="management 目录") l2.add_argument("--name", required=True, help="大需求名称") l2.add_argument("--pm-id", help="PM 编号;未提供时使用“待确认”") l2.add_argument("--source-version", default="v1", help="初始原始需求版本") l2.add_argument( "--subtask", action="append", default=[], help="子任务,格式 ST-001:名称;可重复", ) return parser def main() -> int: parser = build_parser() args = parser.parse_args() try: target = build_l1(args) if args.level == "l1" else build_l2(args) except (OSError, ValueError) as exc: print(f"初始化失败:{exc}", file=sys.stderr) return 1 print(f"初始化完成:{target}") return 0 if __name__ == "__main__": raise SystemExit(main())