openenv/echo_env
6
1# SPDX-License-Identifier: BSD-3-Clause2 3"""Metadata catalog production and inspection. No environment is instantiated."""4 5import json6from pathlib import Path7 8import typer9from openenv.discovery import (10 build_catalog,11 CatalogError,12 load_catalog,13 resolve_entry,14 search_catalog,15 write_catalog,16)17 18app = typer.Typer(19 help="Build and inspect versioned, metadata-only environment catalogs."20)21 22 23@app.command("build")24def build(25 repository: Path = typer.Option(..., help="Local Git repository to read."),26 repository_uri: str = typer.Option(..., help="Declared GitHub HTTPS clone URI."),27 publisher: str = typer.Option(..., help="Explicit catalog publication authority."),28 output: Path = typer.Option(..., help="Destination snapshot JSON."),29 revision: str = typer.Option("HEAD", help="Source commit or ref."),30 root: str = typer.Option("envs", help="Direct-child environment inventory root."),31 namespace: str = typer.Option("openenv", help="Publisher-owned namespace."),32) -> None:33 """Build from committed metadata; uncommitted files and candidate code are not used."""34 try:35 snapshot = build_catalog(36 repository,37 repository_uri=repository_uri,38 publisher=publisher,39 namespace=namespace,40 revision=revision,41 root=root,42 )43 write_catalog(snapshot, output)44 except CatalogError as error:45 typer.echo(f"Catalog error: {error}", err=True)46 raise typer.Exit(1) from error47 typer.echo(48 f"{len(snapshot.entries)} records, snapshot {snapshot.digest}, "49 f"complete={str(snapshot.complete).lower()}",50 err=True,51 )52 for issue in snapshot.issues:53 typer.echo(f"{issue.severity}: {issue.path}: {issue.message}", err=True)54 if not snapshot.complete:55 raise typer.Exit(1)56 57 58@app.command("inspect")59def inspect(60 identifier: str = typer.Argument(..., help="Exact revision-card identifier."),61 catalog: Path = typer.Option(..., help="Complete configured snapshot."),62) -> None:63 """Return the complete selected entry without guessing or fetching a URL."""64 try:65 entry = resolve_entry(load_catalog(catalog), identifier)66 except CatalogError as error:67 typer.echo(f"Catalog error: {error}", err=True)68 raise typer.Exit(1) from error69 typer.echo(entry.model_dump_json(by_alias=True, exclude_none=True, indent=2))70 71 72def discover(73 query: str = typer.Argument("", help="Task to find an environment for."),74 catalog: Path = typer.Option(75 ..., help="Complete, explicitly configured local snapshot."76 ),77 limit: int = typer.Option(20, min=1, max=1000),78 filter: list[str] = typer.Option([], "--filter", help="Exact field=value filter."),79 as_json: bool = typer.Option(False, "--json", help="Emit complete ARD entry data."),80) -> None:81 """Find unfamiliar environments by task before any installation or execution."""82 filters = {}83 for item in filter:84 key, separator, value = item.partition("=")85 if not separator or not key or key in filters:86 typer.echo(87 "Catalog error: filters must be distinct field=value pairs", err=True88 )89 raise typer.Exit(1)90 filters[key] = value91 try:92 snapshot = load_catalog(catalog)93 matches = search_catalog(snapshot, query, filters=filters, limit=limit)94 except CatalogError as error:95 typer.echo(f"Catalog error: {error}", err=True)96 raise typer.Exit(1) from error97 if as_json:98 results = [99 {100 **match.entry.model_dump(by_alias=True, exclude_none=True, mode="json"),101 "score": match.score,102 }103 for match in matches104 ]105 typer.echo(106 json.dumps(107 {"results": results, "snapshot": snapshot.digest, "metadataOnly": True},108 indent=2,109 ensure_ascii=False,110 )111 )112 return113 for match in matches:114 card = match.entry.data115 typer.echo(f"{match.entry.display_name}: {card.description}")116 typer.echo(f" source: {card.source.uri}")117 typer.echo(f" path: {card.source.path}")118 typer.echo(f" revision: {card.source.revision}")119 typer.echo(f" license: {card.license}; artifact: {card.artifact_availability}")120 typer.echo(f" identifier: {match.entry.identifier}")121 typer.echo("Metadata only. Execution approval remains a separate decision.")122 