2025-12-13 05:01:28 +08:00
|
|
|
|
"""配置管理:生成默认配置、读取/写入配置、维护 .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
|
|
|
|
|
|
|
2025-12-18 18:18:43 +08:00
|
|
|
|
|
|
|
|
|
|
def find_project_root(start_path: Path | None = None) -> Path:
|
|
|
|
|
|
"""递归向上查找包含有效 .aide 目录的项目根目录。
|
|
|
|
|
|
|
|
|
|
|
|
类似于 git 查找 .git 目录的逻辑:从当前目录开始向上遍历,
|
|
|
|
|
|
直到找到包含有效 .aide 目录的父目录。
|
|
|
|
|
|
|
2025-12-18 20:33:37 +08:00
|
|
|
|
查找策略(三遍遍历):
|
|
|
|
|
|
0. 首先:如果当前目录有 .aide 目录,直接使用(不向上查找)
|
2025-12-18 18:18:43 +08:00
|
|
|
|
1. 第一遍:优先查找包含 flow-status.json 的目录(活跃任务)
|
|
|
|
|
|
2. 第二遍:如果第一遍未找到,查找包含 config.toml 的目录
|
|
|
|
|
|
|
2025-12-18 20:33:37 +08:00
|
|
|
|
这样可以确保:
|
|
|
|
|
|
- 在子项目目录运行时,使用子项目的配置
|
|
|
|
|
|
- 从子目录运行时,优先找到有活跃任务的项目根目录
|
2025-12-18 18:18:43 +08:00
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
start_path: 起始路径,默认为当前工作目录
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
找到有效 .aide 目录的父目录路径,如果未找到则返回起始路径
|
|
|
|
|
|
"""
|
|
|
|
|
|
if start_path is None:
|
|
|
|
|
|
start_path = Path.cwd()
|
|
|
|
|
|
|
|
|
|
|
|
start_path = start_path.resolve()
|
|
|
|
|
|
|
|
|
|
|
|
def search_upward(check_fn) -> Path | None:
|
|
|
|
|
|
"""向上遍历查找满足条件的目录"""
|
|
|
|
|
|
current = start_path
|
|
|
|
|
|
while current != current.parent:
|
|
|
|
|
|
if check_fn(current):
|
|
|
|
|
|
return current
|
|
|
|
|
|
current = current.parent
|
|
|
|
|
|
# 检查根目录本身
|
|
|
|
|
|
if check_fn(current):
|
|
|
|
|
|
return current
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
2025-12-18 20:33:37 +08:00
|
|
|
|
def has_aide_dir(path: Path) -> bool:
|
|
|
|
|
|
"""检查是否有 .aide 目录"""
|
|
|
|
|
|
return (path / ".aide").is_dir()
|
|
|
|
|
|
|
2025-12-18 18:18:43 +08:00
|
|
|
|
def has_flow_status(path: Path) -> bool:
|
|
|
|
|
|
"""检查是否有活跃任务状态文件"""
|
|
|
|
|
|
return (path / ".aide" / "flow-status.json").exists()
|
|
|
|
|
|
|
|
|
|
|
|
def has_config(path: Path) -> bool:
|
|
|
|
|
|
"""检查是否有配置文件"""
|
|
|
|
|
|
return (path / ".aide" / "config.toml").exists()
|
|
|
|
|
|
|
2025-12-18 20:33:37 +08:00
|
|
|
|
# 步骤 0:如果当前目录有 .aide 目录,直接使用(不向上查找)
|
|
|
|
|
|
if has_aide_dir(start_path):
|
|
|
|
|
|
return start_path
|
|
|
|
|
|
|
2025-12-18 18:18:43 +08:00
|
|
|
|
# 第一遍:优先查找有活跃任务的目录
|
|
|
|
|
|
result = search_upward(has_flow_status)
|
|
|
|
|
|
if result is not None:
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
# 第二遍:查找有配置文件的目录
|
|
|
|
|
|
result = search_upward(has_config)
|
|
|
|
|
|
if result is not None:
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
# 未找到有效 .aide 目录,返回原始起始路径
|
|
|
|
|
|
return start_path
|
|
|
|
|
|
|
2025-12-15 18:09:36 +08:00
|
|
|
|
DEFAULT_CONFIG = """################################################################################
|
|
|
|
|
|
# Aide 配置文件 (config.toml)
|
|
|
|
|
|
################################################################################
|
|
|
|
|
|
#
|
|
|
|
|
|
# 本配置文件为 Aide 工作流体系的核心配置,由 `aide init` 命令生成。
|
|
|
|
|
|
# 所有配置项都有详细说明,用户可仅通过本文件了解所有支持的功能。
|
|
|
|
|
|
#
|
|
|
|
|
|
# 配置操作说明:
|
|
|
|
|
|
# - 读取配置:aide config get <key> 例:aide config get flow.phases
|
|
|
|
|
|
# - 设置配置:aide config set <key> <value> 例:aide config set task.source "my-task.md"
|
|
|
|
|
|
# - 支持点号分隔的嵌套键,如:env.venv.path
|
|
|
|
|
|
#
|
|
|
|
|
|
# 注意:LLM 不应直接编辑此文件,必须通过 aide 命令操作。
|
|
|
|
|
|
#
|
|
|
|
|
|
################################################################################
|
|
|
|
|
|
|
|
|
|
|
|
################################################################################
|
|
|
|
|
|
# [general] - 通用配置
|
|
|
|
|
|
################################################################################
|
|
|
|
|
|
# 控制 Aide 的全局行为设置。
|
|
|
|
|
|
|
|
|
|
|
|
[general]
|
|
|
|
|
|
# 是否在 .gitignore 中忽略 .aide 目录
|
2025-12-19 02:30:45 +08:00
|
|
|
|
# - true:自动添加 .aide/ 到 .gitignore,不跟踪 aide 状态
|
|
|
|
|
|
# - false(默认):不修改 .gitignore,允许 git 跟踪 .aide 目录
|
|
|
|
|
|
# 推荐使用此设置,便于多设备同步 aide 状态和任务历史
|
|
|
|
|
|
gitignore_aide = false
|
2025-12-15 18:09:36 +08:00
|
|
|
|
|
|
|
|
|
|
################################################################################
|
|
|
|
|
|
# [runtime] - Aide 运行时要求
|
|
|
|
|
|
################################################################################
|
|
|
|
|
|
# 定义 aide 程序本身的运行环境要求。
|
|
|
|
|
|
# 这些配置用于 `aide env ensure --runtime` 检测。
|
2025-12-14 05:52:59 +08:00
|
|
|
|
|
2025-12-13 05:01:28 +08:00
|
|
|
|
[runtime]
|
2025-12-15 18:09:36 +08:00
|
|
|
|
# Python 最低版本要求
|
|
|
|
|
|
# 格式:"主版本.次版本",如 "3.11"、"3.12"
|
2025-12-13 05:01:28 +08:00
|
|
|
|
python_min = "3.11"
|
2025-12-15 18:09:36 +08:00
|
|
|
|
|
|
|
|
|
|
# 是否要求使用 uv 包管理器
|
|
|
|
|
|
# - true:检测 uv 是否安装
|
|
|
|
|
|
# - false:不检测 uv
|
2025-12-13 05:01:28 +08:00
|
|
|
|
use_uv = true
|
|
|
|
|
|
|
2025-12-15 18:09:36 +08:00
|
|
|
|
################################################################################
|
|
|
|
|
|
# [task] - 任务文档配置
|
|
|
|
|
|
################################################################################
|
|
|
|
|
|
# 定义任务相关文档的默认路径。
|
|
|
|
|
|
|
2025-12-13 05:01:28 +08:00
|
|
|
|
[task]
|
2025-12-15 18:09:36 +08:00
|
|
|
|
# 任务原文档路径(用户提供的原始任务描述)
|
|
|
|
|
|
# /aide:prep 命令在未指定参数时读取此文件
|
2025-12-13 05:01:28 +08:00
|
|
|
|
source = "task-now.md"
|
2025-12-15 18:09:36 +08:00
|
|
|
|
|
|
|
|
|
|
# 任务细则文档路径(aide 生成的可执行任务细则)
|
|
|
|
|
|
# /aide:exec 命令在未指定参数时读取此文件
|
2025-12-13 05:01:28 +08:00
|
|
|
|
spec = "task-spec.md"
|
|
|
|
|
|
|
2025-12-18 12:50:59 +08:00
|
|
|
|
# 复杂任务计划文档目录
|
|
|
|
|
|
# 当任务被拆分为多个子计划时,存放:
|
|
|
|
|
|
# - guide.md: 任务计划总导览
|
|
|
|
|
|
# - spec-01.md, spec-02.md, ...: 各子计划细则
|
|
|
|
|
|
plans_path = ".aide/task-plans/"
|
|
|
|
|
|
|
2025-12-15 18:09:36 +08:00
|
|
|
|
################################################################################
|
|
|
|
|
|
# [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"]
|
|
|
|
|
|
|
2025-12-13 05:01:28 +08:00
|
|
|
|
[env]
|
2025-12-14 05:52:59 +08:00
|
|
|
|
# 启用的模块列表
|
2025-12-15 18:09:36 +08:00
|
|
|
|
# 只有列在此处的模块才会被 `aide env ensure` 检测
|
|
|
|
|
|
# 示例:
|
|
|
|
|
|
# - 纯 Python 项目:["python", "uv", "venv", "requirements"]
|
|
|
|
|
|
# - Rust 项目:["rust"]
|
|
|
|
|
|
# - Node.js 项目:["node", "node_deps"]
|
|
|
|
|
|
# - 多语言项目:["python", "uv", "venv", "node", "node_deps:frontend"]
|
2025-12-14 05:52:59 +08:00
|
|
|
|
modules = ["python", "uv", "venv", "requirements"]
|
|
|
|
|
|
|
2025-12-15 18:09:36 +08:00
|
|
|
|
# -------------------------------
|
|
|
|
|
|
# 以下为各模块的详细配置示例
|
|
|
|
|
|
# -------------------------------
|
|
|
|
|
|
|
2025-12-14 05:52:59 +08:00
|
|
|
|
# Python 版本要求(可选,默认使用 runtime.python_min)
|
|
|
|
|
|
# [env.python]
|
|
|
|
|
|
# min_version = "3.11"
|
|
|
|
|
|
|
2025-12-15 18:09:36 +08:00
|
|
|
|
# Rust 版本要求(可选)
|
|
|
|
|
|
# [env.rust]
|
|
|
|
|
|
# min_version = "1.70"
|
|
|
|
|
|
|
|
|
|
|
|
# Node.js 版本要求(可选)
|
|
|
|
|
|
# [env.node]
|
|
|
|
|
|
# min_version = "18"
|
|
|
|
|
|
|
|
|
|
|
|
# Flutter 版本要求(可选)
|
|
|
|
|
|
# [env.flutter]
|
|
|
|
|
|
# min_version = "3.0"
|
|
|
|
|
|
|
|
|
|
|
|
# 虚拟环境配置(类型B模块,启用时必须配置)
|
|
|
|
|
|
# path: 虚拟环境目录路径,相对于项目根目录
|
2025-12-14 05:52:59 +08:00
|
|
|
|
[env.venv]
|
|
|
|
|
|
path = ".venv"
|
|
|
|
|
|
|
2025-12-15 18:09:36 +08:00
|
|
|
|
# Python 依赖文件配置(类型B模块,启用时必须配置)
|
|
|
|
|
|
# path: requirements.txt 文件路径,相对于项目根目录
|
2025-12-14 05:52:59 +08:00
|
|
|
|
[env.requirements]
|
|
|
|
|
|
path = "requirements.txt"
|
2025-12-13 05:01:28 +08:00
|
|
|
|
|
2025-12-15 18:09:36 +08:00
|
|
|
|
# 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"
|
|
|
|
|
|
|
2025-12-19 02:30:45 +08:00
|
|
|
|
################################################################################
|
|
|
|
|
|
# [user_docs] - 面向用户的文档配置
|
|
|
|
|
|
################################################################################
|
|
|
|
|
|
# 配置面向用户的文档系统。
|
|
|
|
|
|
# 包括 README、用户文档和流程图等。
|
|
|
|
|
|
|
|
|
|
|
|
[user_docs]
|
|
|
|
|
|
# README 文件路径(相对于项目根目录)
|
|
|
|
|
|
readme_path = "README.md"
|
|
|
|
|
|
|
|
|
|
|
|
# README 编写规范文件路径
|
|
|
|
|
|
# 存放项目的 README 编写规范和模板选择
|
|
|
|
|
|
rules_path = "make-readme-rules.md"
|
|
|
|
|
|
|
|
|
|
|
|
# 用户文档目录路径
|
|
|
|
|
|
docs_path = "docs"
|
|
|
|
|
|
|
|
|
|
|
|
# 用户流程图目录路径
|
|
|
|
|
|
graph_path = "docs/graph-guide"
|
|
|
|
|
|
|
|
|
|
|
|
# 流程图计划文件路径
|
|
|
|
|
|
# 存放流程图编写计划和进度,用于分步执行和接续执行
|
|
|
|
|
|
graph_plan_path = "docs/graph-guide/plan.md"
|
|
|
|
|
|
|
2025-12-15 18:09:36 +08:00
|
|
|
|
################################################################################
|
|
|
|
|
|
# [flow] - 流程追踪配置
|
|
|
|
|
|
################################################################################
|
|
|
|
|
|
# 配置任务执行流程的追踪和校验。
|
|
|
|
|
|
|
2025-12-13 05:01:28 +08:00
|
|
|
|
[flow]
|
2025-12-15 18:09:36 +08:00
|
|
|
|
# 环节名称列表(有序)
|
|
|
|
|
|
# 定义任务执行的标准流程,用于校验环节跳转合法性
|
|
|
|
|
|
# 标准环节:
|
|
|
|
|
|
# task-optimize - 任务优化阶段(/aide:prep 使用)
|
|
|
|
|
|
# flow-design - 流程设计(创建流程图)
|
|
|
|
|
|
# impl - 迭代实现
|
|
|
|
|
|
# verify - 验证交付
|
|
|
|
|
|
# docs - 文档更新
|
2025-12-18 12:50:59 +08:00
|
|
|
|
# confirm - 用户确认(审阅与返工)
|
2025-12-15 18:09:36 +08:00
|
|
|
|
# finish - 收尾
|
2025-12-18 12:50:59 +08:00
|
|
|
|
phases = ["task-optimize", "flow-design", "impl", "verify", "docs", "confirm", "finish"]
|
2025-12-15 02:08:06 +08:00
|
|
|
|
|
2025-12-15 18:09:36 +08:00
|
|
|
|
# 流程图目录路径
|
|
|
|
|
|
# 存放 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 确认界面。
|
|
|
|
|
|
|
2025-12-15 02:08:06 +08:00
|
|
|
|
[decide]
|
|
|
|
|
|
# HTTP 服务起始端口
|
2025-12-15 18:09:36 +08:00
|
|
|
|
# 如果端口被占用,会自动探测下一个可用端口
|
2025-12-15 02:08:06 +08:00
|
|
|
|
port = 3721
|
2025-12-15 18:09:36 +08:00
|
|
|
|
|
|
|
|
|
|
# 监听地址
|
|
|
|
|
|
# - "127.0.0.1"(默认):仅本机访问
|
|
|
|
|
|
# - "0.0.0.0":允许外部访问(注意安全风险)
|
|
|
|
|
|
bind = "127.0.0.1"
|
|
|
|
|
|
|
|
|
|
|
|
# 自定义访问地址(可选)
|
|
|
|
|
|
# 为空时自动生成为 http://{bind}:{port}
|
|
|
|
|
|
# 适用于反向代理或自定义域名场景
|
|
|
|
|
|
# 示例:url = "https://decide.example.com"
|
|
|
|
|
|
url = ""
|
|
|
|
|
|
|
|
|
|
|
|
# 超时时间(秒)
|
|
|
|
|
|
# - 0(默认):不超时,等待用户完成
|
|
|
|
|
|
# - >0:超时后自动关闭服务
|
2025-12-15 02:08:06 +08:00
|
|
|
|
timeout = 0
|
2025-12-15 18:09:36 +08:00
|
|
|
|
|
|
|
|
|
|
################################################################################
|
|
|
|
|
|
# 配置文件版本信息
|
|
|
|
|
|
################################################################################
|
|
|
|
|
|
# 本配置文件格式版本,用于未来兼容性检查
|
|
|
|
|
|
# _version = "1.1.0"
|
2025-12-13 05:01:28 +08:00
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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:
|
2025-12-15 18:09:36 +08:00
|
|
|
|
"""根据配置决定是否在 .gitignore 中添加 .aide/ 忽略项。"""
|
2025-12-19 02:30:45 +08:00
|
|
|
|
# 读取配置,默认为 False(不忽略 .aide 目录)
|
2025-12-15 18:09:36 +08:00
|
|
|
|
config = self.load_config()
|
|
|
|
|
|
gitignore_aide = self._walk_get(config, "general.gitignore_aide")
|
|
|
|
|
|
if gitignore_aide is None:
|
2025-12-19 02:30:45 +08:00
|
|
|
|
gitignore_aide = False # 默认值
|
2025-12-15 18:09:36 +08:00
|
|
|
|
|
|
|
|
|
|
if not gitignore_aide:
|
|
|
|
|
|
# 配置为 False,不添加忽略项
|
|
|
|
|
|
return
|
|
|
|
|
|
|
2025-12-13 05:01:28 +08:00
|
|
|
|
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:
|
2025-12-15 20:07:01 +08:00
|
|
|
|
self.ensure_config()
|
|
|
|
|
|
self._update_config_value(key, value)
|
2025-12-13 05:01:28 +08:00
|
|
|
|
output.ok(f"已更新 {key} = {value!r}")
|
|
|
|
|
|
|
2025-12-15 20:07:01 +08:00
|
|
|
|
def _update_config_value(self, key: str, value: Any) -> None:
|
|
|
|
|
|
"""保守更新配置值,保留注释和格式。"""
|
|
|
|
|
|
import re
|
|
|
|
|
|
|
|
|
|
|
|
content = self.config_path.read_text(encoding="utf-8")
|
|
|
|
|
|
parts = key.split(".")
|
|
|
|
|
|
|
|
|
|
|
|
# 格式化值为 TOML 格式
|
|
|
|
|
|
if isinstance(value, bool):
|
|
|
|
|
|
toml_value = "true" if value else "false"
|
|
|
|
|
|
elif isinstance(value, str):
|
|
|
|
|
|
toml_value = f'"{value}"'
|
|
|
|
|
|
elif isinstance(value, (int, float)):
|
|
|
|
|
|
toml_value = str(value)
|
|
|
|
|
|
elif isinstance(value, list):
|
|
|
|
|
|
toml_value = toml_dumps({"_": value}).split("=", 1)[1].strip()
|
|
|
|
|
|
else:
|
|
|
|
|
|
toml_value = toml_dumps({"_": value}).split("=", 1)[1].strip()
|
|
|
|
|
|
|
|
|
|
|
|
new_content = None
|
|
|
|
|
|
count = 0
|
|
|
|
|
|
|
|
|
|
|
|
if len(parts) == 1:
|
|
|
|
|
|
# 顶层键:key = value
|
|
|
|
|
|
pattern = rf'^({re.escape(parts[0])}\s*=\s*)(.*)$'
|
|
|
|
|
|
new_content, count = re.subn(pattern, rf'\g<1>{toml_value}', content, count=1, flags=re.MULTILINE)
|
|
|
|
|
|
elif len(parts) >= 2:
|
|
|
|
|
|
# 两级或三级键:找到对应 section,然后替换其中的 key
|
|
|
|
|
|
if len(parts) == 2:
|
|
|
|
|
|
section = parts[0]
|
|
|
|
|
|
subkey = parts[1]
|
|
|
|
|
|
else:
|
|
|
|
|
|
section = ".".join(parts[:-1])
|
|
|
|
|
|
subkey = parts[-1]
|
|
|
|
|
|
|
|
|
|
|
|
# 找到 section 的起始位置
|
|
|
|
|
|
section_pattern = rf'^\[{re.escape(section)}\]\s*$'
|
|
|
|
|
|
section_match = re.search(section_pattern, content, flags=re.MULTILINE)
|
|
|
|
|
|
|
|
|
|
|
|
if section_match:
|
|
|
|
|
|
section_start = section_match.end()
|
|
|
|
|
|
# 找到下一个 section 的位置(或文件末尾)
|
|
|
|
|
|
next_section = re.search(r'^\[', content[section_start:], flags=re.MULTILINE)
|
|
|
|
|
|
if next_section:
|
|
|
|
|
|
section_end = section_start + next_section.start()
|
|
|
|
|
|
else:
|
|
|
|
|
|
section_end = len(content)
|
|
|
|
|
|
|
|
|
|
|
|
# 在 section 范围内查找并替换 key
|
|
|
|
|
|
section_content = content[section_start:section_end]
|
|
|
|
|
|
key_pattern = rf'^({re.escape(subkey)}\s*=\s*)(.*)$'
|
|
|
|
|
|
new_section, count = re.subn(key_pattern, rf'\g<1>{toml_value}', section_content, count=1, flags=re.MULTILINE)
|
|
|
|
|
|
|
|
|
|
|
|
if count > 0:
|
|
|
|
|
|
new_content = content[:section_start] + new_section + content[section_end:]
|
|
|
|
|
|
|
|
|
|
|
|
if count == 0 or new_content is None:
|
|
|
|
|
|
# 键不存在,需要添加(回退到传统方式,会丢失注释)
|
|
|
|
|
|
data = self.load_config()
|
|
|
|
|
|
self._walk_set(data, key, value)
|
|
|
|
|
|
self._write_config(data)
|
|
|
|
|
|
output.warn("配置键不存在,已添加(注释可能丢失)")
|
|
|
|
|
|
else:
|
|
|
|
|
|
self.config_path.write_text(new_content, encoding="utf-8")
|
|
|
|
|
|
|
2025-12-13 05:01:28 +08:00
|
|
|
|
def _write_config(self, data: dict[str, Any]) -> None:
|
2025-12-15 20:07:01 +08:00
|
|
|
|
"""完全重写配置文件(会丢失注释,仅在添加新键时使用)。"""
|
2025-12-13 05:01:28 +08:00
|
|
|
|
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
|