diff --git a/docs/src/content/docs/producer/compile.md b/docs/src/content/docs/producer/compile.md index 116ffaced..6c707113c 100644 --- a/docs/src/content/docs/producer/compile.md +++ b/docs/src/content/docs/producer/compile.md @@ -90,10 +90,12 @@ apm compile --all # every canonical target ``` Accepted values: `copilot`, `claude`, `cursor`, `opencode`, `codex`, -`gemini`, `antigravity`, `windsurf`, `kiro`, `agent-skills`, `all`. The `agent-skills` slug -is a no-op for compile (skills are deployed by `apm install`); it is -accepted in target lists for symmetry only. Unknown slugs are -rejected before any work runs. +`gemini`, `antigravity`, `windsurf`, `kiro`, `intellij`, `agent-skills`, +and `all`. The `agent-skills` slug is a no-op for compile (skills are +deployed by `apm install`); it is accepted in target lists for symmetry +only. `intellij` uses the Copilot profile for file primitives and produces +`AGENTS.md`; IntelliJ-specific integration remains MCP-only. Unknown slugs +are rejected before any work runs. Experimental targets (`hermes`, `openclaw`, `copilot-cowork`, `copilot-app`) are deployment targets for `apm install --target ` diff --git a/docs/src/content/docs/reference/cli/compile.md b/docs/src/content/docs/reference/cli/compile.md index 6fbeb27cf..2f8f6c5e0 100644 --- a/docs/src/content/docs/reference/cli/compile.md +++ b/docs/src/content/docs/reference/cli/compile.md @@ -59,7 +59,7 @@ written. Critical findings cause the command to exit non-zero. See | Flag | Description | |------|-------------| -| `-t, --target VALUE` | Target(s) to compile. Comma-separated. Values: `copilot`, `claude`, `cursor`, `opencode`, `codex`, `gemini`, `antigravity`, `windsurf`, `kiro`, `agent-skills`, `all`. | +| `-t, --target VALUE` | Target(s) to compile. Comma-separated. The help text derives its values from the same catalog used for validation, including `intellij`. | | `--all` | Compile for all canonical targets. Equivalent to `--target all` and mutually exclusive with `--target`. Preferred form. | `vscode` and `agents` are accepted as deprecated aliases for `copilot` @@ -67,8 +67,10 @@ and emit a one-line warning. `--target all` also emits a deprecation warning -- prefer `--all`. `agent-skills` is a no-op for `compile` (skills-only deployment target); -`antigravity` is explicit-only and is not included in `all`. Use `apm install` -or `apm deps update` when you want shared `.agents/skills/` output. +`antigravity` is explicit-only and `intellij` is MCP-only. Neither is included in `all`. +For `intellij`, file primitives use the Copilot profile and produce `AGENTS.md`; +IntelliJ-specific integration remains MCP-only. Use `apm install` or +`apm deps update` when you want shared `.agents/skills/` output. ### Output control diff --git a/packages/apm-guide/.apm/skills/apm-usage/commands.md b/packages/apm-guide/.apm/skills/apm-usage/commands.md index 152404620..0ec9e4092 100644 --- a/packages/apm-guide/.apm/skills/apm-usage/commands.md +++ b/packages/apm-guide/.apm/skills/apm-usage/commands.md @@ -63,7 +63,7 @@ If no `--target`, no `targets:` in `apm.yml`, and no harness signal is present, | Command | Purpose | Key flags | |---------|---------|-----------| -| `apm compile` | Compile agent context | `-o` output, `-t` target (comma-separated; resolution chain `--target` > apm.yml `targets:` > auto-detect), `--all` compile for every canonical target (preferred over deprecated `--target all`), `-g`/`--global` (read global instructions from `~/.apm/apm_modules/`, write user-scope root files; cannot combine with project-output flags such as `--target`, `--all`, `--watch`, `--root`, or `--output`; critical hidden-character findings stop the write and exit 1), `--chatmode`, `--dry-run`, `--no-links`, `--watch`, `--validate`, `--single-agents`, `-v` verbose, `--local-only`, `--clean`, `--with-constitution/--no-constitution`, `--force-instructions` / `--no-dedup` (opt out of Claude/Copilot deduplication), `--root DIR` redirect generated artifacts under DIR while sources resolve from `$PWD` (mirrors `pip install --target`; not valid with `--watch`) | +| `apm compile` | Compile agent context | `-o` output, `-t` target (comma-separated; resolution chain `--target` > apm.yml `targets:` > auto-detect; `intellij` is accepted and uses the Copilot profile for file primitives), `--all` compile for every canonical target (preferred over deprecated `--target all`), `-g`/`--global` (read global instructions from `~/.apm/apm_modules/`, write user-scope root files; cannot combine with project-output flags such as `--target`, `--all`, `--watch`, `--root`, or `--output`; critical hidden-character findings stop the write and exit 1), `--chatmode`, `--dry-run`, `--no-links`, `--watch`, `--validate`, `--single-agents`, `-v` verbose, `--local-only`, `--clean`, `--with-constitution/--no-constitution`, `--force-instructions` / `--no-dedup` (opt out of Claude/Copilot deduplication), `--root DIR` redirect generated artifacts under DIR while sources resolve from `$PWD` (mirrors `pip install --target`; not valid with `--watch`) | `apm compile --watch` live-reloads `apm.yml`: editing `target:` / `targets:` mid-session takes effect on the next file event without restarting the watcher. The CLI `--target` flag, when passed to `apm compile --watch`, still outranks `apm.yml`. Re-resolution is gated on the changed file's basename being `apm.yml`, so `.instructions.md` edits do not pay an extra resolver round-trip and a stray `backup_apm.yml` cannot trigger a reload. `--clean` is ignored in watch mode and the watcher prints an explicit `[!]` warning at startup (`--clean is ignored in watch mode; run 'apm compile --clean' separately to remove orphaned outputs.`); run `apm compile --clean` separately between watch sessions to remove orphans. diff --git a/src/apm_cli/commands/compile/cli.py b/src/apm_cli/commands/compile/cli.py index 4977e88fc..d4721d7a0 100644 --- a/src/apm_cli/commands/compile/cli.py +++ b/src/apm_cli/commands/compile/cli.py @@ -15,7 +15,7 @@ from ...compilation import AgentsCompiler, CompilationConfig from ...constants import AGENTS_MD_FILENAME, APM_DIR, APM_MODULES_DIR, APM_YML_FILENAME from ...core.command_logger import CommandLogger -from ...core.target_detection import TargetParamType +from ...core.target_detection import TARGET_VALUES_HELP, TargetParamType from ...primitives.discovery import clear_discovery_cache, discover_primitives from ...utils import perf_stats from ...utils.console import ( @@ -895,7 +895,12 @@ def _coerce_provenance_targets(value): "-t", type=TargetParamType(), default=None, - help="Target platform (comma-separated). 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/ and is explicit-only -- not part of 'all'. 'all' = copilot+claude+cursor+opencode+codex+gemini+windsurf+kiro (excludes agent-skills and antigravity); combine with 'agent-skills' or 'antigravity' to add them.", + help=f"Target platform (comma-separated). Values: {TARGET_VALUES_HELP}. " + "'agent-skills' deploys to .agents/skills/ (cross-client). " + "'antigravity' (alias 'agy') deploys to .agents/ and is explicit-only -- not part of 'all'. " + "'intellij' is MCP-only; file primitives use the Copilot profile. " + "'all' excludes agent-skills, antigravity, experimental targets, and intellij; " + "combine explicit-only targets when needed.", ) @click.option( "--dry-run", diff --git a/src/apm_cli/commands/install.py b/src/apm_cli/commands/install.py index 503a38a08..026c641ce 100644 --- a/src/apm_cli/commands/install.py +++ b/src/apm_cli/commands/install.py @@ -97,7 +97,11 @@ ) from ..core.auth import AuthResolver from ..core.command_logger import InstallLogger, _ValidationOutcome -from ..core.target_detection import TargetParamType, manifest_targets_from_target_option +from ..core.target_detection import ( + TARGET_VALUES_HELP, + TargetParamType, + manifest_targets_from_target_option, +) # MCP --mcp helpers (module-level re-exports for test patches); must stay at # import time per comments in the original mid-file block. @@ -950,7 +954,14 @@ 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. For 'apm compile', use '--all'; '--target all' is deprecated.", + help=f"Target harness(es) to deploy to. Use commas for multiple targets; repeating the flag " + f"keeps only the last value (use commas instead). Values: {TARGET_VALUES_HELP}. " + "IntelliJ-specific integration is MCP-only; file primitives use the Copilot profile. " + "'all' excludes agent-skills, antigravity, experimental targets, and intellij; combine " + "explicit-only targets when needed. Experimental targets 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 549014b0e..0be30c52c 100644 --- a/src/apm_cli/core/target_detection.py +++ b/src/apm_cli/core/target_detection.py @@ -536,6 +536,9 @@ def normalize_policy_targets(value: str | list[str] | None) -> str | list[str] | | frozenset({"all"}) ) +#: Stable user-facing projection of every value accepted by ``--target``. +TARGET_VALUES_HELP = ", ".join(sorted(VALID_TARGET_VALUES)) + def parse_target_field( value: str | list[str] | None, @@ -723,13 +726,13 @@ def convert( # Use the v2 three-section error renderer for unknown targets # so that CLI, apm.yml, and auto-detect all share the same # error format (#1154). - from apm_cli.core.apm_yml import CANONICAL_TARGETS from apm_cli.core.errors import UnknownTargetError, render_unknown_target_error err_msg = str(e) if "is not a valid target" in err_msg: target_name = value if isinstance(value, str) else ",".join(value or []) - rendered = render_unknown_target_error(target_name, sorted(CANONICAL_TARGETS)) + advertised = set(CANONICAL_TARGETS_ORDERED) | MCP_ONLY_TARGETS | {"all"} + rendered = render_unknown_target_error(target_name, sorted(advertised)) raise UnknownTargetError(rendered) from None # Click idiom: route validation errors through self.fail so the # user sees a clean "Invalid value for '--target': ..." message diff --git a/tests/unit/compilation/test_compile_target_flag.py b/tests/unit/compilation/test_compile_target_flag.py index beaefec6e..fc37c1ade 100644 --- a/tests/unit/compilation/test_compile_target_flag.py +++ b/tests/unit/compilation/test_compile_target_flag.py @@ -24,6 +24,7 @@ CompilationConfig, CompilationResult, ) +from apm_cli.core.target_detection import CANONICAL_TARGETS_ORDERED, MCP_ONLY_TARGETS from apm_cli.primitives.models import Instruction, PrimitiveCollection @@ -989,6 +990,38 @@ def temp_project(self): yield temp_path shutil.rmtree(temp_dir, ignore_errors=True) + def test_help_advertises_intellij_target(self, runner): + """Compile help lists every accepted target, including IntelliJ.""" + result = runner.invoke(cli, ["compile", "--help"]) + + assert result.exit_code == 0 + assert "intellij" in result.output.lower() + + def test_invalid_target_error_advertises_supported_catalog(self, runner): + """Unknown-target guidance lists stable and MCP-only targets.""" + result = runner.invoke(cli, ["compile", "--target", "definitely-bogus"]) + + assert result.exit_code == 2 + valid_line = next( + line for line in result.output.splitlines() if line.startswith("Valid targets:") + ) + advertised = {target.strip() for target in valid_line.partition(":")[2].split(",")} + assert advertised == set(CANONICAL_TARGETS_ORDERED) | MCP_ONLY_TARGETS | {"all"} + assert "intellij" in advertised + assert "agents" not in advertised + + def test_target_flag_accepts_intellij(self, runner, temp_project): + """Compile accepts IntelliJ and generates the Copilot-profile AGENTS.md.""" + original_dir = os.getcwd() + try: + os.chdir(temp_project) + result = runner.invoke(cli, ["compile", "--target", "intellij"]) + + assert result.exit_code == 0, result.output + assert (temp_project / "AGENTS.md").is_file() + finally: + os.chdir(original_dir) + def test_target_flag_accepts_vscode(self, runner, temp_project): """Test that --target vscode is accepted.""" original_dir = os.getcwd() diff --git a/tests/unit/integration/test_kiro_target.py b/tests/unit/integration/test_kiro_target.py index 138b522e4..7372b977b 100644 --- a/tests/unit/integration/test_kiro_target.py +++ b/tests/unit/integration/test_kiro_target.py @@ -10,6 +10,7 @@ from click.testing import CliRunner from apm_cli.cli import cli +from apm_cli.core.target_detection import CANONICAL_TARGETS_ORDERED, MCP_ONLY_TARGETS from apm_cli.integration.hook_integrator import HookIntegrator from apm_cli.integration.instruction_integrator import InstructionIntegrator from apm_cli.integration.skill_integrator import SkillIntegrator @@ -55,18 +56,36 @@ def test_kiro_is_discoverable_in_target_help() -> None: install = runner.invoke(cli, ["install", "--help"]) compile_result = runner.invoke(cli, ["compile", "--help"]) - expected_all_targets = "copilot+claude+cursor+opencode+codex+gemini+windsurf+kiro" install_help = "".join(install.output.split()) compile_help = "".join(compile_result.output.split()) assert install.exit_code == 0 assert compile_result.exit_code == 0 - assert expected_all_targets in install_help - assert expected_all_targets in compile_help + # The accepted-target catalog (VALID_TARGET_VALUES) is the single source of + # truth for the help Values list, so every advertised target -- including + # the MCP-only 'intellij' target -- must be discoverable in both help texts. + for target in ("kiro", "intellij", "copilot", "claude", "windsurf"): + assert target in install_help + assert target in compile_help assert "ClaudeCode" in install_help assert "Windsurf" in install_help +def test_unknown_target_error_advertises_stable_and_mcp_targets() -> None: + runner = CliRunner() + + result = runner.invoke(cli, ["compile", "--target", "definitely-bogus"]) + + assert result.exit_code == 2 + valid_line = next( + line for line in result.output.splitlines() if line.startswith("Valid targets:") + ) + advertised = {target.strip() for target in valid_line.partition(":")[2].split(",")} + assert advertised == set(CANONICAL_TARGETS_ORDERED) | MCP_ONLY_TARGETS | {"all"} + assert "intellij" in advertised + assert "agents" not in advertised + + def test_kiro_runtime_discovered_in_user_scope_without_project_dir(tmp_path: Path) -> None: from apm_cli.integration.mcp_integrator_install import _discover_installed_runtimes