diff --git a/src/specify_cli/integrations/base.py b/src/specify_cli/integrations/base.py index 1b09347dcd..0e62e32f50 100644 --- a/src/specify_cli/integrations/base.py +++ b/src/specify_cli/integrations/base.py @@ -231,6 +231,7 @@ def install_scripts( self, project_root: Path, manifest: IntegrationManifest, + script_type: str | None = None, ) -> list[Path]: """Copy integration-specific scripts into the project. @@ -239,12 +240,20 @@ def install_scripts( scripts are made executable. All copied files are recorded in *manifest*. + When *script_type* is ``"sh"`` only ``.sh`` files are copied; when + it is ``"ps"`` only ``.ps1`` files are copied. Passing ``None`` + (or any other value) copies every file — preserving the previous + behaviour for callers that have not been updated yet. + Returns the list of files created. """ scripts_src = self.integration_scripts_dir() if not scripts_src: return [] + _EXTENSION_FILTER: dict[str, str] = {"sh": ".sh", "ps": ".ps1"} + only_ext = _EXTENSION_FILTER.get(script_type) if script_type else None + created: list[Path] = [] scripts_dest = project_root / ".specify" / "integrations" / self.key / "scripts" scripts_dest.mkdir(parents=True, exist_ok=True) @@ -252,6 +261,8 @@ def install_scripts( for src_script in sorted(scripts_src.iterdir()): if not src_script.is_file(): continue + if only_ext is not None and src_script.suffix != only_ext: + continue dst_script = scripts_dest / src_script.name shutil.copy2(src_script, dst_script) if dst_script.suffix == ".sh": @@ -459,8 +470,9 @@ class MarkdownIntegration(IntegrationBase): (and optionally ``context_file``). Everything else is inherited. ``setup()`` processes command templates (replacing ``{SCRIPT}``, - ``{ARGS}``, ``__AGENT__``, rewriting paths) and installs - integration-specific scripts (``update-context.sh`` / ``.ps1``). + ``{ARGS}``, ``__AGENT__``, rewriting paths) and installs the + integration-specific script matching the selected *script_type* + (e.g. ``update-context.sh`` for ``sh``, ``update-context.ps1`` for ``ps``). """ def setup( @@ -504,7 +516,7 @@ def setup( ) created.append(dst_file) - created.extend(self.install_scripts(project_root, manifest)) + created.extend(self.install_scripts(project_root, manifest, script_type)) return created @@ -680,7 +692,7 @@ def setup( ) created.append(dst_file) - created.extend(self.install_scripts(project_root, manifest)) + created.extend(self.install_scripts(project_root, manifest, script_type)) return created @@ -832,5 +844,5 @@ def _quote(v: str) -> str: ) created.append(dst) - created.extend(self.install_scripts(project_root, manifest)) + created.extend(self.install_scripts(project_root, manifest, script_type)) return created diff --git a/src/specify_cli/integrations/forge/__init__.py b/src/specify_cli/integrations/forge/__init__.py index e1c4d9da62..c628d6d9c8 100644 --- a/src/specify_cli/integrations/forge/__init__.py +++ b/src/specify_cli/integrations/forge/__init__.py @@ -146,7 +146,7 @@ def setup( created.append(dst_file) # Install integration-specific update-context scripts - created.extend(self.install_scripts(project_root, manifest)) + created.extend(self.install_scripts(project_root, manifest, script_type)) return created diff --git a/tests/integrations/test_integration_base_markdown.py b/tests/integrations/test_integration_base_markdown.py index 3f8969bcb9..3ad2ab1e51 100644 --- a/tests/integrations/test_integration_base_markdown.py +++ b/tests/integrations/test_integration_base_markdown.py @@ -139,18 +139,27 @@ def test_modified_file_survives_uninstall(self, tmp_path): def test_setup_installs_update_context_scripts(self, tmp_path): i = get_integration(self.KEY) m = IntegrationManifest(self.KEY, tmp_path) - created = i.setup(tmp_path, m) + i.setup(tmp_path, m, script_type="sh") scripts_dir = tmp_path / ".specify" / "integrations" / self.KEY / "scripts" assert scripts_dir.is_dir(), f"Scripts directory not created for {self.KEY}" assert (scripts_dir / "update-context.sh").exists() + assert not (scripts_dir / "update-context.ps1").exists() + + def test_setup_installs_ps1_script_when_ps_type(self, tmp_path): + i = get_integration(self.KEY) + m = IntegrationManifest(self.KEY, tmp_path) + i.setup(tmp_path, m, script_type="ps") + scripts_dir = tmp_path / ".specify" / "integrations" / self.KEY / "scripts" + assert scripts_dir.is_dir(), f"Scripts directory not created for {self.KEY}" assert (scripts_dir / "update-context.ps1").exists() + assert not (scripts_dir / "update-context.sh").exists() def test_scripts_tracked_in_manifest(self, tmp_path): i = get_integration(self.KEY) m = IntegrationManifest(self.KEY, tmp_path) - i.setup(tmp_path, m) + i.setup(tmp_path, m, script_type="sh") script_rels = [k for k in m.files if "update-context" in k] - assert len(script_rels) >= 2 + assert len(script_rels) >= 1 def test_sh_script_is_executable(self, tmp_path): i = get_integration(self.KEY) @@ -222,9 +231,11 @@ def _expected_files(self, script_variant: str) -> list[str]: for stem in self.COMMAND_STEMS: files.append(f"{cmd_dir}/speckit.{stem}.md") - # Integration scripts - files.append(f".specify/integrations/{self.KEY}/scripts/update-context.ps1") - files.append(f".specify/integrations/{self.KEY}/scripts/update-context.sh") + # Integration scripts — only the matching variant is installed + if script_variant == "sh": + files.append(f".specify/integrations/{self.KEY}/scripts/update-context.sh") + else: + files.append(f".specify/integrations/{self.KEY}/scripts/update-context.ps1") # Framework files files.append(f".specify/integration.json") diff --git a/tests/integrations/test_integration_base_skills.py b/tests/integrations/test_integration_base_skills.py index 941f492eb8..9ac17fc276 100644 --- a/tests/integrations/test_integration_base_skills.py +++ b/tests/integrations/test_integration_base_skills.py @@ -224,18 +224,27 @@ def test_pre_existing_skills_not_removed(self, tmp_path): def test_setup_installs_update_context_scripts(self, tmp_path): i = get_integration(self.KEY) m = IntegrationManifest(self.KEY, tmp_path) - i.setup(tmp_path, m) + i.setup(tmp_path, m, script_type="sh") scripts_dir = tmp_path / ".specify" / "integrations" / self.KEY / "scripts" assert scripts_dir.is_dir(), f"Scripts directory not created for {self.KEY}" assert (scripts_dir / "update-context.sh").exists() + assert not (scripts_dir / "update-context.ps1").exists() + + def test_setup_installs_ps1_script_when_ps_type(self, tmp_path): + i = get_integration(self.KEY) + m = IntegrationManifest(self.KEY, tmp_path) + i.setup(tmp_path, m, script_type="ps") + scripts_dir = tmp_path / ".specify" / "integrations" / self.KEY / "scripts" + assert scripts_dir.is_dir(), f"Scripts directory not created for {self.KEY}" assert (scripts_dir / "update-context.ps1").exists() + assert not (scripts_dir / "update-context.sh").exists() def test_scripts_tracked_in_manifest(self, tmp_path): i = get_integration(self.KEY) m = IntegrationManifest(self.KEY, tmp_path) - i.setup(tmp_path, m) + i.setup(tmp_path, m, script_type="sh") script_rels = [k for k in m.files if "update-context" in k] - assert len(script_rels) >= 2 + assert len(script_rels) >= 1 def test_sh_script_is_executable(self, tmp_path): i = get_integration(self.KEY) @@ -318,11 +327,14 @@ def _expected_files(self, script_variant: str) -> list[str]: ".specify/init-options.json", ".specify/integration.json", f".specify/integrations/{self.KEY}.manifest.json", - f".specify/integrations/{self.KEY}/scripts/update-context.ps1", - f".specify/integrations/{self.KEY}/scripts/update-context.sh", ".specify/integrations/speckit.manifest.json", ".specify/memory/constitution.md", ] + # Only the matching script variant is installed + if script_variant == "sh": + files.append(f".specify/integrations/{self.KEY}/scripts/update-context.sh") + else: + files.append(f".specify/integrations/{self.KEY}/scripts/update-context.ps1") # Script variant if script_variant == "sh": files += [ diff --git a/tests/integrations/test_integration_base_toml.py b/tests/integrations/test_integration_base_toml.py index a864bd0c0c..3ddbe96346 100644 --- a/tests/integrations/test_integration_base_toml.py +++ b/tests/integrations/test_integration_base_toml.py @@ -339,18 +339,27 @@ def test_modified_file_survives_uninstall(self, tmp_path): def test_setup_installs_update_context_scripts(self, tmp_path): i = get_integration(self.KEY) m = IntegrationManifest(self.KEY, tmp_path) - created = i.setup(tmp_path, m) + i.setup(tmp_path, m, script_type="sh") scripts_dir = tmp_path / ".specify" / "integrations" / self.KEY / "scripts" assert scripts_dir.is_dir(), f"Scripts directory not created for {self.KEY}" assert (scripts_dir / "update-context.sh").exists() + assert not (scripts_dir / "update-context.ps1").exists() + + def test_setup_installs_ps1_script_when_ps_type(self, tmp_path): + i = get_integration(self.KEY) + m = IntegrationManifest(self.KEY, tmp_path) + i.setup(tmp_path, m, script_type="ps") + scripts_dir = tmp_path / ".specify" / "integrations" / self.KEY / "scripts" + assert scripts_dir.is_dir(), f"Scripts directory not created for {self.KEY}" assert (scripts_dir / "update-context.ps1").exists() + assert not (scripts_dir / "update-context.sh").exists() def test_scripts_tracked_in_manifest(self, tmp_path): i = get_integration(self.KEY) m = IntegrationManifest(self.KEY, tmp_path) - i.setup(tmp_path, m) + i.setup(tmp_path, m, script_type="sh") script_rels = [k for k in m.files if "update-context" in k] - assert len(script_rels) >= 2 + assert len(script_rels) >= 1 def test_sh_script_is_executable(self, tmp_path): i = get_integration(self.KEY) @@ -422,9 +431,11 @@ def _expected_files(self, script_variant: str) -> list[str]: for stem in self.COMMAND_STEMS: files.append(f"{cmd_dir}/speckit.{stem}.toml") - # Integration scripts - files.append(f".specify/integrations/{self.KEY}/scripts/update-context.ps1") - files.append(f".specify/integrations/{self.KEY}/scripts/update-context.sh") + # Integration scripts — only the matching variant is installed + if script_variant == "sh": + files.append(f".specify/integrations/{self.KEY}/scripts/update-context.sh") + else: + files.append(f".specify/integrations/{self.KEY}/scripts/update-context.ps1") # Framework files files.append(f".specify/integration.json") diff --git a/tests/integrations/test_integration_claude.py b/tests/integrations/test_integration_claude.py index 7fd69df176..1323f9e66b 100644 --- a/tests/integrations/test_integration_claude.py +++ b/tests/integrations/test_integration_claude.py @@ -70,11 +70,11 @@ def test_setup_installs_update_context_scripts(self, tmp_path): scripts_dir = tmp_path / ".specify" / "integrations" / "claude" / "scripts" assert scripts_dir.is_dir() assert (scripts_dir / "update-context.sh").exists() - assert (scripts_dir / "update-context.ps1").exists() + assert not (scripts_dir / "update-context.ps1").exists() tracked = {path.resolve().relative_to(tmp_path.resolve()).as_posix() for path in created} assert ".specify/integrations/claude/scripts/update-context.sh" in tracked - assert ".specify/integrations/claude/scripts/update-context.ps1" in tracked + assert ".specify/integrations/claude/scripts/update-context.ps1" not in tracked def test_ai_flag_auto_promotes_and_enables_skills(self, tmp_path): from typer.testing import CliRunner diff --git a/tests/integrations/test_integration_forge.py b/tests/integrations/test_integration_forge.py index 7affd0d160..b888cdbb5f 100644 --- a/tests/integrations/test_integration_forge.py +++ b/tests/integrations/test_integration_forge.py @@ -77,15 +77,15 @@ def test_setup_installs_update_scripts(self, tmp_path): from specify_cli.integrations.forge import ForgeIntegration forge = ForgeIntegration() m = IntegrationManifest("forge", tmp_path) - created = forge.setup(tmp_path, m) + created = forge.setup(tmp_path, m, script_type="sh") script_files = [f for f in created if "scripts" in f.parts] assert len(script_files) > 0 sh_script = tmp_path / ".specify" / "integrations" / "forge" / "scripts" / "update-context.sh" ps_script = tmp_path / ".specify" / "integrations" / "forge" / "scripts" / "update-context.ps1" assert sh_script in created - assert ps_script in created + assert ps_script not in created assert sh_script.exists() - assert ps_script.exists() + assert not ps_script.exists() def test_all_created_files_tracked_in_manifest(self, tmp_path): from specify_cli.integrations.forge import ForgeIntegration