diff --git a/src/specify_cli/__init__.py b/src/specify_cli/__init__.py index 41fb994726..2c04b74d2a 100644 --- a/src/specify_cli/__init__.py +++ b/src/specify_cli/__init__.py @@ -1332,6 +1332,15 @@ def _write_integration_json( ) +def _refresh_init_options_speckit_version(project_root: Path) -> None: + """Refresh only the Spec Kit version recorded in init-options.json.""" + opts = load_init_options(project_root) + if not isinstance(opts, dict) or not opts: + return + opts["speckit_version"] = get_speckit_version() + save_init_options(project_root, opts) + + def _clear_init_options_for_integration(project_root: Path, integration_key: str) -> None: """Clear active integration keys from init-options.json when they match.""" opts = load_init_options(project_root) @@ -1692,6 +1701,8 @@ def integration_install( _write_integration_json(project_root, new_default, new_installed, settings) if new_default == integration.key: _update_init_options_for_integration(project_root, integration, script_type=selected_script) + else: + _refresh_init_options_speckit_version(project_root) except Exception as e: # Attempt rollback of any files written by setup @@ -1775,6 +1786,7 @@ def _update_init_options_for_integration( opts["integration"] = integration.key opts["ai"] = integration.key opts["context_file"] = integration.context_file + opts["speckit_version"] = get_speckit_version() if script_type: opts["script"] = script_type if isinstance(integration, SkillsIntegration) or getattr(integration, "_skills_mode", False): @@ -2339,6 +2351,8 @@ def integration_upgrade( _write_integration_json(project_root, installed_key, installed_keys, settings) if installed_key == key: _update_init_options_for_integration(project_root, integration, script_type=selected_script) + else: + _refresh_init_options_speckit_version(project_root) except Exception as exc: # Don't teardown — setup overwrites in-place, so teardown would # delete files that were working before the upgrade. Just report. diff --git a/tests/integrations/test_integration_subcommand.py b/tests/integrations/test_integration_subcommand.py index abff9a5ee1..bd06f1281b 100644 --- a/tests/integrations/test_integration_subcommand.py +++ b/tests/integrations/test_integration_subcommand.py @@ -230,6 +230,29 @@ def test_install_multi_safe_integration(self, tmp_path): assert (project / ".claude" / "skills" / "speckit-plan" / "SKILL.md").exists() assert (project / ".agents" / "skills" / "speckit-plan" / "SKILL.md").exists() + def test_install_non_default_refreshes_init_options_version_only(self, tmp_path, monkeypatch): + project = _init_project(tmp_path, "claude") + init_options = project / ".specify" / "init-options.json" + opts = json.loads(init_options.read_text(encoding="utf-8")) + opts["speckit_version"] = "0.6.1" + init_options.write_text(json.dumps(opts), encoding="utf-8") + + import specify_cli + + monkeypatch.setattr(specify_cli, "get_speckit_version", lambda: "0.8.11") + + result = _run_in_project(project, [ + "integration", "install", "codex", + "--script", "sh", + ]) + + assert result.exit_code == 0, result.output + updated = json.loads(init_options.read_text(encoding="utf-8")) + assert updated["speckit_version"] == "0.8.11" + assert updated["integration"] == "claude" + assert updated["ai"] == "claude" + assert updated["context_file"] == "CLAUDE.md" + def test_install_additional_preserves_shared_manifest(self, tmp_path): project = _init_project(tmp_path, "claude") shared_manifest = project / ".specify" / "integrations" / "speckit.manifest.json" @@ -1144,6 +1167,56 @@ def test_upgrade_invalid_manifest_reports_cli_error(self, tmp_path): assert "manifest" in result.output assert "unreadable" in result.output + def test_upgrade_refreshes_init_options_speckit_version(self, tmp_path, monkeypatch): + project = _init_project(tmp_path, "claude") + init_options = project / ".specify" / "init-options.json" + opts = json.loads(init_options.read_text(encoding="utf-8")) + opts["speckit_version"] = "0.6.1" + init_options.write_text(json.dumps(opts), encoding="utf-8") + + import specify_cli + + monkeypatch.setattr(specify_cli, "get_speckit_version", lambda: "0.8.11") + + result = _run_in_project(project, [ + "integration", "upgrade", "claude", + "--force", + ]) + + assert result.exit_code == 0, result.output + updated = json.loads(init_options.read_text(encoding="utf-8")) + assert updated["speckit_version"] == "0.8.11" + + def test_upgrade_non_default_refreshes_init_options_version_only(self, tmp_path, monkeypatch): + project = _init_project(tmp_path, "gemini") + install = _run_in_project(project, [ + "integration", "install", "claude", + "--script", "sh", + ]) + assert install.exit_code == 0, install.output + + init_options = project / ".specify" / "init-options.json" + opts = json.loads(init_options.read_text(encoding="utf-8")) + opts["speckit_version"] = "0.6.1" + init_options.write_text(json.dumps(opts), encoding="utf-8") + + import specify_cli + + monkeypatch.setattr(specify_cli, "get_speckit_version", lambda: "0.8.11") + + result = _run_in_project(project, [ + "integration", "upgrade", "claude", + "--script", "sh", + "--force", + ]) + + assert result.exit_code == 0, result.output + updated = json.loads(init_options.read_text(encoding="utf-8")) + assert updated["speckit_version"] == "0.8.11" + assert updated["integration"] == "gemini" + assert updated["ai"] == "gemini" + assert updated["context_file"] == "GEMINI.md" + def test_upgrade_does_not_persist_state_when_template_refresh_fails(self, tmp_path, monkeypatch): project = _init_project(tmp_path, "claude") int_json = project / ".specify" / "integration.json"