Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 54 additions & 3 deletions src/agentready/cli/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,24 @@
default="auto",
help="Primary language (default: auto-detect)",
)
def bootstrap(repository, dry_run, language):
@click.option(
"--enable-release",
is_flag=True,
help="Enable automated release workflow with semantic versioning",
)
@click.option(
"--enable-publishing",
is_flag=True,
help="Enable package publishing (PyPI/npm/GitHub Releases)",
)
@click.option(
"--enable-all",
is_flag=True,
help="Enable all advanced features (release + publishing)",
)
def bootstrap(
repository, dry_run, language, enable_release, enable_publishing, enable_all
):
"""Bootstrap repository with GitHub infrastructure and best practices.

Creates:
Expand All @@ -31,7 +48,26 @@ def bootstrap(repository, dry_run, language):
- Dependabot configuration
- Contributing guidelines

Advanced features (opt-in):
- Release automation with semantic versioning (--enable-release)
- Package publishing to PyPI/npm (--enable-publishing)
- All advanced features (--enable-all)

REPOSITORY: Path to git repository (default: current directory)

Examples:

\b
# Basic bootstrap
agentready bootstrap .

\b
# With release automation
agentready bootstrap . --enable-release

\b
# Full automation
agentready bootstrap . --enable-all
"""
repo_path = Path(repository).resolve()

Expand All @@ -40,14 +76,29 @@ def bootstrap(repository, dry_run, language):
click.echo("Error: Not a git repository", err=True)
sys.exit(1)

# Handle --enable-all flag
if enable_all:
enable_release = True
enable_publishing = True

click.echo("🤖 AgentReady Bootstrap")
click.echo("=" * 50)
click.echo(f"\nRepository: {repo_path}")
click.echo(f"Language: {language}")
click.echo(f"Dry run: {dry_run}\n")
click.echo(f"Dry run: {dry_run}")
if enable_release:
click.echo("Release automation: ENABLED")
if enable_publishing:
click.echo("Package publishing: ENABLED")
click.echo()

# Create generator
generator = BootstrapGenerator(repo_path, language)
generator = BootstrapGenerator(
repo_path,
language,
enable_release=enable_release,
enable_publishing=enable_publishing,
)

# Generate all files
try:
Expand Down
64 changes: 64 additions & 0 deletions src/agentready/cli/drift_check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""Drift detection command for Bootstrap templates."""

import sys
from pathlib import Path

import click

from ..services.drift_detector import DriftDetector


@click.command()
@click.option(
"--verbose",
"-v",
is_flag=True,
help="Show detailed diffs of drifted files",
)
def drift_check(verbose):
"""Check for template drift in AgentReady repository.

Compares Bootstrap templates with AgentReady's actual infrastructure
files to detect drift and ensure templates stay synchronized.

This command should be run from the AgentReady repository root.

Examples:

\b
# Quick drift check
agentready drift-check

\b
# Detailed drift analysis with diffs
agentready drift-check --verbose
"""
repo_path = Path.cwd()

# Verify we're in AgentReady repository
if not (repo_path / "src" / "agentready").exists():
click.echo(
"❌ Error: This command must be run from AgentReady repository root",
err=True,
)
click.echo(" (Looking for src/agentready/ directory)", err=True)
sys.exit(1)

# Create drift detector
detector = DriftDetector(repo_path)

# Generate and display drift report
click.echo("Checking Bootstrap template drift...")
click.echo()

report = detector.generate_drift_report(verbose=verbose)
click.echo(report)

# Exit with error if drift detected
drift_data = detector.check_drift(verbose=False)
if drift_data["drifted"]:
click.echo()
click.echo(
"💡 Tip: Update templates in src/agentready/templates/bootstrap/ to match actual files"
)
sys.exit(1)
2 changes: 2 additions & 0 deletions src/agentready/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
from .benchmark import benchmark
from .bootstrap import bootstrap
from .demo import demo
from .drift_check import drift_check
from .repomix import repomix_generate
from .research import research
from .schema import migrate_report, validate_report
Expand Down Expand Up @@ -546,6 +547,7 @@ def generate_config():
cli.add_command(benchmark)
cli.add_command(bootstrap)
cli.add_command(demo)
cli.add_command(drift_check)
cli.add_command(migrate_report)
cli.add_command(repomix_generate)
cli.add_command(research)
Expand Down
99 changes: 88 additions & 11 deletions src/agentready/services/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,25 @@
class BootstrapGenerator:
"""Generates GitHub infrastructure files for repository."""

def __init__(self, repo_path: Path, language: str = "auto"):
def __init__(
self,
repo_path: Path,
language: str = "auto",
enable_release: bool = False,
enable_publishing: bool = False,
):
"""Initialize bootstrap generator.

Args:
repo_path: Path to repository
language: Primary language or "auto" to detect
enable_release: Enable release automation workflow
enable_publishing: Enable package publishing (PyPI/npm)
"""
self.repo_path = repo_path
self.language = self._detect_language(language)
self.enable_release = enable_release
self.enable_publishing = enable_publishing
self.env = Environment(
loader=PackageLoader("agentready", "templates/bootstrap"),
autoescape=select_autoescape(["html", "xml", "j2", "yaml", "yml"]),
Expand All @@ -43,7 +53,7 @@ def _detect_language(self, language: str) -> str:
return max(languages, key=languages.get).lower()

def generate_all(self, dry_run: bool = False) -> List[Path]:
"""Generate all bootstrap files.
"""Generate all bootstrap files including optional advanced features.

Args:
dry_run: If True, don't create files, just return paths
Expand All @@ -53,21 +63,19 @@ def generate_all(self, dry_run: bool = False) -> List[Path]:
"""
created_files = []

# GitHub Actions workflows
# Base infrastructure (always generated)
created_files.extend(self._generate_workflows(dry_run))

# GitHub templates
created_files.extend(self._generate_github_templates(dry_run))

# Pre-commit hooks
created_files.extend(self._generate_precommit_config(dry_run))

# Dependabot
created_files.extend(self._generate_dependabot(dry_run))

# Contributing guidelines
created_files.extend(self._generate_docs(dry_run))

# Advanced features (opt-in)
if self.enable_release:
created_files.extend(self._generate_release_workflow(dry_run))
created_files.extend(self._generate_release_config(dry_run))
created_files.extend(self._generate_version_sync_scripts(dry_run))

return created_files

def _generate_workflows(self, dry_run: bool) -> List[Path]:
Expand Down Expand Up @@ -177,6 +185,75 @@ def _generate_docs(self, dry_run: bool) -> List[Path]:

return created

def _generate_release_workflow(self, dry_run: bool) -> List[Path]:
"""Generate release automation workflow."""
workflows_dir = self.repo_path / ".github" / "workflows"
release_workflow = workflows_dir / "release.yml"

# Only Python templates implemented in MVP
if self.language != "python":
return []

template = self.env.get_template(f"{self.language}/workflows/release.yml.j2")
context = {
"enable_publishing": self.enable_publishing,
"project_name": self._detect_project_name(),
}
content = template.render(**context)

return [self._write_file(release_workflow, content, dry_run)]

def _generate_release_config(self, dry_run: bool) -> List[Path]:
"""Generate .releaserc.json semantic-release configuration."""
release_config_file = self.repo_path / ".releaserc.json"

# Only Python templates implemented in MVP
if self.language != "python":
return []

template = self.env.get_template(f"{self.language}/releaserc.json.j2")
context = {
"enable_publishing": self.enable_publishing,
"has_claude_md": (self.repo_path / "CLAUDE.md").exists(),
}
content = template.render(**context)

return [self._write_file(release_config_file, content, dry_run)]

def _generate_version_sync_scripts(self, dry_run: bool) -> List[Path]:
"""Generate version synchronization scripts."""
scripts_dir = self.repo_path / "scripts"

# Only Python templates implemented in MVP
if self.language != "python":
return []

sync_script = scripts_dir / "sync-version.sh"
template = self.env.get_template(f"{self.language}/sync-version.sh.j2")
context = {
"project_name": self._detect_project_name(),
"has_claude_md": (self.repo_path / "CLAUDE.md").exists(),
}
content = template.render(**context)

return [self._write_file(sync_script, content, dry_run)]

def _detect_project_name(self) -> str:
"""Detect project name from repository structure."""
if self.language == "python":
# Try pyproject.toml
pyproject = self.repo_path / "pyproject.toml"
if pyproject.exists():
import re

content = pyproject.read_text()
match = re.search(r'name = "([^"]+)"', content)
if match:
return match.group(1)

# Fallback to directory name
return self.repo_path.name

def _write_file(self, path: Path, content: str, dry_run: bool) -> Path:
"""Write file to disk or simulate for dry run."""
if dry_run:
Expand Down
Loading
Loading