apm-cli 0.8.12__tar.gz → 0.9.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. {apm_cli-0.8.12/src/apm_cli.egg-info → apm_cli-0.9.0}/PKG-INFO +14 -2
  2. {apm_cli-0.8.12 → apm_cli-0.9.0}/README.md +13 -1
  3. {apm_cli-0.8.12 → apm_cli-0.9.0}/pyproject.toml +1 -1
  4. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/adapters/client/base.py +6 -0
  5. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/adapters/client/codex.py +3 -1
  6. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/adapters/client/copilot.py +3 -1
  7. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/adapters/client/cursor.py +2 -0
  8. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/adapters/client/opencode.py +2 -0
  9. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/adapters/client/vscode.py +26 -5
  10. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/bundle/lockfile_enrichment.py +39 -8
  11. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/bundle/packer.py +19 -11
  12. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/bundle/plugin_exporter.py +1 -8
  13. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/commands/_helpers.py +7 -9
  14. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/commands/compile/cli.py +55 -5
  15. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/commands/config.py +70 -21
  16. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/commands/deps/cli.py +57 -20
  17. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/commands/init.py +25 -14
  18. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/commands/install.py +769 -38
  19. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/commands/marketplace.py +87 -0
  20. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/commands/mcp.py +133 -14
  21. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/commands/outdated.py +143 -26
  22. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/commands/pack.py +3 -2
  23. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/commands/uninstall/cli.py +1 -1
  24. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/commands/uninstall/engine.py +2 -2
  25. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/commands/update.py +6 -0
  26. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/commands/view.py +130 -0
  27. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/core/auth.py +96 -22
  28. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/core/target_detection.py +142 -1
  29. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/core/token_manager.py +49 -17
  30. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/deps/github_downloader.py +294 -75
  31. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/deps/lockfile.py +43 -8
  32. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/deps/plugin_parser.py +47 -3
  33. apm_cli-0.9.0/src/apm_cli/deps/transport_selection.py +331 -0
  34. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/drift.py +19 -7
  35. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/install/context.py +4 -0
  36. apm_cli-0.9.0/src/apm_cli/install/insecure_policy.py +239 -0
  37. apm_cli-0.9.0/src/apm_cli/install/mcp_registry.py +256 -0
  38. apm_cli-0.9.0/src/apm_cli/install/mcp_warnings.py +122 -0
  39. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/install/phases/resolve.py +29 -1
  40. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/install/pipeline.py +11 -1
  41. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/install/request.py +5 -1
  42. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/install/service.py +4 -0
  43. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/install/sources.py +23 -7
  44. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/install/validation.py +29 -11
  45. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/integration/mcp_integrator.py +66 -0
  46. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/integration/targets.py +48 -10
  47. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/marketplace/client.py +5 -0
  48. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/marketplace/resolver.py +123 -9
  49. apm_cli-0.9.0/src/apm_cli/marketplace/shadow_detector.py +75 -0
  50. apm_cli-0.9.0/src/apm_cli/marketplace/validator.py +80 -0
  51. apm_cli-0.9.0/src/apm_cli/marketplace/version_pins.py +163 -0
  52. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/models/apm_package.py +1 -1
  53. apm_cli-0.9.0/src/apm_cli/models/dependency/mcp.py +254 -0
  54. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/models/dependency/reference.py +181 -71
  55. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/models/validation.py +96 -22
  56. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/policy/policy_checks.py +15 -9
  57. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/registry/client.py +75 -7
  58. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/registry/operations.py +13 -2
  59. apm_cli-0.9.0/src/apm_cli/update_policy.py +51 -0
  60. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/utils/github_host.py +22 -5
  61. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/utils/path_security.py +8 -1
  62. {apm_cli-0.8.12 → apm_cli-0.9.0/src/apm_cli.egg-info}/PKG-INFO +14 -2
  63. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli.egg-info/SOURCES.txt +8 -0
  64. {apm_cli-0.8.12 → apm_cli-0.9.0}/tests/test_apm_package_models.py +127 -0
  65. {apm_cli-0.8.12 → apm_cli-0.9.0}/tests/test_github_downloader.py +1 -1
  66. {apm_cli-0.8.12 → apm_cli-0.9.0}/tests/test_lockfile.py +54 -0
  67. {apm_cli-0.8.12 → apm_cli-0.9.0}/tests/test_token_manager.py +66 -2
  68. apm_cli-0.8.12/src/apm_cli/models/dependency/mcp.py +0 -136
  69. {apm_cli-0.8.12 → apm_cli-0.9.0}/AUTHORS +0 -0
  70. {apm_cli-0.8.12 → apm_cli-0.9.0}/LICENSE +0 -0
  71. {apm_cli-0.8.12 → apm_cli-0.9.0}/setup.cfg +0 -0
  72. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/__init__.py +0 -0
  73. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/adapters/__init__.py +0 -0
  74. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/adapters/client/__init__.py +0 -0
  75. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/adapters/package_manager/__init__.py +0 -0
  76. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/adapters/package_manager/base.py +0 -0
  77. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/adapters/package_manager/default_manager.py +0 -0
  78. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/bundle/__init__.py +0 -0
  79. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/bundle/unpacker.py +0 -0
  80. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/cli.py +0 -0
  81. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/commands/__init__.py +0 -0
  82. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/commands/audit.py +0 -0
  83. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/commands/compile/__init__.py +0 -0
  84. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/commands/compile/watcher.py +0 -0
  85. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/commands/deps/__init__.py +0 -0
  86. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/commands/deps/_utils.py +0 -0
  87. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/commands/list_cmd.py +0 -0
  88. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/commands/prune.py +0 -0
  89. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/commands/run.py +0 -0
  90. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/commands/runtime.py +0 -0
  91. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/commands/uninstall/__init__.py +0 -0
  92. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/compilation/__init__.py +0 -0
  93. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/compilation/agents_compiler.py +0 -0
  94. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/compilation/claude_formatter.py +0 -0
  95. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/compilation/constants.py +0 -0
  96. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/compilation/constitution.py +0 -0
  97. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/compilation/constitution_block.py +0 -0
  98. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/compilation/context_optimizer.py +0 -0
  99. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/compilation/distributed_compiler.py +0 -0
  100. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/compilation/injector.py +0 -0
  101. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/compilation/link_resolver.py +0 -0
  102. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/compilation/template_builder.py +0 -0
  103. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/config.py +0 -0
  104. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/constants.py +0 -0
  105. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/core/__init__.py +0 -0
  106. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/core/command_logger.py +0 -0
  107. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/core/conflict_detector.py +0 -0
  108. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/core/docker_args.py +0 -0
  109. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/core/operations.py +0 -0
  110. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/core/safe_installer.py +0 -0
  111. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/core/scope.py +0 -0
  112. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/core/script_runner.py +0 -0
  113. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/deps/__init__.py +0 -0
  114. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/deps/aggregator.py +0 -0
  115. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/deps/apm_resolver.py +0 -0
  116. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/deps/artifactory_entry.py +0 -0
  117. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/deps/collection_parser.py +0 -0
  118. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/deps/dependency_graph.py +0 -0
  119. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/deps/installed_package.py +0 -0
  120. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/deps/package_validator.py +0 -0
  121. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/deps/registry_proxy.py +0 -0
  122. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/deps/verifier.py +0 -0
  123. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/factory.py +0 -0
  124. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/install/__init__.py +0 -0
  125. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/install/helpers/__init__.py +0 -0
  126. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/install/helpers/security_scan.py +0 -0
  127. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/install/phases/__init__.py +0 -0
  128. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/install/phases/cleanup.py +0 -0
  129. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/install/phases/download.py +0 -0
  130. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/install/phases/finalize.py +0 -0
  131. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/install/phases/integrate.py +0 -0
  132. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/install/phases/local_content.py +0 -0
  133. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/install/phases/lockfile.py +0 -0
  134. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/install/phases/post_deps_local.py +0 -0
  135. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/install/phases/targets.py +0 -0
  136. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/install/presentation/__init__.py +0 -0
  137. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/install/presentation/dry_run.py +0 -0
  138. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/install/services.py +0 -0
  139. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/install/template.py +0 -0
  140. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/integration/__init__.py +0 -0
  141. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/integration/agent_integrator.py +0 -0
  142. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/integration/base_integrator.py +0 -0
  143. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/integration/cleanup.py +0 -0
  144. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/integration/command_integrator.py +0 -0
  145. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/integration/coverage.py +0 -0
  146. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/integration/dispatch.py +0 -0
  147. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/integration/hook_integrator.py +0 -0
  148. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/integration/instruction_integrator.py +0 -0
  149. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/integration/prompt_integrator.py +0 -0
  150. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/integration/skill_integrator.py +0 -0
  151. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/integration/skill_transformer.py +0 -0
  152. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/integration/utils.py +0 -0
  153. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/marketplace/__init__.py +0 -0
  154. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/marketplace/errors.py +0 -0
  155. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/marketplace/models.py +0 -0
  156. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/marketplace/registry.py +0 -0
  157. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/models/__init__.py +0 -0
  158. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/models/dependency/__init__.py +0 -0
  159. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/models/dependency/types.py +0 -0
  160. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/models/plugin.py +0 -0
  161. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/models/results.py +0 -0
  162. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/output/__init__.py +0 -0
  163. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/output/formatters.py +0 -0
  164. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/output/models.py +0 -0
  165. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/output/script_formatters.py +0 -0
  166. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/policy/__init__.py +0 -0
  167. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/policy/ci_checks.py +0 -0
  168. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/policy/discovery.py +0 -0
  169. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/policy/inheritance.py +0 -0
  170. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/policy/matcher.py +0 -0
  171. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/policy/models.py +0 -0
  172. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/policy/parser.py +0 -0
  173. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/policy/schema.py +0 -0
  174. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/primitives/__init__.py +0 -0
  175. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/primitives/discovery.py +0 -0
  176. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/primitives/models.py +0 -0
  177. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/primitives/parser.py +0 -0
  178. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/registry/__init__.py +0 -0
  179. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/registry/integration.py +0 -0
  180. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/runtime/__init__.py +0 -0
  181. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/runtime/base.py +0 -0
  182. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/runtime/codex_runtime.py +0 -0
  183. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/runtime/copilot_runtime.py +0 -0
  184. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/runtime/factory.py +0 -0
  185. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/runtime/llm_runtime.py +0 -0
  186. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/runtime/manager.py +0 -0
  187. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/security/__init__.py +0 -0
  188. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/security/audit_report.py +0 -0
  189. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/security/content_scanner.py +0 -0
  190. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/security/file_scanner.py +0 -0
  191. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/security/gate.py +0 -0
  192. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/utils/__init__.py +0 -0
  193. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/utils/console.py +0 -0
  194. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/utils/content_hash.py +0 -0
  195. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/utils/diagnostics.py +0 -0
  196. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/utils/exclude.py +0 -0
  197. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/utils/file_ops.py +0 -0
  198. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/utils/helpers.py +0 -0
  199. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/utils/paths.py +0 -0
  200. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/utils/version_checker.py +0 -0
  201. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/utils/yaml_io.py +0 -0
  202. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/version.py +0 -0
  203. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/workflow/__init__.py +0 -0
  204. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/workflow/discovery.py +0 -0
  205. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/workflow/parser.py +0 -0
  206. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli/workflow/runner.py +0 -0
  207. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli.egg-info/dependency_links.txt +0 -0
  208. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli.egg-info/entry_points.txt +0 -0
  209. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli.egg-info/requires.txt +0 -0
  210. {apm_cli-0.8.12 → apm_cli-0.9.0}/src/apm_cli.egg-info/top_level.txt +0 -0
  211. {apm_cli-0.8.12 → apm_cli-0.9.0}/tests/test_apm_resolver.py +0 -0
  212. {apm_cli-0.8.12 → apm_cli-0.9.0}/tests/test_codex_docker_args_fix.py +0 -0
  213. {apm_cli-0.8.12 → apm_cli-0.9.0}/tests/test_codex_empty_string_and_defaults.py +0 -0
  214. {apm_cli-0.8.12 → apm_cli-0.9.0}/tests/test_collision_integration.py +0 -0
  215. {apm_cli-0.8.12 → apm_cli-0.9.0}/tests/test_console.py +0 -0
  216. {apm_cli-0.8.12 → apm_cli-0.9.0}/tests/test_distributed_compilation.py +0 -0
  217. {apm_cli-0.8.12 → apm_cli-0.9.0}/tests/test_empty_string_and_defaults.py +0 -0
  218. {apm_cli-0.8.12 → apm_cli-0.9.0}/tests/test_enhanced_discovery.py +0 -0
  219. {apm_cli-0.8.12 → apm_cli-0.9.0}/tests/test_github_downloader_token_precedence.py +0 -0
  220. {apm_cli-0.8.12 → apm_cli-0.9.0}/tests/test_runnable_prompts.py +0 -0
  221. {apm_cli-0.8.12 → apm_cli-0.9.0}/tests/test_runtime_manager_token_precedence.py +0 -0
  222. {apm_cli-0.8.12 → apm_cli-0.9.0}/tests/test_virtual_package_multi_install.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: apm-cli
3
- Version: 0.8.12
3
+ Version: 0.9.0
4
4
  Summary: MCP configuration tool
5
5
  Author-email: Daniel Meppiel <user@example.com>
6
6
  License: MIT License
@@ -90,6 +90,10 @@ dependencies:
90
90
  - github/awesome-copilot/agents/api-architect.agent.md
91
91
  # A full APM package with instructions, skills, prompts, hooks...
92
92
  - microsoft/apm-sample-package#v1.0.0
93
+ mcp:
94
+ # MCP servers -- installed into every detected client
95
+ - name: io.github.github/github-mcp-server
96
+ transport: http # MCP transport name, not URL scheme -- connects over HTTPS
93
97
  ```
94
98
 
95
99
  ```bash
@@ -99,7 +103,7 @@ apm install # every agent is configured
99
103
 
100
104
  ## Highlights
101
105
 
102
- - **[One manifest for everything](https://microsoft.github.io/apm/reference/primitive-types/)** — instructions, skills, prompts, agents, hooks, plugins, MCP servers
106
+ - **[One manifest for everything](https://microsoft.github.io/apm/reference/primitive-types/)** — instructions, skills, prompts, agents, hooks, plugins, and [MCP servers](https://microsoft.github.io/apm/guides/mcp-servers/) declared in `apm.yml` and deployed across every client on install
103
107
  - **[Install from anywhere](https://microsoft.github.io/apm/guides/dependencies/)** — GitHub, GitLab, Bitbucket, Azure DevOps, GitHub Enterprise, any git host
104
108
  - **[Transitive dependencies](https://microsoft.github.io/apm/guides/dependencies/)** — packages can depend on packages; APM resolves the full tree
105
109
  - **[Content security](https://microsoft.github.io/apm/enterprise/security/)** — `apm audit` scans for hidden Unicode; `apm install` blocks compromised packages before agents read them
@@ -161,6 +165,14 @@ apm marketplace add github/awesome-copilot
161
165
  apm install azure-cloud-development@awesome-copilot
162
166
  ```
163
167
 
168
+ Or add an MCP server (wired into Copilot, Claude, Cursor, Codex, and OpenCode):
169
+
170
+ ```bash
171
+ apm install --mcp io.github.github/github-mcp-server --transport http # connects over HTTPS
172
+ ```
173
+
174
+ > *Codex CLI currently does not support remote MCP servers; the install will skip Codex with a notice. Omit `--transport http` to use the local Docker variant on Codex (requires `GITHUB_PERSONAL_ACCESS_TOKEN`).*
175
+
164
176
  See the **[Getting Started guide](https://microsoft.github.io/apm/getting-started/quick-start/)** for the full walkthrough.
165
177
 
166
178
  ## Works with agentrc
@@ -28,6 +28,10 @@ dependencies:
28
28
  - github/awesome-copilot/agents/api-architect.agent.md
29
29
  # A full APM package with instructions, skills, prompts, hooks...
30
30
  - microsoft/apm-sample-package#v1.0.0
31
+ mcp:
32
+ # MCP servers -- installed into every detected client
33
+ - name: io.github.github/github-mcp-server
34
+ transport: http # MCP transport name, not URL scheme -- connects over HTTPS
31
35
  ```
32
36
 
33
37
  ```bash
@@ -37,7 +41,7 @@ apm install # every agent is configured
37
41
 
38
42
  ## Highlights
39
43
 
40
- - **[One manifest for everything](https://microsoft.github.io/apm/reference/primitive-types/)** — instructions, skills, prompts, agents, hooks, plugins, MCP servers
44
+ - **[One manifest for everything](https://microsoft.github.io/apm/reference/primitive-types/)** — instructions, skills, prompts, agents, hooks, plugins, and [MCP servers](https://microsoft.github.io/apm/guides/mcp-servers/) declared in `apm.yml` and deployed across every client on install
41
45
  - **[Install from anywhere](https://microsoft.github.io/apm/guides/dependencies/)** — GitHub, GitLab, Bitbucket, Azure DevOps, GitHub Enterprise, any git host
42
46
  - **[Transitive dependencies](https://microsoft.github.io/apm/guides/dependencies/)** — packages can depend on packages; APM resolves the full tree
43
47
  - **[Content security](https://microsoft.github.io/apm/enterprise/security/)** — `apm audit` scans for hidden Unicode; `apm install` blocks compromised packages before agents read them
@@ -99,6 +103,14 @@ apm marketplace add github/awesome-copilot
99
103
  apm install azure-cloud-development@awesome-copilot
100
104
  ```
101
105
 
106
+ Or add an MCP server (wired into Copilot, Claude, Cursor, Codex, and OpenCode):
107
+
108
+ ```bash
109
+ apm install --mcp io.github.github/github-mcp-server --transport http # connects over HTTPS
110
+ ```
111
+
112
+ > *Codex CLI currently does not support remote MCP servers; the install will skip Codex with a notice. Omit `--transport http` to use the local Docker variant on Codex (requires `GITHUB_PERSONAL_ACCESS_TOKEN`).*
113
+
102
114
  See the **[Getting Started guide](https://microsoft.github.io/apm/getting-started/quick-start/)** for the full walkthrough.
103
115
 
104
116
  ## Works with agentrc
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "apm-cli"
7
- version = "0.8.12"
7
+ version = "0.9.0"
8
8
  description = "MCP configuration tool"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -9,6 +9,12 @@ _INPUT_VAR_RE = re.compile(r"\$\{input:([^}]+)\}")
9
9
  class MCPClientAdapter(ABC):
10
10
  """Base adapter for MCP clients."""
11
11
 
12
+ # Whether this adapter's config path is user/global-scoped (e.g.
13
+ # ``~/.copilot/``) rather than workspace-scoped (e.g. ``.vscode/``).
14
+ # Adapters that target a global path should override this to ``True``
15
+ # so that ``apm install --global`` can install MCP servers to them.
16
+ supports_user_scope: bool = False
17
+
12
18
  @abstractmethod
13
19
  def get_config_path(self):
14
20
  """Get the path to the MCP configuration file."""
@@ -20,7 +20,9 @@ class CodexClientAdapter(MCPClientAdapter):
20
20
  a global ~/.codex/config.toml file, following the TOML format for
21
21
  MCP server configuration.
22
22
  """
23
-
23
+
24
+ supports_user_scope: bool = True
25
+
24
26
  def __init__(self, registry_url=None):
25
27
  """Initialize the Codex CLI client adapter.
26
28
 
@@ -23,7 +23,9 @@ class CopilotClientAdapter(MCPClientAdapter):
23
23
  a global ~/.copilot/mcp-config.json file, following the JSON format for
24
24
  MCP server configuration.
25
25
  """
26
-
26
+
27
+ supports_user_scope: bool = True
28
+
27
29
  def __init__(self, registry_url=None):
28
30
  """Initialize the Copilot CLI client adapter.
29
31
 
@@ -25,6 +25,8 @@ class CursorClientAdapter(CopilotClientAdapter):
25
25
  of global ``~/.copilot/mcp-config.json``.
26
26
  """
27
27
 
28
+ supports_user_scope: bool = False
29
+
28
30
  # ------------------------------------------------------------------ #
29
31
  # Config path
30
32
  # ------------------------------------------------------------------ #
@@ -40,6 +40,8 @@ class OpenCodeClientAdapter(CopilotClientAdapter):
40
40
  and writes to ``opencode.json`` in the project root.
41
41
  """
42
42
 
43
+ supports_user_scope: bool = False
44
+
43
45
  def get_config_path(self):
44
46
  """Return the path to ``opencode.json`` in the repository root."""
45
47
  return str(Path(os.getcwd()) / "opencode.json")
@@ -320,17 +320,25 @@ class VSCodeClientAdapter(MCPClientAdapter):
320
320
  }
321
321
  # Check for remotes (similar to Copilot adapter)
322
322
  elif "remotes" in server_info and server_info["remotes"]:
323
- remotes = server_info["remotes"]
324
- remote = remotes[0] # Take the first remote
325
- transport = remote.get("transport_type", "")
326
- if transport in ("sse", "http", "streamable-http"):
323
+ remote = self._select_remote_with_url(server_info["remotes"])
324
+ if remote:
325
+ transport = (remote.get("transport_type") or "").strip()
326
+ # Default to "http" when transport_type is missing/empty,
327
+ # matching the Copilot adapter behavior (copilot.py:190-192).
328
+ if not transport:
329
+ transport = "http"
330
+ elif transport not in ("sse", "http", "streamable-http"):
331
+ raise ValueError(
332
+ f"Unsupported remote transport '{transport}' for VS Code. "
333
+ f"Server: {server_info.get('name', 'unknown')}. "
334
+ f"Supported transports: http, sse, streamable-http.")
327
335
  headers = remote.get("headers", {})
328
336
  # Normalize header list format to dict
329
337
  if isinstance(headers, list):
330
338
  headers = {h["name"]: h["value"] for h in headers if "name" in h and "value" in h}
331
339
  server_config = {
332
340
  "type": transport,
333
- "url": remote.get("url", ""),
341
+ "url": remote["url"].strip(),
334
342
  "headers": headers,
335
343
  }
336
344
  input_vars.extend(
@@ -424,6 +432,19 @@ class VSCodeClientAdapter(MCPClientAdapter):
424
432
 
425
433
  return []
426
434
 
435
+ @staticmethod
436
+ def _select_remote_with_url(remotes):
437
+ """Return the first remote entry that has a non-empty URL.
438
+
439
+ Returns:
440
+ dict or None: The first usable remote, or None if none found.
441
+ """
442
+ for remote in remotes:
443
+ url = (remote.get("url") or "").strip()
444
+ if url:
445
+ return remote
446
+ return None
447
+
427
448
  def _select_best_package(self, packages):
428
449
  """Select the best package for VS Code installation from available packages.
429
450
 
@@ -1,7 +1,7 @@
1
1
  """Lockfile enrichment for pack-time metadata."""
2
2
 
3
3
  from datetime import datetime, timezone
4
- from typing import Dict, List, Tuple
4
+ from typing import Dict, List, Tuple, Union
5
5
 
6
6
  from ..deps.lockfile import LockFile
7
7
 
@@ -55,7 +55,7 @@ _CROSS_TARGET_MAPS: Dict[str, Dict[str, str]] = {
55
55
 
56
56
 
57
57
  def _filter_files_by_target(
58
- deployed_files: List[str], target: str
58
+ deployed_files: List[str], target: Union[str, List[str]]
59
59
  ) -> Tuple[List[str], Dict[str, str]]:
60
60
  """Filter deployed file paths by target prefix, with cross-target mapping.
61
61
 
@@ -64,16 +64,38 @@ def _filter_files_by_target(
64
64
  remapped to the equivalent target path. Commands, instructions, and hooks
65
65
  are NOT remapped -- they are target-specific.
66
66
 
67
+ *target* may be a single string or a list of strings. For a list, the
68
+ union of all relevant prefixes and cross-target maps is used.
69
+
67
70
  Returns:
68
71
  A tuple of ``(filtered_files, path_mappings)`` where *path_mappings*
69
72
  maps ``bundle_path -> disk_path`` for any file that was cross-target
70
73
  remapped. Direct matches have no entry in the dict.
71
74
  """
72
- prefixes = _TARGET_PREFIXES.get(target, _TARGET_PREFIXES["all"])
75
+ if isinstance(target, list):
76
+ # Union all prefixes for the targets in the list
77
+ prefixes: List[str] = []
78
+ seen_prefixes: set = set()
79
+ for t in target:
80
+ for p in _TARGET_PREFIXES.get(t, []):
81
+ if p not in seen_prefixes:
82
+ seen_prefixes.add(p)
83
+ prefixes.append(p)
84
+ # Union all cross-target maps
85
+ # NOTE: dict.update() means the last target's mapping wins when
86
+ # multiple targets map the same source prefix. In practice this
87
+ # is benign -- common multi-target combos (e.g. claude+copilot)
88
+ # match prefixes directly without needing cross-maps.
89
+ cross_map: Dict[str, str] = {}
90
+ for t in target:
91
+ cross_map.update(_CROSS_TARGET_MAPS.get(t, {}))
92
+ else:
93
+ prefixes = _TARGET_PREFIXES.get(target, _TARGET_PREFIXES["all"])
94
+ cross_map = _CROSS_TARGET_MAPS.get(target, {})
95
+
73
96
  direct = [f for f in deployed_files if any(f.startswith(p) for p in prefixes)]
74
97
 
75
98
  path_mappings: Dict[str, str] = {}
76
- cross_map = _CROSS_TARGET_MAPS.get(target, {})
77
99
  if cross_map:
78
100
  direct_set = set(direct)
79
101
  for f in deployed_files:
@@ -94,7 +116,7 @@ def _filter_files_by_target(
94
116
  def enrich_lockfile_for_pack(
95
117
  lockfile: LockFile,
96
118
  fmt: str,
97
- target: str,
119
+ target: Union[str, List[str]],
98
120
  ) -> str:
99
121
  """Create an enriched copy of the lockfile YAML with a ``pack:`` section.
100
122
 
@@ -109,7 +131,8 @@ def enrich_lockfile_for_pack(
109
131
  lockfile: The resolved lockfile to enrich.
110
132
  fmt: Bundle format (``"apm"`` or ``"plugin"``).
111
133
  target: Effective target used for packing (e.g. ``"copilot"``, ``"claude"``,
112
- ``"all"``). The internal alias ``"vscode"`` is also accepted.
134
+ ``"all"``). May also be a list of target strings for multi-target
135
+ packing. The internal alias ``"vscode"`` is also accepted.
113
136
 
114
137
  Returns:
115
138
  A YAML string with the ``pack:`` block followed by the original
@@ -132,9 +155,12 @@ def enrich_lockfile_for_pack(
132
155
 
133
156
  # Build the pack: metadata section (after filtering so we know if mapping
134
157
  # occurred).
158
+ # Serialize target as a comma-joined string for backward compatibility
159
+ # with consumers that expect a plain string in pack.target.
160
+ target_str = ",".join(target) if isinstance(target, list) else target
135
161
  pack_meta: Dict = {
136
162
  "format": fmt,
137
- "target": target,
163
+ "target": target_str,
138
164
  "packed_at": datetime.now(timezone.utc).isoformat(),
139
165
  }
140
166
  if all_mappings:
@@ -142,7 +168,12 @@ def enrich_lockfile_for_pack(
142
168
  # bundle paths differ from the original lockfile. Use the canonical
143
169
  # prefix keys from _CROSS_TARGET_MAPS rather than reverse-engineering
144
170
  # them from file paths.
145
- cross_map = _CROSS_TARGET_MAPS.get(target, {})
171
+ if isinstance(target, list):
172
+ cross_map: Dict[str, str] = {}
173
+ for t in target:
174
+ cross_map.update(_CROSS_TARGET_MAPS.get(t, {}))
175
+ else:
176
+ cross_map = _CROSS_TARGET_MAPS.get(target, {})
146
177
  used_src_prefixes = set()
147
178
  for original in all_mappings.values():
148
179
  for src_prefix in cross_map:
@@ -5,7 +5,7 @@ import shutil
5
5
  import tarfile
6
6
  from dataclasses import dataclass, field
7
7
  from pathlib import Path
8
- from typing import Dict, List, Optional
8
+ from typing import Dict, List, Optional, Union
9
9
 
10
10
  from ..deps.lockfile import LockFile, get_lockfile_path, migrate_lockfile_if_needed
11
11
  from ..models.apm_package import APMPackage
@@ -28,7 +28,7 @@ def pack_bundle(
28
28
  project_root: Path,
29
29
  output_dir: Path,
30
30
  fmt: str = "apm",
31
- target: Optional[str] = None,
31
+ target: Optional[Union[str, List[str]]] = None,
32
32
  archive: bool = False,
33
33
  dry_run: bool = False,
34
34
  force: bool = False,
@@ -40,7 +40,8 @@ def pack_bundle(
40
40
  project_root: Root of the project containing ``apm.lock.yaml`` and ``apm.yml``.
41
41
  output_dir: Directory where the bundle will be created.
42
42
  fmt: Bundle format -- ``"apm"`` (default) or ``"plugin"``.
43
- target: Target filter -- ``"copilot"``, ``"claude"``, ``"all"``, or *None*
43
+ target: Target filter -- ``"copilot"``, ``"claude"``, ``"all"``, a list of
44
+ target strings (e.g. ``["claude", "vscode"]``), or *None*
44
45
  (auto-detect from apm.yml / project structure).
45
46
  archive: If *True*, produce a ``.tar.gz`` and remove the directory.
46
47
  dry_run: If *True*, resolve the file list but write nothing to disk.
@@ -102,14 +103,21 @@ def pack_bundle(
102
103
  config_target = None
103
104
 
104
105
  # 3. Resolve effective target
105
- effective_target, _reason = detect_target(
106
- project_root,
107
- explicit_target=target,
108
- config_target=config_target,
109
- )
110
- # For packing purposes, "minimal" means nothing to pack -- treat as "all"
111
- if effective_target == "minimal":
112
- effective_target = "all"
106
+ if isinstance(target, list):
107
+ # List from CLI (e.g. --target claude,copilot) passes through directly
108
+ effective_target = target
109
+ elif isinstance(config_target, list) and target is None:
110
+ # List from apm.yml target: [claude, copilot]
111
+ effective_target = config_target
112
+ else:
113
+ effective_target, _reason = detect_target(
114
+ project_root,
115
+ explicit_target=target,
116
+ config_target=config_target if isinstance(config_target, str) else None,
117
+ )
118
+ # For packing purposes, "minimal" means nothing to pack -- treat as "all"
119
+ if effective_target == "minimal":
120
+ effective_target = "all"
113
121
 
114
122
  # 4. Collect deployed_files from all dependencies, filtered by target
115
123
  all_deployed: List[str] = []
@@ -396,14 +396,7 @@ def _update_plugin_json_paths(plugin_json: dict, output_files: List[str]) -> dic
396
396
 
397
397
  def _dep_install_path(dep: LockedDependency, apm_modules_dir: Path) -> Path:
398
398
  """Compute the filesystem install path for a locked dependency."""
399
- dep_ref = DependencyReference(
400
- repo_url=dep.repo_url,
401
- host=dep.host,
402
- virtual_path=dep.virtual_path,
403
- is_virtual=dep.is_virtual,
404
- is_local=(dep.source == "local"),
405
- local_path=dep.local_path,
406
- )
399
+ dep_ref = dep.to_dependency_ref()
407
400
  return dep_ref.get_install_path(apm_modules_dir)
408
401
 
409
402
 
@@ -21,6 +21,7 @@ from ..constants import (
21
21
  GITIGNORE_FILENAME,
22
22
  )
23
23
  from ..utils.console import _rich_echo, _rich_info, _rich_warning
24
+ from ..update_policy import get_update_hint_message, is_self_update_enabled
24
25
  from ..version import get_build_sha, get_version
25
26
  from ..utils.version_checker import check_for_updates
26
27
 
@@ -121,8 +122,6 @@ def _build_expected_install_paths(declared_deps, lockfile, apm_modules_dir: Path
121
122
  (depth > 1 from ``apm.lock``), using ``get_install_path()`` for
122
123
  consistency with how packages are actually installed.
123
124
  """
124
- from ..models.apm_package import DependencyReference
125
-
126
125
  expected = builtins.set()
127
126
  for dep in declared_deps:
128
127
  install_path = dep.get_install_path(apm_modules_dir)
@@ -135,12 +134,7 @@ def _build_expected_install_paths(declared_deps, lockfile, apm_modules_dir: Path
135
134
  if lockfile:
136
135
  for dep in lockfile.get_all_dependencies():
137
136
  if dep.depth is not None and dep.depth > 1:
138
- dep_ref = DependencyReference(
139
- repo_url=dep.repo_url,
140
- host=dep.host,
141
- virtual_path=dep.virtual_path,
142
- is_virtual=dep.is_virtual,
143
- )
137
+ dep_ref = dep.to_dependency_ref()
144
138
  install_path = dep_ref.get_install_path(apm_modules_dir)
145
139
  try:
146
140
  relative_path = install_path.relative_to(apm_modules_dir)
@@ -240,6 +234,10 @@ def print_version(ctx, param, value):
240
234
  def _check_and_notify_updates():
241
235
  """Check for updates and notify user non-blockingly."""
242
236
  try:
237
+ # Skip notifications when self-update is disabled by distribution policy.
238
+ if not is_self_update_enabled():
239
+ return
240
+
243
241
  # Skip version check in E2E test mode to avoid interfering with tests
244
242
  if os.environ.get("APM_E2E_TESTS", "").lower() in ("1", "true", "yes"):
245
243
  return
@@ -260,7 +258,7 @@ def _check_and_notify_updates():
260
258
  )
261
259
 
262
260
  # Show update command using helper for consistency
263
- _rich_echo("Run apm update to upgrade", color="yellow", bold=True)
261
+ _rich_echo(get_update_hint_message(), color="yellow", bold=True)
264
262
 
265
263
  # Add a blank line for visual separation
266
264
  click.echo()
@@ -8,6 +8,7 @@ import click
8
8
  from ...constants import AGENTS_MD_FILENAME, APM_DIR, APM_MODULES_DIR, APM_YML_FILENAME
9
9
  from ...compilation import AgentsCompiler, CompilationConfig
10
10
  from ...core.command_logger import CommandLogger
11
+ from ...core.target_detection import TargetParamType
11
12
  from ...primitives.discovery import discover_primitives
12
13
  from ...utils.console import (
13
14
  _rich_error,
@@ -162,6 +163,36 @@ def _get_validation_suggestion(error_msg):
162
163
  return "Check primitive structure and frontmatter"
163
164
 
164
165
 
166
+ def _resolve_compile_target(target):
167
+ """Map CLI target input to compiler-understood target string.
168
+
169
+ The compiler only understands ``"vscode"``, ``"claude"``, and ``"all"``.
170
+ Multi-target lists are mapped to the narrowest equivalent.
171
+
172
+ Args:
173
+ target: A single target string, a list of target strings, or ``None``.
174
+
175
+ Returns:
176
+ A single string (or ``None``) suitable for :func:`detect_target`.
177
+ """
178
+ if target is None:
179
+ return None # will trigger detect_target() auto-detection
180
+ if isinstance(target, list):
181
+ target_set = set(target)
182
+ # Any target that produces AGENTS.md (copilot/vscode/agents/cursor/opencode/codex)
183
+ has_agents_family = bool(
184
+ target_set & {"copilot", "vscode", "agents", "cursor", "opencode", "codex"}
185
+ )
186
+ has_claude = "claude" in target_set
187
+ if has_agents_family and has_claude:
188
+ return "all"
189
+ elif has_claude:
190
+ return "claude"
191
+ else:
192
+ return "vscode" # agents-family only
193
+ return target # single string pass-through
194
+
195
+
165
196
  @click.command(help="Compile APM context into distributed AGENTS.md files")
166
197
  @click.option(
167
198
  "--output",
@@ -172,9 +203,9 @@ def _get_validation_suggestion(error_msg):
172
203
  @click.option(
173
204
  "--target",
174
205
  "-t",
175
- type=click.Choice(["copilot", "claude", "cursor", "opencode", "codex", "vscode", "agents", "all"]),
206
+ type=TargetParamType(),
176
207
  default=None,
177
- help="Target platform: copilot (AGENTS.md), claude (CLAUDE.md), cursor, opencode, or all. 'vscode' and 'agents' are deprecated aliases for 'copilot'. Auto-detects if not specified.",
208
+ help="Target platform (comma-separated for multiple, e.g. claude,copilot). Use 'all' for every target. Auto-detects if not specified.",
178
209
  )
179
210
  @click.option(
180
211
  "--dry-run",
@@ -354,10 +385,14 @@ def compile(
354
385
  # No apm.yml or parsing error - proceed with auto-detection
355
386
  pass
356
387
 
388
+ # Resolve list targets to compiler-understood string
389
+ compile_target = _resolve_compile_target(target)
390
+ # Also handle config_target being a list (from apm.yml target: [claude, copilot])
391
+ compile_config_target = _resolve_compile_target(config_target)
357
392
  detected_target, detection_reason = detect_target(
358
393
  project_root=Path("."),
359
- explicit_target=target,
360
- config_target=config_target,
394
+ explicit_target=compile_target,
395
+ config_target=compile_config_target,
361
396
  )
362
397
 
363
398
  # Map 'minimal' to 'vscode' for the compiler (AGENTS.md only, no folder integration)
@@ -383,7 +418,22 @@ def compile(
383
418
  # Show target-aware message with detection reason. Use
384
419
  # get_target_description() so any future target added to
385
420
  # target_detection shows up here automatically.
386
- if detected_target == "minimal":
421
+ if isinstance(target, list):
422
+ # Multi-target list: show what the compiler will produce
423
+ _target_label = ",".join(target)
424
+ if effective_target == "all":
425
+ logger.progress(
426
+ f"Compiling for AGENTS.md + CLAUDE.md (--target {_target_label})"
427
+ )
428
+ elif effective_target == "claude":
429
+ logger.progress(
430
+ f"Compiling for CLAUDE.md (--target {_target_label})"
431
+ )
432
+ else:
433
+ logger.progress(
434
+ f"Compiling for AGENTS.md (--target {_target_label})"
435
+ )
436
+ elif detected_target == "minimal":
387
437
  logger.progress(f"Compiling for AGENTS.md only ({detection_reason})")
388
438
  logger.progress(
389
439
  " Create .github/, .claude/, .codex/, .opencode/ or .cursor/ folder for full integration",
@@ -14,6 +14,46 @@ from ._helpers import HIGHLIGHT, RESET, _get_console, _load_apm_config
14
14
  # Restore builtin since a subcommand is named ``set``
15
15
  set = builtins.set
16
16
 
17
+ _BOOLEAN_TRUE_VALUES = {"true", "1", "yes"}
18
+ _BOOLEAN_FALSE_VALUES = {"false", "0", "no"}
19
+ _CONFIG_KEY_DISPLAY_NAMES = {
20
+ "auto_integrate": "auto-integrate",
21
+ "temp_dir": "temp-dir",
22
+ }
23
+
24
+
25
+ def _parse_bool_value(value: str) -> bool:
26
+ """Parse a CLI boolean value."""
27
+ normalized = value.strip().lower()
28
+ if normalized in _BOOLEAN_TRUE_VALUES:
29
+ return True
30
+ if normalized in _BOOLEAN_FALSE_VALUES:
31
+ return False
32
+ raise ValueError(f"Invalid value '{value}'. Use 'true' or 'false'.")
33
+
34
+
35
+ def _get_config_setters():
36
+ """Return config setters keyed by CLI option name."""
37
+ from ..config import set_auto_integrate
38
+
39
+ return {
40
+ "auto-integrate": (set_auto_integrate, "Auto-integration"),
41
+ }
42
+
43
+
44
+ def _get_config_getters():
45
+ """Return config getters keyed by CLI option name."""
46
+ from ..config import get_auto_integrate
47
+
48
+ return {
49
+ "auto-integrate": get_auto_integrate,
50
+ }
51
+
52
+
53
+ def _valid_config_keys() -> str:
54
+ """Return valid config keys for messages."""
55
+ return ", ".join(["auto-integrate", "temp-dir"])
56
+
17
57
 
18
58
  @click.group(help="Configure APM CLI", invoke_without_command=True)
19
59
  @click.pass_context
@@ -128,35 +168,41 @@ def set(key, value):
128
168
  apm config set auto-integrate false
129
169
  apm config set auto-integrate true
130
170
  """
131
- from ..config import set_auto_integrate, set_temp_dir
171
+ from ..config import get_temp_dir, set_temp_dir
132
172
 
133
173
  logger = CommandLogger("config set")
134
- if key == "auto-integrate":
135
- if value.lower() in ["true", "1", "yes"]:
136
- set_auto_integrate(True)
137
- logger.success("Auto-integration enabled")
138
- elif value.lower() in ["false", "0", "no"]:
139
- set_auto_integrate(False)
140
- logger.success("Auto-integration disabled")
141
- else:
142
- logger.error(f"Invalid value '{value}'. Use 'true' or 'false'.")
143
- sys.exit(1)
144
- elif key == "temp-dir":
174
+ if key == "temp-dir":
145
175
  try:
146
176
  set_temp_dir(value)
147
- from ..config import get_temp_dir
148
177
  logger.success(f"Temporary directory set to: {get_temp_dir()}")
149
178
  except ValueError as exc:
150
179
  logger.error(str(exc))
151
180
  sys.exit(1)
152
- else:
181
+ return
182
+
183
+ setters = _get_config_setters()
184
+ config_entry = setters.get(key)
185
+ if config_entry is None:
153
186
  logger.error(f"Unknown configuration key: '{key}'")
154
- logger.progress("Valid keys: auto-integrate, temp-dir")
187
+ logger.progress(f"Valid keys: {_valid_config_keys()}")
155
188
  logger.progress(
156
189
  "This error may indicate a bug in command routing. Please report this issue."
157
190
  )
158
191
  sys.exit(1)
159
192
 
193
+ try:
194
+ enabled = _parse_bool_value(value)
195
+ except ValueError as exc:
196
+ logger.error(str(exc))
197
+ sys.exit(1)
198
+
199
+ setter, label = config_entry
200
+ setter(enabled)
201
+ if enabled:
202
+ logger.success(f"{label} enabled")
203
+ else:
204
+ logger.success(f"{label} disabled")
205
+
160
206
 
161
207
  @config.command(help="Get a configuration value")
162
208
  @click.argument("key", required=False)
@@ -170,23 +216,26 @@ def get(key):
170
216
  from ..config import get_auto_integrate, get_temp_dir
171
217
 
172
218
  logger = CommandLogger("config get")
219
+ getters = _get_config_getters()
173
220
  if key:
174
- if key == "auto-integrate":
175
- value = get_auto_integrate()
176
- click.echo(f"auto-integrate: {value}")
177
- elif key == "temp-dir":
221
+ if key == "temp-dir":
178
222
  value = get_temp_dir()
179
223
  if value is None:
180
224
  click.echo("temp-dir: Not set (using system default)")
181
225
  else:
182
226
  click.echo(f"temp-dir: {value}")
183
- else:
227
+ return
228
+
229
+ getter = getters.get(key)
230
+ if getter is None:
184
231
  logger.error(f"Unknown configuration key: '{key}'")
185
- logger.progress("Valid keys: auto-integrate, temp-dir")
232
+ logger.progress(f"Valid keys: {_valid_config_keys()}")
186
233
  logger.progress(
187
234
  "This error may indicate a bug in command routing. Please report this issue."
188
235
  )
189
236
  sys.exit(1)
237
+ value = getter()
238
+ click.echo(f"{key}: {value}")
190
239
  else:
191
240
  # Show all user-settable keys with their effective values (including
192
241
  # defaults). Iterating raw config keys would hide settings that