From e888829f9d3e1c569b59f50dfcb1a8da14aa45ad Mon Sep 17 00:00:00 2001 From: Sergio Sisternes Date: Mon, 6 Jul 2026 00:21:27 +0100 Subject: [PATCH 01/16] Fix --target intellij validation (#1957) Add intellij to VALID_TARGET_VALUES via a new MCP_ONLY_TARGETS set so that `apm install --target intellij` passes the CLI parser. Previously the adapter, factory registration, auto-detection, and MCP install logic all existed but the --target flag rejected the token at parse time. MCP-only pseudo-targets (like intellij) have a client adapter but no KNOWN_TARGETS entry -- they map to a canonical target for primitive deployment via RUNTIME_TO_CANONICAL_TARGET. The new MCP_ONLY_TARGETS frozenset captures this category explicitly and is included in VALID_TARGET_VALUES alongside the canonical, experimental, and explicit-only sets. Also allows `all,intellij` combination (same pattern as `all,agent-skills`). Closes #1957 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/apm_cli/core/target_detection.py | 10 +++++++++- tests/unit/core/test_target_detection.py | 23 +++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/apm_cli/core/target_detection.py b/src/apm_cli/core/target_detection.py index a9542d5d6..155ae1788 100644 --- a/src/apm_cli/core/target_detection.py +++ b/src/apm_cli/core/target_detection.py @@ -416,6 +416,13 @@ def get_target_description(target: UserTargetType) -> str: #: so there is no Antigravity-unique signal to auto-detect on. EXPLICIT_ONLY_TARGETS: frozenset[str] = frozenset({"agent-skills", "antigravity"}) +#: MCP-only pseudo-targets that have a client adapter but no +#: ``KNOWN_TARGETS`` entry (they map to a canonical target for primitive +#: deployment via ``RUNTIME_TO_CANONICAL_TARGET``). They must be accepted +#: by ``--target`` so the CLI validates them, but they are excluded from +#: ``"all"`` expansion and do not participate in target-profile machinery. +MCP_ONLY_TARGETS: frozenset[str] = frozenset({"intellij"}) + #: Alias mapping: user-facing name -> canonical internal name. TARGET_ALIASES: dict[str, str] = { "copilot": "vscode", @@ -496,6 +503,7 @@ def normalize_target_list( ALL_CANONICAL_TARGETS | EXPERIMENTAL_TARGETS | EXPLICIT_ONLY_TARGETS + | MCP_ONLY_TARGETS | frozenset(TARGET_ALIASES) | frozenset({"all"}) ) @@ -610,7 +618,7 @@ def parse_target_field( # ---- "all" handling ---- if "all" in raw_parts: non_all_tokens = {t for t in raw_parts if t != "all"} - if non_all_tokens - EXPLICIT_ONLY_TARGETS: + if non_all_tokens - EXPLICIT_ONLY_TARGETS - MCP_ONLY_TARGETS: raise ValueError( _target_error( "'all' cannot be combined with other targets", diff --git a/tests/unit/core/test_target_detection.py b/tests/unit/core/test_target_detection.py index eda600f7b..fbebc5673 100644 --- a/tests/unit/core/test_target_detection.py +++ b/tests/unit/core/test_target_detection.py @@ -556,6 +556,10 @@ def test_valid_target_values_includes_all(self): """VALID_TARGET_VALUES contains 'all'.""" assert "all" in VALID_TARGET_VALUES + def test_valid_target_values_includes_intellij(self): + """VALID_TARGET_VALUES contains 'intellij' (MCP-only target, #1957).""" + assert "intellij" in VALID_TARGET_VALUES + # -- None passthrough ------------------------------------------------- def test_none_returns_none(self): @@ -608,6 +612,10 @@ def test_single_all(self): """'all' returns string 'all' for backward compat.""" assert self.tp.convert("all", None, None) == "all" + def test_single_intellij(self): + """intellij is accepted as a valid MCP-only target (#1957).""" + assert self.tp.convert("intellij", None, None) == "intellij" + def test_single_target_returns_string_type(self): """Single target must return str, not list.""" result = self.tp.convert("claude", None, None) @@ -646,6 +654,11 @@ def test_multi_three_targets(self): result = self.tp.convert("claude,cursor,codex", None, None) assert result == ["claude", "cursor", "codex"] + def test_multi_intellij_with_claude(self): + """intellij,claude keeps intellij as-is in multi-target (#1957).""" + result = self.tp.convert("intellij,claude", None, None) + assert result == ["intellij", "claude"] + # -- Alias deduplication ---------------------------------------------- def test_copilot_vscode_deduplicates(self): @@ -869,6 +882,16 @@ def test_all_combined_with_codex_still_rejected(self): with pytest.raises(click.exceptions.BadParameter, match="cannot be combined"): self.tp.convert("all,codex", None, None) + def test_all_combined_with_intellij_allowed(self): + """'all,intellij' is allowed -- intellij is an MCP-only target (#1957).""" + from apm_cli.core.target_detection import parse_target_field + + result = parse_target_field("all,intellij") + assert isinstance(result, list) + for t in ALL_CANONICAL_TARGETS: + assert t in result + assert "intellij" in result + # --------------------------------------------------------------------------- # Cowork parser-layer regression tests (2f96dd5 / #926) From 0fcbd20e8cc6528b927c99c2f4e65815152bb4d6 Mon Sep 17 00:00:00 2001 From: Sergio Sisternes Date: Mon, 6 Jul 2026 00:42:17 +0100 Subject: [PATCH 02/16] Map intellij through detect_target() to vscode Address review feedback: detect_target() now maps explicit_target and config_target 'intellij' to 'vscode' (same group as copilot/agents), preventing silent fallthrough to auto-detect when --target intellij is used with apm compile or apm pack. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/apm_cli/core/target_detection.py | 4 ++-- tests/unit/core/test_target_detection.py | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/apm_cli/core/target_detection.py b/src/apm_cli/core/target_detection.py index 155ae1788..cc4f8b0cc 100644 --- a/src/apm_cli/core/target_detection.py +++ b/src/apm_cli/core/target_detection.py @@ -126,7 +126,7 @@ def detect_target( # noqa: PLR0911 """ # Priority 1: Explicit --target flag if explicit_target: - if explicit_target in ("copilot", "vscode", "agents"): + if explicit_target in ("copilot", "vscode", "agents", "intellij"): return "vscode", "explicit --target flag" elif explicit_target == "claude": return "claude", "explicit --target flag" @@ -151,7 +151,7 @@ def detect_target( # noqa: PLR0911 # Priority 2: apm.yml target setting if config_target: - if config_target in ("copilot", "vscode", "agents"): + if config_target in ("copilot", "vscode", "agents", "intellij"): return "vscode", "apm.yml target" elif config_target == "claude": return "claude", "apm.yml target" diff --git a/tests/unit/core/test_target_detection.py b/tests/unit/core/test_target_detection.py index fbebc5673..fd291c8e5 100644 --- a/tests/unit/core/test_target_detection.py +++ b/tests/unit/core/test_target_detection.py @@ -60,6 +60,16 @@ def test_explicit_target_agents_maps_to_vscode(self, tmp_path): assert target == "vscode" assert reason == "explicit --target flag" + def test_explicit_target_intellij_maps_to_vscode(self, tmp_path): + """Explicit --target intellij maps to vscode (MCP-only target, #1957).""" + target, reason = detect_target( + project_root=tmp_path, + explicit_target="intellij", + ) + + assert target == "vscode" + assert reason == "explicit --target flag" + def test_explicit_target_claude_wins(self, tmp_path): """Explicit --target claude always wins.""" (tmp_path / ".github").mkdir() From 505261058fa6307332f980181b75f2bfd25aa377 Mon Sep 17 00:00:00 2001 From: Sergio Sisternes Date: Tue, 7 Jul 2026 14:15:09 +0100 Subject: [PATCH 03/16] Address review panel follow-ups for intellij target (#2041) - Normalise intellij -> copilot in policy_target_check.py via RUNTIME_TO_CANONICAL_TARGET so org allow-lists are not falsely rejected - Add intellij to --target help text in install.py with MCP-only note - Add CHANGELOG entry under [Unreleased] Fixed - Update ide-tool-integration.md from --runtime intellij to --target intellij - Add constant-split guard tests: intellij not in ALL_CANONICAL_TARGETS, in MCP_ONLY_TARGETS, and all-expansion excludes intellij - Create tests/integration/test_intellij_target.py with constant guards, CLI E2E parser acceptance, and policy normalisation tests Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../docs/integrations/ide-tool-integration.md | 4 +- src/apm_cli/commands/install.py | 2 +- .../install/phases/policy_target_check.py | 8 + tests/integration/test_intellij_target.py | 147 ++++++++++++++++++ tests/unit/core/test_target_detection.py | 35 +++++ 5 files changed, 193 insertions(+), 3 deletions(-) create mode 100644 tests/integration/test_intellij_target.py diff --git a/docs/src/content/docs/integrations/ide-tool-integration.md b/docs/src/content/docs/integrations/ide-tool-integration.md index 79497b9f6..fb8eea8af 100644 --- a/docs/src/content/docs/integrations/ide-tool-integration.md +++ b/docs/src/content/docs/integrations/ide-tool-integration.md @@ -141,7 +141,7 @@ that directory is the auto-detect signal. ```bash # Install an MCP server into the JetBrains user-scope config -apm install --mcp --runtime intellij +apm install --mcp --target intellij ``` Notes and limits: @@ -149,7 +149,7 @@ Notes and limits: - **Auto-detect is user-scope only.** Unlike project markers such as `.cursor/` or `.windsurf/`, JetBrains is detected from the global config directory, not a file in your repo. It is therefore detected for every project on the machine - once the plugin directory exists. Use `--runtime intellij` to target it + once the plugin directory exists. Use `--target intellij` to target it explicitly regardless of auto-detect. - **Runtime env substitution.** JetBrains Copilot resolves `${env:VAR}` in `mcp.json` at server start. APM preserves env-var placeholders as diff --git a/src/apm_cli/commands/install.py b/src/apm_cli/commands/install.py index e90d07c86..d692d35da 100644 --- a/src/apm_cli/commands/install.py +++ b/src/apm_cli/commands/install.py @@ -944,7 +944,7 @@ def _handle_mcp_install( "target", type=TargetParamType(), default=None, - help="Target harness(es) to deploy to. Comma-separated for multiple: --target claude,cursor. Repeating the flag (e.g. '-t a -t b') is NOT supported -- only the last value wins; use commas. Highest-priority entry in the resolution chain (--target > apm.yml targets: > apm config target > auto-detect). Values: copilot, claude, cursor, opencode, codex, gemini, antigravity, windsurf, kiro, agent-skills, all. 'agent-skills' deploys to .agents/skills/ (cross-client). 'antigravity' (alias 'agy') deploys to .agents/ (AGENTS.md + rules + skills + hooks.json + mcp_config.json) and is explicit-only -- not part of 'all' or auto-detection. 'all' = copilot+claude+cursor+opencode+codex+gemini+windsurf+kiro (excludes agent-skills and antigravity); combine with 'agent-skills' or 'antigravity' to add them. 'copilot-cowork' is also accepted when the copilot-cowork experimental flag is enabled (run 'apm experimental enable copilot-cowork'). 'copilot-app' is also accepted when the copilot-app experimental flag is enabled (run 'apm experimental enable copilot-app'). Note: '--target all' on 'apm compile' is deprecated; use 'apm compile --all' instead.", + help="Target harness(es) to deploy to. Comma-separated for multiple: --target claude,cursor. Repeating the flag (e.g. '-t a -t b') is NOT supported -- only the last value wins; use commas. Highest-priority entry in the resolution chain (--target > apm.yml targets: > apm config target > auto-detect). Values: copilot, claude, cursor, opencode, codex, gemini, antigravity, windsurf, kiro, intellij, agent-skills, all. 'intellij' is MCP-only -- it configures the JetBrains GitHub Copilot plugin MCP server list but does not deploy file-level primitives. 'agent-skills' deploys to .agents/skills/ (cross-client). 'antigravity' (alias 'agy') deploys to .agents/ (AGENTS.md + rules + skills + hooks.json + mcp_config.json) and is explicit-only -- not part of 'all' or auto-detection. 'all' = copilot+claude+cursor+opencode+codex+gemini+windsurf+kiro (excludes agent-skills, antigravity, and intellij); combine with 'agent-skills', 'antigravity', or 'intellij' to add them. 'copilot-cowork' is also accepted when the copilot-cowork experimental flag is enabled (run 'apm experimental enable copilot-cowork'). 'copilot-app' is also accepted when the copilot-app experimental flag is enabled (run 'apm experimental enable copilot-app'). Note: '--target all' on 'apm compile' is deprecated; use 'apm compile --all' instead.", ) @click.option( "--allow-insecure", diff --git a/src/apm_cli/install/phases/policy_target_check.py b/src/apm_cli/install/phases/policy_target_check.py index 524d28b2b..21b2a9e94 100644 --- a/src/apm_cli/install/phases/policy_target_check.py +++ b/src/apm_cli/install/phases/policy_target_check.py @@ -60,6 +60,14 @@ def run(ctx: InstallContext) -> None: if effective_target is None: return # no target to check -- trivially passes + # ------------------------------------------------------------------ + # 3b. Normalize MCP-only pseudo-targets to their canonical form so + # policy allow-lists match (e.g. intellij -> copilot). + # ------------------------------------------------------------------ + from apm_cli.integration.targets import RUNTIME_TO_CANONICAL_TARGET + + effective_target = RUNTIME_TO_CANONICAL_TARGET.get(effective_target, effective_target) + # ------------------------------------------------------------------ # 4. Run policy checks with effective_target populated # ------------------------------------------------------------------ diff --git a/tests/integration/test_intellij_target.py b/tests/integration/test_intellij_target.py new file mode 100644 index 000000000..ab10f46ab --- /dev/null +++ b/tests/integration/test_intellij_target.py @@ -0,0 +1,147 @@ +"""Integration tests for the 'intellij' MCP-only pseudo-target. + +Covers: + 1. Parser-layer constants: intellij in VALID_TARGET_VALUES / MCP_ONLY_TARGETS, + not in ALL_CANONICAL_TARGETS. + 2. TargetParamType accepts intellij as single and multi-target. + 3. CLI parser accepts ``--target intellij`` without error. + 4. Policy target check normalises intellij -> copilot via + RUNTIME_TO_CANONICAL_TARGET so org allow-lists are not falsely rejected. + +IntelliJ is an MCP-only pseudo-target: it has an IntelliJClientAdapter +registered in the MCP client registry but no KNOWN_TARGETS entry and no +file-level primitive deployment. It maps to 'copilot' via +RUNTIME_TO_CANONICAL_TARGET. + +Ref: issue #1957, PR #2041. +""" + +from __future__ import annotations + +from pathlib import Path + +import pytest +from click.testing import CliRunner + +from apm_cli.cli import cli +from apm_cli.core.target_detection import ( + ALL_CANONICAL_TARGETS, + MCP_ONLY_TARGETS, + VALID_TARGET_VALUES, + TargetParamType, + normalize_target_list, +) +from apm_cli.integration.targets import RUNTIME_TO_CANONICAL_TARGET + +_MINIMAL_APM_YML = "name: test\ndescription: test\nversion: 0.0.1\n" +_BASE_ENV: dict[str, str] = {"APM_E2E_TESTS": "1"} + + +@pytest.fixture() +def fake_home(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path: + """Isolated home directory wired into every APM config lookup.""" + home = tmp_path / "home" + apm_dir = home / ".apm" + apm_dir.mkdir(parents=True) + (apm_dir / "apm.yml").write_text(_MINIMAL_APM_YML, encoding="ascii") + + monkeypatch.setattr(Path, "home", staticmethod(lambda: home)) + + import apm_cli.config as _conf + + monkeypatch.setattr(_conf, "CONFIG_DIR", str(apm_dir)) + monkeypatch.setattr(_conf, "CONFIG_FILE", str(apm_dir / "config.json")) + monkeypatch.setattr(_conf, "_config_cache", None) + yield home + monkeypatch.setattr(_conf, "_config_cache", None) + + +# =========================================================================== +# Constant guards +# =========================================================================== + + +class TestIntelliJConstants: + """Constant-split guards ensuring intellij stays MCP-only.""" + + def test_intellij_in_valid_target_values(self) -> None: + """intellij must be accepted by --target.""" + assert "intellij" in VALID_TARGET_VALUES + + def test_intellij_not_in_all_canonical_targets(self) -> None: + """intellij must NOT bleed into ALL_CANONICAL_TARGETS / 'all'.""" + assert "intellij" not in ALL_CANONICAL_TARGETS + + def test_intellij_in_mcp_only_targets(self) -> None: + """intellij must live in MCP_ONLY_TARGETS.""" + assert "intellij" in MCP_ONLY_TARGETS + + def test_intellij_runtime_canonical_maps_to_copilot(self) -> None: + """RUNTIME_TO_CANONICAL_TARGET must map intellij -> copilot.""" + assert RUNTIME_TO_CANONICAL_TARGET.get("intellij") == "copilot" + + def test_parser_accepts_single(self) -> None: + """TargetParamType.convert accepts 'intellij' as a single token.""" + tp = TargetParamType() + result = tp.convert("intellij", None, None) + assert result == "intellij" + + def test_parser_accepts_multi(self) -> None: + """TargetParamType.convert accepts 'intellij,claude' as multi-target.""" + tp = TargetParamType() + result = tp.convert("intellij,claude", None, None) + assert isinstance(result, list) + assert "intellij" in result + assert "claude" in result + + def test_all_expansion_excludes_intellij(self) -> None: + """normalize_target_list('all') must NOT include intellij.""" + result = normalize_target_list("all") + assert "intellij" not in result + + +# =========================================================================== +# CLI E2E +# =========================================================================== + + +class TestIntelliJCliE2E: + """CliRunner tests for 'apm install --target intellij'.""" + + def test_cli_parser_accepts_target_intellij(self, tmp_path: Path, fake_home: Path) -> None: + """``apm install --target intellij`` must not fail with 'Unknown target'. + + We do not assert a successful install (no package is provided), + just that the CLI parser accepts the target token without error. + """ + runner = CliRunner() + result = runner.invoke( + cli, + ["install", "--target", "intellij"], + env=_BASE_ENV, + catch_exceptions=False, + ) + # No "Unknown target" error -- the parser accepted the value. + # The command will fail for other reasons (no package specified) + # but that is fine -- we only care about parser acceptance. + assert "Unknown target" not in (result.output or "") + + +# =========================================================================== +# Policy target normalisation +# =========================================================================== + + +class TestIntelliJPolicyNormalisation: + """Verify policy_target_check normalises intellij -> copilot.""" + + def test_runtime_to_canonical_normalisation(self) -> None: + """The RUNTIME_TO_CANONICAL_TARGET mapping must resolve intellij. + + This is the mapping used in policy_target_check.py to normalise + the effective_target before passing it to _check_compilation_target. + An org with ``allow: [copilot]`` must not reject --target intellij. + """ + effective_target = "intellij" + normalised = RUNTIME_TO_CANONICAL_TARGET.get(effective_target, effective_target) + assert normalised == "copilot" diff --git a/tests/unit/core/test_target_detection.py b/tests/unit/core/test_target_detection.py index fd291c8e5..0e3d6a4be 100644 --- a/tests/unit/core/test_target_detection.py +++ b/tests/unit/core/test_target_detection.py @@ -8,6 +8,7 @@ from apm_cli.core.target_detection import ( ALL_CANONICAL_TARGETS, EXPERIMENTAL_TARGETS, + MCP_ONLY_TARGETS, VALID_TARGET_VALUES, TargetParamType, can_dedup_agents_md_instructions, @@ -903,6 +904,40 @@ def test_all_combined_with_intellij_allowed(self): assert "intellij" in result +class TestIntelliJConstantGuards: + """Constant-split guards ensuring intellij stays MCP-only (#1957). + + IntelliJ is an MCP-only pseudo-target -- it must be in MCP_ONLY_TARGETS + and VALID_TARGET_VALUES, but NOT in ALL_CANONICAL_TARGETS. The 'all' + expansion must never include it. + """ + + def test_intellij_not_in_all_canonical_targets(self): + """'intellij' must NOT appear in ALL_CANONICAL_TARGETS. + + ALL_CANONICAL_TARGETS drives the 'all' expansion. IntelliJ is + MCP-only and must live in MCP_ONLY_TARGETS instead. + """ + assert "intellij" not in ALL_CANONICAL_TARGETS + + def test_intellij_in_mcp_only_targets(self): + """'intellij' must appear in MCP_ONLY_TARGETS (constant guard).""" + assert "intellij" in MCP_ONLY_TARGETS + + def test_all_expansion_excludes_intellij(self): + """normalize_target_list('all') must NOT include 'intellij'. + + 'all' expands only to ALL_CANONICAL_TARGETS. MCP-only targets + require explicit opt-in via '--target intellij' or 'all,intellij'. + """ + result = normalize_target_list("all") + assert isinstance(result, list) + assert "intellij" not in result + # Verify all canonical targets ARE present + for t in ALL_CANONICAL_TARGETS: + assert t in result + + # --------------------------------------------------------------------------- # Cowork parser-layer regression tests (2f96dd5 / #926) # --------------------------------------------------------------------------- From 58dd3556986888aa55ab43f5bd8ea4bae0072d11 Mon Sep 17 00:00:00 2001 From: Sergio Sisternes Date: Tue, 7 Jul 2026 14:19:27 +0100 Subject: [PATCH 04/16] Fix policy target normalisation to only apply to MCP-only targets RUNTIME_TO_CANONICAL_TARGET maps vscode -> copilot too, so applying it unconditionally broke existing tests that pass vscode as effective_target. Guard the normalisation with an MCP_ONLY_TARGETS membership check so only pseudo-targets like intellij are remapped. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/apm_cli/install/phases/policy_target_check.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/apm_cli/install/phases/policy_target_check.py b/src/apm_cli/install/phases/policy_target_check.py index 21b2a9e94..8f0498058 100644 --- a/src/apm_cli/install/phases/policy_target_check.py +++ b/src/apm_cli/install/phases/policy_target_check.py @@ -63,10 +63,14 @@ def run(ctx: InstallContext) -> None: # ------------------------------------------------------------------ # 3b. Normalize MCP-only pseudo-targets to their canonical form so # policy allow-lists match (e.g. intellij -> copilot). + # Only MCP_ONLY_TARGETS need this -- canonical targets like + # 'vscode' must pass through unchanged. # ------------------------------------------------------------------ + from apm_cli.core.target_detection import MCP_ONLY_TARGETS from apm_cli.integration.targets import RUNTIME_TO_CANONICAL_TARGET - effective_target = RUNTIME_TO_CANONICAL_TARGET.get(effective_target, effective_target) + if effective_target in MCP_ONLY_TARGETS: + effective_target = RUNTIME_TO_CANONICAL_TARGET.get(effective_target, effective_target) # ------------------------------------------------------------------ # 4. Run policy checks with effective_target populated From 31ef6078b9009fe8a5c69c1a47a77a6e7e2b302a Mon Sep 17 00:00:00 2001 From: danielmeppiel Date: Fri, 10 Jul 2026 01:10:54 +0200 Subject: [PATCH 05/16] Fix IntelliJ MCP target end-to-end Forward --target through the direct MCP install path, prove the real CLI writes JetBrains Copilot configuration, and align policy, help, and reference documentation with the user-visible behavior. Addresses panel follow-ups for PR #2041. Co-authored-by: Sergio Sisternes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../docs/integrations/ide-tool-integration.md | 2 +- .../src/content/docs/reference/cli/install.md | 2 +- .../content/docs/reference/targets-matrix.md | 6 ++ .../.apm/skills/apm-usage/commands.md | 2 +- src/apm_cli/commands/install.py | 7 +- src/apm_cli/core/target_detection.py | 7 +- src/apm_cli/install/mcp/command.py | 6 +- .../install/phases/policy_target_check.py | 8 +- tests/integration/test_intellij_target.py | 81 +++++++++++++++++++ .../install/test_policy_target_check_phase.py | 16 ++++ 10 files changed, 127 insertions(+), 10 deletions(-) diff --git a/docs/src/content/docs/integrations/ide-tool-integration.md b/docs/src/content/docs/integrations/ide-tool-integration.md index fb8eea8af..b559354a9 100644 --- a/docs/src/content/docs/integrations/ide-tool-integration.md +++ b/docs/src/content/docs/integrations/ide-tool-integration.md @@ -141,7 +141,7 @@ that directory is the auto-detect signal. ```bash # Install an MCP server into the JetBrains user-scope config -apm install --mcp --target intellij +apm install --mcp io.github.github/github-mcp-server --target intellij ``` Notes and limits: diff --git a/docs/src/content/docs/reference/cli/install.md b/docs/src/content/docs/reference/cli/install.md index b6175fe69..0b433c96a 100644 --- a/docs/src/content/docs/reference/cli/install.md +++ b/docs/src/content/docs/reference/cli/install.md @@ -46,7 +46,7 @@ With no arguments it installs everything from `apm.yml`. With one or more `PACKA | Flag | Default | Description | |---|---|---| -| `--target`, `-t VALUE` | auto-detect | Force deployment targets. Comma-separated for multiple (`-t claude,cursor`). Values: `copilot`, `claude`, `cursor`, `opencode`, `codex`, `gemini`, `antigravity`, `windsurf`, `kiro`, `intellij`, `vscode`, `agent-skills`, `all`; experimental `copilot-cowork` and `copilot-app` are also accepted when enabled. `all` expands to every harness above except `agent-skills` and `antigravity`; combine `all,agent-skills` or `all,antigravity` to add them. Highest precedence in the chain `--target` > `apm.yml targets:` > `apm config set target ...` > auto-detect. With nothing to detect, install exits `2` with a teaching message. | +| `--target`, `-t VALUE` | auto-detect | Force deployment targets. Comma-separated for multiple (`-t claude,cursor`). Values: `copilot`, `claude`, `cursor`, `opencode`, `codex`, `gemini`, `antigravity`, `windsurf`, `kiro`, `intellij`, `vscode`, `agent-skills`, `all`; experimental `copilot-cowork` and `copilot-app` are also accepted when enabled. `intellij` is MCP-only and writes JetBrains Copilot's user-scope MCP config without deploying file-level primitives. `all` excludes `agent-skills`, `antigravity`, and `intellij`; combine them explicitly to add them. Highest precedence in the chain `--target` > `apm.yml targets:` > `apm config set target ...` > auto-detect. With nothing to detect, install exits `2` with a teaching message. | | `--runtime VALUE` | unset | Legacy alias for `--target` (single value only). Still accepted; prefer `--target`. | | `--exclude VALUE` | unset | Skip a single runtime that auto-detect or `targets:` would otherwise enable. | | `--only apm\|mcp` | both | Install only APM packages or only MCP servers. | diff --git a/docs/src/content/docs/reference/targets-matrix.md b/docs/src/content/docs/reference/targets-matrix.md index e091827f5..ec7a61c82 100644 --- a/docs/src/content/docs/reference/targets-matrix.md +++ b/docs/src/content/docs/reference/targets-matrix.md @@ -28,6 +28,7 @@ see [Primitive types](./primitive-types/). | opencode | `.opencode/` | [ ] | [ ] | [x] | [x] | [x] | [ ] | [x] | | windsurf | `.windsurf/` + `.agents/` | [x] | [ ] | [ ] | [x] | [x] | [x] | [x] | | kiro | `.kiro/` | [x] | [ ] | [ ] | [x] | [ ] | [x] | [x] | +| intellij | OS-specific user config | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | [x] | | agent-skills | `.agents/` | [ ] | [ ] | [ ] | [x] | [ ] | [ ] | [ ] | Skills deploy to `.agents/skills/` for Copilot, Cursor, OpenCode, @@ -64,6 +65,11 @@ list before `compile` or `install`. | opencode | `.opencode/` directory | | windsurf | `.windsurf/` directory | | kiro | `.kiro/` directory | +| intellij | Global `github-copilot/intellij/` config directory | + +`intellij` is an MCP-only target. It writes the JetBrains Copilot user-scope +`mcp.json` and never participates in file-level primitive deployment or plain +`all` expansion. `agent-skills` is a canonical target key; `antigravity` is explicit-only for auto-detection. Both are available with `--target` and can be listed in a diff --git a/packages/apm-guide/.apm/skills/apm-usage/commands.md b/packages/apm-guide/.apm/skills/apm-usage/commands.md index cf98f2697..76d0994f2 100644 --- a/packages/apm-guide/.apm/skills/apm-usage/commands.md +++ b/packages/apm-guide/.apm/skills/apm-usage/commands.md @@ -10,7 +10,7 @@ | Command | Purpose | Key flags | |---------|---------|-----------| -| `apm install [PKGS...]` | Install APM and MCP dependencies (supports APM packages, Claude skills (SKILL.md), and plugin collections (plugin.json)) | `--update` (deprecated; prefer `apm update`) refresh refs, `--refresh` re-fetch all deps from upstream and re-resolve all ref pins, `--force` overwrite (does NOT refresh refs; use `apm update` for that), `--frozen` CI-safe install that fails fast when `apm.lock.yaml` is missing or out of sync with `apm.yml` (mutually exclusive with `--update`; structural presence check only -- use `apm audit` for SHA integrity), `--dry-run`, `--verbose`, `--only [apm\|mcp]`, `--target` (comma-separated, e.g. `--target claude,cursor`; highest-priority entry in the resolution chain `--target` > apm.yml `targets:` > auto-detect; on auto-bootstrap when no `apm.yml` exists, recognized manifest target(s) are persisted to the new manifest's `targets:` so a later bare `apm update` reuses them; `--target all` deprecated, see `apm compile --all`; use `kiro` for Kiro IDE; use `copilot-cowork` with `--global` after `apm experimental enable copilot-cowork`; use `hermes` after `apm experimental enable hermes` to deploy skills + `AGENTS.md` and, at `--global`, MCP servers to `~/.hermes/config.yaml`), `--dev`, `-g` global (MCP deploys only to user-scope runtimes: Copilot CLI, Claude Code, Codex CLI, Gemini CLI, Antigravity CLI, Kiro, Windsurf, JetBrains Copilot, and Hermes when enabled), `--trust-transitive-mcp`, `--parallel-downloads N`, `--allow-insecure`, `--allow-insecure-host HOSTNAME`, `--skill NAME` install named skill(s) from a skill collection (SKILL_BUNDLE or plugin manifest; repeatable; plugin manifests accept a leaf name or manifest path; persisted in apm.yml; additive across separate installs -- a later `--skill X` adds to the existing pin (union) rather than replacing it, so previously deployed skills are never silently removed; `'*'` resets to the full bundle; drop a single skill by editing the `skills:` list in apm.yml then re-running install), `--legacy-skill-paths` restore per-client skill dirs, `--mcp NAME` add MCP entry (NAME goes through the same `--target` > `targets:` > auto-detect resolver as APM packages, so a project whitelisting `targets: [copilot]` will not write `.cursor/mcp.json` even if `.cursor/` exists; `apm install -g --mcp NAME` writes user-scope and bypasses the project-scope gate by design), `--transport`, `--url`, `--env KEY=VAL`, `--header KEY=VAL`, `--mcp-version`, `--registry URL` custom MCP registry, `--root DIR` redirect writes (`apm_modules/`, lockfile, `.gitignore`, integrated harness files) under DIR while `apm.yml`/`.apm/`/local deps resolve from `$PWD` (mirrors `pip install --target`; created if missing; not valid with `-g`/`--global`, which exits 2) | +| `apm install [PKGS...]` | Install APM and MCP dependencies (supports APM packages, Claude skills (SKILL.md), and plugin collections (plugin.json)) | `--update` (deprecated; prefer `apm update`) refresh refs, `--refresh` re-fetch all deps from upstream and re-resolve all ref pins, `--force` overwrite (does NOT refresh refs; use `apm update` for that), `--frozen` CI-safe install that fails fast when `apm.lock.yaml` is missing or out of sync with `apm.yml` (mutually exclusive with `--update`; structural presence check only -- use `apm audit` for SHA integrity), `--dry-run`, `--verbose`, `--only [apm\|mcp]`, `--target` (comma-separated, e.g. `--target claude,cursor`; highest-priority entry in the resolution chain `--target` > apm.yml `targets:` > auto-detect; `intellij` is MCP-only and writes JetBrains Copilot's user-scope config; on auto-bootstrap when no `apm.yml` exists, recognized manifest target(s) are persisted to the new manifest's `targets:` so a later bare `apm update` reuses them; `--target all` deprecated, see `apm compile --all`; use `kiro` for Kiro IDE; use `copilot-cowork` with `--global` after `apm experimental enable copilot-cowork`; use `hermes` after `apm experimental enable hermes` to deploy skills + `AGENTS.md` and, at `--global`, MCP servers to `~/.hermes/config.yaml`), `--dev`, `-g` global (MCP deploys only to user-scope runtimes: Copilot CLI, Claude Code, Codex CLI, Gemini CLI, Antigravity CLI, Kiro, Windsurf, JetBrains Copilot, and Hermes when enabled), `--trust-transitive-mcp`, `--parallel-downloads N`, `--allow-insecure`, `--allow-insecure-host HOSTNAME`, `--skill NAME` install named skill(s) from a skill collection (SKILL_BUNDLE or plugin manifest; repeatable; plugin manifests accept a leaf name or manifest path; persisted in apm.yml; additive across separate installs -- a later `--skill X` adds to the existing pin (union) rather than replacing it, so previously deployed skills are never silently removed; `'*'` resets to the full bundle; drop a single skill by editing the `skills:` list in apm.yml then re-running install), `--legacy-skill-paths` restore per-client skill dirs, `--mcp NAME` add MCP entry (NAME goes through the same `--target` > `targets:` > auto-detect resolver as APM packages, so `apm install --mcp NAME --target intellij` writes only JetBrains Copilot's MCP config; `apm install -g --mcp NAME` writes user-scope and bypasses the project-scope gate by design), `--transport`, `--url`, `--env KEY=VAL`, `--header KEY=VAL`, `--mcp-version`, `--registry URL` custom MCP registry, `--root DIR` redirect writes (`apm_modules/`, lockfile, `.gitignore`, integrated harness files) under DIR while `apm.yml`/`.apm/`/local deps resolve from `$PWD` (mirrors `pip install --target`; created if missing; not valid with `-g`/`--global`, which exits 2) | | `apm targets` | Show resolved deployment targets for the current project (Click group; reads filesystem signals; works with or without `apm.yml`) | `--all` also include the `agent-skills` meta-target (only meaningful with `--json`), `--json` machine-readable output. No provenance line is printed (the table is the provenance). | | `apm uninstall PKGS...` | Remove packages (accepts `owner/repo` or `name@marketplace`) | `--dry-run`, `-g` global | | `apm prune` | Remove orphaned packages | `--dry-run` | diff --git a/src/apm_cli/commands/install.py b/src/apm_cli/commands/install.py index d692d35da..f3d514737 100644 --- a/src/apm_cli/commands/install.py +++ b/src/apm_cli/commands/install.py @@ -777,7 +777,7 @@ def _validate_and_add_packages_to_apm_yml( # --------------------------------------------------------------------------- -def _handle_mcp_install( +def _handle_mcp_install( # noqa: PLR0913 *, mcp_name, transport, @@ -789,6 +789,7 @@ def _handle_mcp_install( dev, force, runtime, + target, exclude, verbose, logger, @@ -878,6 +879,7 @@ def _handle_mcp_install( dev=dev, force=force, runtime=runtime, + target=target, exclude=exclude, logger=logger, apm_dir=mcp_apm_dir, @@ -944,7 +946,7 @@ def _handle_mcp_install( "target", type=TargetParamType(), default=None, - help="Target harness(es) to deploy to. Comma-separated for multiple: --target claude,cursor. Repeating the flag (e.g. '-t a -t b') is NOT supported -- only the last value wins; use commas. Highest-priority entry in the resolution chain (--target > apm.yml targets: > apm config target > auto-detect). Values: copilot, claude, cursor, opencode, codex, gemini, antigravity, windsurf, kiro, intellij, agent-skills, all. 'intellij' is MCP-only -- it configures the JetBrains GitHub Copilot plugin MCP server list but does not deploy file-level primitives. 'agent-skills' deploys to .agents/skills/ (cross-client). 'antigravity' (alias 'agy') deploys to .agents/ (AGENTS.md + rules + skills + hooks.json + mcp_config.json) and is explicit-only -- not part of 'all' or auto-detection. 'all' = copilot+claude+cursor+opencode+codex+gemini+windsurf+kiro (excludes agent-skills, antigravity, and intellij); combine with 'agent-skills', 'antigravity', or 'intellij' to add them. 'copilot-cowork' is also accepted when the copilot-cowork experimental flag is enabled (run 'apm experimental enable copilot-cowork'). 'copilot-app' is also accepted when the copilot-app experimental flag is enabled (run 'apm experimental enable copilot-app'). Note: '--target all' on 'apm compile' is deprecated; use 'apm compile --all' instead.", + help="Target harness(es) to deploy to. Use commas for multiple targets; repeated flags are not supported. Values: copilot, claude, cursor, opencode, codex, gemini, antigravity, windsurf, kiro, intellij, agent-skills, all. 'intellij' is MCP-only and configures JetBrains Copilot without file-level primitive deployment. 'all' excludes agent-skills, antigravity, and intellij; combine them explicitly when needed. Experimental copilot-cowork and copilot-app values require their feature flags. Resolution order: --target > apm.yml targets: > apm config target > auto-detect.", ) @click.option( "--allow-insecure", @@ -1405,6 +1407,7 @@ def install( # noqa: PLR0913 dev=dev, force=force, runtime=runtime, + target=target, exclude=exclude, verbose=verbose, logger=logger, diff --git a/src/apm_cli/core/target_detection.py b/src/apm_cli/core/target_detection.py index cc4f8b0cc..5e67684f9 100644 --- a/src/apm_cli/core/target_detection.py +++ b/src/apm_cli/core/target_detection.py @@ -126,7 +126,10 @@ def detect_target( # noqa: PLR0911 """ # Priority 1: Explicit --target flag if explicit_target: - if explicit_target in ("copilot", "vscode", "agents", "intellij"): + if ( + explicit_target in ("copilot", "vscode", "agents") + or explicit_target in MCP_ONLY_TARGETS + ): return "vscode", "explicit --target flag" elif explicit_target == "claude": return "claude", "explicit --target flag" @@ -151,7 +154,7 @@ def detect_target( # noqa: PLR0911 # Priority 2: apm.yml target setting if config_target: - if config_target in ("copilot", "vscode", "agents", "intellij"): + if config_target in ("copilot", "vscode", "agents") or config_target in MCP_ONLY_TARGETS: return "vscode", "apm.yml target" elif config_target == "claude": return "claude", "apm.yml target" diff --git a/src/apm_cli/install/mcp/command.py b/src/apm_cli/install/mcp/command.py index aaf8964a9..5ca266b88 100644 --- a/src/apm_cli/install/mcp/command.py +++ b/src/apm_cli/install/mcp/command.py @@ -35,7 +35,7 @@ pass -def run_mcp_install( +def run_mcp_install( # noqa: PLR0913 *, mcp_name: str, transport: str | None, @@ -51,6 +51,7 @@ def run_mcp_install( logger, apm_dir: Path, scope: str | None, + target: str | list[str] | None = None, registry_url: str | None = None, ) -> None: """Execute the --mcp install path. ``registry_url`` is the validated @@ -124,10 +125,11 @@ def run_mcp_install( old_configs = dict(_existing_lock.mcp_configs) if _existing_lock else {} MCPIntegrator.install( [dep], - runtime, + target if isinstance(target, str) else runtime, exclude, verbose, stored_mcp_configs=old_configs, + explicit_target=target, scope=scope, ) new_names = MCPIntegrator.get_server_names([dep]) diff --git a/src/apm_cli/install/phases/policy_target_check.py b/src/apm_cli/install/phases/policy_target_check.py index 8f0498058..30ef4261d 100644 --- a/src/apm_cli/install/phases/policy_target_check.py +++ b/src/apm_cli/install/phases/policy_target_check.py @@ -70,7 +70,13 @@ def run(ctx: InstallContext) -> None: from apm_cli.integration.targets import RUNTIME_TO_CANONICAL_TARGET if effective_target in MCP_ONLY_TARGETS: - effective_target = RUNTIME_TO_CANONICAL_TARGET.get(effective_target, effective_target) + canonical_target = RUNTIME_TO_CANONICAL_TARGET[effective_target] + if ctx.logger: + ctx.logger.verbose_detail( + f"Normalizing MCP-only target '{effective_target}' -> " + f"'{canonical_target}' for policy check" + ) + effective_target = canonical_target # ------------------------------------------------------------------ # 4. Run policy checks with effective_target populated diff --git a/tests/integration/test_intellij_target.py b/tests/integration/test_intellij_target.py index ab10f46ab..941abccc6 100644 --- a/tests/integration/test_intellij_target.py +++ b/tests/integration/test_intellij_target.py @@ -18,6 +18,11 @@ from __future__ import annotations +import json +import os +import shutil +import subprocess +import sys from pathlib import Path import pytest @@ -37,6 +42,16 @@ _BASE_ENV: dict[str, str] = {"APM_E2E_TESTS": "1"} +@pytest.fixture() +def apm_command() -> str: + """Return the APM executable used by the end-to-end test.""" + apm_on_path = shutil.which("apm") + if apm_on_path: + return apm_on_path + venv_apm = Path(__file__).parents[2] / ".venv" / "bin" / "apm" + return str(venv_apm) + + @pytest.fixture() def fake_home(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path: """Isolated home directory wired into every APM config lookup.""" @@ -126,6 +141,72 @@ def test_cli_parser_accepts_target_intellij(self, tmp_path: Path, fake_home: Pat # but that is fine -- we only care about parser acceptance. assert "Unknown target" not in (result.output or "") + def test_mcp_install_writes_intellij_config(self, tmp_path: Path, apm_command: str) -> None: + """The real CLI install flow writes JetBrains Copilot's MCP config.""" + project_dir = tmp_path / "project" + project_dir.mkdir() + (project_dir / "apm.yml").write_text(_MINIMAL_APM_YML, encoding="ascii") + fake_home = tmp_path / "home" + fake_home.mkdir() + xdg_data = fake_home / "xdg" + local_app_data = fake_home / "local-app-data" + + env = os.environ.copy() + env.update( + { + "HOME": str(fake_home), + "XDG_DATA_HOME": str(xdg_data), + "LOCALAPPDATA": str(local_app_data), + "GIT_TERMINAL_PROMPT": "0", + "APM_NON_INTERACTIVE": "1", + } + ) + + result = subprocess.run( + [ + apm_command, + "install", + "--mcp", + "test-http-server", + "--target", + "intellij", + "--transport", + "http", + "--url", + "https://example.com/mcp", + ], + cwd=project_dir, + capture_output=True, + text=True, + timeout=120, + env=env, + ) + + assert result.returncode == 0, ( + f"apm install failed (rc={result.returncode}).\n" + f"STDOUT:\n{result.stdout}\nSTDERR:\n{result.stderr}" + ) + if sys.platform == "win32": + config_path = local_app_data / "github-copilot" / "intellij" / "mcp.json" + elif sys.platform == "darwin": + config_path = ( + fake_home + / "Library" + / "Application Support" + / "github-copilot" + / "intellij" + / "mcp.json" + ) + else: + config_path = xdg_data / "github-copilot" / "intellij" / "mcp.json" + + assert config_path.exists(), ( + f"Expected IntelliJ MCP config at {config_path}.\n" + f"STDOUT:\n{result.stdout}\nSTDERR:\n{result.stderr}" + ) + config = json.loads(config_path.read_text(encoding="utf-8")) + assert config["servers"]["test-http-server"]["url"] == "https://example.com/mcp" + # =========================================================================== # Policy target normalisation diff --git a/tests/unit/install/test_policy_target_check_phase.py b/tests/unit/install/test_policy_target_check_phase.py index ac460d030..be28cf53f 100644 --- a/tests/unit/install/test_policy_target_check_phase.py +++ b/tests/unit/install/test_policy_target_check_phase.py @@ -337,6 +337,22 @@ def test_cli_override_fixes_disallowed_manifest_target(self, mock_checks): ctx.logger.policy_violation.assert_not_called() + @patch(_PATCH_CHECKS) + def test_intellij_override_is_normalized_to_copilot(self, mock_checks): + """MCP-only IntelliJ uses its canonical policy target.""" + mock_checks.return_value = _target_passing_audit() + ctx = _make_ctx( + enforcement_active=True, + policy_fetch=_make_policy_fetch(enforcement="block", allow=("copilot",)), + target_override="intellij", + ) + + run(ctx) + + mock_checks.assert_called_once() + assert mock_checks.call_args.kwargs["effective_target"] == "copilot" + ctx.logger.policy_violation.assert_not_called() + # ===================================================================== # Test: double-emit filtering From 6a891072d674f71f236ed39e3c902c040d3a63b1 Mon Sep 17 00:00:00 2001 From: danielmeppiel Date: Fri, 10 Jul 2026 01:16:06 +0200 Subject: [PATCH 06/16] Fix target list compatibility Guard MCP-only membership checks for list-valued target inputs and retain the documented all-target expansion in CLI help. Co-authored-by: Sergio Sisternes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/apm_cli/commands/install.py | 2 +- src/apm_cli/core/target_detection.py | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/apm_cli/commands/install.py b/src/apm_cli/commands/install.py index f3d514737..9513f4203 100644 --- a/src/apm_cli/commands/install.py +++ b/src/apm_cli/commands/install.py @@ -946,7 +946,7 @@ def _handle_mcp_install( # noqa: PLR0913 "target", type=TargetParamType(), default=None, - help="Target harness(es) to deploy to. Use commas for multiple targets; repeated flags are not supported. Values: copilot, claude, cursor, opencode, codex, gemini, antigravity, windsurf, kiro, intellij, agent-skills, all. 'intellij' is MCP-only and configures JetBrains Copilot without file-level primitive deployment. 'all' excludes agent-skills, antigravity, and intellij; combine them explicitly when needed. Experimental copilot-cowork and copilot-app values require their feature flags. Resolution order: --target > apm.yml targets: > apm config target > auto-detect.", + help="Target harness(es) to deploy to. Use commas for multiple targets; repeated flags are not supported. Values: copilot, claude, cursor, opencode, codex, gemini, antigravity, windsurf, kiro, intellij, agent-skills, all. 'intellij' is MCP-only and configures JetBrains Copilot without file-level primitive deployment. 'all' = copilot+claude+cursor+opencode+codex+gemini+windsurf+kiro; combine agent-skills, antigravity, or intellij explicitly when needed. Experimental copilot-cowork and copilot-app values require their feature flags. Resolution order: --target > apm.yml targets: > apm config target > auto-detect.", ) @click.option( "--allow-insecure", diff --git a/src/apm_cli/core/target_detection.py b/src/apm_cli/core/target_detection.py index 5e67684f9..116ed9029 100644 --- a/src/apm_cli/core/target_detection.py +++ b/src/apm_cli/core/target_detection.py @@ -126,9 +126,8 @@ def detect_target( # noqa: PLR0911 """ # Priority 1: Explicit --target flag if explicit_target: - if ( - explicit_target in ("copilot", "vscode", "agents") - or explicit_target in MCP_ONLY_TARGETS + if explicit_target in ("copilot", "vscode", "agents") or ( + isinstance(explicit_target, str) and explicit_target in MCP_ONLY_TARGETS ): return "vscode", "explicit --target flag" elif explicit_target == "claude": @@ -154,7 +153,9 @@ def detect_target( # noqa: PLR0911 # Priority 2: apm.yml target setting if config_target: - if config_target in ("copilot", "vscode", "agents") or config_target in MCP_ONLY_TARGETS: + if config_target in ("copilot", "vscode", "agents") or ( + isinstance(config_target, str) and config_target in MCP_ONLY_TARGETS + ): return "vscode", "apm.yml target" elif config_target == "claude": return "claude", "apm.yml target" From 8383a57e70188a00444db98f3487c9b33e7596de Mon Sep 17 00:00:00 2001 From: danielmeppiel Date: Fri, 10 Jul 2026 01:34:19 +0200 Subject: [PATCH 07/16] Fold final IntelliJ review feedback Make the subprocess regression select the worktree binary, harden policy mapping invariants, improve target diagnostics and typing, and complete IntelliJ documentation. Co-authored-by: Sergio Sisternes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../docs/integrations/ide-tool-integration.md | 2 ++ .../content/docs/reference/targets-matrix.md | 12 ++++++++++++ src/apm_cli/commands/install.py | 2 +- src/apm_cli/install/mcp/command.py | 2 ++ .../install/phases/policy_target_check.py | 8 +++++++- src/apm_cli/integration/mcp_integrator.py | 2 +- .../integration/mcp_integrator_install.py | 4 ++-- tests/integration/test_intellij_target.py | 17 +++++++++++++++-- tests/unit/core/test_target_detection.py | 11 +++++++++++ 9 files changed, 53 insertions(+), 7 deletions(-) diff --git a/docs/src/content/docs/integrations/ide-tool-integration.md b/docs/src/content/docs/integrations/ide-tool-integration.md index b559354a9..9290c7b44 100644 --- a/docs/src/content/docs/integrations/ide-tool-integration.md +++ b/docs/src/content/docs/integrations/ide-tool-integration.md @@ -154,6 +154,8 @@ Notes and limits: - **Runtime env substitution.** JetBrains Copilot resolves `${env:VAR}` in `mcp.json` at server start. APM preserves env-var placeholders as `${env:VAR}` instead of writing matching host secrets into the config. +- **Policy evaluation.** APM maps `intellij` to `copilot` for organization + allow-lists, so a policy that allows `copilot` also covers IntelliJ installs. ## Per-tool reference pages diff --git a/docs/src/content/docs/reference/targets-matrix.md b/docs/src/content/docs/reference/targets-matrix.md index ec7a61c82..8668b6055 100644 --- a/docs/src/content/docs/reference/targets-matrix.md +++ b/docs/src/content/docs/reference/targets-matrix.md @@ -218,6 +218,18 @@ Kiro IDE. - **MCP shape.** JSON `mcpServers` entries use `command`/`args`/`env` for stdio and `url`/`headers` for remote servers. Kiro resolves `${VAR}` placeholders at runtime, so APM preserves them rather than writing secrets to disk. - **Scope.** This is the documented Kiro IDE layout only. Kiro CLI differences are tracked separately and are not part of this target. +## intellij + +GitHub Copilot for JetBrains IDEs. + +- **Detection.** Global `github-copilot/intellij/` config directory. +- **Deploy directory.** User-scope `mcp.json`; see the + [JetBrains integration guide](../integrations/ide-tool-integration/#jetbrains) + for OS-specific paths. +- **Supported primitives.** mcp only. +- **Scope.** User scope only. IntelliJ never participates in file-level + primitive deployment or plain `all` expansion. + ## agent-skills Cross-client shared skills directory. diff --git a/src/apm_cli/commands/install.py b/src/apm_cli/commands/install.py index 9513f4203..7d144e2d8 100644 --- a/src/apm_cli/commands/install.py +++ b/src/apm_cli/commands/install.py @@ -946,7 +946,7 @@ def _handle_mcp_install( # noqa: PLR0913 "target", type=TargetParamType(), default=None, - help="Target harness(es) to deploy to. Use commas for multiple targets; repeated flags are not supported. Values: copilot, claude, cursor, opencode, codex, gemini, antigravity, windsurf, kiro, intellij, agent-skills, all. 'intellij' is MCP-only and configures JetBrains Copilot without file-level primitive deployment. 'all' = copilot+claude+cursor+opencode+codex+gemini+windsurf+kiro; combine agent-skills, antigravity, or intellij explicitly when needed. Experimental copilot-cowork and copilot-app values require their feature flags. Resolution order: --target > apm.yml targets: > apm config target > auto-detect.", + help="Target harness(es) to deploy to. Use commas for multiple targets; repeating the flag silently keeps only the last value. Values: copilot, claude, cursor, opencode, codex, gemini, antigravity (agy), windsurf, kiro, intellij, agent-skills, all. 'intellij' is MCP-only and configures JetBrains Copilot without file-level primitive deployment. 'all' = copilot+claude+cursor+opencode+codex+gemini+windsurf+kiro; combine agent-skills, antigravity, or intellij explicitly when needed. Experimental copilot-cowork and copilot-app values require their feature flags. Resolution order: --target > apm.yml targets: > apm config target > auto-detect. With nothing to detect, install exits 2 with a teaching message.", ) @click.option( "--allow-insecure", diff --git a/src/apm_cli/install/mcp/command.py b/src/apm_cli/install/mcp/command.py index 5ca266b88..53403ab4a 100644 --- a/src/apm_cli/install/mcp/command.py +++ b/src/apm_cli/install/mcp/command.py @@ -117,6 +117,8 @@ def run_mcp_install( # noqa: PLR0913 if APM_DEPS_AVAILABLE: if registry_url and logger and verbose: logger.verbose_detail(f"Registry: {registry_url}") + if target is not None and logger: + logger.verbose_detail(f"Target: {target}") with registry_env_override(registry_url): try: _mcp_lock_path = get_lockfile_path(apm_dir) diff --git a/src/apm_cli/install/phases/policy_target_check.py b/src/apm_cli/install/phases/policy_target_check.py index 30ef4261d..be728492f 100644 --- a/src/apm_cli/install/phases/policy_target_check.py +++ b/src/apm_cli/install/phases/policy_target_check.py @@ -66,11 +66,17 @@ def run(ctx: InstallContext) -> None: # Only MCP_ONLY_TARGETS need this -- canonical targets like # 'vscode' must pass through unchanged. # ------------------------------------------------------------------ + # Keep these lazy: target_detection and integration.targets reference each + # other at runtime, so module-level imports would create an import cycle. from apm_cli.core.target_detection import MCP_ONLY_TARGETS from apm_cli.integration.targets import RUNTIME_TO_CANONICAL_TARGET if effective_target in MCP_ONLY_TARGETS: - canonical_target = RUNTIME_TO_CANONICAL_TARGET[effective_target] + canonical_target = RUNTIME_TO_CANONICAL_TARGET.get(effective_target) + if canonical_target is None: + raise RuntimeError( + f"MCP-only target '{effective_target}' has no canonical policy mapping" + ) if ctx.logger: ctx.logger.verbose_detail( f"Normalizing MCP-only target '{effective_target}' -> " diff --git a/src/apm_cli/integration/mcp_integrator.py b/src/apm_cli/integration/mcp_integrator.py index dad2673d4..9f2323147 100644 --- a/src/apm_cli/integration/mcp_integrator.py +++ b/src/apm_cli/integration/mcp_integrator.py @@ -1210,7 +1210,7 @@ def install( stored_mcp_configs: dict = None, # noqa: RUF013 project_root=None, user_scope: bool = False, - explicit_target: str | None = None, + explicit_target: str | list[str] | None = None, logger=None, diagnostics=None, scope=None, diff --git a/src/apm_cli/integration/mcp_integrator_install.py b/src/apm_cli/integration/mcp_integrator_install.py index 2f252376e..93eac4d26 100644 --- a/src/apm_cli/integration/mcp_integrator_install.py +++ b/src/apm_cli/integration/mcp_integrator_install.py @@ -330,7 +330,7 @@ def _resolve_target_runtimes( apm_config: dict | None, project_root, user_scope: bool, - explicit_target: str | None, + explicit_target: str | list[str] | None, scope: InstallScope | None, logger, console, @@ -631,7 +631,7 @@ def run_mcp_install( stored_mcp_configs: dict | None = None, project_root=None, user_scope: bool = False, - explicit_target: str | None = None, + explicit_target: str | list[str] | None = None, logger=None, diagnostics=None, scope: InstallScope | None = None, diff --git a/tests/integration/test_intellij_target.py b/tests/integration/test_intellij_target.py index 941abccc6..e438be6c5 100644 --- a/tests/integration/test_intellij_target.py +++ b/tests/integration/test_intellij_target.py @@ -45,11 +45,19 @@ @pytest.fixture() def apm_command() -> str: """Return the APM executable used by the end-to-end test.""" + executable_name = "apm.exe" if sys.platform == "win32" else "apm" + venv_apm = ( + Path(__file__).parents[2] + / ".venv" + / ("Scripts" if sys.platform == "win32" else "bin") + / executable_name + ) + if venv_apm.exists(): + return str(venv_apm) apm_on_path = shutil.which("apm") if apm_on_path: return apm_on_path - venv_apm = Path(__file__).parents[2] / ".venv" / "bin" / "apm" - return str(venv_apm) + pytest.fail("APM executable not found in the project virtualenv or PATH") @pytest.fixture() @@ -95,6 +103,11 @@ def test_intellij_runtime_canonical_maps_to_copilot(self) -> None: """RUNTIME_TO_CANONICAL_TARGET must map intellij -> copilot.""" assert RUNTIME_TO_CANONICAL_TARGET.get("intellij") == "copilot" + def test_all_mcp_only_targets_have_canonical_mapping(self) -> None: + """Every MCP-only target must have a fail-closed policy mapping.""" + missing = MCP_ONLY_TARGETS - RUNTIME_TO_CANONICAL_TARGET.keys() + assert not missing, f"MCP-only targets lack canonical mappings: {missing}" + def test_parser_accepts_single(self) -> None: """TargetParamType.convert accepts 'intellij' as a single token.""" tp = TargetParamType() diff --git a/tests/unit/core/test_target_detection.py b/tests/unit/core/test_target_detection.py index 0e3d6a4be..82518511a 100644 --- a/tests/unit/core/test_target_detection.py +++ b/tests/unit/core/test_target_detection.py @@ -125,6 +125,17 @@ def test_config_target_vscode(self, tmp_path): assert target == "vscode" assert reason == "apm.yml target" + def test_config_target_intellij(self, tmp_path): + """Config target intellij maps to the Copilot deployment profile.""" + target, reason = detect_target( + project_root=tmp_path, + explicit_target=None, + config_target="intellij", + ) + + assert target == "vscode" + assert reason == "apm.yml target" + def test_config_target_claude(self, tmp_path): """Config target claude is used when no explicit target.""" target, reason = detect_target( From 542b9edd2cff992666502f0ee3822c5bb95349e7 Mon Sep 17 00:00:00 2001 From: danielmeppiel Date: Fri, 10 Jul 2026 01:38:23 +0200 Subject: [PATCH 08/16] Document OpenAPM conformance scope apm-spec-waiver: This restores documented IntelliJ behavior through existing target and MCP extension points; it does not extend the OpenAPM specification. Co-authored-by: Sergio Sisternes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> From 873839e7706f2369fadedae21a369d9d4a4bf39d Mon Sep 17 00:00:00 2001 From: danielmeppiel Date: Fri, 10 Jul 2026 01:50:40 +0200 Subject: [PATCH 09/16] Clarify IntelliJ target behavior Align documentation with the Copilot primitive profile, make verbose target diagnostics stable for lists, and document the dual MCP routing contract. apm-spec-waiver: This restores documented IntelliJ behavior through existing target and MCP extension points; it does not extend the OpenAPM specification. Co-authored-by: Sergio Sisternes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../docs/integrations/ide-tool-integration.md | 2 +- docs/src/content/docs/reference/cli/install.md | 2 +- .../src/content/docs/reference/targets-matrix.md | 16 +++++++++------- src/apm_cli/commands/install.py | 2 +- src/apm_cli/core/target_detection.py | 4 ++-- src/apm_cli/install/mcp/command.py | 7 +++++-- 6 files changed, 19 insertions(+), 14 deletions(-) diff --git a/docs/src/content/docs/integrations/ide-tool-integration.md b/docs/src/content/docs/integrations/ide-tool-integration.md index 9290c7b44..c9b9db90b 100644 --- a/docs/src/content/docs/integrations/ide-tool-integration.md +++ b/docs/src/content/docs/integrations/ide-tool-integration.md @@ -131,7 +131,7 @@ written to `.kiro/settings/mcp.json` or `~/.kiro/settings/mcp.json` for This target covers the documented Kiro IDE layout. Kiro CLI configuration differences are tracked separately; see [the targets matrix](../reference/targets-matrix/#kiro). -### JetBrains (IntelliJ IDEA, PyCharm, GoLand, and others) +### JetBrains GitHub Copilot for JetBrains reads MCP servers from a single user-scope `mcp.json` (the per-OS path above), so configuration is global rather than diff --git a/docs/src/content/docs/reference/cli/install.md b/docs/src/content/docs/reference/cli/install.md index 0b433c96a..a36f95c59 100644 --- a/docs/src/content/docs/reference/cli/install.md +++ b/docs/src/content/docs/reference/cli/install.md @@ -46,7 +46,7 @@ With no arguments it installs everything from `apm.yml`. With one or more `PACKA | Flag | Default | Description | |---|---|---| -| `--target`, `-t VALUE` | auto-detect | Force deployment targets. Comma-separated for multiple (`-t claude,cursor`). Values: `copilot`, `claude`, `cursor`, `opencode`, `codex`, `gemini`, `antigravity`, `windsurf`, `kiro`, `intellij`, `vscode`, `agent-skills`, `all`; experimental `copilot-cowork` and `copilot-app` are also accepted when enabled. `intellij` is MCP-only and writes JetBrains Copilot's user-scope MCP config without deploying file-level primitives. `all` excludes `agent-skills`, `antigravity`, and `intellij`; combine them explicitly to add them. Highest precedence in the chain `--target` > `apm.yml targets:` > `apm config set target ...` > auto-detect. With nothing to detect, install exits `2` with a teaching message. | +| `--target`, `-t VALUE` | auto-detect | Force deployment targets. Comma-separated for multiple (`-t claude,cursor`). Values: `copilot`, `claude`, `cursor`, `opencode`, `codex`, `gemini`, `antigravity`, `windsurf`, `kiro`, `intellij`, `vscode`, `agent-skills`, `all`; experimental `copilot-cowork` and `copilot-app` are also accepted when enabled. IntelliJ-specific integration is MCP-only and writes JetBrains Copilot's user-scope MCP config; package file primitives use the Copilot profile. `all` excludes `agent-skills`, `antigravity`, and `intellij`; combine them explicitly to add them. Highest precedence in the chain `--target` > `apm.yml targets:` > `apm config set target ...` > auto-detect. With nothing to detect, install exits `2` with a teaching message. | | `--runtime VALUE` | unset | Legacy alias for `--target` (single value only). Still accepted; prefer `--target`. | | `--exclude VALUE` | unset | Skip a single runtime that auto-detect or `targets:` would otherwise enable. | | `--only apm\|mcp` | both | Install only APM packages or only MCP servers. | diff --git a/docs/src/content/docs/reference/targets-matrix.md b/docs/src/content/docs/reference/targets-matrix.md index 8668b6055..07399b4ce 100644 --- a/docs/src/content/docs/reference/targets-matrix.md +++ b/docs/src/content/docs/reference/targets-matrix.md @@ -28,7 +28,7 @@ see [Primitive types](./primitive-types/). | opencode | `.opencode/` | [ ] | [ ] | [x] | [x] | [x] | [ ] | [x] | | windsurf | `.windsurf/` + `.agents/` | [x] | [ ] | [ ] | [x] | [x] | [x] | [x] | | kiro | `.kiro/` | [x] | [ ] | [ ] | [x] | [ ] | [x] | [x] | -| intellij | OS-specific user config | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | [x] | +| intellij | `.github/` + user MCP config | [x] | [x] | [x] | [x] | [ ] | [x] | [x] | | agent-skills | `.agents/` | [ ] | [ ] | [ ] | [x] | [ ] | [ ] | [ ] | Skills deploy to `.agents/skills/` for Copilot, Cursor, OpenCode, @@ -67,9 +67,9 @@ list before `compile` or `install`. | kiro | `.kiro/` directory | | intellij | Global `github-copilot/intellij/` config directory | -`intellij` is an MCP-only target. It writes the JetBrains Copilot user-scope -`mcp.json` and never participates in file-level primitive deployment or plain -`all` expansion. +IntelliJ-specific integration is MCP-only and writes JetBrains Copilot's +user-scope `mcp.json`. Package file primitives use the Copilot profile. +`intellij` does not participate in plain `all` expansion. `agent-skills` is a canonical target key; `antigravity` is explicit-only for auto-detection. Both are available with `--target` and can be listed in a @@ -226,9 +226,11 @@ GitHub Copilot for JetBrains IDEs. - **Deploy directory.** User-scope `mcp.json`; see the [JetBrains integration guide](../integrations/ide-tool-integration/#jetbrains) for OS-specific paths. -- **Supported primitives.** mcp only. -- **Scope.** User scope only. IntelliJ never participates in file-level - primitive deployment or plain `all` expansion. +- **Supported primitives.** The IntelliJ-specific adapter supports mcp. + Package file primitives use the Copilot profile. +- **Scope.** MCP configuration is user scope only. File primitives use the + project or user scope selected for the Copilot profile. IntelliJ does not + participate in plain `all` expansion. ## agent-skills diff --git a/src/apm_cli/commands/install.py b/src/apm_cli/commands/install.py index 7d144e2d8..9c528c68a 100644 --- a/src/apm_cli/commands/install.py +++ b/src/apm_cli/commands/install.py @@ -946,7 +946,7 @@ def _handle_mcp_install( # noqa: PLR0913 "target", type=TargetParamType(), default=None, - help="Target harness(es) to deploy to. Use commas for multiple targets; repeating the flag silently keeps only the last value. Values: copilot, claude, cursor, opencode, codex, gemini, antigravity (agy), windsurf, kiro, intellij, agent-skills, all. 'intellij' is MCP-only and configures JetBrains Copilot without file-level primitive deployment. 'all' = copilot+claude+cursor+opencode+codex+gemini+windsurf+kiro; combine agent-skills, antigravity, or intellij explicitly when needed. Experimental copilot-cowork and copilot-app values require their feature flags. Resolution order: --target > apm.yml targets: > apm config target > auto-detect. With nothing to detect, install exits 2 with a teaching message.", + help="Target harness(es) to deploy to. Use commas for multiple targets; repeating the flag keeps only the last value (use commas instead). Values: copilot, claude, cursor, opencode, codex, gemini, antigravity (agy), windsurf, kiro, intellij, agent-skills, all. IntelliJ-specific integration is MCP-only; file primitives use the Copilot profile. 'all' = copilot+claude+cursor+opencode+codex+gemini+windsurf+kiro; combine agent-skills, antigravity, or intellij explicitly when needed. Experimental copilot-cowork and copilot-app values require their feature flags. Resolution order: --target > apm.yml targets: > apm config target > auto-detect. With nothing to detect, install exits 2 with a teaching message.", ) @click.option( "--allow-insecure", diff --git a/src/apm_cli/core/target_detection.py b/src/apm_cli/core/target_detection.py index 116ed9029..8d35adda3 100644 --- a/src/apm_cli/core/target_detection.py +++ b/src/apm_cli/core/target_detection.py @@ -109,8 +109,8 @@ def agents_alias_was_detected() -> bool: def detect_target( # noqa: PLR0911 project_root: Path, - explicit_target: str | None = None, - config_target: str | None = None, + explicit_target: str | list[str] | None = None, + config_target: str | list[str] | None = None, ) -> tuple[TargetType, str]: """Detect the appropriate target for compilation and integration. diff --git a/src/apm_cli/install/mcp/command.py b/src/apm_cli/install/mcp/command.py index 53403ab4a..3c9a78751 100644 --- a/src/apm_cli/install/mcp/command.py +++ b/src/apm_cli/install/mcp/command.py @@ -117,14 +117,17 @@ def run_mcp_install( # noqa: PLR0913 if APM_DEPS_AVAILABLE: if registry_url and logger and verbose: logger.verbose_detail(f"Registry: {registry_url}") - if target is not None and logger: - logger.verbose_detail(f"Target: {target}") + if target is not None and logger and verbose: + rendered_target = target if isinstance(target, str) else ", ".join(target) + logger.verbose_detail(f"Target: {rendered_target}") with registry_env_override(registry_url): try: _mcp_lock_path = get_lockfile_path(apm_dir) _existing_lock = LockFile.read(_mcp_lock_path) old_servers = set(_existing_lock.mcp_servers) if _existing_lock else set() old_configs = dict(_existing_lock.mcp_configs) if _existing_lock else {} + # A scalar target selects the runtime directly; explicit_target + # also preserves the flag for downstream active-target gating. MCPIntegrator.install( [dep], target if isinstance(target, str) else runtime, From 887bd4088c03b55551fc0319dd75c21d0cda60df Mon Sep 17 00:00:00 2001 From: danielmeppiel Date: Fri, 10 Jul 2026 01:58:15 +0200 Subject: [PATCH 10/16] Complete IntelliJ target guidance Make delegated Copilot primitives explicit in every docs surface, gate the binary integration test declaratively, and tighten target diagnostics and legacy type boundaries. apm-spec-waiver: This restores documented IntelliJ behavior through existing target and MCP extension points; it does not extend the OpenAPM specification. Co-authored-by: Sergio Sisternes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../docs/integrations/ide-tool-integration.md | 4 ++-- docs/src/content/docs/reference/targets-matrix.md | 12 ++++++++---- src/apm_cli/commands/install.py | 2 +- src/apm_cli/core/target_detection.py | 13 ++++++------- src/apm_cli/install/mcp/command.py | 4 ++-- src/apm_cli/install/phases/targets.py | 4 ++-- tests/integration/test_intellij_target.py | 1 + 7 files changed, 22 insertions(+), 18 deletions(-) diff --git a/docs/src/content/docs/integrations/ide-tool-integration.md b/docs/src/content/docs/integrations/ide-tool-integration.md index c9b9db90b..653fd1515 100644 --- a/docs/src/content/docs/integrations/ide-tool-integration.md +++ b/docs/src/content/docs/integrations/ide-tool-integration.md @@ -24,7 +24,7 @@ The full slot-by-slot capability table lives in [Targets matrix](../reference/ta | OpenCode | `.opencode/` | Skills, MCP | | Windsurf | `.windsurf/` | Rules + Skills + Workflows + MCP | | Kiro | `.kiro/` | Steering + Skills + Hooks + MCP | -| JetBrains Copilot | user-scope config dir (global) | MCP only (user-scope path, `${env:VAR}` env substitution) | +| JetBrains Copilot | user-scope config dir (global) | MCP (user-scope path, `${env:VAR}` substitution); file primitives use the Copilot profile | | Agent-Skills (cross) | `.agents/skills/` | Vendor-neutral skill sharing | For exact per-target capabilities (which primitives are supported, transformer used, file layout), see [Targets matrix](../reference/targets-matrix/). @@ -131,7 +131,7 @@ written to `.kiro/settings/mcp.json` or `~/.kiro/settings/mcp.json` for This target covers the documented Kiro IDE layout. Kiro CLI configuration differences are tracked separately; see [the targets matrix](../reference/targets-matrix/#kiro). -### JetBrains +### JetBrains (IntelliJ IDEA, PyCharm, GoLand, and others) GitHub Copilot for JetBrains reads MCP servers from a single user-scope `mcp.json` (the per-OS path above), so configuration is global rather than diff --git a/docs/src/content/docs/reference/targets-matrix.md b/docs/src/content/docs/reference/targets-matrix.md index 07399b4ce..21b927c00 100644 --- a/docs/src/content/docs/reference/targets-matrix.md +++ b/docs/src/content/docs/reference/targets-matrix.md @@ -28,13 +28,16 @@ see [Primitive types](./primitive-types/). | opencode | `.opencode/` | [ ] | [ ] | [x] | [x] | [x] | [ ] | [x] | | windsurf | `.windsurf/` + `.agents/` | [x] | [ ] | [ ] | [x] | [x] | [x] | [x] | | kiro | `.kiro/` | [x] | [ ] | [ ] | [x] | [ ] | [x] | [x] | -| intellij | `.github/` + user MCP config | [x] | [x] | [x] | [x] | [ ] | [x] | [x] | +| intellij | user MCP config; files via Copilot | [x] (*) | [x] (*) | [x] (*) | [x] (*) | [ ] | [x] (*) | [x] | | agent-skills | `.agents/` | [ ] | [ ] | [ ] | [x] | [ ] | [ ] | [ ] | Skills deploy to `.agents/skills/` for Copilot, Cursor, OpenCode, Gemini, Antigravity, Codex, and Windsurf by default (see [Skills convergence](#skills-convergence) below). Claude and Kiro keep target-native skill directories. +(*) For `intellij`, file primitives route through the Copilot profile under +`.github/`; the IntelliJ-specific adapter configures MCP only. + `copilot-cowork` (Microsoft 365 Copilot), `copilot-app` (GitHub Copilot desktop App), `openclaw` (OpenClaw agent runtime), and `hermes` are gated behind experimental flags and not listed above. See @@ -224,10 +227,11 @@ GitHub Copilot for JetBrains IDEs. - **Detection.** Global `github-copilot/intellij/` config directory. - **Deploy directory.** User-scope `mcp.json`; see the - [JetBrains integration guide](../integrations/ide-tool-integration/#jetbrains) + [JetBrains integration guide](../integrations/ide-tool-integration/#jetbrains-intellij-idea-pycharm-goland-and-others) for OS-specific paths. -- **Supported primitives.** The IntelliJ-specific adapter supports mcp. - Package file primitives use the Copilot profile. +- **Supported primitives.** The IntelliJ-specific adapter supports MCP. + Instructions, prompts, agents, skills, and hooks deploy through the Copilot + profile under `.github/`. - **Scope.** MCP configuration is user scope only. File primitives use the project or user scope selected for the Copilot profile. IntelliJ does not participate in plain `all` expansion. diff --git a/src/apm_cli/commands/install.py b/src/apm_cli/commands/install.py index 9c528c68a..40dd8a04e 100644 --- a/src/apm_cli/commands/install.py +++ b/src/apm_cli/commands/install.py @@ -946,7 +946,7 @@ def _handle_mcp_install( # noqa: PLR0913 "target", type=TargetParamType(), default=None, - help="Target harness(es) to deploy to. Use commas for multiple targets; repeating the flag keeps only the last value (use commas instead). Values: copilot, claude, cursor, opencode, codex, gemini, antigravity (agy), windsurf, kiro, intellij, agent-skills, all. IntelliJ-specific integration is MCP-only; file primitives use the Copilot profile. 'all' = copilot+claude+cursor+opencode+codex+gemini+windsurf+kiro; combine agent-skills, antigravity, or intellij explicitly when needed. Experimental copilot-cowork and copilot-app values require their feature flags. Resolution order: --target > apm.yml targets: > apm config target > auto-detect. With nothing to detect, install exits 2 with a teaching message.", + help="Target harness(es) to deploy to. Use commas for multiple targets; repeating the flag keeps only the last value (use commas instead). Values: copilot, claude, cursor, opencode, codex, gemini, antigravity (agy), windsurf, kiro, intellij, agent-skills, all. IntelliJ-specific integration is MCP-only; file primitives use the Copilot profile. 'all' = copilot+claude+cursor+opencode+codex+gemini+windsurf+kiro; combine agent-skills, antigravity, or intellij explicitly when needed. Experimental copilot-cowork and copilot-app values require their feature flags. Resolution order: --target > apm.yml targets: > apm config target > auto-detect. With nothing to detect, install exits 2 with a teaching message. For 'apm compile', use '--all'; '--target all' is deprecated.", ) @click.option( "--allow-insecure", diff --git a/src/apm_cli/core/target_detection.py b/src/apm_cli/core/target_detection.py index 8d35adda3..5e67684f9 100644 --- a/src/apm_cli/core/target_detection.py +++ b/src/apm_cli/core/target_detection.py @@ -109,8 +109,8 @@ def agents_alias_was_detected() -> bool: def detect_target( # noqa: PLR0911 project_root: Path, - explicit_target: str | list[str] | None = None, - config_target: str | list[str] | None = None, + explicit_target: str | None = None, + config_target: str | None = None, ) -> tuple[TargetType, str]: """Detect the appropriate target for compilation and integration. @@ -126,8 +126,9 @@ def detect_target( # noqa: PLR0911 """ # Priority 1: Explicit --target flag if explicit_target: - if explicit_target in ("copilot", "vscode", "agents") or ( - isinstance(explicit_target, str) and explicit_target in MCP_ONLY_TARGETS + if ( + explicit_target in ("copilot", "vscode", "agents") + or explicit_target in MCP_ONLY_TARGETS ): return "vscode", "explicit --target flag" elif explicit_target == "claude": @@ -153,9 +154,7 @@ def detect_target( # noqa: PLR0911 # Priority 2: apm.yml target setting if config_target: - if config_target in ("copilot", "vscode", "agents") or ( - isinstance(config_target, str) and config_target in MCP_ONLY_TARGETS - ): + if config_target in ("copilot", "vscode", "agents") or config_target in MCP_ONLY_TARGETS: return "vscode", "apm.yml target" elif config_target == "claude": return "claude", "apm.yml target" diff --git a/src/apm_cli/install/mcp/command.py b/src/apm_cli/install/mcp/command.py index 3c9a78751..5c49175e8 100644 --- a/src/apm_cli/install/mcp/command.py +++ b/src/apm_cli/install/mcp/command.py @@ -115,9 +115,9 @@ def run_mcp_install( # noqa: PLR0913 # MCPServerOperations() (constructed deep inside MCPIntegrator.install) # picks up the override; prior env restored on exit. if APM_DEPS_AVAILABLE: - if registry_url and logger and verbose: + if registry_url and logger: logger.verbose_detail(f"Registry: {registry_url}") - if target is not None and logger and verbose: + if target is not None and logger: rendered_target = target if isinstance(target, str) else ", ".join(target) logger.verbose_detail(f"Target: {rendered_target}") with registry_env_override(registry_url): diff --git a/src/apm_cli/install/phases/targets.py b/src/apm_cli/install/phases/targets.py index 0d3940e9e..0a243c951 100644 --- a/src/apm_cli/install/phases/targets.py +++ b/src/apm_cli/install/phases/targets.py @@ -526,8 +526,8 @@ def run(ctx: InstallContext) -> None: # the pre-refactor mega-function. detect_target( project_root=ctx.project_root, - explicit_target=_explicit, - config_target=config_target, + explicit_target=_explicit if isinstance(_explicit, str) else None, + config_target=config_target if isinstance(config_target, str) else None, ) # ------------------------------------------------------------------ diff --git a/tests/integration/test_intellij_target.py b/tests/integration/test_intellij_target.py index e438be6c5..c5d84cab9 100644 --- a/tests/integration/test_intellij_target.py +++ b/tests/integration/test_intellij_target.py @@ -154,6 +154,7 @@ def test_cli_parser_accepts_target_intellij(self, tmp_path: Path, fake_home: Pat # but that is fine -- we only care about parser acceptance. assert "Unknown target" not in (result.output or "") + @pytest.mark.requires_apm_binary def test_mcp_install_writes_intellij_config(self, tmp_path: Path, apm_command: str) -> None: """The real CLI install flow writes JetBrains Copilot's MCP config.""" project_dir = tmp_path / "project" From 16e377dc0b31687168400429bac273a674911e73 Mon Sep 17 00:00:00 2001 From: Sergio Sisternes Date: Fri, 10 Jul 2026 01:13:33 +0100 Subject: [PATCH 11/16] Guard MCP_ONLY_TARGETS membership checks for list-valued targets Add isinstance(str) guards before 'in MCP_ONLY_TARGETS' checks in detect_target() and policy_target_check.py to prevent TypeError when explicit_target is a list (multi-target mode like ['intellij', 'claude']). Also handle list normalisation in policy_target_check. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/apm_cli/core/target_detection.py | 9 +++++---- src/apm_cli/install/phases/policy_target_check.py | 7 ++++++- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/apm_cli/core/target_detection.py b/src/apm_cli/core/target_detection.py index 5e67684f9..116ed9029 100644 --- a/src/apm_cli/core/target_detection.py +++ b/src/apm_cli/core/target_detection.py @@ -126,9 +126,8 @@ def detect_target( # noqa: PLR0911 """ # Priority 1: Explicit --target flag if explicit_target: - if ( - explicit_target in ("copilot", "vscode", "agents") - or explicit_target in MCP_ONLY_TARGETS + if explicit_target in ("copilot", "vscode", "agents") or ( + isinstance(explicit_target, str) and explicit_target in MCP_ONLY_TARGETS ): return "vscode", "explicit --target flag" elif explicit_target == "claude": @@ -154,7 +153,9 @@ def detect_target( # noqa: PLR0911 # Priority 2: apm.yml target setting if config_target: - if config_target in ("copilot", "vscode", "agents") or config_target in MCP_ONLY_TARGETS: + if config_target in ("copilot", "vscode", "agents") or ( + isinstance(config_target, str) and config_target in MCP_ONLY_TARGETS + ): return "vscode", "apm.yml target" elif config_target == "claude": return "claude", "apm.yml target" diff --git a/src/apm_cli/install/phases/policy_target_check.py b/src/apm_cli/install/phases/policy_target_check.py index be728492f..1afd5952d 100644 --- a/src/apm_cli/install/phases/policy_target_check.py +++ b/src/apm_cli/install/phases/policy_target_check.py @@ -71,7 +71,7 @@ def run(ctx: InstallContext) -> None: from apm_cli.core.target_detection import MCP_ONLY_TARGETS from apm_cli.integration.targets import RUNTIME_TO_CANONICAL_TARGET - if effective_target in MCP_ONLY_TARGETS: + if isinstance(effective_target, str) and effective_target in MCP_ONLY_TARGETS: canonical_target = RUNTIME_TO_CANONICAL_TARGET.get(effective_target) if canonical_target is None: raise RuntimeError( @@ -83,6 +83,11 @@ def run(ctx: InstallContext) -> None: f"'{canonical_target}' for policy check" ) effective_target = canonical_target + elif isinstance(effective_target, list): + effective_target = [ + RUNTIME_TO_CANONICAL_TARGET.get(t, t) if t in MCP_ONLY_TARGETS else t + for t in effective_target + ] # ------------------------------------------------------------------ # 4. Run policy checks with effective_target populated From 090f16bb392d2fec5559337a75633d8beb50cb38 Mon Sep 17 00:00:00 2001 From: danielmeppiel Date: Fri, 10 Jul 2026 02:17:52 +0200 Subject: [PATCH 12/16] fix: converge IntelliJ target policy handling Normalize scalar and plural selectors through one policy contract, preserve exact direct MCP runtime sets, and add empirical composed-target coverage. Addresses final panel follow-ups for PR #2041. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../docs/integrations/ide-tool-integration.md | 13 +++++--- .../src/content/docs/reference/cli/install.md | 2 +- .../content/docs/reference/targets-matrix.md | 11 ++++--- .../.apm/skills/apm-usage/commands.md | 2 +- src/apm_cli/commands/install.py | 4 +++ src/apm_cli/core/target_detection.py | 25 ++++++++++++++ src/apm_cli/install/context.py | 2 +- .../install/phases/policy_target_check.py | 29 +++------------- .../integration/mcp_integrator_install.py | 8 +++++ src/apm_cli/policy/install_preflight.py | 4 +++ src/apm_cli/policy/policy_checks.py | 2 +- tests/integration/test_intellij_target.py | 30 +++++++++++++++-- .../unit/install/test_mcp_preflight_policy.py | 22 ++++++++++++- .../install/test_policy_target_check_phase.py | 33 ++++++++++++++++++- 14 files changed, 145 insertions(+), 42 deletions(-) diff --git a/docs/src/content/docs/integrations/ide-tool-integration.md b/docs/src/content/docs/integrations/ide-tool-integration.md index 653fd1515..6ad5bfa98 100644 --- a/docs/src/content/docs/integrations/ide-tool-integration.md +++ b/docs/src/content/docs/integrations/ide-tool-integration.md @@ -146,11 +146,14 @@ apm install --mcp io.github.github/github-mcp-server --target intellij Notes and limits: -- **Auto-detect is user-scope only.** Unlike project markers such as `.cursor/` - or `.windsurf/`, JetBrains is detected from the global config directory, not a - file in your repo. It is therefore detected for every project on the machine - once the plugin directory exists. Use `--target intellij` to target it - explicitly regardless of auto-detect. +- **MCP auto-detect is user-scope only.** Unlike project markers such as + `.cursor/` or `.windsurf/`, MCP runtime discovery detects JetBrains from the + global config directory. It is therefore considered for MCP configuration in + every project once the plugin directory exists. This signal does not select a + file-primitive profile; use `--target intellij` explicitly. +- **Composed targets stay exact.** `--target intellij,claude` writes the + JetBrains and Claude MCP configs. `--target all,intellij` adds JetBrains to + the normal `all` target set; plain `all` excludes it. - **Runtime env substitution.** JetBrains Copilot resolves `${env:VAR}` in `mcp.json` at server start. APM preserves env-var placeholders as `${env:VAR}` instead of writing matching host secrets into the config. diff --git a/docs/src/content/docs/reference/cli/install.md b/docs/src/content/docs/reference/cli/install.md index a36f95c59..af1bb7b4d 100644 --- a/docs/src/content/docs/reference/cli/install.md +++ b/docs/src/content/docs/reference/cli/install.md @@ -46,7 +46,7 @@ With no arguments it installs everything from `apm.yml`. With one or more `PACKA | Flag | Default | Description | |---|---|---| -| `--target`, `-t VALUE` | auto-detect | Force deployment targets. Comma-separated for multiple (`-t claude,cursor`). Values: `copilot`, `claude`, `cursor`, `opencode`, `codex`, `gemini`, `antigravity`, `windsurf`, `kiro`, `intellij`, `vscode`, `agent-skills`, `all`; experimental `copilot-cowork` and `copilot-app` are also accepted when enabled. IntelliJ-specific integration is MCP-only and writes JetBrains Copilot's user-scope MCP config; package file primitives use the Copilot profile. `all` excludes `agent-skills`, `antigravity`, and `intellij`; combine them explicitly to add them. Highest precedence in the chain `--target` > `apm.yml targets:` > `apm config set target ...` > auto-detect. With nothing to detect, install exits `2` with a teaching message. | +| `--target`, `-t VALUE` | auto-detect | Force deployment targets. Comma-separated for multiple (`-t claude,cursor`). Values: `copilot`, `claude`, `cursor`, `opencode`, `codex`, `gemini`, `antigravity`, `windsurf`, `kiro`, `intellij`, `vscode`, `agent-skills`, `all`; experimental `copilot-cowork` and `copilot-app` are also accepted when enabled. IntelliJ-specific integration is MCP-only and writes JetBrains Copilot's user-scope MCP config; package file primitives use the Copilot profile. `all` excludes `agent-skills`, `antigravity`, and `intellij`; combine them explicitly to add them, for example `all,intellij`. Explicit MCP target lists are exact: `intellij,claude` writes only those two client configs. Highest precedence in the chain `--target` > `apm.yml targets:` > `apm config set target ...` > auto-detect. With nothing to detect, install exits `2` with a teaching message. | | `--runtime VALUE` | unset | Legacy alias for `--target` (single value only). Still accepted; prefer `--target`. | | `--exclude VALUE` | unset | Skip a single runtime that auto-detect or `targets:` would otherwise enable. | | `--only apm\|mcp` | both | Install only APM packages or only MCP servers. | diff --git a/docs/src/content/docs/reference/targets-matrix.md b/docs/src/content/docs/reference/targets-matrix.md index 21b927c00..c2f629d58 100644 --- a/docs/src/content/docs/reference/targets-matrix.md +++ b/docs/src/content/docs/reference/targets-matrix.md @@ -68,11 +68,12 @@ list before `compile` or `install`. | opencode | `.opencode/` directory | | windsurf | `.windsurf/` directory | | kiro | `.kiro/` directory | -| intellij | Global `github-copilot/intellij/` config directory | +| intellij | Global `github-copilot/intellij/` config directory (MCP runtime discovery only) | IntelliJ-specific integration is MCP-only and writes JetBrains Copilot's -user-scope `mcp.json`. Package file primitives use the Copilot profile. -`intellij` does not participate in plain `all` expansion. +user-scope `mcp.json`. That global signal does not auto-select file-primitive +deployment. When `intellij` is selected explicitly, package file primitives use +the Copilot profile. `intellij` does not participate in plain `all` expansion. `agent-skills` is a canonical target key; `antigravity` is explicit-only for auto-detection. Both are available with `--target` and can be listed in a @@ -225,7 +226,9 @@ Kiro IDE. GitHub Copilot for JetBrains IDEs. -- **Detection.** Global `github-copilot/intellij/` config directory. +- **Detection.** MCP runtime discovery uses the global + `github-copilot/intellij/` config directory. It does not auto-select a + file-primitive target. - **Deploy directory.** User-scope `mcp.json`; see the [JetBrains integration guide](../integrations/ide-tool-integration/#jetbrains-intellij-idea-pycharm-goland-and-others) for OS-specific paths. diff --git a/packages/apm-guide/.apm/skills/apm-usage/commands.md b/packages/apm-guide/.apm/skills/apm-usage/commands.md index 76d0994f2..679ed90dc 100644 --- a/packages/apm-guide/.apm/skills/apm-usage/commands.md +++ b/packages/apm-guide/.apm/skills/apm-usage/commands.md @@ -10,7 +10,7 @@ | Command | Purpose | Key flags | |---------|---------|-----------| -| `apm install [PKGS...]` | Install APM and MCP dependencies (supports APM packages, Claude skills (SKILL.md), and plugin collections (plugin.json)) | `--update` (deprecated; prefer `apm update`) refresh refs, `--refresh` re-fetch all deps from upstream and re-resolve all ref pins, `--force` overwrite (does NOT refresh refs; use `apm update` for that), `--frozen` CI-safe install that fails fast when `apm.lock.yaml` is missing or out of sync with `apm.yml` (mutually exclusive with `--update`; structural presence check only -- use `apm audit` for SHA integrity), `--dry-run`, `--verbose`, `--only [apm\|mcp]`, `--target` (comma-separated, e.g. `--target claude,cursor`; highest-priority entry in the resolution chain `--target` > apm.yml `targets:` > auto-detect; `intellij` is MCP-only and writes JetBrains Copilot's user-scope config; on auto-bootstrap when no `apm.yml` exists, recognized manifest target(s) are persisted to the new manifest's `targets:` so a later bare `apm update` reuses them; `--target all` deprecated, see `apm compile --all`; use `kiro` for Kiro IDE; use `copilot-cowork` with `--global` after `apm experimental enable copilot-cowork`; use `hermes` after `apm experimental enable hermes` to deploy skills + `AGENTS.md` and, at `--global`, MCP servers to `~/.hermes/config.yaml`), `--dev`, `-g` global (MCP deploys only to user-scope runtimes: Copilot CLI, Claude Code, Codex CLI, Gemini CLI, Antigravity CLI, Kiro, Windsurf, JetBrains Copilot, and Hermes when enabled), `--trust-transitive-mcp`, `--parallel-downloads N`, `--allow-insecure`, `--allow-insecure-host HOSTNAME`, `--skill NAME` install named skill(s) from a skill collection (SKILL_BUNDLE or plugin manifest; repeatable; plugin manifests accept a leaf name or manifest path; persisted in apm.yml; additive across separate installs -- a later `--skill X` adds to the existing pin (union) rather than replacing it, so previously deployed skills are never silently removed; `'*'` resets to the full bundle; drop a single skill by editing the `skills:` list in apm.yml then re-running install), `--legacy-skill-paths` restore per-client skill dirs, `--mcp NAME` add MCP entry (NAME goes through the same `--target` > `targets:` > auto-detect resolver as APM packages, so `apm install --mcp NAME --target intellij` writes only JetBrains Copilot's MCP config; `apm install -g --mcp NAME` writes user-scope and bypasses the project-scope gate by design), `--transport`, `--url`, `--env KEY=VAL`, `--header KEY=VAL`, `--mcp-version`, `--registry URL` custom MCP registry, `--root DIR` redirect writes (`apm_modules/`, lockfile, `.gitignore`, integrated harness files) under DIR while `apm.yml`/`.apm/`/local deps resolve from `$PWD` (mirrors `pip install --target`; created if missing; not valid with `-g`/`--global`, which exits 2) | +| `apm install [PKGS...]` | Install APM and MCP dependencies (supports APM packages, Claude skills (SKILL.md), and plugin collections (plugin.json)) | `--update` (deprecated; prefer `apm update`) refresh refs, `--refresh` re-fetch all deps from upstream and re-resolve all ref pins, `--force` overwrite (does NOT refresh refs; use `apm update` for that), `--frozen` CI-safe install that fails fast when `apm.lock.yaml` is missing or out of sync with `apm.yml` (mutually exclusive with `--update`; structural presence check only -- use `apm audit` for SHA integrity), `--dry-run`, `--verbose`, `--only [apm\|mcp]`, `--target` (comma-separated, e.g. `--target claude,cursor`; highest-priority entry in the resolution chain `--target` > apm.yml `targets:` > auto-detect; `intellij` is MCP-only and writes JetBrains Copilot's user-scope config; explicit lists are exact, so `intellij,claude` writes those two MCP configs and `all,intellij` adds JetBrains to `all`; on auto-bootstrap when no `apm.yml` exists, recognized manifest target(s) are persisted to the new manifest's `targets:` so a later bare `apm update` reuses them; `--target all` deprecated, see `apm compile --all`; use `kiro` for Kiro IDE; use `copilot-cowork` with `--global` after `apm experimental enable copilot-cowork`; use `hermes` after `apm experimental enable hermes` to deploy skills + `AGENTS.md` and, at `--global`, MCP servers to `~/.hermes/config.yaml`), `--dev`, `-g` global (MCP deploys only to user-scope runtimes: Copilot CLI, Claude Code, Codex CLI, Gemini CLI, Antigravity CLI, Kiro, Windsurf, JetBrains Copilot, and Hermes when enabled), `--trust-transitive-mcp`, `--parallel-downloads N`, `--allow-insecure`, `--allow-insecure-host HOSTNAME`, `--skill NAME` install named skill(s) from a skill collection (SKILL_BUNDLE or plugin manifest; repeatable; plugin manifests accept a leaf name or manifest path; persisted in apm.yml; additive across separate installs -- a later `--skill X` adds to the existing pin (union) rather than replacing it, so previously deployed skills are never silently removed; `'*'` resets to the full bundle; drop a single skill by editing the `skills:` list in apm.yml then re-running install), `--legacy-skill-paths` restore per-client skill dirs, `--mcp NAME` add MCP entry (NAME goes through the same `--target` > `targets:` > auto-detect resolver as APM packages, so `apm install --mcp NAME --target intellij` writes only JetBrains Copilot's MCP config; compilation target policy applies to every resolved target; `apm install -g --mcp NAME` writes user-scope and bypasses the project-scope gate by design), `--transport`, `--url`, `--env KEY=VAL`, `--header KEY=VAL`, `--mcp-version`, `--registry URL` custom MCP registry, `--root DIR` redirect writes (`apm_modules/`, lockfile, `.gitignore`, integrated harness files) under DIR while `apm.yml`/`.apm/`/local deps resolve from `$PWD` (mirrors `pip install --target`; created if missing; not valid with `-g`/`--global`, which exits 2) | | `apm targets` | Show resolved deployment targets for the current project (Click group; reads filesystem signals; works with or without `apm.yml`) | `--all` also include the `agent-skills` meta-target (only meaningful with `--json`), `--json` machine-readable output. No provenance line is printed (the table is the provenance). | | `apm uninstall PKGS...` | Remove packages (accepts `owner/repo` or `name@marketplace`) | `--dry-run`, `-g` global | | `apm prune` | Remove orphaned packages | `--dry-run` | diff --git a/src/apm_cli/commands/install.py b/src/apm_cli/commands/install.py index 40dd8a04e..503a38a08 100644 --- a/src/apm_cli/commands/install.py +++ b/src/apm_cli/commands/install.py @@ -840,6 +840,9 @@ def _handle_mcp_install( # noqa: PLR0913 registry=False if _is_self_defined else None, url=url, ) + from ..core.target_detection import normalize_policy_targets + + policy_targets = normalize_policy_targets(target or runtime) try: _pf_result, _pf_active = run_policy_preflight( @@ -848,6 +851,7 @@ def _handle_mcp_install( # noqa: PLR0913 no_policy=no_policy, logger=logger, dry_run=logger.dry_run, + effective_target=policy_targets, ) except PolicyBlockError: # Diagnostics already emitted by the helper + logger. diff --git a/src/apm_cli/core/target_detection.py b/src/apm_cli/core/target_detection.py index 116ed9029..10715cdc0 100644 --- a/src/apm_cli/core/target_detection.py +++ b/src/apm_cli/core/target_detection.py @@ -497,6 +497,31 @@ def normalize_target_list( return result +def normalize_policy_targets(value: str | list[str] | None) -> str | list[str] | None: + """Normalize MCP-only selectors for compilation-target policy checks. + + The return shape matches the input shape so scalar callers remain + backward-compatible while plural target sets are evaluated together. + """ + if value is None: + return None + + from apm_cli.integration.targets import RUNTIME_TO_CANONICAL_TARGET + + values = [value] if isinstance(value, str) else list(value) + normalized: list[str] = [] + for target in values: + if target in MCP_ONLY_TARGETS: + canonical = RUNTIME_TO_CANONICAL_TARGET.get(target) + if canonical is None: + raise RuntimeError(f"MCP-only target '{target}' has no canonical policy mapping") + target = canonical + if target not in normalized: + normalized.append(target) + + return normalized[0] if isinstance(value, str) else normalized + + # --------------------------------------------------------------------------- # Click parameter type for --target (comma-separated multi-target support) # --------------------------------------------------------------------------- diff --git a/src/apm_cli/install/context.py b/src/apm_cli/install/context.py index 126efa91e..51184e640 100644 --- a/src/apm_cli/install/context.py +++ b/src/apm_cli/install/context.py @@ -56,7 +56,7 @@ class InstallContext: marketplace_provenance: dict[str, Any] | None = None parallel_downloads: int = 4 logger: Any = None # InstallLogger - target_override: str | None = None # effective --target value (CLI or config default) + target_override: str | list[str] | None = None # effective --target value # Provenance label for ``target_override`` when it did NOT come from the CLI. # None means an explicit CLI ``--target`` selector. When the value is # populated from the configured default (``apm config target``), this is diff --git a/src/apm_cli/install/phases/policy_target_check.py b/src/apm_cli/install/phases/policy_target_check.py index 1afd5952d..93383cd59 100644 --- a/src/apm_cli/install/phases/policy_target_check.py +++ b/src/apm_cli/install/phases/policy_target_check.py @@ -61,33 +61,14 @@ def run(ctx: InstallContext) -> None: return # no target to check -- trivially passes # ------------------------------------------------------------------ - # 3b. Normalize MCP-only pseudo-targets to their canonical form so - # policy allow-lists match (e.g. intellij -> copilot). - # Only MCP_ONLY_TARGETS need this -- canonical targets like - # 'vscode' must pass through unchanged. + # 3b. Normalize scalar or plural MCP-only pseudo-targets to their + # canonical policy form (e.g. intellij -> copilot). # ------------------------------------------------------------------ # Keep these lazy: target_detection and integration.targets reference each # other at runtime, so module-level imports would create an import cycle. - from apm_cli.core.target_detection import MCP_ONLY_TARGETS - from apm_cli.integration.targets import RUNTIME_TO_CANONICAL_TARGET - - if isinstance(effective_target, str) and effective_target in MCP_ONLY_TARGETS: - canonical_target = RUNTIME_TO_CANONICAL_TARGET.get(effective_target) - if canonical_target is None: - raise RuntimeError( - f"MCP-only target '{effective_target}' has no canonical policy mapping" - ) - if ctx.logger: - ctx.logger.verbose_detail( - f"Normalizing MCP-only target '{effective_target}' -> " - f"'{canonical_target}' for policy check" - ) - effective_target = canonical_target - elif isinstance(effective_target, list): - effective_target = [ - RUNTIME_TO_CANONICAL_TARGET.get(t, t) if t in MCP_ONLY_TARGETS else t - for t in effective_target - ] + from apm_cli.core.target_detection import normalize_policy_targets + + effective_target = normalize_policy_targets(effective_target) # ------------------------------------------------------------------ # 4. Run policy checks with effective_target populated diff --git a/src/apm_cli/integration/mcp_integrator_install.py b/src/apm_cli/integration/mcp_integrator_install.py index 93eac4d26..6eac661ff 100644 --- a/src/apm_cli/integration/mcp_integrator_install.py +++ b/src/apm_cli/integration/mcp_integrator_install.py @@ -347,6 +347,14 @@ def _resolve_target_runtimes( # Single runtime mode - skip auto-discovery entirely. logger.progress(f"Targeting specific runtime: {runtime}") target_runtimes: list[str] = [runtime] + elif explicit_target is not None: + # A plural --target value is already parser-normalized. Use that exact + # runtime set instead of broad discovery so selecting IntelliJ does not + # also select adjacent runtimes that share its Copilot policy profile. + target_runtimes = ( + [explicit_target] if isinstance(explicit_target, str) else list(explicit_target) + ) + logger.progress(f"Targeting specific runtimes: {', '.join(target_runtimes)}") else: project_root_path = Path(project_root) if project_root is not None else Path.cwd() diff --git a/src/apm_cli/policy/install_preflight.py b/src/apm_cli/policy/install_preflight.py index 58d13b0fc..fb06db679 100644 --- a/src/apm_cli/policy/install_preflight.py +++ b/src/apm_cli/policy/install_preflight.py @@ -69,6 +69,7 @@ def run_policy_preflight( logger, dry_run: bool = False, registries: dict[str, str] | None = None, + effective_target: str | list[str] | None = None, ) -> tuple[PolicyFetchResult | None, bool]: """Discover + enforce policy for a non-pipeline command site. @@ -81,6 +82,8 @@ def run_policy_preflight( dep checks. mcp_deps: Iterable of ``MCPDependency``, or ``None`` to skip MCP checks. + effective_target: + Resolved scalar or plural target selection for compilation policy. no_policy: CLI ``--no-policy`` flag value. logger: @@ -150,6 +153,7 @@ def run_policy_preflight( lockfile=None, policy=policy, mcp_deps=mcp_deps, + effective_target=effective_target, fail_fast=(enforcement == "block"), registries=registries, direct_dep_keys={d.get_unique_key() for d in apm_deps_list}, diff --git a/src/apm_cli/policy/policy_checks.py b/src/apm_cli/policy/policy_checks.py index 4227be0ce..1e1910fa4 100644 --- a/src/apm_cli/policy/policy_checks.py +++ b/src/apm_cli/policy/policy_checks.py @@ -1101,7 +1101,7 @@ def run_dependency_policy_checks( lockfile=None, policy: ApmPolicy, mcp_deps=None, - effective_target: str | None = None, + effective_target: str | list[str] | None = None, fetch_outcome: str | None = None, fail_fast: bool = True, manifest_includes=_INCLUDES_NOT_PROVIDED, diff --git a/tests/integration/test_intellij_target.py b/tests/integration/test_intellij_target.py index c5d84cab9..77675b453 100644 --- a/tests/integration/test_intellij_target.py +++ b/tests/integration/test_intellij_target.py @@ -155,10 +155,25 @@ def test_cli_parser_accepts_target_intellij(self, tmp_path: Path, fake_home: Pat assert "Unknown target" not in (result.output or "") @pytest.mark.requires_apm_binary - def test_mcp_install_writes_intellij_config(self, tmp_path: Path, apm_command: str) -> None: - """The real CLI install flow writes JetBrains Copilot's MCP config.""" + @pytest.mark.parametrize( + ("target", "expect_claude"), + [ + ("intellij", False), + ("intellij,claude", True), + ("all,intellij", True), + ], + ) + def test_mcp_install_respects_composed_target_set( + self, + tmp_path: Path, + apm_command: str, + target: str, + expect_claude: bool, + ) -> None: + """The real CLI writes only configs selected by the target set.""" project_dir = tmp_path / "project" project_dir.mkdir() + (project_dir / ".claude").mkdir() (project_dir / "apm.yml").write_text(_MINIMAL_APM_YML, encoding="ascii") fake_home = tmp_path / "home" fake_home.mkdir() @@ -183,7 +198,7 @@ def test_mcp_install_writes_intellij_config(self, tmp_path: Path, apm_command: s "--mcp", "test-http-server", "--target", - "intellij", + target, "--transport", "http", "--url", @@ -221,6 +236,15 @@ def test_mcp_install_writes_intellij_config(self, tmp_path: Path, apm_command: s config = json.loads(config_path.read_text(encoding="utf-8")) assert config["servers"]["test-http-server"]["url"] == "https://example.com/mcp" + claude_config = project_dir / ".mcp.json" + assert claude_config.exists() is expect_claude + if expect_claude: + claude = json.loads(claude_config.read_text(encoding="utf-8")) + assert claude["mcpServers"]["test-http-server"]["url"] == "https://example.com/mcp" + + if target == "intellij": + assert not (project_dir / ".vscode" / "mcp.json").exists() + # =========================================================================== # Policy target normalisation diff --git a/tests/unit/install/test_mcp_preflight_policy.py b/tests/unit/install/test_mcp_preflight_policy.py index 4d8e29db1..1212f7f8d 100644 --- a/tests/unit/install/test_mcp_preflight_policy.py +++ b/tests/unit/install/test_mcp_preflight_policy.py @@ -31,6 +31,8 @@ from apm_cli.policy.parser import load_policy from apm_cli.policy.schema import ( ApmPolicy, + CompilationPolicy, + CompilationTargetPolicy, DependencyPolicy, # noqa: F401 McpPolicy, McpTransportPolicy, # noqa: F401 @@ -138,7 +140,7 @@ def test_env_disable_zero_does_not_skip(self, monkeypatch): # the dep is denied under block enforcement. with pytest.raises(PolicyBlockError): run_policy_preflight( - project_root=Path("/tmp/fake"), + project_root=Path("fake"), mcp_deps=[_make_mcp_dep("io.github.untrusted/evil-mcp")], no_policy=False, logger=logger, @@ -166,6 +168,24 @@ def test_allowed_mcp_under_block_proceeds(self): assert result is not None assert active is True + def test_direct_mcp_plural_targets_are_policy_checked(self): + """Direct --mcp applies compilation policy to the whole target set.""" + policy = ApmPolicy( + enforcement="block", + compilation=CompilationPolicy( + target=CompilationTargetPolicy(allow=("copilot",)), + ), + ) + fetch = _make_fetch_result(policy=policy) + + with _patch_discover(fetch), pytest.raises(PolicyBlockError): + run_policy_preflight( + project_root=Path("/tmp/fake"), + mcp_deps=[_make_mcp_dep("test-server", transport="http")], + effective_target=["copilot", "claude"], + logger=_make_logger(), + ) + def test_allowed_mcp_under_warn_proceeds(self): """Allowed MCP proceeds under warn enforcement too.""" policy, _ = load_policy(MCP_POLICY_FIXTURE) diff --git a/tests/unit/install/test_policy_target_check_phase.py b/tests/unit/install/test_policy_target_check_phase.py index be28cf53f..9e9654b2b 100644 --- a/tests/unit/install/test_policy_target_check_phase.py +++ b/tests/unit/install/test_policy_target_check_phase.py @@ -73,7 +73,7 @@ class _FakeContext: # From caller / CLI apm_package: Any = None - target_override: str | None = None + target_override: str | list[str] | None = None # From policy_gate policy_fetch: Any = None @@ -353,6 +353,37 @@ def test_intellij_override_is_normalized_to_copilot(self, mock_checks): assert mock_checks.call_args.kwargs["effective_target"] == "copilot" ctx.logger.policy_violation.assert_not_called() + @patch(_PATCH_CHECKS) + def test_plural_override_is_normalized_as_one_target_set(self, mock_checks): + """Plural target policy checks preserve every selected target.""" + mock_checks.return_value = _target_passing_audit() + ctx = _make_ctx( + enforcement_active=True, + policy_fetch=_make_policy_fetch( + enforcement="block", + allow=("copilot", "claude"), + ), + target_override=["intellij", "claude"], + ) + + run(ctx) + + mock_checks.assert_called_once() + assert mock_checks.call_args.kwargs["effective_target"] == ["copilot", "claude"] + + def test_plural_override_blocks_when_any_target_is_disallowed(self): + """A plural target set cannot bypass compilation.target.allow.""" + ctx = _make_ctx( + enforcement_active=True, + policy_fetch=_make_policy_fetch(enforcement="block", allow=("copilot",)), + target_override=["intellij", "claude"], + ) + + with pytest.raises(PolicyViolationError, match="compilation target"): + run(ctx) + + ctx.logger.policy_violation.assert_called_once() + # ===================================================================== # Test: double-emit filtering From 5ef252b1da33ef45d37ef828ab27acb76fc2c75a Mon Sep 17 00:00:00 2001 From: danielmeppiel Date: Fri, 10 Jul 2026 02:30:48 +0200 Subject: [PATCH 13/16] docs: clarify IntelliJ MCP auto-discovery Separate JetBrains MCP runtime discovery from file-primitive target detection. Addresses the final doc-writer panel finding for PR #2041. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 5 +++++ packages/apm-guide/.apm/skills/apm-usage/commands.md | 7 ++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f7a888f0..306f31531 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- `apm install --target intellij` no longer rejected as "Unknown target" -- IntelliJ is now a valid MCP-only pseudo-target that configures the JetBrains Copilot plugin without deploying file-level primitives. + (by @sergio-sisternes-epam; closes #1957) (#2041) + ## [0.24.1] - 2026-07-10 ### Fixed diff --git a/packages/apm-guide/.apm/skills/apm-usage/commands.md b/packages/apm-guide/.apm/skills/apm-usage/commands.md index 679ed90dc..b34456107 100644 --- a/packages/apm-guide/.apm/skills/apm-usage/commands.md +++ b/packages/apm-guide/.apm/skills/apm-usage/commands.md @@ -40,7 +40,12 @@ When a default registry is configured, plain shorthand deps (`owner/repo#`) 1. `--target` flag (highest; CSV form: `--target claude,cursor`). 2. `apm.yml` `targets:` list (or singular `target:` sugar). 3. `apm config set target ` default. -4. Auto-detect from filesystem signals (`.claude/` or `CLAUDE.md` -> claude, `.cursor/` -> cursor, `.github/copilot-instructions.md` or any of `.github/instructions/`, `.github/agents/`, `.github/prompts/`, `.github/hooks/` -> copilot, `.codex/` -> codex, `.gemini/` or `GEMINI.md` -> gemini, `.opencode/` -> opencode, `.windsurf/` -> windsurf, `.kiro/` -> kiro, the user-scope JetBrains Copilot MCP config directory `github-copilot/intellij/` -- `%LOCALAPPDATA%\github-copilot\intellij\` on Windows, `~/Library/Application Support/github-copilot/intellij/` on macOS, `~/.local/share/github-copilot/intellij/` on Linux -> intellij). All signals except JetBrains are project-scoped repo markers; the JetBrains signal is a machine-global user-scope directory, so once the Copilot plugin is installed it is detected for every project on that machine. +4. Auto-detect file-primitive targets from project signals (`.claude/` or `CLAUDE.md` -> claude, `.cursor/` -> cursor, `.github/copilot-instructions.md` or any of `.github/instructions/`, `.github/agents/`, `.github/prompts/`, `.github/hooks/` -> copilot, `.codex/` -> codex, `.gemini/` or `GEMINI.md` -> gemini, `.opencode/` -> opencode, `.windsurf/` -> windsurf, `.kiro/` -> kiro). + +MCP runtime discovery separately recognizes the user-scope JetBrains Copilot +config directory (`github-copilot/intellij/`). That machine-global signal can +select IntelliJ for MCP configuration in every project, but it never +auto-selects a file-primitive target. `apm install` prints a one-line provenance summary before any mutation: From 8f018b84379f42f20636c1b66104e80224868cf4 Mon Sep 17 00:00:00 2001 From: danielmeppiel Date: Fri, 10 Jul 2026 16:29:46 +0200 Subject: [PATCH 14/16] fix: fold IntelliJ target review feedback Correct user-facing policy and primitive-routing claims, pin policy target normalization directly, and simplify the typed target checks. Addresses panel follow-ups from the PR #2041 convergence pass. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 3 +- .../.apm/skills/apm-usage/commands.md | 2 +- src/apm_cli/core/target_detection.py | 9 +++-- tests/unit/core/test_target_detection.py | 34 +++++++++++++++++++ 4 files changed, 41 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 306f31531..f0508fd13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- `apm install --target intellij` no longer rejected as "Unknown target" -- IntelliJ is now a valid MCP-only pseudo-target that configures the JetBrains Copilot plugin without deploying file-level primitives. +- `apm install --target intellij` now configures JetBrains Copilot MCP support + while routing package file primitives through the Copilot profile. (by @sergio-sisternes-epam; closes #1957) (#2041) ## [0.24.1] - 2026-07-10 diff --git a/packages/apm-guide/.apm/skills/apm-usage/commands.md b/packages/apm-guide/.apm/skills/apm-usage/commands.md index b34456107..152404620 100644 --- a/packages/apm-guide/.apm/skills/apm-usage/commands.md +++ b/packages/apm-guide/.apm/skills/apm-usage/commands.md @@ -10,7 +10,7 @@ | Command | Purpose | Key flags | |---------|---------|-----------| -| `apm install [PKGS...]` | Install APM and MCP dependencies (supports APM packages, Claude skills (SKILL.md), and plugin collections (plugin.json)) | `--update` (deprecated; prefer `apm update`) refresh refs, `--refresh` re-fetch all deps from upstream and re-resolve all ref pins, `--force` overwrite (does NOT refresh refs; use `apm update` for that), `--frozen` CI-safe install that fails fast when `apm.lock.yaml` is missing or out of sync with `apm.yml` (mutually exclusive with `--update`; structural presence check only -- use `apm audit` for SHA integrity), `--dry-run`, `--verbose`, `--only [apm\|mcp]`, `--target` (comma-separated, e.g. `--target claude,cursor`; highest-priority entry in the resolution chain `--target` > apm.yml `targets:` > auto-detect; `intellij` is MCP-only and writes JetBrains Copilot's user-scope config; explicit lists are exact, so `intellij,claude` writes those two MCP configs and `all,intellij` adds JetBrains to `all`; on auto-bootstrap when no `apm.yml` exists, recognized manifest target(s) are persisted to the new manifest's `targets:` so a later bare `apm update` reuses them; `--target all` deprecated, see `apm compile --all`; use `kiro` for Kiro IDE; use `copilot-cowork` with `--global` after `apm experimental enable copilot-cowork`; use `hermes` after `apm experimental enable hermes` to deploy skills + `AGENTS.md` and, at `--global`, MCP servers to `~/.hermes/config.yaml`), `--dev`, `-g` global (MCP deploys only to user-scope runtimes: Copilot CLI, Claude Code, Codex CLI, Gemini CLI, Antigravity CLI, Kiro, Windsurf, JetBrains Copilot, and Hermes when enabled), `--trust-transitive-mcp`, `--parallel-downloads N`, `--allow-insecure`, `--allow-insecure-host HOSTNAME`, `--skill NAME` install named skill(s) from a skill collection (SKILL_BUNDLE or plugin manifest; repeatable; plugin manifests accept a leaf name or manifest path; persisted in apm.yml; additive across separate installs -- a later `--skill X` adds to the existing pin (union) rather than replacing it, so previously deployed skills are never silently removed; `'*'` resets to the full bundle; drop a single skill by editing the `skills:` list in apm.yml then re-running install), `--legacy-skill-paths` restore per-client skill dirs, `--mcp NAME` add MCP entry (NAME goes through the same `--target` > `targets:` > auto-detect resolver as APM packages, so `apm install --mcp NAME --target intellij` writes only JetBrains Copilot's MCP config; compilation target policy applies to every resolved target; `apm install -g --mcp NAME` writes user-scope and bypasses the project-scope gate by design), `--transport`, `--url`, `--env KEY=VAL`, `--header KEY=VAL`, `--mcp-version`, `--registry URL` custom MCP registry, `--root DIR` redirect writes (`apm_modules/`, lockfile, `.gitignore`, integrated harness files) under DIR while `apm.yml`/`.apm/`/local deps resolve from `$PWD` (mirrors `pip install --target`; created if missing; not valid with `-g`/`--global`, which exits 2) | +| `apm install [PKGS...]` | Install APM and MCP dependencies (supports APM packages, Claude skills (SKILL.md), and plugin collections (plugin.json)) | `--update` (deprecated; prefer `apm update`) refresh refs, `--refresh` re-fetch all deps from upstream and re-resolve all ref pins, `--force` overwrite (does NOT refresh refs; use `apm update` for that), `--frozen` CI-safe install that fails fast when `apm.lock.yaml` is missing or out of sync with `apm.yml` (mutually exclusive with `--update`; structural presence check only -- use `apm audit` for SHA integrity), `--dry-run`, `--verbose`, `--only [apm\|mcp]`, `--target` (comma-separated, e.g. `--target claude,cursor`; highest-priority entry in the resolution chain `--target` > apm.yml `targets:` > auto-detect; `intellij` is MCP-only and writes JetBrains Copilot's user-scope config; explicit lists are exact, so `intellij,claude` writes those two MCP configs and `all,intellij` adds JetBrains to `all`; on auto-bootstrap when no `apm.yml` exists, recognized manifest target(s) are persisted to the new manifest's `targets:` so a later bare `apm update` reuses them; `--target all` deprecated, see `apm compile --all`; use `kiro` for Kiro IDE; use `copilot-cowork` with `--global` after `apm experimental enable copilot-cowork`; use `hermes` after `apm experimental enable hermes` to deploy skills + `AGENTS.md` and, at `--global`, MCP servers to `~/.hermes/config.yaml`), `--dev`, `-g` global (MCP deploys only to user-scope runtimes: Copilot CLI, Claude Code, Codex CLI, Gemini CLI, Antigravity CLI, Kiro, Windsurf, JetBrains Copilot, and Hermes when enabled), `--trust-transitive-mcp`, `--parallel-downloads N`, `--allow-insecure`, `--allow-insecure-host HOSTNAME`, `--skill NAME` install named skill(s) from a skill collection (SKILL_BUNDLE or plugin manifest; repeatable; plugin manifests accept a leaf name or manifest path; persisted in apm.yml; additive across separate installs -- a later `--skill X` adds to the existing pin (union) rather than replacing it, so previously deployed skills are never silently removed; `'*'` resets to the full bundle; drop a single skill by editing the `skills:` list in apm.yml then re-running install), `--legacy-skill-paths` restore per-client skill dirs, `--mcp NAME` add MCP entry (NAME goes through the same `--target` > `targets:` > auto-detect resolver as APM packages, so `apm install --mcp NAME --target intellij` writes only JetBrains Copilot's MCP config; compilation target policy applies to every explicitly selected target; `apm install -g --mcp NAME` writes user-scope and bypasses the project-scope gate by design), `--transport`, `--url`, `--env KEY=VAL`, `--header KEY=VAL`, `--mcp-version`, `--registry URL` custom MCP registry, `--root DIR` redirect writes (`apm_modules/`, lockfile, `.gitignore`, integrated harness files) under DIR while `apm.yml`/`.apm/`/local deps resolve from `$PWD` (mirrors `pip install --target`; created if missing; not valid with `-g`/`--global`, which exits 2) | | `apm targets` | Show resolved deployment targets for the current project (Click group; reads filesystem signals; works with or without `apm.yml`) | `--all` also include the `agent-skills` meta-target (only meaningful with `--json`), `--json` machine-readable output. No provenance line is printed (the table is the provenance). | | `apm uninstall PKGS...` | Remove packages (accepts `owner/repo` or `name@marketplace`) | `--dry-run`, `-g` global | | `apm prune` | Remove orphaned packages | `--dry-run` | diff --git a/src/apm_cli/core/target_detection.py b/src/apm_cli/core/target_detection.py index 10715cdc0..549014b0e 100644 --- a/src/apm_cli/core/target_detection.py +++ b/src/apm_cli/core/target_detection.py @@ -126,8 +126,9 @@ def detect_target( # noqa: PLR0911 """ # Priority 1: Explicit --target flag if explicit_target: - if explicit_target in ("copilot", "vscode", "agents") or ( - isinstance(explicit_target, str) and explicit_target in MCP_ONLY_TARGETS + if ( + explicit_target in ("copilot", "vscode", "agents") + or explicit_target in MCP_ONLY_TARGETS ): return "vscode", "explicit --target flag" elif explicit_target == "claude": @@ -153,9 +154,7 @@ def detect_target( # noqa: PLR0911 # Priority 2: apm.yml target setting if config_target: - if config_target in ("copilot", "vscode", "agents") or ( - isinstance(config_target, str) and config_target in MCP_ONLY_TARGETS - ): + if config_target in ("copilot", "vscode", "agents") or config_target in MCP_ONLY_TARGETS: return "vscode", "apm.yml target" elif config_target == "claude": return "claude", "apm.yml target" diff --git a/tests/unit/core/test_target_detection.py b/tests/unit/core/test_target_detection.py index 82518511a..f7a903835 100644 --- a/tests/unit/core/test_target_detection.py +++ b/tests/unit/core/test_target_detection.py @@ -5,6 +5,7 @@ import click import pytest +from apm_cli.core import target_detection from apm_cli.core.target_detection import ( ALL_CANONICAL_TARGETS, EXPERIMENTAL_TARGETS, @@ -15,6 +16,7 @@ detect_target, get_dedup_rules_dir, get_target_description, + normalize_policy_targets, normalize_target_list, should_compile_agents_md, should_compile_claude_md, @@ -949,6 +951,38 @@ def test_all_expansion_excludes_intellij(self): assert t in result +class TestNormalizePolicyTargets: + """Direct contract tests for MCP-only policy target normalization.""" + + @pytest.mark.parametrize( + ("value", "expected"), + [ + (None, None), + ("intellij", "copilot"), + (["intellij", "claude"], ["copilot", "claude"]), + (["intellij", "copilot"], ["copilot"]), + ], + ) + def test_normalizes_shape_and_deduplicates( + self, + value: str | list[str] | None, + expected: str | list[str] | None, + ) -> None: + """Normalize MCP-only values while preserving scalar/list shape.""" + assert normalize_policy_targets(value) == expected + + def test_unmapped_mcp_only_target_fails_closed(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Reject an MCP-only target without a canonical policy mapping.""" + monkeypatch.setattr( + target_detection, + "MCP_ONLY_TARGETS", + frozenset({*MCP_ONLY_TARGETS, "unmapped-mcp"}), + ) + + with pytest.raises(RuntimeError, match="has no canonical policy mapping"): + normalize_policy_targets("unmapped-mcp") + + # --------------------------------------------------------------------------- # Cowork parser-layer regression tests (2f96dd5 / #926) # --------------------------------------------------------------------------- From 08b53580a34aa8237290080e0e2dbea1fe3c80a3 Mon Sep 17 00:00:00 2001 From: danielmeppiel Date: Fri, 10 Jul 2026 16:44:24 +0200 Subject: [PATCH 15/16] fix: use singular IntelliJ target progress Match the progress label to the resolved target count so a scalar IntelliJ selection does not render a plural runtime message. Addresses the final CLI logging panel follow-up. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/apm_cli/integration/mcp_integrator_install.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/apm_cli/integration/mcp_integrator_install.py b/src/apm_cli/integration/mcp_integrator_install.py index 6eac661ff..3016e7cf6 100644 --- a/src/apm_cli/integration/mcp_integrator_install.py +++ b/src/apm_cli/integration/mcp_integrator_install.py @@ -354,7 +354,8 @@ def _resolve_target_runtimes( target_runtimes = ( [explicit_target] if isinstance(explicit_target, str) else list(explicit_target) ) - logger.progress(f"Targeting specific runtimes: {', '.join(target_runtimes)}") + runtime_label = "runtime" if len(target_runtimes) == 1 else "runtimes" + logger.progress(f"Targeting specific {runtime_label}: {', '.join(target_runtimes)}") else: project_root_path = Path(project_root) if project_root is not None else Path.cwd() From 519883a386a6058ec7e77db3d0718fd8795954de Mon Sep 17 00:00:00 2001 From: danielmeppiel Date: Fri, 10 Jul 2026 17:01:17 +0200 Subject: [PATCH 16/16] docs: correct IntelliJ primitive paths Distinguish Copilot-profile skill deployment under .agents/skills from the .github paths used by other primitives. Addresses the final doc-writer panel follow-up. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/src/content/docs/reference/targets-matrix.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/src/content/docs/reference/targets-matrix.md b/docs/src/content/docs/reference/targets-matrix.md index c2f629d58..65f815700 100644 --- a/docs/src/content/docs/reference/targets-matrix.md +++ b/docs/src/content/docs/reference/targets-matrix.md @@ -35,8 +35,9 @@ Skills deploy to `.agents/skills/` for Copilot, Cursor, OpenCode, Gemini, Antigravity, Codex, and Windsurf by default (see [Skills convergence](#skills-convergence) below). Claude and Kiro keep target-native skill directories. -(*) For `intellij`, file primitives route through the Copilot profile under -`.github/`; the IntelliJ-specific adapter configures MCP only. +(*) For `intellij`, file primitives route through the Copilot profile: +instructions, prompts, agents, and hooks use `.github/`, while skills use +`.agents/skills/`. The IntelliJ-specific adapter configures MCP only. `copilot-cowork` (Microsoft 365 Copilot), `copilot-app` (GitHub Copilot desktop App), `openclaw` (OpenClaw agent runtime), and `hermes` are @@ -233,8 +234,8 @@ GitHub Copilot for JetBrains IDEs. [JetBrains integration guide](../integrations/ide-tool-integration/#jetbrains-intellij-idea-pycharm-goland-and-others) for OS-specific paths. - **Supported primitives.** The IntelliJ-specific adapter supports MCP. - Instructions, prompts, agents, skills, and hooks deploy through the Copilot - profile under `.github/`. + Instructions, prompts, agents, and hooks deploy through the Copilot profile + under `.github/`; skills deploy under `.agents/skills/`. - **Scope.** MCP configuration is user scope only. File primitives use the project or user scope selected for the Copilot profile. IntelliJ does not participate in plain `all` expansion.