"""配置管理:生成默认配置、读取/写入配置、维护 .aide 目录与 .gitignore。""" from __future__ import annotations from pathlib import Path from typing import Any import tomllib from tomli_w import dumps as toml_dumps from aide.core import output DEFAULT_CONFIG = """################################################################################ # Aide 配置文件 (config.toml) ################################################################################ # # 本配置文件为 Aide 工作流体系的核心配置,由 `aide init` 命令生成。 # 所有配置项都有详细说明,用户可仅通过本文件了解所有支持的功能。 # # 配置操作说明: # - 读取配置:aide config get 例:aide config get flow.phases # - 设置配置:aide config set 例:aide config set task.source "my-task.md" # - 支持点号分隔的嵌套键,如:env.venv.path # # 注意:LLM 不应直接编辑此文件,必须通过 aide 命令操作。 # ################################################################################ ################################################################################ # [general] - 通用配置 ################################################################################ # 控制 Aide 的全局行为设置。 [general] # 是否在 .gitignore 中忽略 .aide 目录 # - true(默认):自动添加 .aide/ 到 .gitignore,不跟踪 aide 状态 # - false:不修改 .gitignore,允许 git 跟踪 .aide 目录 # 适用于需要在多设备同步 aide 状态的场景 gitignore_aide = true ################################################################################ # [runtime] - Aide 运行时要求 ################################################################################ # 定义 aide 程序本身的运行环境要求。 # 这些配置用于 `aide env ensure --runtime` 检测。 [runtime] # Python 最低版本要求 # 格式:"主版本.次版本",如 "3.11"、"3.12" python_min = "3.11" # 是否要求使用 uv 包管理器 # - true:检测 uv 是否安装 # - false:不检测 uv use_uv = true ################################################################################ # [task] - 任务文档配置 ################################################################################ # 定义任务相关文档的默认路径。 [task] # 任务原文档路径(用户提供的原始任务描述) # /aide:prep 命令在未指定参数时读取此文件 source = "task-now.md" # 任务细则文档路径(aide 生成的可执行任务细则) # /aide:exec 命令在未指定参数时读取此文件 spec = "task-spec.md" ################################################################################ # [env] - 环境检测配置 ################################################################################ # 配置项目的开发环境检测模块。 # # 模块分为两类: # 类型A - 无需配置即可检测(全局工具): # python - Python 解释器版本检测 # uv - uv 包管理器检测 # rust - Rust 工具链(rustc + cargo)检测 # node - Node.js 运行时检测 # flutter - Flutter SDK 检测 # android - Android SDK 检测 # # 类型B - 需要配置路径才能检测(项目级): # venv - Python 虚拟环境 # requirements - Python 依赖文件(requirements.txt) # node_deps - Node.js 项目依赖(package.json) # # 模块实例化(多项目场景): # 格式:模块类型:实例名 # 例如:node_deps:frontend、node_deps:backend # 各实例独立配置:[env."node_deps:frontend"] [env] # 启用的模块列表 # 只有列在此处的模块才会被 `aide env ensure` 检测 # 示例: # - 纯 Python 项目:["python", "uv", "venv", "requirements"] # - Rust 项目:["rust"] # - Node.js 项目:["node", "node_deps"] # - 多语言项目:["python", "uv", "venv", "node", "node_deps:frontend"] modules = ["python", "uv", "venv", "requirements"] # ------------------------------- # 以下为各模块的详细配置示例 # ------------------------------- # Python 版本要求(可选,默认使用 runtime.python_min) # [env.python] # min_version = "3.11" # Rust 版本要求(可选) # [env.rust] # min_version = "1.70" # Node.js 版本要求(可选) # [env.node] # min_version = "18" # Flutter 版本要求(可选) # [env.flutter] # min_version = "3.0" # 虚拟环境配置(类型B模块,启用时必须配置) # path: 虚拟环境目录路径,相对于项目根目录 [env.venv] path = ".venv" # Python 依赖文件配置(类型B模块,启用时必须配置) # path: requirements.txt 文件路径,相对于项目根目录 [env.requirements] path = "requirements.txt" # Node.js 依赖配置示例(类型B模块) # 注意:需先在 modules 中添加 "node_deps" 才会生效 # [env.node_deps] # path = "." # package.json 所在目录 # manager = "npm" # 包管理器:npm、yarn、pnpm # 多项目实例化示例(前端 + 后端分离项目) # modules = ["node_deps:frontend", "node_deps:backend"] # # [env."node_deps:frontend"] # path = "frontend" # manager = "pnpm" # # [env."node_deps:backend"] # path = "backend" # manager = "npm" ################################################################################ # [docs] - 项目文档配置(面向 LLM) ################################################################################ # 配置面向 LLM 的项目文档系统。 # 这些文档用于帮助 LLM 理解项目结构,支持增量更新。 [docs] # 项目文档目录路径 # 存放总导览和各区块文档 # 默认:.aide/project-docs path = ".aide/project-docs" # 区块计划文档路径 # 记录文档区块划分和生成进度,用于多对话续接 # 默认:.aide/project-docs/block-plan.md block_plan_path = ".aide/project-docs/block-plan.md" ################################################################################ # [flow] - 流程追踪配置 ################################################################################ # 配置任务执行流程的追踪和校验。 [flow] # 环节名称列表(有序) # 定义任务执行的标准流程,用于校验环节跳转合法性 # 标准环节: # task-optimize - 任务优化阶段(/aide:prep 使用) # flow-design - 流程设计(创建流程图) # impl - 迭代实现 # verify - 验证交付 # docs - 文档更新 # finish - 收尾 phases = ["task-optimize", "flow-design", "impl", "verify", "docs", "finish"] # 流程图目录路径 # 存放 PlantUML 源文件(.puml)和生成的图片(.png) # flow-design 阶段会校验此目录下的 .puml 文件 diagram_path = ".aide/diagrams" ################################################################################ # [plantuml] - PlantUML 配置 ################################################################################ # 配置 PlantUML 流程图生成工具。 [plantuml] # PlantUML jar 文件路径 # 支持绝对路径或相对于 aide-program 目录的相对路径 # 默认使用 aide-program/lib/plantuml.jar jar_path = "lib/plantuml.jar" # Java 命令路径(可选) # 默认使用系统 PATH 中的 java # java_path = "/usr/bin/java" ################################################################################ # [decide] - 待定项确认配置 ################################################################################ # 配置待定项 Web 确认界面。 [decide] # HTTP 服务起始端口 # 如果端口被占用,会自动探测下一个可用端口 port = 3721 # 监听地址 # - "127.0.0.1"(默认):仅本机访问 # - "0.0.0.0":允许外部访问(注意安全风险) bind = "127.0.0.1" # 自定义访问地址(可选) # 为空时自动生成为 http://{bind}:{port} # 适用于反向代理或自定义域名场景 # 示例:url = "https://decide.example.com" url = "" # 超时时间(秒) # - 0(默认):不超时,等待用户完成 # - >0:超时后自动关闭服务 timeout = 0 ################################################################################ # 配置文件版本信息 ################################################################################ # 本配置文件格式版本,用于未来兼容性检查 # _version = "1.1.0" """ class ConfigManager: def __init__(self, root: Path): self.root = root self.aide_dir = self.root / ".aide" self.config_path = self.aide_dir / "config.toml" self.decisions_dir = self.aide_dir / "decisions" self.logs_dir = self.aide_dir / "logs" def ensure_base_dirs(self) -> None: self.aide_dir.mkdir(parents=True, exist_ok=True) self.decisions_dir.mkdir(parents=True, exist_ok=True) self.logs_dir.mkdir(parents=True, exist_ok=True) def ensure_gitignore(self) -> None: """根据配置决定是否在 .gitignore 中添加 .aide/ 忽略项。""" # 读取配置,默认为 True(忽略 .aide 目录) config = self.load_config() gitignore_aide = self._walk_get(config, "general.gitignore_aide") if gitignore_aide is None: gitignore_aide = True # 默认值 if not gitignore_aide: # 配置为 False,不添加忽略项 return gitignore_path = self.root / ".gitignore" marker = ".aide/" if gitignore_path.exists(): content = gitignore_path.read_text(encoding="utf-8").splitlines() if any(line.strip() == marker for line in content): return content.append(marker) gitignore_path.write_text("\n".join(content) + "\n", encoding="utf-8") else: gitignore_path.write_text(f"{marker}\n", encoding="utf-8") def ensure_config(self) -> dict[str, Any]: self.ensure_base_dirs() if not self.config_path.exists(): self.config_path.write_text(DEFAULT_CONFIG, encoding="utf-8") output.ok("已创建默认配置 .aide/config.toml") return self.load_config() def load_config(self) -> dict[str, Any]: if not self.config_path.exists(): return {} try: with self.config_path.open("rb") as f: return tomllib.load(f) except Exception as exc: # pragma: no cover - 兼容性输出 output.err(f"读取配置失败: {exc}") return {} def get_value(self, key: str) -> Any: data = self.load_config() return self._walk_get(data, key) def set_value(self, key: str, value: Any) -> None: data = self.ensure_config() self._walk_set(data, key, value) self._write_config(data) output.ok(f"已更新 {key} = {value!r}") def _write_config(self, data: dict[str, Any]) -> None: self.config_path.write_text(toml_dumps(data), encoding="utf-8") @staticmethod def _walk_get(data: dict[str, Any], dotted_key: str) -> Any: current: Any = data for part in dotted_key.split("."): if not isinstance(current, dict): return None if part not in current: return None current = current[part] return current @staticmethod def _walk_set(data: dict[str, Any], dotted_key: str, value: Any) -> None: parts = dotted_key.split(".") current = data for part in parts[:-1]: if part not in current or not isinstance(current[part], dict): current[part] = {} current = current[part] current[parts[-1]] = value