Show full metadata and the fully expanded component set (== what install adds).
(
bundle_id: str = typer.Argument(..., help="Bundle id to inspect"),
offline: bool = typer.Option(False, "--offline", help="Do not access the network"),
as_json: bool = typer.Option(False, "--json", help="Emit JSON to stdout"),
)
| 193 | |
| 194 | @bundle_app.command("info") |
| 195 | def bundle_info( |
| 196 | bundle_id: str = typer.Argument(..., help="Bundle id to inspect"), |
| 197 | offline: bool = typer.Option(False, "--offline", help="Do not access the network"), |
| 198 | as_json: bool = typer.Option(False, "--json", help="Emit JSON to stdout"), |
| 199 | ) -> None: |
| 200 | """Show full metadata and the fully expanded component set (== what install adds).""" |
| 201 | try: |
| 202 | project_root = find_project_root() or Path.cwd() |
| 203 | stack = _build_stack(project_root, offline=offline) |
| 204 | resolved = stack.resolve(bundle_id) |
| 205 | # `info` must show the fully expanded component set that `install` would |
| 206 | # apply (contracts/cli-commands.md). Expansion happens regardless of |
| 207 | # install policy — discovery-only bundles stay inspectable; only |
| 208 | # `install` is refused. But if the manifest itself can't be resolved |
| 209 | # (e.g. --offline against an https:// download_url, or a download |
| 210 | # failure), fail loudly and exit non-zero rather than silently |
| 211 | # degrading to catalog `provides` counts, so users never mistake an |
| 212 | # unverifiable bundle for a known/installable one. |
| 213 | manifest = _download_manifest(resolved, offline=offline) |
| 214 | except BundlerError as exc: |
| 215 | _fail(str(exc)) |
| 216 | return |
| 217 | |
| 218 | overlaps = _bundle_overlaps(project_root, manifest, offline=offline) |
| 219 | components = _manifest_component_view(manifest) |
| 220 | |
| 221 | entry = resolved.entry |
| 222 | if as_json: |
| 223 | payload = { |
| 224 | "id": entry.id, |
| 225 | "name": entry.name, |
| 226 | "version": entry.version, |
| 227 | "role": entry.role, |
| 228 | "description": entry.description, |
| 229 | "author": entry.author, |
| 230 | "license": entry.license, |
| 231 | "source": resolved.source.id, |
| 232 | "install_policy": resolved.source.install_policy.value, |
| 233 | "provides": entry.provides, |
| 234 | "requires": {"speckit_version": entry.requires_speckit_version}, |
| 235 | "verified": entry.verified, |
| 236 | "trust": _trust_level(entry.verified), |
| 237 | "integration": (manifest.integration.id if manifest and manifest.integration else None), |
| 238 | "components": components, |
| 239 | "overlaps": overlaps, |
| 240 | } |
| 241 | print(_json.dumps(payload, indent=2)) |
| 242 | return |
| 243 | |
| 244 | console.print(f"\n[bold cyan]{entry.id}[/bold cyan] v{entry.version} — {entry.name}") |
| 245 | console.print(f" Role: {entry.role}") |
| 246 | console.print(f" {entry.description}") |
| 247 | console.print(f" Author: {entry.author} License: {entry.license}") |
| 248 | console.print(f" Source: {resolved.source.id} ({resolved.source.install_policy.value})") |
| 249 | console.print(f" Trust: {_trust_badge(entry.verified)}") |
| 250 | if entry.requires_speckit_version: |
| 251 | console.print(f" Requires Spec Kit: {entry.requires_speckit_version}") |
| 252 | if manifest and manifest.integration: |
nothing calls this directly
no test coverage detected