Files
kaizen-agentic/src/kaizen_agentic/installer.py
tegwick d68310793b Fix linting violations for v1.0.0 release preparation
- Apply black formatting to all Python files
- Fix various flake8 violations in agent system code
- Clean up imports and whitespace issues

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-19 20:44:58 +02:00

626 lines
18 KiB
Python

"""Agent installation and management utilities."""
import shutil
import json
from pathlib import Path
from typing import List, Dict, Optional
from dataclasses import dataclass
from .registry import AgentRegistry
@dataclass
class InstallationConfig:
"""Configuration for agent installation."""
target_dir: Path
claude_config_path: Optional[Path] = None
makefile_path: Optional[Path] = None
update_docs: bool = True
create_backup: bool = True
class AgentInstaller:
"""Handles installation and management of agents in projects."""
def __init__(self, registry: AgentRegistry):
self.registry = registry
def install_agents(
self, agent_names: List[str], config: InstallationConfig
) -> Dict[str, str]:
"""Install agents into a project.
Returns:
Dict mapping agent names to installation status
"""
results = {}
# Resolve dependencies
resolved_agents = self.registry.resolve_dependencies(agent_names)
# Create target directory if it doesn't exist
agents_dir = config.target_dir / "agents"
agents_dir.mkdir(parents=True, exist_ok=True)
# Create backup if requested
if config.create_backup and agents_dir.exists():
self._create_backup(agents_dir)
# Install each agent
for agent_name in resolved_agents:
try:
agent = self.registry.get_agent(agent_name)
if not agent:
results[agent_name] = "ERROR: Agent not found"
continue
target_path = agents_dir / f"agent-{agent_name}.md"
shutil.copy2(agent.file_path, target_path)
results[agent_name] = "INSTALLED"
except Exception as e:
results[agent_name] = f"ERROR: {str(e)}"
# Update configuration files
if config.claude_config_path:
self._update_claude_config(resolved_agents, config.claude_config_path)
if config.makefile_path and config.makefile_path.exists():
self._update_makefile(resolved_agents, config.makefile_path)
if config.update_docs:
self._update_documentation(resolved_agents, config.target_dir)
return results
def list_installed_agents(self, project_dir: Path) -> List[str]:
"""List agents currently installed in a project."""
agents_dir = project_dir / "agents"
if not agents_dir.exists():
return []
installed = []
for agent_file in agents_dir.glob("agent-*.md"):
agent_name = agent_file.stem.replace("agent-", "")
installed.append(agent_name)
return sorted(installed)
def update_agents(
self, project_dir: Path, agent_names: Optional[List[str]] = None
) -> Dict[str, str]:
"""Update installed agents to latest versions."""
if agent_names is None:
agent_names = self.list_installed_agents(project_dir)
config = InstallationConfig(target_dir=project_dir)
return self.install_agents(agent_names, config)
def remove_agents(
self, agent_names: List[str], project_dir: Path
) -> Dict[str, str]:
"""Remove agents from a project."""
results = {}
agents_dir = project_dir / "agents"
for agent_name in agent_names:
try:
agent_file = agents_dir / f"agent-{agent_name}.md"
if agent_file.exists():
agent_file.unlink()
results[agent_name] = "REMOVED"
else:
results[agent_name] = "NOT_FOUND"
except Exception as e:
results[agent_name] = f"ERROR: {str(e)}"
return results
def validate_installation(self, project_dir: Path) -> Dict[str, List[str]]:
"""Validate agents installed in a project."""
errors = {}
agents_dir = project_dir / "agents"
if not agents_dir.exists():
return {"project": ["No agents directory found"]}
# Check each installed agent
for agent_file in agents_dir.glob("agent-*.md"):
agent_name = agent_file.stem.replace("agent-", "")
agent_errors = []
try:
# Check if agent exists in registry (validates the file)
agent_def = self.registry.get_agent(agent_name)
if not agent_def:
agent_errors.append("Agent not found in registry")
except Exception as e:
agent_errors.append(f"Invalid agent format: {str(e)}")
if agent_errors:
errors[agent_name] = agent_errors
return errors
def _create_backup(self, agents_dir: Path):
"""Create a backup of the existing agents directory."""
import datetime
import time
# Add microseconds to avoid collisions in tests
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
microseconds = int(time.time() * 1000000) % 1000000
backup_dir = agents_dir.parent / f"agents_backup_{timestamp}_{microseconds}"
# Ensure unique backup directory
counter = 0
while backup_dir.exists():
counter += 1
backup_dir = (
agents_dir.parent
/ f"agents_backup_{timestamp}_{microseconds}_{counter}"
)
shutil.copytree(agents_dir, backup_dir)
print(f"Created backup at: {backup_dir}")
def _update_claude_config(self, agent_names: List[str], config_path: Path):
"""Update Claude Code configuration with agent references."""
try:
# Read existing config
config = {}
if config_path.exists():
with open(config_path, "r") as f:
config = json.load(f)
# Ensure agents section exists
if "agents" not in config:
config["agents"] = {}
# Add agent references
for agent_name in agent_names:
config["agents"][agent_name] = {
"path": f"agents/agent-{agent_name}.md",
"enabled": True,
}
# Write updated config
with open(config_path, "w") as f:
json.dump(config, f, indent=2)
print(f"Updated Claude configuration: {config_path}")
except Exception as e:
print(f"Warning: Could not update Claude config: {e}")
def _update_makefile(self, agent_names: List[str], makefile_path: Path):
"""Update Makefile with agent-specific targets."""
try:
# Read existing Makefile
with open(makefile_path, "r") as f:
content = f.read()
# Add agent management targets if not present
agent_targets = """
# Agent Management Targets
agents-list:
\t@echo "Installed agents:"
\t@ls agents/ 2>/dev/null | grep agent- | sed 's/agent-//g' | sed 's/.md//g' \\
\t|| echo "No agents installed"
agents-update:
\t@echo "Updating agents..."
\t@kaizen-agentic update
agents-validate:
\t@echo "Validating agents..."
\t@kaizen-agentic validate agents/
"""
if "agents-list:" not in content:
content += agent_targets
# Write updated Makefile
with open(makefile_path, "w") as f:
f.write(content)
print(f"Updated Makefile: {makefile_path}")
except Exception as e:
print(f"Warning: Could not update Makefile: {e}")
def _update_documentation(self, agent_names: List[str], project_dir: Path):
"""Update project documentation with agent information."""
try:
claude_md = project_dir / "CLAUDE.md"
agent_section = "## Installed Agents\n\n"
agent_section += (
"This project includes the following specialized agents:\n\n"
)
# Group agents by category
categories = {}
for agent_name in agent_names:
agent = self.registry.get_agent(agent_name)
if agent:
category = agent.category.value
if category not in categories:
categories[category] = []
categories[category].append(agent)
# Generate documentation
for category, agents in categories.items():
agent_section += f"### {category.replace('-', ' ').title()}\n\n"
for agent in agents:
agent_section += f"- **{agent.name}**: {agent.description}\n"
agent_section += "\n"
agent_section += (
"Use these agents by referencing them in your "
"Claude Code interactions.\n\n"
)
# Update or create CLAUDE.md
if claude_md.exists():
with open(claude_md, "r") as f:
content = f.read()
# Replace existing agent section or append
if "## Installed Agents" in content:
import re
content = re.sub(
r"## Installed Agents.*?(?=##|\Z)",
agent_section,
content,
flags=re.DOTALL,
)
else:
content += "\n" + agent_section
with open(claude_md, "w") as f:
f.write(content)
else:
# Create new CLAUDE.md
header = "# Claude Code Configuration\n\n"
header += "This file contains Claude Code configuration and agent information.\n\n"
with open(claude_md, "w") as f:
f.write(header + agent_section)
print(f"Updated documentation: {claude_md}")
except Exception as e:
print(f"Warning: Could not update documentation: {e}")
class ProjectInitializer:
"""Initializes new projects with agents and templates."""
def __init__(self, registry: AgentRegistry):
self.registry = registry
def init_project(
self,
project_dir: Path,
template: str = "python-basic",
agent_names: Optional[List[str]] = None,
project_name: Optional[str] = None,
) -> Dict[str, str]:
"""Initialize a new project with agents and structure."""
results = {}
# Create project directory
project_dir.mkdir(parents=True, exist_ok=True)
# Get agents for template
if agent_names is None:
templates = self.registry.get_agent_templates()
agent_names = templates.get(template, templates["python-basic"])
# Set up project name
if project_name is None:
project_name = project_dir.name
# Install agents
config = InstallationConfig(
target_dir=project_dir,
claude_config_path=project_dir / "CLAUDE.md",
makefile_path=project_dir / "Makefile",
)
installer = AgentInstaller(self.registry)
install_results = installer.install_agents(agent_names, config)
results.update(install_results)
# Create basic project structure
self._create_project_structure(project_dir, project_name, template)
return results
def _create_project_structure(
self, project_dir: Path, project_name: str, template: str
):
"""Create basic project structure based on template."""
# Create directories
dirs_to_create = ["src", "tests", "docs"]
if template.startswith("python"):
dirs_to_create.extend(
[f"src/{project_name.replace('-', '_')}", ".github/workflows"]
)
for dir_name in dirs_to_create:
(project_dir / dir_name).mkdir(parents=True, exist_ok=True)
# Create basic files
self._create_gitignore(project_dir)
self._create_readme(project_dir, project_name)
self._create_makefile(project_dir, project_name)
if template.startswith("python"):
self._create_pyproject_toml(project_dir, project_name)
self._create_init_py(project_dir, project_name)
def _create_gitignore(self, project_dir: Path):
"""Create .gitignore file."""
gitignore_content = """# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# Virtual environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# IDEs
.vscode/
.idea/
*.swp
*.swo
*~
# Testing
.pytest_cache/
.coverage
htmlcov/
.tox/
"""
(project_dir / ".gitignore").write_text(gitignore_content)
def _create_readme(self, project_dir: Path, project_name: str):
"""Create README.md file."""
readme_content = f"""# {project_name}
A Python project following best practices with Kaizen Agentic agents.
## Setup
```bash
# Clone the repository
git clone <repository-url>
cd {project_name}
# Set up development environment
make setup-complete
# Activate virtual environment
source .venv/bin/activate
```
## Development
```bash
# Run tests
make test
# Check code quality
make lint
# Format code
make format
```
## Agents
This project uses Kaizen Agentic agents for development workflow automation.
See CLAUDE.md for agent details and usage.
"""
(project_dir / "README.md").write_text(readme_content)
def _create_pyproject_toml(self, project_dir: Path, project_name: str):
"""Create pyproject.toml file."""
pyproject_content = f"""[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "{project_name}"
version = "0.1.0"
description = "A Python project with Kaizen Agentic agents"
readme = "README.md"
requires-python = ">=3.9"
license = {{text = "MIT"}}
authors = [
{{name = "Author Name", email = "author@example.com"}}
]
dependencies = []
[project.optional-dependencies]
dev = [
"pytest>=7.0",
"black>=22.0",
"flake8>=5.0",
"mypy>=1.0",
]
# Note: Add kaizen-agentic dependency when published to PyPI
# dev = [..., "kaizen-agentic>=0.1.0"]
[tool.setuptools.packages.find]
where = ["src"]
[tool.black]
line-length = 88
target-version = ['py39']
[tool.flake8]
max-line-length = 100
exclude = [".git", "__pycache__", "build", "dist"]
[tool.mypy]
python_version = "3.9"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
"""
(project_dir / "pyproject.toml").write_text(pyproject_content)
def _create_init_py(self, project_dir: Path, project_name: str):
"""Create package __init__.py file."""
package_name = project_name.replace("-", "_")
init_content = f'''"""
{project_name} - A Python project with Kaizen Agentic agents.
"""
__version__ = "0.1.0"
'''
(project_dir / f"src/{package_name}/__init__.py").write_text(init_content)
def _create_makefile(self, project_dir: Path, project_name: str):
"""Create Makefile with standard targets."""
package_name = project_name.replace("-", "_")
makefile_content = f"""# {project_name} - Makefile for development workflow
# Generated by Kaizen Agentic
.PHONY: help setup-complete setup-python setup-tools test lint format clean agents-status agents-update
# Default target
help:
@echo "Available targets:"
@echo " setup-complete - Complete development environment setup"
@echo " setup-python - Set up Python virtual environment and dependencies"
@echo " setup-tools - Install development tools"
@echo " test - Run test suite"
@echo " lint - Run code quality checks"
@echo " format - Format code with black"
@echo " clean - Clean build artifacts"
@echo " agents-status - Show installed agents status"
@echo " agents-update - Update agents to latest versions"
# Virtual environment detection
VENV := .venv
PYTHON := $(VENV)/bin/python
PIP := $(VENV)/bin/pip
# Complete setup
setup-complete: setup-python setup-tools
@echo "✅ Development environment setup complete!"
@echo "Next steps:"
@echo " source $(VENV)/bin/activate # Activate virtual environment"
@echo " make test # Run tests"
@echo " make lint # Check code quality"
# Python environment setup
setup-python: $(VENV)/bin/activate
$(VENV)/bin/activate: pyproject.toml
python3 -m venv $(VENV)
$(PIP) install --upgrade pip
$(PIP) install -e ".[dev]"
touch $(VENV)/bin/activate
# Development tools setup
setup-tools: $(VENV)/bin/activate
@echo "Development tools installed via pyproject.toml"
# Testing
test: $(VENV)/bin/activate
$(PYTHON) -m pytest tests/ -v
test-coverage: $(VENV)/bin/activate
$(PYTHON) -m pytest tests/ --cov=src/{package_name} --cov-report=html --cov-report=term-missing
# Code quality
lint: $(VENV)/bin/activate
$(PYTHON) -m flake8 src/ tests/
$(PYTHON) -m mypy src/
format: $(VENV)/bin/activate
$(PYTHON) -m black src/ tests/
format-check: $(VENV)/bin/activate
$(PYTHON) -m black --check src/ tests/
# Cleanup
clean:
rm -rf build/
rm -rf dist/
rm -rf *.egg-info/
rm -rf .pytest_cache/
rm -rf .coverage
rm -rf htmlcov/
find . -type d -name __pycache__ -exec rm -rf {{}} +
find . -type f -name "*.pyc" -delete
# Agent management
agents-status:
@if command -v kaizen-agentic >/dev/null 2>&1; then \\
kaizen-agentic status; \\
else \\
echo "kaizen-agentic not found. Install with: pip install kaizen-agentic"; \\
fi
agents-update:
@if command -v kaizen-agentic >/dev/null 2>&1; then \\
kaizen-agentic update; \\
else \\
echo "kaizen-agentic not found. Install with: pip install kaizen-agentic"; \\
fi
agents-list:
@if command -v kaizen-agentic >/dev/null 2>&1; then \\
kaizen-agentic list; \\
else \\
echo "kaizen-agentic not found. Install with: pip install kaizen-agentic"; \\
fi
agents-validate:
@if command -v kaizen-agentic >/dev/null 2>&1; then \\
kaizen-agentic validate; \\
else \\
echo "kaizen-agentic not found. Install with: pip install kaizen-agentic"; \\
fi
"""
(project_dir / "Makefile").write_text(makefile_content)