✨ feat: 实现部分aide
This commit is contained in:
16
aide-program/aide.bat
Normal file
16
aide-program/aide.bat
Normal 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
15
aide-program/aide.sh
Executable 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 "$@"
|
||||
5
aide-program/aide/__init__.py
Normal file
5
aide-program/aide/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""
|
||||
Aide 命令行工具包。
|
||||
|
||||
当前版本聚焦基础环境检测与配置管理,flow/decide 功能尚未实现。
|
||||
"""
|
||||
5
aide-program/aide/__main__.py
Normal file
5
aide-program/aide/__main__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from aide.main import main
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
1
aide-program/aide/core/__init__.py
Normal file
1
aide-program/aide/core/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# 核心基础模块(配置与输出)
|
||||
109
aide-program/aide/core/config.py
Normal file
109
aide-program/aide/core/config.py
Normal 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
|
||||
24
aide-program/aide/core/output.py
Normal file
24
aide-program/aide/core/output.py
Normal 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
1
aide-program/aide/env/__init__.py
vendored
Normal file
@@ -0,0 +1 @@
|
||||
# 环境检测相关模块
|
||||
119
aide-program/aide/env/ensure.py
vendored
Normal file
119
aide-program/aide/env/ensure.py
vendored
Normal 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
110
aide-program/aide/main.py
Normal 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())
|
||||
Reference in New Issue
Block a user