mirror of
https://github.com/aljazceru/Auditor.git
synced 2025-12-17 03:24:18 +01:00
273 lines
8.6 KiB
Python
273 lines
8.6 KiB
Python
"""Claude Code integration setup - Zero-optional bulletproof installer."""
|
|
|
|
import hashlib
|
|
import json
|
|
import platform
|
|
import shutil
|
|
import stat
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Dict, List, Optional
|
|
|
|
from .venv_install import setup_project_venv, find_theauditor_root
|
|
|
|
# Detect if running on Windows for character encoding
|
|
IS_WINDOWS = platform.system() == "Windows"
|
|
|
|
|
|
def write_file_atomic(path: Path, content: str, executable: bool = False) -> str:
|
|
"""
|
|
Write file atomically with backup if content differs.
|
|
|
|
Args:
|
|
path: File path to write
|
|
content: Content to write
|
|
executable: Make file executable (Unix only)
|
|
|
|
Returns:
|
|
"created" if new file
|
|
"updated" if file changed (creates .bak)
|
|
"skipped" if identical content
|
|
"""
|
|
# Ensure parent directory exists
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
if path.exists():
|
|
existing = path.read_text(encoding='utf-8')
|
|
if existing == content:
|
|
return "skipped"
|
|
|
|
# Create backup (only once per unique content)
|
|
bak_path = path.with_suffix(path.suffix + ".bak")
|
|
if not bak_path.exists():
|
|
shutil.copy2(path, bak_path)
|
|
|
|
path.write_text(content, encoding='utf-8')
|
|
status = "updated"
|
|
else:
|
|
path.write_text(content, encoding='utf-8')
|
|
status = "created"
|
|
|
|
# Set executable if needed
|
|
if executable and platform.system() != "Windows":
|
|
st = path.stat()
|
|
path.chmod(st.st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)
|
|
|
|
return status
|
|
|
|
|
|
class WrapperTemplates:
|
|
"""Cross-platform wrapper script templates."""
|
|
|
|
POSIX_WRAPPER = '''#!/usr/bin/env bash
|
|
# Auto-generated wrapper for project-local aud
|
|
PROJ_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
|
VENV="$PROJ_ROOT/.auditor_venv/bin/aud"
|
|
if [ -x "$VENV" ]; then
|
|
exec "$VENV" "$@"
|
|
fi
|
|
# Fallback to module execution
|
|
exec "$PROJ_ROOT/.auditor_venv/bin/python" -m theauditor.cli "$@"
|
|
'''
|
|
|
|
POWERSHELL_WRAPPER = r'''# Auto-generated wrapper for project-local aud
|
|
$proj = Split-Path -Path (Split-Path -Parent $MyInvocation.MyCommand.Path) -Parent
|
|
$aud = Join-Path $proj ".auditor_venv\Scripts\aud.exe"
|
|
if (Test-Path $aud) {
|
|
& $aud @args
|
|
exit $LASTEXITCODE
|
|
}
|
|
# Fallback to module execution
|
|
$python = Join-Path $proj ".auditor_venv\Scripts\python.exe"
|
|
& $python "-m" "theauditor.cli" @args
|
|
exit $LASTEXITCODE
|
|
'''
|
|
|
|
CMD_WRAPPER = r'''@echo off
|
|
REM Auto-generated wrapper for project-local aud
|
|
set PROJ=%~dp0..\..
|
|
if exist "%PROJ%\.auditor_venv\Scripts\aud.exe" (
|
|
"%PROJ%\.auditor_venv\Scripts\aud.exe" %*
|
|
exit /b %ERRORLEVEL%
|
|
)
|
|
REM Fallback to module execution
|
|
"%PROJ%\.auditor_venv\Scripts\python.exe" -m theauditor.cli %*
|
|
exit /b %ERRORLEVEL%
|
|
'''
|
|
|
|
|
|
def create_wrappers(target_dir: Path) -> Dict[str, str]:
|
|
"""
|
|
Create cross-platform wrapper scripts.
|
|
|
|
Args:
|
|
target_dir: Project root directory
|
|
|
|
Returns:
|
|
Dict mapping wrapper paths to their status
|
|
"""
|
|
wrappers_dir = target_dir / ".claude" / "bin"
|
|
results = {}
|
|
|
|
# POSIX wrapper (bash)
|
|
posix_wrapper = wrappers_dir / "aud"
|
|
status = write_file_atomic(posix_wrapper, WrapperTemplates.POSIX_WRAPPER, executable=True)
|
|
results[str(posix_wrapper)] = status
|
|
|
|
# PowerShell wrapper
|
|
ps_wrapper = wrappers_dir / "aud.ps1"
|
|
status = write_file_atomic(ps_wrapper, WrapperTemplates.POWERSHELL_WRAPPER)
|
|
results[str(ps_wrapper)] = status
|
|
|
|
# CMD wrapper
|
|
cmd_wrapper = wrappers_dir / "aud.cmd"
|
|
status = write_file_atomic(cmd_wrapper, WrapperTemplates.CMD_WRAPPER)
|
|
results[str(cmd_wrapper)] = status
|
|
|
|
return results
|
|
|
|
|
|
def copy_agent_templates(source_dir: Path, target_dir: Path) -> Dict[str, str]:
|
|
"""
|
|
Copy all .md agent template files directly to target/.claude/agents/.
|
|
|
|
Args:
|
|
source_dir: Directory containing agent template .md files
|
|
target_dir: Project root directory
|
|
|
|
Returns:
|
|
Dict mapping agent paths to their status
|
|
"""
|
|
agents_dir = target_dir / ".claude" / "agents"
|
|
agents_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
results = {}
|
|
|
|
# Find all .md files in source directory
|
|
for md_file in source_dir.glob("*.md"):
|
|
if md_file.is_file():
|
|
# Read content
|
|
content = md_file.read_text(encoding='utf-8')
|
|
|
|
# Write to target
|
|
target_file = agents_dir / md_file.name
|
|
status = write_file_atomic(target_file, content)
|
|
results[str(target_file)] = status
|
|
|
|
return results
|
|
|
|
|
|
def setup_claude_complete(
|
|
target: str,
|
|
source: str = "agent_templates",
|
|
sync: bool = False,
|
|
dry_run: bool = False
|
|
) -> Dict[str, List[str]]:
|
|
"""
|
|
Complete Claude setup: venv, wrappers, hooks, and agents.
|
|
|
|
Args:
|
|
target: Target project root (absolute or relative path)
|
|
source: Path to TheAuditor agent templates directory
|
|
sync: Force update (still creates .bak on first change)
|
|
dry_run: Print plan without executing
|
|
|
|
Returns:
|
|
Dict with created, updated, and skipped file lists
|
|
"""
|
|
# Resolve paths
|
|
target_dir = Path(target).resolve()
|
|
|
|
if not target_dir.exists():
|
|
raise ValueError(f"Target directory does not exist: {target_dir}")
|
|
|
|
# Find source docs
|
|
if Path(source).is_absolute():
|
|
source_dir = Path(source)
|
|
else:
|
|
theauditor_root = find_theauditor_root()
|
|
source_dir = theauditor_root / source
|
|
|
|
if not source_dir.exists():
|
|
raise ValueError(f"Source agent templates directory not found: {source_dir}")
|
|
|
|
print(f"\n{'='*60}")
|
|
print(f"Claude Setup - Zero-Optional Installation")
|
|
print(f"{'='*60}")
|
|
print(f"Target: {target_dir}")
|
|
print(f"Source: {source_dir}")
|
|
print(f"Mode: {'DRY RUN' if dry_run else 'EXECUTE'}")
|
|
print(f"{'='*60}\n")
|
|
|
|
if dry_run:
|
|
print("DRY RUN - Plan of operations:")
|
|
print(f"1. Create/verify venv at {target_dir}/.auditor_venv")
|
|
print(f"2. Install TheAuditor (editable) into venv")
|
|
print(f"3. Create wrappers at {target_dir}/.claude/bin/")
|
|
print(f"4. Copy agent templates from {source_dir}/*.md")
|
|
print(f"5. Write agents to {target_dir}/.claude/agents/")
|
|
print("\nNo files will be modified.")
|
|
return {"created": [], "updated": [], "skipped": []}
|
|
|
|
results = {
|
|
"created": [],
|
|
"updated": [],
|
|
"skipped": [],
|
|
"failed": []
|
|
}
|
|
|
|
# Step 1: Setup venv
|
|
print("Step 1: Setting up Python virtual environment...", flush=True)
|
|
try:
|
|
venv_path, success = setup_project_venv(target_dir, force=sync)
|
|
if success:
|
|
results["created"].append(str(venv_path))
|
|
else:
|
|
results["failed"].append(f"venv setup at {venv_path}")
|
|
print("ERROR: Failed to setup venv. Aborting.")
|
|
return results
|
|
except Exception as e:
|
|
print(f"ERROR setting up venv: {e}")
|
|
results["failed"].append("venv setup")
|
|
return results
|
|
|
|
# Step 2: Create wrappers
|
|
print("\nStep 2: Creating cross-platform wrappers...", flush=True)
|
|
wrapper_results = create_wrappers(target_dir)
|
|
for path, status in wrapper_results.items():
|
|
results[status].append(path)
|
|
|
|
# Step 3: Copy agent templates
|
|
print("\nStep 3: Copying agent templates...", flush=True)
|
|
try:
|
|
agent_results = copy_agent_templates(source_dir, target_dir)
|
|
for path, status in agent_results.items():
|
|
results[status].append(path)
|
|
|
|
if not agent_results:
|
|
print("WARNING: No .md files found in agent_templates directory")
|
|
|
|
except Exception as e:
|
|
print(f"ERROR copying agent templates: {e}")
|
|
results["failed"].append("agent template copy")
|
|
|
|
# Summary
|
|
print(f"\n{'='*60}")
|
|
print("Setup Complete - Summary:")
|
|
print(f"{'='*60}")
|
|
print(f"Created: {len(results['created'])} files")
|
|
print(f"Updated: {len(results['updated'])} files")
|
|
print(f"Skipped: {len(results['skipped'])} files (unchanged)")
|
|
|
|
if results['failed']:
|
|
print(f"FAILED: {len(results['failed'])} operations")
|
|
for item in results['failed']:
|
|
print(f" - {item}")
|
|
|
|
check_mark = "[OK]" if IS_WINDOWS else "✓"
|
|
print(f"\n{check_mark} Project configured at: {target_dir}")
|
|
print(f"{check_mark} Wrapper available at: {target_dir}/.claude/bin/aud")
|
|
print(f"{check_mark} Agents installed to: {target_dir}/.claude/agents/")
|
|
print(f"{check_mark} Professional linters installed (ruff, mypy, black, ESLint, etc.)")
|
|
|
|
return results |