AirPlan V2 initial release — unified scheduler with 12 sub-modes
Merges 8 V1 plugins into 1 unified plugin, adds 4 new components (dep, tst, sec, rvr). 12 sub-modes: arc, eng, do, dbg, xdb, sdb, ndb, ctx, dep, tst, sec, rvr. L1 code-level guarantees: atomic writes, file locks, evidence gating, forced debug routing, 3-phase arc gate, evidence-first debug gate, Chinese language lock, scheduler boundary. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
121
lib/air_runtime/installer.py
Normal file
121
lib/air_runtime/installer.py
Normal file
@@ -0,0 +1,121 @@
|
||||
from __future__ import annotations
|
||||
import json
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def resolve_plugin_paths() -> dict[str, Path]:
|
||||
"""
|
||||
解析插件的实际安装路径。
|
||||
顺序: 1) 环境变量 AIRPLAN_HOME > 2) ~/.airplan > 3) 相对脚本位置推测
|
||||
"""
|
||||
# 1. 环境变量
|
||||
if "AIRPLAN_HOME" in __import__("os").environ:
|
||||
return {"root": Path(__import__("os").environ["AIRPLAN_HOME"])}
|
||||
|
||||
# 2. ~/.airplan 默认
|
||||
home = Path.home()
|
||||
default = home / ".airplan"
|
||||
if default.exists():
|
||||
return {"root": default}
|
||||
|
||||
# 3. 尝试从当前脚本位置推测
|
||||
# 脚本位于 {root}/scripts/airplan.py
|
||||
import sys
|
||||
script_root = Path(sys.argv[0]).resolve().parent if sys.argv else None
|
||||
if script_root and (script_root.name == "scripts"):
|
||||
plugin_root = script_root.parent
|
||||
if (plugin_root / ".claude-plugin").exists():
|
||||
return {"root": plugin_root}
|
||||
|
||||
# 4. 从本模块位置推测
|
||||
# 本模块位于 {root}/lib/air_runtime/installer.py
|
||||
module_root = Path(__file__).resolve().parent.parent.parent # lib/air_runtime/installer -> lib/air_runtime -> lib -> root
|
||||
if (module_root / ".claude-plugin").exists():
|
||||
return {"root": module_root}
|
||||
|
||||
raise FileNotFoundError("Cannot locate plugin installation directory")
|
||||
|
||||
|
||||
def get_plugin_meta() -> dict:
|
||||
"""读取 plugin.json 元数据"""
|
||||
paths = resolve_plugin_paths()
|
||||
meta_path = paths["root"] / ".claude-plugin" / "plugin.json"
|
||||
if not meta_path.exists():
|
||||
raise FileNotFoundError(f"plugin.json not found at {meta_path}")
|
||||
return json.loads(meta_path.read_text())
|
||||
|
||||
|
||||
def post_install_verify() -> dict:
|
||||
"""
|
||||
安装后验证:检查所有依赖工具是否存在。
|
||||
返回 {tool: bool} 映射,False 表示缺失。
|
||||
"""
|
||||
required_tools = [
|
||||
"git", # 版本控制
|
||||
"cmake", # 构建
|
||||
"ffmpeg", # XDB 截图
|
||||
"xvfb-run", # XDB 虚拟显示
|
||||
]
|
||||
|
||||
result = {"ok": True, "missing": []}
|
||||
for tool in required_tools:
|
||||
found = shutil.which(tool) is not None
|
||||
result[tool] = found
|
||||
if not found:
|
||||
result["ok"] = False
|
||||
result["missing"].append(tool)
|
||||
|
||||
# 检查 Python 依赖
|
||||
try:
|
||||
import yaml
|
||||
result["pyyaml"] = True
|
||||
except ImportError:
|
||||
result["pyyaml"] = False
|
||||
result["ok"] = False
|
||||
result["missing"].append("pyyaml")
|
||||
|
||||
# 检查目录结构
|
||||
paths = resolve_plugin_paths()
|
||||
for subdir in ["lib", "scripts", "skills", "commands"]:
|
||||
p = paths["root"] / subdir
|
||||
result[f"dir_{subdir}"] = p.exists()
|
||||
if not p.exists():
|
||||
result["ok"] = False
|
||||
result["missing"].append(f"dir:{subdir}")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def verify_plugin_json_paths() -> dict:
|
||||
"""
|
||||
验证 plugin.json 里的路径是否可解析。
|
||||
设计原文 P0-9 指出硬编码 $HOME 是问题,这里修复它。
|
||||
"""
|
||||
meta = get_plugin_meta()
|
||||
issues = []
|
||||
|
||||
# 检查 scripts 路径
|
||||
entry = meta.get("entry", {})
|
||||
if isinstance(entry, dict):
|
||||
script_path_str = entry.get("args", [""])[0] if entry.get("args") else ""
|
||||
else:
|
||||
script_path_str = str(entry)
|
||||
if "$HOME" in script_path_str:
|
||||
# 尝试解析
|
||||
resolved = script_path_str.replace("$HOME", str(Path.home()))
|
||||
if not Path(resolved).exists():
|
||||
issues.append(f"script path not found: {resolved}")
|
||||
else:
|
||||
# 修复:改用相对路径或 AIRPLAN_HOME 变量
|
||||
issues.append(f"script uses $HOME: {script_path_str} (should use relative path)")
|
||||
|
||||
# 检查 marketplace 路径
|
||||
market_path = meta.get("marketplace", "")
|
||||
if "$HOME" in market_path:
|
||||
issues.append(f"marketplace uses $HOME: {market_path}")
|
||||
|
||||
return {
|
||||
"issues": issues,
|
||||
"resolved_paths": resolve_plugin_paths(),
|
||||
}
|
||||
Reference in New Issue
Block a user