From 3231f06b69f536213b4725fa7950a42e2c019be1 Mon Sep 17 00:00:00 2001 From: kartsan03 <48928509+kartsan03@users.noreply.github.com> Date: Wed, 14 Oct 2026 22:46:59 +0000 Subject: [PATCH 1/3] fix(bundles): don't refresh installed components on a repeated bundle init bundle_init called the Typer-decorated bundle_install() without refresh, so refresh kept its typer.Option(False) default, a truthy OptionInfo. A second `specify bundle init ` then took the explicit-refresh path: it replaced local edits to installed workflows and accepted a changed bundle version that a plain `bundle install` refuses without --refresh. Pass refresh=False explicitly, as #4284 did for the workflow_add call. Assisted-by: Codex CLI (model: gpt-6-astra, autonomous) Assisted-by: Claude Code (model: claude-opus-5-5, autonomous) --- docs/reference/bundles.md | 2 + src/specify_cli/bundles/command_init.py | 7 +- .../specify_cli/bundles/test_command_init.py | 72 +++++++++++++++++++ 3 files changed, 80 insertions(+), 1 deletion(-) diff --git a/docs/reference/bundles.md b/docs/reference/bundles.md index d9ca00804c..5e1e367fac 100644 --- a/docs/reference/bundles.md +++ b/docs/reference/bundles.md @@ -191,6 +191,8 @@ specify bundle init [] 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. To refresh them, use `bundle update ` for a catalog bundle or `bundle install --refresh` for a local source. + ## Validate a Bundle ```bash diff --git a/src/specify_cli/bundles/command_init.py b/src/specify_cli/bundles/command_init.py index 0b2e8133c1..c61dd53615 100644 --- a/src/specify_cli/bundles/command_init.py +++ b/src/specify_cli/bundles/command_init.py @@ -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, + ) diff --git a/tests/specify_cli/bundles/test_command_init.py b/tests/specify_cli/bundles/test_command_init.py index adf60e0035..7d692b23fb 100644 --- a/tests/specify_cli/bundles/test_command_init.py +++ b/tests/specify_cli/bundles/test_command_init.py @@ -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: @@ -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 ): From 335d29ff8c277be66dce43dd7c411266601da51c Mon Sep 17 00:00:00 2001 From: kartsan03 Date: Thu, 15 Oct 2026 14:04:30 +0000 Subject: [PATCH 2/3] docs(bundles): say a repeated bundle init rejects manifest changes With refresh no longer implied, a repeated `bundle init` goes through the same record check as a plain `bundle install`: a changed bundle version, or a changed or removed owned component, fails until `bundle update` or `bundle install --refresh` applies it. Assisted-by: Claude Code (model: claude-opus-5-5, autonomous) --- docs/reference/bundles.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/bundles.md b/docs/reference/bundles.md index 5e1e367fac..f1a64072d4 100644 --- a/docs/reference/bundles.md +++ b/docs/reference/bundles.md @@ -191,7 +191,7 @@ specify bundle init [] 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. To refresh them, use `bundle update ` for a catalog bundle or `bundle install --refresh` for a local source. +Repeating `bundle init` skips components already installed by the bundle and preserves their local edits. It does not apply manifest changes: as with a normal [`bundle install`](#install-a-bundle), 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 ` for a catalog bundle or `bundle install --refresh` for a local source. ## Validate a Bundle From 2ceec75bf64bd0489f56f7679bff6a7507020acc Mon Sep 17 00:00:00 2001 From: kartsan03 Date: Thu, 15 Oct 2026 18:58:47 +0000 Subject: [PATCH 3/3] docs(bundles): say a repeated bundle init still installs added components Assisted-by: Claude Code (model: claude-opus-5-5, autonomous) --- docs/reference/bundles.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/bundles.md b/docs/reference/bundles.md index f1a64072d4..0e060efc2a 100644 --- a/docs/reference/bundles.md +++ b/docs/reference/bundles.md @@ -191,7 +191,7 @@ specify bundle init [] 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 does not apply manifest changes: as with a normal [`bundle install`](#install-a-bundle), 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 ` for a catalog bundle or `bundle install --refresh` for a local source. +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 ` for a catalog bundle or `bundle install --refresh` for a local source. ## Validate a Bundle