feat: 实现部分aide

This commit is contained in:
2025-12-13 05:01:28 +08:00
parent bb58aee6ee
commit 6dc675ffdc
14 changed files with 511 additions and 1 deletions

16
aide-program/aide.bat Normal file
View File

@@ -0,0 +1,16 @@
@echo off
setlocal
set SCRIPT_DIR=%~dp0
set PROJECT_ROOT=%SCRIPT_DIR%..
set VENV_PY=%PROJECT_ROOT%\.venv\Scripts\python.exe
set PYTHONPATH=%PROJECT_ROOT%\aide-program;%PYTHONPATH%
if not exist "%VENV_PY%" (
echo ✗ 未找到虚拟环境请先运行uv venv .venv ^&^& uv pip install -r requirements.txt
exit /b 1
)
pushd "%PROJECT_ROOT%"
"%VENV_PY%" -m aide %*
popd

15
aide-program/aide.sh Executable file
View File

@@ -0,0 +1,15 @@
#!/usr/bin/env bash
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
VENV_PY="${PROJECT_ROOT}/.venv/bin/python"
if [ ! -x "$VENV_PY" ]; then
echo "✗ 未找到虚拟环境请先运行uv venv .venv && uv pip install -r requirements.txt" >&2
exit 1
fi
cd "$PROJECT_ROOT"
export PYTHONPATH="${PROJECT_ROOT}/aide-program${PYTHONPATH:+:$PYTHONPATH}"
exec "$VENV_PY" -m aide "$@"

View File

@@ -0,0 +1,5 @@
"""
Aide 命令行工具包。
当前版本聚焦基础环境检测与配置管理flow/decide 功能尚未实现。
"""

View File

@@ -0,0 +1,5 @@
from aide.main import main
if __name__ == "__main__":
main()

View File

@@ -0,0 +1 @@
# 核心基础模块(配置与输出)

View File

@@ -0,0 +1,109 @@
"""配置管理:生成默认配置、读取/写入配置、维护 .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 默认配置(由 aide init 生成)
# runtime: aide 自身运行要求
# task: 任务文档路径
# env: 虚拟环境与依赖配置
# flow: 环节名称列表,供流程校验使用
[runtime]
python_min = "3.11"
use_uv = true
[task]
source = "task-now.md"
spec = "task-spec.md"
[env]
venv = ".venv"
requirements = "requirements.txt"
[flow]
phases = ["task-optimize", "flow-design", "impl", "verify", "docs", "finish"]
"""
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_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

View File

@@ -0,0 +1,24 @@
"""输出格式工具,统一使用前缀符号。"""
def ok(message: str) -> None:
print(f"{message}")
def warn(message: str) -> None:
print(f"{message}")
def err(message: str) -> None:
print(f"{message}")
def info(message: str) -> None:
print(f"{message}")
def step(message: str, current: int | None = None, total: int | None = None) -> None:
if current is not None and total is not None:
print(f"[{current}/{total}] {message}")
else:
print(f"[·] {message}")

1
aide-program/aide/env/__init__.py vendored Normal file
View File

@@ -0,0 +1 @@
# 环境检测相关模块

119
aide-program/aide/env/ensure.py vendored Normal file
View File

@@ -0,0 +1,119 @@
"""环境检测与修复逻辑。"""
from __future__ import annotations
import platform
import subprocess
import sys
from pathlib import Path
from aide.core import output
from aide.core.config import ConfigManager
class EnvManager:
def __init__(self, root: Path):
self.root = root
def ensure(self, runtime_only: bool, cfg: ConfigManager) -> bool:
"""运行环境检测入口。"""
required_py = self._get_required_python(cfg, runtime_only)
if not self._check_python_version(required_py):
return False
uv_version = self._check_uv()
if uv_version is None:
return False
if runtime_only:
output.ok(f"运行时环境就绪 (python:{platform.python_version()}, uv:{uv_version})")
return True
config = cfg.ensure_config()
cfg.ensure_gitignore()
env_config = config.get("env", {})
venv_path = self.root / env_config.get("venv", ".venv")
req_path = self.root / env_config.get("requirements", "requirements.txt")
self._ensure_requirements_file(req_path)
if not self._ensure_venv(venv_path):
return False
if not self._install_requirements(venv_path, req_path):
return False
task_config = config.get("task", {})
output.info(f"任务原文档: {task_config.get('source', 'task-now.md')}")
output.info(f"任务细则文档: {task_config.get('spec', 'task-spec.md')}")
output.ok(f"环境就绪 (python:{platform.python_version()}, uv:{uv_version}, venv:{venv_path})")
return True
@staticmethod
def _get_required_python(cfg: ConfigManager, runtime_only: bool) -> str:
if runtime_only:
return "3.11"
data = cfg.load_config()
runtime = data.get("runtime", {})
return str(runtime.get("python_min", "3.11"))
@staticmethod
def _parse_version(version: str) -> tuple[int, ...]:
parts = []
for part in version.split("."):
try:
parts.append(int(part))
except ValueError:
break
return tuple(parts)
def _check_python_version(self, required: str) -> bool:
current = self._parse_version(platform.python_version())
target = self._parse_version(required)
if current >= target:
return True
output.err(f"Python 版本不足,要求>={required},当前 {platform.python_version()}")
return False
def _check_uv(self) -> str | None:
try:
result = subprocess.run(
["uv", "--version"],
check=True,
capture_output=True,
text=True,
)
return result.stdout.strip()
except (subprocess.CalledProcessError, FileNotFoundError) as exc:
output.err(f"未检测到 uv请先安装{exc}")
return None
def _ensure_venv(self, venv_path: Path) -> bool:
if venv_path.exists():
return True
output.info(f"创建虚拟环境: {venv_path}")
try:
subprocess.run(["uv", "venv", str(venv_path)], check=True)
output.ok("已创建虚拟环境")
return True
except subprocess.CalledProcessError as exc:
output.err(f"创建虚拟环境失败: {exc}")
return False
@staticmethod
def _ensure_requirements_file(req_path: Path) -> None:
if req_path.exists():
return
req_path.write_text("# 在此添加依赖\n", encoding="utf-8")
output.warn(f"未找到 {req_path.name},已创建空文件")
def _install_requirements(self, venv_path: Path, req_path: Path) -> bool:
if not req_path.exists():
output.err(f"缺少 {req_path}")
return False
cmd = ["uv", "pip", "install", "-r", str(req_path), "--python", str(venv_path)]
output.info("安装依赖uv pip install -r requirements.txt")
try:
subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
return True
except subprocess.CalledProcessError as exc:
output.err(f"安装依赖失败: {exc}")
return False

110
aide-program/aide/main.py Normal file
View File

@@ -0,0 +1,110 @@
"""aide 命令行入口。"""
from __future__ import annotations
import argparse
import sys
from pathlib import Path
from typing import Any
from aide.core import output
from aide.core.config import ConfigManager
from aide.env.ensure import EnvManager
def main(argv: list[str] | None = None) -> int:
parser = build_parser()
args = parser.parse_args(argv)
if not hasattr(args, "func"):
parser.print_help()
return 0
try:
result = args.func(args)
except KeyboardInterrupt:
output.err("操作已取消")
return 1
if result is False:
return 1
return 0
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(prog="aide", description="Aide 工作流辅助工具")
subparsers = parser.add_subparsers(dest="command")
init_parser = subparsers.add_parser("init", help="初始化 .aide 目录与默认配置")
init_parser.set_defaults(func=handle_init)
env_parser = subparsers.add_parser("env", help="环境管理")
env_sub = env_parser.add_subparsers(dest="env_command")
ensure_parser = env_sub.add_parser("ensure", help="检测并修复运行环境")
ensure_parser.add_argument("--runtime", action="store_true", help="仅检查 aide 运行时环境")
ensure_parser.set_defaults(func=handle_env_ensure)
config_parser = subparsers.add_parser("config", help="配置管理")
config_sub = config_parser.add_subparsers(dest="config_command")
get_parser = config_sub.add_parser("get", help="读取配置值")
get_parser.add_argument("key", help="使用点号分隔的键名,如 task.source")
get_parser.set_defaults(func=handle_config_get)
set_parser = config_sub.add_parser("set", help="设置配置值")
set_parser.add_argument("key", help="使用点号分隔的键名,如 task.source")
set_parser.add_argument("value", help="要写入的值,支持 bool/int/float/字符串")
set_parser.set_defaults(func=handle_config_set)
parser.add_argument("--version", action="version", version="aide dev")
return parser
def handle_init(args: argparse.Namespace) -> bool:
root = Path.cwd()
cfg = ConfigManager(root)
cfg.ensure_config()
cfg.ensure_gitignore()
output.ok("初始化完成,.aide/ 与默认配置已准备就绪")
return True
def handle_env_ensure(args: argparse.Namespace) -> bool:
if args.env_command != "ensure":
output.err("请指定 env 子命令,如: aide env ensure")
return False
root = Path.cwd()
cfg = ConfigManager(root)
manager = EnvManager(root)
return manager.ensure(runtime_only=args.runtime, cfg=cfg)
def handle_config_get(args: argparse.Namespace) -> bool:
root = Path.cwd()
cfg = ConfigManager(root)
value = cfg.get_value(args.key)
if value is None:
output.warn(f"未找到配置项 {args.key}")
return False
output.info(f"{args.key} = {value!r}")
return True
def handle_config_set(args: argparse.Namespace) -> bool:
root = Path.cwd()
cfg = ConfigManager(root)
parsed_value = _parse_value(args.value)
cfg.set_value(args.key, parsed_value)
return True
def _parse_value(raw: str) -> Any:
lowered = raw.lower()
if lowered in {"true", "false"}:
return lowered == "true"
try:
if "." in raw:
return float(raw)
return int(raw)
except ValueError:
return raw
if __name__ == "__main__":
sys.exit(main())