"""Typer CLI for bulkgen: build, clean, graph commands.""" from __future__ import annotations import asyncio from pathlib import Path from typing import Annotated import typer from bulkgen.builder import run_build from bulkgen.config import load_config from bulkgen.graph import build_graph, get_build_order app = typer.Typer(name="bulkgen", help="AI artifact build tool.") def _find_config(directory: Path) -> Path: """Find the single ``*.bulkgen.yaml`` file in *directory*.""" candidates = list(directory.glob("*.bulkgen.yaml")) if len(candidates) == 0: typer.echo("Error: No .bulkgen.yaml file found in current directory", err=True) raise typer.Exit(code=1) if len(candidates) > 1: names = ", ".join(str(c.name) for c in candidates) typer.echo(f"Error: Multiple .bulkgen.yaml files found: {names}", err=True) raise typer.Exit(code=1) return candidates[0] @app.command() def build( target: Annotated[ str | None, typer.Argument(help="Specific target to build.") ] = None, ) -> None: """Build all targets (or a specific target) in dependency order.""" project_dir = Path.cwd() config_path = _find_config(project_dir) config = load_config(config_path) result = asyncio.run(run_build(config, project_dir, target)) if result.built: typer.echo(f"\nBuilt {len(result.built)} target(s)") if result.skipped: typer.echo(f"Skipped {len(result.skipped)} target(s) (up to date)") if result.failed: typer.echo(f"Failed {len(result.failed)} target(s):", err=True) for name, err in result.failed.items(): typer.echo(f" {name}: {err}", err=True) raise typer.Exit(code=1) @app.command() def clean() -> None: """Remove generated artifacts (targets only, not input files).""" project_dir = Path.cwd() config_path = _find_config(project_dir) config = load_config(config_path) removed = 0 for target_name in config.targets: target_path = project_dir / target_name if target_path.exists(): target_path.unlink() typer.echo(f"Removed: {target_name}") removed += 1 state_path = project_dir / ".bulkgen.state.yaml" if state_path.exists(): state_path.unlink() typer.echo("Removed: .bulkgen.state.yaml") typer.echo(f"Cleaned {removed} artifact(s)") @app.command() def graph() -> None: """Print the dependency graph with build stages.""" project_dir = Path.cwd() config_path = _find_config(project_dir) config = load_config(config_path) dep_graph = build_graph(config, project_dir) generations = get_build_order(dep_graph) target_names = set(config.targets) for i, gen in enumerate(generations): targets_in_gen = [n for n in gen if n in target_names] externals_in_gen = [n for n in gen if n not in target_names] if externals_in_gen: typer.echo(f"Stage {i} (inputs): {', '.join(externals_in_gen)}") if targets_in_gen: typer.echo(f"Stage {i} (targets): {', '.join(targets_in_gen)}") for node in gen: predecessors = list(dep_graph.predecessors(node)) if predecessors: typer.echo(f" {node} <- {', '.join(predecessors)}")