Skip to content

Commit fdd0003

Browse files
fix: gate .claude/commands/ deployment behind integrate_claude flag (#443)
* fix: gate .claude/commands/ deployment behind integrate_claude flag The CommandIntegrator was called unconditionally in _integrate_package_primitives(), ignoring the integrate_claude flag derived from the detected or configured target. This caused .claude/commands/ to be created on every apm install, even when the project is configured with target: copilot (vscode), which sets integrate_claude=False. The docstring of should_integrate_claude() explicitly states that 'commands' should be governed by this flag, matching the pattern already applied to Claude agents and hooks. The OpenCode commands integrator has a similar self-guard (.opencode/ must exist). Fix: wrap integrate_package_commands() in if integrate_claude: so it is consistent with all other Claude-specific integration paths. Reproducer: set target: copilot in apm.yml, add .prompt.md files under .apm/prompts/, run apm install — .claude/commands/ is created despite no Claude target being configured. * test: add regression tests for integrate_claude gating of .claude/commands/ Covers the fix in _integrate_package_primitives(): - When integrate_claude=False (target=copilot/vscode), integrate_package_commands must not be called and .claude/commands/ must not be created. - When integrate_claude=True (target=claude/all), integrate_package_commands must be called. * test: fix regression test import --------- Co-authored-by: Daniel Meppiel <51440732+danielmeppiel@users.noreply.github.com>
1 parent 55c715f commit fdd0003

2 files changed

Lines changed: 133 additions & 13 deletions

File tree

src/apm_cli/commands/install.py

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -971,19 +971,20 @@ def _log_integration(msg):
971971
deployed.append(tp.relative_to(project_root).as_posix())
972972

973973
# --- commands (.claude) ---
974-
command_result = command_integrator.integrate_package_commands(
975-
package_info, project_root,
976-
force=force, managed_files=managed_files,
977-
diagnostics=diagnostics,
978-
)
979-
if command_result.files_integrated > 0:
980-
result["commands"] += command_result.files_integrated
981-
_log_integration(f" └─ {command_result.files_integrated} commands integrated -> .claude/commands/")
982-
if command_result.files_updated > 0:
983-
_log_integration(f" └─ {command_result.files_updated} commands updated")
984-
result["links_resolved"] += command_result.links_resolved
985-
for tp in command_result.target_paths:
986-
deployed.append(tp.relative_to(project_root).as_posix())
974+
if integrate_claude:
975+
command_result = command_integrator.integrate_package_commands(
976+
package_info, project_root,
977+
force=force, managed_files=managed_files,
978+
diagnostics=diagnostics,
979+
)
980+
if command_result.files_integrated > 0:
981+
result["commands"] += command_result.files_integrated
982+
_log_integration(f" └─ {command_result.files_integrated} commands integrated -> .claude/commands/")
983+
if command_result.files_updated > 0:
984+
_log_integration(f" └─ {command_result.files_updated} commands updated")
985+
result["links_resolved"] += command_result.links_resolved
986+
for tp in command_result.target_paths:
987+
deployed.append(tp.relative_to(project_root).as_posix())
987988

988989
# --- OpenCode commands (.opencode) ---
989990
opencode_command_result = command_integrator.integrate_package_commands_opencode(

tests/unit/integration/test_command_integrator.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,3 +365,122 @@ def test_sync_handles_missing_dir(self, temp_project_no_opencode):
365365
integrator = CommandIntegrator()
366366
result = integrator.sync_integration_opencode(None, temp_project_no_opencode)
367367
assert result["files_removed"] == 0
368+
369+
370+
class TestIntegratePackagePrimitivesTargetGating:
371+
"""Tests that _integrate_package_primitives respects the integrate_claude flag.
372+
373+
Regression test for: CommandIntegrator was called unconditionally, causing
374+
.claude/commands/ to be created even when target=copilot (integrate_claude=False).
375+
"""
376+
377+
def _make_mock_integrators(self):
378+
"""Return a dict of MagicMock integrators for _integrate_package_primitives."""
379+
from unittest.mock import MagicMock
380+
381+
def _empty_result(*args, **kwargs):
382+
r = MagicMock()
383+
r.files_integrated = 0
384+
r.files_updated = 0
385+
r.links_resolved = 0
386+
r.target_paths = []
387+
r.skill_created = False
388+
r.sub_skills_promoted = 0
389+
r.hooks_integrated = 0
390+
return r
391+
392+
integrators = {}
393+
for name in (
394+
"prompt_integrator",
395+
"agent_integrator",
396+
"skill_integrator",
397+
"instruction_integrator",
398+
"command_integrator",
399+
"hook_integrator",
400+
):
401+
m = MagicMock()
402+
for method in (
403+
"integrate_package_prompts",
404+
"integrate_package_agents",
405+
"integrate_package_agents_claude",
406+
"integrate_package_agents_cursor",
407+
"integrate_package_agents_opencode",
408+
"integrate_package_skill",
409+
"integrate_package_instructions",
410+
"integrate_package_instructions_cursor",
411+
"integrate_package_commands",
412+
"integrate_package_commands_opencode",
413+
"integrate_package_hooks",
414+
"integrate_package_hooks_claude",
415+
"integrate_package_hooks_cursor",
416+
):
417+
getattr(m, method).side_effect = _empty_result
418+
integrators[name] = m
419+
return integrators
420+
421+
def test_integrate_claude_false_does_not_call_integrate_package_commands(self):
422+
"""When integrate_claude=False, integrate_package_commands must not be called.
423+
424+
This is the regression test for the bug where .claude/commands/ was created
425+
even when target=copilot (vscode) set integrate_claude=False.
426+
"""
427+
import tempfile, shutil
428+
from apm_cli.commands.install import _integrate_package_primitives
429+
from apm_cli.utils.diagnostics import DiagnosticCollector
430+
431+
temp_dir = tempfile.mkdtemp()
432+
try:
433+
project_root = Path(temp_dir)
434+
(project_root / ".github").mkdir()
435+
436+
package_info = MagicMock()
437+
integrators = self._make_mock_integrators()
438+
diagnostics = DiagnosticCollector(verbose=False)
439+
440+
_integrate_package_primitives(
441+
package_info,
442+
project_root,
443+
integrate_vscode=True,
444+
integrate_claude=False,
445+
integrate_opencode=False,
446+
managed_files=set(),
447+
force=False,
448+
diagnostics=diagnostics,
449+
**integrators,
450+
)
451+
452+
integrators["command_integrator"].integrate_package_commands.assert_not_called()
453+
assert not (project_root / ".claude" / "commands").exists()
454+
finally:
455+
shutil.rmtree(temp_dir, ignore_errors=True)
456+
457+
def test_integrate_claude_true_calls_integrate_package_commands(self):
458+
"""When integrate_claude=True, integrate_package_commands must be called."""
459+
import tempfile, shutil
460+
from apm_cli.commands.install import _integrate_package_primitives
461+
from apm_cli.utils.diagnostics import DiagnosticCollector
462+
463+
temp_dir = tempfile.mkdtemp()
464+
try:
465+
project_root = Path(temp_dir)
466+
(project_root / ".claude").mkdir()
467+
468+
package_info = MagicMock()
469+
integrators = self._make_mock_integrators()
470+
diagnostics = DiagnosticCollector(verbose=False)
471+
472+
_integrate_package_primitives(
473+
package_info,
474+
project_root,
475+
integrate_vscode=False,
476+
integrate_claude=True,
477+
integrate_opencode=False,
478+
managed_files=set(),
479+
force=False,
480+
diagnostics=diagnostics,
481+
**integrators,
482+
)
483+
484+
integrators["command_integrator"].integrate_package_commands.assert_called_once()
485+
finally:
486+
shutil.rmtree(temp_dir, ignore_errors=True)

0 commit comments

Comments
 (0)