#!/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 = "" MARKER_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()