apm-cli 0.8.4__tar.gz → 0.8.6__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 (174) hide show
  1. {apm_cli-0.8.4/src/apm_cli.egg-info → apm_cli-0.8.6}/PKG-INFO +1 -1
  2. {apm_cli-0.8.4 → apm_cli-0.8.6}/pyproject.toml +2 -2
  3. apm_cli-0.8.6/src/apm_cli/bundle/lockfile_enrichment.py +154 -0
  4. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/bundle/packer.py +26 -9
  5. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/bundle/plugin_exporter.py +2 -1
  6. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/bundle/unpacker.py +15 -0
  7. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/commands/_helpers.py +4 -7
  8. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/commands/audit.py +245 -100
  9. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/commands/compile/cli.py +2 -2
  10. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/commands/deps/_utils.py +23 -0
  11. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/commands/deps/cli.py +24 -13
  12. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/commands/install.py +191 -67
  13. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/commands/mcp.py +1 -1
  14. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/commands/pack.py +105 -9
  15. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/commands/runtime.py +2 -2
  16. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/commands/uninstall/cli.py +3 -5
  17. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/commands/uninstall/engine.py +5 -5
  18. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/compilation/agents_compiler.py +12 -42
  19. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/compilation/claude_formatter.py +6 -5
  20. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/compilation/context_optimizer.py +16 -16
  21. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/compilation/distributed_compiler.py +12 -11
  22. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/compilation/template_builder.py +7 -6
  23. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/config.py +26 -9
  24. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/core/auth.py +23 -18
  25. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/core/script_runner.py +7 -8
  26. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/core/target_detection.py +28 -5
  27. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/deps/aggregator.py +2 -2
  28. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/deps/github_downloader.py +41 -52
  29. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/deps/lockfile.py +22 -3
  30. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/deps/plugin_parser.py +4 -2
  31. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/deps/verifier.py +2 -2
  32. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/integration/agent_integrator.py +7 -6
  33. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/integration/command_integrator.py +3 -2
  34. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/integration/hook_integrator.py +2 -1
  35. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/integration/instruction_integrator.py +3 -2
  36. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/integration/mcp_integrator.py +2 -4
  37. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/integration/prompt_integrator.py +2 -1
  38. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/integration/skill_integrator.py +107 -140
  39. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/integration/targets.py +40 -13
  40. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/models/apm_package.py +2 -2
  41. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/models/dependency/reference.py +328 -241
  42. apm_cli-0.8.6/src/apm_cli/policy/__init__.py +47 -0
  43. apm_cli-0.8.6/src/apm_cli/policy/ci_checks.py +322 -0
  44. apm_cli-0.8.6/src/apm_cli/policy/discovery.py +426 -0
  45. apm_cli-0.8.6/src/apm_cli/policy/inheritance.py +260 -0
  46. apm_cli-0.8.6/src/apm_cli/policy/matcher.py +84 -0
  47. apm_cli-0.8.6/src/apm_cli/policy/models.py +143 -0
  48. apm_cli-0.8.6/src/apm_cli/policy/parser.py +269 -0
  49. apm_cli-0.8.6/src/apm_cli/policy/policy_checks.py +788 -0
  50. apm_cli-0.8.6/src/apm_cli/policy/schema.py +110 -0
  51. apm_cli-0.8.6/src/apm_cli/security/file_scanner.py +85 -0
  52. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/security/gate.py +2 -1
  53. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/utils/__init__.py +3 -0
  54. apm_cli-0.8.6/src/apm_cli/utils/file_ops.py +294 -0
  55. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/utils/path_security.py +50 -6
  56. apm_cli-0.8.6/src/apm_cli/utils/paths.py +27 -0
  57. apm_cli-0.8.6/src/apm_cli/utils/yaml_io.py +55 -0
  58. {apm_cli-0.8.4 → apm_cli-0.8.6/src/apm_cli.egg-info}/PKG-INFO +1 -1
  59. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli.egg-info/SOURCES.txt +13 -0
  60. {apm_cli-0.8.4 → apm_cli-0.8.6}/tests/test_apm_package_models.py +463 -348
  61. {apm_cli-0.8.4 → apm_cli-0.8.6}/tests/test_github_downloader.py +9 -0
  62. {apm_cli-0.8.4 → apm_cli-0.8.6}/tests/test_lockfile.py +68 -0
  63. apm_cli-0.8.4/src/apm_cli/bundle/lockfile_enrichment.py +0 -76
  64. {apm_cli-0.8.4 → apm_cli-0.8.6}/AUTHORS +0 -0
  65. {apm_cli-0.8.4 → apm_cli-0.8.6}/LICENSE +0 -0
  66. {apm_cli-0.8.4 → apm_cli-0.8.6}/README.md +0 -0
  67. {apm_cli-0.8.4 → apm_cli-0.8.6}/setup.cfg +0 -0
  68. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/__init__.py +0 -0
  69. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/adapters/__init__.py +0 -0
  70. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/adapters/client/__init__.py +0 -0
  71. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/adapters/client/base.py +0 -0
  72. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/adapters/client/codex.py +0 -0
  73. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/adapters/client/copilot.py +0 -0
  74. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/adapters/client/cursor.py +0 -0
  75. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/adapters/client/opencode.py +0 -0
  76. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/adapters/client/vscode.py +0 -0
  77. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/adapters/package_manager/__init__.py +0 -0
  78. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/adapters/package_manager/base.py +0 -0
  79. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/adapters/package_manager/default_manager.py +0 -0
  80. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/bundle/__init__.py +0 -0
  81. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/cli.py +0 -0
  82. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/commands/__init__.py +0 -0
  83. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/commands/compile/__init__.py +0 -0
  84. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/commands/compile/watcher.py +0 -0
  85. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/commands/config.py +0 -0
  86. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/commands/deps/__init__.py +0 -0
  87. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/commands/init.py +0 -0
  88. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/commands/list_cmd.py +0 -0
  89. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/commands/prune.py +0 -0
  90. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/commands/run.py +0 -0
  91. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/commands/uninstall/__init__.py +0 -0
  92. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/commands/update.py +0 -0
  93. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/compilation/__init__.py +0 -0
  94. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/compilation/constants.py +0 -0
  95. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/compilation/constitution.py +0 -0
  96. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/compilation/constitution_block.py +0 -0
  97. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/compilation/injector.py +0 -0
  98. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/compilation/link_resolver.py +0 -0
  99. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/constants.py +0 -0
  100. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/core/__init__.py +0 -0
  101. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/core/command_logger.py +0 -0
  102. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/core/conflict_detector.py +0 -0
  103. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/core/docker_args.py +0 -0
  104. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/core/operations.py +0 -0
  105. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/core/safe_installer.py +0 -0
  106. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/core/token_manager.py +0 -0
  107. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/deps/__init__.py +0 -0
  108. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/deps/apm_resolver.py +0 -0
  109. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/deps/collection_parser.py +0 -0
  110. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/deps/dependency_graph.py +0 -0
  111. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/deps/package_validator.py +0 -0
  112. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/drift.py +0 -0
  113. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/factory.py +0 -0
  114. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/integration/__init__.py +0 -0
  115. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/integration/base_integrator.py +0 -0
  116. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/integration/skill_transformer.py +0 -0
  117. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/integration/utils.py +0 -0
  118. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/models/__init__.py +0 -0
  119. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/models/dependency/__init__.py +0 -0
  120. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/models/dependency/mcp.py +0 -0
  121. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/models/dependency/types.py +0 -0
  122. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/models/plugin.py +0 -0
  123. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/models/results.py +0 -0
  124. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/models/validation.py +0 -0
  125. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/output/__init__.py +0 -0
  126. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/output/formatters.py +0 -0
  127. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/output/models.py +0 -0
  128. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/output/script_formatters.py +0 -0
  129. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/primitives/__init__.py +0 -0
  130. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/primitives/discovery.py +0 -0
  131. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/primitives/models.py +0 -0
  132. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/primitives/parser.py +0 -0
  133. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/registry/__init__.py +0 -0
  134. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/registry/client.py +0 -0
  135. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/registry/integration.py +0 -0
  136. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/registry/operations.py +0 -0
  137. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/runtime/__init__.py +0 -0
  138. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/runtime/base.py +0 -0
  139. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/runtime/codex_runtime.py +0 -0
  140. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/runtime/copilot_runtime.py +0 -0
  141. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/runtime/factory.py +0 -0
  142. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/runtime/llm_runtime.py +0 -0
  143. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/runtime/manager.py +0 -0
  144. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/security/__init__.py +0 -0
  145. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/security/audit_report.py +0 -0
  146. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/security/content_scanner.py +0 -0
  147. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/utils/console.py +0 -0
  148. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/utils/content_hash.py +0 -0
  149. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/utils/diagnostics.py +0 -0
  150. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/utils/github_host.py +0 -0
  151. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/utils/helpers.py +0 -0
  152. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/utils/version_checker.py +0 -0
  153. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/version.py +0 -0
  154. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/workflow/__init__.py +0 -0
  155. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/workflow/discovery.py +0 -0
  156. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/workflow/parser.py +0 -0
  157. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli/workflow/runner.py +0 -0
  158. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli.egg-info/dependency_links.txt +0 -0
  159. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli.egg-info/entry_points.txt +0 -0
  160. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli.egg-info/requires.txt +0 -0
  161. {apm_cli-0.8.4 → apm_cli-0.8.6}/src/apm_cli.egg-info/top_level.txt +0 -0
  162. {apm_cli-0.8.4 → apm_cli-0.8.6}/tests/test_apm_resolver.py +0 -0
  163. {apm_cli-0.8.4 → apm_cli-0.8.6}/tests/test_codex_docker_args_fix.py +0 -0
  164. {apm_cli-0.8.4 → apm_cli-0.8.6}/tests/test_codex_empty_string_and_defaults.py +0 -0
  165. {apm_cli-0.8.4 → apm_cli-0.8.6}/tests/test_collision_integration.py +0 -0
  166. {apm_cli-0.8.4 → apm_cli-0.8.6}/tests/test_console.py +0 -0
  167. {apm_cli-0.8.4 → apm_cli-0.8.6}/tests/test_distributed_compilation.py +0 -0
  168. {apm_cli-0.8.4 → apm_cli-0.8.6}/tests/test_empty_string_and_defaults.py +0 -0
  169. {apm_cli-0.8.4 → apm_cli-0.8.6}/tests/test_enhanced_discovery.py +0 -0
  170. {apm_cli-0.8.4 → apm_cli-0.8.6}/tests/test_github_downloader_token_precedence.py +0 -0
  171. {apm_cli-0.8.4 → apm_cli-0.8.6}/tests/test_runnable_prompts.py +0 -0
  172. {apm_cli-0.8.4 → apm_cli-0.8.6}/tests/test_runtime_manager_token_precedence.py +0 -0
  173. {apm_cli-0.8.4 → apm_cli-0.8.6}/tests/test_token_manager.py +0 -0
  174. {apm_cli-0.8.4 → apm_cli-0.8.6}/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.4
3
+ Version: 0.8.6
4
4
  Summary: MCP configuration tool
5
5
  Author-email: Daniel Meppiel <user@example.com>
6
6
  License: MIT License
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "apm-cli"
7
- version = "0.8.4"
7
+ version = "0.8.6"
8
8
  description = "MCP configuration tool"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -71,7 +71,7 @@ warn_return_any = true
71
71
  warn_unused_configs = true
72
72
 
73
73
  [tool.pytest.ini_options]
74
- addopts = "-m 'not benchmark' -n auto"
74
+ addopts = "-m 'not benchmark'"
75
75
  markers = [
76
76
  "integration: marks tests as integration tests that may require network access",
77
77
  "slow: marks tests as slow running tests",
@@ -0,0 +1,154 @@
1
+ """Lockfile enrichment for pack-time metadata."""
2
+
3
+ from datetime import datetime, timezone
4
+ from typing import Dict, List, Tuple
5
+
6
+ from ..deps.lockfile import LockFile
7
+
8
+
9
+ # Authoritative mapping of target names to deployed-file path prefixes.
10
+ _TARGET_PREFIXES = {
11
+ "copilot": [".github/"],
12
+ "vscode": [".github/"],
13
+ "claude": [".claude/"],
14
+ "cursor": [".cursor/"],
15
+ "opencode": [".opencode/"],
16
+ "all": [".github/", ".claude/", ".cursor/", ".opencode/"],
17
+ }
18
+
19
+ # Cross-target path equivalences for skills/ and agents/ directories.
20
+ # Only these two directory types are semantically identical across targets;
21
+ # commands, instructions, hooks are target-specific and are NOT mapped.
22
+ #
23
+ # .github/ is the canonical interop prefix -- install always creates it, so
24
+ # all non-github targets map FROM .github/. The copilot target additionally
25
+ # maps FROM .claude/ for the common case of Claude-first projects packing
26
+ # for Copilot. Cursor/opencode sources are niche; if someone publishes
27
+ # skills exclusively under .cursor/, they must pack with --target cursor.
28
+ _CROSS_TARGET_MAPS: Dict[str, Dict[str, str]] = {
29
+ "claude": {
30
+ ".github/skills/": ".claude/skills/",
31
+ ".github/agents/": ".claude/agents/",
32
+ },
33
+ "vscode": {
34
+ ".claude/skills/": ".github/skills/",
35
+ ".claude/agents/": ".github/agents/",
36
+ },
37
+ "copilot": {
38
+ ".claude/skills/": ".github/skills/",
39
+ ".claude/agents/": ".github/agents/",
40
+ },
41
+ "cursor": {
42
+ ".github/skills/": ".cursor/skills/",
43
+ ".github/agents/": ".cursor/agents/",
44
+ },
45
+ "opencode": {
46
+ ".github/skills/": ".opencode/skills/",
47
+ ".github/agents/": ".opencode/agents/",
48
+ },
49
+ }
50
+
51
+
52
+ def _filter_files_by_target(
53
+ deployed_files: List[str], target: str
54
+ ) -> Tuple[List[str], Dict[str, str]]:
55
+ """Filter deployed file paths by target prefix, with cross-target mapping.
56
+
57
+ When files are deployed under one target prefix (e.g. ``.github/skills/``)
58
+ but the pack target is different (e.g. ``claude``), skills and agents are
59
+ remapped to the equivalent target path. Commands, instructions, and hooks
60
+ are NOT remapped -- they are target-specific.
61
+
62
+ Returns:
63
+ A tuple of ``(filtered_files, path_mappings)`` where *path_mappings*
64
+ maps ``bundle_path -> disk_path`` for any file that was cross-target
65
+ remapped. Direct matches have no entry in the dict.
66
+ """
67
+ prefixes = _TARGET_PREFIXES.get(target, _TARGET_PREFIXES["all"])
68
+ direct = [f for f in deployed_files if any(f.startswith(p) for p in prefixes)]
69
+
70
+ path_mappings: Dict[str, str] = {}
71
+ cross_map = _CROSS_TARGET_MAPS.get(target, {})
72
+ if cross_map:
73
+ direct_set = set(direct)
74
+ for f in deployed_files:
75
+ if f in direct_set:
76
+ continue
77
+ for src_prefix, dst_prefix in cross_map.items():
78
+ if f.startswith(src_prefix):
79
+ mapped = dst_prefix + f[len(src_prefix):]
80
+ if mapped not in direct_set:
81
+ direct.append(mapped)
82
+ direct_set.add(mapped)
83
+ path_mappings[mapped] = f
84
+ break
85
+
86
+ return direct, path_mappings
87
+
88
+
89
+ def enrich_lockfile_for_pack(
90
+ lockfile: LockFile,
91
+ fmt: str,
92
+ target: str,
93
+ ) -> str:
94
+ """Create an enriched copy of the lockfile YAML with a ``pack:`` section.
95
+
96
+ Filters each dependency's ``deployed_files`` to only include paths
97
+ matching the pack *target*, so the bundle lockfile is consistent with
98
+ the files actually shipped in the bundle.
99
+
100
+ Does NOT mutate the original *lockfile* object -- serialises a copy and
101
+ prepends the pack metadata.
102
+
103
+ Args:
104
+ lockfile: The resolved lockfile to enrich.
105
+ fmt: Bundle format (``"apm"`` or ``"plugin"``).
106
+ target: Effective target used for packing (e.g. ``"copilot"``, ``"claude"``,
107
+ ``"all"``). The internal alias ``"vscode"`` is also accepted.
108
+
109
+ Returns:
110
+ A YAML string with the ``pack:`` block followed by the original
111
+ lockfile content.
112
+ """
113
+ import yaml
114
+
115
+ # Build a filtered lockfile YAML: each dep's deployed_files is narrowed
116
+ # to only the paths matching the pack target (with cross-target mapping).
117
+ all_mappings: Dict[str, str] = {}
118
+ data = yaml.safe_load(lockfile.to_yaml())
119
+ if data and "dependencies" in data:
120
+ for dep in data["dependencies"]:
121
+ if "deployed_files" in dep:
122
+ filtered, mappings = _filter_files_by_target(
123
+ dep["deployed_files"], target
124
+ )
125
+ dep["deployed_files"] = filtered
126
+ all_mappings.update(mappings)
127
+
128
+ # Build the pack: metadata section (after filtering so we know if mapping
129
+ # occurred).
130
+ pack_meta: Dict = {
131
+ "format": fmt,
132
+ "target": target,
133
+ "packed_at": datetime.now(timezone.utc).isoformat(),
134
+ }
135
+ if all_mappings:
136
+ # Record the source prefixes that were remapped so consumers know the
137
+ # bundle paths differ from the original lockfile. Use the canonical
138
+ # prefix keys from _CROSS_TARGET_MAPS rather than reverse-engineering
139
+ # them from file paths.
140
+ cross_map = _CROSS_TARGET_MAPS.get(target, {})
141
+ used_src_prefixes = set()
142
+ for original in all_mappings.values():
143
+ for src_prefix in cross_map:
144
+ if original.startswith(src_prefix):
145
+ used_src_prefixes.add(src_prefix)
146
+ break
147
+ pack_meta["mapped_from"] = sorted(used_src_prefixes)
148
+
149
+ from ..utils.yaml_io import yaml_to_str
150
+
151
+ pack_section = yaml_to_str({"pack": pack_meta})
152
+
153
+ lockfile_yaml = yaml_to_str(data)
154
+ return pack_section + lockfile_yaml
@@ -5,12 +5,12 @@ import shutil
5
5
  import tarfile
6
6
  from dataclasses import dataclass, field
7
7
  from pathlib import Path
8
- from typing import List, Optional
8
+ from typing import Dict, List, Optional
9
9
 
10
10
  from ..deps.lockfile import LockFile, get_lockfile_path, migrate_lockfile_if_needed
11
11
  from ..models.apm_package import APMPackage
12
12
  from ..core.target_detection import detect_target
13
- from .lockfile_enrichment import enrich_lockfile_for_pack, _TARGET_PREFIXES, _filter_files_by_target
13
+ from .lockfile_enrichment import enrich_lockfile_for_pack, _filter_files_by_target
14
14
 
15
15
 
16
16
  @dataclass
@@ -20,6 +20,8 @@ class PackResult:
20
20
  bundle_path: Path
21
21
  files: List[str] = field(default_factory=list)
22
22
  lockfile_enriched: bool = False
23
+ mapped_count: int = 0
24
+ path_mappings: Dict[str, str] = field(default_factory=dict)
23
25
 
24
26
 
25
27
  def pack_bundle(
@@ -38,7 +40,7 @@ def pack_bundle(
38
40
  project_root: Root of the project containing ``apm.lock.yaml`` and ``apm.yml``.
39
41
  output_dir: Directory where the bundle will be created.
40
42
  fmt: Bundle format -- ``"apm"`` (default) or ``"plugin"``.
41
- target: Target filter -- ``"vscode"``, ``"claude"``, ``"all"``, or *None*
43
+ target: Target filter -- ``"copilot"``, ``"claude"``, ``"all"``, or *None*
42
44
  (auto-detect from apm.yml / project structure).
43
45
  archive: If *True*, produce a ``.tar.gz`` and remove the directory.
44
46
  dry_run: If *True*, resolve the file list but write nothing to disk.
@@ -114,7 +116,7 @@ def pack_bundle(
114
116
  for dep in lockfile.get_all_dependencies():
115
117
  all_deployed.extend(dep.deployed_files)
116
118
 
117
- filtered_files = _filter_files_by_target(all_deployed, effective_target)
119
+ filtered_files, path_mappings = _filter_files_by_target(all_deployed, effective_target)
118
120
  # Deduplicate while preserving order
119
121
  seen = set()
120
122
  unique_files: List[str] = []
@@ -133,14 +135,16 @@ def pack_bundle(
133
135
  raise ValueError(
134
136
  f"Refusing to pack unsafe path from lockfile: {rel_path!r}"
135
137
  )
136
- abs_path = project_root / rel_path
138
+ # For cross-target mapped files, verify the original (on-disk) path
139
+ disk_path = path_mappings.get(rel_path, rel_path)
140
+ abs_path = project_root / disk_path
137
141
  if not abs_path.resolve().is_relative_to(project_root_resolved):
138
142
  raise ValueError(
139
- f"Refusing to pack path that escapes project root: {rel_path!r}"
143
+ f"Refusing to pack path that escapes project root: {disk_path!r}"
140
144
  )
141
145
  # deployed_files may reference directories (ending with /)
142
146
  if not abs_path.exists():
143
- missing.append(rel_path)
147
+ missing.append(disk_path)
144
148
  if missing:
145
149
  raise ValueError(
146
150
  f"The following deployed files are missing on disk -- "
@@ -155,6 +159,8 @@ def pack_bundle(
155
159
  bundle_path=bundle_dir,
156
160
  files=unique_files,
157
161
  lockfile_enriched=True,
162
+ mapped_count=len(path_mappings),
163
+ path_mappings=path_mappings,
158
164
  )
159
165
 
160
166
  # 5b. Scan files for hidden characters before bundling.
@@ -168,7 +174,8 @@ def pack_bundle(
168
174
 
169
175
  _scan_findings_total = 0
170
176
  for rel_path in unique_files:
171
- src = project_root / rel_path
177
+ disk_path = path_mappings.get(rel_path, rel_path)
178
+ src = project_root / disk_path
172
179
  if src.is_symlink():
173
180
  continue
174
181
  if src.is_dir():
@@ -193,13 +200,21 @@ def pack_bundle(
193
200
  # 6. Build output directory
194
201
  bundle_dir = output_dir / f"{pkg_name}-{pkg_version}"
195
202
  bundle_dir.mkdir(parents=True, exist_ok=True)
203
+ bundle_dir_resolved = bundle_dir.resolve()
196
204
 
197
205
  # 7. Copy files preserving directory structure
198
206
  for rel_path in unique_files:
199
- src = project_root / rel_path
207
+ # For cross-target mapped files, read from the original disk path
208
+ disk_path = path_mappings.get(rel_path, rel_path)
209
+ src = project_root / disk_path
200
210
  if src.is_symlink():
201
211
  continue # Never bundle symlinks
202
212
  dest = bundle_dir / rel_path
213
+ # Defense-in-depth: verify mapped destination stays inside the bundle
214
+ if not dest.resolve().is_relative_to(bundle_dir_resolved):
215
+ raise ValueError(
216
+ f"Refusing to write outside bundle directory: {rel_path!r}"
217
+ )
203
218
  if src.is_dir():
204
219
  from ..security.gate import ignore_symlinks
205
220
  shutil.copytree(src, dest, dirs_exist_ok=True, ignore=ignore_symlinks)
@@ -215,6 +230,8 @@ def pack_bundle(
215
230
  bundle_path=bundle_dir,
216
231
  files=unique_files,
217
232
  lockfile_enriched=True,
233
+ mapped_count=len(path_mappings),
234
+ path_mappings=path_mappings,
218
235
  )
219
236
 
220
237
  # 10. Archive if requested
@@ -285,7 +285,8 @@ def _get_dev_dependency_urls(apm_yml_path: Path) -> Set[Tuple[str, str]]:
285
285
  ``github/awesome-copilot``).
286
286
  """
287
287
  try:
288
- data = yaml.safe_load(apm_yml_path.read_text(encoding="utf-8"))
288
+ from ..utils.yaml_io import load_yaml
289
+ data = load_yaml(apm_yml_path)
289
290
  except (yaml.YAMLError, OSError, ValueError):
290
291
  return set()
291
292
  if not isinstance(data, dict):
@@ -22,6 +22,7 @@ class UnpackResult:
22
22
  skipped_count: int = 0
23
23
  security_warnings: int = 0
24
24
  security_critical: int = 0
25
+ pack_meta: Dict = field(default_factory=dict)
25
26
 
26
27
 
27
28
  def unpack_bundle(
@@ -98,6 +99,18 @@ def unpack_bundle(
98
99
  legacy_lockfile_path = source_dir / LEGACY_LOCKFILE_NAME
99
100
  if legacy_lockfile_path.exists():
100
101
  lockfile_path = legacy_lockfile_path
102
+
103
+ # Extract pack: metadata (written by apm pack) before structured parse
104
+ pack_meta: Dict = {}
105
+ try:
106
+ import yaml
107
+ raw = yaml.safe_load(lockfile_path.read_text(encoding="utf-8"))
108
+ if isinstance(raw, dict):
109
+ val = raw.get("pack", {})
110
+ pack_meta = val if isinstance(val, dict) else {}
111
+ except Exception:
112
+ pass # non-critical -- proceed without metadata
113
+
101
114
  lockfile = LockFile.read(lockfile_path)
102
115
  if lockfile is None:
103
116
  if not lockfile_path.exists():
@@ -176,6 +189,7 @@ def unpack_bundle(
176
189
  dependency_files=dep_file_map,
177
190
  security_warnings=security_warnings,
178
191
  security_critical=security_critical,
192
+ pack_meta=pack_meta,
179
193
  )
180
194
 
181
195
  # 4. Copy target files to output_dir (additive, no deletes)
@@ -219,6 +233,7 @@ def unpack_bundle(
219
233
  skipped_count=skipped,
220
234
  security_warnings=security_warnings,
221
235
  security_critical=security_critical,
236
+ pack_meta=pack_meta,
222
237
  )
223
238
  finally:
224
239
  # Clean up temp dir if we created one
@@ -331,9 +331,8 @@ def _update_gitignore_for_apm_modules(logger=None):
331
331
  def _load_apm_config():
332
332
  """Load configuration from apm.yml."""
333
333
  if Path(APM_YML_FILENAME).exists():
334
- with open(APM_YML_FILENAME, "r") as f:
335
- yaml = _lazy_yaml()
336
- return yaml.safe_load(f)
334
+ from ..utils.yaml_io import load_yaml
335
+ return load_yaml(APM_YML_FILENAME)
337
336
  return None
338
337
 
339
338
 
@@ -440,8 +439,6 @@ def _create_minimal_apm_yml(config, plugin=False):
440
439
  config: dict with name, version, description, author keys.
441
440
  plugin: if True, include a devDependencies section.
442
441
  """
443
- yaml = _lazy_yaml()
444
-
445
442
  # Create minimal apm.yml structure
446
443
  apm_yml_data = {
447
444
  "name": config["name"],
@@ -457,5 +454,5 @@ def _create_minimal_apm_yml(config, plugin=False):
457
454
  apm_yml_data["scripts"] = {}
458
455
 
459
456
  # Write apm.yml
460
- with open(APM_YML_FILENAME, "w") as f:
461
- yaml.safe_dump(apm_yml_data, f, default_flow_style=False, sort_keys=False)
457
+ from ..utils.yaml_io import dump_yaml
458
+ dump_yaml(apm_yml_data, APM_YML_FILENAME)