Skip to content
Open
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
2 changes: 2 additions & 0 deletions docs/reference/bundles.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,8 @@ specify bundle init [<bundle_id>]

Ensures the current directory is a Spec Kit project (initializing it idempotently if needed), then optionally installs the given bundle. Useful as an explicit one-step bootstrap for a new checkout.

Repeating `bundle init` skips components already installed by the bundle and preserves their local edits. It follows the same rules as a normal [`bundle install`](#install-a-bundle): components newly added to the manifest are installed and recorded, but it fails without changing the installed bundle if the manifest changes the recorded bundle version, changes an owned component's version, source, preset priority, or strategy, or removes an owned component. To refresh components or apply such changes, use `bundle update <id>` for a catalog bundle or `bundle install <path> --refresh` for a local source.

## Validate a Bundle

```bash
Expand Down
7 changes: 6 additions & 1 deletion src/specify_cli/bundles/command_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,9 @@ def bundle_init(
f"{_escape_markup(str(project_root))}."
)
if bundle:
bundle_install(bundle, integration=integration, offline=offline)
bundle_install(
bundle,
integration=integration,
offline=offline,
refresh=False,
)
72 changes: 72 additions & 0 deletions tests/specify_cli/bundles/test_command_init.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
from __future__ import annotations

from pathlib import Path
from unittest.mock import Mock

import pytest
import yaml
from typer.testing import CliRunner

from specify_cli import app
from tests.specify_cli.bundles.helpers import write_manifest

runner = CliRunner()
REPO_ROOT = Path(__file__).parents[3]


def _make_project(tmp_path: Path, name: str) -> Path:
Expand All @@ -16,6 +20,74 @@ def _make_project(tmp_path: Path, name: str) -> Path:
return project


@pytest.mark.parametrize("bundle", [None, "bugfix"])
def test_bundle_init_delegates_without_refresh(project, monkeypatch, bundle):
from specify_cli.bundles import command_init

install = Mock()
monkeypatch.setattr(command_init, "bundle_install", install)

command_init.bundle_init(bundle, integration=None, offline=True)

if bundle is None:
install.assert_not_called()
else:
install.assert_called_once_with(
bundle, integration=None, offline=True, refresh=False,
)


@pytest.mark.parametrize(
("command", "refresh"),
[("init", False), ("install", False), ("install", True)],
)
def test_bundle_init_preserves_workflow_unless_explicitly_refreshed(
project, command, refresh,
):
bundle = REPO_ROOT / "bundles" / "bugfix"
initial = runner.invoke(app, ["bundle", "init", str(bundle), "--offline"])
assert initial.exit_code == 0, initial.output
workflow = project / ".specify/workflows/bugfix/workflow.yml"
original = workflow.read_bytes()
edited = original + b"# Local review note: use staging.\r\n"
workflow.write_bytes(edited)
registry = project / ".specify/workflows/workflow-registry.json"
registry_before = registry.read_bytes()
args = ["bundle", command, str(bundle), "--offline"]
if refresh:
args.append("--refresh")

repeated = runner.invoke(app, args)

assert repeated.exit_code == 0, repeated.output
assert workflow.read_bytes() == (original if refresh else edited)
if not refresh:
assert registry.read_bytes() == registry_before
assert "2 already present" in repeated.output
assert "refreshed" not in repeated.output


def test_bundle_init_rejects_changed_manifest_without_refresh(project):
bundle = REPO_ROOT / "bundles" / "bugfix"
initial = runner.invoke(app, ["bundle", "init", str(bundle), "--offline"])
assert initial.exit_code == 0, initial.output
records = project / ".specify/bundle-records.json"
records_before = records.read_bytes()
workflow = project / ".specify/workflows/bugfix/workflow.yml"
edited = workflow.read_bytes() + b"# Local review note: use staging.\r\n"
workflow.write_bytes(edited)
data = yaml.safe_load((bundle / "bundle.yml").read_text(encoding="utf-8"))
data["bundle"]["version"] = "999.0.0"
changed = write_manifest(project / "changed-bundle", data)

result = runner.invoke(app, ["bundle", "init", str(changed), "--offline"])

assert result.exit_code == 1, result.output
assert "--refresh" in result.output
assert records.read_bytes() == records_before
assert workflow.read_bytes() == edited


def test_override_symlinked_specify_errors_bundle_init_no_fallback(
tmp_path, monkeypatch
):
Expand Down