Includes AirPlan design documents, AircOding-alpha1-plan, AirPlanV2, AirPlan-ParaV2, AirPlan-Para V1 reference docs, and all working code changes across packages. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
319 lines
13 KiB
Python
Executable File
319 lines
13 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Bootstrap AirSDB static-analysis context and cppcheck tooling."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import platform
|
|
import shutil
|
|
import subprocess
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
from typing import Dict, Optional, Tuple
|
|
|
|
MARKER_BEGIN = "<!-- AIRSDB:BEGIN -->"
|
|
MARKER_END = "<!-- AIRSDB:END -->"
|
|
|
|
|
|
def now_iso() -> str:
|
|
return datetime.now(timezone.utc).isoformat()
|
|
|
|
|
|
def stamp() -> str:
|
|
return datetime.now().strftime("%Y%m%d-%H%M%S")
|
|
|
|
|
|
def read_env(path: Path) -> Dict[str, str]:
|
|
vals: Dict[str, str] = {}
|
|
if path.exists():
|
|
for raw in path.read_text(encoding="utf-8-sig").splitlines():
|
|
line = raw.strip()
|
|
if line and not line.startswith("#") and "=" in line:
|
|
key, value = line.split("=", 1)
|
|
vals[key.strip()] = value.strip().strip('"').strip("'")
|
|
for key in ["AIRSDB_CPPCHECK", "AIRSDB_CPPCHECK_OPTIONS"]:
|
|
if os.environ.get(key):
|
|
vals[key] = os.environ[key]
|
|
return vals
|
|
|
|
|
|
def write_env(path: Path, updates: Dict[str, str]) -> None:
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
old = path.read_text(encoding="utf-8") if path.exists() else ""
|
|
keys = set(updates)
|
|
lines = []
|
|
for raw in old.splitlines():
|
|
key = raw.split("=", 1)[0].strip() if "=" in raw and not raw.strip().startswith("#") else None
|
|
if key not in keys:
|
|
lines.append(raw)
|
|
if lines and lines[-1].strip():
|
|
lines.append("")
|
|
for key, value in updates.items():
|
|
if value:
|
|
lines.append(f"{key}={value}")
|
|
path.write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8", newline="\n")
|
|
|
|
|
|
def ensure_files(project: Path) -> None:
|
|
air = project / "AirPlan" / "state" / "airsdb"
|
|
air.mkdir(parents=True, exist_ok=True)
|
|
example = air / "tool.env.example"
|
|
if not example.exists():
|
|
example.write_text(
|
|
"# AirSDB local cppcheck configuration\n"
|
|
"AIRSDB_CPPCHECK=cppcheck\n"
|
|
"AIRSDB_CPPCHECK_OPTIONS=\n"
|
|
"# AirSDB defaults to --check-level=exhaustive for maximum branch analysis detail.\n",
|
|
encoding="utf-8",
|
|
newline="\n",
|
|
)
|
|
gitignore = air / ".gitignore"
|
|
old = gitignore.read_text(encoding="utf-8") if gitignore.exists() else ""
|
|
for item in ["tool.env", "remote-device.env", "reports/", "cppcheck-build/"]:
|
|
if item not in old.splitlines():
|
|
old = (old.rstrip() + f"\n{item}\n").lstrip()
|
|
gitignore.write_text(old, encoding="utf-8", newline="\n")
|
|
|
|
static = project / "AirPlan" / "docs" / "staticanalysis.md"
|
|
if not static.exists():
|
|
static.write_text(
|
|
"# Static Analysis\n\n"
|
|
"Short AI-context reports for AirSDB cppcheck runs. Keep entries concise.\n\n"
|
|
"## Entry Template\n\n"
|
|
"### YYYY-MM-DD HH:MM:SS: AirSDB cppcheck\n\n"
|
|
"- Target: TODO\n"
|
|
"- Tool: TODO\n"
|
|
"- Command: TODO\n"
|
|
"- Result: TODO\n"
|
|
"- Counts: TODO\n"
|
|
"- Reports: TODO\n"
|
|
"- Top findings: TODO\n"
|
|
"- AirDbg/AirDo handoff: TODO\n"
|
|
"- Residual risk: TODO\n",
|
|
encoding="utf-8",
|
|
newline="\n",
|
|
)
|
|
|
|
|
|
def airsdb_agents_block() -> str:
|
|
return f"""{MARKER_BEGIN}
|
|
## AirSDB Static Analysis Workflow
|
|
|
|
1. Use AirSDB for `/airsdb` sessions that run cppcheck static analysis for C/C++ code quality, security-relevant defects, and debugging evidence.
|
|
2. On first entry, detect cppcheck; if missing, auto-configure it with the best available package manager or stop with install instructions.
|
|
3. Maintain `AirPlan/docs/staticanalysis.md` as the short AI-context report for AirDbg and AirDo. Keep detailed XML/JSON artifacts under `AirPlan/state/airsdb/reports/`.
|
|
4. Prefer `compile_commands.json` when available; otherwise scan the narrowest useful source tree with common generated/vendor directories excluded.
|
|
5. Default local and remote cppcheck scans to `--check-level=exhaustive` so AirSDB provides the most detailed branch-analysis evidence it can.
|
|
6. For local analysis, use `airsdb_cppcheck.py`; for remote targets over SSH, use `airsdb_remote_device.py` before local analysis.
|
|
7. When remote cppcheck is missing, the remote helper may auto-configure it with non-interactive package-manager commands; if sudo/password or unsupported OS blocks it, stop and record the blocker.
|
|
8. Record command, target, report paths, counts, top findings, AirDbg/AirDo handoff, and residual risk in `AirPlan/docs/staticanalysis.md`.
|
|
9. Update ADR/C4 only when static analysis tooling becomes a durable project boundary or changes implementation decisions.
|
|
{MARKER_END}
|
|
"""
|
|
|
|
|
|
def upsert_agents_md(path: Path) -> str:
|
|
block = airsdb_agents_block().rstrip() + "\n"
|
|
if path.exists():
|
|
original = path.read_text(encoding="utf-8")
|
|
existed = True
|
|
else:
|
|
original = "# AGENTS.md\n\n"
|
|
existed = False
|
|
begin = original.find(MARKER_BEGIN)
|
|
end = original.find(MARKER_END)
|
|
if begin >= 0 and end > begin:
|
|
end += len(MARKER_END)
|
|
updated = original[:begin].rstrip() + "\n\n" + block + original[end:].lstrip()
|
|
status = "updated"
|
|
else:
|
|
updated = original.rstrip() + "\n\n" + block
|
|
status = "updated" if existed else "created"
|
|
path.write_text(updated, encoding="utf-8", newline="\n")
|
|
return status
|
|
|
|
|
|
def run_args(args: list[str], timeout: int = 600) -> subprocess.CompletedProcess[str]:
|
|
return subprocess.run(args, capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=timeout)
|
|
|
|
|
|
def run_shell(cmd: str, timeout: int = 600) -> subprocess.CompletedProcess[str]:
|
|
return subprocess.run(cmd, shell=True, capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=timeout)
|
|
|
|
|
|
def cppcheck_version(tool: str) -> str:
|
|
try:
|
|
result = run_args([tool, "--version"], timeout=20)
|
|
except Exception:
|
|
return ""
|
|
text = (result.stdout or result.stderr).strip()
|
|
return text.splitlines()[0] if text else ""
|
|
|
|
|
|
def detect_cppcheck(project: Path) -> Tuple[Optional[str], str]:
|
|
env = read_env(project / "AirPlan" / "state" / "airsdb" / "tool.env")
|
|
candidates = []
|
|
configured = env.get("AIRSDB_CPPCHECK", "").strip()
|
|
if configured:
|
|
candidates.append(configured)
|
|
candidates.append("cppcheck")
|
|
if platform.system().lower() == "windows":
|
|
candidates.extend(
|
|
[
|
|
r"C:\Program Files\Cppcheck\cppcheck.exe",
|
|
r"C:\Program Files (x86)\Cppcheck\cppcheck.exe",
|
|
]
|
|
)
|
|
for candidate in candidates:
|
|
path = shutil.which(candidate) or candidate
|
|
if Path(path).exists() or shutil.which(path):
|
|
version = cppcheck_version(path)
|
|
if version:
|
|
return path, version
|
|
return None, ""
|
|
|
|
|
|
def install_cppcheck(project: Path) -> Dict[str, str]:
|
|
system = platform.system().lower()
|
|
attempts = []
|
|
if system == "windows":
|
|
package_commands = [
|
|
["winget", "install", "--id", "Cppcheck.Cppcheck", "-e", "--accept-source-agreements", "--accept-package-agreements", "--silent"],
|
|
["choco", "install", "cppcheck", "-y"],
|
|
["scoop", "install", "cppcheck"],
|
|
]
|
|
for cmd in package_commands:
|
|
exe = shutil.which(cmd[0])
|
|
if not exe:
|
|
continue
|
|
run = run_args([exe] + cmd[1:], timeout=1200)
|
|
attempts.append({"command": " ".join(cmd), "returncode": str(run.returncode)})
|
|
tool, version = detect_cppcheck(project)
|
|
if tool:
|
|
write_env(project / "AirPlan" / "state" / "airsdb" / "tool.env", {"AIRSDB_CPPCHECK": tool})
|
|
return {"status": "ok", "tool": tool, "version": version, "installer": cmd[0], "attempts": json.dumps(attempts)}
|
|
return {
|
|
"status": "blocked",
|
|
"reason": "cppcheck_not_found",
|
|
"hint": "Install Cppcheck from https://cppcheck.sourceforge.io/ or configure AIRSDB_CPPCHECK.",
|
|
"attempts": json.dumps(attempts),
|
|
}
|
|
|
|
commands = [
|
|
"if command -v apt-get >/dev/null 2>&1; then sudo -n apt-get update && sudo -n apt-get install -y cppcheck; "
|
|
"elif command -v dnf >/dev/null 2>&1; then sudo -n dnf install -y cppcheck; "
|
|
"elif command -v yum >/dev/null 2>&1; then sudo -n yum install -y cppcheck; "
|
|
"elif command -v apk >/dev/null 2>&1; then sudo -n apk add cppcheck; "
|
|
"elif command -v pacman >/dev/null 2>&1; then sudo -n pacman -Sy --noconfirm cppcheck; "
|
|
"elif command -v brew >/dev/null 2>&1; then brew install cppcheck; "
|
|
"elif command -v port >/dev/null 2>&1; then sudo -n port install cppcheck; "
|
|
"else exit 42; fi"
|
|
]
|
|
run = run_shell(commands[0], timeout=1200)
|
|
attempts.append({"command": "system-package-manager", "returncode": str(run.returncode)})
|
|
tool, version = detect_cppcheck(project)
|
|
if tool:
|
|
write_env(project / "AirPlan" / "state" / "airsdb" / "tool.env", {"AIRSDB_CPPCHECK": tool})
|
|
return {"status": "ok", "tool": tool, "version": version, "installer": "system-package-manager", "attempts": json.dumps(attempts)}
|
|
return {
|
|
"status": "blocked",
|
|
"reason": "cppcheck_not_found",
|
|
"hint": "Install cppcheck with the OS package manager or configure AIRSDB_CPPCHECK.",
|
|
"attempts": json.dumps(attempts),
|
|
}
|
|
|
|
|
|
def setup_cppcheck(project: Path, auto: bool = True) -> Dict[str, str]:
|
|
ensure_files(project)
|
|
tool, version = detect_cppcheck(project)
|
|
if tool:
|
|
write_env(project / "AirPlan" / "state" / "airsdb" / "tool.env", {"AIRSDB_CPPCHECK": tool})
|
|
return {"status": "ok", "tool": tool, "version": version, "autoConfigure": "skipped"}
|
|
if not auto:
|
|
return {"status": "blocked", "reason": "cppcheck_not_found", "example": str(project / "AirPlan" / "state" / "airsdb" / "tool.env.example")}
|
|
return install_cppcheck(project)
|
|
|
|
|
|
def artifact_map(project: Path) -> Dict[str, Path]:
|
|
return {
|
|
"AGENTS.md": project / "AirPlan" / "AGENTS.md",
|
|
"staticanalysis": project / "AirPlan" / "docs" / "staticanalysis.md",
|
|
"state": project / "AirPlan" / "state" / "airsdb" / "state.json",
|
|
"tool_env": project / "AirPlan" / "state" / "airsdb" / "tool.env",
|
|
}
|
|
|
|
|
|
def write_state(path: Path, enabled: bool, project: Path, tool_info: Dict[str, str]) -> None:
|
|
artifacts = artifact_map(project)
|
|
payload = {
|
|
"enabled": enabled,
|
|
"updatedAt": now_iso(),
|
|
"projectRoot": str(project),
|
|
"cppcheck": tool_info,
|
|
"artifactHealth": {name: p.exists() for name, p in artifacts.items() if name != "state"},
|
|
}
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8", newline="\n")
|
|
|
|
|
|
def enter_mode(project: Path, auto: bool = True) -> Tuple[str, Dict[str, str]]:
|
|
ensure_files(project)
|
|
results = {"AGENTS.md": upsert_agents_md(project / "AirPlan" / "AGENTS.md"), "staticanalysis": "exists"}
|
|
tool_info = setup_cppcheck(project, auto=auto)
|
|
for key, value in tool_info.items():
|
|
results[f"cppcheck_{key}"] = value
|
|
write_state(project / "AirPlan" / "state" / "airsdb" / "state.json", True, project, tool_info)
|
|
return "enabled", results
|
|
|
|
|
|
def exit_mode(project: Path) -> Tuple[str, Dict[str, str]]:
|
|
ensure_files(project)
|
|
tool_info = setup_cppcheck(project, auto=False)
|
|
write_state(project / "AirPlan" / "state" / "airsdb" / "state.json", False, project, tool_info)
|
|
return "disabled", {}
|
|
|
|
|
|
def status_mode(project: Path) -> Tuple[str, Dict[str, str]]:
|
|
ensure_files(project)
|
|
state = project / "AirPlan" / "state" / "airsdb" / "state.json"
|
|
enabled = False
|
|
if state.exists():
|
|
try:
|
|
enabled = bool(json.loads(state.read_text(encoding="utf-8")).get("enabled"))
|
|
except json.JSONDecodeError:
|
|
enabled = False
|
|
tool_info = setup_cppcheck(project, auto=False)
|
|
results = {name: ("ok" if path.exists() else "missing") for name, path in artifact_map(project).items() if name != "state"}
|
|
for key, value in tool_info.items():
|
|
results[f"cppcheck_{key}"] = value
|
|
return ("enabled" if enabled else "disabled"), results
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(description="Manage AirSDB static-analysis artifacts.")
|
|
parser.add_argument("--mode", choices=["enter", "setup", "exit", "status"], default="enter")
|
|
parser.add_argument("--project", default=".")
|
|
parser.add_argument("--no-auto-configure", action="store_true")
|
|
return parser.parse_args()
|
|
|
|
|
|
def main() -> None:
|
|
args = parse_args()
|
|
project = Path(args.project).expanduser().resolve()
|
|
if args.mode in ["enter", "setup"]:
|
|
mode_state, result = enter_mode(project, auto=not args.no_auto_configure)
|
|
elif args.mode == "exit":
|
|
mode_state, result = exit_mode(project)
|
|
else:
|
|
mode_state, result = status_mode(project)
|
|
print(f"airsdb_mode={mode_state}")
|
|
print(f"project_root={project}")
|
|
for key, value in result.items():
|
|
print(f"{key}={value}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|