Install a bundle's full component set through each primitive's machinery. ``bundle_id`` may be a catalog bundle id, or a local path to a built artifact (``.zip``), a bundle directory, or a ``bundle.yml`` file. Local sources install directly without consulting the catalog stack.
(
bundle_id: str = typer.Argument(
...,
help="Bundle id (from the catalog stack) or a local path to a .zip "
"artifact, bundle directory, or bundle.yml",
),
integration: str = typer.Option(None, "--integration", help="Override integration"),
offline: bool = typer.Option(False, "--offline", help="Do not access the network"),
)
| 312 | |
| 313 | @bundle_app.command("install") |
| 314 | def bundle_install( |
| 315 | bundle_id: str = typer.Argument( |
| 316 | ..., |
| 317 | help="Bundle id (from the catalog stack) or a local path to a .zip " |
| 318 | "artifact, bundle directory, or bundle.yml", |
| 319 | ), |
| 320 | integration: str = typer.Option(None, "--integration", help="Override integration"), |
| 321 | offline: bool = typer.Option(False, "--offline", help="Do not access the network"), |
| 322 | ) -> None: |
| 323 | """Install a bundle's full component set through each primitive's machinery. |
| 324 | |
| 325 | ``bundle_id`` may be a catalog bundle id, or a local path to a built |
| 326 | artifact (``.zip``), a bundle directory, or a ``bundle.yml`` file. Local |
| 327 | sources install directly without consulting the catalog stack. |
| 328 | """ |
| 329 | try: |
| 330 | from ...bundler.lib.project import find_project_root |
| 331 | from ...bundler.services.adapters import DefaultPrimitiveInstaller |
| 332 | from ...bundler.services.installer import install_bundle |
| 333 | from ...bundler.services.resolver import resolve_install_plan |
| 334 | |
| 335 | project_root = find_project_root() |
| 336 | |
| 337 | local_manifest = _local_manifest_source(bundle_id) |
| 338 | if local_manifest is not None: |
| 339 | manifest = local_manifest |
| 340 | else: |
| 341 | stack = _build_stack(project_root or Path.cwd(), offline=offline) |
| 342 | resolved = stack.resolve(bundle_id) |
| 343 | |
| 344 | if not resolved.install_allowed: |
| 345 | raise BundlerError( |
| 346 | f"Bundle '{bundle_id}' resolves only from a discovery-only source " |
| 347 | f"('{resolved.source.id}'); it cannot be installed from there." |
| 348 | ) |
| 349 | manifest = _download_manifest(resolved, offline=offline) |
| 350 | |
| 351 | if project_root is None: |
| 352 | init_integration = _resolve_init_integration(integration, manifest) |
| 353 | console.print( |
| 354 | f"[cyan]No Spec Kit project here; initializing with integration " |
| 355 | f"'{init_integration}'…[/cyan]" |
| 356 | ) |
| 357 | _run_init(init_integration, script_type=_default_script_type(), offline=offline) |
| 358 | project_root = require_project_root() |
| 359 | |
| 360 | for overlap in _bundle_overlaps(project_root, manifest, offline=offline): |
| 361 | console.print(f"[yellow]![/yellow] {overlap}") |
| 362 | |
| 363 | # For an already-initialized project, the project's recorded active |
| 364 | # integration is authoritative — an explicit --integration must not be |
| 365 | # able to bypass the FR-019 integration-clash guard. The override only |
| 366 | # selects the integration at init time (handled above) or confirms the |
| 367 | # target when the active integration cannot be determined. |
| 368 | detected = active_integration(project_root) |
| 369 | plan = resolve_install_plan( |
| 370 | manifest, |
| 371 | speckit_version=_speckit_version(), |
no test coverage detected