Skip to content

Commit 94a348a

Browse files
committed
Remove --force from integration install, ensure shared infra on install/switch
- Remove --force parameter entirely from integration install; users must uninstall before reinstalling to prevent orphaned files - Auto-install shared infrastructure (.specify/scripts/, .specify/templates/) when missing during install or switch - Add test for shared infra creation on bare project install
1 parent ffc1b66 commit 94a348a

2 files changed

Lines changed: 37 additions & 25 deletions

File tree

β€Žsrc/specify_cli/__init__.pyβ€Ž

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1608,7 +1608,6 @@ def integration_install(
16081608
key: str = typer.Argument(help="Integration key to install (e.g. claude, copilot)"),
16091609
script: str = typer.Option(None, "--script", help="Script type: sh or ps (default: from init-options.json or platform default)"),
16101610
integration_options: str = typer.Option(None, "--integration-options", help='Options for the integration (e.g. --integration-options="--commands-dir .myagent/cmds")'),
1611-
force: bool = typer.Option(False, "--force", help="Overwrite existing integration without uninstalling first"),
16121611
):
16131612
"""Install an integration into an existing project."""
16141613
from .integrations import INTEGRATION_REGISTRY, get_integration
@@ -1632,23 +1631,24 @@ def integration_install(
16321631
current = _read_integration_json(project_root)
16331632
installed_key = current.get("integration")
16341633

1635-
if installed_key and installed_key == key and not force:
1634+
if installed_key and installed_key == key:
16361635
console.print(f"[yellow]Integration '{key}' is already installed.[/yellow]")
1637-
console.print("Use [cyan]--force[/cyan] to reinstall, or [cyan]specify integration switch <target>[/cyan] to change.")
1636+
console.print("Run [cyan]specify integration uninstall[/cyan] first, then reinstall.")
16381637
raise typer.Exit(0)
16391638

16401639
if installed_key and installed_key != key:
16411640
console.print(f"[red]Error:[/red] Integration '{installed_key}' is already installed.")
1642-
console.print(f"Use [cyan]specify integration switch {key}[/cyan] to switch integrations.")
1643-
if force:
1644-
console.print(
1645-
"[yellow]--force only supports reinstalling the currently installed integration "
1646-
"and cannot overwrite a different integration.[/yellow]"
1647-
)
1641+
console.print(f"Run [cyan]specify integration uninstall[/cyan] first, or use [cyan]specify integration switch {key}[/cyan].")
16481642
raise typer.Exit(1)
16491643

16501644
selected_script = _resolve_script_type(project_root, script)
16511645

1646+
# Ensure shared infrastructure exists
1647+
if not (project_root / ".specify" / "scripts").exists():
1648+
_install_shared_infra(project_root, selected_script)
1649+
if os.name != "nt":
1650+
ensure_executable_scripts(project_root)
1651+
16521652
manifest = IntegrationManifest(
16531653
integration.key, project_root, version=get_speckit_version()
16541654
)
@@ -1862,6 +1862,12 @@ def integration_switch(
18621862
opts.pop("ai_skills", None)
18631863
save_init_options(project_root, opts)
18641864

1865+
# Ensure shared infrastructure exists
1866+
if not (project_root / ".specify" / "scripts").exists():
1867+
_install_shared_infra(project_root, selected_script)
1868+
if os.name != "nt":
1869+
ensure_executable_scripts(project_root)
1870+
18651871
# Phase 2: Install target integration
18661872
console.print(f"Installing integration: [cyan]{target}[/cyan]")
18671873
manifest = IntegrationManifest(

β€Žtests/integrations/test_integration_subcommand.pyβ€Ž

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ def test_install_already_installed(self, tmp_path):
106106
os.chdir(old_cwd)
107107
assert result.exit_code == 0
108108
assert "already installed" in result.output
109+
assert "uninstall" in result.output
109110

110111
def test_install_different_when_one_exists(self, tmp_path):
111112
project = _init_project(tmp_path, "copilot")
@@ -117,22 +118,7 @@ def test_install_different_when_one_exists(self, tmp_path):
117118
os.chdir(old_cwd)
118119
assert result.exit_code != 0
119120
assert "already installed" in result.output
120-
121-
def test_force_blocked_with_different_integration(self, tmp_path):
122-
"""--force must not allow overwriting a different integration."""
123-
project = _init_project(tmp_path, "copilot")
124-
old_cwd = os.getcwd()
125-
try:
126-
os.chdir(project)
127-
result = runner.invoke(app, [
128-
"integration", "install", "claude", "--force",
129-
"--script", "sh",
130-
])
131-
finally:
132-
os.chdir(old_cwd)
133-
assert result.exit_code != 0
134-
assert "already installed" in result.output
135-
assert "cannot overwrite a different integration" in result.output
121+
assert "uninstall" in result.output
136122

137123
def test_install_into_bare_project(self, tmp_path):
138124
"""Install into a project with .specify/ but no integration."""
@@ -161,6 +147,26 @@ def test_install_into_bare_project(self, tmp_path):
161147
# Claude uses skills directory (not commands)
162148
assert (project / ".claude" / "skills" / "speckit-plan" / "SKILL.md").exists()
163149

150+
def test_install_bare_project_gets_shared_infra(self, tmp_path):
151+
"""Installing into a bare project should create shared scripts and templates."""
152+
project = tmp_path / "bare"
153+
project.mkdir()
154+
(project / ".specify").mkdir()
155+
old_cwd = os.getcwd()
156+
try:
157+
os.chdir(project)
158+
result = runner.invoke(app, [
159+
"integration", "install", "claude",
160+
"--script", "sh",
161+
], catch_exceptions=False)
162+
finally:
163+
os.chdir(old_cwd)
164+
assert result.exit_code == 0, result.output
165+
166+
# Shared infrastructure should be present
167+
assert (project / ".specify" / "scripts").is_dir()
168+
assert (project / ".specify" / "templates").is_dir()
169+
164170

165171
# ── uninstall ────────────────────────────────────────────────────────
166172

0 commit comments

Comments
Β (0)