222 lines
8.2 KiB
Python
222 lines
8.2 KiB
Python
#!/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())
|