mspec-cli 4.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mspec/__init__.py +5 -0
- mspec/__main__.py +7 -0
- mspec/cli.py +44 -0
- mspec/commands/__init__.py +7 -0
- mspec/commands/doctor.py +132 -0
- mspec/commands/init.py +157 -0
- mspec/commands/update.py +104 -0
- mspec/core/__init__.py +6 -0
- mspec/core/config.py +141 -0
- mspec/core/template.py +131 -0
- mspec/templates/config.yaml +187 -0
- mspec/templates/design-issue-taxonomy.md +180 -0
- mspec/templates/design-issues.md +218 -0
- mspec/templates/requirement-issue-taxonomy.md +155 -0
- mspec/templates/requirement-issues.md +214 -0
- mspec/utils/__init__.py +17 -0
- mspec/utils/console.py +39 -0
- mspec_cli-4.0.0.dist-info/METADATA +281 -0
- mspec_cli-4.0.0.dist-info/RECORD +23 -0
- mspec_cli-4.0.0.dist-info/WHEEL +5 -0
- mspec_cli-4.0.0.dist-info/entry_points.txt +2 -0
- mspec_cli-4.0.0.dist-info/licenses/LICENSE +21 -0
- mspec_cli-4.0.0.dist-info/top_level.txt +1 -0
mspec/__init__.py
ADDED
mspec/__main__.py
ADDED
mspec/cli.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""mspec CLI 入口"""
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
|
|
7
|
+
from mspec.commands import doctor, init, update
|
|
8
|
+
from mspec.utils.console import print_banner
|
|
9
|
+
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@click.group()
|
|
14
|
+
@click.version_option(version="4.0.0", prog_name="mspec")
|
|
15
|
+
@click.option("--verbose", "-v", is_flag=True, help="显示详细日志")
|
|
16
|
+
@click.pass_context
|
|
17
|
+
def cli(ctx: click.Context, verbose: bool) -> None:
|
|
18
|
+
"""mspec - 成员规范驱动开发工作流 CLI 工具
|
|
19
|
+
|
|
20
|
+
快速开始:
|
|
21
|
+
mspec init 初始化项目配置
|
|
22
|
+
mspec doctor 检查环境配置
|
|
23
|
+
mspec update 更新模板到最新版本
|
|
24
|
+
|
|
25
|
+
更多信息: https://github.com/mspec-dev/mspec
|
|
26
|
+
"""
|
|
27
|
+
ctx.ensure_object(dict)
|
|
28
|
+
ctx.obj["verbose"] = verbose
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# 注册命令
|
|
32
|
+
cli.add_command(init.init_cmd)
|
|
33
|
+
cli.add_command(update.update_cmd)
|
|
34
|
+
cli.add_command(doctor.doctor_cmd)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def main() -> None:
|
|
38
|
+
"""主入口函数"""
|
|
39
|
+
print_banner()
|
|
40
|
+
cli()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
if __name__ == "__main__":
|
|
44
|
+
main()
|
mspec/commands/doctor.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""doctor 命令实现"""
|
|
3
|
+
|
|
4
|
+
import shutil
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
|
|
12
|
+
from mspec.utils.console import print_error, print_success, print_warning
|
|
13
|
+
|
|
14
|
+
console = Console()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@click.command(name="doctor")
|
|
18
|
+
def doctor_cmd() -> None:
|
|
19
|
+
"""诊断环境配置
|
|
20
|
+
|
|
21
|
+
检查 mspec 运行所需的环境配置是否正确。
|
|
22
|
+
|
|
23
|
+
示例:
|
|
24
|
+
mspec doctor
|
|
25
|
+
"""
|
|
26
|
+
console.print("[bold blue]🔍 mspec 环境诊断[/bold blue]\n")
|
|
27
|
+
|
|
28
|
+
checks = [
|
|
29
|
+
("Python 版本", _check_python_version),
|
|
30
|
+
("openspec/ 目录", _check_openspec_dir),
|
|
31
|
+
("config.yaml", _check_config_file),
|
|
32
|
+
("模板文件", _check_template_files),
|
|
33
|
+
("Git 安装", _check_git),
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
results = []
|
|
37
|
+
all_passed = True
|
|
38
|
+
|
|
39
|
+
for name, check_fn in checks:
|
|
40
|
+
console.print(f" 检查 {name}...", end=" ")
|
|
41
|
+
try:
|
|
42
|
+
result = check_fn()
|
|
43
|
+
if result["ok"]:
|
|
44
|
+
message = result.get("message", "")
|
|
45
|
+
if message:
|
|
46
|
+
console.print(f"[green]✓[/green] {message}")
|
|
47
|
+
else:
|
|
48
|
+
console.print("[green]✓[/green]")
|
|
49
|
+
results.append((name, True, result.get("message", "")))
|
|
50
|
+
else:
|
|
51
|
+
console.print(f"[red]✗[/red] {result['message']}")
|
|
52
|
+
results.append((name, False, result["message"]))
|
|
53
|
+
all_passed = False
|
|
54
|
+
except Exception as e:
|
|
55
|
+
console.print(f"[red]✗[/red] {e}")
|
|
56
|
+
results.append((name, False, str(e)))
|
|
57
|
+
all_passed = False
|
|
58
|
+
|
|
59
|
+
# 显示结果表格
|
|
60
|
+
console.print()
|
|
61
|
+
table = Table(show_header=False, box=None)
|
|
62
|
+
table.add_column("检查项")
|
|
63
|
+
table.add_column("状态")
|
|
64
|
+
|
|
65
|
+
for name, passed, _ in results:
|
|
66
|
+
status = "[green]✓ 通过[/green]" if passed else "[red]✗ 失败[/red]"
|
|
67
|
+
table.add_row(name, status)
|
|
68
|
+
|
|
69
|
+
console.print(table)
|
|
70
|
+
console.print()
|
|
71
|
+
|
|
72
|
+
if all_passed:
|
|
73
|
+
print_success("所有检查通过,环境正常!")
|
|
74
|
+
else:
|
|
75
|
+
print_warning("部分检查未通过,请参考上述提示修复")
|
|
76
|
+
sys.exit(1)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _check_python_version() -> dict:
|
|
80
|
+
"""检查 Python 版本"""
|
|
81
|
+
if sys.version_info < (3, 8):
|
|
82
|
+
return {"ok": False, "message": "需要 Python 3.8+"}
|
|
83
|
+
return {"ok": True, "message": f"{sys.version.split()[0]}"}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _check_openspec_dir() -> dict:
|
|
87
|
+
"""检查 openspec 目录"""
|
|
88
|
+
if not Path("openspec").exists():
|
|
89
|
+
return {
|
|
90
|
+
"ok": False,
|
|
91
|
+
"message": "未找到 openspec/ 目录,请先运行 mspec init",
|
|
92
|
+
}
|
|
93
|
+
return {"ok": True, "message": ""}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _check_config_file() -> dict:
|
|
97
|
+
"""检查配置文件"""
|
|
98
|
+
config_path = Path("openspec/config.yaml")
|
|
99
|
+
if not config_path.exists():
|
|
100
|
+
return {"ok": False, "message": "未找到 config.yaml"}
|
|
101
|
+
return {"ok": True, "message": ""}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _check_template_files() -> dict:
|
|
105
|
+
"""检查模板文件"""
|
|
106
|
+
templates_path = Path("openspec/templates")
|
|
107
|
+
if not templates_path.exists():
|
|
108
|
+
return {"ok": False, "message": "未找到 templates/ 目录"}
|
|
109
|
+
|
|
110
|
+
required_files = [
|
|
111
|
+
"requirement-issue-taxonomy.md",
|
|
112
|
+
"design-issue-taxonomy.md",
|
|
113
|
+
"requirement-issues.md",
|
|
114
|
+
"design-issues.md",
|
|
115
|
+
]
|
|
116
|
+
|
|
117
|
+
missing = []
|
|
118
|
+
for file in required_files:
|
|
119
|
+
if not (templates_path / file).exists():
|
|
120
|
+
missing.append(file)
|
|
121
|
+
|
|
122
|
+
if missing:
|
|
123
|
+
return {"ok": False, "message": f"缺少文件: {', '.join(missing)}"}
|
|
124
|
+
|
|
125
|
+
return {"ok": True, "message": ""}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _check_git() -> dict:
|
|
129
|
+
"""检查 Git 安装"""
|
|
130
|
+
if shutil.which("git") is None:
|
|
131
|
+
return {"ok": False, "message": "未找到 Git,请先安装 Git"}
|
|
132
|
+
return {"ok": True, "message": ""}
|
mspec/commands/init.py
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""init 命令实现"""
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
import questionary
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.panel import Panel
|
|
11
|
+
from rich.table import Table
|
|
12
|
+
|
|
13
|
+
from mspec.core.config import ConfigManager
|
|
14
|
+
from mspec.core.template import TemplateManager
|
|
15
|
+
from mspec.utils.console import print_error, print_success
|
|
16
|
+
|
|
17
|
+
console = Console()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@click.command(name="init")
|
|
21
|
+
@click.argument("path", required=False, default=".")
|
|
22
|
+
@click.option("--template", "-t", help="使用自定义模板仓库 URL")
|
|
23
|
+
@click.option("--force", "-f", is_flag=True, help="强制覆盖已有配置")
|
|
24
|
+
@click.option("--dry-run", is_flag=True, help="模拟运行,不实际创建文件")
|
|
25
|
+
@click.option("--yes", "-y", is_flag=True, help="跳过确认,使用默认配置")
|
|
26
|
+
@click.pass_context
|
|
27
|
+
def init_cmd(
|
|
28
|
+
ctx: click.Context,
|
|
29
|
+
path: str,
|
|
30
|
+
template: Optional[str],
|
|
31
|
+
force: bool,
|
|
32
|
+
dry_run: bool,
|
|
33
|
+
yes: bool,
|
|
34
|
+
) -> None:
|
|
35
|
+
"""初始化 mspec 项目
|
|
36
|
+
|
|
37
|
+
PATH 是项目目录路径,默认为当前目录。
|
|
38
|
+
|
|
39
|
+
示例:
|
|
40
|
+
mspec init # 初始化当前目录
|
|
41
|
+
mspec init ./my-project # 初始化指定目录
|
|
42
|
+
mspec init --force # 强制覆盖已有配置
|
|
43
|
+
mspec init --template <url> # 使用自定义模板
|
|
44
|
+
"""
|
|
45
|
+
target_path = Path(path).resolve()
|
|
46
|
+
openspec_path = target_path / "openspec"
|
|
47
|
+
|
|
48
|
+
console.print(
|
|
49
|
+
Panel.fit(
|
|
50
|
+
f"[bold blue]📦 mspec 初始化[/bold blue]\n"
|
|
51
|
+
f"目标目录: [cyan]{target_path}[/cyan]",
|
|
52
|
+
title="mspec",
|
|
53
|
+
border_style="blue",
|
|
54
|
+
)
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# 检查现有配置
|
|
58
|
+
if openspec_path.exists() and not force:
|
|
59
|
+
if not yes:
|
|
60
|
+
overwrite = questionary.confirm(
|
|
61
|
+
"openspec/ 目录已存在,是否覆盖?", default=False
|
|
62
|
+
).ask()
|
|
63
|
+
if not overwrite:
|
|
64
|
+
console.print("[yellow]已取消[/yellow]")
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
# 获取模板
|
|
68
|
+
template_manager = TemplateManager()
|
|
69
|
+
|
|
70
|
+
if template:
|
|
71
|
+
console.print(f"[dim]正在下载自定义模板: {template}[/dim]")
|
|
72
|
+
try:
|
|
73
|
+
files = template_manager.download_from_url(template)
|
|
74
|
+
except Exception as e:
|
|
75
|
+
print_error(f"下载模板失败: {e}")
|
|
76
|
+
return
|
|
77
|
+
else:
|
|
78
|
+
console.print("[dim]使用内置模板[/dim]")
|
|
79
|
+
files = template_manager.get_builtin_templates()
|
|
80
|
+
|
|
81
|
+
# 模拟运行模式
|
|
82
|
+
if dry_run:
|
|
83
|
+
_show_dry_run(files, target_path)
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
# 确认安装
|
|
87
|
+
if not yes:
|
|
88
|
+
_show_files_table(files, target_path)
|
|
89
|
+
confirm = questionary.confirm("确认初始化?", default=True).ask()
|
|
90
|
+
if not confirm:
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
# 创建目录和文件
|
|
94
|
+
console.print("\n[dim]正在创建文件...[/dim]")
|
|
95
|
+
try:
|
|
96
|
+
_create_files(files, openspec_path)
|
|
97
|
+
print_success("文件创建完成")
|
|
98
|
+
except Exception as e:
|
|
99
|
+
print_error(f"创建文件失败: {e}")
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
# 验证安装
|
|
103
|
+
console.print("[dim]正在验证配置...[/dim]")
|
|
104
|
+
config_manager = ConfigManager(openspec_path)
|
|
105
|
+
validation = config_manager.validate()
|
|
106
|
+
|
|
107
|
+
if validation.valid:
|
|
108
|
+
print_success("验证通过")
|
|
109
|
+
_print_success_message()
|
|
110
|
+
else:
|
|
111
|
+
print_error("验证失败:")
|
|
112
|
+
for error in validation.errors:
|
|
113
|
+
console.print(f" [red]• {error}[/red]")
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _show_dry_run(files: dict, target_path: Path) -> None:
|
|
117
|
+
"""显示模拟运行信息"""
|
|
118
|
+
console.print("\n[cyan]📋 模拟运行 - 将创建以下文件:[/cyan]")
|
|
119
|
+
for file_path in files.keys():
|
|
120
|
+
full_path = target_path / "openspec" / file_path
|
|
121
|
+
console.print(f" [green]✓[/green] {full_path}")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _show_files_table(files: dict, target_path: Path) -> None:
|
|
125
|
+
"""显示文件表格"""
|
|
126
|
+
table = Table(title="将创建以下文件")
|
|
127
|
+
table.add_column("文件", style="cyan")
|
|
128
|
+
table.add_column("大小", style="dim")
|
|
129
|
+
|
|
130
|
+
for file_path, content in files.items():
|
|
131
|
+
size = len(content.encode("utf-8"))
|
|
132
|
+
size_str = f"{size} B" if size < 1024 else f"{size / 1024:.1f} KB"
|
|
133
|
+
table.add_row(str(file_path), size_str)
|
|
134
|
+
|
|
135
|
+
console.print()
|
|
136
|
+
console.print(table)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _create_files(files: dict, openspec_path: Path) -> None:
|
|
140
|
+
"""创建文件"""
|
|
141
|
+
openspec_path.mkdir(parents=True, exist_ok=True)
|
|
142
|
+
(openspec_path / "templates").mkdir(exist_ok=True)
|
|
143
|
+
|
|
144
|
+
for file_path, content in files.items():
|
|
145
|
+
full_path = openspec_path / file_path
|
|
146
|
+
full_path.parent.mkdir(parents=True, exist_ok=True)
|
|
147
|
+
full_path.write_text(content, encoding="utf-8")
|
|
148
|
+
console.print(f" [green]✓[/green] {file_path}")
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _print_success_message() -> None:
|
|
152
|
+
"""打印成功消息"""
|
|
153
|
+
console.print("\n[bold green]✅ 初始化完成![/bold green]\n")
|
|
154
|
+
console.print("接下来你可以:")
|
|
155
|
+
console.print(" • 使用 [cyan]/opsx:propose[/cyan] 创建新变更")
|
|
156
|
+
console.print(" • 使用 [cyan]mspec doctor[/cyan] 检查环境")
|
|
157
|
+
console.print(" • 查看 [cyan]https://docs.mspec.io[/cyan] 获取完整文档")
|
mspec/commands/update.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""update 命令实现"""
|
|
3
|
+
|
|
4
|
+
import shutil
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
import semver
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
|
|
12
|
+
from mspec.core.config import ConfigManager
|
|
13
|
+
from mspec.core.template import TemplateManager
|
|
14
|
+
from mspec.utils.console import print_error, print_success, print_warning
|
|
15
|
+
|
|
16
|
+
console = Console()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@click.command(name="update")
|
|
20
|
+
@click.option("--check", is_flag=True, help="仅检查更新,不执行更新")
|
|
21
|
+
def update_cmd(check: bool) -> None:
|
|
22
|
+
"""更新模板到最新版本
|
|
23
|
+
|
|
24
|
+
自动检测并更新到最新版本的模板,同时保留用户的自定义配置。
|
|
25
|
+
|
|
26
|
+
示例:
|
|
27
|
+
mspec update # 更新到最新版本
|
|
28
|
+
mspec update --check # 仅检查是否有更新
|
|
29
|
+
"""
|
|
30
|
+
console.print("[bold blue]🔄 检查更新...[/bold blue]\n")
|
|
31
|
+
|
|
32
|
+
# 检查当前版本
|
|
33
|
+
openspec_path = Path("openspec")
|
|
34
|
+
if not openspec_path.exists():
|
|
35
|
+
print_error("未找到 openspec/ 目录,请先运行 mspec init")
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
config_manager = ConfigManager(openspec_path)
|
|
39
|
+
current_version = config_manager.get_version()
|
|
40
|
+
|
|
41
|
+
console.print(f"当前版本: [cyan]{current_version}[/cyan]")
|
|
42
|
+
|
|
43
|
+
# 获取远程最新版本
|
|
44
|
+
template_manager = TemplateManager()
|
|
45
|
+
try:
|
|
46
|
+
latest_version = template_manager.get_latest_version()
|
|
47
|
+
except Exception as e:
|
|
48
|
+
print_error(f"获取最新版本失败: {e}")
|
|
49
|
+
return
|
|
50
|
+
|
|
51
|
+
console.print(f"最新版本: [cyan]{latest_version}[/cyan]")
|
|
52
|
+
|
|
53
|
+
# 比较版本
|
|
54
|
+
try:
|
|
55
|
+
if semver.compare(current_version, latest_version) >= 0:
|
|
56
|
+
print_success(f"已是最新版本 ({current_version})")
|
|
57
|
+
return
|
|
58
|
+
except ValueError:
|
|
59
|
+
# 版本号格式错误,继续尝试更新
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
if check:
|
|
63
|
+
print_warning(f"发现新版本: {current_version} → {latest_version}")
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
# 执行更新
|
|
67
|
+
console.print(f"\n[yellow]正在更新到 {latest_version}...[/yellow]\n")
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
# 备份旧配置
|
|
71
|
+
backup_path = _backup_config()
|
|
72
|
+
console.print(f"[dim]旧配置已备份到: {backup_path}[/dim]")
|
|
73
|
+
|
|
74
|
+
# 下载新模板
|
|
75
|
+
new_templates = template_manager.download_latest()
|
|
76
|
+
|
|
77
|
+
# 合并配置
|
|
78
|
+
merged_config = config_manager.merge_with_new(new_templates)
|
|
79
|
+
|
|
80
|
+
# 写入新配置
|
|
81
|
+
config_manager.write(merged_config)
|
|
82
|
+
|
|
83
|
+
print_success("更新完成!")
|
|
84
|
+
console.print(f"\n[dim]旧配置备份: {backup_path}[/dim]")
|
|
85
|
+
|
|
86
|
+
except Exception as e:
|
|
87
|
+
print_error(f"更新失败: {e}")
|
|
88
|
+
console.print("[yellow]建议从备份恢复[/yellow]")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _backup_config() -> Path:
|
|
92
|
+
"""备份当前配置"""
|
|
93
|
+
openspec_path = Path("openspec")
|
|
94
|
+
backup_dir = Path.home() / ".mspec" / "backups"
|
|
95
|
+
backup_dir.mkdir(parents=True, exist_ok=True)
|
|
96
|
+
|
|
97
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
98
|
+
backup_path = backup_dir / f"openspec_backup_{timestamp}"
|
|
99
|
+
|
|
100
|
+
if backup_path.exists():
|
|
101
|
+
shutil.rmtree(backup_path)
|
|
102
|
+
|
|
103
|
+
shutil.copytree(openspec_path, backup_path)
|
|
104
|
+
return backup_path
|
mspec/core/__init__.py
ADDED
mspec/core/config.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""配置管理模块"""
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Dict, List
|
|
7
|
+
|
|
8
|
+
import yaml
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class ValidationResult:
|
|
13
|
+
"""验证结果"""
|
|
14
|
+
valid: bool
|
|
15
|
+
errors: List[str] = field(default_factory=list)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ConfigManager:
|
|
19
|
+
"""配置管理器"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, openspec_path: Path):
|
|
22
|
+
self.openspec_path = Path(openspec_path)
|
|
23
|
+
self.config_path = self.openspec_path / "config.yaml"
|
|
24
|
+
|
|
25
|
+
def get_version(self) -> str:
|
|
26
|
+
"""获取当前配置版本"""
|
|
27
|
+
if not self.config_path.exists():
|
|
28
|
+
return "0.0.0"
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
content = yaml.safe_load(self.config_path.read_text(encoding="utf-8"))
|
|
32
|
+
if content and isinstance(content, dict):
|
|
33
|
+
return content.get("version", "4.0.0")
|
|
34
|
+
return "4.0.0"
|
|
35
|
+
except Exception:
|
|
36
|
+
return "4.0.0"
|
|
37
|
+
|
|
38
|
+
def validate(self) -> ValidationResult:
|
|
39
|
+
"""验证配置是否完整
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
验证结果
|
|
43
|
+
"""
|
|
44
|
+
errors = []
|
|
45
|
+
|
|
46
|
+
# 检查 config.yaml
|
|
47
|
+
if not self.config_path.exists():
|
|
48
|
+
errors.append("未找到 config.yaml")
|
|
49
|
+
else:
|
|
50
|
+
try:
|
|
51
|
+
config = yaml.safe_load(self.config_path.read_text(encoding="utf-8"))
|
|
52
|
+
if not config or not isinstance(config, dict):
|
|
53
|
+
errors.append("config.yaml 格式错误")
|
|
54
|
+
elif not config.get("schema"):
|
|
55
|
+
errors.append("config.yaml 缺少 schema 字段")
|
|
56
|
+
except yaml.YAMLError as e:
|
|
57
|
+
errors.append(f"config.yaml 格式错误: {e}")
|
|
58
|
+
|
|
59
|
+
# 检查模板文件
|
|
60
|
+
templates_path = self.openspec_path / "templates"
|
|
61
|
+
required_templates = [
|
|
62
|
+
"requirement-issue-taxonomy.md",
|
|
63
|
+
"design-issue-taxonomy.md",
|
|
64
|
+
"requirement-issues.md",
|
|
65
|
+
"design-issues.md",
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
for template in required_templates:
|
|
69
|
+
if not (templates_path / template).exists():
|
|
70
|
+
errors.append(f"缺少模板文件: {template}")
|
|
71
|
+
|
|
72
|
+
return ValidationResult(valid=len(errors) == 0, errors=errors)
|
|
73
|
+
|
|
74
|
+
def merge_with_new(self, new_templates: Dict[str, str]) -> Dict[str, Any]:
|
|
75
|
+
"""合并现有配置与新模板
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
new_templates: 新模板内容
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
合并后的配置
|
|
82
|
+
"""
|
|
83
|
+
# 读取现有配置
|
|
84
|
+
current_config = {}
|
|
85
|
+
if self.config_path.exists():
|
|
86
|
+
try:
|
|
87
|
+
current_config = yaml.safe_load(self.config_path.read_text(encoding="utf-8")) or {}
|
|
88
|
+
except Exception:
|
|
89
|
+
current_config = {}
|
|
90
|
+
|
|
91
|
+
# 解析新配置
|
|
92
|
+
new_config = {}
|
|
93
|
+
if "config.yaml" in new_templates:
|
|
94
|
+
try:
|
|
95
|
+
new_config = yaml.safe_load(new_templates["config.yaml"]) or {}
|
|
96
|
+
except Exception:
|
|
97
|
+
new_config = {}
|
|
98
|
+
|
|
99
|
+
# 合并策略:
|
|
100
|
+
# 1. 系统字段使用新版本
|
|
101
|
+
# 2. 用户自定义字段保留
|
|
102
|
+
merged = {
|
|
103
|
+
"schema": new_config.get("schema", "spec-driven"),
|
|
104
|
+
"version": new_config.get("version", "4.0.0"),
|
|
105
|
+
"language": current_config.get("language", new_config.get("language", "zh")),
|
|
106
|
+
"context": self._merge_context(
|
|
107
|
+
current_config.get("context", ""),
|
|
108
|
+
new_config.get("context", "")
|
|
109
|
+
),
|
|
110
|
+
"rules": self._merge_rules(
|
|
111
|
+
current_config.get("rules", {}),
|
|
112
|
+
new_config.get("rules", {})
|
|
113
|
+
),
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return merged
|
|
117
|
+
|
|
118
|
+
def _merge_context(self, current: str, new: str) -> str:
|
|
119
|
+
"""合并 context 字段"""
|
|
120
|
+
if current and new and new not in current:
|
|
121
|
+
return f"{current}\n\n# 以下为新版本添加的内容\n{new}"
|
|
122
|
+
return new or current
|
|
123
|
+
|
|
124
|
+
def _merge_rules(self, current: Dict, new: Dict) -> Dict:
|
|
125
|
+
"""合并 rules 字段"""
|
|
126
|
+
# 使用新版本的规则为主
|
|
127
|
+
merged = dict(new)
|
|
128
|
+
|
|
129
|
+
# 但保留用户的自定义规则
|
|
130
|
+
for key, value in current.items():
|
|
131
|
+
if key not in merged:
|
|
132
|
+
merged[key] = value
|
|
133
|
+
|
|
134
|
+
return merged
|
|
135
|
+
|
|
136
|
+
def write(self, config: Dict[str, Any]) -> None:
|
|
137
|
+
"""写入配置"""
|
|
138
|
+
self.config_path.write_text(
|
|
139
|
+
yaml.dump(config, allow_unicode=True, sort_keys=False),
|
|
140
|
+
encoding="utf-8"
|
|
141
|
+
)
|